转载

架构师之路 - SOLID设计原则

code小生,一个专注 Android 领域的技术平台

公众号回复 Android 加入我的安卓技术群

作者: Brown_

链接: https://www.jianshu.com/p/f555c5ace8d9

声明: 本文已获 Brown_

授权发表,转发等请联系原作者授权

  • SRP 单一职责原则

  • OCP 开闭原则

  • LSP 里氏替换原则

  • ISP 接口隔离原则

  • DIP 依赖反转原则

在架构之路上和代码设计上,我们一定要明白上面的几个原则,在这几个原则的指导下,才能设计出优良的架构,才能经得住撕逼。

SRP 单一职责原则

SRP是五大原则里最容易被误解的一个,很多程序员根据SRP这个名字想当然的认为这个原则就是指:每个模块都应该只做一件事。

但这是只是一个面向底层实现细节的设计原则,并不是SRP的全部。

在历史上,我们曾经这样描述SRP这一原则:任何一个软件模块都应该有且仅有一个被修改的原因。

在现实环境中,软件系统为了满足用户和所有者的要求,总是会面临这样那样的修改。而系统用户或者所有者就是该设计原则中所指的‘被修改的原因’。所以也可以这样描述SRP。

任何一个软件模块都应该只对一个用户或者系统相关者负责。

这里的‘用户’和‘系统相关者’在用词上也不完全准确,它们很有可能指的是一个或多个用户和利益相关者,只要这些人希望对系统进行的变更相似的,就可以归为一类或者称其为行为者。所以,SRP的最终描述变成了:

任何一个软件模块都应该只对一类行为者负责

这个软件模块指的是什么呢?可以一个源代码文件,或者是一组紧密相关的函数和数据结构。

我们看一下下面的商品类设计

架构师之路 - SOLID设计原则

类里有三个方法

  • getPrice() 获取价格

  • getOrderList() 获取订单列表

  • getReturnList() 获取退货列表

消费者需要getPrice 函数 , 库房管理员需要getOrderList() 函数 ,售后人员 需要 getReturnList() 函数

这个类同事对三个相关者负责,可能因为商品订单函数的修改影响到售后。所以这个类违背了  单一职责原则 原则。

接下来对类按照行为拆分

架构师之路 - SOLID设计原则

商品类 对 消费者负责

订单类 对 库房管理员负责

收收类 对 售后人员负责

其中的每个类的修改都不会牵扯他人,也只对一类行为者负责,这原则极大的降低了代码的耦合。

OCP 开闭原则

OCP 开闭原则,看起来感觉很难懂,其实可以这么理解。

设计良好的软件应该易于扩展,同时抗拒修改。

简单的说就是系统应该不需要修改的前提下就可以轻易扩展。这个原则是软件设计,系统架构中非常重要的原则。

接下来看一组架构

架构师之路 - SOLID设计原则
image.png

这里将PC 和WAP的数据展示放到了一个类里面,如果此时要产品要再加一种PAD的显示方式,就要修改展示代码,否则无法加入新的功能。

接下来按照OCP 原则优化

架构师之路 - SOLID设计原则
image.png

设计了一个数据运算层,也可以说是MVC中的M层,这个层主要生成格式化数据,给前端的展示层提供标准化数据,在这个结构中添加PAD展示类,无需修改任何代码,可以很容易添加。在再添加其他任何展示类都可以不用修改,从这个设计中可以看出是易于扩展,同时抗拒修改的。同时底层的数据发生变化,只用修改数据运算层,无需修改前端展示类,这样也解除了依赖,做到了依赖反转。

OCP 主要是让我们的系统已于扩展,同时限制其每次被修改的所影响的范围。

LSP 里氏替换原则

派生的子类应该是可替换基类的,也就是说任何基类可以出现的地方,子类一定可以出现。

简单的来说就是,当你通过继承实现多态行为时,如果派生类没有遵守LSP,可能会让系统引发异常。所以请谨慎使用继承,只有确定是“is-a”的关系时才使用继承。

我们来看一个经典的错误模型。

架构师之路 - SOLID设计原则
image.png

当用户调用矩形类时:

矩形 r = new 矩形()

r.setH = 10

r.setW = 2

assert(r.area() == 20 )

很显然换成 正方形 的类,用户这样调用就会存在问题。也就是说当 矩形 出现的地方不能替换成子类 正方形,这里就违背了里氏替换原则(LSP)。

我们来看一个正确的设计模型。

架构师之路 - SOLID设计原则
image.png

在警察检查身份证号,每个中国人出生就有一个身份证号,所以这里中国人的子类,工人或者司机都存在这个获取身份证号的方法,任何父类出现的地方子类都可以替换,这就是里氏替换原则。

LSP 可以且应该应用于原件架构的各个层面,因为一旦违背了可替换性,系统就不得不为此增加大量复杂的对应机制

ISP 接口隔离原则

接口隔离原则(ISP)表明类不应该被迫依赖他们不使用的方法。

我们先来看一个设计。

架构师之路 - SOLID设计原则
image.png

假设这里

User1 调用 op1

User2 调用 op2

User3 调用 op3

