Hexo

点滴积累 豁达处之

0%

01 设计模式简介

设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。

1 设计模式类型

1.1 创建型

单例模式、工厂模式、抽象工厂模式、原型模式、建造者模式;

设计模式-创建型

1.2 结构型

代理模式,装饰器模式、适配器模式、外观模式、组合模式、享元模式、桥梁模式;

设计模式-结构型

1.3 行为型

策略模式、责任链模式、命令模式、中介者模式、模板方法模式、迭代器模式、访问者模式、观察者模式、解释器模式、备忘录模式、状态模式。

设计模式-行为型

2 设计模式的特点

目的 创建型模式 (Creational) 结构型模式 (Structural) 行为型模式 (Behavioral)
概念 创建型模式,就是创建对象的模式,抽象了实例化的过程。它帮助一个系统独立于如何创建,组合和表示它的那些对象。关注的是对象的创建,创建型模式将创建对象的过程进行了抽象,也可以理解为将创建的过程进行了封装,作为客户程序仅仅需要去使用对象,而不再关心创建对象过程中的逻辑 结构型模式是为解决怎样组装现有的类,设计他们的交互方式,从而达到实现一定的功能目的,结构型模式包含了对很多问题的解决。例如:扩展性(外观、组成、代理、装饰)封装性(适配器、桥接) 行为型模式设计到算法和对象间职责的分配,行为模式描述了对象和类的模式,以及它们间的通信模式,行为型模式刻划了再程序运行时难以跟踪的复杂的控制流可分为行为类模式和行为对象模式,1行为模式使用继承机制在类间分派行为,2行为对象模式使用对象聚合来分配行为,一些行为对象模式描述了一组对等的对象怎样相互协作完成其中任何一个对象都无法单独完成的任务
factory method adapter interpreter template method
对象 Abstract Factory Builder Prototype Singleton Adapter Bridge Composite Decorator Facade Flyweight Proxy Chain of Responslbllty Command Iterator Mediator Memento Observer State Strategy Visitor

3 六大原则

3.0 开放封闭(简称开闭)原则

开闭原则就是说**对扩展开放,对修改关闭**。在程序需要进行拓展的时候,不能去修改原有的代码,而是要扩展原有代码,实现一个热插拔的效果。所以一句话概括就是:为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类等。 

开放-封闭原则(Open-closed principle,OCP)也称开闭原则,是面向对象设计(OOD)中比较重要、常见的一种,下面来总结开放-封闭原则的知识点,包括: 

   1、什么是开放-封闭原则?

   2、为什么需要遵守开放-封闭原则?

   3、怎么做到开放-封闭原则?

   4、开放-封闭原则需要注意什么?

3.0.1 什么是开放-封闭原则

1-1、原则的定义

开放-封闭原则(Open-closedprinciple,OCP)可以表示为:

软件实体(类、模块、函数等)应该是可以扩展的,但是不可修改的。

1-2、两个主要的特征

遵循开放-封闭原则设计出的模块具有两个主要的特征,如下:

1、对于扩展是开放的(open for extension)

      这意味着模块的行为是可以扩展的。

       当应用的需求改变时,我们可以对模块进行扩展,使其具有满足那些改变的新行为。

       换句话说,我们可以改变模块的功能。

2、对于修改是封闭的(closed for modification)

       对模块行为进行扩展时,不必改动模块的源代码或者二进制代码。

模块的二进制可执行版本,无论是可链接的库、DLL或者.EXE文件,都无需改动

3.0.2 为什么需要

2-1、该原则带来什么

可以从下面的一些问题来描述:

  怎么样的设计才能面对需求的改变却可以保持相对稳定,从而使得系统可以在第一版本以后不断推出新的版本呢?

  怎样可能在不改动模块源代码的情况下去更改它的行为呢?

  如果不改变一个模块,又怎么能够去改变它的功能呢?

  bertrand meyer 在1988年提出的著名的开放—封闭原则(the open-closed princle)为我们提供了指引。

  在许多方面,开放-封闭原则都是面向对象设计的核心所在,遵循这个原则可以带来面向对象技术所声称的巨大好处:灵活性、可重用性以及灵活性。
