设计模式-搞个接口,留有余地,让你我不再尴尬

设计模式,Design Patterns,Pattern,翻译为“模式”总感觉不够接地气,用今天的话来说可以叫“套路”。设计模式就是写代码的过程中一些常规打法和套路。只不过被列入GOF的套路是那些大牛们巨人们在长期的生产劳动中总结出来的公认的并且能解决一大部分人的就业问题的最佳实践。

这些年来,民间流传着一种说法就是 “模式无用论”。就是认为设计模式没什么鸟用。 由于本人资历尚浅,也不知道对不对。但每每听到这些话的时候,我就在想,spring很多人都说好,他不就是用了一个个设计模式架构起来的吗?

这里就不拿冷饮店的生产流程来讲解模板模式或者装饰者模式了。我就把我在写代码过程中遇到的一些设计模式的内容分享给大家。

在分享这些之前,我们还是来回顾下设计模式的基本内容吧。

你肯定看到过很多的设计模式的书籍,一般都会分别介绍每一种模式,而且把每种模式的内部细节一一对比和分析,来强化每种模式一定要有的样子。

但你也许也发现了,就是有很多模式其实都长得差不多。没错,其实设计模式就是围绕着java的面向对象和java语言本身有的特性做各种闪转腾挪,从而优雅的解决一些实际开发中的一些问题。

别看,GOF列了那么多,其实可大体分三类:

创建型

这一类就是围绕着怎么new出来一个对象而不遗余力,各种姿势的new对象。

行为型

这一类,核心的就是使用interface来搞,所以你看到很多是设计模式都离不开搞个接口。

结构型

这一类设计模式,就是围绕着,怎么把类和类进行组合,配合,组装。

既然是套路,肯定就是能给我带来很多好处。比如你玩耍王者农药,比如玩亚瑟的套路就是先去适当打野,好有个大,然后再去砍杀。这就是套路。

回到设计模式。我认为设计模式的核心追求就是 整个架构的可持续性。或者更直白的讲就是如何更好的应对变化。再往外说就是:面对新的需求,我们能够以更优雅的姿势 回答 需求提出者 的问题。

可持续性主要有两层意思:

1、一层意思是如果出现问题,需要回溯我可以优雅支持。

2、一层意思是如果来了新的需求,我可以优雅的应对。

如果解决了上面两个诉求,我想,这个事情就应该是可持续的。

接下来我就列举几个在我开发工作中遇到的几个比较典型的工厂模式的例子。

案例一、多缓存支持

一个是我们在开发缓存组件的cilent的时候,需要支持同一应用下支持多个缓存实例的同时存在。比如我同时可以使用redis和ehcache。那么这时候我就希望他们在项目启动时一次性全部实例化好,然后存在一个map中。这样的话,我就可以在使用缓存的业务方法上加缓存注解,然后指定对应的缓存实例ID,然后使用这个缓存实例进行存取操作,如果想使用另一种缓存,则只需要修改实例ID就可以了。

案例二、多通知渠道支持

还有最近我们在做一个监控告警中心。里边需要支持邮件、短信、电话语音、微信等多种通知渠道。而每个通知方式的实现逻辑是不一样的。那么这时候我就希望这些逻辑开发者可以自定义。然后在应用启动时一次性初始化好自定义的这些实例,然后保存到map中。在真正需要用这些逻辑的时候再通过getChannnel的方式获取对应的逻辑。由于在具体getChannel的时候我并不知道具体实现,所以只能基于接口来抽象。

案例三、WebMvc URL-Mapping

还有之前我自己尝试过去写一个简单的webserver。其实就是我们经常用到的controller那一层的映射。就是在应用启动时自动扫描所有含有microservice注解的类,然后实例化好以后,然后把注解上的URL mapping作为key,当前类对象作为value存入map。这样就完成了URLmapping。然后在具体的httpserver的总handler中根据request请求的URL来去map中寻找还URL对应的handler实现。当然这里的map是被封装到一个context中的。你可以认为这个就是那个XXXFactory。没错,包括spring的webmvc也是这么实现的。

其实你发现在我们的开发过程中除了我们使用的那些开源框架在用factory pattern。在我们自己的开发中也经常需要用到工厂模式,而且适当的使用该模式也能解决我们像上面一样的问题。

核心就是基于接口然后加个map。这就是工厂模式的核心。

