阅读 637

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

0x0、引言

🤡驾照到手,继续啃《设计模式之美》,本文是 设计原则(15-22)浓缩总结,实战部分(23-26)拆到下节,没做过Web开发,要点时间消化。

还是那句话:二手知识加工难免有所纰漏,感兴趣有时间的可自行查阅原文,谢谢。


0x1、SOLID原则

并非单纯的一个原则,而是由下述五个设计原则组成,看到几个有趣的图片顺便贴上,来源:SOLID Development Principles – In Motivational Pictures

① 单一职责原则 (SRP,Single Responsibility Principle)

一个类或模块只负责完成一个职责(或功能),就是说:不要设计大而全的类,要设计粒度小、功能单一的类。

只是因为你能够,不代表你需要

举个例子:

一个类中即包含了订单的操作,又包含了用户的操作,而订单和用户是两个独立的业务领域模型(DDD),将两个不相干的功能放在一个类中,就违反了单一职责原则,可以将类拆分成粒度更小、功能更单一的两个类:订单类 和 用户类。

但,大多数时候,类是否职责单一,却很难拿捏,比如:

public class UserInfo {
    private long userId;
    private String userName;
    private String avatarUrl;
    private String email;
    private String telephone; 
    private long createTime; 
    private long lastLoginTime; 
    private String provinceOfAddress; //省 
    private String cityOfAddress; // 市 
    private String regionOfAddress; // 区 
    private String detailedAddress; // 详细地址
    // ...省略其他属性和方法
}
复制代码

两种不同的观点:

  • UserInfo类包含的都是跟用户相关的信息,都属于用户这个业务模型,满足单一职责;
  • 地址信息在类中占比较高,可以将其拆到一个独立的UserAddress类中,拆分后的两个类职责更加单一;

哪种观点更对?

答:要根据具体的应用场景,如果地址信息和其他信息一样只是单纯用来展示,现在的设计就是合理的。

如果其他功能模块也会用到用户的地址信息,最好还是拆一拆。

持续重构

不要一开始就想着拆多细,可以先写一个粗粒度的类,满足业务需求,随着业务的发展,粗粒度的类越来越庞大,代码越来越多,此时,再来将这个类拆分成多个更细粒度的类。

判断类是否职责单一的几个技巧

  • 类中代码行数、函数或属性过多 → 会影响代码的可读性和可维护性,考虑拆下;
  • 类依赖的其他类过多,或依赖类的其他类过多 → 不符合高内聚低耦合,考虑拆下;
  • 私有方法过多 → 考虑下能否独立到新的类中,设置为public方法,供更多类使用,提高复用性;
  • 给类命名时困难 → 很难用一个业务名词概括,说明类的职责定义得可能不够清晰;
  • 类中大量的方法都是几种操作类中的某几个属性 → 如上述例子,若有半数方法都在操作address,考虑拆下;

类是不是拆得越细越好

不是,单一职责原则通过避免设计大而全的类,避免将不相关的功能耦合在一起,以此提高类内聚性,降低代码耦合性。但如果拆分过细,反而会适得其反,降低内聚性,影响代码的可维护性。

谨记:应用设计原则/模式的最终目的:提高代码的可读性、可扩展性、复用性、可维护性 等。


② 开闭原则 (OCP,Open Closed Principle)

对扩展开放、对修改关闭,就是说:添加一个新功能,应该是在代码基础上扩展,而非修改已有代码。

穿上外套时,不需要你做开胸手术

写个简单例子:

public class OcpTest {
    public static void main(String[] args) {
        GraphicEditor editor = new GraphicE-ditor();
        editor.drawShape(new Rectangle());
        editor.drawShape(new Circle());
    }
}

// 绘图类
class GraphicEditor {
    public void drawShape(Shape shape) {
        if (shape.type == 1) drawRectangle(shape);
        else if(shape.type == 2) drawCircle(shape);
    }
    public void drawRectangle(Shape s) { System.out.println("画矩形"); }
    public void drawCircle(Shape s) { System.out.println("画圆形"); }
}

// 图形类,只有一个type属性代表类别
class Shape { int type;  }
class Rectangle extends Shape { Rectangle() { super.type = 1; } }
class Circle extends Shape { Circle() { super.type = 2; } }

复制代码

如果想新增一个画三角形的功能,需要对上述代码进行修改:

// 绘图类
class GraphicEditor {
    public void drawShape(Shape shape) {
        if (shape.type == 1) drawRectangle(shape);
        else if(shape.type == 2) drawCircle(shape);
        else if(shape.type == 3) drawTriangle(shape);   // 改动③
    }
    public void drawRectangle(Shape s) { System.out.println("画矩形"); }
    public void drawCircle(Shape s) { System.out.println("画圆形"); }
    public void drawTriangle(Shape s) { System.out.println("画三角形"); }   // 改动②
}

