code小生,一个专注 Android 领域的技术平台
公众号回复 Android 加入我的安卓技术群
作者:Brown_
链接:https://www.jianshu.com/p/f555c5ace8d9
声明:本文已获Brown_
授权发表,转发等请联系原作者授权
在架构之路上和代码设计上,我们一定要明白上面的几个原则,在这几个原则的指导下,才能设计出优良的架构,才能经得住撕逼。
SRP是五大原则里最容易被误解的一个,很多程序员根据SRP这个名字想当然的认为这个原则就是指:每个模块都应该只做一件事。
但这是只是一个面向底层实现细节的设计原则,并不是SRP的全部。
在历史上,我们曾经这样描述SRP这一原则:任何一个软件模块都应该有且仅有一个被修改的原因。
在现实环境中,软件系统为了满足用户和所有者的要求,总是会面临这样那样的修改。而系统用户或者所有者就是该设计原则中所指的‘被修改的原因’。所以也可以这样描述SRP。
任何一个软件模块都应该只对一个用户或者系统相关者负责。
这里的‘用户’和‘系统相关者’在用词上也不完全准确,它们很有可能指的是一个或多个用户和利益相关者,只要这些人希望对系统进行的变更相似的,就可以归为一类或者称其为行为者。所以,SRP的最终描述变成了:
任何一个软件模块都应该只对一类行为者负责
这个软件模块指的是什么呢?可以一个源代码文件,或者是一组紧密相关的函数和数据结构。
我们看一下下面的商品类设计
类里有三个方法
消费者需要getPrice 函数 , 库房管理员需要getOrderList() 函数 ,售后人员 需要 getReturnList() 函数
这个类同事对三个相关者负责,可能因为商品订单函数的修改影响到售后。所以这个类违背了 单一职责原则 原则。
接下来对类按照行为拆分
商品类 对 消费者负责
订单类 对 库房管理员负责
收收类 对 售后人员负责
其中的每个类的修改都不会牵扯他人,也只对一类行为者负责,这原则极大的降低了代码的耦合。
OCP 开闭原则,看起来感觉很难懂,其实可以这么理解。
设计良好的软件应该易于扩展,同时抗拒修改。
简单的说就是系统应该不需要修改的前提下就可以轻易扩展。这个原则是软件设计,系统架构中非常重要的原则。
接下来看一组架构
image.png这里将PC 和WAP的数据展示放到了一个类里面,如果此时要产品要再加一种PAD的显示方式,就要修改展示代码,否则无法加入新的功能。
接下来按照OCP 原则优化
image.png设计了一个数据运算层,也可以说是MVC中的M层,这个层主要生成格式化数据,给前端的展示层提供标准化数据,在这个结构中添加PAD展示类,无需修改任何代码,可以很容易添加。在再添加其他任何展示类都可以不用修改,从这个设计中可以看出是易于扩展,同时抗拒修改的。同时底层的数据发生变化,只用修改数据运算层,无需修改前端展示类,这样也解除了依赖,做到了依赖反转。
OCP 主要是让我们的系统已于扩展,同时限制其每次被修改的所影响的范围。
派生的子类应该是可替换基类的,也就是说任何基类可以出现的地方,子类一定可以出现。
简单的来说就是,当你通过继承实现多态行为时,如果派生类没有遵守LSP,可能会让系统引发异常。所以请谨慎使用继承,只有确定是“is-a”的关系时才使用继承。
我们来看一个经典的错误模型。
image.png当用户调用矩形类时:
矩形 r = new 矩形()
r.setH = 10
r.setW = 2
assert(r.area() == 20 )
很显然换成 正方形 的类,用户这样调用就会存在问题。也就是说当 矩形 出现的地方不能替换成子类 正方形,这里就违背了里氏替换原则(LSP)。
我们来看一个正确的设计模型。
image.png在警察检查身份证号,每个中国人出生就有一个身份证号,所以这里中国人的子类,工人或者司机都存在这个获取身份证号的方法,任何父类出现的地方子类都可以替换,这就是里氏替换原则。
LSP 可以且应该应用于原件架构的各个层面,因为一旦违背了可替换性,系统就不得不为此增加大量复杂的对应机制
接口隔离原则(ISP)表明类不应该被迫依赖他们不使用的方法。
我们先来看一个设计。
假设这里
User1 调用 op1
User2 调用 op2
User3 调用 op3
再假设这里的代码是java 这种编译语言写的,那么很明显User1虽然不用调用op2 op3,但在源代码上形成了依赖关系,这种依赖关系意味着我们队OPS 中op2所做的任何修改,即使不影响User1的功能,也会导致他需要重新被编译和部署。
这个问题可以通过接口隔离来解决。
image.png现在User1 的源代码会依赖U1Ops 和op1 ,但不会依赖U2Ops op2,U2Ops做任何修改都不需要重新编译和部署User1。
看到这里大家是不是任务接口隔离只是对编译语言的一种优化,像PHP 和Python 就不需要这种设计呢?
这原理在软件架构中也有很大的意义。
image.png我们来开系统S引入了框架F,框架F必须使用数据库D。那么就形成了S依赖于F,F依赖于D的关系。
在这种D中包含了F中的不需要的功能,那么这些功能也是S中不需要的。而我对D的修改会导致F可能会重新部署,接着又会导致S的重新部署。更可怕是D中的一个无关功能修改的错误,导致F和S都无法运行。
任何层次的软件设计如果依赖了它并不需要的东西,就会带来意料之外的麻烦
我们每次修改抽象接口的时候,一定回去修改对应的具体实现,但是反过来,当我们修改具体实现时,却很修改对应的抽象接口。所以我们认为接口比实现更加的稳定。
也就是说,如果想要在软件设计上追求稳定,就必须使用稳定的抽象接口,少依赖多变的具体实现。下面,我们将该设计的原则归结为以下几条具体守则。
先不考虑依赖反转的设计,我们来看这样一个设计
image.pngrun(); }}class BMW{ public function run() { echo "宝马上路......"; }}class Client{ public function goToWork(){ $driver = new Driver(); $bmw = new BMW(); $driver->drive($bmw); }}
这样的设计乍一看好像也没有问题,开着宝马去上班,但是如果我们买了奔驰,想开奔驰去上班,这咋办呢?要去修改Driver类,才能使用奔驰车,这样的设计导致了代码耦合很高,具体实现类的修改,必须修改抽象逻辑。
我们按照依赖反转原则进行设计
image.pngrun(); }}class Client{ public function goToWork() { $driver = new Driver(); $bmw = new BMW(); $driver->drive($bmw); /** * 很轻松的添加车辆 */ $benz = new Benz(); $driver->drive($benz); }}
抽象是对实现的约束,是对依赖者的一种契约,不仅仅约束自己,还同时约束自己与外部的关系,其目的就是保证所有的细节不脱离契约的范畴,确保约束双方按照规定好的契约(抽象)共同发展,只要抽象这条线还在,细节就脱离不了这个圈圈。
当学习完设计原则后,我发现依赖反转原则,其实是其他几个原则的综合,接口的设计保证了单一职责原则,依赖反转的部分实现也满足了开闭原则,通过抽象进行约束很大程度上也是一种里氏替换原则,接口的设计又实现各个接口的隔离,这里也提现了接口隔离原则。
综上所述可以得出,好的依赖隔离的设计是同时满足SOLID原则的。那么反之可以得出如果其中任意原则实现的不好,我们就要反思依赖反转是否没有做好。
SOLID设计原则,看起来说的都是一些老掉牙的原则,一些工作多年的工程师,都或多或少的使用了其中一些原则,但其中大部分都不能全部的说出这些原则的使用场景和使用方式。这就像一个行走多年的武林人士会很多的招式,但是知其然不知其所以然。
认真的学习设计的基本功,就像郭靖大侠,一步一步的打好基础,未来遇到更大更复杂的招式都可以化繁为简,快速学习。
推荐阅读
使用 Dagger2 前你必须了解的一些设计原则
通用的 Android 客户端架构设计
扫一扫 关注我的公众号
如果你想要跟大家分享你的文章,欢迎投稿~