OOP基本设计原则
单一职责原则(Single Responsibility Principle(SRP)),尽量保持类的功能单一。
里氏替换原则(Liskov Substitution Principle)
里氏替换原则通俗的来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。它包含以下4层含义:
- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
- 子类中可以增加自己特有的方法。
- 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
- 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
依赖倒置原则(Dependence Inversion Principle),抽象不应该依赖细节,细节应该依赖于抽象,针对接口编程,而不是针对实现编程。例如A模块调用B模块,在实现A的时候,应该保证A无需关心B模块的具体实现,只需要知道B模块提供的接口就可以完成A模块的编码。
接口隔离原则(Interface Segregation Principle),避免出现一个接口中包含太多方法的情况,否则会导致有的client依赖它并不需要用到的接口方法。
迪米特原则(最少知识原则 Principle of Least Knowledge),实现模块A/类A的时候,应该尽量减少A对其他模块的依赖,也就是要做到高内聚、低耦合。
开闭原则(OpenClosed Design Principle),模块、类、方法/函数应当是对扩展(新功能)开放,对修改闭合。即如果模块A是基于该原则实现的,那么外部在扩展A的功能的时候,可以做到不修改A原有的代码。
例如,实现一个播放器,可以提取出这个播放器基本的功能,以接口形式暴露,然后提供默认的播放器实现这个接口,如果外部需要接入自己的播放器,则只需要实现接口即可,无需再修改原先播放器模块的代码。开闭原则无非就是想表达这样一层意思:用抽象构建框架,用实现扩展细节。因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节,我们用从抽象派生的实现类来进行扩展,当软件需要发生变化时,我们只需要根据需求重新派生一个实现类来扩展就可以了。当然前提是我们的抽象要合理,要对需求的变更有前瞻性和预见性才行。
其它的5项原则,恰恰是告诉我们用抽象构建框架,用实现扩展细节的注意事项而已:单一职责原则告诉我们实现类要职责单一;里氏替换原则告诉我们不要破坏继承体系;依赖倒置原则告诉我们要面向接口编程;接口隔离原则告诉我们在设计接口的时候要精简单一;迪米特法则告诉我们要降低耦合。而开闭原则是总纲,他告诉我们要对扩展开放,对修改关闭。
多用组合,慎用继承,除非有明显继承关系的,否则都优先考虑组合来实现。
DRY (Don’t repeat yourself),抽象成可复用的代码块,如果有两处以上相同的代码块,考虑把它们抽象成一个单独的方法;或者多次使用了 硬编码的值,考虑把它们设置成公共常量。但是需要注意的是,可被抽取的代码块除了代码实现类似,还应该在业务上是用于处理类似问题的,否则可能会因为之后的业务差异越来越大,代码不再可复用。
封装经常修改的代码,在代码迭代过程中,注意有些代码模块的变动频率明显高于其他,则这一部分代码可以被单独提取出来。
策略模式
使用场景:有多个类似的类/实例,唯一的区别是某些行为的不一致。
目标:封装一系列的算法,使他们可以动态得配置到目标类中。
注意事项:如果一个系统的策略多于四个,就需要考虑使用混合模式,解决策略类膨胀的问题。
观察者模式
使用场景:一个对象的状态被多个对象所依赖,该对象的状态的改变会影响到多个对象。
目标:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
装饰器模式
使用场景:需要给一个类添加额外的功能或者属性。相比于直接使用继承,组合的形式更灵活。
目标:在不修改基础类的前提下扩展基础类的功能,修改基础类通用接口的实现。
注意事项:避免装饰层数过多
具体实现:

工厂模式
使用场景:我们明确地计划不同条件下创建不同实例时。
目标:以统一的接口创建不同对象,创建过程与具体创建的对象对客户端透明。
具体实现:让子类实现工厂接口,create方法返回抽象的产品。