// 图形类
class Shape { int type;  }
class Rectangle extends Shape { Rectangle() { super.type = 1; } }
class Circle extends Shape { Circle() { super.type = 2; } }
class Triangle extends Shape { Triangle() { super.type = 3; } }     // 改动①
复制代码

一下子改动了三处,这里的绘图类可以看做 上游系统(调用方),图形类则是 下游系统(提供方),开闭原则的愿景:

对提供方可扩展,对调用方修改关闭(不改动或少改动)

所以这里明显是违背开闭原则的,那应该怎么做:

将可变部分封装起来,隔离变化,提供抽象化的不可变接口,供上游系统调用。当具体实现发生改变时,只需基于相同的抽象接口,扩展一个新的实现,替换掉旧实现即可,上游系统的代码几乎不需要修改。

按照这样的思想,我们来改动上面的代码,变化的是draw()方法,我们将Shape类改进为抽象类,并定义此方法,然后让子类实现。

// 绘图类
class GraphicEditor {
    public void drawShape(Shape shape) {
        shape.draw();   // 新增图形也无需修改代码
    }
}

// 图形类
abstract class Shape {
    int type;
    public abstract void draw(); // 将变化部分抽象出来
}
class Rectangle extends Shape { Rectangle() { super.type = 1; }
    @Override
    public void draw() {
        System.out.println("画矩形");
    }
}
class Circle extends Shape { Circle() { super.type = 2; }
    @Override
    public void draw() {
        System.out.println("画圆形");
    }
}
class Triangle extends Shape { Triangle() { super.type = 3; }
    @Override
    public void draw() {
        System.out.println("画三角形");
    }
}
复制代码

现在如果想新增一个椭圆形,只需集成Shape,重写draw()方法,GraphicEditor无需任何改动。

如何做到 "对扩展开放、修改关闭"

时刻具备 扩展、抽象、封装意识,写代码时多思考下,这段代码未来可能有哪些需求变更,如何设计代码结构,实现留好扩展点,以便在未来需求变更的时候,在不改动代码整体结构、做到最小代码改动的情况下,将新代码灵活地插入到扩展点上。


③ 里式替换原则 (LSP,Liskov Substitution Principle)

子类对象能够替换程序中父类对象出现的任何地方,并且保证原来程序的逻辑行为不变及正确性不被破坏。

如果它看起来像一直鸭,像鸭一样嘎嘎叫,但需要电池——你可能有错误的抽象

里式替换原则和多态是有区别的!!!

多态是面向对象编程的特性,一种代码实现思路,里式替换是一种 设计原则,用来指导继承关系中的子类该如何设计。多态语法代码实现子类替换父类不报错,不代表就符合 里式替换原则,原则除了子类能替换父类外,还不能改变原有程序逻辑及破坏原有程序的正确性。

一些违反了里式替换原则的例子:

  • 子类违背父类声明要实现的功能 (如父类订单排序函数按金额升序排,子类重写此函数变成了按日期排);
  • 子类违背父类对输入、输出、异常的约定 (如父类某函数输入可以是任何整数,子类实现时输入只允许正整数);
  • 子类违背父类注释中所罗列的任何特殊说明

判断子类设计是否违背里式替换原则的小窍门

拿父类单元测试去验证子类代码,如果某些单元测试运行失败,就有可能说明,子类违背了里式替换原则。


