1.前言
设计模式我们多少都有些了解,但是往往也只是知道是什么。
在真实的业务场景中,你有用过什么设计模式来编写更优雅的代码吗?
我们更多的是每天从产品经理那里接受到新需求后,就开始MVC一把梭,面向sql编程了。
我们习惯采用MVC架构,实时上是非常容易创建很多贫血对象模型,然后写出过程式代码。我们使用的对象,往往只是数据的载体,没有任何逻辑行为。我们的设计过程,也是从ER图开始,以数据为中心进行驱动设计。一个需求一个接口,从controller到service到dao,这样日复一日的CRUD。
什么设计模式?根本不存在的!
今天,我们尝试从常用设计模式(工厂模式、代理模式、模版模式)在CRUD中的可落地场景,希望能给大家带来一些启发。
2.理解设计模式
设计模式(Design pattern),不是前人凭空想象的,而是在长期的软件设计实践过程中,经过总结得到的。
使用设计模式是为了让代码具有可扩展性,实现高聚合、低耦合的特性。
世上本来没有设计模式,写代码的人多了,便有了设计模式。
面向对象的设计模式有七大基本原则:
- 开闭原则(Open Closed Principle,OCP)
- 单一职责原则(Single Responsibility Principle, SRP)
- 里氏代换原则(Liskov Substitution Principle,LSP)
- 依赖倒转原则(Dependency Inversion Principle,DIP)
- 接口隔离原则(Interface Segregation Principle,ISP)
- 合成/聚合复用原则(Composite/Aggregate Reuse Principle,CARP)
- 最少知识原则(Least Knowledge Principle,LKP)或者迪米特法则(Law of Demeter,LOD)
简单理解就是:开闭原则是总纲,它指导我们要对扩展开放,对修改关闭;单一职责原则指导我们实现类要职责单一;里氏替换原则指导我们不要破坏继承体系;依赖倒置原则指导我们要面向接口编程;接口隔离原则指导我们在设计接口的时候要精简单一;迪米特法则指导我们要降低耦合。
过去,我们会去学习设计模式的理论,今天,我们尝试从常用设计模式(工厂模式、代理模式、模版模式)在CRUD中的可落地场景,希望能给大家带来一些实战启发。
3.设计模式实战案例
3.1工厂模式
1)工厂模式介绍
工厂模式应该是我们最熟悉的设计模式了,很多框架都会有经典的xxxxFactory,然后通过xxxFactory.create来获取对象。这里不详细展开介绍,给出一个大家耳熟能详的工厂模式类图应该就能回忆起来了。
工厂模式的优点很明显:
- 一个调用者想创建一个对象,只要知道其名称就可以了。
- 扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。
- 屏蔽产品的具体实现,调用者只关心产品的接口。
那么,实际业务开发该怎么落地使用呢?
2)需求举例
我们需要做一个HBase的管理系统,类似于MySQL的navicat或者workbench。
那么有一个很重要的模块,就是实现HBase的增删改查。
而开源的HBase-client已经提供了标准的增删改查的api,我们如何集成到系统中呢?
3)简单代码
@RestController("/hbase/execute") public class DemoController { private HBaseExecuteService hbaseExecuteService; public DemoController(ExecuteService executeService) { this.hbaseExecuteService = executeService; } @PostMapping("/insert") public void insertDate(InsertCondition insertCondition) { hbaseExecuteService.insertDate(insertCondition); } @PostMapping("/update") public void updateDate(UpdateCondition updateCondition) { hbaseExecuteService.updateDate(updateCondition; } @PostMapping("/delete") public void deleteDate(DeleteCondition deleteCondition) { hbaseExecuteService.deleteData(deleteCondition); } @GetMapping("/select") public Object selectDate(SelectCondition selectCondition) { return hbaseExecuteService.seletData(selectCondition); } }
每次增加一个功能,都需要从controller到service写一遍类似的操作。
还需要构建很多相关dto进行数据传递,里面会带着很多重复的变量,比如表名、列名等查询条件。
4)模式应用
抽象接口
public interface HBaseCommand { /** * 执行命令 */ ExecResult execute(); }
抽象类实现公共配置
public class AbstractHBaseCommand implements HBaseCommand { Configuration configuration; AbstractHBaseCommand(ExecuteCondition executeCondition) { this.configuration = getConfiguration(executeCondition.getResourceId()); } private Configuration getConfiguration(String resourceId) { Configuration conf = HBaseConfiguration.create(); //做一些配置相关事情 //。。。。 return conf; } @Override public ExecResult execute() { return null; } }
工厂类生产具体的命令
public class CommandFactory { private ExecuteCondition executeCondition; public CommandFactory(ExecuteCondition executeCondition) { this.executeCondition = executeCondition; } public HBaseCommand create() { HBaseCommand hBaseCommand; switch (ExecuteTypeEnum.getTypeForName(executeCondition.getQueryType())) { case Get: return new GetCommand(executeCondition); case Put: return new PutCommand(executeCondition); case Delete: return new DeleteCommand(executeCondition); } return null; } }
一个执行接口,执行增删改查多个命令
public class ExecuteController { private ExecuteService executeService; public ExecuteController(ExecuteService executeService) { this.executeService = executeService; } @GetMapping public Object exec(ExecuteCondition executeCondition) { ExecResult execResult = executeService.execute(executeCondition); return transform(execResult); } }
service调用工厂来创建具体的命令进行执行
@Service public class ExecuteService { public ExecResult execute(ExecuteCondition executeCondition) { CommandFactory factory = new CommandFactory(executeCondition); HBaseCommand command = factory.create(); return command.execute(); } }
每次添加一个新的命令,只需要实现一个新的命令相关内容的类即可
3.2 代理模式
1) 模式介绍
代理模式也是大家非常熟悉的一种模式。
它给一个对象提供一个代理,并由代理对象控制对原对象的引用。它使得用户不能直接与真正的目标对象通信。
代理对象类似于客户端和目标对象之间的中介,能发挥比较多作用,比如扩展原对象的能力、做一些切面工作(打日志)、限制原对象的能力,同时也在一定程度上面减少了系统的耦合度。
2)需求举例
现在已经有一个client对象,实现了若干方法。
现在,有两个需求:
- 希望这个对象的方法A不再被支持。
- 希望对这个client的所有方法的执行时间进行记录。
3)简单代码
对原本的类进行修改
- 删除方法A(不兼容改动,如果别人有引用,可能会编译报错)
- 对每个方法的前后埋点计算时间(业务入侵太大,代码严重冗余)
4)模式应用
对于方法A的不再支持,其实有挺多办法的,继承或者静态代理都可以。
静态代理代码:
public class ConnectionProxy implements Connection { private Connection connection; public ConnectionProxy(Connection connection) { this.connection = connection; } @Override public Admin getAdmin() throws IOException { //抛出一个异常 throw new UnsupportedOperationException(); } @Override public boolean isClosed() { return connection.isClosed(); } @Override public void abort(String why, Throwable e) { connection.abort(why, e); } }
对于每个方法的前后计算埋点,可以使用动态代理进行实现。
public class TableProxy implements InvocationHandler { private Object target; public TableProxy(Object target) { this.target = target; } /** * 获取被代理接口实例对象 * * @param <T> * @return */ public <T> T getProxy() { return (T) Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { long current = System.currentTimeMillis(); Object invoke; try { invoke = method.invoke(target, args); } catch (Throwable throwable) { throw throwable; } long cost = System.currentTimeMillis() - current; System.out.println("cost time: " + cost); return invoke; } }
3.3 模板方法
1) 模式介绍
定义一个操作中的流程框架,而将一些流程中的具体步骤延迟到子类中实现。
模板方法使得子类可以不改变一个流程框架下,通过重定义该算法的某些特定步骤实现自定义的行为。
当然,最便利之处在于,我们可以保证一套完善的流程,使得不同子类明确知道自己需要实现哪些方法来完成这套流程。
2)需求举例
其实模板方法是最容易理解的,也非常高效。
我们最常用模版方法的一类需求就是工单审批流。
具体来说,假如我们现在需要定义一套流程来实现一个工单审批,包含工单创建、审批操作、事件执行、消息通知等流程(实际上流程可能会更加复杂)。
而工单的对象非常多,可以是一个服务的申请、一个数据库的变更申请、或者是一个权限申请。
3)简单代码
每个工单流程写一套代码。
- 重复工作多;
- 流程某些关键环节可能会缺失,比如事件执行以后忘记通知了。
4)模式应用
定义一个接口,里面包括了若干方法
public interface ChangeFlow { void createOrder(); boolean approval(); boolean execute(); void notice(); }
在一个流程模版中,拼接各个方法,实现完整工作流
public class MainFlow { public void mainFlow(ChangeFlow flow) { flow.createOrder(); if (!flow.approval()){ System.out.println("抱歉,审批没有通过"); } if (!flow.execute()) { System.out.println("抱歉,执行失败"); } flow.notice(); } }
然后,可以在AbstractChangeFlow里面实现通用的方法,比如approval、notice,大家都是一样的逻辑。
public class AbstractChangeFlow implements ChangeFlow { @Override public void createOrder() { System.out.println("创建订单"); } @Override public boolean approval() { if (xxx) { System.out.println("审批通过"); return true; } return false; } @Override public boolean execute() { //交给其他子类自己复写 return true; } @Override public void notice() { System.out.println("notice"); } }
最后,就根据具体的工单来实现自定义的excute()方法就行了,实现DbFlow、MainFlow就行了。
4.总结
学习设计模式最重要的就是要在业务开发过程中保持思考,在某一个特定的业务场景中,结合对业务场景的理解和领域模型的建立,才能体会到设计模式思想的精髓。
如果脱离具体的业务场景去学习或者谈论设计模式,那是没有意义的。
有人说,怎么区分设计模式和过度设计呢?
其实很简单。
1)业务设计初期。如果非常熟悉业务特性,理解业务迭代方向,那么就可以做一些简单的设计了。
2)业务迭代过程中。当你的代码随着业务的调整需要不断动刀改动,破坏了设计模式的七大原则,尤其是开闭原则,那么,你就该去考虑考虑使用设计模式了。