设计模式:设计模式·转

关于要掌握的设计模式
一、为什么使用设计模式
对任何设计都可以凭主观(对设计很难做出客观评价)判断得出它是一个好的设计,还是一个坏的设计。使用设计模式是为了避免坏的设计。

目前拙劣设计的症状:
· 僵化性(Rigidity):设计难以改变
· 脆弱性(Fragility):设计易于遭到破坏
· 牢固性(Immobility):设计难以重用
· 粘滞性(Viscosity):难以做正确的事情。
· 不必要的复杂性(Needless Complexity):过分设计
· 不必要的重复(Needless Repetition):过多的重复
· 晦涩性(Opacity):混乱的表达
二、什么时候使用设计模式
  使用设计模式的目的是为了适应未来的变化,变化之所以存在是因为它的不可预知性——如果可以预知,则不能称其为变化。如何判断哪些需求可能变化,哪些需求可能不变,并且在最大程度上保持设计的干净、简单,这些是工艺问题,而不是工程问题。既然是工艺问题,那么就只能给出原则,不能给出标准。使用设计模式的大体原则可能是:对未来极有可能发生变化的问题给出最简单、修改成本最低的解决方案。

三、面向对象设计的基本原则
  一个良好的面向对象设计需要遵循一些基本原则,如单一职责原则(SRP)、开放-封闭原则(OCP)、Liskov替换原则(LSP)、依赖倒置原则(DIP)、接口分离原则(ISP)等。

1、“开放-封闭”(OCP)原则

  描述:“对于扩展是开放的(Open for extension)。”-- 这意味着模块的行为是可以扩展的。当应用的需求改变时,可以对模块进行扩展,使其具有满足改变的新行为。也就是说,我们可以改变模块的功能。
     “对于更改是封闭的(Close for modification)。”-- 对模块行为进行扩展时,不必改动模块的源代码或者二进制代码。
  应用:高级语言中的接口与虚拟类。
  带来的好处:提高灵活性、可重用性、可维护性。
  个人观点:OCP的关键是抽象化。抽象化就是:找到一个系统的可变因素,将之封装起来。抽象的目的是:创建一个固定却能够描述一组任意个可能行为的基类。而这一组可能的行为则表现为派生类。对于基类的更改是封闭的,所以它里边的方法一旦确定就不能更改(对接口里的方法进行更改将带来灾难性的后果)。模块通过抽象基类进行引用,对派生类的扩展并不影响整个模块,所以它是开放的。遵循OCP的代价也是昂贵的,创建正确的抽象是要花费开发时间和精力的,同时抽象也增加了软件设计的复杂性。因此有效的预知变化是OCP设计的要点,这需要我们进行适当的调查,提出正确的问题,并利用我们的经验和一般常识来做出判断。正确的做法是,只对程序中频繁变化的部分做出抽象,拒绝不成熟的抽象和抽象本身一样重要。
  体现模式:简单工厂模式、工厂方法模式、抽象工厂模式、建造模式、桥梁模式、门面模式、调停者模式、访问者模式、迭代子模式。
2、里氏代换(LSP)原则

  描述:若对每个类型S的对象O1,都存在一个类型T的对象O2,使得在所有针对T编写的程序P中,用O1替换O2后,程序P行为功能不变,则S是T的子类型。
  应用:在实现继承时,子类型(subtype)必须能替换掉它们的基类型(base type)。如果一个软件实体使用的是基类的话那么也一定适用于子类。但反过来的代换不成立。
  个人观点: LSP是使OCP成为可能的主要原则之一,对LSP的违反将导致对OCP的违反,同时二者是OOD中抽象和多态的理论基础,在OOPL中表现为继承。在高级语言(JAVA、C#)中,只要我们严格按照接口和虚拟类的语法规范来做就能很好遵循此原则,另外我们还应该避免一些更微妙的违规情况。举个例子,正方形和矩形,矩形可以做为正方形的基类,因为正方形也是一种矩形,但对于正方形来说,setWidth()和setHeight()是冗余的,且容易引起错误,这样的设计就违反了LSP原则。如果有两个具体类A和B之间的关系违反了LSP,可以在以下两种重构方案中选择一种:
  A) 创建一个新的抽象类C,作为两个具体类的超类,将A和B共同的行为移动到C中,从而解决A和B行为不完全一致的问题。
  B) 从B到A的继承关系改写为委派关系。
  体现模式:策略模式、合成模式、代理模式
3、依赖倒转(DIP)原则

  描述:A、高层模块不应该依赖于低层模块,二者都应该依赖于抽象。
     B、抽象不应该依赖于细节,细节应该依赖于抽象。
  应用:要依赖抽象,不要依赖于具体。即针对接口编程,不要针对实现编程。针对接口编程的意思是,应当使用接口和抽象类进行变量的类型声明、参量的类型声明,方法的返还类型声明,以及数据类型的转换等。不要针对实现编程的意思就是说,不应当使用具体类进行变量的类型声明、参量的类型声明,方法的返还类型声明,以及数据类型的转换等。
  结论:DIP虽然强大,但却不易实现,因为依赖倒转的缘故,对象的创建很可能要使用对象工厂,以避免对具体类的直接引用,此原则的使用将导致大量的类文件。给维护带来不必要的麻烦。所以,正确的做法是只对程序中频繁变化的部分进行依赖倒置。
  体现模式:工厂方法模式、模版方法模式、迭代子模式