2-2、违反该原则的坏处
违反开放-封闭原则的程序中,通常一处改动就会产生连锁反应,导致一系列相关模块的改动,那么设计就具有僵化性。

       OCP建议我们应该对系统进行重构,这样以后对系统再进行这样那样的改动时,就不会导致更多的修改。

如果正确地应用OCP,那么以后再进行同样的改动时,就只需要添加新的代码,而不必改动已经正常运行的代码。

3.0.3 怎么做到开放-封闭原则

3-1、抽象是关键
  由于模块依赖于一个固定的抽象体,所以它对于更改可以是封闭的。

  同时,通过从这个抽象体派生,也可以扩展此模块的行为。
3-2、应用该原则的方法
对于类来说,开始OCP依赖于面向对象继承(特别是实现继承)的概念;

  而后来,开放/封闭原理被重新定义使用抽象接口,其中可以改变实现,还可以创建多个实现,并且彼此多态地替换。

 即可以有两种方法应用OCP:

1、从抽象类继承

  最简单的方法是在继承原始类实现的新派生(子)类上实现新的功能。

2、实现抽象接口

  另一种方法是使用抽象接口来调解客户端对原始类的访问,因此可以通过同一接口访问的新类实现新功能。

 两种方法都可以创建新类,并保持原始实现不变,但提倡的是第二种。

3.0.4 该原则需要注意什么

4-1、无法做到100%的封闭
无论模块是多么的"封闭",都会存在一些无法对之封闭的变化。

      设计人员需要预测出最在可能发生的变化,然后对它的遵循OCP原则;这需要设计人员有一定的经验,但通常都会预测错误。

对此,我们需要刺激变化:因为变化发生(发现)越早、越快就越有利;刺激变化的一些方法:

  1、首先编写测试。

  2、使用很短的迭代周期进行开发。

  3、经常把开发特性展示给涉众。

  4、首先开发最重要的特性。

  5、尽早地、经常性地发布软件。尽可能频繁地把软件展示给客户和使用人员,得到他们的反馈。
4-2、抽象是该原则的基础
遵循开放-封闭原则的代价是昂贵的,如: 

