一、六大设计原则
SRP-单一职责原则(Single Responsibility Principle)
定义:
There should never be more than one reason for a class to change.
就一个类而言,应该仅有一个引起它变化的原因。
But,This is sometimes hard to see.
单一职责,适用于接口、类,同时也适用于方法。
优点:
- 类的复杂度降低
- 可读性提高
- 变更引起的风险降低
缺点:
- 人为制造复杂性
- 类的剧增,维护不一定简单
- 纯理论,有实现的难处
LSP-里氏替换原则(Liskov Substitution Principle)
定义:
Functions that use pointer or references to base classes must be able to use objects of derived classes without knowing it.
所有引用基类的地方,必须能透明地使用其子类的对象。
即子类是特殊的基类。大象是动物。但反过来未必能适应。
子类继承父类,从整体上看,利大于弊。利在于提高代码重用,提高扩展性。弊在于继承是侵入性的,增强耦合性。引入里氏替换,发挥利的作用,减少弊的麻烦。该原则定义了良好的继承的规范,增强程序的健壮性。
- 子类必须完全实现父类的方法,否则可考虑使用依赖、聚集、组合灯关系代替继承
- 子类可以有自己的个性(最佳实践中尽量避免个性)
- 覆盖或实现父类的方法时输入参数可以被放大
- 覆盖或实现父类的方法时输出结果可以被缩小
DIP-依赖倒置原则(Dependence Inversion Principle)
含义:
- 高层模块不应该依赖低层模块,两者都应该依赖其抽象。
- 抽象不应该依赖细节
- 细节应该依赖抽象
以java为例,不可分割的原子逻辑是低层模块,再组装就是高层模块。
抽象是指接口或抽象类,两者都不能被实例化。细节就是实现类,实现接口或继承抽象类而产生的类就是细节。可被实例化。
精简的定义就是面向接口的编程OOD,它是面向对象设计的精髓之一。本质是,通过抽象使各个类或模块的实现彼此独立,实现松耦合。
依赖的三种写法:
- 构造函数传递依赖对象
- setter方法传递依赖对象
- 接口声明传递依赖对象
ISP-接口隔离原则(Interface Segregation Principle)
定义:
- 客户端不应该依赖它不需要的接口
- 类间的依赖关系应该建立在最小的接口上
保证接口的纯洁性
- 接口尽量小,但必须满足单一职责原则,不要无限拆分。
- 接口要高内聚,减少对外的交互
- 通过定制实现接口拆分
- 接口设计是有限度的,根据实践、经验和领悟来把握粒度。
LoD-迪米特法则(Law of Demeter),也叫最少知识原则
Only talk to your immediate friends.
- 一个类只和朋友交流
- 朋友间也是有距离的,不要暴露太多的方法或属性,尽量内敛
- 是自己的就是自己的
- 谨慎使用Serializable
核心观念:类间解耦,弱耦合,提高类的复用率。结果是产生了大量的跳转类,导致系统复杂性提高。
OCP-开闭原则(Open Close Principle)
Software entities like classes,modules and functions should be open for extension but closed for modifications。
软件实体应该对扩展开放,对修改关闭。即尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来完成变化。保持历史代码的纯洁性,提高系统的稳定性。
优点:
开闭原则是基础,是抽象类,其他五大原则是具体的实现类
- 开闭原则对测试的影响,修改后原有的测试不需要重新测试,新增加的类和方法单独测试就好。
- 提高复用性
- 提高可维护性
- 面向对象开发的要求
如果使用:
- 抽象约束。通过接口或抽象类来约束拓展,参数类型和引用对象尽量使用接口或抽象类,抽象层尽量保证稳定。
- 元数据控制模块行为。通过修改配置文件来完成业务变化。
- 制定项目规范,约定优于配置。
- 封装变化,23个设计模式都是从不同角度对变化进行封装。
二、设计模式
定义
- 模式:问题及解决方案共同的本质;
- 设计模式:是一套被反复使用,多数人知晓的,经过分类编目的,代码设计经验的总结;
- 是由四人帮设计的23种设计模式;
- 想让程序具有某种特性,就借鉴某种模式即可;
框架和设计模式的区别
- 构件是代码重用,设计模式是设计重用,框架介于两者之间,是部分代码重用部分设计重用;
- 设计模式比框架更抽象:框架可以用代码来表示,模式只有实例可以用代码来表示;
- 设计模式是比框架更小的元素,一个框架可以包含多个设计模式;
- 框架针对某个特定的应用领域,解决某个问题,相当于一个软件;同一个设计模式则适用于不同的应用;
模式包含的要素:
名字(可以有多个名字,但主要的名字应该只有一个,其他的应是别名),问题(描述了在何时使用模式),效果,解答(解决方案);
MVC:模型-视图-控制器模式;是架构模式,不是设计模式;它是在合成模式,策略模式和观察者模式的基础上加一些别的东西组成的;
架构模式:一个架构模式描述软件系统里的基本的结构组织或纲要。架构模式提供一些先定义好的子系统,指定他们的责任,并给出把他们组织在一起的法则和指南;
设计模式:一个设计模式提供一种提炼子系统或软件系统中的组件或他们之间的关系的纲要设计。它描述普遍存在的在相互通讯的组件中重复出现的结构,这种结构解决在一定的背景中具有一般性的设计问题;
- Model层:实现系统中业务逻辑,可用JavaBean或EJB来实现;
- Controller层:Controller是位于model和view之间沟通的桥梁,使用servlet技术实现;
- View层:用于与用户交互,使用JSP实现;
使用设计模式可以:
- 确定并不明显的抽象和描述这些抽象的对象;
- 决定一个对象应该是什么
- 定义对象的操作
- 描述对象的实现
- 最大程度的复用
选择设计模式的步骤:
- 理解问题需求:需求是模式选择的基础,通过对需求的分析可以找到多个模式,形成模式组;
- 研究组内模式:需求分析得出的组内模式有一些共性,但是每种模式都有其特殊的意图,使用动机和使用条件,因此需要对组内模式进行研究
- 考虑设计模式如何解决设计问题:在此过程中,主要考虑设计模式在设计中所支持的可变化因素,即确定改变什么而不需要重新设计,根据这一点可以找到所需要的设计模式。此外考虑与其相关的设计模式;
三、5种创建型设计模式
单例模式(Singleton Pattern)
确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
优点:
- 减少对象频繁创建、销毁时的内存开支和性能开销
- 避免对资源的多重占用,如I/O资源
- 全局的访问点,优化和共享资源访问
缺点:
- 没有借口,拓展困难
- 不利于测试,没有接口,也不能使用mock方式虚拟
- 与单一职责原则冲突
场景:
- 要求生成唯一序列号的环境
- 共享访问点或共享数据
- 创建对象需要消耗大量资源
- 需要定义大量静态常量和静态方法(如工具类)的环境
- Spring的Bean默认使用单例模式,通过ThreadLocal解决线程安全问题
注意事项:
- 高并发情况下的线程同步问题。可在方法前加synchronized关键字,或方法内实现synchronized。
- 考虑对象的复制情况,最好不好实现Cloneable接口。
- 可扩展成有上限的多例模式。
- 单例创建有很多种方式,饿汉、懒汉(是否线程安全)、双检锁、登记、枚举等。一般使用饿汉。
工厂方法模式(Factory Pattern)
定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。
优点:
- 良好的封装性,代码结构清晰。只需要传入产品类名即可创建。
- 扩展性优秀,增加产品类后不需要修改工厂类。
- 工厂方法模式是典型的解耦框架,符合迪米特法则,依赖倒置原则,里氏替换原则等。
- 屏蔽产品类,产品类实现的变化,只需要保持接口不变,调用者无须关心。
使用场景:
- 需要生成对象的地方都可使用,但需要考虑增加工厂类会增加代码复杂度。
- 需要灵活可拓展的架构时可考虑工厂方法模式。
- 用在异构项目中,减少与外围系统的耦合。
- 用在测试驱动开发的框架下,目前基本被Mock取代。
扩展:
- 简单工厂模式(静态工厂模式),使用静态方法生产。
- 升级为多个工厂类
- 可替代单例模式
- 延迟初始化,对象被消费完毕后不立刻释放,工厂类保持初始状态,等待再次被使用。可在扩展限制最大实例化数量。
抽象工厂模式(Abstract Factory Pattern)
为创建一组相关或相互依赖的对象提供一个接口,而且无须指定他们的具体类。
优点:
- 封装性,不需要关心实现类,只关心接口,知道工厂类,就能通过接口创建出对象。
- 产品族内的约束为非公开状态。对调用工厂类的高层模块是透明的。
缺点:
- 产品扩展非常困难,严重违反开闭原则。
(1) 当增加一个产品时,抽象类增加方法,两个实现类增加方法。
(2) 当增加一个工厂时,扩展容易。
使用场景:一个对象族(或是一组没有任何关系的对象)都有相同的约束,则可以使用抽象工厂模式。
建造者模式(Builder Pattern)
建造者模式,也叫生成器模式。将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
优点:
- 封装性,不必知道内部组成的细节
- 建造者独立,容易扩展,各个Builder相互独立。
- 建造者独立,可以逐步细化建造过程,不对其他模块产生影响。
- 可和模板方法模式混用
使用场景:
- 相同的方法,不同的执行顺序,产生不同的时间结果
- 多个零部件,装配到一个对象中,产生的运行结果不相同
- 产品类复杂,使用多个简单的对象一步一步构建成一个复杂的对象。
原型模式(Prototype Pattern)
用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
优点:
- 性能优良,原型模式是在内存二进制流的拷贝,要比直接new一个性能好很多
- 逃避构造函数的约束,直接拷贝,构造函数不会执行。
使用场景:
- 资源优化场景,类初始化需要消化非常多的资源
- 性能和安全要求的场景,通过 new 产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式。
- 一个对象多个修改者的场景。一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以考虑使用原型模式拷贝多个对象供调用者使用。
注意事项:
通过对一个类进行实例化来构造新对象不同的是,原型模式是通过拷贝一个现有对象生成新对象的。浅拷贝实现 Cloneable,内部数组和引用对象不拷贝,原始类型和String会拷贝。
四、8种结构性设计模式
适配器模式(Adapter Pattern)
将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
优点:
- 可以让任何两个没有关联的类一起运行。
- 提高了类的复用。
- 增加了类的透明度。
- 灵活性好。
缺点:
- 过多地使用适配器,会让系统非常零乱,不易整体进行把握。比如,明明看到调用的是 A 接口,其实内部被适配成了 B 接口的实现
- 由于 JAVA 至多继承一个类,所以至多只能适配一个适配者类,而且目标类必须是抽象类。
使用场景:有动机地修改一个正常运行的系统的接口,这时应该考虑使用适配器模式。
注意事项:适配器不是在详细设计时添加的,而是解决正在服役的项目的问题。
桥接模式(Bridge Pattern)
桥接模式是用于把抽象化与实现化解耦,使得二者可以独立变化。
抽象化:其概念是将复杂物体的一个或几个特性抽出去而只注意其他特性的行动或过程。在面向对象就是将对象共同的性质抽取出去而形成类的过程。
实现化:针对抽象化给出的具体实现。它和抽象化是一个互逆的过程,实现化是对抽象化事物的进一步具体化。
脱耦:脱耦就是将抽象化和实现化之间的耦合解脱开,或者说是将它们之间的强关联改换成弱关联,将两个角色之间的继承关系改为关联关系。
通用uml:
- Abstraction,抽象化角色,定义出该角色的行为,保存一个对实现化角色的引用,一般是抽象类。
- Implementor,实现化角色,它是接口或抽象类,定义角色的行为和属性。
- RefinedAbstraction,修正抽象化角色,引用实现化角色对抽象化角色进行修正。
- ConcreteImplementor,具体实现化角色,它实现接口或抽象类定义的属性和方法。
优点:
- 分离抽象接口及其实现部分。提出了比继承更好的解决方案。
- 桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统。
- 实现细节对客户透明,可以对用户隐藏实现细节。
缺点:
- 桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程。
- 桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性。
模式使用场景:
- 如果一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。
- 对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。
- 一个类存在两个独立变化的维度,且这两个维度都需要进行扩展。
组合模式(Composite Pattern)
组合模式,又叫部分整体模式,将对象组合成树形结构以表示”部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。
优点:
- 高层模块调用简单。
- 节点自由增加。
缺点:在使用组合模式时,其叶子和树枝的声明都是实现类,而不是接口,违反了依赖倒置原则。
使用场景:部分、整体场景,如树形菜单,文件、文件夹的管理。
装饰器模式(Decorator Pattern)
动态地给一个对象添加一些额外的职责。就增加功能来说,装饰器模式相比生成子类更为灵活。
通用uml:
- Component 抽象构建,代表原始对象
- ConcreteComponent 具体构建,需要装饰的对象
- Decorator 装饰角色,一般是抽象类,且有一个private属性指向Component 抽象构建
- ConcreteDecorator 具体装饰角色,具体装饰类。
优点:
- 装饰类和被装饰类可以独立发展,不会相互耦合
- 装饰模式是继承的一个替代模式
- 装饰模式可以动态扩展一个实现类的功能。
缺点:多层装饰比较复杂。
使用场景:
- 扩展一个类的功能。
- 动态增加功能,动态撤销。
外观模式(Facade Pattern)
也成为门面模式,要求一个子系统的外部和内部的通信必须通过一个统一的对象进行,外观模式定义了一个高层次的接口,使得子系统更加容易使用。
优点:
- 减少系统相互依赖。
- 提高灵活性。
- 提高了安全性。
缺点:不符合开闭原则,如果要改东西很麻烦,继承重写都不合适。
使用场景:
- 为复杂的模块或子系统提供外界访问的模块。
- 子系统相对独立。
- 预防低水平人员带来的风险。
享元模式(Flyweight Pattern)
使用共享对象有效地支持大量细粒度的对象。
优点:大大减少对象的创建,降低系统的内存,使效率提高。
缺点:提高了系统的复杂度,需要分离出外部状态和内部状态,而且外部状态具有固有化的性质,不应该随着内部状态的变化而变化,否则会造成系统的混乱。
使用场景:
- 系统有大量相似对象。
- 需要缓冲池的场景。
注意事项:
- 注意划分外部状态和内部状态,否则可能会引起线程安全问题。
- 这些类必须有一个工厂对象加以控制。
代理模式(Proxy Pattern)
也叫委托模式,为其他对象提供一种代理以控制对这个对象的访问。例如Spring AOP的动态代理。
优点:
- 职责清晰。
- 高扩展性。
- 智能化。
缺点:
- 由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。
- 实现代理模式需要额外的工作,有些代理模式的实现非常复杂。
五、11种行为型设计模式
责任链模式
使多个对象都有机会处理请求,从而避免请求发送者与接收者之间的耦合关系。将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。
优点:
- 降低耦合度。它将请求的发送者和接收者解耦。
- 简化了对象。使得对象不需要知道链的结构。
- 增强给对象指派职责的灵活性。通过改变链内的成员或者调动它们的次序,允许动态地新增或者删除责任。
- 增加新的请求处理类很方便。
缺点:
- 不能保证请求一定被接收。
- 系统性能将受到一定影响,而且在进行代码调试时不太方便,可能会造成循环调用。
- 可能不容易观察运行时的特征,有碍于除错。
命令模式(Command Pattern)
将一个请求封装成一个对象,从而使您可以用不同的请求对客户端进行参数化。对请求排队或者记录请求日志,可以提供命令的撤销和恢复功能。
优点:
- 降低了系统耦合度。
- 新的命令可以很容易添加到系统中去。
缺点:使用命令模式可能会导致某些系统有过多的具体命令类。
解释器模式(Interpreter Pattern)
给定一个语言,定义它的文法表示,并定义一个解释器,这个解释器使用该标识来解释语言中的句子。例如java的expression4J
优点:
- 可扩展性比较好,灵活。
- 增加了新的解释表达式的方式。
- 易于实现简单文法。
缺点:
- 可利用场景比较少。
- 对于复杂的文法比较难维护。
- 解释器模式会引起类膨胀。
- 解释器模式采用递归调用方法。
迭代器模式(Iterator Pattern)
提供一种方法顺序访问一个聚合对象中各个元素, 而又无须暴露该对象的内部表示。
使用场景:
- 尽量不要自己写迭代器,使用java提供的Iterator。
- 访问一个聚合对象的内容而无须暴露它的内部表示。
- 需要为聚合对象提供多种遍历方式。
- 为遍历不同的聚合结构提供一个统一的接口。
中介者模式(Mediator Pattern)
用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
优点:
- 降低了类的复杂度,将一对多转化成了一对一。
- 各个类之间的解耦。
- 符合迪米特原则。
缺点:中介者会庞大,变得复杂难以维护。
使用场景:
- 系统中对象之间存在比较复杂的引用关系,导致它们之间的依赖关系结构混乱而且难以复用该对象。
- 想通过一个中间类来封装多个类中的行为,而又不想生成太多的子类。
备忘录模式(Memento Pattern)
在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。
优点:
- 给用户提供了一种可以恢复状态的机制,可以使用户能够比较方便地回到某个历史的状态。
- 实现了信息的封装,使得用户不需要关心状态的保存细节。
缺点:消耗资源。如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存。
使用场景:
- 需要保存/恢复数据的相关状态场景。
- 提供一个可回滚的操作。
观察者模式(Observer Pattern)
也叫发布订阅模式,定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
优点:
- 观察者和被观察者是抽象耦合的。
- 建立一套触发机制。
缺点:
- 如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。
- 如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。
状态模式(State Pattern)
允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类。
优点:
- 结构清晰,避免了大量的条件、分支语句。
- 遵循设计原则,每个状态都是一个子类,体现了开闭原则和单一职责原则。
- 封装性非常好,封装了转换规则。
缺点: 状态模式的使用必然会增加系统类和对象的个数。
策略模式(Strategy Pattern)
定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。
优点:
- 算法可以自由切换。
- 避免使用多重条件判断。
- 扩展性良好。
缺点:
- 策略类会增多。
- 所有策略类都需要对外暴露。
使用场景:
- 如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。
- 一个系统需要动态地在几种算法中选择一种。
- 如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。
模板模式(Template Pattern)
定义一个操作中的算法的框架,而将一些步骤延迟到子类中。使子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
一般模板方法都加上final不允许被覆写。
优点:
- 封装不变部分,扩展可变部分。不变部分算法封装到父类,可变部分通过继承来扩展。
- 提取公共代码,便于维护。
- 行为由父类控制,由子类实现。
缺点:
抽象类定义了部分抽象方法,由子类实现,子类执行结果影响父类,带来代码阅读的难度。
场景:
- 多个子类有公共的方法,逻辑基本相同
- 重要、复杂的算法,将核心算法设计为模板方法,细节功能由子类实现
- 重构时将相同的代码抽取到父类中
- 可通过钩子方法,拓展模板方法模式
访问者模式(Visitor Pattern)
封装一些作用于某种数据结构中的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。
优点:
- 符合单一职责原则。
- 优秀的扩展性。
- 灵活性。
缺点:
- 具体元素对访问者公布细节,违反了迪米特原则。
- 具体元素变更比较困难。
- 违反了依赖倒置原则,依赖了具体类,没有依赖抽象。
使用场景:
- 对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作。
- 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作”污染”这些对象的类,也不希望在增加新操作时修改这些类。