第六章
详细设计阶段的类结构设计
详细设计阶段的一个十分重要的问题,就是进行类设计。类设计直接对应于实现设计,它的设计质量直接影响着软件的质量,所以这个阶段是十分重要的。
这就给我们提出了一个问题,类是如何确定的,如何合理的规划类,这就要给我们提出一些原则,或者是一些模式。
设计模式是有关中小尺度的对象和框架的设计。应用在实现架构模式定义的大尺度的连接解决方案中。也适合于任何局部的详细设计。设计模式也称之为微观架构模式。
第一节
类结构设计中的通用职责分配软件模式
GRASP模式(General Responsibility Assignment Software Patterns 通用职责分配软件模式)能够帮助我们理解基本的对象设计技术,并且用一种系统的、可推理的、可说明的方式来应用设计理论。
一、根据职责设计对象
职责:职责与一个对象的义务相关联,职责主要分为两种类型:
1
)了解型(knowing
)
了解私有的封装数据;
了解相关联的相关对象;
了解能够派生或者计算的事物。
2
)行为型(doing
)
自身执行一些行为,如建造一个对象或者进行一个计算;
在其它对象中进行初始化操作;
在其它对象中控制或者协调各项活动。
职责是对象设计过程中,被分配给对象的类的。
我们常常能从领域模型推理出了解型相关的职责,这是因为领域模型实际上展示了对象的属性和相互关联。
二、职责和交互图
在UML中,职责分配到何处(通过操作来实现)这样的问题,贯穿了交互图生成的整个过程。
例如:
所以,当交互图创建的时候,实际上已经为对象分配了职责,这体现到交互图就是发送消息到不同的对象。
三、在职责分配中的通用原则
总结一下:
l 巧妙的职责分配在对象设计中非常重要。
l 决定职责如何分配的行为常常在创建交互图的之后发生,当然也会贯穿于编程过程。
l 模式是已经命名的问题/解决方案组合,它把与职责分配有关的好的建议和原则汇编成文。
四、信息专家模式
解决方案:
将职责分配给拥有履行一个职责所必需信息的类,也就是信息专家。
问题:
在开始分配职责的时候,首先要清晰的陈述职责。
假定某个类需要知道一次销售的总额。
根据专家模式,我们应该寻找一个对象类,它具有计算总额所需要的信息。
关键:
使用概念模型(现实世界领域的概念类)还是设计模型(软件类)来分析所具有所需信息的类呢?
答:
1. 如果设计模型中存在相关的类,先在设计模型中查看。
2. 如果设计模相中不存在相关的类,则查看概念模型,试着应用或者扩展概念模型,得出相应的概念类。
我们下面来讨论一下这个例子。
假定有如下概念模型。
到底谁是信息专家呢?
如果我们需要确定销售总额。
可以看出来,一个Sale类的实例,将包括“销售线项目”和“产品规格说明”的全部信息。也就是说,Sale类是一个关于销售总额的合适的信息专家。
而SalesLineItem可以确定子销售额,这就是确定子销售额的信息专家。
进一步,ProductSpecification能确定价格等,它就是“产品规格说明”的信息专家。
上面已经提到,在创建交互图语境的时候,常常出现职责分配的问题。
设想我们正在绘设计模型图,并且在为对象分配职责,从软件的角度,我们关注一下:
为了得到总额信息,需要向Sale发出请求总额请求,于是Sale得到了getTotal方法。
而销售需要取得数量信息,就要向“销售线项目”发出请求,这就在SalesLineItem得到了getSubtotal方法。
而销售线项目需要向“产品规格说明”取得价格信息,这就在ProductSpecification类得到了getPrice方法。
这样的思考,我们就在概念模型的基础上,得到了设计模型。
注意:
职责的实现需要信息,而信息往往分布在不同的对象中,这就意味着需要许多“部分”的信息专家来协作完成一个任务。
信息专家模式于现实世界具有相似性,它往往导致这样的设计:软件对象完成它所代表的现实世界对象的机械操作。
但是,某些情况下专家模式所描述的解决方案并不合适,这主要会造成耦合性和内聚性的一些问题。后面我们会加以讨论。
五、创建者模式
解决方案:
如果符合下面一个或者多个条件,则可以把创建类A的职责分配给类B。
1,类B聚和类A的对象。
2,类B包含类A的对象。
3,类B记录类A的对象的实例。
4,类B密切使用类A的对象。
5,类B初始化数据并在创建类A的实例的时候传递给类A(因此,类B是创建类A实例的一个专家)。
如果符合多个条件,类B聚合或者包含类A 的条件优先。
问题:
谁应该负责产生类的实例?
创建对象是面向对象系统最普遍的活动之一,因此,拥有一个分配创建对象职责的通用原则是非常有用的。如果职责分配合理,设计就能降低耦合度,提高设计的清晰度、封装性和重用性。
讨论:
创建者模式指导怎样分配和创建对象(一个非常重要的任务)相关的职责。
通过下面的交互图,我们立刻就能发现Sale具备Payment创建者的职责。
创建者模式的一个基本目的,就是找到一个在任何情况下都与被创建对象相关联的创建者,选择这样的类作为创建者能支持低耦合。
限制:
创建过程经常非常复杂,在这种情况下,最好的办法是把创建委托给一个工厂,而不是使用创建者模式所建议的类。
六、低耦合模式
解决方案:
分配一个职责,是的保持低耦合度。
问题:
怎样支持低的依赖性,减少变更带来的影响,提高重用性?
耦合(coupling)是测量一个元素连接、了解或者依赖其它元素强弱的尺度。具有低耦合的的元素不过多的依赖其它的元素,“过多”这个词和元素所处的语境有关,需要进行考查。
元素包括类、子系统、系统等。
具有高耦合性地类过多的依赖其它的类,设计这种高耦合的类是不受欢迎的。因为它可能出现以下问题:
l 相关类的变化强制局部变化。
l 当元素分离出来的时候很难理解
l 因为使用高耦合类的时候需要它所依赖的类,所以很难重用。
示例:
我们来看一下订单处理子系统中的一个例子,有如下三个类。
Payment(付款)
Register(登记)
Sale(销售)
要求:创建一个Payment类的实例,并且与Sale相关联。
哪个类更适合完成这项工作呢?
创建者模式认为,Register记录了现实世界中的一次Payment,因此建议用Register作为创建者。
第一方案:
由Register构造一个Payment对象。
再由Register把构造的Payment实例通过addPayment消息发送给Sale对象。
第二方案:
由Register向Sale提供付款信息(通过makePayment消息),再由Sale创建Payment对象。
两种方案到底那种支持低的耦合度呢?
第一方案,Register构造一个Payment对象,增加了Register与Payment对象的耦合度。
第二方案,Payment对象是由Sale创建的,因此并没有增加Register与Payment对象的耦合度。
单纯从耦合度来考虑,第二种方案更优。
在实际工作中,耦合度往往和其它模式是矛盾的。但耦合性是提高设计质量必须考虑的一个因素。
讨论:
在确定设计方案的过程中,低耦合是一个应该时刻铭记于心的原则。它是一个应该时常考虑的设计目标,在设计者评估设计方案的时候,低耦合也是一个评估原则。
低耦合使类的设计更独立,减少类的变更带来的不良影响,但是,我们会时时发现低耦合的要求,是和其它面向对象的设计要求是矛盾的,这就不能把它看成唯一的原则,而是众多原则中的一个重要的原则。
比如继承性必然导致高的耦合性,但不用继承性,这就失去了面向对象设计最重要的特点。
没有绝对的尺度来衡量耦合度,关键是开发者能够估计出来,当前的耦合度会不会导致问题。事实上越是表面上简单而且一般化的类,往往具有强的可重用性和低的耦合度。
低耦合度的需要,导致了一个著名的设计原则,那就是优先使用组合而不是继承。但这样又会导致许多臃肿、复杂而且设计低劣的类的产生。
所以,一个优秀的设计师,关键是用一种深入理解和权衡利弊的态度来面对设计。
设计师的灵魂不是记住了多少原则,而是能灵活合理的使用这些原则,这就需要在大量的设计实践中总结经验,特别是在失败中总结教训,来形成自己的设计理念。
设计师水平的差距,正在于此。
七、高内聚模式
解决方案:
分配一个职责,使得保持高的内聚。
问题:
怎么样才能使得复杂性可以管理?
从对象设计的角度,内聚是一个元素的职责被关联和关注的强弱尺度。如果一个元素具有很多紧密相关的职责,而且只完成有限的功能,那这个元素就是高度内聚的。这些元素包括类、子系统等。
一个具有低内聚的类会执行许多互不相关的事物,或者完成太多的功能,这样的类是不可取的,因为它们会导致以下问题:
1,难于理解。
2,难于重用。
3,难于维护。
4,系统脆弱,常常受到变化带来的困扰。
低内聚类常常代表抽象化的“大粒度对象”,或者承担着本来可以委托给其它对象的职责。
示例:
我们还是来看一下刚刚讨论过的订单处理子系统的例子,有如下三个类。
Payment(付款)
Register(登记)
Sale(销售)
要求:创建一个Payment类的实例,并且与Sale相关联。
哪个类更适合完成这项工作呢?
创建者模式认为,Register记录了现实世界中的一次Payment,因此建议用Register作为创建者。
第一方案:
由Register构造一个Payment对象。
再由Register把构造的Payment实例通过addPayment消息发送给Sale对象。
第二方案:
由Register向Sale提供付款信息(通过makePayment消息),再由Sale创建Payment对象。
在第一个方案中,由于Register要执行多个任务,在任务很多的时候,就会显得十分臃肿,这种要执行多个任务的类,内聚是比较低的。
在第二种方案里面,由于创建Payment对象的任务,委托给了Sale,每个类的任务都比较简单而且单一,这就实现了高的内聚性。
从开发技巧的角度,至少有一个开发者要去考虑内聚所产生的影响。
一般来说,高的内聚往往导致低的耦合度。
讨论:
和低耦合性模式一样,高内聚模式在制定设计方案的过程中,一个应该时刻铭记于心的原则。
同样,它往往会和其它的设计原则相抵触,因此必须综合考虑。
Grady Booch是建模的大师级人物,它在描述高内聚的定义的时候是这样说的:“一个组件(比如类)的所有元素,共同协作提供一些良好受限的行为。”
根据经验,一个具有高内聚的类,具有数目相对较少的方法,和紧密相关的功能。它并不完成太多的工作,当需要实现的任务过大的时候,可以和其它的对象协作来分担过大的工作量。
一个类具有高内聚是非常有利的,因为它对于理解、维护和重用都相对比较容易。
限制:
少数情况下,接受低内聚是合理的。
比如,把SQL专家编写的语句综合在一个类里面,这就可以使程序设计专家不必要特别关注SQL语句该怎么写。
又比如,远程对象处理,利用很多细粒度的接口与客户联系,造成网络流量大幅度增加而降低性能,就不如把能力封装起来,做一个粗粒度的接口给客户,大部分工作在远程对象内部完成,减少远程调用的次数。
第二节
设计模式与软件架构
一、设计模式
在模块设计阶段,最关键的问题是,用户需求是变化的,我们的设计如何适应这种变化呢?
1,如果我们试图发现事情怎样变化,那我们将永远停留在分析阶段。
2,如果我们编写的软件能面向未来,那将永远处在设计阶段。
3,我们的时间和预算不允许我们面向未来设计软件。过分的分析和过分的设计,事实上被称之为“分析瘫痪”。
如果我们预料到变化将要发生,而且也预料到将会在哪里发生。这样就形成了几个原则:
1,针对接口编程而不是针对实现编程。
2,优先使用对象组合,而不是类的继承。
3,考虑您的设计哪些是可变的,注意,不是考虑什么会迫使您的设计改变,而是考虑要素变化的时候,不会引起重新设计。
也就是说,封装变化的概念是模块设计的主题。
解决这个问题,最著名的要数GoF的23种模式,在GoF中,把设计模式分为结构型、创建型和行为型三大类。
本课程假定学员已经熟悉这23个模式,因此主要从设计的角度讨论如何正确选用恰当的设计模式。
整个讨论依据三个原则:
1)开放-封闭原则
2)从场景进行设计的原则
3)包容变化的原则
下面的讨论会有一些代码例子,尽管在详细设计的时候,并不考虑代码实现的,但任何架构设计思想如果没有代码实现做基础,将成为无木之本,所以后面的几个例子我们还是把代码实现表示出来,举这些例子的目的并不是提供样板,而是希望更深入的描述想法。
另外,所用的例子大部分使用C#来编写,这主要因为希望表达比较简单,但这不是必要的,可以用任何面向对象的语言(Java、C++)来讨论这些问题。
二、封装变化与面向接口编程
设计模式分为结构型、构造型和行为型三种问题域,我们来看一下行为型设计模式,行为型设计模式的要点之一是“封装变化”,这类模式充分体现了面向对象的设计的抽象性。在这类模式中,“动作”或者叫“行为”,被抽象后封装为对象或者为方法接口。通过这种抽象,将使“动作”的对象和动作本身分开,从而达到降低耦合性的效果。这样一来,使行为对象可以容易的被维护,而且可以通过类的继承实现扩展。
行为型模式大多数涉及两种对象,即封装可变化特征的新对象,和使用这些新对象的已经有的对象。二者之间通过对象组合在一起工作。如果不使用这些模式,这些新对象的功能就会变成这些已有对象的难以分割的一部分。因此,大多数行为型模式具有如下结构。
下面是上述结构的代码片断:
public abstract class
行为类接口
{
public abstract void
行为();
}
public class
具体行为1:行为类接口
{
public override void
行为()
{
}
}
public class
行为使用者
{
public
行为类接口 我的行为;
public
行为使用者()
{
我的行为=new 具体行为1();
}
public void
执行()
{
我的行为.行为();
}
}
第三节
合理使用外观和适配器模式
一、使用外观模式(
Facade
)使用户和子系统绝缘
外观模式为了一组子系统提供一个一致的方式对外交互。这样就可以使客户和子系统绝缘,可以大大减少客户处理对象的数目,我们已经讨论过这个主题,这里就不再讨论了。
二、使用适配器模式(
Adapter
)调适接口
在系统之间集成的时候,最常见的问题是接口不一致,很多能满足功能的软件模块,由于接口不同,而导致无法使用。
适配器模式的含义在于,把一个类的接口转换为另一个接口,使原本不兼容而不能一起工作的类能够一起工作。适配器有类适配器和对象适配器两种类型,二者的意图相同,只是实现的方法和适用的情况不同。类适配器采用继承的方法来实现,而对象适配器采用组合的方法来实现。
1
)类适配器
类适配器采用多重继承对一个接口与另一个接口进行匹配,其结构如下。
由于Adapter类继承了Adaptee类,通过接口的Do()方法,我们可以使用Adaptee中的Execute方法。
这种方式当语言不承认多重继承的时候就没有办法实现,这时可以使用对象适配器。
2
)对象适配器
对象适配器采用对象组合,通过引用一个类与另一个类的接口,来实现对多个类的适配。
`
第四节
封装变化的三种方式及评价
设计可升级的架构,关键是要把模块中不变部分与预测可变部分分开,以防止升级过程中对基本代码的干扰。这种分开可以有多种方式,一般来说可以从纵向、横向以及外围三个方面考虑。
一、纵向处理:模板方法(
Template Method
)
1
、意图
定义一个操作中的算法骨架,而将一些步骤延伸到子类中去,使得子类可以不改变一个算法的结构,即可重新定义改算法的某些特定步骤。这里需要复用的使算法的结构,也就是步骤,而步骤的实现可以在子类中完成。
2
、使用场合
1)一次性实现一个算法的不变部分,并且将可变的行为留给子类来完成。
2)各子类公共的行为应该被提取出来并集中到一个公共父类中以避免代码的重复。首先识别现有代码的不同之处,并且把不同部分分离为新的操作,最后,用一个调用这些新的操作的模板方法来替换这些不同的代码。
3)控制子类的扩展。
3
、结构
模板方法的结构如下:
在抽象类中定义模板方法的关键是:
在一个非抽象方法中调用调用抽象方法,而这些抽象方法在子类中具体实现。
代码:
public abstract class Payment
{
private decimal amount;
public decimal Amount
{
get
{
return amount;
}
set
{
amount = value;
}
}
public virtual string goSale()
{
string x = "
不变的流程一 "
;
x += Action(); //
可变的流程
x += amount + ",
正在查询库存状态"
; //
属性和不变的流程二
return x;
}
public abstract string Action();
}
public class CashPayment : Payment
{
public override string Action()
{
return "
现金支付"
;
}
}
调用:
Payment
o;
private void button1_Click(object sender, EventArgs e)
{
o = new CashPayment();
if (textBox1.Text != "")
{
o.Amount = decimal.Parse(textBox1.Text);
}
textBox2.Text = o.goSale();
}
假定系统已经投运,用户提出新的需求,要求加上信用卡支付和支票支付,可以这样写:
public
class CreditPayment : Payment
{
public override string Action()
{
return "
信用卡支付,联系支付机构"
;
}
}
public class CheckPayment : Payment
{
public override string Action()
{
return "
支票支付,联系财务部门"
;
}
}
调用:
Payment
o;
//
现金方式
private void button1_Click(object sender, EventArgs e)
{
o = new CashPayment();
if (textBox1.Text != "")
{
o.Amount = decimal.Parse(textBox1.Text);
}
textBox2.Text = o.goSale();
}
//
信用卡方式
private void button2_Click(object sender, EventArgs e)
{
o = new CreditPayment();
if (textBox1.Text != "")
{
o.Amount = decimal.Parse(textBox1.Text);
}
textBox2.Text = o.goSale();
}
//
支票方式
private void button3_Click(object sender, EventArgs e)
{
o = new CheckPayment();
if (textBox1.Text != "")
{
o.Amount = decimal.Parse(textBox1.Text);
}
textBox2.Text = o.goSale();
}
二、横向处理:桥接模式(
Bridge
)
模板方法是利用继承来完成切割,当对耦合性要求比较高,无法使用继承的时候,可以横向切割,也就是使用桥接模式。
桥接模式结构如下图。
其中:
Abstraction:定义抽象类的接口,并维护Implementor接口的指针。
其内部有一个OperationImp实例方法,但使用Implementor接口的方法。
RefindAbstraction:扩充Abstraction类定义的接口,重要的是需要实例化Implementor接口。
Implementor:定义实现类的接口,关键是内部有一个defaultMethod方法,这个方法会被OperationImp实例方法使用。
ConcreteImplementor:实现Implementor接口,这是定义它的具体实现。
我们通过上面的关于支付的简单例子可以说明它的原理。
public
class Payment
{
private decimal amount;
public decimal Amount
{
get
{
return amount;
}
set
{
amount = value;
}
}
Implementor s;
public Implementor Imp
{
set
{
s = value;
}
}
public virtual string goSale()
{
string x = "
不变的流程一 "
;
x += s.Action(); //
可变的流程
x += amount + ",
正在查询库存状态"
; //
属性和不变的流程二
return x;
}
}
public interface Implementor
{
string Action();
}
public class CashPayment : Implementor
{
public string Action()
{
return "
现金支付"
;
}
}
调用:
Payment
o=new Payment();
private void button1_Click(object sender, EventArgs e)
{
o.Imp = new CashPayment();
if (textBox1.Text != "")
{
o.Amount = decimal.Parse(textBox1.Text);
}
textBox2.Text = o.goSale();
}
假定系统投运以后,需要修改性能,可以直接加入新的类:
public
class CreditPayment : Implementor
{
public string Action()
{
return "
信用卡支付,联系机构"
;
}
}
public class CheckPayment : Implementor
{
public string Action()
{
return "
支票支付,联系财务部"
;
}
}
调用:
Payment
o=new Payment();
private void button1_Click(object sender, EventArgs e)
{
o.Imp = new CashPayment();
if (textBox1.Text != "")
{
o.Amount = decimal.Parse(textBox1.Text);
}
textBox2.Text = o.goSale();
}
private void button2_Click(object sender, EventArgs e)
{
o.Imp = new CreditPayment();
if (textBox1.Text != "")
{
o.Amount = decimal.Parse(textBox1.Text);
}
textBox2.Text = o.goSale();
}
private void button3_Click(object sender, EventArgs e)
{
o.Imp = new CheckPayment();
if (textBox1.Text != "")
{
o.Amount = decimal.Parse(textBox1.Text);
}
textBox2.Text = o.goSale();
}
这样就减少了系统的耦合性。而在系统升级的时候,并不需要改变原来的代码。
三、核心和外围:装饰器模式(
Decorator
)
有的时候,希望实现一个基本的核心代码快,由外围代码实现专用性能的包装,最简单的方法,是使用继承。
public
abstract class Payment
{
private decimal amount;
public decimal Amount
{
get
{
return amount;
}
set
{
amount = value;
}
}
public virtual string goSale()
{
return Action() + "
完成,金额为"
+ amount + ",
正在查询库存状态"
;
}
public abstract string Action();
}
public class CashPayment : Payment
{
public override string Action()
{
return "
现金支付"
;
}
}
实现:
Payment
o;
private
void button1_Click(object sender, EventArgs e)
{
o = new CashPayment1();
o.Amount = decimal.Parse(textBox1.Text);
textBox2.Text = o.goSale();
}
加入继承:
public
class CashPayment1 : CashPayment
{
public override string Action()
{
//
在执行原来的代码之前,弹出提示框
System.Windows.Forms.MessageBox.Show("
现金支付"
);
return base.Action();
}
}
实现:
Payment
o;
private void button1_Click(object sender, EventArgs e)
{
o = new CashPayment1();
o.Amount = decimal.Parse(textBox1.Text);
textBox2.Text = o.goSale();
}
缺点:
继承层次多一层,提升了耦合性。
当实现类比较多的时候,实现起来就比较复杂。
在这样的情况下,也可以使用装饰器模式,这是用组合取代继承的一个很好的方式。
1
、意图
事实上,上面所要解决的意图可以归结为“在不改变对象的前提下,动态增加它的功能”,也就是说,我们不希望改变原有的类,或者采用创建子类的方式来增加功能,在这种情况下,可以采用装饰模式。
2
、结构
装饰器结构的一个重要的特点是,它继承于一个抽象类,但它又使用这个抽象类的聚合(即即装饰类对象可以包含抽象类对象),恰当的设计,可以达到我们提出来的目的。
模式中的参与者如下:
Component (组成):定义一个对象接口,可以动态添加这些对象的功能,其中包括Operation(业务)方法。
ConcreteComponent(具体组成):定义一个对象,可以为它添加一些功能。
Decorator(装饰):维持一个对Component对象的引用,并定义与Component接口一致的接口。
ConcreteDecorator(具体装饰):为组件添加功能。它可能包括AddedBehavior(更多的行为)和AddedState(更多的状态)。
举个例子:
假定我们已经构造了一个基于支付的简单工厂模式的系统。
using
System;
using
System.Collections.Generic;
using
System.Text;
namespace
PaymentDemo
{
public abstract class Payment
{
private decimal amount;
public decimal Amount
{
get
{
return amount;
}
set
{
amount = value;
}
}
public string goSale()
{
return Action() + "
完成,金额为"
+ amount + ",
正在查询库存状态"
;
}
public abstract string Action();
}
public class CashPayment : Payment
{
public override string Action()
{
return "
现金支付"
;
}
}
public class CreditPayment : Payment
{
public override string Action()
{
return "
信用卡支付"
;
}
}
public class CheckPayment : Payment
{
public override string Action()
{
return "
支票支付"
;
}
}
}
工厂类,注意这是独立的模块。
using
System;
using
System.Collections.Generic;
using
System.Text;
namespace
PaymentDemo
{
//
这是一个工厂类
public class Factory
{
public static Payment PaymentFactory(string PaymentName)
{
Payment mdb=null;
switch (PaymentName)
{
case "
现金"
:
mdb=new CashPayment();
break;
case "
信用卡"
:
mdb=new CreditPayment();
break;
case "
支票"
:
mdb=new CheckPayment();
break;
}
return mdb;
}
}
}
调用:
Payment obj;
private void button1_Click(object sender, EventArgs e)
{
obj = Factory.PaymentFactory("
现金"
);
obj.Amount = decimal.Parse(textBox1.Text);
textBox2.Text = obj.goSale();
}
private void button2_Click(object sender, EventArgs e)
{
obj = Factory.PaymentFactory("
信用卡"
);
obj.Amount = decimal.Parse(textBox1.Text);
textBox2.Text = obj.goSale();
}
private void button3_Click(object sender, EventArgs e)
{
obj = Factory.PaymentFactory("
支票"
);
obj.Amount = decimal.Parse(textBox1.Text);
textBox2.Text = obj.goSale();
}
现在需要每个类在调用方法goSale()的时候,除了完成原来的功能以外,先弹出一个对话框,显示工厂的名称,而且不需要改变来的系统,为此,在工厂类的模块种添加一个装饰类
Decorator,同时略微的改写一下工厂类的代码。
//
这是一个工厂类
public class Factory
{
public static Payment PaymentFactory(string PaymentName)
{
Payment mdb=null;
switch (PaymentName)
{
case "
现金"
:
mdb=new CashPayment();
break;
case "
信用卡"
:
mdb=new CreditPayment();
break;
case "
支票"
:
mdb=new CheckPayment();
break;
}
//return mdb;
//
下面是实现装饰模式新加的代码
Decorator obj = new Decorator(PaymentName);
obj.Pm = mdb;
return obj;
}
}
//
装饰类
public class Decorator : Payment
{
string strName;
public Decorator(string strName)
{
this.strName = strName;
}
Payment pm;
public Payment Pm
{
get
{
return pm;
}
set
{
pm = value;
}
}
public override string Action()
{
//
在执行原来的代码之前,弹出提示框
System.Windows.Forms.MessageBox.Show(strName);
return pm.Action();
}
}
这就可以在用户不知晓的情况下,也不更改原来的类的情况下,改变了性能。
第五节
利用策略与工厂模式实现通用的框架
一、应用策略模式提升层的通用性
1、意图
将算法封装,使系统可以更换或扩展算法,策略模式的关键是所有子类的目标一致。
2、结构
策略模式的结构如下。
其中:Strategy(策略):抽象类,定义需要支持的算法接口,策略由上下文接口调用。
3、示例:
目标:在C#下构造通用的框架,就需要使用XML配置文件技术。
构造一个一个类容器框架,可以动态装入和构造对象,装入类可以使用配置文件,这里利用了反射技术。
问题:
如何动态构造对象,集中管理对象。
解决方案:
策略模式,XML文档读入,反射的应用。
我们现在要处理的架构如下:
App.xml文档的结构如下。
<configuration>
<description>说明
</description>
<class id="标记
" type="类名,dll文件名">
<property name="属性名
">
<value>属性值
</value>
</property>
………
</class>
</configuration>
应用程序上下文接口:ApplicationContext
只有一个方法,也就是由用户提供的id提供类的实例。
代码:
using
System;
using
System.Collections.Generic;
using
System.Text;
using
System.Windows.Forms;
using
System.Collections;
using
System.Xml;
using
System.Reflection;
namespace
处理类
{
public interface ApplicationContext
{
Object getClass(string id);
}
public class FileSystemXmlApplicationContext : ApplicationContext
{
//
用一个哈西表保留从XML读来的数据
private Hashtable hs = new Hashtable();
public FileSystemXmlApplicationContext(string fileName)
{
try
{
readXml(fileName);
}
catch (Exception e)
{
MessageBox.Show(e.ToString());
}
}
//
私有的读XML方法。
private void readXml(String fileName)
{
//
读XML把数据放入哈西表
hs = Configuration.Attribute(fileName, "class", "property");
}
public Object getClass(string id)
{
//
由id取出内部的哈西表对象
Hashtable hsb = (Hashtable)hs[id];
Object obj = null;
string m = hsb["type"].ToString();
int i = m.IndexOf(',');
string classname = m.Substring(0, i);
string filename = m.Substring(i + 1) + ".dll";
//
利用反射动态构造对象
//
定义一个公共语言运行库应用程序构造块
System.Reflection.Assembly MyDll;
Type[] types;
MyDll = Assembly.LoadFrom(filename);
types = MyDll.GetTypes();
obj = MyDll.CreateInstance(classname);
Type t = obj.GetType();
IEnumerator em = hsb.GetEnumerator();
//
指针放在第一个之前
em.Reset();
while (em.MoveNext())
{
DictionaryEntry s1 = (DictionaryEntry)em.Current;
if (s1.Key.ToString() != "type")
{
string pname = "set_" + s1.Key.ToString();
t.InvokeMember(pname, BindingFlags.InvokeMethod, null, obj, new object[] { s1.Value });
}
}
return obj;
}
}
//
这是一个专门用于读配置文件的类
class Configuration
{
public static Hashtable Attribute(String configname,
String mostlyelem,
String childmostlyelem)
{
Hashtable hs = new Hashtable();
XmlDocument doc = new XmlDocument();
doc.Load(configname);
//
建立所有元素的列表
XmlElement root = doc.DocumentElement;
//
把所有的主要标记都找出来放到节点列表中
XmlNodeList elemList = root.GetElementsByTagName(mostlyelem);
for (int i = 0; i < elemList.Count; i++)
{
//
获取这个节点的属性集合
XmlAttributeCollection ac = elemList[i].Attributes;
//
构造一个表,记录属性和类的名字
Hashtable hs1 = new Hashtable();
hs1.Add("type", ac["type"].Value);
//
获取二级标记子节点
XmlNodeList elemList1 = ((XmlElement)elemList[i]).GetElementsByTagName(childmostlyelem);
for (int j = 0; j < elemList1.Count; j++)
{
//
获取这个节点的属性集合
XmlAttributeCollection ac1 = elemList1[j].Attributes;
string key = ac1["name"].Value;
XmlNodeList e1 = ((XmlElement)elemList1[j]).GetElementsByTagName("value");
string value = e1[0].InnerText;
hs1.Add(key, value);
}
hs.Add(ac["id"].Value, hs1);
}
return hs;
}
}
}
做一个抽象类类库:AbstractPayment.dll
using
System;
using
System.Collections.Generic;
using
System.Text;
namespace
AbstractPayment
{
public abstract class Payment
{
private decimal amount;
public string Amount
{
get
{
return amount.ToString();
}
set
{
amount = decimal.Parse(value);
}
}
public virtual string goSale()
{
return Action() + "
完成,金额为"
+ amount + ",
正在查询库存状态"
;
}
public abstract string Action();
}
}
实现类:
Cash.dll
using
System;
using
System.Collections.Generic;
using
System.Text;
namespace
Cash
{
public class CashPayment : AbstractPayment.Payment
{
public override string Action()
{
return "
现金支付"
;
}
}
}
界面:
添加引用 :AbstractPayment.dll
配置文件:App.xml
<configuration>
<description></description>
<class id="A" type="Cash.CashPayment,d:/Cash">
<property name="Amount">
<value>124</value>
</property>
</class>
</configuration>
使用:
ApplicationContext
s;
private
void Form1_Load(object sender, EventArgs e)
{
s = new FileSystemXmlApplicationContext("d://App.xml");
}
private
void button1_Click(object sender, EventArgs e)
{
AbstractPayment.Payment m = (AbstractPayment.Payment)s.getClass("A");
if (textBox1.Text != "")
{
m.Amount = textBox1.Text;
}
textBox2.Text = m.goSale();
}
如果需要添加两个新的实现类:
CreditCheck.dll
using
System;
using
System.Collections.Generic;
using
System.Text;
namespace
CreditCheck
{
public class CreditPayment : AbstractPayment.Payment
{
public override string Action()
{
return "
信用卡支付"
;
}
}
public class CheckPayment : AbstractPayment.Payment
{
public override string Action()
{
return "
支票支付"
;
}
}
}
把这个类放在当前D盘根目录下:
配置文件:
<configuration>
<description></description>
<class id="A" type="Cash.CashPayment,d:/Cash">
<property name="Amount">
<value>124</value>
</property>
</class>
<class id="B" type="CreditCheck.CreditPayment,d:/CreditCheck">
<property name="Amount">
<value>52000</value>
</property>
</class>
<class id="C" type="CreditCheck.CheckPayment,d:/CreditCheck">
<property name="Amount">
<value>63000</value>
</property>
</class>
</configuration>
实现:
ApplicationContext
s;
private void Form1_Load(object sender, EventArgs e)
{
s = new FileSystemXmlApplicationContext("d://App.xml");
}
private void button1_Click(object sender, EventArgs e)
{
AbstractPayment.Payment m = (AbstractPayment.Payment)s.getClass("A");
if (textBox1.Text != "")
{
m.Amount = textBox1.Text;
}
textBox2.Text = m.goSale();
}
private void button2_Click(object sender, EventArgs e)
{
AbstractPayment.Payment m = (AbstractPayment.Payment)s.getClass("B");
if (textBox1.Text != "")
{
m.Amount = textBox1.Text;
}
textBox2.Text = m.goSale();
}
private void button3_Click(object sender, EventArgs e)
{
AbstractPayment.Payment m = (AbstractPayment.Payment)s.getClass("C");
if (textBox1.Text != "")
{
m.Amount = textBox1.Text;
}
textBox2.Text = m.goSale();
}
-----------------------------------------------------------------------------------------
同样的原理,Java实现的Bean容器框架
Bean.xml文档的结构如下。
<beans>
<description>说明</description>
<bean id="标记"
class="类名">
<property name="属性名">
<value>内容</value>
</property>
………..
</bean>
</beans>
应用程序上下文接口:ApplicationContext.java
只有一个方法,也就是由用户提供的id提供Bean的实例。
package springdemo;
public interface ApplicationContext
{
public Object getBean(String id) throws Exception;
}
上下文实现类:FileSystemXmlApplicationContext.java
package springdemo;
import java.util.*;
import javax.xml.parsers.*;
import org.w3c.dom.*;
import java.io.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.*;
public class FileSystemXmlApplicationContext
implements ApplicationContext
{
//用一个哈西表保留从XML读来的数据
private Hashtable hs=new Hashtable();
public FileSystemXmlApplicationContext()
{}
public FileSystemXmlApplicationContext(String fileName)
{
try
{
readXml(fileName);
}
catch(Exception e)
{
e.printStackTrace();
}
}
//私有的读XML方法。
private void readXml(String fileName) throws Exception
{
//读XML把数据放入哈西表
hs=Configuration.Attribute(fileName,"bean","property");
}
public Object getBean(String id) throws Exception
{
//由id取出内部的哈西表对象
Hashtable hsb=(Hashtable)hs.get(id);
//利用反射动态构造对象
Object obj =Class.forName(hsb.get("class").toString()).newInstance();
java.util.Enumeration hsNames1 =hsb.keys();
//利用反射写入属性的值
while (hsNames1.hasMoreElements())
{
//写入利用Set方法
String ka=(String)hsNames1.nextElement();
if (! ka.equals("class"))
{
//写入属性值为字符串
String m1="String";
Class[] a1={m1.getClass()};
//拼接方法的名字
String sa1=ka.substring(0,1).toUpperCase();
sa1="set"+sa1+ka.substring(1);
//动态调用方法
java.lang.reflect.Method fm=obj.getClass().getMethod(sa1,a1);
Object[] a2={hsb.get(ka)};
//通过set 方法写入属性
fm.invoke(obj,a2);
}
}
return obj;
}
}
//这是一个专门用于读配置文件的类
class Configuration
{
public static Hashtable Attribute(String configname,
String mostlyelem,
String childmostlyelem) throws Exception
{
Hashtable hs=new Hashtable();
//建立文档,需要一个工厂
DocumentBuilderFactory factory=DocumentBuilderFactory.newInstance();
DocumentBuilder builder=factory.newDocumentBuilder();
Document doc=builder.parse(configname);
//建立所有元素的列表
Element root = doc.getDocumentElement();
//把所有的主要标记都找出来放到节点列表中
NodeList elemList = root.getElementsByTagName(mostlyelem);
for (int i=0; i < elemList.getLength(); i++)
{
//获取这个节点的属性集合
NamedNodeMap ac = elemList.item(i).getAttributes();
//构造一个表,记录属性和类的名字
Hashtable hs1=new Hashtable();
hs1.put("class",ac.getNamedItem("class").getNodeValue());
//获取二级标记子节点
Element node=(Element)elemList.item(i);
//获取第二级节点的集合
NodeList elemList1 =node.getElementsByTagName(childmostlyelem);
for (int j=0; j < elemList1.getLength(); j++)
{
//获取这个节点的属性集合
NamedNodeMap ac1 = elemList1.item(j).getAttributes();
String key=ac1.getNamedItem("name").getNodeValue();
NodeList node1=((Element)elemList1.item(j)).getElementsByTagName("value");
String value=node1.item(0).getFirstChild().getNodeValue();
hs1.put(key,value);
}
hs.put(ac.getNamedItem("id").getNodeValue(),hs1);
}
return hs;
}
}
做一个程序实验一下。
首先做一个关于交通工具的接口:Vehicle.java
package springdemo;
public interface Vehicle
{
public String execute(String str);
public String getMessage();
public void setMessage(String str);
}
做一个实现类:Car.java
package springdemo;
public class Car implements Vehicle
{
private String message="";
private String x;
public String getMessage()
{
return message;
}
public void setMessage(String str)
{
message = str;
}
public String execute(String str)
{
return getMessage() + str+"汽车在公路上开";
}
}
Bean.xml文档。
<beans>
<description>Spring Quick Start</description>
<bean id="Car"
class="springdemo.Car">
<property name="message">
<value>hello!</value>
</property>
</bean>
</beans>
测试:Test.java
public class Test
{
public static void main (String[] args) throws Exception
{
springdemo.ApplicationContext m=
new springdemo.FileSystemXmlApplicationContext("Bean.xml");
//实现类,使用标记Car
springdemo.Vehicle s1=(springdemo.Vehicle)m.getBean("Car");
System.out.println(s1.execute("我的"));
}
}
基于接口编程将使系统具备很好的扩充性。
再做一个类:Train.java
package springdemo;
public class Train implements Vehicle
{
private String message="";
public String getMessage()
{
return message;
}
public void setMessage(String str)
{
message = str;
}
public String execute(String str)
{
return getMessage() + str+"火车在铁路上走";
}
}
Bean.xml改动如下。
<beans>
<description>Spring Quick Start</description>
<bean id="Car"
class="springdemo.Car">
<property name="message">
<value>hello!</value>
</property>
</bean>
<bean id="Train"
class="springdemo.Train">
<property name="message">
<value>haha!</value>
</property>
</bean>
</beans>
改动一下Test.java
public class Test
{
public static void main (String[] args) throws Exception
{
springdemo.ApplicationContext m=
new springdemo.FileSystemXmlApplicationContext("Bean.xml");
springdemo.Vehicle s1=(springdemo.Vehicle)m.getBean("Car");
System.out.println(s1.execute("我的"));
s1=(springdemo.Vehicle)m.getBean("Train");
System.out.println(s1.execute("新的"));
}
}
我们发现,在加入新的类的时候,使用方法几乎不变。
再做一组不同的接口和类来看一看:Idemo.java
package springdemo;
public interface IDemo
{
public void setX(String x);
public void setY(String y);
public double Sum();
}
实现类:Demo.java
package springdemo;
public class Demo implements IDemo
{
private String x;
private String y;
public void setX(String x)
{
this.x = x;
}
public void setY(String y)
{
this.y = y;
}
public double Sum()
{
return Double.parseDouble(x)+Double.parseDouble(y);
}
}
Bean.xml改动如下:
<beans>
<description>Spring Quick Start</description>
<bean id="Car"
class="springdemo.Car">
<property name="message">
<value>hello!</value>
</property>
</bean>
<bean id="Train"
class="springdemo.Train">
<property name="message">
<value>haha!</value>
</property>
</bean>
<bean id="demo" class="springdemo.Demo">
<property name="x">
<value>20</value>
</property>
<property name="y">
<value>30</value>
</property>
</bean>
</beans>
改写Test.java
public class Test
{
public static void main (String[] args) throws Exception
{
springdemo.ApplicationContext m=
new springdemo.FileSystemXmlApplicationContext("Bean.xml");
springdemo.Vehicle s1=(springdemo.Vehicle)m.getBean("Car");
System.out.println(s1.execute("我的"));
s1=(springdemo.Vehicle)m.getBean("Train");
System.out.println(s1.execute("新的"));
springdemo.IDemo s2=(springdemo.IDemo)m.getBean("demo");
System.out.println(s2.Sum());
}
}
------------------------------------------------------------------------------------------------------
二、创建者工厂的合理应用
实例:用.NET数据提供者作为例子,讨论实现通用数据访问工厂的方法。
目的:
利用工厂实现与数据库无关的数据访问框架。
问题:
由于数据提供者是针对不同类型数据库的,造成升级和维护相当困难。现在需要把与数据库相关的部分独立出来,使应用程序不因为数据库不同而有所变化。
解决方案:
配置文件读取,不同提供者的集中处理,后期的可扩从性。
.NET提供了一组连接对象。
Connection对象用于连接数据库,它被称之为提供者。
连接类对象的关系:
在.NET 2003,它的继承关系是这样的。
当数据提供者要求非常灵活的时候,尤其是对于领域中的层设计,需要考虑更多的问题。
下面的例子,希望用一个配置文件来决定提供者的参数,而实现代码不希望由于数据库的变化而变化,而且当系统升级的时候,不应该有很大的困难。
配置文档:App.xml
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<connectionStrings>
<add name="PubsData" providerName="System.Data.SqlClient"
connectionString="data source=zktb; initial catalog=奖金数据库;persist security info=false; workstation id= COMMONOR-02A84C; packet size=4096;UID=sa;PWD=;Max Pool Size=50;"/>
<add name="Nothing" providerName="System.Data.OleDb"
connectionString="Provider=Microsoft.Jet.OLEDB.4.0;Data Source=c://奖金数据库.mdb"/>
<add name="Bank" providerName="System.Data.SqlClient"
connectionString="data source=zktb; initial catalog=Bank;persist security info=false; workstation id= COMMONOR-02A84C; packet size=4096;UID=sa;PWD=;Max Pool Size=50;"/>
</connectionStrings>
<appSettings>
<add key="provider" value="SqlClient"/>
</appSettings>
</configuration>
类代码:
using System.Data;
using System.Data.Common;
using System.Data.OleDb;
using System.Data.SqlClient;
using System.Data.OracleClient;
namespace DbComm
{
//
工厂接口
public interface IDbFactory
{
string connectionString
{
get;
set;
}
IDbConnection CreateConnection();
IDbCommand CreateCommand();
DbDataAdapter CreateDataAdapter(IDbCommand comm);
}
//
工厂类,工厂方法模式
public class DbFactories
{
public static IDbFactory GetFactory(string name)
{
string providerName=null;
string connectionString=null;
ArrayList al=configuration.Attribute("c://App.xml","connectionStrings","add");
for (int i=0;i<al.Count;i++)
{
Hashtable hs=(Hashtable)al[i];
if (hs["name"].ToString().Equals(name))
{
providerName=hs["providerName"].ToString();
connectionString=hs["connectionString"].ToString();
break;
}
}
IDbFactory da=null;
switch (providerName)
{
case "System.Data.SqlClient":
da=new CSqlServer();
break;
case "System.Data.OleDb":
da=new COleDb();
break;
case "System.Data.Oracle":
da=new COracle();
break;
}
da.connectionString=connectionString;
return da;
}
}
//
这是一个专门用于读配置文件的类
public class configuration
{
public static ArrayList Attribute(string configname,
string mostlyelem,
string childmostlyelem)
{
ArrayList al=new ArrayList();
XmlDocument doc = new XmlDocument();
doc.Load(configname);
//
建立所有元素的列表
XmlElement root = doc.DocumentElement;
//
把所有的主要标记都找出来放到节点列表中
XmlNodeList elemList = root.GetElementsByTagName(mostlyelem);
for (int i=0; i < elemList.Count; i++)
{
//
获取二级标记子节点
XmlNodeList elemList1 = ((XmlElement)elemList[i]).GetElementsByTagName(childmostlyelem);
//
获取这个节点的数目
int N=elemList1.Count;
for (int j=0; j < elemList1.Count; j++)
{
Hashtable hs=new Hashtable();
//
获取这个节点的属性集合
XmlAttributeCollection ac = elemList1[j].Attributes;
for( int k = 0; k < ac.Count; k++ )
{
hs.Add(ac[k].Name,ac[k].Value);
}
al.Add(hs);
}
}
return al;
}
}
//SqlServer
实现类
public class CSqlServer:IDbFactory
{
private string strConn;
public string connectionString
{
get
{
return this.strConn;
}
set
{
this.strConn=value;
}
}
public IDbConnection CreateConnection()
{
SqlConnection conn=new SqlConnection(strConn);
return conn;
}
public IDbCommand CreateCommand()
{
return new SqlCommand();
}
public DbDataAdapter CreateDataAdapter(IDbCommand comm)
{
SqlDataAdapter adp=new SqlDataAdapter();
adp.SelectCommand=(SqlCommand)comm;
SqlCommandBuilder cb=new SqlCommandBuilder(adp);
return adp;
}
}
//OleDb
实现类
public class COleDb:IDbFactory
{
private string strConn;
public string connectionString
{
get
{
return this.strConn;
}
set
{
this.strConn=value;
}
}
public IDbConnection CreateConnection()
{
OleDbConnection conn=new OleDbConnection(strConn);
return conn;
}
public IDbCommand CreateCommand()
{
return new OleDbCommand();
}
public DbDataAdapter CreateDataAdapter(IDbCommand comm)
{
OleDbDataAdapter adp=new OleDbDataAdapter();
adp.SelectCommand=(OleDbCommand)comm;
OleDbCommandBuilder cb=new OleDbCommandBuilder(adp);
return adp;
}
}
//Oracle
实现类
public class COracle:IDbFactory
{
private string strConn;
public string connectionString
{
get
{
return this.strConn;
}
set
{
this.strConn=value;
}
}
public IDbConnection CreateConnection()
{
OracleConnection conn=new OracleConnection(strConn);
return conn;
}
public IDbCommand CreateCommand()
{
return new OracleCommand();
}
public DbDataAdapter CreateDataAdapter(IDbCommand comm)
{
OracleDataAdapter adp=new OracleDataAdapter();
adp.SelectCommand=(OracleCommand)comm;
OracleCommandBuilder cb=new OracleCommandBuilder(adp);
return adp;
}
}
}
应用:
System.Data.Common.DbDataAdapter adp;
DataTable dt=new DataTable();
//
处理数据
DataTable callData(string name,string query)
{
DataTable dt=new DataTable();
try
{
DbComm.IDbFactory df=DbComm.DbFactories.GetFactory(name);
IDbConnection conn=df.CreateConnection();
IDbCommand comm=df.CreateCommand();
comm.CommandText=query;
comm.Connection=conn;
adp=df.CreateDataAdapter(comm);
adp.Fill(dt);
}
catch
{
MessageBox.Show("
错误,可能配置文件不对");
}
return dt;
}
//
调SqlServer
private void button1_Click(object sender, System.EventArgs e)
{
dt=callData("PubsData","select * from
奖金");
dataGrid1.DataSource=dt;
}
//
调OleDb
private void button2_Click(object sender, System.EventArgs e)
{
dt=callData("Nothing","select * from
奖金");
dataGrid1.DataSource=dt;
}
//
提交
private void button3_Click(object sender, System.EventArgs e)
{
adp.Update(dt);
}
.NET 2005为了扩充功能,在接口和实现类之间又加了一个抽象类(
DbConnection等)。
为了更好的实现上面类似的性能,提供了一套基于工厂的对象:
System.Data.Common.DbProviderFactory
System.Data.Common.DbConnectionStringBuilder
System.Data.Common.DbProviderFactories
等一系列的类,我们看一下在
.NET 2005上实现的几个例子,有了上面的基础,这几个类的理解当不会感到困难。
配置文档 App.config和上面的例子几乎是相同的。
等一系列的类,我们看一下在
.NET 2005上实现的几个例子,有了上面的基础,这几个类的理解当不会感到困难。
配置文档 App.config
和上面的例子是一模一样的。
<?
xml
version
=
"1.0"encoding="utf-8" ?>
<
configuration
>
<
connectionStrings
>
<
clear
/>
<
add
name
=
"PubsData"providerName="System.Data.SqlClient"
connectionString
=
"data source=COMMONOR-02A84C; initial catalog=
奖金数据库;persist security info=false; workstation id= COMMONOR-02A84C; packet size=4096;UID=sa;PWD=;Max Pool Size=50;
"/>
<
add
name
=
"Nothing"providerName="System.Data.OleDb"
connectionString
=
"Provider=Microsoft.Jet.OLEDB.4.0;Data Source=c://
虚拟公司.mdb
"/>
<
add
name
=
"Bank"providerName="System.Data.SqlClient"
connectionString
=
"data source=COMMONOR-02A84C; initial catalog=Bank;persist security info=false; workstation id= COMMONOR-02A84C; packet size=4096;UID=sa;PWD=;Max Pool Size=50;"/>
</
connectionStrings
>
<
appSettings
>
<
add
key
=
"provider"value="SqlClient"/>
</
appSettings
>
</
configuration
>
实现代码:
private void button4_Click(object sender, EventArgs e)
{
DataTable dt = null;
//
返回一个 DataTable,
//
其中包含有关实现 DbProviderFactory 的所有已安装提供程序的信
dt = System.Data.Common.DbProviderFactories.GetFactoryClasses();
dataGridView1.DataSource = dt;
}
//
表示一组方法,这些方法用于创建提供程序对数据源类的实现的实例。
//
命名空间:System.Data.Common
//
程序集:System.Data(在 system.data.dll 中)
System.Data.Common.DbProviderFactory factory = System.Data.SqlClient.SqlClientFactory.Instance;
public System.Data.Common.DbConnection GetProConn()
{
System.Data.Common.DbConnection conn = factory.CreateConnection();
System.Configuration.ConnectionStringSettings
publ = System.Configuration.ConfigurationManager.ConnectionStrings["PubsData"];
conn.ConnectionString = publ.ConnectionString;
return conn;
}
private void button5_Click(object sender, EventArgs e)
{
System.Data.Common.DbCommand comm = factory.CreateCommand();
comm.Connection = GetProConn();
comm.CommandText = "select * from
奖金"
;
comm.Connection.Open();
DataTable dt = new DataTable();
dt.Load(comm.ExecuteReader());
comm.Connection.Close();
dataGridView1.DataSource = dt;
}
//
与提供者无关的数据访问
private void SeeData(string protext, string tablename)
{
System.Configuration.ConnectionStringSettings
publ = System.Configuration.ConfigurationManager.ConnectionStrings[protext];
System.Data.Common.DbProviderFactory factory =
System.Data.Common.DbProviderFactories.GetFactory(publ.ProviderName);
System.Data.Common.DbConnectionStringBuilder bld =
factory.CreateConnectionStringBuilder();
bld.ConnectionString = publ.ConnectionString;
System.Data.Common.DbConnection cn = factory.CreateConnection();
cn.ConnectionString = bld.ConnectionString;
System.Data.Common.DbDataAdapter da = factory.CreateDataAdapter();
System.Data.Common.DbCommand cmd = factory.CreateCommand();
cmd.CommandText = "select * from " + tablename;
cmd.CommandType = CommandType.Text;
cmd.Connection = cn;
da.SelectCommand = cmd;
System.Data.Common.DbCommandBuilder cb = factory.CreateCommandBuilder();
cb.DataAdapter = da;
DataSet ds = new DataSet();
da.Fill(ds, "auth");
dataGridView1.DataSource = ds;
dataGridView1.DataMember = "auth";
}
private void button6_Click(object sender, EventArgs e)
{
SeeData("PubsData", "
奖金"
);
}
private void button7_Click(object sender, EventArgs e)
{
SeeData("Nothing", "Orders");
}
利用上面的讨论过的原理,还可以把代码进一步封装,形成一个基于领域的层,再给予接口编程的原则下,系统的升级性能是非常好的。
三、单件模式的应用问题
有时候,我们需要一个全局唯一的连接对象,这个对象可以管理多个通信会话,在使用这个对象的时候,不关心它是否实例化及其内部如何调度,这种情况很多,例如串口通信和数据库访问对象等等,这些情况都可以采用单件模式。
1
、意图
单件模式保证应用只有一个全局唯一的实例,并且提供一个访问它的全局访问点。
2
、使用场合
当类只能有一个实例存在,并且可以在全局访问的时候,这个唯一的实例应该可以通过子类实现扩展,而且用户无需更改代码即可以使用。
3
、结构
单件模式的结构非常简单,包括防止其它对象创建实例的私有构造函数,保持唯一实例的私有变量和全局变量访问接口等,请看下面的例子:
using System;
namespace
单件模式
{
public class CShapeSingletion
{
private static CShapeSingletion mySingletion=null;
//
为了防止用户实例化对象,这里把构造函数设为私有的
private CShapeSingletion()
{}
//
这个方法是调用的入口
public static CShapeSingletion Instance()
{
if (mySingletion==null)
{
mySingletion=new CShapeSingletion();
}
return mySingletion;
}
private int intCount=0;
//
计数器,虽然是实例方法,但这里的表现类同静态
public int Count()
{
intCount+=1;
return intCount;
}
}
}
private void button1_Click(object sender, System.EventArgs e)
{
label1.Text=CShapeSingletion.Instance().Count().ToString();
}
4
、效果
单件提供了全局唯一的访问入口,因此比较容易控制可能发生的冲突。
单件是对静态函数的一种改进,首先避免了全局变量对系统的污染,其次它可以有子类,业可以定义虚函数,因此它具有多态性,而类中的静态方法是不能定义成虚函数的。
单件模式也可以定义成多件,即允许有多个受控的实例存在。
单件模式维护了自身的实例化,在使用的时候是安全的,一个全局对象无法避免创建多个实例,系统资源会被大量占用,更糟糕的是会出现逻辑问题,当访问象串口这样的资源的时候,会发生冲突。
5
、单件与实用类中的静态方法
实用类提供了系统公用的静态方法,并且也经常采用私有的构造函数,和单件不同,它没有实例,其中的方法都是静态方法。
实用类和单件的区别如下:
1)实用类不保留状态,仅提供功能。
2)实用类不提供多态性,而单件可以有子类。
3)单件是对象,而实用类只是方法的集合。
应该说在实际应用中,实用类的应用更加广泛,但是在涉及对象的情况下需要使用单件,例如,能不能用实用类代替抽象工厂呢?如果用传统的方式显然不行,因为实用类没有多态性,会导致每个工厂的接口不同,在这个情况下,必须把工厂对象作为单件。
因此何时使用单件,要具体情况具体分析,不能一概而论。
第六节
在团队并行开发中应用代理模式
代理模式的意图,是为其它对象提供一个代理,以控制对这个对象的访问。
首先作为代理对象必须与被代理对象有相同的接口,换句话说,用户不能因为使不使用代理而做改变。
其次,需要通过代理控制对对象的访问,这时,对于不需要代理的客户,被代理对象应该是不透明的,否则谈不上代理。
下图是代理模式的结构。
实例:测试中的“占位”对象
软件开发需要协同工作,希望开发进度能够得到保证,为此需要合理划分软件,每个成员完成自己的模块,为同伴留下相应的接口。
在开发过程中,需要不断的测试。然而,由于软件模块之间需要相互调用,对某一模块的测试,又需要其它模块的配合。而且在模块开发过程中也不可能完全同步,从而给测试带来了问题。
假定,有一个系统,其中Ordre(订单)和OrderItme(订单项)的UML图如下。
其中:Ordre包括若干OrderItme,订单的总价是每个订单项之和。
假定这是由两个开发组完成的,如果OrderItme没有完成,Ordre也就没有办法测试。一个简单的办法,是Ordre开发的时候屏蔽OrderItme调用,但这样代码完成的时候需要做大量的垃圾清理工作,显然这是不合适的,我们的问题是,如何把测试代码和实际代码分开,这样更便于测试,而且可以很好的集成。
如果我们把OrderItem抽象为一个接口或一个抽象类,实现部分有两个平行的子类,一个是真正的OrderItem,另一个是供测试用的TestOrderItem,在这个类中编写测试代码,我们称之为Mock。
这时,Order可以使用TestOrderItem,测试。当OrderItem完成以后,有需要使用OrderItem进行集成测试,如果OrderItem还要修改,又需要转回TestOrderItem。
我们希望只用一个参数就可以完成这种切换,比如在配置文件中,测试设为true,而正常使用为false。
这些需求牵涉到代理模式的应用,现在可以把代理结构画清楚。
这就很好的解决了问题。
实例:
首先编写一个配置文件:config.xml
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="istest" value="false" />
</appSettings>
</configuration>
代码:
using System;
using System.IO;
using System.Xml;
using System.Collections;
namespace
代理一
{
//
这是统一的接口
public abstract class AbstractOrderItem
{
private string m_GoodName;
private long m_Count;
private double m_Price;
public virtual string GoodName
{
get
{
return m_GoodName;
}
set
{
m_GoodName=value;
}
}
public virtual long Count
{
get
{
return m_Count;
}
set
{
m_Count=value;
}
}
public virtual double Price
{
get
{
return m_Price;
}
set
{
m_Price=value;
}
}
//
价格求和,这个计算方式是另外的人编写的
public abstract double GetTotalPrice();
}
//
处理订单的代码
public class Order
{
public string Name;
public DateTime OrderDate;
private ArrayList oitems;
public Order()
{
oitems=new System.Collections.ArrayList();
}
public void AddItem(AbstractOrderItem it)
{
oitems.Add(it);
}
public void RemoveItem(AbstractOrderItem it)
{
oitems.Remove(it);
}
public double OrderPrice()
{
AbstractOrderItem it;
double op=0;
for (int i=0;i<oitems.Count;i++)
{
it=(AbstractOrderItem)oitems[i];
op+=it.GetTotalPrice();
}
return op;
}
public ArrayList GetOrder()
{
return oitems;
}
}
//
由另外的队伍编写的处理代码
//
主要需要调用客户服务的计算方法,这里只处理合计
public class OrderItme:AbstractOrderItem
{
public override double GetTotalPrice()
{
return this.Count*this.Price;
}
}
//
这是一个专门用于读配置文件的类
public class configuration
{
public static string[] Attribute(string configname,
string mostlyelem,
string childmostlyelem)
{
ArrayList al=new ArrayList();
XmlDocument doc = new XmlDocument();
doc.Load(configname);
//
建立所有元素的列表
XmlElement root = doc.DocumentElement;
//
把所有的主要标记都找出来放到节点列表中
XmlNodeList elemList = root.GetElementsByTagName(mostlyelem);
for (int i=0; i < elemList.Count; i++)
{
//
获取二级标记子节点
XmlNodeList elemList1 = ((XmlElement)elemList[i]).GetElementsByTagName(childmostlyelem);
//
获取这个节点的数目
int N=elemList1.Count;
for (int j=0; j < elemList1.Count; j++)
{
//
获取这个节点的属性集合
XmlAttributeCollection ac = elemList1[j].Attributes;
for( int k = 0; k < ac.Count; k++ )
{
if (ac[k].Name=="value")
{
al.Add(ac[k].Value);
}
}
}
}
string[] strOut=new string[al.Count];
al.CopyTo(strOut,0);
return strOut;
}
}
//
这是一个代理类,由配置文件决定状态
public class TestOrderItem:AbstractOrderItem
{
private OrderItme myOrderItme=null;
public override double GetTotalPrice()
{
//
读配置文件,看是不是处于测试状态
string[] istest=configuration.Attribute("config.xml","appSettings","add");
bool s=bool.Parse(istest[0]);
if (s)
{
//
这个返回的数据称之为“占位”
return 1000;
}
else
{
myOrderItme=new OrderItme();
myOrderItme.GoodName=this.GoodName;
myOrderItme.Count=this.Count;
myOrderItme.Price=this.Price;
return myOrderItme.GetTotalPrice();
}
}
}
}
Order o=new Order();
//
加入
private void button1_Click(object sender, System.EventArgs e)
{
TestOrderItem t1=new TestOrderItem();
t1.GoodName=textBox1.Text;
t1.Count=long.Parse(textBox2.Text);
t1.Price=double.Parse(textBox3.Text);
o.AddItem(t1);
}
//
显示
private void button2_Click(object sender, System.EventArgs e)
{
listBox1.Items.Clear();
ArrayList m=o.GetOrder();
for (int i=0;i<m.Count;i++)
{
AbstractOrderItem s=(AbstractOrderItem)m[i];
listBox1.Items.Add(s.GoodName+" "+s.Count.ToString()+" "+s.Price.ToString());
}
listBox1.Items.Add("
合计:"+o.OrderPrice());
}
第七节
利用观察者模式延长架构的生命周期
当需要上层对底层的操作的时候,可以使用观察者模式实现向上协作。也就是上层响应底层的事件,但这个事件的执行代码由上层提供。
1
、意图:
定义对象一对多的依赖关系,当一个对象发生变化的时候,所有依赖它的对象都得到通知并且被自动更新。
2
、结构
传统的观察者模式结构如下。
3
,举例:
public
class Payment
{
private decimal amount;
public decimal Amount
{
get
{
return amount;
}
set
{
amount = value;
}
}
public delegate string PersonAction(string x);
//
定义事件
public event PersonAction Action;
protected virtual string onAction(string x1)
{
if (Action != null)
return Action(x1);
else
return x1;
}
public virtual string goSale()
{
string x = "
不变的流程一 "
;
x = onAction(x); //
可变的流
x += amount + ",
正在查询库存状态"
; //
属性和不变的流程二
return x;
}
}
调用:
private
Payment o1 = new Payment();
private Payment o2 = new Payment();
private Payment o3 = new Payment();
private Payment o4 = new Payment();
private void Form1_Load(object sender, EventArgs e)
{
o2.Action += new Payment.PersonAction(Cash);
o3.Action += new Payment.PersonAction(Credit);
o4.Action += new Payment.PersonAction(Check);
}
private string Cash(string x)
{
return x + "
现金支付 "
;
}
private string Credit(string x)
{
return x + "
信用卡支付,联系机构 "
;
}
private string Check(string x)
{
return x + "
支票支付,联系财务部 "
;
}
//
没有事件
private void button1_Click(object sender, EventArgs e)
{
if (textBox1.Text != "")
{
o1.Amount = decimal.Parse(textBox1.Text);
}
textBox2.Text = o1.goSale();
}
//
现金支付
private void button2_Click(object sender, EventArgs e)
{
if (textBox1.Text != "")
{
o2.Amount = decimal.Parse(textBox1.Text);
}
textBox2.Text = o2.goSale();
}
//
信用卡支付
private void button3_Click(object sender, EventArgs e)
{
if (textBox1.Text != "")
{
o3.Amount = decimal.Parse(textBox1.Text);
}
textBox2.Text = o3.goSale();
}
//
支票支付
private void button4_Click(object sender, EventArgs e)
{
if (textBox1.Text != "")
{
o4.Amount = decimal.Parse(textBox1.Text);
}
textBox2.Text = o4.goSale();
}
第八节
树状结构和链形结构的对象组织
对象的组织方式,可以是树状结构和链形结构两种。
一、树状结构:组合模式
组合可以说是非常常见的一种结构,我们经常会遇到一些装配关系,从数据结构上来说,这些关系往往表达为一种树状结构,这就用到了组合模式。
它的意图是,把对象组合成树形结构来来表示“部分
-整体”关系,使得用户对单个对象和组合对象的使用具有一致性。
1
、结构
组合模式的结构可以有多种形式,一个最典型的结构如下。
2
、效果
使用组合模式有以下优点:
1)组合对象可以由基本对象和其它组合对象构成,这样,采用有限的基本对象就可以构造数量众多的组合对象。
2)组合对象和基本对象有相同的接口,这样操作组合对象和操作基本对象基本相同,这样就可以大大简化客户代码。
3)可以很容易的增加类型,由于新类型符合相同的接口,因此不需要改动客户代码。
采用组合方式的代价是,由于组合对象和基本对象的接口相同,所以程序不能依赖具体的类型,不过这个问题本身并不大。
3
、组合模式的不同实现方式
组合模式可以有多钟实现方式,下面列出三种。
1)强制类型集合
这里有自己定义一个表示控件聚合的Metrics对象,由这个对象放置类型(比如Metric),采用强制类型集合的优点为:
代码含义清楚:集合中的类型是确定的;
不容易出错:由于集合中的类型是确定的,所以有类型错误在编译的时候可以早期发现。
这种方式的缺点是需要自行定制集合,编码比较复杂。
2)基础节点和复合节点相同
这种方式集合就在基础节点中,便于构造复杂的数据结构。
3)非强制类型集合
非强制类型集合主要是采用象ArrayList这类集合,它的数据类型是Object,因此可以保留任何数据类型,由于.NET中具备大量的可供选择的集合类,编码比较方便。
缺点是:
代码不够清晰,特别是ArrayList内部保留的数据结构往往看得不清楚。
需要进行类型转换,这样有些问题只有在运行中才会暴露出来,增加了调试的难度。
另外一个问题,组合模式往往需要遍历数据,这需要使用递归方法。
二、链形结构:职责链模式
当算法牵涉到一种链型运算,而且不希望处理过程中有过多的循环和条件选择语句,并且希望比较容易的扩充文法,可以采用职责链模式。
1
、意图
使多个对象都有机会处理请求,避免请求的发送者和接收者之间的耦合关系,可以把这些对象链成一个链,并且沿着这个链来传递请求,直到处理完为止。
当处理方式需要动态宽展的时候,职责链是一个很好的方式。
2
、使用场合
以下情况可以使用职责链模式:
1)有多个对象处理请求,到底怎么处理在运行时确定。
2)希望在不明确指定接收者的情况下,向多个对象中的一个提交请求。
3)可处理一个请求的对象集合应该被动态指定。
3
、结构
职责链的结构如下。
其中:
Handler 处理者
方法:HandlerRequest 处理请求
ConcreteHandler 具体处理者
关联变量:successor (后续)是组成链所必需。
4
、实例
下面的实例反映了上面职责链的工作过程,注意链是在运行中建立的。
using System;
using System.Windows.Forms;
namespace
基本职责链
{
public abstract class Handler
{
public Handler successor;
public int s;
public abstract int HandlerRequest(int k);
}
public class ConcreteHandler1:Handler
{
public override int HandlerRequest(int k)
{
int c=k+s;
return c;
}
}
}
调用
Handler m;
private void Form1_Load(object sender, System.EventArgs e)
{
//
建立职责链
Handler ct=new ConcreteHandler1();
Handler ct1=null;
ct.s=0;
m=ct;
ct1=m;
for (int i=1;i<10;i++)
{
ct=new ConcreteHandler1();
ct.s=i;
ct1.successor=ct;
ct1=ct;
}
}
private void button1_Click(object sender, System.EventArgs e)
{
//
显示
see(m,10);
}
void see(Handler m,int b)
{
if (m !=null)
{
int s=m.HandlerRequest(b);
listBox1.Items.Add(s);
m=m.successor;
see(m,s);
}
}
}
第九节
委托技术与行为型设计模式
一、委托技术的使用场合
上面关于行为模式的讨论,关键是行为方法的多态性实现,行为使用者引用行为类接口的目的,仅仅是为了获得行为方法,这样,为了使用行为,使用者必须依赖行为类接口。尽管这样耦合性已经很小了,但有些情况下仍然不很方便。
假如,现有一个类实现了行为的抽象方法,但没有实现行为类接口,我们要使用它的“行为”方法,这时,就必须引入一个行为类适配器,从而使系统的复杂性增加了,下面就是这样一种结构。
代码:
public class
现有行为类
{
public void
行为()
{
}
public void
其它行为()
{
}
}
public class
行为适配器:行为类接口
{
public override void
行为()
{
现有行为类 m=new 现有行为类();
m.
行为();
}
}
public class
行为使用者
{
public
行为类接口 我的行为;
public
行为使用者()
{
我的行为=new 行为适配器();
}
public void
执行()
{
我的行为.行为();
}
}
这会使问题变得复杂。
如果行为类接口的引入单存是为了行为扩充,我们可以用委托来代替行为类接口,也就是这些继承关系都不需要存在,而是用委托来定义行为的格式。
public class
具体行为1
{
public void
行为()
{
}
}
public class
具体行为2
{
public void
行为()
{
}
}
public class
现有行为类
{
public void
行为()
{
}
public void
其它行为()
{
}
}
public delegate void
行为代理();
public class
行为使用者
{
public
行为代理 行为代理行为;
public
行为使用者()
{
行为代理行为=new 行为代理((new 现有行为类()).行为);
}
public void
执行()
{
行为代理行为();
}
}
同样可以实现上面的目的,但系统的耦合度大幅度的降下来了。在只关心方法的场合,由“行为代理”来决定方法的格式,这时各个类并不需要一致的接口。
采用委托技术一个现实的缺点是,现有的UML标准并不包括委托结构,因此在静态图上并不能很好的表达出来,同时,本身系统的结构关系也不太清楚,这给调试和测试带来了难度。
当然,面向对象的理论是发展的,传统的面向对象理论多态性主要是通过继承或者是通过接口实现的,但是,委托同样也是实现多态性的一种方式(同样,还可以通过反射实现多态性),这也表明了传统的技术理论并不是一成不变的道理。
二、采用委托技术实现模板方法结构
在前面相关的章节已经讨论了一些设计模式使用委托的情况,这里我们把它们放在一起,有助于更深的理解如何使用委托。
我们已经知道,传统的模板方法结构和实现如下:
public abstract class AbstractClass
{
public virtual string TemplateMethod()
{
string s="";
s=s+Title();
s=s+Body();
return s;
}
public abstract string Title();
public abstract string Body();
}
public class ConcreteClass: AbstractClass
{
public override string Title()
{
return "
我是名称";
}
public override string Body()
{
return "
我是内容";
}
}
public class ConcreteClass1: AbstractClass
{
public override string Title()
{
return "
新的名称";
}
public override string Body()
{
return "
新的内容";
}
}
调用:
AbstractClass obj;
private void button1_Click(object sender, System.EventArgs e)
{
obj=new ConcreteClass();
label1.Text=obj.TemplateMethod();
}
private void button2_Click(object sender, System.EventArgs e)
{
obj=new ConcreteClass1();
label1.Text=obj.TemplateMethod();
}
但我们也可以利用委托来实现模板方法,类的结构并没有多大变化,只是不需要定义成抽象类,也不需要继承,这样也就减少了耦合性。
//
首先定义两个委托,决定方法调用的格式
public delegate string DTitle();
public delegate string DBody();
public class TemClass
{
//
模板类,基于委托的实现
public string TemplateMethod()
{
string s="";
s=s+Title()+" ";
s=s+Body();
return s;
}
public DTitle Title;
public DBody Body;
}
//
实现类,并不需要继承,但被调用的方法格式要一致
public class ConcreteClass1
{
public string myTitle1()
{
return "
我是名称";
}
public string myBody1()
{
return "
我是内容";
}
}
public class ConcreteClass2
{
public string myTitle2()
{
return "
新的名称";
}
public string myBody2()
{
return "
新的内容";
}
}
调用:
//
构造模板的实例
TemClass tm=new TemClass();
private void button1_Click(object sender, System.EventArgs e)
{
//
利用委托定位到需要处理的方法指针
ConcreteClass1 csc=new ConcreteClass1();
tm.Title=new DTitle(csc.myTitle1);
tm.Body=new DBody(csc.myBody1);
//
实现
label1.Text=tm.TemplateMethod();
}
private void button2_Click(object sender, System.EventArgs e)
{
//
利用委托定位到需要处理的方法指针
ConcreteClass2 csc=new ConcreteClass2();
tm.Title=new DTitle(csc.myTitle2);
tm.Body=new DBody(csc.myBody2);
//
实现
label1.Text=tm.TemplateMethod();
}
显然,使用委托减少了类的层次结构,同时也减少了耦合性。
很多情况下,模板方法和工厂方法一起使用,可以达到非常好的效果。
三、采用委托实现策略模式
策略模式的结构如下:
策略模式的目的是使某种算法独立于调用算法的对象,在这种模式中,Context依赖Strategy接口,这就有可能遇到和上面的讨论同样的问题,也就是某个现存对象实现了算法,但并不符合Strategy接口的规则,这就需要引入适配器。
应用委托技术可以避免这种情况,也就是用委托来代替
Strategy接口。
using System;
using System.Collections;
namespace
策略和委托
{
//
构造委托取代接口
public delegate string Strateinteface(string m);
//
包含算法的具体实现
//
注意,不使用继承,而且方法的名字也不同
public class ConcreteStrategy1
{
public string myinteface(string m)
{
return m+":
算法一";
}
}
public class ConcreteStrategy2
{
public string yourinteface(string ma)
{
return ma+":
算法二";
}
}
public class Context
{
public Strateinteface strateinteface;
//
聚合可以用集合实现
private Hashtable hs=new Hashtable();
public Context()
{
hs.Add("s1",new Strateinteface((new ConcreteStrategy1()).myinteface));
hs.Add("s2",new Strateinteface((new ConcreteStrategy2()).yourinteface));
}
public string contextinteface(string KeyName,string strIn)
{
strateinte>
return strateinteface(strIn);
}
}
}
实现:
Context c=new Context();
private void button1_Click(object sender, System.EventArgs e)
{
textBox1.Text=c.contextinteface("s1","
王小丫");
}
private void button2_Click(object sender, System.EventArgs e)
{
textBox1.Text=c.contextinteface("s2","
黄建翔");
}
四、用委托和事件机制实现观察者模式
.NET提供了处理聚集的基本方法和事件响应方法,并且引入了委托的概念,因此,>NET天然的支持观察者模式。
下面我们用一个最简单的例子,说明它是如何处理问题的。
首先我们必须定义一个委托,这个委托事实上提供了对法方法的指针调用,也定义了被调用的方法必须遵循的形式:
public delegate void LoopEvent(int i,int j);
下面是例子,请注意代码中的注释。
using System;
using System.Windows.Forms;
using System.Drawing;
namespace
观察者
{
//
主题,相当于Subject
public class DoLoop
{
public delegate void LoopEvent(int i,int j);
public event LoopEvent le;
public void BeginLoop(int LoopNumber)
{
for (int i=0;i<LoopNumber;i++)
{
for (int j=0;j<LoopNumber;j++)
{
le(i,j);
}
}
}
}
//
观察者显示窗口
public class Display:Form
{
private Button button1=new Button();
private TextBox textBox1=new TextBox();
private Label label1=new Label();
public Display()
{
this.label1.Location = new Point(72, 8);
this.label1.Size = new Size(112, 24);
this.label1.Text = "label1";
this.button1.Location = new Point(160, 48);
this.button1.Size = new Size(96, 24);
this.button1.Text = "
测量";
this.button1.Click += new System.EventHandler(this.button1_Click);
this.textBox1.Location = new Point(16, 48);
this.ClientSize = new Size(272, 85);
this.Controls.Add(this.textBox1);
this.Controls.Add(this.button1);
this.Controls.Add(this.label1);
this.Text = "Display";
}
//
这是观察者显示方法
public void UpdateDisplay(int i,int j)
{
this.label1.Text=i.ToString()+","+j.ToString();
}
//
测量,这是本身工作的事件,和观察者模式无关
private void button1_Click(object sender, System.EventArgs e)
{
textBox1.Text=label1.Text;
}
}
}
调用:
//
定义主题对象
DoLoop d1;
private void Form2_Load(object sender, System.EventArgs e)
{
d1=new DoLoop();
}
//
实现委托,这个定义把观察者和主题联系了起来
private void button1_Click(object sender, System.EventArgs e)
{
Display f=new Display();
d1.le+=new DoLoop.LoopEvent(f.UpdateDisplay);
f.Show();
}
//
可以把每个对象放在不同的线程中
//
主题发送信息的时候,各个对象本身还能正常工作
private void button2_Click(object sender, System.EventArgs e)
{
Thread t=new Thread(new ThreadStart(aaa));
t.Start();
}
void aaa()
{
d1.BeginLoop(100);
}
}
五、采用委托技术简化中介者模式
传统的中介者模式中可以看到Colleague对Mediator的引用,引用的目的是通知Mediator自己要执行的操作。这样结构的缺点是Colleague对Mediator之间是紧耦合的。在.NET开发的时候,可以使用委托技术,这样就解开了Colleague对Mediator的依赖,所以.NET开发这种技术用的很多,静态结构可以变成下面的情况。
实例:窗体和控件
[STAThread]
static void Main()
{
Application.Run(new MainForm());
}
private void MainForm_Load(object sender, System.EventArgs e)
{
lsAll.Items.Add("
中国");
lsAll.Items.Add("
美国");
lsAll.Items.Add("
英国");
lsAll.Items.Add("
俄罗斯");
lsAll.Items.Add("
法国");
lsAll.Items.Add("
德国");
lsAll.SelectionMode=System.Windows.Forms.SelectionMode.MultiSimple;
lsSelected.SelectionMode=System.Windows.Forms.SelectionMode.MultiSimple;
}
private void btAdd_Click(object sender, System.EventArgs e)
{
for (int i=0;i<lsAll.SelectedItems.Count;i++)
{
lsSelected.Items.Add(lsAll.SelectedItems[i]);
}
for (int i=lsAll.SelectedItems.Count-1;i>=0;i--)
{
lsAll.Items.Remove(lsAll.SelectedItems[i]);
}
}
private void btRemove_Click(object sender, System.EventArgs e)
{
for (int i=0;i<lsSelected.SelectedItems.Count;i++)
{
lsAll.Items.Add(lsSelected.SelectedItems[i]);
}
for (int i=lsSelected.SelectedItems.Count-1;i>=0;i--)
{
lsSelected.Items.Remove(lsSelected.SelectedItems[i]);
}
}
需要注意的问题是,Windows本身就是一个中介者,所以大多数情况下并没有必要再定义一个中介者。之所以提出这个问题,是因为我们看到大多数介绍中介者模式的文章,均以人机界面为例子,但他们往往用实例生成ListBox和Button的子类,使生成的子类互相调用,然后又做一个Mediator解耦。但ListBox和Button本身并没有耦合性,生成一个子类人为地加入耦合性显然是多此一举,而且增加了程序调试的难度,这是一种设计过度的典型的例子。
这样的程序不但没有提高性能,相反更难维护。
之所以如此,是因为很多人受GoF的《设计模式》书中列举的那个例子影响,加上读书不求甚解,依猫画虎,其实这是学习设计模式最忌讳的事情。我们应该知道,《设计模式》形成的那个年代,很多待解决的问题在当前大多数平台上都得到解决了,时代在发展,我们不需要再走回头路。
六、设计模式的发展
委托可以使方法接口做为最小的接口单元使用,从而避免了不必要的继承,减少了类的层次,这也是用组合代替继承的最好的体现。
但是,委托也使设计更难理解,因为对象是动态建立的。
随着技术的进步,设计模式是需要随着时代的进步而进步的。从某种意义上讲,
GoF的“设计模式”从概念上的意义,要超过对实际开发上的指导,很多当初难以解决的问题,现在已经可以轻而易举的解决了,模式的实现更加洗练,这就是我们设计的时候不能死搬硬套的原因。
第十节
C
语言嵌入式系统应用设计模式的讨论
上面关于软件架构的讨论,特别是在详细设计的过程,完全是基于面向对象的技术的,显而易见,利用面向对象的基本概念,像封装性、继承性尤其是多态性,可以大大提升系统的可升级可维护性能,成为现代架构设计的主体。
但是,
C语言嵌入式系统编程中,软件架构的理论能适用吗?这也是许多人感觉困惑的问题。其实仔细研究面向对象开发的本质,可以发现在面向过程的语言体系中,完全可以应用面向对象的思想,下面谈得仅仅是我个人的看法,供参考。
一、利用指针调用函数实现多态性
仔细研究一下上面关于委托应用的问题,也可以发现利用指针,完全可以实现基于接口编程的多态性的,而
23个模式本质上是利用了多态性。调用函数的指针在声明的时候,本质上是规定了被调函数的格式,这和接口的作用是一样的。
例:有两个方法
max和min:
int max(int x,int y)
{
int z;
if (x>y) z=x;
else
z=y;
return(z);
}
int min(int x,int y)
{
int z;
if (x<y) z=x;
else
z=y;
return(z);
}
//定义
max函数的指针
int (*p)(int,int);
p=max;
c=(*p)(5,6);
p=min;
c=(*p)(5,6);
二、C的面向对象化
在面向对象的语言里面,出现了类的概念。类是对特定数据的特定操作的集合体。类包含了两个范畴:数据和操作。而
C语言中的struct仅仅是数据的集合,我们可以利用函数指针将struct模拟为一个包含数据和操作的"类"。下面的C程序模拟了一个最简单的"类":
#ifndef C_Class
#define C_Class struct
#endif
C_Class A
{
C_Class A *A_this; /* this指针 */
void (*Foo)(C_Class A *A_this); /* 行为:函数指针 */
int a; /* 数据 */
int b;
};
我们完全可以利用
C语言模拟出面向对象的三个特性:封装、继承和多态,但是更多的时候,我们只是需要将数据与行为封装以解决软件结构混乱的问题。C模拟面向对象思想的目的不在于模拟行为本身,而在于解决某些情况下使用C语言编程时程序整体框架结构分散、数据和函数脱节的问题。
在这样的情况下,面向对象的理论和优点安全可以在
C语言嵌入式系统编程中得到应用,你甚至于还可以编写比较紧凑的层,共二次开发使用。
三、C的模块划分
C语言由于其特点,模块划分并不像典型的面向对象语言(
Java、C#)那么清楚,但我们还是可以认真的去研究,结合实际的搞好模块的划分。
模块划分是指怎样合理的将一个很大的软件划分为一系列功能独立的部分,通过合作完成系统的需求,这也是架构设计的核心知识。
C语言作为一种结构化的程序设计语言,在模块的划分上主要依据功能,
但在面向对象的架构设计中,功能块的概念实际上被弱化了,但在
C语言设计中,这个思想仍然是可行的,C语言模块化程序设计需要注意以下几个问题:
(
1) 模块即是一个.c文件和一个.h文件的结合,头文件(.h)中是对于该模块接口的声明。
(
2) 某模块提供给其它模块调用的外部函数及数据需在.h中文件中冠以extern关键字声明。
(
3) 模块内的函数和全局变量需在.c文件开头冠以static关键字声明。
(
4) 永远不要在.h文件中定义变量,定义变量和声明变量的区别在于定义会产生内存分配的操作,是汇编阶段的概念;而声明则只是告诉包含该声明的模块在连接阶段从其它模块寻找外部函数和变量。如:
/*module1.h*/
int a = 5; /* 在模块
1的.h文件中定义int a */
/*module1 .c*/
#include "module1.h" /* 在模块
1中包含模块1的.h文件 */
/*module2 .c*/
#include "module1.h" /* 在模块
2中包含模块1的.h文件 */
/*module3 .c*/
#include "module1.h" /* 在模块
3中包含模块1的.h文件 */
以上程序的结果是在模块
1、2、3中都定义了整型变量a,a在不同的模块中对应不同的地址单元,其实我们从来不需要这样的程序。正确的做法是:
/*module1.h*/
extern int a; /* 在模块
1的.h文件中声明int a */
/*module1 .c*/
#include "module1.h" /* 在模块
1中包含模块1的.h文件 */
int a = 5; /* 在模块
1的.c文件中定义int a */
/*module2 .c*/
#include "module1.h" /* 在模块
2中包含模块1的.h文件 */
/*module3 .c*/
#include "module1.h" /* 在模块
3中包含模块1的.h文件 */
这样如果模块
1、2、3操作a的话,对应的是同一片内存单元。
一个嵌入式系统通常包括两类模块:
(
1)硬件驱动模块,一种特定硬件对应一个模块;
(
2)软件功能模块,其模块的划分应满足低偶合、高内聚的要求。
也就是前面所述的原则这里都是适用的。
四、多任务系统和单任务系统
所谓
"单任务系统"是指该系统不能支持多任务并发操作,宏观串行地执行一个任务。而多任务系统则可以宏观并行(微观上可能串行)地"同时"执行多个任务。
多任务的并发执行通常依赖于一个多任务操作系统(
OS),多任务OS的核心是系统调度器,它使用任务控制块(TCB)来管理任务调度功能。TCB包括任务的当前状态、优先级、要等待的事件或资源、任务程序码的起始地址、初始堆栈指针等信息。
调度器在任务被激活时,要用到这些信息。此外,
TCB还被用来存放任务的"上下文"(context)。任务的上下文就是当一个执行中的任务被停止时,所要保存的所有信息。通常,上下文就是计算机当前的状态,也即各个寄存器的内容。当发生任务切换时,当前运行的任务的上下文被存入TCB,并将要被执行的任务的上下文从它的TCB中取出,放入各个寄存器中。
究竟选择多任务还是单任务方式,依赖于软件的体系是否庞大。例如,绝大多数手机程序都是多任务的,但也有一些小灵通的协议栈是单任务的,没有操作系统,它们的主程序轮流调用各个软件模块的处理程序,模拟多任务环境。
五、合理应用中断服务程序
中断是嵌入式系统中重要的组成部分,但是在标准
C中不包含中断。许多编译开发商在标准C上增加了对中断的支持,提供新的关键字用于标示中断服务程序(ISR),类似于__interrupt、#program interrupt等。当一个函数被定义为ISR的时候,编译器会自动为该函数增加中断服务程序所需要的中断现场入栈和出栈代码。
中断服务程序需要满足如下要求:
(1)不能返回值;
(2)不能向ISR传递参数;
(3) ISR应该尽可能的短小精悍;
(4) printf(char * lpFormatString,…)函数会带来重入和性能问题,不能在ISR中采用。
例如,在项目的开发中,我们设计了一个队列,在中断服务程序中,只是将中断类型添加入该队列中,在主程序的死循环中不断扫描中断队列是否有中断,有则取出队列中的第一个中断类型,进行相应处理。
/* 存放中断的队列
*/
typedef struct tagIntQueue
{
int intType; /* 中断类型 */
struct tagIntQueue *next;
}IntQueue;
IntQueue lpIntQueueHead;
__interrupt ISRexample ()
{
int intType;
intType = GetSystemType();
QueueAddTail(lpIntQueueHead, intType);/* 在队列尾加入新的中断 */
}
在主程序循环中判断是否有中断:
While(1)
{
If( !IsIntQueueEmpty() )
{
intType = GetFirstInt();
switch(intType) /* 是不是很象WIN32程序的消息解析函数? */
{
/* 对,我们的中断类型解析很类似于消息驱动 */
case xxx: /* 我们称其为"中断驱动"吧? */
…
break;
case xxx:
…
break;
…
}
}
}
按上述方法设计的中断服务程序很小,实际的工作都交由主程序执行了。
六、硬件驱动模块的设计
一个硬件驱动模块通常应包括如下函数:
(
1)中断服务程序ISR
(
2)硬件初始化
a.修改寄存器,设置硬件参数(如
UART应设置其波特率,AD/DA设备应设置其采样速率等);
b.将中断服务程序入口地址写入中断向量表:
/* 设置中断向量表
*/
m_myPtr = make_far_pointer(0l); /* 返回
void far型指针void far * */
m_myPtr += ITYPE_UART; /* ITYPE_UART:
uart中断服务程序 */
/* 相对于中断向量表首地址的偏移
*/
*m_myPtr = &UART _Isr; /* UART _Isr:
UART的中断服务程序 */
(
3)设置CPU针对该硬件的控制线
a.如果控制线可作PIO(可编程I/O)和控制信号用,则设置CPU内部对应寄存器使其作为控制信号;
b.设置CPU内部的针对该设备的中断屏蔽位,设置中断方式(电平触发还是边缘触发)。
(
4)提供一系列针对该设备的操作接口函数。例如,对于LCD,其驱动模块应提供绘制像素、画线、绘制矩阵、显示字符点阵等函数;而对于实时钟,其驱动模块则需提供获取时间、设置时间等函数。
可见,
C语言嵌入式系统架构确实有自己的特点,考虑问题的重点也不太一样,但是只要深刻理解架构设计的基本思想,理解它的精神实质,结合自己的情况,走符合自己实际的路子,一样能设计出性能相当好的架构来。
只是单纯从形式上看问题是永远不会有前途的。
第十一节
架构师在软件架构设计中的作用
在软件组织中,架构是的作用是举足轻重的,为了尽快成长为一个成熟的软件架构师,最后我给你提如下一些建议:
1
,聚焦于人,而不是工艺技术
事实上,软件开发是一个交流游戏,你必须保证人们能彼此有效的沟通(开发人员、项目涉众)。架构师要以最高效的可能方式与客户和开发人员一起工作和讨论,白板上书写讲解、视频会议、电子邮件都是有效的交流手段。
2
,保持简单
建议“最简单的解决方案就是最好的”,你不应该过度制作软件。在架构设计上,你不应该描述用户并不真正需要的附加特性一个辅助的原则就是:“模型来自于目的”。
这个原则引发了两个核心的实践。
第一个就是描述模型的文档力求简单明了,切中要害。
第二个就是架构设计避免不必要的复杂性,以减少不必要的开发、测试和维护工作。
你的架构和文档只要足够好,并不需要完美无缺,事实上也做不到,因为建模的原则就是“拥抱变化”。你的文档应该重点突出,这样可以增加受众理解它的机会,同样这个文档会不断更新,因此如何管理好文档显得十分重要。
3
,迭代和递增的工作
这种迭代和递增的工作,对项目管理和软件产品开发,事实上提出了更高的要求,你必须时时检验你的项目进展,不要使它偏离了方向。
4
,亲自动手
考察一下你遇见过的“架构师”,最棒的那一个一定是需要的时候立刻卷起袖子参加到核心软件开发中的那个人。架构师首先必须是编程专家,这是没有错的。
积极参与开发,和开发人员一起工作,帮助他们理解架构,并在实践中试用它,会带来很多好处。
l 你会很快发现你的想法是否可行。
l 你增加了项目组理解架构的机会。
l 你从项目组使用的工具和技术,以及业务领域获得了经验,提高了你自己对正在进行的架构事务的理解。
l 你获得了具体的反馈,用它来提高架构水平。
l 你获得客户和主要开发人员的尊重,这很重要。
l 你可以指导项目组的开发人员建模和小粒度架构。
5
,在开口谈论之前先实践
不要作无谓的空谈和争论,你可以写一个满足你的技术主张的小版本,来保证你的方案切实可行,这个小版本只研究最最关键的技术。这些主张只写够用的代码就行了,来验证你的方案切实可行。这会减少你的技术风险,因为你让技术决策基于已知的事实,而不是美好的猜想。
6
,让架构吸引你的客户
架构师需要很好的与客户沟通,让客户理解你的工作的价值,他如果明白你的架构工作会帮助他们的任务,那他们就会很乐意的和你一起工作。架构师的这种与客户沟通的技巧极其重要,因为如果客户认为你在浪费他的时间,那他就会想方设法回避你。你的架构描述能不能吸引客户,也成了建模是不是能顺利进行的关键。
7
、架构师面对时代的考验
年轻人需要成长为合格的架构师,需要扎扎上实实的从基础做起,不断提升自己的能力,并不是听过几个课程,就能够成为一个合格的架构师的。架构师必须善于学习,一个人最大的投资莫过于对自己的投资,每周花三个小时时间用于学习是完全必要的。
架构师的知识在必要的时候要发生飞跃,但是,这种知识的飞跃必须是可靠的,是经过深思熟虑和实验的,同时要反复思索,把自己的思维实践和这种知识的飞跃有机的结合起来。
架构师要更看重企业所要解决的问题。
架构师要学会在保证性能的前提下,寻找更简单的解决方案。
做一个好的架构师并不是一个容易的事情,这需要我们付出极其艰苦的努力。
我这个课程的主题,就是“拥抱变化”,需求是在变化的,架构是在变化的,设计模式也是在变化的,项目管理当然也是变化的。在这个大变动时期,给我们每个人提供了巨大的机会,也提出了巨大的挑战。
好的架构师的优势在于他的智慧,而智慧的获得,需要实实在在的努力。