18 年,仍是个小白的时候写了一篇 谈谈模板方法模式
这篇文章也不想再复制一些难以读懂的概念,简而言之,模板方法就是提取一些公用代码到父类,然后由子类去具体实现父类里没有实现的代码逻辑,目的就是为了代码的复用
这个业务是这样的,由于系统运行在某个不可描述的内网里面,数据交换都是通过 FTP 服务器来进行的,所以当数据发送到 FTP 服务器之后,我们就需要定时拉取上面的文件下来,并自定义文件的业务处理流程,处理成功之后就需要把文件删除掉
所以就有了这么样的一个设计:
abstract class AbstractFtpConsumeJob extends Task { public void run() { var client = buildFtpClient(); // 遍历 FTP 上的文件 // 文件处理 if (consume(file)) delete(file); // ... } abstract FTPClient buildFtpClient(); // 其他待子类实现的抽象方法...}class ConcreteFtpJob extends AbstractFtpConsumeJob { boolean consume(file) {...} FTPClient buildFtpClient() {...} // 其他抽象方法实现}
这个设计直至现在用的仍很舒服,但经过了几次改进,主要就是当每次新定义任务时,子类基本上只要实现 consume
方法而已,所以我将 AbstractFtpConsumeJob
的所有抽象方法都替换成了 protected 的实现,这样不仅有了默认的实现,同时也满足子类自定义化的需求
同时由于业务的原因,需要从许多接口上爬取数据或者需要传递一些数据到三方接口上,这个设计就是提取公用的 HTTPClient 到基类,然后提供一个发送请求及转换响应的 protected 方法,类似下面:
abstract class BaseRemote { protected HTTPClient httpclient; protected Resposne execute(Request request){...} protected byte[] convertResponse(Response response){...}}
事实证明,这个设计很失败,由于选择了错误的代码复用方式,结果就导致了这个 BaseRemote
的子类的个性化需求基本上无法在基类上面复用,于是只能一层层继承往上套,一直小修小补
abstract class BaseBussiness1Remote extends BaseRemote {...}abstract class BaseBussiness2Remote extends BaseBussiness1Remote {...}
对于有代码洁癖的人,一旦看到重复的代码就恨不得把它们提取出来,使用一个统一入口来管理,我早期也是这样的,但随着系统的演进,这种不加以评估的复用是有问题的
看到过许多人接手到一套屎山系统,为了实现一个简单的需求,改了一个小小的地方,需求是完成了,但另外一个看似不相关的地方甚至整个系统都崩了,这其中有一部分原因就是没有评估代码的复用所带来的的影响
所以在复用封装之前一定要评估好,如果一个方法复用另外一个方法之前,还需要看看这个方法的实现而非通过 API 来了解方法的具体作用,那这个封装复用就是失败的
抽象也是代码复用的一种,对于上面构建的那些抽象基类,都不是从一开始就设计抽象类,都是从一堆堆极为相似的类中提取出来的
软件工程有句极为经典的话
过早优化是万恶之源
根据我的经验,过早抽象也是万恶之源,有些早期看似一样的代码,随着系统演进发展到后面会变得完全不一样,如果过早把它们抽取到同一个地方,一旦两个位置出现不同的需求,就会出现是改这个公用方法呢,还是改外部方法两难的境地
这种方式的重用范围一般在本类的范围之内
void main() { // ... // ...}↓void main() { f1() // ...}void f1() {...}
另外一种就是工具类吗,工具类的代码复用就突破了作用域的限制,常见的 xxxUtils, xxxHelper 都是属于工具类
StringUtils.hasLength("cxk");RedisHelper.set("a", "b");
许多设计模式都有代码复用的作用,最明显的当然就是模板方法模式
其他的诸如组合、单例等都有复用的作用