使用接口,让在遇到新的需求时候,不用去修改你写好的核心代码。因为你的核心代码都是接口。你只需要基于指定的接口编写新的实现就可以了。你发现没?你并没有修改一行代码。只是单独增加了一个实现类,然后一切就优雅满足。这不就是我们经常说的那句:面向修改关闭,面向扩展开放吗。至少对于我自己来说,能不改代码就不改。为了“不改”这个目标,我会想尽一切办法来完成这一目标,比如我总是会用接口;还有有时候在向前端push一个具体的满足UI的数据entity,为了不修改dao层的entity,我甚至会在自己的业务类中写一个私有的entity然后继承现有的dao的entity,然后添加一些属性,总之我就是不喜欢修改现有代码,我总是喜欢扩展。

原因很简单,我扩展的属性只有这一处用到,如果基于dao entity来添加属性,对于其他场景来说,这一个并不通用的属性就是一个累赘和负担。

而且修改现有代码总有种隐隐的不安,而且我新建一个类似乎要安全很多。这样的话,即使我新的代码出现问题了,我也可以快速的回退(或者叫回滚)到之前的实现中去。有时候设计模式在茫茫的代码中会充当着像版本管理软件一样的角色。它让你在具体的代码实现内部可以保留之前版本的代码,如果想要升级只是基于原有的代码文件进行扩展和覆盖就可以了。而不需要基于之前的代码来硬修改。

说到修改,还有一个小故事分享,一天某部门的A来找我说,缓存组件的使用的序列化方式不满足它们的需求,问我能不能修改。然后我告诉他,缓存组件默认的序列化实现是基于kyro的,如果你觉得不满足你的需求,你可以自定义自己的序列化实现,然后配置就是了。

你想想如果当初没有基于接口来做,那么这时候我只能告诉那位哥们说那你没法使用了。再想的极端一点,如果默认的序列化实现有bug,恰好在他所在的场景下出现问题了。如果当初没有用接口来抽象,估计这会我已经在仓促的开始修改实现了,然后仓皇的去走流程发布新版本了。

这种仓皇失措显然已经露出了不可持续的端倪。长此以往,代码的质量能够保证吗?

留个接口,留有余地,彼此都不至于太尴尬!

接下来我们再回顾下工厂模式的典型代码吧:

总是得先有个接口:

public interface Shape {
   void draw();
}

然后是两个不同的实现:

public class Rectangle implements Shape {
   public void draw() {
      System.out.println("Inside Rectangle::draw() method.");
   }
}
public class Circle implements Shape {
   public void draw() {
      System.out.println("Inside Circle::draw() method.");
   }
}

然后就是XXXFactory上阵了:

public class ShapeFactory {
  final static Map<String, Supplier<Shape>> map = new HashMap<>();
  static {
    map.put("CIRCLE", Circle::new);
    map.put("RECTANGLE", Rectangle::new);
  }   
  public Shape getShape(String shapeType){
     Supplier<Shape> shape = map.get(shapeType.toUpperCase());
     if(shape != null) {
       return shape.get();
     }
     throw new IllegalArgumentException("No such shape " + shapeType.toUpperCase());
  }
}

然后我们运行main:

public class FactoryPatternDemo {
   public static void main(String[] args) {
     Supplier<ShapeFactory> shapeFactory =  ShapeFactory::new;

     shapeFactory.get().getShape("circle").draw();
    
     shapeFactory.get().getShape("rectangle").draw();      
   }
}

这里的代码还是典型的圆和矩形的例子。但这里使用到了一些java8的特性,比如Supplier等等。

接口和不同实现这几乎是标配。除了这个再除去main类。你发现其实就只有一个类你需要特别关注下,而这个类又特别简单,就是里边持有一个map。然后暴露一个map的get方法而已。就是那个XXXFacotry类,工厂类。

就是这么简单,但却有如此神奇的魔力,我们使用的webmvc urlmapping、多个缓存实例支持还有上面说到的多通知渠道支持都是基于工厂模式来实现。

当然了,在具体的生产代码中不会像示例中那样直接在工厂类里初始化map中的数据,而是通过setter或构造函数来传入。

OK,说了这么多,你也许会发现这个工厂类甚至都不是最重要的,最重要的还是java的“接口”!

本文主要拿工厂模式来举例,工厂模式从主要动作上来看应该归属于上面三类中的创建型,但里边其实也包含了行为型的一些内容,比如一个接口,多个行为实现。这不就是典型的行为型么?!

所以说,无论是哪一种类型,都离不开接口和继承这些java基本的特性支持。

既然是这样那么就从接口开始吧,还是那句话留个接口,留个余地,让彼此都不至于太尴尬!

什么时候适合使用接口?

足球场的运动员的肌肉和行为都会有惯性记忆,那么我们写代码也会有惯性记忆。什么情况出现的时候该使用接口呢?一直比较粗暴的做法就是什么都先搞个接口,这当然比较保险,但会造成接口滥用。这里列举几个我自己的实践:

