设计模式的学习,巩固面向对象的开发知识,力求写出更加优雅的代码。
写在前面
设计模式的运用个人觉得不用刻意,以解决需求为前提。需求是驱动技术的根本。
在业务需求不稳定的时候,设计模式的使用更加需要谨慎衡量。
设计模式应用在业务需求的发展十分明确的场景下,比如重构或者写基础框架。
设计模式模式是遵循软件设计七大原则的典范,而面向对象的多态、继承与封装是他们实现的基础。
面向对象
封装
抽象现实模型(现实对象的行为和属性)为类,隐藏用户不关心的属性和方法,暴露用户需要的属性和方法。
继承
子类通过继承父类,扩展甚至修改父类的功能,并复用父类的代码。表达的是IS-A关系。
多态
- 定义:同一父类型的引用在不同运行时的情况下表现出不同的子对象行为形式。
- 解析:多态发生条件如下:
- 存在继承
- 子类重写父类方法
- 父类引用指向子类对象
父类引用指向子类对象,父类引用调用的方法会是其子类对象复写的父类方法。
- 例子:123List<String> list = new ArrayList<String>();list.add("Hello,World!");//在这里父类引用list指向子类对象ArrayList,父类引用调用add方法,实际上调用的是子类ArrayList的add方法。
软件设计原则
开闭原则(Open-Closed Principle, OCP)
- 定义:软件实体(classes,modules,functions,etc.)应当对扩展开放,对修改关闭。
- 解析:即在软件设计中,需求变更或者业务的增加,不应修改原有的代码,影响原来的业务逻辑;而是通过增加代码或者配置的形式来解决新需求。
- 例子:Spring的Bean注入,只需要修改配置文件,便可以注入新的Bean,且不影响原来的Bean。典型地通过工厂模式遵循了开闭原则。这里的依赖注入还遵循了控制翻转的思想,将原本应在代码里实现的对象依赖暴露给配置文件(调用方)控制。
里氏代换原则(Liskov Substitution Principle,LSP)
- 定义:如果是S是T的子类,则S的实例可以透明地转换为T类型。(多态)
- 解析:在语言层面,Java早已支持多态。而在代码设计层面,应遵循:
- 子类尽量不覆盖父类的非抽象方法。(因为此举会破坏父类制定的行为,然而事实上并不严格遵守此要求)
- 子类方法的返回类型应比父类更为严格。(如果父类返回List,那么子类就返回ArrayList)
- 例子:
List<String> list = new ArrayList<String>();
依赖倒置原则(Dependence Inversion Principle,DIP)
- 定义:高层模块不应依赖低层模块。两者都应依赖其抽象。抽象不依赖具体。具体依赖于抽象。
- 解析:面向接口编程,编程时应高屋建瓴,先制定接口约束实现类的行为规范,再去写具体的实现类。
- 例子:一般代码里面写服务层,都是写XXXService和XXXServiceImpl,这便是面向接口编程。不过这里的依赖并非指继承,Service之间的依赖通过组合来体现。
接口隔离原则(Interface Segregation Principle,ISP)
- 定义:客户端不应依赖它们不需要使用的接口,类之间的依赖应尽可能建立在最小的接口上。
- 解析:程序开发中,接口应该尽量细化,避免冗余的接口实现。
- 例子:
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
,ArrayList的多接口实现便是接口细化的结果。
合成/聚合复用原则(Composite/Aggregate Reuse Principle,CARP)
- 定义:通过组合/继承实现代码复用。
- 解析:尽量通过组合HAS-A实现代码复用,除非有十分明确的继承IS-A语义(代码上则体现为是否需要使用多态),才使用继承实现代码复用。
- 例子:轮子组合为汽车,轮子是汽车的内部属性(组合),奔驰继承自汽车,汽车可以派生出奔驰、宝马一类的汽车。
迪米特法则(Law of Demeter,LoD)
- 定义:最少知道原则,调用者尽量少地调用被调用者的接口来实现自己的功能。通俗总结如下:
- 每个单元应有限度地知道其他单元,其只局限于与当前单元紧密联系的单元
- 每个单元都应只和熟人说话,不要和陌生人说话
- 只和你的直系朋友说话
- 解析:服务端暴露给客户端的接口应该尽可能简单(或者说统一的对外接口),使得服务端修改代码后,而不影响服务端的业务逻辑。
- 例子:实际开发中,通常通过private、protected之类的关键字控制代码的可见性,目的便是遵循最少知道原则;在提供对外API时,也常常封装统一的对外接口,通过传参来确定内部服务的复杂调用逻辑,而不是将调用逻辑暴露给客户端(有问题优先内部解决)。
单一职责原则(Simple responsibility pinciple,SRP)
- 定义:每个模块/类只负责单一的业务功能,遵循高内聚的思想。
- 解析:额,略。
- 例子:日常开发中,我们早已不自觉的遵循了这个原则,比如服务层和控制器层,会用package去分类。而每个业务功能的Service,通过类再次划分。实现了低耦合和高内聚。
UML类图
- 实现关系:代码体现为实现接口,或者继承抽象类。
- 组合关系:HAS-A。小汽车含有轮胎和发动机。并且整体不存在,则组合个体不存在。(整体强依赖)即:小汽车不存在了,便没有了轮胎和发动机。
- 聚合关系:学生聚合成班级,班级由学生组成。即使整体不存在,但个体依然存在。(整体弱依赖)即:班级不存在了,学生依然存在。
- 泛化关系:理解为继承关系,IS-A。SUV继承自小汽车,SUV是小汽车。
- 关联关系:表示一种“强关联”的关系,比如学生和身份证。代码体现一般为成员变量,身份证是学生的成员变量。
- 依赖关系:依赖关系一般体现在运行时。在代码中体现为被依赖者作为参数给依赖者。例如:
student.rideBike(bike);
创建型设计模式
单例模式
简单工厂模式
Simple Factory,隐藏了创建对象的细节,实现简单。但是在增加对象类型的时候,需要修改工厂类。违反看“开闭原则”。
- Product:抽象产品。
- ConcreteProduct:具体产品,实现了Product接口。
- Factory:对象工厂。通过调用工厂的
createProduct(String)
方法来创建不同的对象。
例子:
简单工厂比较简单,随便举个例子。
|
|
工厂方法模式
对比简单工厂模式,工厂方法模式通过扩展工厂类来增加新产品;而不是修改工厂类。
例子:
dubbo的路由扩展便利用到了工厂方法模式。对应关系如下:
- Factory:RouterFactory
- ConcreteFactory:ConditionRouterFactory
- Product:Router
- ConcreteProduct:ConditionRouter
抽象工厂模式
抽象工厂模式较为复杂,用于扩展整个产品族。
产品族与产品等级结构:产品族指由同一个工厂生产位于不同产品等级结构的一组产品。产品等级结构在代码中表现为一个抽象接口。多个抽象接口的具体产品组成产品族。比如下面类图的ConcreteProductA_HW和ConcreteProductB_HW组成了华为的产品族。
由类图可知:抽象工厂模式容易扩展工厂和产品族。但是如果添加新的产品,则会违反“开闭原则”。
例子:
dubbo代理创建以及组装代理Invoker。
Factory:ProxyFactory
ConcreteFactory:JdkProxyFactory
ConcreteProduct
- A类产品:返回
Invoker<T>
的T
类型 - B类产品:具体可看对应ConcreteFactory的代码,创建了匿名类(父类是AbstractProxyInvoker)作为ConcreteProduct。
- A类产品:返回
Product
T
是通过jdk的Proxy.newProxyInstance
生成的代理类,所以没有抽象产品。
建造者模式
原型模式
结构型设计模式
适配器模式
桥接模式
组合模式
装饰者模式
使用组合来替代继承,扩展对象的功能。
- Component:构件接口
- ConcreteComponent:具体构件
- Decorator:装饰者接口,继承自Component
- ConcreteDecorator:具体装饰者
例子:
Storm处理数据流的时候,常常需要ACK。Bolt操作本身是不会执行ACK操作的,而需要用户自己进行ACK的操作。所以Storm Client提供了BasicBoltExecutor来执行BasicBolt,并且可以自动进行ACK操作,而不需要用户显式调用ACK函数。(不看源码之前,当时我一度以为是使用了模板方法模式的= =||)
Component:IComponent
ConcreteComponent:BaseBasicBolt
Decorator:Executor
ConcreteDecorator:BasicBoltExecutor 将装饰者的属性
private IBasicBolt _bolt;
写在了具体装饰者里。IBasicBolt
也是IComponent
的子类。BasicBoltExecutor
通过复写public void execute(Tuple input)
方法,扩展了Bolt的功能。使得bolt execute之后会进行ACK。123456789101112public void execute(Tuple input) {_collector.setContext(input);try {_bolt.execute(input, _collector);_collector.getOutputter().ack(input);} catch(FailedException e) {if(e instanceof ReportedFailedException) {_collector.reportError(e);}_collector.getOutputter().fail(input);}}
例子:dubbo的ProtocolFilterWrapper
ProtocolFilterWrapper
扩展的功能如下:export
和refer
之前先判断是否是Constants.REGISTRY_PROTOCOL
,执行不同的操作。
|
|
- Component:Protocol
- ConcreteComponent:DubboProtocol
- Decorator:AbstractProtocol
- ConcreteDecorator:ProtocolFilterWrapper
代理模式
不直接调用真实对象,而是通过代理去调用。和装饰者模式十分相似。
装饰者模式里,装饰者在运行时确定,被装饰者一般作为装饰者构造器的参数传入;
代理模式里,装饰者在编译期便确定(指的是代码确定),客户端对代理对象不可见。
举个例子:
|
|
如图:
Subject:抽象主题
- do():主题的方法
RealSubject:抽象主题
Proxy:代理类
doBefore():前置方法
do():主题的方法
doAfter():后置方法
具体可以联想Spring的AOP处理,代理模式可以很好充当拦截器的功能。实际上利用装饰者模式也可以很好的实现功能扩展,其区别主要在于真实被调用的对象是否希望对客户端可见。
外观模式
享元模式
行为型设计模式
责任链模式
Chain of Responsibility,做过工作流的话,可以看到两者的相似之处。
解耦流程处理者与调用者的关系,带来更复杂的代码以及部分性能损耗。
- successor:成功处理后的下一个处理者。
- handleRequest():处理客户端/下级Handler的请求的方法
- Handler:抽象处理者接口
- ConcreteHandler:具体处理者,实现了Handler接口
例子:dubbo的ProtocolFilterWrapper
源码中,通过方法buildInvokerChain(final Invoker<T> invoker, String key, String group)
构建Invoker调用链(本质上是Filter的调用链)。
分析源码,可以得出如下对应角色:
successor:
Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException;
传入的参数invoker
即next invoker。handleRequest():对应Invoker的
public Result invoke(Invocation invocation) throws RpcException
。 该方法内部调用的是Filter的方法Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException;
在此链中,对于每个Filter都会包装成一个新的Invoker(匿名类)。(通过final修饰filter,将引用传递给new Invoker)此Invoker组成了责任链的一环。Handler:Invoker,抽象接口。
ConcreteHandler:每个Filter都会包装成一个新的Invoker(匿名类),这些匿名类便是具体处理者。实质ConcreteHandler对应的是Filter:
ExecuteLimitFilter
、TokenFilter
、ValidationFilter
等。buildInvokerChain
返回了责任链的最后一个环,假设是lastInvoker
。那么当执行lastInvoker.invoke(invocation)
的时候,便会执行filter.invoke(nextInvoker, invocation)
。nextInvoker
是final对象引用(引用不可变)。在filter.invoke(nextInvoker, invocation)
中,还会执行nextInvoker.invoke(invocation)
,而nextInvoker.invoke(invocation)
又会去调用filter.invoke(next, invocation)
。通过这样的递归调用,实际上会优先执行第一个环(递归栈:先进后出)。
命令模式
迭代器模式
中介者模式
备忘录模式
观察者模式
观察者模式在有pub/sub(发布/订阅)的地方普遍存在,并用来替代轮询。比如:redis的pub/sub,Kafka Consumer的Listener订阅注册等。还有RxJava的实现。
比如在锁的运用中,自旋锁便是轮询,而通过notify()去通知锁的Monitor便是观察者。
Observer:抽象观察者
- doWhenNotify():表示被观察者变化,通知观察者响应变化的操作。
ConcreteObserver:具体观察者
Subject:抽象观察目标
- observerList:订阅了事件的观察者列表
- regist(Oberver):被观察者将观察者注册,
notify()
将会通知这些观察者执行doWhenNotify()
- unregist(Oberver):将指定Oberver从订阅者列表移除
- notify():遍历observerList,然后调用
doWhenNotify()
ConcreteSubject:抽象观察目标
- state:被观察者状态,该状态如果改变。则应调用
notify()
通知所有订阅者(观察者)。
- state:被观察者状态,该状态如果改变。则应调用
dubbo注册中心的服务发布订阅也是典型的观察者模式。消费者观察注册中心,若注册中心发生变化,则调用notify更新服务。
TODO:这里的代码较为复杂,暂时不分析了。
- Observer:无,
public final class URL implements Serializable
- ConcreteObserver:URL 消费者
- doWhenNotify():具体看
RegistryDirectory#public synchronized void notify(List<URL> urls)
里面的代码
- doWhenNotify():具体看
- Subject:AbstractDirectory
- ConcreteSubject:RegistryDirectory 注册中心,被观察者
- observerList:URLs
- regist(Oberver):
public void subscribe(URL url)
- unregist(Oberver):
public void destroy()
,该方法内部调用egistry.unsubscribe(getConsumerUrl(), this);
取消订阅。 - notify():
public synchronized void notify(List<URL> urls)