4、接口隔离(ISP)原则

  描述:不要强迫客户依赖于它们不用的方法。
  应用:一个类对另外一个类的依赖性应当是建立在最小的接口上的。如果客户端只需要某一些方法的话,那么就应当向客户端提供这些需要的方法,而不要提供不需要的方法。提供接口意味着向客户端作出承诺,过多的承诺会给系统的维护造成不必要的负担。
  结论:使用多个专门的接口比使用单一的接口要好 -- 定制服务。
  如何实现接口隔离原则:
   1)利用委托分离接口。
   2)利用多继承分离接口。
  体现模式:备忘录模式、迭代子模式
5、合成/聚合复用原则(CARP)

  描述:在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新对象通过向这些对象的委派达到复用已有功能的目的。简而言之,要尽量使用合成/聚合,尽量不要使用继承。
  应用:合成(Composition)和聚合(Aggregation)均是关联(Association)的特殊种类。聚合用来表示“拥有”关系或者整体与部分的关系(共性关系);而合成则用来表示一种强得多的“拥有”关系(个性关系)。更进一步来讲,一个合成的多重性不能超过1,换言之,一个合成关系中的成分对象是不能与另一个合成关系共享。一个成分对在一个时间内只能属于一个合成关系。
  在面向对象的设计里,有两种基本的办法可以在不同的环境中复用已有的设计和实现,即通过合成/聚合或通过继承。
  由于合成或聚合可以将已有的对象纳入到新对象中,使之成为新对象的一部分,因此新的对象可以调用已有对象的功能。这样的好处如下:
  A、新对象存取成分对象的唯一方法是通过成分对象的接口。
  B、这种复用是黑箱复用,因为成分对象的内部细节是新对象所看不见的。
  C、这种复用支持包装。
  D、这种复用所需的依赖较少。
  E、每一个新的类可以将焦点集中在一个任务上。
  F、这种复用可以在运行时间内动态进行,新对象可以动态地引用与成分对象类型相同的对象。
  一般而言,如果一个角色得到了更多的责任,那么可以使用和合成/聚合关系将新的责任委派到合适的对象。这种复用的缺点就是通过使用这种复用建造的系统会有较多的对象需要管理。
  继承复用的优点:
  · 新的实现较为容易,因为超类的大部分功能都可以通过继承关系自动进入子类。
  · 修改或扩展继承而来的实现较为容易。
  继承复用的缺点:
  · 继承复用破坏包装,因为继承将超类的实现细节暴露给子类。因为超类的内部细节常常是对子类透明的,因此这种复用是透明的复用,又称“白箱”复用。
  · 如果超类的实现发生改变,那么子类的实现也不得不发生改变。
  · 从超类忌辰而来的实现是静态的,不可能在运行时间内发生改变,因此没有足够的灵活性。
  一般来说,对违反里氏代换原则的设计进行重构时,可以采取两种办法:
  一是:加入一个抽象超类;二是:将继承关系改写为合成/聚合关系。
  “IS-A”是严格的分类学意义上的定义,意思是一个类是另一个类的“一种”。而“HAS-A”则不同,它表示某一个角色具有某一项责任。
  体现模式:
6、迪米特(LoD)法则

  描述: 一个软件实体应当尽可能少的与其他实体发生相互作用。 这样,当一个模块修改时,就会尽量少的影响其他的模块。扩展会相对容易。
  定义:迪米特法则又叫作最少知识原则(LeastKnowledgePrinciple或简写为LKP),就是说一个对象应当对其他对象有尽可能少的了解。 迪米特法则的主要用意是控制信息的过载。这是对软件实体之间通信的限制。它要求限制软件实体之间通信的宽度和深度。
  迪米特法则与设计模式:门面(外观)模式和调停者(中介者)模式实际上就是迪米特法则的具体应用。
总而言之:
  遵循以上原则,可以使我们的软件更具灵活性,强壮性。但灵活是需要付出代价的,由多态带来的性能损失就是最明显的一个问题。所以我们需要权衡,需要做出选择,在灵活与性能之间做出选择。
  追本溯源,促使我们使用这些原则的原因是为了满足需求的变更,于是需求分析就显得格外重要。然而不管怎么充分的需求分析都可能遭遇需求变更,于是预测变化就成了一个让人头痛的事。还是让我们来看看敏捷设计(XP)是怎么解决这些问题的:“敏捷开发人员不会对一个庞大的预先设计应用那些原则和模式,相反,这些原则和模式被应用在一次次的迭代中,力图使代码以及代码所表达的设计保持干净。”也就是说敏捷设计通过快速的迭代来刺激变化,让这些变化及早暴露,再根据变化进行相应改动。很明显这要比一次性完整设计轻松容易的多。
  软件开发的全部艺术就是权衡:在简单与复杂之间权衡,在一种方案与另一种方案之间权衡。如果把每个问题、每个权衡的利弊都考虑得清清楚楚,恐怕开发一个应用程序的成本会高得惊人。所以,很多时候我们更依赖自己的审美眼光,用平静的心去设计一个赏心悦目的系统。