再假设这里的代码是java 这种编译语言写的,那么很明显User1虽然不用调用op2 op3,但在源代码上形成了依赖关系,这种依赖关系意味着我们队OPS 中op2所做的任何修改,即使不影响User1的功能,也会导致他需要重新被编译和部署。

这个问题可以通过接口隔离来解决。

架构师之路 - SOLID设计原则
image.png

现在User1 的源代码会依赖U1Ops 和op1 ,但不会依赖U2Ops op2,U2Ops做任何修改都不需要重新编译和部署User1。

看到这里大家是不是任务接口隔离只是对编译语言的一种优化,像PHP 和Python 就不需要这种设计呢?

这原理在软件架构中也有很大的意义。

架构师之路 - SOLID设计原则
image.png

我们来开系统S引入了框架F,框架F必须使用数据库D。那么就形成了S依赖于F,F依赖于D的关系。

在这种D中包含了F中的不需要的功能,那么这些功能也是S中不需要的。而我对D的修改会导致F可能会重新部署,接着又会导致S的重新部署。更可怕是D中的一个无关功能修改的错误,导致F和S都无法运行。

任何层次的软件设计如果依赖了它并不需要的东西,就会带来意料之外的麻烦

SRP 依赖反转原则

我们每次修改抽象接口的时候,一定回去修改对应的具体实现,但是反过来,当我们修改具体实现时,却很修改对应的抽象接口。所以我们认为接口比实现更加的稳定。

也就是说,如果想要在软件设计上追求稳定,就必须使用稳定的抽象接口,少依赖多变的具体实现。下面,我们将该设计的原则归结为以下几条具体守则。

  • 应该在代码中多使用抽象接口,尽量避免使用那些多变的具体实现类。

  • 不要在具体实现类上创建衍生类

  • 不要覆盖包含具体实现的函数

  • 避免在代码中写入与任何具体实现相关的名字,或者是其他容易变动的事务名称。

先不考虑依赖反转的设计,我们来看这样一个设计

架构师之路 - SOLID设计原则
image.png
<?php
class Driver
{
    public function drive(BMW $bmw)
    {
        $bmw->run();
    }
}

class BMW
{
    public function run()
    {
        echo "宝马上路......";
    }
}

class Client{
    public function goToWork(){
        $driver = new Driver();
        $bmw = new BMW();
        $driver->drive($bmw);
    }
}

这样的设计乍一看好像也没有问题,开着宝马去上班,但是如果我们买了奔驰,想开奔驰去上班,这咋办呢?要去修改Driver类,才能使用奔驰车,这样的设计导致了代码耦合很高,具体实现类的修改,必须修改抽象逻辑。

我们按照依赖反转原则进行设计

架构师之路 - SOLID设计原则
image.png
<?php
abstract class CarBase
{
    public abstract function run();
}

class BMW extends CarBase
{
    public function run()
    {
        echo "宝马上路......";
    }
}

class Benz extends CarBase
{
    public function run()
    {
        echo "奔驰上路......";
    }
}

abstract class DriverBase
{
    public abstract function drive(CarBase $car);

}

class Driver extends DriverBase
{
    public function drive(CarBase $car)
    {
        $car->run();
    }
}

class Client
{
    public function goToWork()
    {
        $driver = new Driver();

        $bmw = new BMW();
        $driver->drive($bmw);
        /**
         * 很轻松的添加车辆
         */
        $benz = new Benz();
        $driver->drive($benz);
    }
}

抽象是对实现的约束,是对依赖者的一种契约,不仅仅约束自己,还同时约束自己与外部的关系,其目的就是保证所有的细节不脱离契约的范畴,确保约束双方按照规定好的契约(抽象)共同发展,只要抽象这条线还在,细节就脱离不了这个圈圈。

当学习完设计原则后,我发现依赖反转原则,其实是其他几个原则的综合,接口的设计保证了单一职责原则,依赖反转的部分实现也满足了开闭原则,通过抽象进行约束很大程度上也是一种里氏替换原则,接口的设计又实现各个接口的隔离,这里也提现了接口隔离原则。

综上所述可以得出,好的依赖隔离的设计是同时满足SOLID原则的。那么反之可以得出如果其中任意原则实现的不好,我们就要反思依赖反转是否没有做好。

总结

SOLID设计原则,看起来说的都是一些老掉牙的原则,一些工作多年的工程师,都或多或少的使用了其中一些原则,但其中大部分都不能全部的说出这些原则的使用场景和使用方式。这就像一个行走多年的武林人士会很多的招式,但是知其然不知其所以然。

认真的学习设计的基本功,就像郭靖大侠,一步一步的打好基础,未来遇到更大更复杂的招式都可以化繁为简,快速学习。

推荐阅读

使用 Dagger2 前你必须了解的一些设计原则

通用的 Android 客户端架构设计

架构师之路 - SOLID设计原则

扫一扫  关注我的公众号

如果你想要跟大家分享你的文章,欢迎投稿~

原文  http://mp.weixin.qq.com/s?__biz=MzIxNzU1Nzk3OQ==&mid=2247488667&idx=1&sn=008a4159aea55b162ebddca9d3713391
正文到此结束
Loading...