1、当你还没想清楚具体实现的时候。比如你正在搭建主要执行步骤,只是想好了要大体做那几大步,具体每一步的具体实现你可能还没有规划好,或者某一步可能需要其他部门的人来写实现。这时候你只能在你的逻辑中写个接口:

public class Demo {
    .....
    private Doer doer;

    public Demo(Doer doer){
        this.doer=doer;
    }

    public void doIt(){
        .....
        doer.doSomething();

        ......
    }

}
public interface Doer {
     void doSomething();
}

比如上面的Demo类中,你也许并没有想好具体要做什么。所以这时候你就可以定义一个接口,然后通过构造函数或者setter来传入具体实现。具体实现你暂时没有想好,完全可以暂时不做。

2、当你已经明确知道了有很多种方案要同时支持的时候。比如你已经很清楚了告警需要支持四种通道的通知发送:微信、电话、短信、邮件。这显然是四种不同的实现方式,但他们都同属一个抽象就是“发送”。而且在具体的告警执行代码中肯定是循环遍历每一个然后执行具体的逻辑。然而循环遍历,只能是基于接口才普适,所以很明显这个时候你需要定义类似下面这样的接口:

public interface Channnel {
    Result send(Message msg);
}

3、当你自己写了一个实现,但害怕不能满足所有人的需求。这时候你可以基于接口,自己写个默认实现,然后向项目接入方或使用者暴露接口传参。比如上面提到过的缓存序列化的例子。

4、当你希望其他人来做具体实现时。这个和上面的一些情况有些重合,但当你脑中出现类似念头时,你可以不假思索的想到接口。

以上只是一些个人总监的常见情况,当你对接口慢慢熟练并深入后,然后就可以跳出任何套路,随机应变的使用了。也就是熟能生巧,熟了自然就会发现各种奇技淫“巧”。

不变应万变!接口,是为不变,让你不改一行核心代码!接口,是为改变,让你支持万千实现。多么神奇的魔法!

搞个接口,留有余地,让你我不再尴尬!

原文发布于微信公众号 - ImportSource(importsource)

原文发表时间:2017-05-01

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏坚毅的PHP

HBase client访问ZooKeeper获取root-region-server DeadLock问题(zookeeper.ClientCnxn Unable to get data of zn

2012年11月28日 出现故障," Unable to get data of znode /hbase/root-region-server" 问题比较诡异...

5604
来自专栏CDA数据分析师

精选26个Python实用技巧,想秀技能先Get这份技术列表!

【导读】Python 虽然是脚本语言,但是因为其易学,迅速成为科学家的工具,从而积累了大量的工具库、架构,人工智能涉及大量的数据科学,用 Python 是很自然...

1182
来自专栏海说

深入理解计算机系统(3.1)---走进汇编的世界

  本系列拖了蛮久了,主要是因为LZ写的时候其实刚看到第二章,因此这一段时间快速看了下第三章,并花了点时间沉淀了一下,这才耽误了下来。

973
来自专栏轮子工厂

如果你想学好Python,这几本书说不定可以帮助到你哦

752
来自专栏互联网杂技

如何去了解JavaScript引擎的工作原理

1. 什么是JavaScript解析引擎? 简单地说,JavaScript解析引擎就是能够“读懂”JavaScript代码,并准确地给出代码运行结果的一段程序。...

3957
来自专栏专知

【干货】如何写代码 -编程内功心法

写代码就是学一门语言然后开始撸代码吗?看完了我的《GoF设计模式》系列文章的同学或者本身已经就是老鸟的同学显然不会这么认为。 编程是一项非常严谨的工作!虽然我们...

3538
来自专栏Pythonista

Python之路,Day1 - Python基础1

python的创始人为吉多·范罗苏姆(Guido van Rossum)。1989年的圣诞节期间,吉多·范罗苏姆为了在阿姆斯特丹打发时间,决心开发一个新的脚本解...

1772
来自专栏我的小碗汤

使用pprof优化golang性能

Donald E.Knuth说过一句非常著名的话,过早的优化是万恶之源。原文如下:

1964
来自专栏企鹅号快讯

实战:从Python分析17-18赛季NBA胜率超70%球队数据开始…

干货 观点 案例 资讯 我们 ? 撸主: Casey 岂安业务风险分析师 主要负责岂安科技RED.Q的数据分析和运营工作。 就在昨天,12月19日,科比再...

2677
来自专栏敏捷开发&项目管理

TDD 一个简单的例子

我们按照 TDD的1个准备步骤+关键5步来看做一个小例子。 需求: 假设我有一个叫Dollar的class, 那它有个方法叫做Times. 我现在的目的是要实现...

3718

扫码关注云+社区

领取腾讯云代金券