④ 接口隔离原则 (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等。


0x2、KISS原则

Keep It Simple and Stupid.代码尽量保持简单

并不是代码行数越少就越简单 → 还要考虑逻辑复杂度、实现难度、代码可读性等。 也不是代码逻辑复杂就违背KISS原则 → 本身就复杂的问题,用复杂的方法解决就不违背(如KMP算法)。

同样的代码,在某个业务场景下满足KISS原则,换个场景可能就不满足了。

如何写出满足KISS原则的代码

  • 不要使用同事可能不懂的技术来实现代码(如正则,编程语言中过于高级的语法);
  • 不要重复造轮子,而是善用已经有的工具库类;
  • 不要过度优化,过度使用一些奇技淫巧 (位运算代替算数运算,复杂的条件语句代替if-else)

代码是否足够简单也是挺主观的判断,可以通过代码Code Review间接验证。

顺带提提 YAGNI原则,You Ain't Gonna Need It → 你不需要它,核心思想就是:

不要做过度设计,当前不需要的就不要做!(比如引入一堆当前不需要的依赖库)

!!!不代表不需要考虑代码的扩展性,还是要预留好扩展点,等需要的时候再去实现。


0x3、DRY原则

Don't Repeat Yourself → 不要重复自己,编程中的三种代码重复:

① 实现逻辑重复

实现逻辑重复,但功能语义重复不重复,并不违反DRY原则,比如:有两个方法,一个用于验证用户名合法性,一个用于验证密码合法性,而验证逻辑现在都是一致的:判空 → 长度(4-64) → 由数组或字母组成。那问题来了:

验证逻辑的代码重复了,违反了DRY原则吧?把两个方法合成一个,岂不美哉?

恰恰相反,合了的话就违背单一职责原则和接口隔离原则了,而且合并了以后产品改需求的时候你可能又得拆,比如:用户名长度改为(4-20),支持emoji表情,23333。

另外,并没有违反DRY原则,语义上是不同的:一个验证用户名,一个验证密码,对于上面这种更好的解法是抽象成更细粒度函数的方式来解决,将:长度和字符限制的逻辑抽取成另外两个函数,动态传参。

② 功能语义重复

比如检查IP地址是否合法,项目里写了两套不同校验逻辑的方法,逻辑不重复,功能重复,违反DRY原则。

另外,这样的操作也是在 "埋坑",项目里一会调这个一会调那个,增加了接盘仔阅读的难度,以为有更深的考量,结果却是代码设计的问题。而且还有个坑,哪天判断IP是否合法的规则改了,改了一个忘了改另一个,或者根本不知道有另一个,就会出现一些莫名其妙的BUG。

③ 代码执行重复

比如验证用户登录是否成功的逻辑:

查数据库看用户名是否存在 → 存在,查数据库判断用户名和密码是否存在 → 存在,查用户信息返回

上面的查了3次数据库,实际上2次就可以了,检查是否存在那一步可以跳过,I/O操作是比较耗时的,尽量减少此类操作。

另外,有时可能重复调用了某个函数(比如校验email是否有效),可以试着把代码重构,移除重复代码。

代码复用性 (Code Reusability)

先区分概念

  • 代码复用 → 开发新功能尽量复用已存在的代码;
  • 代码复用性 → 一段代码可被复用的特性或能力,写代码时应尽量让代码可复用。
  • DRY原则 → 不要写重复代码;

如何提高代码复用性

  • 减少代码耦合;
  • 满足单一职责原则;
  • 模块化; (不局限与一组类构成的模块,还可以理解为单个类、函数)
  • 业务与非业务逻辑分离; (越与业务无关的代码越易复用,抽取成通用的框架、类库、组件等)
  • 通用代码下沉; (分层角度: 越底层代码越通用,应设计得足够可复用,杜绝下层代码调用上层代码)
  • 继承、多态、抽象、封装
  • 应用模板等设计模式

0x4、迪米特法则 (LOD,Law Of Demeter)

在讲解这个原则前,先了解下常说的 高内聚低(松)耦合

① 高内聚

相近的功能应该放到同一个类中,不相近的功能不要放到同一个类中。相近的功能往往会被同时修改,放到同一个类中,修改会比较集中,代码容易维护。

② 低耦合

类与类间的依赖关系简单清晰,即使两个类有依赖关系,一个类的代码改动,不会或者很少导致依赖类的代码改动。

③ 高内聚和低耦合的关系

高内聚 → 指导类本身的设计,低耦合 → 指导类与类间依赖关系的设计;

高内聚有助于低耦合,低耦合又需要高内聚的支持。

④ 最小知识原则

迪米特法则,单从名字上,根本猜不出这个原则讲什么,它的命名典故:

1987年秋由Ian Holland在美国东北大学为一个叫迪米特的项目设计的。

它还有一个更达意的名字,叫做 最小知识原则,解读如下:

不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类间,尽量只依赖必要的接口。

有点懵逼,举个经典案例来帮助理解,超市购物流程的模拟

// 钱包类
public class Wallet {
    private float balance;  // 钱包余额
    
    public Wallet(float money) { this.balance = money; }
    
    // 依次是获取、设置、增加、减少余额的方法
    public float getBalance() { return balance; }
    public void setBalance(float balance) { this.balance = balance; }
    public void increaseBalance(float deposit) { balance += deposit; }
    public void decreaseBalance(float expend) { balance -= expend; }
}

// 顾客类
public class Customer {
    private String name;
    private Wallet wallet;
    
    public Customer(String name, Wallet wallet) {
        this.name = name;
        this.wallet = wallet;
    }
    
    // 依次是设置&获取 名字和钱包的方法
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public Wallet getWallet() { return wallet; }
    public void setWallet(Wallet wallet) { this.wallet = wallet; }
}

// 收银员类
public class Cashier {
    public void charge(Customer customer, float payment) {
        System.out.println("您需要支付:" + payment + " 元");
        Wallet wallet = customer.getWallet();
        if (wallet.getBalance() > payment) {
            wallet.decreaseBalance(payment);
            System.out.println("扣款成功,你钱包还剩下:" + wallet.getBalance());
        } else {
            System.out.println("扣款失败,你钱包只有:" + wallet.getBalance());
        }
    }
}

// 测试用例
public class Shopping {
    public static void main(String[] args) {
        Customer customer = new Customer("杰哥", new Wallet(100.0f));
        Cashier cashier = new Cashier();
        cashier.charge(customer, 66.6f);
    }
}

// 运行输出结果:
// 您需要支付:66.6 元
// 扣款成功,你钱包还剩下:33.4
复制代码

结果正常输出,看上去代码没啥毛病,对吧?但实际上去违背了迪米特法则,想想上面的流程:

结账时:顾客把钱包给收银员 → 收银员检查余额是否足够支付 → 够的话扣完里面的前然后顺带告诉下你余额???

你钱包里有多少钱关收银员屁事?这样的设计明显是不合理的:

收银员只管有没有收到足够的钱就好,顾客管好自己的钱包掏钱就好,通过 这个要素解耦:

public class Customer {
    // ...新增一个支付现金的方法
    public float payCash(float amount) {
        if(wallet != null) {
            if(wallet.getBalance() > amount) {
                wallet.decreaseBalance(amount);
                return amount;
            }
        }
        return 0;
    }
}

public class Cashier {
    // 修改此方法
    public void charge(Customer customer, float payment) {
        System.out.println("您需要支付:" + payment + " 元");
        float customerPay = customer.payCash(payment);
        if(customerPay == payment) {
            System.out.println("扣款成功,欢迎下次光临~");
        } else {
            System.out.println("支付金额和待支付金额不一致!");
        }
    }
}

// 运行输出结果如下:
// 您需要支付:66.6 元
// 扣款成功,欢迎下次光临~
复制代码

稍微动一下,利用迪米特法则,解耦且提高了代码的重用性,比如顾客改成微信支付、代付等,都不会收银员的收钱的行为。

不止是类设计用到了迪米特法则,平时常说的架构分层也是它的体现:

每层模块只能调用自己层中的模块,跳过某一层直接调用另一层中的模块其实就是违反了分层架构的原则。

当然迪米特法则也不是完美的:

拆分时容易引入很多过小的中间类和方法;不同模块间传递消息效率可能降低(需要跨越多个中间层模块);

⑤ 扩展:面向切面编程(AOP,Aspect Oriented Programming)

简单点说就是:在不修改已有程序代码功能的前提下给程序动态添加功能的一种技术。

迪米特法则是在 程序设计时(静态) 降低代码耦合,AOP则是在 程序运行期间(动态)

OOP与AOP的区别

  • OOP → 强调对象的内在自恰性,更适合业务功能,比如商品、订单、会员。
  • AOP → 对于**统一的行为动作**,如日志记录、性能统计等,关注系统本身的行为,而不影响功能业务的实现和演进。

小结

内容有点多,整理下,方便记忆:

  • 单一职责原则(SRP) → 类/模块只完成一个职责;
  • 开闭原则(OCP) → 对扩展开放(提供方),对修改关闭(调用方) → 封装可变部分,提供抽象化的不可变接口供调用者调用;
  • 里式替换原则(LSP) → 子类对象可以替换父类对象,同时保证程序的逻辑行为不变和正确性不被破坏;
  • 接口隔离原则(ISP) → 不要给调用者提供一些它不需要的接口或方法;
  • 依赖反转原则(DIP) → 高层模块不要直接依赖底层模块,而是模块间抽象出一个协议,通过实现这个协议来互相依赖;
  • KISS原则 → 代码尽量保持简单;
  • YAGNI原则 → 不要过度设计;
  • DRY原则 → 不要重复自己,区分逻辑重复、语义重复、代码执行重复!
  • 迪米特法则(LOD) → 不该有依赖关系的类不要依赖,有依赖关系的类尽量只依赖必要接口;

理论要在实际开发中去应用,最近也将这些原则应用到项目开发中,真·久违的 醍醐灌顶

不只是把事做完,而是把事做好,不是代码的胡堆乱砌,而是经过各种考量权衡~

不知道怎么表述,反正心中默念高内聚、低耦合就对了~


参考文献

  • 《趣学设计模式》09 | 最少原则:如何实现“最少知识”代码?

文章分类
Android