前言

我常常觉得人们低估了设计模式的作用和意义。它们不仅是简历上的金边、程序员的黑话,也不仅是常见业务的常用处理方式或经验总结。

设计模式不仅是这些,它们更是面向对象思想理论结合实践的切入点。我们前面聊过抽象、高内聚低耦合、封装继承多态、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”——“只和与你同忧同乐的朋友说话”。

《常用设计模式-策略模式》

策略模式把属于同一类别的不同行为封装为某种“策略抽象”,而把这些行为统一为这个抽象下的某个“策略实现”。这样,我们就可以很灵活地决定在哪种场景下使用哪种“策略”了。

《常用设计模式-工厂模式(一)》

《常用设计模式-工厂模式(二)》

工厂模式的定义其实很简单:提供一个独立组件,用以根据不同条件选择并构建不同实例。这个组件就是“工厂”。