设计模式
设计原则
单一职责原则
里式替换原则
依赖倒置原则
接口隔离原则
迪米特法则
开闭原则
创建型模式
Abstract Factory (抽象工厂) — 对象创建型模式
Builder (生成器) — 对象创建型模式
Factory Method (工厂方法) — 对象创建型模式
Prototype (原型) — 对象创建型模式
Singleton (单例) — 对象创建型模式
结构型模式
Adapter (适配器) — 类对象结构型模式
Bridge (桥接) — 对象结构型模式
Composite (组合) — 对象结构型模式
Decorator (装饰) — 对象结构型模式
Facade (外观) — 对象结构型模式
Flyweight (享元) — 对象结构型模式
Proxy (代理) — 对象结构型模式
行为模式
Chain of Responsibility (职责链) — 对象行为型模式
Command (命令) — 对象行为型模式
Interpreter (解释器) — 类行为型模式
Iterator (迭代器) — 对象行为型模式
Mediator (中介者) — 对象行为型模式
Memento (备忘录) — 对象行为型模式
Observer (观察者) — 对象行为型模式
State (状态) — 对象行为型模式
Strategy (策略) — 对象行为型模式
Template Method (模板方法) — 类行为型模式
Visitor (访问者) — 对象行为型模式
设计原则
单一职责原则
单一职责原则(Single Responsibility Principle),简称是SRP
定义:There should never be more than one reason for a class to change(应该有且仅有一个原因引起类的变更)
单一职责原则要求一个接口或类只有一个原因引起变化,也就是一个接口或类只有一个职责,它就负责一件事情
单一职责原则有什么好处
类的复杂性降低,实现什么职责都有清晰明确的定义
可读性提高,复杂性降低
可维护性提高,可读性提高
变更引起的风险降低,变更是必不可少的,如果接口的单一职责做得好,一个接口修改只对相应的实现类有影响,对其他的接口无影响,这对系统的扩展性、维护性都有非常大的帮助
注意:单一职责原则提出了一个编写程序的标准,用“职责”或“变化原因”来衡量接口或类设计得是否优良,但是“职责”和“变化原因”都是不可度量的,因项目而异,因环境而异
单一职责适用于接口、类,同时也适用于方法,一个方法尽可能做一件事, 类的单一职责确实受非常多因素的制约
单一职责原则,建议是接口一定要做到单一职责,类的设计尽量做到只有一个原因引起变化。
里氏替换原则
父类,子类,继承
继承优点:
代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性
提高代码的重用性
提高代码的可扩展性
提高产品或项目的开放性
继承缺点:
继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法
降低代码的灵活性。子类必须拥有父类的属性和方法,让子类自由的世界中多了些约束
增强了耦合性。当父类的常量、变量和方法被修改时,需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果——大段的代码需要重构
为减少继承的弊端,解决方案是引入里氏替换原则
氏替换原则(Liskov Substitution Principle,LSP)
第一种定义,也是最正宗的定义:If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T,the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.(如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型。)
第二种定义:Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.(所有引用基类的地方必须能透明地使用其子类的对象。)
第二个定义是最清晰明确的,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。但是,反过来就不行了,有子类出现的地方,父类未必就能适应
里氏替换原则为良好的继承定义了一个规范,一句简单的定义包含了4层含义
子类必须完全实现父类的方法, 注意:如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系,采用依赖、聚集、组合等关系代替继承
子类可以有自己的个性, 里氏替换原则可以正着用,但是不能反过来用。在子类出现的地方,父类未必就可以胜任
覆盖或实现父类的方法时输入参数可以被放大, 父类方法的输入参数是HashMap类型,子类的输入参数是Map类型,也就是说子类的输入参数类型的范围扩大了,子类代替父类传递到调用者中,子类的方法永远都不会被执行。这是正确的,如果你想让子类的方法运行,就必须覆写父类的方法。大家可以这样想,在一个Invoker类中关联了一个父类,调用了一个父类的方法,子类可以覆写这个方法,也可以重载这个方法,前提是要扩大这个前置条件,就是输入参数的类型宽于父类的类型覆盖范围。这样说可能比较难理解,我们再反过来想一下,如果Father类的输入参数类型宽于子类的输入参数类型,会出现什么问题呢?会出现父类存在的地方,子类就未必可以存在,因为一旦把子类作为参数传入,调用者就很可能进入子类的方法范畴
覆写或实现父类的方法时输出结果可以被缩小, 父类的一个方法的返回值是一个类型T,子类的相同方法(重载或覆写)的返回值为S,那么里氏替换原则就要求S必须小于等于T,也就是说,要么S和T是同一个类型,要么S是T的子类
采用里氏替换原则的目的就是增强程序的健壮性,版本升级时也可以保持非常好的兼容性。即使增加子类,原有的子类还可以继续运行。在实际项目中,每个子类对应不同的业务含义,使用父类作为参数,传递不同的子类完成不同的业务逻辑
依赖倒置原则
依赖倒置原则(Dependence Inversion Principle,DIP)
High level modules should not depend upon low level modules.Both should depend upon abstractions.Abstractions should not depend upon details.Details should depend upon abstractions.
高层模块不应该依赖低层模块,两者都应该依赖其抽象
抽象不应该依赖细节
细节应该依赖抽象
低层模块 每一个逻辑的实现都是由原子逻辑组成的,不可分割的原子逻辑就是低层模块
高层模块 原子逻辑的再组装就是高层模块
抽象 指接口或抽象类,两者都是不能直接被实例化的
细节 就是实现类,实现接口或继承抽象类而产生的类就是细节,其特点就是可以直接被实例化
依赖倒置原则在Java语言中的表现就是:
模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的
接口或抽象类不依赖于实现类
实现类依赖接口或抽象类
更加精简的定义就是“面向接口编程”——OOD(Object-Oriented Design,面向对象设计)的精髓之一
采用依赖倒置原则可以减少类间的耦合性,提高系统的稳定性,降低并行开发引起的风险,提高代码的可读性和可维护性
接口只是一个抽象化的概念,是对一类事物的最抽象描述,具体的实现代码由相应的实现类来完成
抽象是对实现的约束,对依赖者而言,也是一种契约,不仅仅约束自己,还同时约束自己与外部的关系,其目的是保证所有的细节不脱离契约的范畴,确保约束双方按照既定的契约(抽象)共同发展,只要抽象这根基线在,细节就脱离不了这个圈圈,始终让对象做到“言必信,行必果”
依赖的三种写法
- 构造函数传递依赖对象
在类中通过构造函数声明依赖对象,按照依赖注入的说法,这种方式叫做构造函数注入
public interface IDriver {
//是司机就应该会驾驶汽车
public void drive();
}
public class Driver implements IDriver{
private ICar car;
//构造函数注入
public Driver(ICar _car){
this.car = _car;
}
//司机的主要职责就是驾驶汽车
public void drive(){
this.car.run();
}
}
- Setter方法传递依赖对象
在抽象中设置Setter方法声明依赖关系,依照依赖注入的说法,这是Setter依赖注入
public interface IDriver {
//车辆型号
public void setCar(ICar car);
//是司机就应该会驾驶汽车
public void drive();
}
public class Driver implements IDriver{
private ICar car;
public void setCar(ICar car){
this.car = car;
}
//司机的主要职责就是驾驶汽车
public void drive(){
this.car.run();
}
}
- 接口声明依赖对象
在接口的方法中声明依赖对象,该方法也叫做接口注入
public interface IDriver {
//是司机就应该会驾驶汽车
public void drive(ICar car);
}
public class Driver implements IDriver{
//司机的主要职责就是驾驶汽车
public void drive(ICar car){
car.run();
}
}
依赖倒置原则的本质就是通过抽象(接口或抽象类)使各个类或模块的实现彼此独立,不互相影响,实现模块间的松耦合
每个类尽量都有接口或抽象类,或者抽象类和接口两者都具备
变量的表面类型尽量是接口或者是抽象类
任何类都不应该从具体类派生
尽量不要覆写基类的方法
结合里氏替换原则使用
接口隔离原则
接口
实例接口(Object Interface)
在Java中声明一个类,然后用new关键字产生一个实例,它是对一个类型的事物的描述,这是一种接口类接口(Class Interface)
定义Person这个类,然后使用Person zhangSan=new Person()产生了一个实例,这个实例要遵从的标准就是Person这个类,Person类就是zhangSan的接口
定义
Clients should not be forced to depend upon interfaces that they don't use.(客户端不应该依赖它不需要的接口。)
The dependency of one class to another one should depend on the smallest possible interface.(类间的依赖关系应该建立在最小的接口上。)
概括:建立单一接口,不要建立臃肿庞大的接口。接口尽量细化,同时接口中的方法尽量少
接口隔离原则与单一职责的审视角度是不相同的,单一职责要求的是类和接口职责单一,注重的是职责,这是业务逻辑上的划分
接口隔离原则要求接口的方法尽量少
接口隔离原则是对接口进行规范约束
接口要尽量小
- 这是接口隔离原则的核心定义,不出现臃肿的接口(Fat Interface),但是“小”是有限度的,首先就是不能违反单一职责原则
接口要高内聚
- 高内聚就是提高接口、类、模块的处理能力,减少对外的交互, 具体到接口隔离原则就是,要求在接口中尽量少公布public方法,接口是对外的承诺,承诺越少对系统的开发越有利,变更的风险也就越少,同时也有利于降低成本
定制服务
- 定制服务就是单独为一个个体提供优良的服务。在做系统设计时也需要考虑对系统之间或模块之间的接口采用定制服务。采用定制服务就必然有一个要求:只提供访问者需要的方法
接口设计是有限度的
- 接口的设计粒度越小,系统越灵活,这是不争的事实。但是,灵活的同时也带来了结构的复杂化,开发难度增加,可维护性降低,这不是一个项目或产品所期望看到的,所以接口设计一定要注意适度
接口隔离原则是对接口的定义,同时也是对类的定义,接口和类尽量使用原子接口或原子类来组装
一个接口只服务于一个子模块或业务逻辑
通过业务逻辑压缩接口中的public方法,接口时常去回顾,尽量让接口达到“满身筋骨肉”,而不是一大堆方法
已经被污染了的接口,尽量去修改,若变更的风险较大,则采用适配器模式进行转化处理;
了解环境,拒绝盲从。每个项目或产品都有特定的环境因素。环境不同,接口拆分的标准就不同。深入了解业务逻辑
迪米特法则
定义
迪米特法则(Law of Demeter,LoD)也称为最少知识原则(Least Knowledge Principle,LKP)
一个对象应该对其他对象有最少的了解。一个类应该对自己需要耦合或调用的类知道得最少,你(被耦合或调用的类)的内部是如何复杂都和我没关系,那是你的事情,我就知道你提供的这么多public方法,我就调用这么多,其他的我一概不关心。
只和朋友交流 Only talk to your immediate friends 每个对象都必然会与其他对象有耦合关系,两个对象之间的耦合就成为朋友关系,这种关系的类型有很多,例如组合、聚合、依赖等。
朋友间也是有距离的
是自己的就是自己的 如果一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响,那就放置在本类中
谨慎使用Serializable
迪米特法则的核心观念就是类间解耦,弱耦合,只有弱耦合了以后,类的复用率才可以提高
开闭原则
定义
Software entities like classes,modules and functions should be open for extension but closed for modifications.(一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。)
开闭原则对测试的影响
开闭原则可以提高复用性
开闭原则可以提高可维护性
面向对象开发的要求
单例模式
定义
Ensure a class has only one instance, and provide a global point of access to it.(确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
优点
减少了内存开支
只生成一个实例,所以减少了系统的性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后用永久驻留内存的方式来解决
可以避免对资源的多重占用,例如一个写文件动作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作
可以在系统设置全局的访问点,优化和共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理
缺点
单例模式一般没有接口,扩展很困难
要求“自行实例化”,并且提供单一实例、接口或抽象类是不可能被实例化的, 特殊情况下,单例模式可以实现接口、被继承等,需要在系统开发中根据环境判断
单例模式对测试是不利的。在并行开发环境中,如果单例模式没有完成,是不能进行测试的,没有接口也不能使用mock的方式虚拟一个对象
单例模式与单一职责原则有冲突
工厂方法模式
定义
Define an interface for creating an object,but let subclasses decide which class to instantiate.Factory Method lets a class defer instantiation to subclasses.(定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。)
优点
良好的封装性,代码结构清晰, 降低模块间的耦合
扩展性非常优秀
典型的解耦框架。高层模块值需要知道产品的抽象类,其他的实现类都不用关心,符合迪米特法则,我不需要的就不要去交流;也符合依赖倒置原则,只依赖产品类的抽象;当然也符合里氏替换原则
抽象工厂模式
定义
Provide an interface for creating families of related or dependent objects without specifying their concrete classes.(为创建一组相关或相互依赖的对象提供一个接口,而且无须指定它们的具体类。)
优点
- 封装性
缺点
- 不易扩展
模板方法模式
定义
Define the skeleton of an algorithm in an operation,deferring some steps to subclasses.Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm's structure.(定义一个操作中的算法的框架,而将一些步骤延迟到子类中。使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。)
优点
封装不变部分,扩展可变部分
提取公共部分代码,便于维护
行为由父类控制,子类实现
缺点
抽象类负责声明最抽象、最一般的事物属性和方法,实现类完成具体的事物属性和方法。但是模板方法模式却颠倒了,抽象类定义了部分抽象方法,由子类实现,子类执行的结果影响了父类的结果,也就是子类对父类产生了影响,这在复杂的项目中,会带来代码阅读的难度