四、应掌握的模式
  01、简单工厂(Factory)模式
  02、工厂方法(Factory Method)模式:工厂方法模式的应用情景明确,设计思想简单。我习惯使用只有静态方法的工厂方法模式。
  03、抽象工厂(Abstract Factory)模式:抽象工厂是工厂模式的推广。抽象工厂模式的应用情景更加特殊和严格。在使用抽象工厂模式之前,一定要保证从现在到未来都能够用一致的方式使用这些产品族。
  04、单例(Singleton)模式:单例(Singleton)模式和工厂模式关系密切。从实现的角度讲,单例模式是工厂模式的一个特例,但是两个模式的应用情景不同,因此它们属于不同的模式。
  05、建造(Builder)模式:将工厂模式稍加变化可以得到建造(Builder)模式。工厂模式的“加工工艺”是隐藏的,而建造模式的“加工工艺”是暴露的。这点不同,使建造模式在更加灵活的同时也有失优雅。
◆ 模板模式和策略模式的应用情景类似,但实现方式不同,前者使用继承,后者使用委托。模板模式和策略模式通常可以互相替换。它们都像试卷,模板模式是填空题,策略模式是选择题。
  06、模板(Template Method)模式:模板模式和策略模式的应用情景类似,但实现方式不同,前者使用继承,后者使用委托。模板模式的缺点是把具体实现和通用算法紧密地耦合起来,使得具体实现只能被一个通用算法操纵。
  07、策略(Strategy)模式:策略模式是委托的经典用法。策略模式消除了通用算法和具体实现的耦合,使得具体实现可以被多个通用算法操纵。策略模式也增加了类层次,比模板模式复杂。
◆ 下面几个模式对使用者而言,都在一定程度上起到了简化问题的作用。
  08、门面(Facade)模式:把一组复杂的接口隐藏在一个简单且特定的接口后面。
  09、调停者(Mediator)模式:把对象之间的引用关系包装在一个特定的容器里面。
  10、合成(Composite)模式:描述了整体与部分的结构关系,并且允许用一致的方式处理这个结构。
◆ 访问者(Visitor)模式和装饰(Decorator)模式都可以在不改变现有类结构的基础上,动态地增加功能。
◆ 访问者模式和装饰模式在实际效果上是不同的。访问者模式可以把对象分配到相应的方法里,从而对每个对象分别进行加工或扩展。而装饰模式只能用一致的方式对所有的被装饰对象进行加工或扩展,要想实现不同的加工或扩展,只能增加新的装饰类。
◆ 过多的“装饰类”有可能使业务逻辑分散,并且使程序结构复杂。针对每一个具体的派生类,“访问类”都要有一个对应的方法,增加派生类的时候也要增加访问类的方法。扩展功能的需求是经常发生的,是否有必要使用上述模式则值得再三考虑。
  11、装饰(Decorator)模式:把现有类结构上的对象“注入”一个装饰类中,在装饰类中扩展它的功能。
  12、访问者(Visitor)模式:把现有类结构上的对象“分配”到一个名为访问者的类中,在访问者的相应方法中配置对象、改变对象或扩展功能。
◆ 其他常用的模式
  13、适配器(Adapter)模式:是常用模式,它比较简单,有时和其他的模式配合使用。
  14、桥梁(Bridge)模式:Class是封装了行为和属性的容器,然而Class的一组行为可能独立演化,这时最直接的想法是使用继承,把各不相同的行为封装在不同的子类里。桥梁模式从另外的角度解决了这个问题。桥梁模式把独立演化的行为封装在另外一个类体系里,与原来的类体系分别独立演化,两个类体系在抽象层次是“使用”关系。在很多OO教材里面用Shape类封装属性和Draw方法,在桥梁模式里,“形状”和“画笔”是两组独立演化的类体系,在抽象层次,“形状”使用“画笔”绘制自己。
  15、命令(Command)模式:被Martin称为“最简单、最优雅的模式之一”。命令模式的魅力在于它为每个类“培训”出了相同的技能,经过“培训”的类“柔性”更强,能够产生不可思议的能力。
◆ 下面是一些不太需要的模式
  16、观察者(Observer)模式。Java和C# 都实现了观察者模式。
  17、迭代子(Iterator)模式。在Java和C#语言里,可以用聚集类代替。
  18、备忘录(Memento)模式。可以用Class的序列化能力代替。
  19、责任链(Chain of Responsibility)模式。可以用其他的方
Tags:  软件设计模式 java设计模式 大话设计模式 设计模式

延伸阅读

最新评论

发表评论