前言
我常常觉得人们低估了设计模式的作用和意义。它们不仅是简历上的金边、程序员的黑话,也不仅是常见业务的常用处理方式或经验总结。
设计模式不仅是这些,它们更是面向对象思想理论结合实践的切入点。我们前面聊过抽象、高内聚低耦合、封装继承多态、SOLID设计原则。它们更偏理论指导,离编码实践还有一段距离。而这里要聊的设计模式,不仅有扎实的理论基础,而且实实在在地俯下身子、扎根到了实践当中。
从编码实践的角度来讲设计模式,这类文章没有一万也有八千。细抠几种设计模式之间的区别,这类文章写再多也没太大意义。这里就不凑这些热闹了。
这里,我会简单聊聊几个主要设计模式的编程应用,然后把主要精力放在它们与面向对象思想的关联上。另外,在聊过几种设计模式之后,计划提供一种复合模式,作为我使用设计模式的“最佳实践”,供读者参考。
注册器模式
是什么
注册器模式的定义似乎比较模糊,我在网上没找到比较满意的说法,所以自己总结了一个:
注册器模式提供了一个注册器,一组相同类型的实例可以被注册到注册器上并由后者进行保存。调用方则可以通过注册器来取用这些实例。
这个定义由三组对象和三套服务组成。三组对象是“一组实例”——对应后面的“调用方”,以后统称“服务方实例”好了——注册器,以及调用方。三套服务显然全都是注册器提供的,即注册、保存和取用服务。
显然,注册器对象是这一模式的核心,它提供的注册服务则是注册器模式的精髓。
所谓注册服务,即注册器对象提供的一个全局接入点。服务方实例通过这个接入点,将自己“注册”到注册器上,供后者的保存和取用服务使用。
这里要着重指出,注册服务必须是服务方实例主动把自己“注册”到注册器中,而不是注册器主动把服务方实例“收集”起来。除了发起方不同之外,二者最大的区别体现在“新增服务实例”场景下。
借助“注册服务”,新增服务实例时,我们不需要对注册器做任何修改,由新的服务实例来调用注册服务即可。如果是“收集服务”,则需要修改注册器,由注册器来收集新的服务实例。
例如,在SpringMVC中,如果要新增Controller,我们只需要在对应的类上加上@controller或@RestController注解,而不需要改动SpringMVC框架一行代码。这就是一个典型的注册器。
服务方实例调用注册服务后,注册器只会保存它们,自身并不需要调用其服务。这是注册器的第二个要点:注册器并不是服务方实例的服务对象。
服务方实例的服务对象不是注册器,显然就是三组对象中的最后一位:调用方。在注册器模式中,调用方并不直接调用服务方,而是从注册器中“取用”一个服务方实例,然后再做服务调用。
仍用SpringMVC举例:Controller类通过@Controller注解注册到SpringMVC中;显然,SpringMVC自己不会去调用其中的业务接口,而只是把它们缓存起来;当接收到HTTP请求时,服务器会从SpringMVC中获取一个Controller实例,然后用这个实例去处理请求。
总之,服务方实例-注册器-调用方,注册-缓存-取用,三组对象和三套服务,注册器模式就是这么一回事。
怎么做
注册器模式可以有多种实现方式。这里介绍一种比较常用、也比较简单的例子。
虽然介绍注册器模式的顺序是“注册-保存-取用”,但在这个例子中,我们改由“取用”服务入手。
取用服务
绝大多数时候,调用方“取用”服务方实例的方式,都是“按某种凭据获取对应的服务方实例”。例如,按照省份代码来获取车船税计计算器,或按照还款方式来获取还款还款计划试算器。在这种方式中,“凭据”和“服务方实例”,显然是1:1的关系。
因此,我们的注册器可以这样实现:
public interface ServiceRegistry{
/** 按某种凭据获取特定服务方实例
* @param t 获取服务方实例所用的凭据。
*/
public Service fetch(Ticket t);
}
public class ServiceRegistryByTicket implements ServiceRegistry{
private Map<Ticket, Service> map;
/** 按"凭据"取用服务 */
public Service fetch(Ticket t){
return map.get(t);
}
}
诚然,很多时候取用服务都更复杂。例如,有些地区全省用一套车船税计算器,有些地区则要按省市两级来区分。不过,这种复杂逻辑也可以简化为“按凭据获取实例”,无非是在前面加一步“按规则获取凭据”而已:
public interface ServiceRegistry{
/** 按某种复杂规则获取特定服务方实例
* @param data 复杂规则计算时用到的数据
*/
public Service fetch(Data data);
}
public class ServiceRegistryByRule implements ServiceRegistry{
private Map<Ticket, Service> map;
public Service fetch(Data data){
Ticket t;
// 先按规则,获取凭据
if(data.getProvince()=="JiangXi"){
// 江西就南昌比较特殊
if(data.getCity()=="NanChang"){
t = Ticket.NanChang;
}else{
t = Ticket.JiangXi;
}
}else if(data.getProvince()=="JiangSu"){
// 散装江苏,全部按城市取值,略
}else{
// 其它地区,按省份取值
}
// 再按凭据,获取服务
return map.get(t);
}
}
保存服务
前面的代码示例中,已经展示了注册器的保存服务的实现方式:
public class ServiceRegistryByTicket implements ServiceRegistry{
/** 这个map就是"保存服务"的提供者 */
private Map<Ticket, Service> map;
public Service fetch(Ticket t){
return map.get(t);
}
}
使用Map<Ticket,Service>来实现保存服务有一个前提,即取用服务中,凭据和服务实例是严格的1:1关系。如果取用服务无法遵守这一约束,那么保存服务的实现方式就得另当别论了。好在,从我个人经验来看,这个约束条件几乎牢不可破。
注册服务
虽然是注册器模式的核心,注册服务其实并没有多复杂。注册器只需提供一个方法,供服务方实例主动调用即可:
public interface ServiceRegistry{
public Service fetch(Ticket t);
/** 注册服务。把服务方实例 s 注册到凭据 t 上
* @param t 取用凭据。
* @param s 注册到t上的服务方实例
*/
public void register(Ticket t, Service s);
}
public class ServiceRegistryByTicket implements ServiceRegistry{
private Map<Ticket, Service> map;
public Service fetch(Ticket t){
return map.get(t);
}
/** 把凭据和服务方实例保存下来 */
public void register(Ticket t, Service s){
map.put(t,s);
}
}
public class ServiceA implements Service{
private ServiceRegistry serviceRegistry;
public ServiceA(){
// 把自己注册到凭据A上
serviceRegistry.register(Ticket.A, this);
}
// 其它服务实现,略
}
在Spring世界中,借助Autowired注解的一些特性,我们可以更轻松地实现注册器模式。
当Autowired注解在Collection<T>
或者Map<String, T>
类型的成员变量上时,SpringIOC会把Spring上下文中所有T类型的bean全部注入到这个成员变量中。由此,我们可以对上面的注册服务做一点改造:
/** 注册器接口只需要提供取用服务;注册服务由SpringIOC实现。 */
public interface ServiceRegistry{
public Service fetch(Ticket t);
}
@Service
public class ServiceRegistryByTicket implements ServiceRegistry{
/** 开启SpringIOC自动装配 */
@Autowired
private Map<String, Service> map;
public Service fetch(Ticket t){
// 取用服务中,对凭据做一点简单加工
return map.get("service4Ticket"+t);
}
}
/** 注意这里的 beanName,和取用、保存服务中的"凭据"必须一一映射。 */
@Service("service4Ticket"+Ticket.A)
public class ServiceA implements Service{
// 其它服务实现,略
}
/** 注意这里的 beanName,和取用、保存服务中的"凭据"必须一一映射。 */
@Service("service4Ticket"+Ticket.B)
public class ServiceB implements Service{
// 其它服务实现,略
}
这种方式当然也有弊端,更有改进空间。这里不做赘述,兵来将挡水来土掩即可。
为什么
注册器模式比较小众,无论学习还是实践,都较少有人提到它。也许在CRUD中,连设计模式都是多余,遑论如此小众的注册器模式。其实,在设计和开发系统框架时,注册器模式是一个非常好用的工具。
粗略地说,系统框架是某种通用逻辑的公共实现。例如SpringMVC框架,它是Http协议对接业务代码的通用逻辑及公共实现。如何从HttpRequest中解析业务数据,怎样把业务数据写回HttpResponse,这些通用功能,SpringMVC框架都已提供,无需我们再造轮子了。
遗憾的是,业务系统总是会有层出不穷的特定逻辑,光靠通用逻辑无法满足其需求。例如,当内部抛出异常时,http接口必须按特定的格式和内容返回一段JSON报文。单凭SpringMVC框架满足不了这一点,我们必须在框架基础上做“定制化开发”。
麻烦的是,在Java中,框架往往是不可修改的jar包。绝大多数情况下,我们都不能、也不应该修改jar包里的框架代码。可是,定制化需求同样不能、也不应该放弃。那么,怎样才能既不改动现有框架、又容纳新的需求呢?
首先,在设计框架时,我们就必须充分考虑到哪些地方会出现怎样的扩展需求。这是设计框架的一个难点,但不是本文的重点,所以此处省略一万字。然后,我们要在框架中预留“扩展点”,以便在扩展需求出现时,能够简单快捷地实现“既不改动现有框架、又能容纳新的需求”。
显而易见,注册器模式就是一个预留扩展点的绝佳方式。我们只需要在扩展点中埋入一个注册器,就可以轻松实现“既不改动现有框架、又容纳新的需求”了。
例如,借助前面那个注册器的代码示例,我们可以预留下这样一个扩展点:
/** 注册器;具体实现参见前面的例子。 */
public interface ServiceRegistry{
public Validator fetchValidator(Ticket t);
public MainService fetch(Ticket t);
public ExceptionHanlder fetchExceptionHandler(Ticket t);
}
public class BizFrame{
@Autowired
private ServiceRegistry serviceRegistry;
public void doBusiness(BizData data)throws Exception{
Ticket t = data.getTicket();
try{
// 从注册器中取出校验器
Validator validiator = serviceRegistry.fetchValidator(t);
if(validiator != null){
// 如果这里指定了校验器,那么做校验处理。校验出错直接跑异常
validator.valid(data);
}
MainService mainService = serviceRegistry.fetch(t);
mainService.doBusiness(data);
}catch(Exception e){
ExceptionHanlder exceptionHandler = serviceRegistry.fetchExceptionHandler(t);
if(exceptionHandler!=null){
exceptionHandler.handle(data, e);
}else{
throw e;
}
}
}
}
上面是一段框架代码。如果我们要针对 Ticket.C 增加一套新的校验、业务逻辑和异常处理,相信都知道要怎么做了吧!
多说两句。注册器模式非常实用,却非常小众。这是为什么呢?我想,主要问题还是“供”“需”不匹配。注册器模式提供了一个写框架的设计模式,而广大程序员需要的还是写CRUD的工具——或者说,广大老板需要的还是写CRUD的程序员。如果只需要写写CRUD就心满意足,那确实“奚以之九万里而南为”。
早些年钢铁行业搞“去产能”,推动了一波产业升级,我们现在“下饺子”、出岛链才有了底气。后来互联网行业传“35岁危机”,不知道大家有没有感受到“产业升级”的紧迫感和驱动力呢?升级方向有很多,不一定就是设计模式(实话说,这个方向也有点老掉牙了)。但有一点可以肯定:这些方向都是背对“CRUD”的。
无论去向何方,祝愿大家在升级的路上都能如愿以偿。
往期索引
《面向对象是什么》
从具体的语言和实现中抽离出来,面向对象思想究竟是什么?
《抽象》
抽象这个东西,说起来很抽象,其实很简单。
《高内聚与低耦合》
《细说几种内聚》
《细说几种耦合》
"高内聚"与"低耦合"是软件设计和开发中经常出现的一对概念。它们既是做好设计的途径,也是评价设计好坏的标准。
《封装》
《继承》
《多态》
——“面向对象的三大特性是什么?”
——“封装、继承、多态。”
《[5+1]单一职责原则》
单一职责原则非常好理解:一个类应当只承担一种职责。因为只承担一种职责,所以,一个类应该只有一个发生变化的原因。
《[5+1]开闭原则(一)》
《[5+1]开闭原则(二)》
什么是扩展?就Java而言,实现接口(implements SomeInterface)、继承父类(extends SuperClass),甚至重载方法(Overload),都可以称作是“扩展”。
什么是修改?在Java中,严格来说,凡是会导致一个类重新编译、生成不同的class文件的操作,都是对这个类做的修改。实践中我们会放宽一点,只有改变了业务逻辑的修改,才会归入开闭原则所说的“修改”之中。
《[5+1]里氏替换原则(一)》
《[5+1]里氏替换原则(二)》
里氏替换原则(Liskov Substitution principle)是一条针对对象继承关系提出的设计原则。它以芭芭拉·利斯科夫(Barbara Liskov)的名字命名。1987年,芭芭拉在一次名为“数据的抽象与层次”的演讲中首次提出这条原则;1994年,芭芭拉与另一位女性计算机科学家周以真(Jeannette Marie Wing)合作发表论文,正式提出了这条面向对象设计原则
《[5+1]接口隔离原则(一)》
《[5+1]接口隔离原则(二)》
一般我们会说,接口隔离原则是指:把庞大而臃肿的接口拆分成更 小、更具体的接口。
不过,这并不是接口隔离原则的定义。实际上,接口隔离原则的定义其实是这样的……客户端不应被迫依赖它们压根用不上的接口;或者反过来说,客户端应该只依赖它们要用的接口。
《[5+1]依赖倒置原则(一)》
《[5+1]依赖倒置原则(二)》
在Java世界里谈到依赖倒置原则,相信90%的人都会立即想起SpringIOC;还有9%的人会想起“面向接口编程”。最多只有1%的人能想起依赖倒置原则的真正定义。
《[5+1]迪米特法则(一)》
《[5+1]迪米特法则(二)》
迪米特法则可以用一句话概括:Only talk to your friends。
“只和你的朋友说话”,这是1987年的表述。2003/2004年左右,Karl Liebertherr对迪米特法则做了一次升级:由“Only talk to your friends”升级为了“Only talk to your friends who share your concerns”——“只和与你同忧同乐的朋友说话”。
《常用设计模式-策略模式》
策略模式把属于同一类别的不同行为封装为某种“策略抽象”,而把这些行为统一为这个抽象下的某个“策略实现”。这样,我们就可以很灵活地决定在哪种场景下使用哪种“策略”了。
《常用设计模式-工厂模式(一)》
《常用设计模式-工厂模式(二)》
工厂模式的定义其实很简单:提供一个独立组件,用以根据不同条件选择并构建不同实例。这个组件就是“工厂”。