单件模式
1 | public class SingleObject { |
命令模式
使用场景:需要解耦行为的请求者与实现者,或者对多个行为有较强的管理需求的,可以使用命令模式将行为(请求)封装成一个对象。
具体实现:

细节解释:
1、Invoker可以通过一个栈保存已经执行过的Commond实例,这样可以添加undo()操作以精确回退到某一个命令。
2、可以实现一个持有多个Commond的类,用来表示组合命令。
3、Commond需要持有Receiver实例,而Commond是在invoker中实例化的,如果Receiver与Invoker是一一对应的(多数情况下如此),那么Invoker就需要持有Receiver实例,而不是每次都重新与Commond一起实例化。
适配器模式、外观模式
适配器模式:转换不同的接口或者转换不同的数据。
外观模式:封装多个接口,对外界提供更简洁清楚的接口。
模板方法模式
实现:
- 将相同部分的代码放在抽象的父类中,而将不同的代码放入不同的子类中。
- 子类通过实现父类的抽象方法可以控制父类方法具体实现。方法调用由父类发生,子类仅提供方法执行所必须的信息;即允许低层组件将自己挂钩到高层组件上,高层组件决定如何以及何时使用这些低层组件。
迭代器模式
提供一种方法顺序访问一个集合对象中的各个元素,而又不暴露其内部结构。典型实现如JDK中的迭代器。
组合模式
组合模式将对象组合成树形结构用以表现“整体/部分”层次结构。对象A和对象组B(包含一组A),均实现同一个接口,这样对于客户端来说,无需去差分A与B。
状态模式
当一个类(Context)需要根据自身状态对同一个调用做不同响应的时候,可以考虑使用状态模式。状态模式将改变状态的操作封装到具体的状态类中,而Context类只负责提供外层调用接口和自身状态信息的维护。

代理模式
1、通常意义上的代理,转发实际服务的请求与结果,做一些数据上的转换,作为服务器与客户端通信的媒介。
2、虚拟代理,除去转发功能,也可以承担一部分服务器的功能。比如代理去请求服务器数据,但是服务器运算时间很久,此时代理会先给客户端返回一个默认的数据(行使一部分服务端的功能),等服务端数据到达后再转发。

反模式
桥接模式
如果一个类有两个可扩展的维度,则将这两个维度抽象出来,并在抽象层定义他们之间的关系,这样使得这两个维度都易于扩展都变得易于扩展。
有这样一个需求,Interface有两个抽象方法A、B(对应于上述的两个可变化的维度),要求它的具体实现中,方法A、B各自可能相同也可能不同,普通的实现如下:

可以发现如果出现B相同A不同或者A、B都不相同的情况,会导致类继承结构非常复杂。此时就可以使用桥接模式,将A、B单独抽象为一个独立的抽象类、接口,使他们通过组合联系起来。实现如下:

生成器模式
将某个类的构造赋值过程封装在一个特定的Builder类中。
1 | public static class Builder { |
责任链模式
当你想要一个以上的对象有机会处理某个请求的时候,就可以考虑使用责任链模式。
从下图的具体实现中可以看出,所有的Handler形成了一个链表,事件由此逐步传递处理。

蝇量模式
如果想要让某一个类的实例能提供许多的“虚拟实例”,就可以考虑使用蝇量模式。使用该模式可以减少运行时的对象个数,减少内存,同时也可以集中式的管理“虚拟对象”的状态。
比如我们需要绘制很多棵树,每棵树唯一的不同点在于坐标不同,那么就可以只实例化一个树对象,然后用集合保存每棵树的坐标值,每次渲染的时候就给树对象赋值然后绘制。
中介者模式
可以用来集中相关对象之间复杂的沟通和控制方式。
假如多个对象之间存在复杂的相互关系,如下图:

在加入中介者之后,所有的对象都不直接与其他对象交互,而是通过中介者通信,中介者维护各个对象之间的关系。这样可以使对象之间的关系更加清晰。

备忘录模式
当你需要让对象返回之前的状态时,可以使用备忘录模式。
具体实现就是用另外一个专用的备份类,来存储需要被存储的对象的状态,下次恢复对象时传入这些状态参数。
典型的如Android Framework中的onSaveInstanceState方法的设计。
原型模式
当创建给定类的实例的过程很昂贵或很复杂时,使用原型模式。在Java中,其实就等价于使用clone()或者反序列化方式创建一个新的实例。
访问者模式
访问者适合于数据结构相对稳定的系统。它把数据结构和作用于结构之上的操作之间的耦合解脱开来,使得操作集合可以相对自由地演化。
访问者模式的目的是要把处理从数据结构分离出来。如果这样的系统有比较稳定的数据结构,又有易于变化的算法的话,使用访问者模式就是比较适合的,因为访问者模式使得算法操作的增加变得容易。
那其实访问者模式的优点就是增加新的操作很容易,因为增加新的操作就意味着增加一个新的访问者,访问者模式将有关的行为集中到一个访问者对象中。
那访问者的缺点其实也就是使得增加新的数据结构变的困难了。
结合下面的代码,Wheel、Engine就代表稳定的数据结构,而Visitor则代表具体的操作,可见Visitor是易于扩展的。
1 | interface Visitor { |