发布新日志

  • HEAD IN DESIGN PATTERNS读书笔记——DECORATOR 模式

    2007-06-26 21:36:59

    第三个PATTERN——DECORATOR

    StarBuzz Coffee最近发展的非常迅速,他们决定更新他们的饮料订单系统。

    他们开始提出的类图是这样的:

    然而除了咖啡之外,你可能还会要一些调料,比如牛奶、豆汁、摩卡等等,StarBuzz Coffee会对这些收取一点费用。而订单系统需要根据没一种需求指定项目并计算总价。于是按照上面的方案最终导致了类的数量的爆炸。

    也许你会说,为什么需要那么多的类,可以把调料作为成员变量加到基类中,子类继承得到这些调料的信息。那么试试看:

    现在我们在Beverage类里实现cost()方法(而不是让它作为抽象方法),这样它可以计算一个具体的饮料实例的调料的花费。子类仍然会重写cost(),但它们依然会调用超类的版本,这样它们就可以计算基本的饮料加上调料的总花费。

    但是有一系列因素的变化会影响这个设计:

    调料价格的变化会迫使我们修改现有的代码

    新的调料会迫使我们在超类中增加新方法以及修改cost()方法。

    我们可能会有一些新的饮料类型,对于其中的一些饮料,可能使用调料是不恰当的,但是它们还是会继承hasWhip()这样的方法(就像第一章开始的例子中橡皮鸭继承fly()方法一样)。

    如果客户要求双份的牛奶怎么办?

    下面是我们改变的设计:我们将由一个饮料开始,在运行时用调料来“装饰”它。例如,客户需要一个有摩卡和蛋奶的Dark Roast,我们的步骤将是:

    1.       得到DarkRoast对象

     

    2.       Mocha对象来装饰它

    Mocha对象是一个decorator,它的类型镜像了它所装饰的类型——Beverage(此处的镜像指它们是同一种类型)。因此,Mocha也有一个cost()方法。

    3.       Whip对象来装饰它

    4.       调用cost()方法,依靠委派来增加调料的花费

    首先调用最外层Whip对象的cost()方法,它调用Mocha对象的cost()方法,Mocha对象调用DarkRoast对象的cost()方法。

    DarkRoast对象返回它的花费,99美分,Mocha对象加上它的花费20美分,返回总值共1.19美元,Whip对象加上它的花费10美分,返回总值1.29美元

     

    l        Decorator和被它装饰的对象有着同样的超类。

    l        可以使用一个或多个decorator来包装一个对象

    l        既然decorator和被它装饰的对象有着同样的超类,在使用一个原始(被包装的)对象的地方,我们可以传递一个装饰了的对象。

    l        Decorator在把方法委派给它装饰的对象之前或者之后,把它自己的行为加上去来完成剩余的工作(这一点很关键!)

    l        对象可以在任何时候被装饰,因此我们可以在运行时用任意多个decorator来装饰它。

     

    现在我们来看DECORATOR PATTERN的描述:

           Decorator Pattern动态地为一个对象附加额外的职责。Decorators提供了区别于通过子类化来扩展功能的另一种解决方案。

          

     

    现在我们的Starbuzz饮料系统变成下面的框架:

    这里CondimentDecorator继承Beverage类是为了获得类型的匹配,而不是通过继承来获得行为(behavīor)。行为是通过其中包含的基本的component以及其他decorator的组合来获得的。

    如果依靠继承,我们的行为只能在编译时静态地决定。通过composition,我们可以在运行时任意地混合使用decorators

    Decorator模式的一个实例是Java中的I/O类。FileInputStream, BufferedInputStream, LineNumberInputStream——看着眼熟吧。


  • HEAD IN DESIGN PATTERNS读书笔记——STRATEGY模式

    2007-06-24 10:06:05

    1. 第一个PATTERN: STRATEGY PATTERN

    应用程序:小鸭模拟程序(MiniDuckSimulator)

    问题的提出:

     

           初始的情况是这样的:Duck类中包含了quack()swim()方法,以及一个抽象方法display()Duck实际上为抽象类)。MallardDuckRedheadDuck以及其他许多鸭子类都继承Duck类。

    之后增加了一个新的需求,就是在这个鸭子模拟系统中需要增加会飞的鸭子。程序员Joe认为只要在Duck类中增加一个fly()方法就可以了。

    但事实并不是这么简单。上面代码的直接结果是橡皮鸭也开始在屏幕上飞来飞去了,而这是不合逻辑的。事实上上面代码等于把fly()方法赋予了所有Duck类的子类,而其中许多都是不会飞的鸭子。

    一种解决方式是在子类中修改fly()的行为,将橡皮鸭的fly()方法改成什么也不做。但同时存在另一些情况比如木头鸭既不会叫也不会飞。同时Joe知道接下来每半年要更新一次产品加入一些新的变更,他需要不断的为每一个新加入的子类修改fly()quack()的实现……

    于是Joe想到了使用接口,把fly()方法和quack()方法从Duck类中分离出来,抽象成两个接口FlyableQuackable

    但是这样的代码可重用性很差,因为即便是能飞的鸭子,飞行的行为也可能不只一种。

     

    解决方案:

    这里Duck类通过performQuack()performFly()方法把quack()fly()代理给了两个接口类,可以在运行时为Duck的子类设置具体的行为类,此时根据子类中包含的具体行为类对象决定具体的fly(),quack()行为。

    使用HAS-A的组成来创建系统提供了很大的灵活性。不仅可以把封装一组算法封装进自身的一组类,而且可以在运行时改变行为,只要用来组成系统的对象实现了正确的行为类。

  • [转]面向对象的设计原则-类设计原则

    2007-06-23 09:43:12

    作者:中国系统分析员顾问团高级顾问 张华 来自:CSAI.cn

    在面向对象设计中,如何通过很小的设计改变就可以应对设计需求的变化,这是令设计者极为关注的问题。为此不少OO先驱提出了很多有关面向对象的设计原则用于指导OO的设计和开发。下面是几条与类设计相关的设计原则。

    1. 开闭原则(the Open Closed Principle OCP)

      一个模块在扩展性方面应该是开放的而在更改性方面应该是封闭的。因此在进行面向对象设计时要尽量考虑接口封装机制、抽象机制和多态技术。该原则同样适合于非面向对象设计的方法,是软件工程设计方法的重要原则之一。
    我们以收音机的例子为例,讲述面向对象的开闭原则。我们收听节目时需要打开收音机电源,对准电台频率和进行音量调节。但是对于不同的收音机,实现这三个步骤的细节往往有所不同。比如自动收缩电台的收音机和按钮式收缩在操作细节上并不相同。因此,我们不太可能针对每种不同类型的收音机通过一个收音机类来实现(通过重载)这些不同的操作方式。但是我们可以定义一个收音机接口,提供开机、关机、增加频率、降低频率、增加音量、降低音量六个抽象方法。不同的收音机继承并实现这六个抽象方法。这样新增收音机类型不会影响其它原有的收音机类型,收音机类型扩展极为方便。此外,已存在的收音机类型在修改其操作方法时也不会影响到其它类型的收音机。
    图1是一个应用OCP生成的收音机类图的例子:



    图1 OCP应用(收音机)

    2. 替换原则 (the Liskov Substitution Principle LSP)

      子类应当可以替换父类并出现在父类能够出现的任何地方。这个原则是Liskov于1987年提出的设计原则。它同样可以从Bertrand Meyer 的DBC (Design by Contract) 的概念推出

      我们以学生为例,夜校生为学生的子类,因此在任何学生可以出现的地方,夜校生均可出现。这个例子有些牵强,一个能够反映这个原则的例子时圆和椭圆,圆是椭圆的一个特殊子类。因此任何出现椭圆的地方,圆均可以出现。但反过来就可能行不通。

      Liskov的相关图示见图2:



    图2 Liskov 原则

      运用替换原则时,我们尽量把类B设计为抽象类或者接口,让C类继承类B(接口B)并实现操作A和操作B,运行时,类C实例替换B,这样我们即可进行新类的扩展(继承类B或接口B),同时无须对类A进行修改。

    3. 依赖原则 (the Dependency Inversion Principle DIP)

      在进行业务设计时,与特定业务有关的依赖关系应该尽量依赖接口和抽象类,而不是依赖于具体类。具体类只负责相关业务的实现,修改具体类不影响与特定业务有关的依赖关系。

      在结构化设计中,我们可以看到底层的模块是对高层抽象模块的实现(高层抽象模块通过调用底层模块),这说明,抽象的模块要依赖具体实现相关的模块,底层模块的具体实现发生变动时将会严重影响高层抽象的模块,显然这是结构化方法的一个"硬伤"。

      面向对象方法的依赖关系刚好相反,具体实现类依赖于抽象类和接口(见图-3)。

      为此,我们在进行业务设计时,应尽量在接口或抽象类中定义业务方法的原型,并通过具体的实现类(子类)来实现该业务方法,业务方法内容的修改将不会影响到运行时业务方法的调用。



    图3依赖原则图示

    4. 接口分离原则(the Interface Segregation Principle ISP)

      采用多个与特定客户类有关的接口比采用一个通用的涵盖多个业务方法的接口要好。

      ISP原则是另外一个支持诸如COM等组件化的使能技术。缺少ISP,组件、类的可用性和移植性将大打折扣。

      这个原则的本质相当简单。如果你拥有一个针对多个客户的类,为每一个客户创建特定业务接口,然后使该客户类继承多个特定业务接口将比直接加载客户所需所有方法有效。

      图4展示了一个拥有多个客户的类。它通过一个巨大的接口来服务所有的客户。只要针对客户A的方法发生改变,客户B和客户C就会受到影响。因此可能需要进行重新编译和发布。这是一种不幸的做法。



    图4 带有集成接口的服务类

      我们再看图-5中所展示的技术。每个特定客户所需的方法被置于特定的接口中,这些接口被Service类所继承并实现。


    图5 使用接口分离的服务类设计

      如果针对客户A的方法发生改变,客户B和客户C并不会受到任何影响,也不需要进行再次编译和重新发布。

      以上四个原则是面向对象中常常用到的原则。此外,除上述四原则外,还有一些常用的经验诸如类结构层次以三到四层为宜、类的职责明确化(一个类对应一个具体职责)等可供我们在进行面向对象设计参考。但就上面的几个原则看来,我们看到这些类在几何分布上呈现树型拓扑的关系,这是一种良好、开放式的线性关系、具有较低的设计复杂度。一般说来,在软件设计中我们应当尽量避免出现带有闭包、循环的设计关系,它们反映的是较大的耦合度和设计复杂化。

     

  • UML学习笔记(五)

    2007-06-15 18:27:18

    1. 聚集是强关联,它是整体与部分之间的关系。在UML中,聚集显示为连接两个类的直线,整体端画一个菱形。

    2. 和关联关系一样,聚集可以自反。类A的一个实例,由同为A的一个或几个其他实例构成。

    3. 对聚集关系生成代码时,ROSE生成支持聚集的属性。

    4. 泛化关系是两个类之间的继承关系。

    5. ROSE对关联和聚集关系产生属性。Static字段确定生成的属性是否静态的。如果将一个角色(Role)设置成静态的,则产生的关联属性为静态的。

    6. 链接元素(Link Element)也称为关联类,可以放置与关联相关的属性。例如两个类Student和Course,则Grade可作为关联类。

  • UML学习笔记(四)——依赖关系

    2007-06-15 16:23:27

    1. 对存在依赖关系的两个类生成代码时,并不对关系的类增加属性。但产生支持关系所需的特定语句。在C++中,生成代码中会包括必要的#include语句。

    例如类A依赖于类B,类A没有B属性,因此要用其他方法查找B。有三种方法:

    • 如果B是全局的,则类A知道它存在。
    • 如果B实例化为类A操作中的本地变量,则类A知道它存在。
    • 如果B作为参数传递到类A中,则类A知道它存在。

    在依赖关系中,必须采用这三种方法之一。

    关联于依赖的第二个差别在于方向,关联可以是双向的,而依赖只能是单向的。

    2. 包之间同样存在依赖性。例如包A依赖于包B。则不能直接在另一个应用程序中复用A包,而要同时复用B包。而B包更容易复用,因为它没有依赖于其他包。

    要确定包依赖性关系,就要检查Class框图中的关系。如果不同包中的类之间有关系,则包也有关系。

    生成包依赖关系时,要尽量避免循环依赖性。要避免循环依赖,可以把一个包一分为二。

  • UML学习笔记(三)——关联关系

    2007-06-15 16:11:31

    1. 类之间可以建立四种关系:关联、依赖、聚集(Aggregation)和泛化(Generalization)。

    2. 关联可以是双向的,也可以是单向的.

    3. 依赖总是单向的,显示一个类依赖于另一个类的定义。依赖用虚线箭头表示。

    4. 聚集是强关联。聚集关系是整体和个体间的关系。

    5. 泛化显示类之间的继承关系

    6. 通过Sequence或Collaboration框图可以确定关联方向。如果Interaction框图中总是类A向类B发消息,则是类A到类B的单向关系。如果又有类B到类A的消息,则需要双向关系。

    单向关系更容易建立和维护,有助于寻找可复用的类。如果类A和类B之间的关系是双向的,则每个类都需要知道对方,因此两者都不能复用。但假设是从类A到类B的单向关系,则类A需要知道类B,没有类B就无法复用,而类B不需要知道类A,因此类B是可以复用的。

    任何输出多个单向关系的类都很难复用,而只接收单向关系的类则很容易复用。

    7. 关联也可以自反。自反关联让类的一个实例同该类的其他实例相联系。

  • UML学习笔记(二)

    2007-06-15 16:10:36

    1. 边界类:边界类位于系统与外界的交界处,包括所有窗体、报表、与打印机和扫描仪等硬件的接口、以及与其他系统的接口。

    要寻找边界类,可以检查Use Case框图。每个角色/用例交互至少要有一个边界类。边界类使角色与系统交互。

    2. 实体类:实体类保存要放进持续存储体的信息。实体类通常在事件流和Interaction框图中,是对用户最有意义的类。实体类中的每一个字段都是数据库结构中的字段。

    3. 控制类:控制类负责协调其他类的工作。也称为管理者类。

  • UML学习笔记(一)

    2007-06-14 22:54:17

    1.不要在两个用例之间画箭头(除了使用与扩展关系)

    2. UML将使用关系显示为箭头和<<uses>>字样,被使用的用例为抽象用例。

    3. 扩展关系也是用箭头表示,注明<<extends>>字样,扩展的用例为抽象用例(比如ExpressWithdraw扩展了Withdraw,则ExpressWithdraw提供了扩展的功能,为抽象用例)

    具体用例和抽象用例之间存在一个区别。具体用例由主角来启动,并且构成一个完整的事件流。“完整”意味着该用例的一个实例执行由主角调用的全部操作。

          抽象用例本身从来不会被实例化。抽象用例包括在(请参阅指南:包含关系)其他用例中,扩展到(请参阅指南:扩展关系)或泛化关系(请参阅指南:用例泛化关系)其他用例。在启动一个具体用例时,也就创建了该用例的一个实例。这一实例还展示了由其关联关系的抽象用例指定的活动。因而,从抽象用例中无法创建单独的实例。

Open Toolbar