创建适当的抽象是要花费开发时间和精力的;

    同时,那些抽象也增加了[软件设计](https://www.baidu.com/s?wd=%E8%BD%AF%E4%BB%B6%E8%AE%BE%E8%AE%A1&tn=24004469_oem_dg&rsv_dl=gh_pl_sl_csd)的复杂性;

    有能力处理抽象的开发人员少。

​ 所以,对于应用程序中的每个部分都肆意地进行抽象同样不是一个好主意,正确的做法是,开发人员仅仅对程序中出现频繁变化的那些部分作出抽象,而拒绝不成熟的抽象。

3.0.5 总结

开放-封闭原则:

软件实体(类、模块、函数等)应该是可以扩展的,但是不可修改的。

 **即:**

软件质量的下降,来源于修改;

替换整个实现类,而不是修改其中的某行;

替换的扩展类可以从抽象类继承、或实现抽象接口。

提倡使用实现抽象接口的方式(面向接口编程)。

开放-封闭原则是面向对象设计的核心所在,但主要的还是基本的抽象、继承、接口的知识,这和我们平时开发[息息相关](https://www.baidu.com/s?wd=%E6%81%AF%E6%81%AF%E7%9B%B8%E5%85%B3&tn=24004469_oem_dg&rsv_dl=gh_pl_sl_csd),所以基础很重要。 

3.1 单一职责原则

不要存在多于一个导致类变更的原因,也就是说每个类应该实现单一的职责,如若不然,就应该把类拆分。

单一职责原则(Single responsibility principle,SRP)是面向对象设计(OOD)中比较重要、常见的一种,下面来总结单一职责原则的知识点,包括:

       1、什么是单一职责原则、什么是职责?

       2、为什么需要遵守单一职责原则,违反单一职责有什么坏处?

       3、单一职责原则需要注意什么?

   4、单一职责原则的应用案例。

3.1.1 什么是单一职责原则

1-1、原则的定义

单一职责原则(Single responsibility principle,SRP)可以表示为:

一个类应该仅有一个引起它变化的原因。
1-2、什么是职责

职责定义为:

变化的原因"(a reason for change. 如果你能够想到多于一个的动机去改变类,那么这个类就具有多于一个的职责。这条原则曾经被认为与内聚性(cohesion)相关。内聚性可以定义为:一个模块的组合元素之间的功能相关性。即对一个类来说,这个类是否只专心做一件事。       Robert Cecil Martin("Bob"大叔)把职责定义为变化的原因,更能表达出该原则的特性。

3.1.2 为什么需要该原则

 即单一职责原则能带来什么,可以从下面两个问题来描述。

2-1、职责分离
因为每个职责都是一个变化的原因。当需求变化时,该变化会反映到类的职责的变化。如果一个类承担了多于一个的职责,那么引起它变化的原因就会有多个。而单一职责原则正是反映了《代码大全》中所说的:软件的首要技术使命--管理复杂度,而找出容易变化改变的区域,隔离变化,就是一种很好的管理复杂度的启发方法
2-2、违反该原则坏处
如果一个类承担的职责过多,等于把这些职责耦合在了一起;一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。

 这种耦合会导致脆弱的设计,当变化发生时,设计会遭受到意想不到的破坏。

3.1.3 该原则注意事项

3-1、一个类实现多个接口
一个类可能需要实现多个接口,这会产生职责耦合;但多个接口就是一种解耦表现,一个接口相关职责的变化一般不会引起其他接口的变化。 后面我们会介绍到接口隔离原则(ISP):不应该强迫客户依赖于它们不用的方法。  其中一种方法就是使用多重继承分离接口,即一个类实现了多个接口,分别提供给不同客户的接口只有该客户需要的操作。
3-2、同时变化职责
如果应用程序的变化总是导致这两方面职责同时变化,那么就不必分离他们。

    实际上,分离他们就会具有不必要的复杂性。
3-3、不要预先设计
仅当变化发生时,针对该变化的设计才具有实际意义。

    如果没有前兆,那么应用SRP或者任何其他原则都是不明智的。

3.1.4 单一职责原则的应用

常见的容易变化的区域包括:业务规则、对硬件的依赖性、输入和输出、非标准的语言特性、困难的设计区域和构建区域。

      例如:

      通常,业务规则和对于持久化的控制这两个职责不应该混合在一起。

      因为业务规则往往会频繁地变化,而持久化的方式却不会如此频繁地变化,并且变化的原因也是完全不同的。

这在平时开发中是很常见的分层应用,如用户管理的业务规则为增/删/改/查对应UserManageService接口

1
2
3
4
5
6
public interface UserManageService {    
public ResultMsg create(Integer tokenId, String sign, User user);
public ResultMsg delete(Integer tokenId, String sign, Integer userId);
public ResultMsg update(Integer tokenId, String sign, User user);
public ResultMsg get(Integer tokenId, String sign, Integer userId);
}

而用户管理的持久化也为增/删/改/查对应UserDao接口:

1
2
3
4
5
6
public interface UserDao{
public boolean add(User user);
public boolean delete( Integer userId);
public boolean update(User user);
public User get(Integer userId);
}

 实现UserManageService接口的UserManageServiceImpl类中处理完业务后,判断是否需要调用UserDao接口的相应实现,一些代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Service("userManageService")
public class UserManageServiceImpl implements UserManageService {
@Resource(name="UserDaoImpl")
private UserDao<User> userDao;
……
@Override
public ResultMsg create(Integer tokenId, String sign, User user) {
…..
//处理完业务后,判断是否需要调用UserDao接口的相应实现
userDao.add(user);
……
}
…..
}

另外,一般应该尽早分离两个职责,后期才发现的可以考虑使用FACADE(外观)、PROXY(代理)模式对设计进行重构,分离两个职责。

3.1.5 总结

 单一职责原则:

一个类应该仅有一个引起它变化的原因。

即:

管理复杂度,找出容易变化/改变的区域,隔离变化。

3.2 里氏替换原则(Liskov Substitution Principle)

里氏替换原则(Liskov Substitution Principle,LSP)是面向对象设计(OOD)中比较重要、常见的一种,下面来总结里氏替换原则的知识点,包括: 

  1、什么是里氏替换原则?

      2、为什么需要遵守里氏替换原则?

      3、怎么实现里氏替换原则,保证子类能透明的替换父类?

      4、里氏替换原则(LSP)与开闭原则(OCP)的区别与联系;  

  5、里氏替换原则冲突时如何重构,以及违反里氏替换原则的一些情况

3.2.1 什么是里氏替换原则

1-1、里氏替换原则的定义

 里氏替换原则(Liskov Substitution Principle,LSP)可以解释为:

派生类型(子类)必须能够替换掉它们的基类型(父类)。

Barbara LisKov首次写下这个原则是在1988年。她说道:

 “这里需要如下的替换性质:若对类型S的每一个对象O1,都存在一个类型T的对象O2,使得在所有针对T编写的程序P中,用O1替换O2后,程序P的行为功能不变,则S是T的子类型。”

1-2、定义解释

可以用简单的伪代码解释:

 Sub extends Base;

 对于所有使用基类的方法:

 method(Base b);

对于每一个派生类,可以这样使用:

method(new Sub());

但反之则不行,定义:

method(Sub b);

不可以这样使用:

method(new Base());

Andy Hunt和Dave Thomas总结(2000):

派生类必须能通过基类的接口而被使用,且使用者无须了解两者之间的差异。

3.2.2 为什么需要该原则

即:为什么要让所有派生类可以替换基类呢?

对于基类中定义的所有子程序,用在它的任何一个派生类中时的含义都应该是相同的。

这样继承才不会增加复杂度,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为。

如果我们必须要不断地思考不同派生类的实现在语义上的差异,继承就只会增加复杂度了。

 最终,还是回到了这个主题:

软件的首要技术使命–管理复杂度。

程序遵循Liskov替换原则,就能降低继承带来的复杂度。

3.2.4 怎么实现里氏替换原则

即:如何使用里氏替换原则(LSP),使得继承子类能透明的替换父类?

首先得理解继承:

Liskov提出(1998): 

除非派生类是一个”更特殊”的类,否则不应该从基类继承。

 继承是IS-A关系("Sub是一个Base"): 

如果一个新类型的对象被认为和一个已有类的对象之间满足IS-A关系,那么这个新对象的类应该从这个已用对象的类派生。 

 LSP清楚的指出:

  IS-A是关于行为的,对象的行为才是客户程序所依赖的,是软件真正所关注的问题。

 而对象的行为方式是可以进行合理假设的,基于契约的设计技术可以使这些合理的假设明确化,从而支持了LSP。 

基于契约设计(Design By Contract,DBC):

 类的编写者显式地规定针对该类的契约;而客户代码的编写者可以通过该契约获悉可以依赖的行为方式。

 契约是通过为每个方法声明前置条件(Precondition) 和后置条件(Postcondition)来指定的。

前置条件和后置条件:

 要使一个方法得以执行,前置条件必须要为真。执行完毕后,该方法要保证后置条件为真。

 按照Meyer所述,派生类的前置条件和后置条件规则是:

  “在重新声明派生类中的例程时,只能使用相等或者更弱的前置条件来替换原始的前置条件,只能使用相等或者更强的后置条件来替换原始的后置条件。”

示例说明:

C++、Java和C#中都没有对前置条件和后置条件的直接支持,如Java可以编写断言assert语句来实现,伪代码示例如下: 

    父类的前置条件:

//用户名不为空,且密码不能为空

     assert((userName != null) && (password != null));

 子类的前置条件(更弱):

      //用户名不为空

      assert((userName != null));

      父类的后置条件:

      //返回结果Token不为空

      assert((result.token != null));

      子类的后置条件(更强):

      //返回结果Token不为空,且需要设置过期时间

assert((result.token!= null) && (result.expiredTime != null))

当用户(单元测试)使用父类的时候,只需要知道父类的前置条件和后置条件,就可以替换的使用其所有子类,因为: 

 对于前置条件,满足父类的前置条件:用户名不为空,且密码不能为空;必定满足子类的前置条件(更弱):用户名不为空。即:子类必须接受父类可以接受的一切。

对于后置条件,满足父类的后置条件:返回结果Token不为空;必定满足子类的后置条件(更强):返回结果Token不为空,且需要设置过期时间。即:父类的用户不应被所使用的子类的输出扰乱。

3.2.4 该原则需要注意什么

4-1、与开闭原则区别联系
在上一篇文章[《面向对象设计原则(二):开放-封闭原则(OCP)》](http://blog.csdn.net/tjiyu/article/details/57079927)我们知道: 

开放-封闭原则(OCP): 

    软件实体(类、模块、函数等)应该是可以扩展的,但是不可修改的。 

    即: 

             软件质量的下降,来源于修改;

             替换整个实现类,而不是修改其中的某行;

             替换的扩展类可以从抽象类继承、或实现抽象接口。

             提倡使用实现抽象接口的方式。

 **OCP扩展类可以继承或实现接口两种方式,而里氏替换原则(LSP)主要针对继承的,它能降低继承带来的复杂度,所以可以说:LSP是OCP的重要方式之一。** 

当然,有很多文章说到LSP与OCP第二种"实现接口来扩展(替换)"相关,个人理解:**面向接口编程,替换使用的时候,是需要考虑每个实现类的细节的;而LSP强调的是透明的替换使用。** 
4-2、里氏替换原则与重构

 当继承不能满足里氏替换原则时,应该进行重构,两种重构方法:

1、重新提取公共部分的方法

把冲突的派生类与基类的共部分提取出来作为一个抽象基类,然后分别继承这个类。 

   对于提取公共部分:

    提取公共部分是一个设计工具,最好在代码不是很多的时候应用。 

    因为如果两个派生类中具有一些公共的特性,那么很可能稍后出现的其他类也会需要这些特性。 

2、改变继承关系

即:从父子关系变为委派关系或兄弟关系。

可以把它们的一些公有特性提取到一个抽象接口,再分别实现。  
4-3、违反该原则的情况
违反LSP一般都是派生类以某种方式从其基类中去除一些功能有关,完成的功能少于其基类的派生类通常是不能替换其基类的,因为违反了LSP。 

1、派生类中的退化函数

基类一个函数提供默认实现,派生类中重写却什么都不做,这很可能违反LSP。 

2、从派生类中抛出新异常

从派生类的方法中抛出了其基类不会抛出的异常。 

  因为如果基类使用都不期望这些异常,这些派生类的方法就会导致不可替换性。

3.2.5 总结

 里氏替换原则(LSP):

**派生类型(子类)必须能够替换掉它们的基类型(父类)。** 

  即:

    **派生类必须能通过基类的接口而被使用,且使用者无须了解两者之间的差异。** 

    **LSP主要针对继承,能降低继承带来的复杂度。** 

    **可以通过基于契约设计的前置条件和后置条件来保证透明替换。** 

     **LSP是开闭原则(OCP)的重要方式之一。** 

3.3 依赖倒转原则(Dependence Inversion Principle)

依赖倒置原则(Dependency Inversion Principle,DIP)也称依赖反转原则,是面向对象设计(OOD)中比较重要、常见的一种,下面来总结依赖倒置原则的知识点,包括: 

 1、什么是依赖倒置原则?

     2、为什么需要遵守依赖倒置原则?

     3、在面向对象设计中如何实现依赖倒置原则?

     4、依赖倒置原则的实例应用(包括面向对象程序设计、系统架构、社会活动中的应用)。

3.3.1 什么是依赖倒置原则

依赖倒置原则(Dependency Inversion Principle,DIP)的定义可以总结为以下两点:

   a. 高层模块不应该依赖于低层模块,两者都应该依赖于抽象

   b. 抽象不应该依赖于细节;细节应该依赖于抽象。

3.3.2 遵守该原则

依赖倒置原则是一个应用广泛的原则,**不仅在面向对象程序框架设计中(是核心原则),还是在架构系统中, 甚至是在社会活动构建组织等方面,都发挥重要作用。** 

所以,高层、低层、抽象层可以表示为多种意义: 

    **高层:客户端、服务消费者、方法调用者……(可能变动、依赖性高)** 

    **低层:服务端、服务提供者、方法实现者,细节实现者……(易变动、通常需要扩展)** 

    **抽象:交互行为、策略、契约、流程、业务模型……(稳定、可重用)** 

依赖倒置

1、传统系统

高层模块直接调用易变动的低层模块。

优点:

实现难度小,方便快捷。 

缺点:

没有抽象,耦合度高;当低层模块变动时,高层模块也得变动; 

高层模块过度依赖低层模块,很难扩展。 

而且这种依赖关系具有传递性,即如果是多层次的调用,最低层改动会影响较高层……直到最高层。 

 2、常见的面向抽象系统

高层模块依赖低层模块提供的抽象,低层模块实现抽象。

优点:

**这是通常意义的面向接口编程,或开闭原则;** 

**抽象接口就是低层模块可对外提供的服务。** 

  一定程度上降低了耦合,使得低层模块易于扩展。

缺点:

高层模块还是依赖低层模块,只不过依赖的是低层抽象 

低层抽象还是有较高变动性,而且还是会传递; 

使得高层模块的可重用性降低。 

3 依赖倒置:第一层境界

高层模块为它所需要的服务声明一个抽象接口,低层模块实现了这些抽象接口。

 即:低层模块依赖高层模块,依赖倒置了。

 优点:

首先,还是具有前面"常见的面向抽象系统"的优点:降低了耦合,使得低层模块易于扩展。

      另外,依赖倒置后,高层模块完全不依赖低层模块,不受低层模块易变性影响,增加高略层的可重用性。

还有,低层模块为高层模块而生,更符合"先有需求,后去实现";而不是低层模块的"假设",这种"假设"通常就是定义说的"抽象依赖细节"。

缺点:

低层模块依赖高层模块,受到高层模块的抽象的限制和变动影响,可重用性低。 

  另外,高层模块只能使用实现其抽象的低层模块。

4、依赖倒置:第二层境界

把高层模块和低层模块交互行为抽象出来,做到互不依赖,都依赖于提取出来的抽象。

即:无论高层模块或低层模块实现的细节,都依赖于抽象

优点:

 高层模块和低层模块彻底[解耦](https://www.baidu.com/s?wd=%E8%A7%A3%E8%80%A6&tn=24004469_oem_dg&rsv_dl=gh_pl_sl_csd),都很容易实现扩展;

     而抽象模块具有很高的稳定性、可重用性,对高/低层模块来说才是真正"可依赖的"。

缺点:

  增加了一层抽象层,增加实现难度;

     对一些简单的调用关系来说,可能是[得不偿失](https://www.baidu.com/s?wd=%E5%BE%97%E4%B8%8D%E5%81%BF%E5%A4%B1&tn=24004469_oem_dg&rsv_dl=gh_pl_sl_csd)的。

     对一些稳定的调用关系,反而增加复杂度,是不正确的。

3.3.3 小结

每种实现都有其优点和缺点,都有其存在的理由;如果我们认识到这些,在设计就能作出更合理的选择。

而依赖倒置原则,是构建大型、易扩展、可重用框架/系统的核心原则。

 一般来说,系统中存在违反依赖倒置原则的地方,很可能就是我们需要优化的地方。

3.4 接口隔离原则(Interface Segregation Principle)

这个原则的意思是:每个接口中不存在子类用不到却必须实现的方法,如果不然,就要将接口拆分。使用多个隔离的接口,比使用单个接口(多个接口方法集合到一个的接口)要好。 

3.5 迪米特法则(最少知道原则)(Demeter Principle)

就是说:一个类对自己依赖的类知道的越少越好。也就是说无论被依赖的类多么复杂,都应该将逻辑封装在方法的内部,通过public方法提供给外部。这样当被依赖的类变化时,才能最小的影响该类。

最少知道原则的另一个表达方式是:只与直接的朋友通信。类之间只要有耦合关系,就叫朋友关系。耦合分为依赖、关联、聚合、组合等。我们称出现为成员变量、方法参数、方法返回值中的类为直接朋友。局部变量、临时变量则不是直接的朋友。我们要求陌生的类不要作为局部变量出现在类中。

3.6 合成复用原则(Composite Reuse Principle)

原则是尽量首先使用合成/聚合的方式,而不是使用继承。

参考链接

参考链接