把书读薄 | 《设计模式之美》设计原则(上)(二)

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
云数据库 RDS MySQL Serverless,0.5-2RCU 50GB
云数据库 RDS MySQL Serverless,价值2615元额度,1个月
简介: 继续啃《设计模式之美》,本文是 设计原则(15-22)浓缩总结,实战部分(23-26)拆到下节,没做过Web开发,要点时间消化。 还是那句话:二手知识加工难免有所纰漏,感兴趣有时间的可自行查阅原文,谢谢。

④ 接口隔离原则 (ISP,Interface Segregation Principle)


客户端不应该被强迫依赖它不需要的接口,这里的客户端可以理解为 接口的调用者或使用者,对应的服务端就是 接口的设计者或提供者


网络异常,图片无法展示
|


这里的 接口 只是一个方便描述的词汇,为了将我们的注意力从具体实现细节中抽离出来,可以将其理解为下面三种东西:


1) 一组API接口集合


比如:提供了一组用户相关的API给其他系统使用,包含注册、登录、获取用户信息等。现在,后台管理系统要实现一个删除用户的功能,直接在原有用户Service加上这个接口可以解决这个问题,但也带来了安全隐患。


所有用到这个用户Service的系统都可以调用这个接口,不加限制地被其他系统调用,很可能造成用户误删。


最好的解决方法是接口鉴权方式限制,而在代码设计层面,则可以参照隔离原则,将删除接口单独放到另外一个Service中,然后此Service只打包提供给后台管理系统使用。


2) 单个API接口或函数


函数的设计要功能单一,不要将多个不同的功能逻辑放在一个函数中实现。比如下面的代码示例:


public class Statistics {
    private Long max;
    private Long min;
    private Long average;
    private Long sum;
    // ...省略getter和setter等方法
}
public Statistics count(Collection dataSet) {
    Statistics statistics = new Statistics();
    // 计算max
    // 计算min
    // 计算average
    // 计算sum等
    return statistics;
}


这里的count()函数是否符合职责单一,还得看场景,比如每次统计count()中所有的统计信息都涉及,那就是职责单一的。


如果有些只用到max、min,有些只用到sum、average,那每次调用count()都要计算一遍所有统计信息,就很必要了,应该将其拆分成粒度更细的多个统计函数,如:


public Long max(Collection dataSet) { //... }
public Long min(Collection dataSet) { //... }


单一职责原则 跟 接口隔离原则 的区别


两者有点类似,但前者针对的是 模块、类、接口的设计,后者则更侧重于 接口的设计,思考角度也不同,它提供了一种判断接口是否职责单一的标准:通过调用者如何使用接口来间接地判定,如果调用者 只使用了部分接口或接口的部分功能,那接口的设计就不够职责单一。


3) OOP 中的接口概念


举例:项目中用到三个外部系统:Redis、MySQL、Kafka,每个系统对应一个配置信息的类:


public class RedisConfig {
    private ConfigSource configSource; //配置中心(比如zookeeper)
    private String address;
    private int timeout;
    private int maxTotal;
    //省略其他配置: maxWaitMillis,maxIdle,minIdle...
    public RedisConfig(ConfigSource configSource) {
        this.configSource = configSource;
    }
    public String getAddress() {
        return this.address;
    }
    //...省略其他get()、init()方法...
    public void update() {
      //从configSource加载配置到address/timeout/maxTotal...
    }
}
public class KafkaConfig { //...省略... }
public class MysqlConfig { //...省略... }


接着增加需求:Redis 和 Kafka 配置信息需要热更新,MySQL不需要,抽象一个更新接口 Updater


public interface Updater { void update(); }
public class RedisConfig implements Updater() { void update() { /*...具体实现*/ } }
public class MysqlConfig implements Updater() { void update() { /*...具体实现*/ }}
public class Application {
    ConfigSource configSource = new ZookeeperConfigSource(/*省略参数*/);
    public static final RedisConfig redisConfig = new RedisConfig(configSource); 
    public static final KafkaConfig kafkaConfig = new KakfaConfig(configSource); 
    public static final MySqlConfig mysqlConfig = new MysqlConfig(configSource);
    public static void main(String[] args) { 
        ScheduledUpdater redisConfigUpdater = new ScheduledUpdater(redisConfig, 300, 300);  
        redisConfigUpdater.run(); 
        ScheduledUpdater kafkaConfigUpdater = new ScheduledUpdater(kafkaConfig, 60, 60); 
        kafkaConfigUpdater.run(); }
}


接着又加了一个新需求:MySQL 和 Redis 需要监控功能,Kafka不需要,抽象一个监控接口 Viewer


public interface Viewer { 
    String outputInPlainText(); 
    Map output();
}


同理 MySqlConfig 和 RedisConfig实现此接口重写方法,ScheduledUpdater只依赖Updater接口而不用被强迫依赖不需要的Viewer接口,满足接口隔离原则。


如果不遵守这个原则,而使用一个大而全的Config接口,让每个Config继承,这样的结果是做了一些 无用功


MySQL不需要热更新,却需要实现热更新的update()方法,Kafka不需要监控,也要实现监控相关的方法。出之外,往Config中添加新的接口,所有的实现类都要改动。


⑤ 依赖反转原则 (DIP,Dependence Inversion Principle)


高层模块不要依赖低层模块,应该通过抽象来互相依赖,抽象不要依赖具体实现细节,具体实现细节依赖抽象


网络异常,图片无法展示
|


看定义有点难理解,以上图为例:


上层是灯,下层是墙里的电线,灯直接依赖电线的话,意味着你要手动把灯焊到电线上,灯才能亮起来(高层依赖低层)。


挺智障的对吧?它们间的交互其实就是 连接,不关心灯这边要怎么连,电线那边要怎么连,而是 抽象 出一个 协议/约束/规范连接插座


插座可不管你是灯、冰箱、4平方线还是6平方线(不依赖具体实现细节),但你要连接的话都得按插座规范来走(具体实现细节依赖抽象)。


使用DIP的意义:


  • 有效地控制代码变化的影响范围 → 统一接口,接口不变,外部系统怎么变,内部系统不用变。


  • 使代码具有更强的可读性和可维护性 → 代码通过统一抽象后,功能相同的处理都在同一个地方。


用上面的灯和电线写个例子:


public class Lamp {
    void weld(String origin) {
        System.out.println("焊接到" + origin);
    }
}
public class Wire {
    String pull() { return "墙里电线"; }
}
public class ConnectProcessor {
    private Lamp lamp;
    private Wire wire;
    public ConnectProcessor(Lamp lamp, Wire wire) {
        this.lamp = lamp;
        this.wire = wire;
    }
    public void connect() {
        lamp.weld(wire.pull());
    }
    // 测试用例
    public static void main(String[] args) {
        Lamp lp = new Lamp();
        Wire we = new Wire();
        ConnectProcessor processor = new ConnectProcessor(lp, we);
        processor.connect();    // 输出:焊接到墙里电线
    }
}


高层组件ConnectProcessor,低层组件Lamp和Wire,代码看似简单,却有两个问题,第一个:ConnectProcessor复用性差,要复用的地方要写很多重复代码,引入抽象隔离变化,定义一个单独的的IConnectProcessor接口,ConnectProcessor实现此接口:


public interface IConnectProcessor {
    void connect(Lamp lamp, Wire wire);
}
public class ConnectProcessor implements IConnectProcessor {
    @Override
    public void connect(Lamp lamp, Wire wire) {
        lamp.weld(wire.pull());
    }
    public static void main(String[] args) {
        Lamp lp = new Lamp();
        Wire we = new Wire();
        ConnectProcessor processor = new ConnectProcessor();
        processor.connect(lp, we);
    }
}


清爽不少,接着第二个问题:高层组件依赖低层组件,后者发生改变也会影响前者,所以要对低层组件也进行抽象:


public interface ILamp {
    void weld(String origin);
}
public interface IWire {
    String pull();
}
public class Lamp implements ILamp {
    @Override
    public void weld(String origin) {
        System.out.println("焊接到" + origin);
    }
}
public class Wire implements IWire {
    @Override
    public String pull() {
        return "墙里电线";
    }
}
public interface IConnectProcessor {
    void connect(ILamp lamp, IWire wire);
}
public class ConnectProcessor implements IConnectProcessor {
    @Override
    public void connect(ILamp lamp, IWire wire) {
        lamp.weld(wire.pull());
    }
    public static void main(String[] args) {
        ILamp lp = new Lamp();
        IWire we = new Wire();
        ConnectProcessor processor = new ConnectProcessor();
        processor.connect(lp, we);
    }
}


从ConnectProcessor依赖Lamp和Wire (上层依赖低层),到抽象出规则IConnectProcessor,然后模块的具体实现都依赖这个规则,这就是 依赖倒置 !!!


网络异常,图片无法展示
|


说完依赖反转,再说说经常听到的其他三个名词以防混淆:


1) 控制反转(IOC,Inversion Of Control)


一种较笼统的 设计思想,一般用来指导 框架 层面的设计,就是将 程序执行流程的控制交给框架完成


2) 依赖注入(DI,Dependency Injection)


实现IOC的 设计模式在类外创建依赖对象,通过不同方式将对象提供给类(构造函数、属性、方法)。


3) 依赖注入框架(DI Framework)


用于实现自动依赖注入的框架,管理对象的创建及生命周期,并提供向类注入依赖项的具体实现,不用开发者手动创建和管理对象,现成的注入框架有很多,如Java Spring,Android中的ButterKnife、Dagger2等。


相关文章
|
7月前
|
设计模式 关系型数据库
【设计模式——学习笔记】设计模式简介+七大设计原则介绍(下)
【设计模式——学习笔记】设计模式简介+七大设计原则介绍
22 0
|
7月前
|
设计模式 前端开发 算法
设计模式之设计原则
程序设计的要遵循的一些理论,也可以理解为程序设计的一种要求和目标,是面向对象程序设计的基石,也是面向对象程序设计的质量保障和依据。
39 0
|
8月前
|
设计模式 Java 关系型数据库
Java设计模式中的设计原则 2
Java设计模式中的设计原则
56 0
|
4月前
|
设计模式 关系型数据库 程序员
【设计模式】设计原则
【1月更文挑战第12天】【设计模式】设计原则
|
5月前
|
设计模式 程序员
设计模式-设计原则
设计模式-设计原则
|
7月前
|
设计模式 存储 Java
JAVA设计模式第一讲:设计原则
JAVA设计模式第一讲:设计原则
|
7月前
|
设计模式 Java 程序员
【设计模式——学习笔记】设计模式简介+七大设计原则介绍(上)
【设计模式——学习笔记】设计模式简介+七大设计原则介绍
29 2
|
8月前
|
设计模式 算法 Java
Java设计模式中的设计原则 1
Java设计模式中的设计原则
67 0
|
10月前
|
设计模式 算法 关系型数据库
设计模式——设计模式简介、分类及面向对象设计原则
23 种设计模式中有些模式今天已经不流行了,有些模型已经被语言机制替代了,有些模式你可能常常会忘记,但这些都不重要,重要的是设计原则,因为有了这些设计原则,你有可能发明自己的模式,你也可以理解未来千千万万其他领域的模式。
49 1
|
10月前
|
设计模式 关系型数据库 区块链