• 1.19 MB
  • 2022-04-22 11:26:00 发布

架构师培训讲义6-类结构设计

  • 91页
  • 当前文档由用户上传发布,收益归属用户
  1. 1、本文档共5页,可阅读全部内容。
  2. 2、本文档内容版权归属内容提供方,所产生的收益全部归内容提供方所有。如果您对本文有版权争议,可选择认领,认领后既往收益都归您。
  3. 3、本文档由用户上传,本站不保证质量和数量令人满意,可能有诸多瑕疵,付费之前,请仔细先通过免费阅读内容等途径辨别内容交易风险。如存在严重挂羊头卖狗肉之情形,可联系本站下载客服投诉处理。
  4. 文档侵权举报电话:19940600175。
'详细设计阶段的类结构设计详细设计阶段的一个十分重要的问题,就是进行类设计。类设计直接对应于实现设计,它的设计质量直接影响着软件的质量,所以这个阶段是十分重要的。这就给我们提出了一个问题,类是如何确定的,如何合理的规划类,这就要给我们提出一些原则,或者是一些模式。设计模式是有关中小尺度的对象和框架的设计。应用在实现架构模式定义的大尺度的连接解决方案中。也适合于任何局部的详细设计。设计模式也称之为微观架构模式。第一节类结构设计中的通用职责分配软件模式GRASP模式(GeneralResponsibilityAssignmentSoftwarePatterns通用职责分配软件模式)能够帮助我们理解基本的对象设计技术,并且用一种系统的、可推理的、可说明的方式来应用设计理论。一、根据职责设计对象职责:职责与一个对象的义务相关联,职责主要分为两种类型: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,每个类的任务都比较简单而且单一,这就实现了高的内聚性。 从开发技巧的角度,至少有一个开发者要去考虑内聚所产生的影响。一般来说,高的内聚往往导致低的耦合度。讨论:和低耦合性模式一样,高内聚模式在制定设计方案的过程中,一个应该时刻铭记于心的原则。同样,它往往会和其它的设计原则相抵触,因此必须综合考虑。GradyBooch是建模的大师级人物,它在描述高内聚的定义的时候是这样说的:“一个组件(比如类)的所有元素,共同协作提供一些良好受限的行为。”根据经验,一个具有高内聚的类,具有数目相对较少的方法,和紧密相关的功能。它并不完成太多的工作,当需要实现的任务过大的时候,可以和其它的对象协作来分担过大的工作量。一个类具有高内聚是非常有利的,因为它对于理解、维护和重用都相对比较容易。限制:少数情况下,接受低内聚是合理的。比如,把SQL专家编写的语句综合在一个类里面,这就可以使程序设计专家不必要特别关注SQL语句该怎么写。又比如,远程对象处理,利用很多细粒度的接口与客户联系,造成网络流量大幅度增加而降低性能,就不如把能力封装起来,做一个粗粒度的接口给客户,大部分工作在远程对象内部完成,减少远程调用的次数。第二节设计模式与软件架构一、设计模式在模块设计阶段,最关键的问题是,用户需求是变化的,我们的设计如何适应这种变化呢?1,如果我们试图发现事情怎样变化,那我们将永远停留在分析阶段。2,如果我们编写的软件能面向未来,那将永远处在设计阶段。3,我们的时间和预算不允许我们面向未来设计软件。过分的分析和过分的设计,事实上被称之为“分析瘫痪”。如果我们预料到变化将要发生,而且也预料到将会在哪里发生。这样就形成了几个原则:1,针对接口编程而不是针对实现编程。2,优先使用对象组合,而不是类的继承。3,考虑您的设计哪些是可变的,注意,不是考虑什么会迫使您的设计改变,而是考虑要素变化的时候,不会引起重新设计。也就是说,封装变化的概念是模块设计的主题。解决这个问题,最著名的要数GoF的23种模式,在GoF中,把设计模式分为结构型、创建型和行为型三大类。本课程假定学员已经熟悉这23个模式,因此主要从设计的角度讨论如何正确选用恰当的设计模式。 整个讨论依据三个原则:1)开放-封闭原则2)从场景进行设计的原则3)包容变化的原则下面的讨论会有一些代码例子,尽管在详细设计的时候,并不考虑代码实现的,但任何架构设计思想如果没有代码实现做基础,将成为无木之本,所以后面的几个例子我们还是把代码实现表示出来,举这些例子的目的并不是提供样板,而是希望更深入的描述想法。另外,所用的例子大部分使用C#来编写,这主要因为希望表达比较简单,但这不是必要的,可以用任何面向对象的语言(Java、C++)来讨论这些问题。二、封装变化与面向接口编程设计模式分为结构型、构造型和行为型三种问题域,我们来看一下行为型设计模式,行为型设计模式的要点之一是“封装变化”,这类模式充分体现了面向对象的设计的抽象性。在这类模式中,“动作”或者叫“行为”,被抽象后封装为对象或者为方法接口。通过这种抽象,将使“动作”的对象和动作本身分开,从而达到降低耦合性的效果。这样一来,使行为对象可以容易的被维护,而且可以通过类的继承实现扩展。行为型模式大多数涉及两种对象,即封装可变化特征的新对象,和使用这些新对象的已经有的对象。二者之间通过对象组合在一起工作。如果不使用这些模式,这些新对象的功能就会变成这些已有对象的难以分割的一部分。因此,大多数行为型模式具有如下结构。下面是上述结构的代码片断:publicabstractclass行为类接口{publicabstractvoid行为();}publicclass具体行为1:行为类接口{publicoverridevoid行为(){}}publicclass行为使用者 {public行为类接口我的行为;public行为使用者(){我的行为=new具体行为1();}publicvoid执行(){我的行为.行为();}}第三节合理使用外观和适配器模式一、使用外观模式(Facade)使用户和子系统绝缘外观模式为了一组子系统提供一个一致的方式对外交互。这样就可以使客户和子系统绝缘,可以大大减少客户处理对象的数目,我们已经讨论过这个主题,这里就不再讨论了。二、使用适配器模式(Adapter)调适接口在系统之间集成的时候,最常见的问题是接口不一致,很多能满足功能的软件模块,由于接口不同,而导致无法使用。适配器模式的含义在于,把一个类的接口转换为另一个接口,使原本不兼容而不能一起工作的类能够一起工作。适配器有类适配器和对象适配器两种类型,二者的意图相同,只是实现的方法和适用的情况不同。类适配器采用继承的方法来实现,而对象适配器采用组合的方法来实现。1)类适配器类适配器采用多重继承对一个接口与另一个接口进行匹配,其结构如下。 由于Adapter类继承了Adaptee类,通过接口的Do()方法,我们可以使用Adaptee中的Execute方法。这种方式当语言不承认多重继承的时候就没有办法实现,这时可以使用对象适配器。2)对象适配器对象适配器采用对象组合,通过引用一个类与另一个类的接口,来实现对多个类的适配。`第四节封装变化的三种方式及评价设计可升级的架构,关键是要把模块中不变部分与预测可变部分分开,以防止升级过程中对基本代码的干扰。这种分开可以有多种方式,一般来说可以从纵向、横向以及外围三个方面考虑。一、纵向处理:模板方法(TemplateMethod)1、意图定义一个操作中的算法骨架,而将一些步骤延伸到子类中去,使得子类可以不改变一个算法的结构,即可重新定义改算法的某些特定步骤。这里需要复用的使算法的结构,也就是步骤,而步骤的实现可以在子类中完成。2、使用场合1)一次性实现一个算法的不变部分,并且将可变的行为留给子类来完成。2)各子类公共的行为应该被提取出来并集中到一个公共父类中以避免代码的重复。首先识别现有代码的不同之处,并且把不同部分分离为新的操作,最后,用一个调用这些新的操作的模板方法来替换这些不同的代码。3)控制子类的扩展。3、结构 模板方法的结构如下:在抽象类中定义模板方法的关键是:在一个非抽象方法中调用调用抽象方法,而这些抽象方法在子类中具体实现。代码:publicabstractclassPayment{privatedecimalamount;publicdecimalAmount{get{returnamount;}set{amount=value;}}publicvirtualstringgoSale(){stringx="不变的流程一";x+=Action();//可变的流程x+=amount+",正在查询库存状态";//属性和不变的流程二returnx;}publicabstractstringAction(); }publicclassCashPayment:Payment{publicoverridestringAction(){return"现金支付";}}调用:Paymento;privatevoidbutton1_Click(objectsender,EventArgse){o=newCashPayment();if(textBox1.Text!=""){o.Amount=decimal.Parse(textBox1.Text);}textBox2.Text=o.goSale();} 假定系统已经投运,用户提出新的需求,要求加上信用卡支付和支票支付,可以这样写:publicclassCreditPayment:Payment{publicoverridestringAction(){return"信用卡支付,联系支付机构";}}publicclassCheckPayment:Payment{publicoverridestringAction(){return"支票支付,联系财务部门";}}调用:Paymento;//现金方式privatevoidbutton1_Click(objectsender,EventArgse){o=newCashPayment();if(textBox1.Text!=""){o.Amount=decimal.Parse(textBox1.Text);}textBox2.Text=o.goSale();}//信用卡方式privatevoidbutton2_Click(objectsender,EventArgse){o=newCreditPayment();if(textBox1.Text!=""){o.Amount=decimal.Parse(textBox1.Text);}textBox2.Text=o.goSale();}//支票方式privatevoidbutton3_Click(objectsender,EventArgse) {o=newCheckPayment();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接口,这是定义它的具体实现。我们通过上面的关于支付的简单例子可以说明它的原理。 publicclassPayment{privatedecimalamount;publicdecimalAmount{get{returnamount;}set{amount=value;}}Implementors;publicImplementorImp{set{s=value;}}publicvirtualstringgoSale(){stringx="不变的流程一";x+=s.Action();//可变的流程x+=amount+",正在查询库存状态";//属性和不变的流程二returnx;}} publicinterfaceImplementor{stringAction();}publicclassCashPayment:Implementor{publicstringAction(){return"现金支付";}}调用:Paymento=newPayment();privatevoidbutton1_Click(objectsender,EventArgse){o.Imp=newCashPayment();if(textBox1.Text!=""){o.Amount=decimal.Parse(textBox1.Text);}textBox2.Text=o.goSale();}假定系统投运以后,需要修改性能,可以直接加入新的类:publicclassCreditPayment:Implementor{publicstringAction(){return"信用卡支付,联系机构"; }}publicclassCheckPayment:Implementor{publicstringAction(){return"支票支付,联系财务部";}}调用:Paymento=newPayment();privatevoidbutton1_Click(objectsender,EventArgse){o.Imp=newCashPayment();if(textBox1.Text!=""){o.Amount=decimal.Parse(textBox1.Text);}textBox2.Text=o.goSale();}privatevoidbutton2_Click(objectsender,EventArgse){o.Imp=newCreditPayment();if(textBox1.Text!=""){o.Amount=decimal.Parse(textBox1.Text);}textBox2.Text=o.goSale();}privatevoidbutton3_Click(objectsender,EventArgse){o.Imp=newCheckPayment();if(textBox1.Text!=""){o.Amount=decimal.Parse(textBox1.Text);}textBox2.Text=o.goSale(); }这样就减少了系统的耦合性。而在系统升级的时候,并不需要改变原来的代码。三、核心和外围:装饰器模式(Decorator)有的时候,希望实现一个基本的核心代码快,由外围代码实现专用性能的包装,最简单的方法,是使用继承。publicabstractclassPayment{privatedecimalamount;publicdecimalAmount{get{returnamount;}set{amount=value;}}publicvirtualstringgoSale(){returnAction()+"完成,金额为"+amount+",正在查询库存状态";}publicabstractstringAction(); }publicclassCashPayment:Payment{publicoverridestringAction(){return"现金支付";}}实现:Paymento;privatevoidbutton1_Click(objectsender,EventArgse){o=newCashPayment1();o.Amount=decimal.Parse(textBox1.Text);textBox2.Text=o.goSale();}加入继承:publicclassCashPayment1:CashPayment{publicoverridestringAction(){//在执行原来的代码之前,弹出提示框System.Windows.Forms.MessageBox.Show("现金支付");returnbase.Action();}}实现: Paymento;privatevoidbutton1_Click(objectsender,EventArgse){o=newCashPayment1();o.Amount=decimal.Parse(textBox1.Text);textBox2.Text=o.goSale();}缺点:继承层次多一层,提升了耦合性。当实现类比较多的时候,实现起来就比较复杂。在这样的情况下,也可以使用装饰器模式,这是用组合取代继承的一个很好的方式。1、意图事实上,上面所要解决的意图可以归结为“在不改变对象的前提下,动态增加它的功能”,也就是说,我们不希望改变原有的类,或者采用创建子类的方式来增加功能,在这种情况下,可以采用装饰模式。2、结构装饰器结构的一个重要的特点是,它继承于一个抽象类,但它又使用这个抽象类的聚合(即即装饰类对象可以包含抽象类对象),恰当的设计,可以达到我们提出来的目的。模式中的参与者如下:Component(组成):定义一个对象接口,可以动态添加这些对象的功能,其中包括Operation(业务)方法。ConcreteComponent(具体组成):定义一个对象,可以为它添加一些功能。Decorator(装饰):维持一个对Component对象的引用,并定义与Component接口一致的接口。 ConcreteDecorator(具体装饰):为组件添加功能。它可能包括AddedBehavior(更多的行为)和AddedState(更多的状态)。举个例子:假定我们已经构造了一个基于支付的简单工厂模式的系统。usingSystem;usingSystem.Collections.Generic;usingSystem.Text;namespacePaymentDemo{publicabstractclassPayment{privatedecimalamount;publicdecimalAmount{get{returnamount;}set{amount=value;}}publicstringgoSale(){ returnAction()+"完成,金额为"+amount+",正在查询库存状态";}publicabstractstringAction();}publicclassCashPayment:Payment{publicoverridestringAction(){return"现金支付";}}publicclassCreditPayment:Payment{publicoverridestringAction(){return"信用卡支付";}}publicclassCheckPayment:Payment{publicoverridestringAction(){return"支票支付";}}}工厂类,注意这是独立的模块。usingSystem;usingSystem.Collections.Generic;usingSystem.Text;namespacePaymentDemo{//这是一个工厂类publicclassFactory {publicstaticPaymentPaymentFactory(stringPaymentName){Paymentmdb=null;switch(PaymentName){case"现金":mdb=newCashPayment();break;case"信用卡":mdb=newCreditPayment();break;case"支票":mdb=newCheckPayment();break;}returnmdb;}}}调用:Paymentobj;privatevoidbutton1_Click(objectsender,EventArgse){obj=Factory.PaymentFactory("现金");obj.Amount=decimal.Parse(textBox1.Text);textBox2.Text=obj.goSale();}privatevoidbutton2_Click(objectsender,EventArgse){obj=Factory.PaymentFactory("信用卡");obj.Amount=decimal.Parse(textBox1.Text);textBox2.Text=obj.goSale();}privatevoidbutton3_Click(objectsender,EventArgse){obj=Factory.PaymentFactory("支票");obj.Amount=decimal.Parse(textBox1.Text);textBox2.Text=obj.goSale();} 现在需要每个类在调用方法goSale()的时候,除了完成原来的功能以外,先弹出一个对话框,显示工厂的名称,而且不需要改变来的系统,为此,在工厂类的模块种添加一个装饰类Decorator,同时略微的改写一下工厂类的代码。//这是一个工厂类publicclassFactory{publicstaticPaymentPaymentFactory(stringPaymentName){Paymentmdb=null;switch(PaymentName){case"现金":mdb=newCashPayment();break;case"信用卡":mdb=newCreditPayment();break;case"支票":mdb=newCheckPayment();break;}//returnmdb;//下面是实现装饰模式新加的代码Decoratorobj=newDecorator(PaymentName);obj.Pm=mdb;returnobj; }}//装饰类publicclassDecorator:Payment{stringstrName;publicDecorator(stringstrName){this.strName=strName;}Paymentpm;publicPaymentPm{get{returnpm;}set{pm=value;}}publicoverridestringAction(){//在执行原来的代码之前,弹出提示框System.Windows.Forms.MessageBox.Show(strName);returnpm.Action();}}这就可以在用户不知晓的情况下,也不更改原来的类的情况下,改变了性能。第五节利用策略与工厂模式实现通用的框架一、应用策略模式提升层的通用性1、意图将算法封装,使系统可以更换或扩展算法,策略模式的关键是所有子类的目标一致。2、结构策略模式的结构如下。 其中:Strategy(策略):抽象类,定义需要支持的算法接口,策略由上下文接口调用。3、示例:目标:在C#下构造通用的框架,就需要使用XML配置文件技术。构造一个一个类容器框架,可以动态装入和构造对象,装入类可以使用配置文件,这里利用了反射技术。问题:如何动态构造对象,集中管理对象。解决方案:策略模式,XML文档读入,反射的应用。我们现在要处理的架构如下:App.xml文档的结构如下。说明属性值 ………应用程序上下文接口:ApplicationContext只有一个方法,也就是由用户提供的id提供类的实例。代码:usingSystem;usingSystem.Collections.Generic;usingSystem.Text;usingSystem.Windows.Forms;usingSystem.Collections;usingSystem.Xml;usingSystem.Reflection;namespace处理类{publicinterfaceApplicationContext{ObjectgetClass(stringid);}publicclassFileSystemXmlApplicationContext:ApplicationContext{//用一个哈西表保留从XML读来的数据privateHashtablehs=newHashtable();publicFileSystemXmlApplicationContext(stringfileName){try{readXml(fileName);}catch(Exceptione){MessageBox.Show(e.ToString());}}//私有的读XML方法。privatevoidreadXml(StringfileName){//读XML把数据放入哈西表 hs=Configuration.Attribute(fileName,"class","property");}publicObjectgetClass(stringid){//由id取出内部的哈西表对象Hashtablehsb=(Hashtable)hs[id];Objectobj=null;stringm=hsb["type"].ToString();inti=m.IndexOf(",");stringclassname=m.Substring(0,i);stringfilename=m.Substring(i+1)+".dll";//利用反射动态构造对象//定义一个公共语言运行库应用程序构造块System.Reflection.AssemblyMyDll;Type[]types;MyDll=Assembly.LoadFrom(filename);types=MyDll.GetTypes();obj=MyDll.CreateInstance(classname);Typet=obj.GetType();IEnumeratorem=hsb.GetEnumerator();//指针放在第一个之前em.Reset();while(em.MoveNext()){DictionaryEntrys1=(DictionaryEntry)em.Current;if(s1.Key.ToString()!="type"){stringpname="set_"+s1.Key.ToString();t.InvokeMember(pname,BindingFlags.InvokeMethod,null,obj,newobject[]{s1.Value});}}returnobj;}}//这是一个专门用于读配置文件的类classConfiguration{publicstaticHashtableAttribute(Stringconfigname, Stringmostlyelem,Stringchildmostlyelem){Hashtablehs=newHashtable();XmlDocumentdoc=newXmlDocument();doc.Load(configname);//建立所有元素的列表XmlElementroot=doc.DocumentElement;//把所有的主要标记都找出来放到节点列表中XmlNodeListelemList=root.GetElementsByTagName(mostlyelem);for(inti=0;i124使用:ApplicationContexts;privatevoidForm1_Load(objectsender,EventArgse) {s=newFileSystemXmlApplicationContext("d:\App.xml");}privatevoidbutton1_Click(objectsender,EventArgse){AbstractPayment.Paymentm=(AbstractPayment.Payment)s.getClass("A");if(textBox1.Text!=""){m.Amount=textBox1.Text;}textBox2.Text=m.goSale();}如果需要添加两个新的实现类:CreditCheck.dllusingSystem;usingSystem.Collections.Generic;usingSystem.Text;namespaceCreditCheck{publicclassCreditPayment:AbstractPayment.Payment{publicoverridestringAction(){return"信用卡支付";}}publicclassCheckPayment:AbstractPayment.Payment{publicoverridestringAction(){return"支票支付";}}}把这个类放在当前D盘根目录下: 配置文件:1245200063000实现:ApplicationContexts;privatevoidForm1_Load(objectsender,EventArgse){s=newFileSystemXmlApplicationContext("d:\App.xml");}privatevoidbutton1_Click(objectsender,EventArgse){AbstractPayment.Paymentm=(AbstractPayment.Payment)s.getClass("A");if(textBox1.Text!=""){m.Amount=textBox1.Text;}textBox2.Text=m.goSale();}privatevoidbutton2_Click(objectsender,EventArgse){AbstractPayment.Paymentm=(AbstractPayment.Payment)s.getClass("B");if(textBox1.Text!="") {m.Amount=textBox1.Text;}textBox2.Text=m.goSale();}privatevoidbutton3_Click(objectsender,EventArgse){AbstractPayment.Paymentm=(AbstractPayment.Payment)s.getClass("C");if(textBox1.Text!=""){m.Amount=textBox1.Text;}textBox2.Text=m.goSale();}-----------------------------------------------------------------------------------------同样的原理,Java实现的Bean容器框架Bean.xml文档的结构如下。说明内容………..应用程序上下文接口:ApplicationContext.java只有一个方法,也就是由用户提供的id提供Bean的实例。packagespringdemo;publicinterfaceApplicationContext{publicObjectgetBean(Stringid)throwsException;}上下文实现类:FileSystemXmlApplicationContext.java packagespringdemo;importjava.util.*;importjavax.xml.parsers.*;importorg.w3c.dom.*;importjava.io.*;importjavax.xml.transform.dom.DOMSource;importjavax.xml.transform.stream.StreamResult;importjavax.xml.transform.*;publicclassFileSystemXmlApplicationContextimplementsApplicationContext{//用一个哈西表保留从XML读来的数据privateHashtablehs=newHashtable();publicFileSystemXmlApplicationContext(){}publicFileSystemXmlApplicationContext(StringfileName){try{readXml(fileName);}catch(Exceptione){e.printStackTrace();}}//私有的读XML方法。privatevoidreadXml(StringfileName)throwsException{//读XML把数据放入哈西表hs=Configuration.Attribute(fileName,"bean","property");}publicObjectgetBean(Stringid)throwsException{//由id取出内部的哈西表对象Hashtablehsb=(Hashtable)hs.get(id);//利用反射动态构造对象Objectobj=Class.forName(hsb.get("class").toString()).newInstance(); java.util.EnumerationhsNames1=hsb.keys();//利用反射写入属性的值while(hsNames1.hasMoreElements()){//写入利用Set方法Stringka=(String)hsNames1.nextElement();if(!ka.equals("class")){//写入属性值为字符串Stringm1="String";Class[]a1={m1.getClass()};//拼接方法的名字Stringsa1=ka.substring(0,1).toUpperCase();sa1="set"+sa1+ka.substring(1);//动态调用方法java.lang.reflect.Methodfm=obj.getClass().getMethod(sa1,a1);Object[]a2={hsb.get(ka)};//通过set方法写入属性fm.invoke(obj,a2);}}returnobj;}}//这是一个专门用于读配置文件的类classConfiguration{publicstaticHashtableAttribute(Stringconfigname,Stringmostlyelem,Stringchildmostlyelem)throwsException{Hashtablehs=newHashtable();//建立文档,需要一个工厂DocumentBuilderFactoryfactory=DocumentBuilderFactory.newInstance();DocumentBuilderbuilder=factory.newDocumentBuilder();Documentdoc=builder.parse(configname);//建立所有元素的列表Elementroot=doc.getDocumentElement();//把所有的主要标记都找出来放到节点列表中NodeListelemList=root.getElementsByTagName(mostlyelem);for(inti=0;iSpringQuickStarthello!测试:Test.javapublicclassTest{publicstaticvoidmain(String[]args)throwsException{springdemo.ApplicationContextm=newspringdemo.FileSystemXmlApplicationContext("Bean.xml");//实现类,使用标记Carspringdemo.Vehicles1=(springdemo.Vehicle)m.getBean("Car");System.out.println(s1.execute("我的"));}} 基于接口编程将使系统具备很好的扩充性。再做一个类:Train.javapackagespringdemo;publicclassTrainimplementsVehicle{privateStringmessage="";publicStringgetMessage(){returnmessage;}publicvoidsetMessage(Stringstr){message=str;}publicStringexecute(Stringstr){returngetMessage()+str+"火车在铁路上走";}} Bean.xml改动如下。SpringQuickStarthello!haha!改动一下Test.javapublicclassTest{publicstaticvoidmain(String[]args)throwsException{springdemo.ApplicationContextm=newspringdemo.FileSystemXmlApplicationContext("Bean.xml");springdemo.Vehicles1=(springdemo.Vehicle)m.getBean("Car");System.out.println(s1.execute("我的"));s1=(springdemo.Vehicle)m.getBean("Train");System.out.println(s1.execute("新的"));}} 我们发现,在加入新的类的时候,使用方法几乎不变。再做一组不同的接口和类来看一看:Idemo.javapackagespringdemo;publicinterfaceIDemo{publicvoidsetX(Stringx);publicvoidsetY(Stringy);publicdoubleSum();}实现类:Demo.javapackagespringdemo;publicclassDemoimplementsIDemo{privateStringx;privateStringy;publicvoidsetX(Stringx){ this.x=x;}publicvoidsetY(Stringy){this.y=y;}publicdoubleSum(){returnDouble.parseDouble(x)+Double.parseDouble(y);}}Bean.xml改动如下:SpringQuickStarthello!haha!2030改写Test.javapublicclassTest{publicstaticvoidmain(String[]args)throwsException{ springdemo.ApplicationContextm=newspringdemo.FileSystemXmlApplicationContext("Bean.xml");springdemo.Vehicles1=(springdemo.Vehicle)m.getBean("Car");System.out.println(s1.execute("我的"));s1=(springdemo.Vehicle)m.getBean("Train");System.out.println(s1.execute("新的"));springdemo.IDemos2=(springdemo.IDemo)m.getBean("demo");System.out.println(s2.Sum());}}------------------------------------------------------------------------------------------------------二、创建者工厂的合理应用 实例:用.NET数据提供者作为例子,讨论实现通用数据访问工厂的方法。目的:利用工厂实现与数据库无关的数据访问框架。问题:由于数据提供者是针对不同类型数据库的,造成升级和维护相当困难。现在需要把与数据库相关的部分独立出来,使应用程序不因为数据库不同而有所变化。解决方案:配置文件读取,不同提供者的集中处理,后期的可扩从性。.NET提供了一组连接对象。Connection对象用于连接数据库,它被称之为提供者。连接类对象的关系:在.NET2003,它的继承关系是这样的。当数据提供者要求非常灵活的时候,尤其是对于领域中的层设计,需要考虑更多的问题。下面的例子,希望用一个配置文件来决定提供者的参数,而实现代码不希望由于数据库的变化而变化,而且当系统升级的时候,不应该有很大的困难。配置文档:App.xml 类代码:usingSystem.Data;usingSystem.Data.Common;usingSystem.Data.OleDb;usingSystem.Data.SqlClient;usingSystem.Data.OracleClient;namespaceDbComm{//工厂接口publicinterfaceIDbFactory{stringconnectionString{get;set;}IDbConnectionCreateConnection();IDbCommandCreateCommand();DbDataAdapterCreateDataAdapter(IDbCommandcomm);} //工厂类,工厂方法模式publicclassDbFactories{publicstaticIDbFactoryGetFactory(stringname){stringproviderName=null;stringconnectionString=null;ArrayListal=configuration.Attribute("c:\App.xml","connectionStrings","add");for(inti=0;i 实现代码:privatevoidbutton4_Click(objectsender,EventArgse){DataTabledt=null;//返回一个DataTable,//其中包含有关实现DbProviderFactory的所有已安装提供程序的信dt=System.Data.Common.DbProviderFactories.GetFactoryClasses();dataGridView1.DataSource=dt;}//表示一组方法,这些方法用于创建提供程序对数据源类的实现的实例。//命名空间:System.Data.Common//程序集:System.Data(在system.data.dll中)System.Data.Common.DbProviderFactoryfactory=System.Data.SqlClient.SqlClientFactory.Instance;publicSystem.Data.Common.DbConnectionGetProConn(){System.Data.Common.DbConnectionconn=factory.CreateConnection();System.Configuration.ConnectionStringSettingspubl=System.Configuration.ConfigurationManager.ConnectionStrings["PubsData"];conn.ConnectionString=publ.ConnectionString; returnconn;}privatevoidbutton5_Click(objectsender,EventArgse){System.Data.Common.DbCommandcomm=factory.CreateCommand();comm.Connection=GetProConn();comm.CommandText="select*from奖金";comm.Connection.Open();DataTabledt=newDataTable();dt.Load(comm.ExecuteReader());comm.Connection.Close();dataGridView1.DataSource=dt;}//与提供者无关的数据访问privatevoidSeeData(stringprotext,stringtablename){System.Configuration.ConnectionStringSettingspubl=System.Configuration.ConfigurationManager.ConnectionStrings[protext];System.Data.Common.DbProviderFactoryfactory=System.Data.Common.DbProviderFactories.GetFactory(publ.ProviderName);System.Data.Common.DbConnectionStringBuilderbld=factory.CreateConnectionStringBuilder();bld.ConnectionString=publ.ConnectionString;System.Data.Common.DbConnectioncn=factory.CreateConnection();cn.ConnectionString=bld.ConnectionString;System.Data.Common.DbDataAdapterda=factory.CreateDataAdapter();System.Data.Common.DbCommandcmd=factory.CreateCommand();cmd.CommandText="select*from"+tablename;cmd.CommandType=CommandType.Text;cmd.Connection=cn;da.SelectCommand=cmd;System.Data.Common.DbCommandBuildercb=factory.CreateCommandBuilder();cb.DataAdapter=da;DataSetds=newDataSet();da.Fill(ds,"auth");dataGridView1.DataSource=ds;dataGridView1.DataMember="auth";}privatevoidbutton6_Click(objectsender,EventArgse) {SeeData("PubsData","奖金");}privatevoidbutton7_Click(objectsender,EventArgse){SeeData("Nothing","Orders");}利用上面的讨论过的原理,还可以把代码进一步封装,形成一个基于领域的层,再给予接口编程的原则下,系统的升级性能是非常好的。三、单件模式的应用问题有时候,我们需要一个全局唯一的连接对象,这个对象可以管理多个通信会话,在使用这个对象的时候,不关心它是否实例化及其内部如何调度,这种情况很多,例如串口通信和数据库访问对象等等,这些情况都可以采用单件模式。1、意图单件模式保证应用只有一个全局唯一的实例,并且提供一个访问它的全局访问点。2、使用场合当类只能有一个实例存在,并且可以在全局访问的时候,这个唯一的实例应该可以通过子类实现扩展,而且用户无需更改代码即可以使用。3、结构单件模式的结构非常简单,包括防止其它对象创建实例的私有构造函数,保持唯一实例的私有变量和全局变量访问接口等,请看下面的例子:usingSystem;namespace单件模式{publicclassCShapeSingletion{privatestaticCShapeSingletionmySingletion=null;//为了防止用户实例化对象,这里把构造函数设为私有的privateCShapeSingletion(){}//这个方法是调用的入口publicstaticCShapeSingletionInstance(){if(mySingletion==null){mySingletion=newCShapeSingletion();}returnmySingletion; }privateintintCount=0;//计数器,虽然是实例方法,但这里的表现类同静态publicintCount(){intCount+=1;returnintCount;}}}privatevoidbutton1_Click(objectsender,System.EventArgse){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代码:usingSystem;usingSystem.IO;usingSystem.Xml;usingSystem.Collections;namespace代理一{//这是统一的接口publicabstractclassAbstractOrderItem{privatestringm_GoodName;privatelongm_Count;privatedoublem_Price;publicvirtualstringGoodName {get{returnm_GoodName;}set{m_GoodName=value;}}publicvirtuallongCount{get{returnm_Count;}set{m_Count=value;}}publicvirtualdoublePrice{get{returnm_Price;}set{m_Price=value;}}//价格求和,这个计算方式是另外的人编写的publicabstractdoubleGetTotalPrice();}//处理订单的代码publicclassOrder{publicstringName;publicDateTimeOrderDate;privateArrayListoitems;publicOrder(){ oitems=newSystem.Collections.ArrayList();}publicvoidAddItem(AbstractOrderItemit){oitems.Add(it);}publicvoidRemoveItem(AbstractOrderItemit){oitems.Remove(it);}publicdoubleOrderPrice(){AbstractOrderItemit;doubleop=0;for(inti=0;iNET 天然的支持观察者模式。下面我们用一个最简单的例子,说明它是如何处理问题的。首先我们必须定义一个委托,这个委托事实上提供了对法方法的指针调用,也定义了被调用的方法必须遵循的形式:publicdelegatevoidLoopEvent(inti,intj);下面是例子,请注意代码中的注释。usingSystem;usingSystem.Windows.Forms;usingSystem.Drawing;namespace观察者{//主题,相当于SubjectpublicclassDoLoop{publicdelegatevoidLoopEvent(inti,intj);publiceventLoopEventle;publicvoidBeginLoop(intLoopNumber){for(inti=0;i=0;i--){lsAll.Items.Remove(lsAll.SelectedItems[i]);}}privatevoidbtRemove_Click(objectsender,System.EventArgse){for(inti=0;i=0;i--){ lsSelected.Items.Remove(lsSelected.SelectedItems[i]);}}需要注意的问题是,Windows本身就是一个中介者,所以大多数情况下并没有必要再定义一个中介者。之所以提出这个问题,是因为我们看到大多数介绍中介者模式的文章,均以人机界面为例子,但他们往往用实例生成ListBox和Button的子类,使生成的子类互相调用,然后又做一个Mediator解耦。但ListBox和Button本身并没有耦合性,生成一个子类人为地加入耦合性显然是多此一举,而且增加了程序调试的难度,这是一种设计过度的典型的例子。这样的程序不但没有提高性能,相反更难维护。之所以如此,是因为很多人受GoF的《设计模式》书中列举的那个例子影响,加上读书不求甚解,依猫画虎,其实这是学习设计模式最忌讳的事情。我们应该知道,《设计模式》形成的那个年代,很多待解决的问题在当前大多数平台上都得到解决了,时代在发展,我们不需要再走回头路。六、设计模式的发展委托可以使方法接口做为最小的接口单元使用,从而避免了不必要的继承,减少了类的层次,这也是用组合代替继承的最好的体现。但是,委托也使设计更难理解,因为对象是动态建立的。随着技术的进步,设计模式是需要随着时代的进步而进步的。从某种意义上讲,GoF的“设计模式”从概念上的意义,要超过对实际开发上的指导,很多当初难以解决的问题,现在已经可以轻而易举的解决了,模式的实现更加洗练,这就是我们设计的时候不能死搬硬套的原因。 第十节C语言嵌入式系统应用设计模式的讨论上面关于软件架构的讨论,特别是在详细设计的过程,完全是基于面向对象的技术的,显而易见,利用面向对象的基本概念,像封装性、继承性尤其是多态性,可以大大提升系统的可升级可维护性能,成为现代架构设计的主体。但是,C语言嵌入式系统编程中,软件架构的理论能适用吗?这也是许多人感觉困惑的问题。其实仔细研究面向对象开发的本质,可以发现在面向过程的语言体系中,完全可以应用面向对象的思想,下面谈得仅仅是我个人的看法,供参考。一、利用指针调用函数实现多态性仔细研究一下上面关于委托应用的问题,也可以发现利用指针,完全可以实现基于接口编程的多态性的,而23个模式本质上是利用了多态性。调用函数的指针在声明的时候,本质上是规定了被调函数的格式,这和接口的作用是一样的。例:有两个方法max和min:intmax(intx,inty){intz;if(x>y)z=x;elsez=y;return(z);}intmin(intx,inty){intz;if(x