[不做怎么知道系列之Android开发者的30天后端养成故事 Day10] - 你/妳SOLID了吗? #什么是SOLI

http://img2.58codes.com/2024/20124548fAwRCK44Iw.png

哈啰,我们又见面了,今天我们不实作电商网站,来看看 SOLID Principles 说了些什么,为什么可以透过遵守 SOLID 来达到 高品质程式码 呢 ?,类此构 !

今天参考的文件是 solid.python,里面用简洁明了的方式,并且搭配 Python 的範例,来解释 SOLID Principles 的核心概念,全篇提到的作者都是写 solid.python 的 作者。

SOLID 是啥 ?

Single Responsibility Principle,简称 SRP,单一责任原则Open/Closed Principle,简称 OCP,开放封闭原则Liskov Substitution Principle,简称 LSP,里氏替换原则Interface Segregation Principle,简称 ISP,介面隔离原则Dependency Inversion Principle,简称 DIP,依赖反转原则

相信你看到这里,一定还是不知道在干嘛 XD,直接往下看吧。

Single Responsibility Principle (SRP)

单一责任,从字面上来理解,就是一个物件 (class),就应该负责一项责任就好,是不是听完有种「喔,所以呢 ?」的感觉,那直接来看个例子吧~

如果我们想创立一个动物的类别,只有一个 name 的属性,然后可以储存到 DB。

class Animal:    def __init__(self, name: str):        self.name = name        def get_name(self) -> str:        pass    def save(self, animal: Animal):        pass

上面这段 code 来自 SRP | solid.python

先花十秒看一下 code,感觉一下,10,9,8,1,好,有感觉到什么吗 ?

乍看之下没什么问题,用 name 当作参数到 constructor 创建 Animal,然后还能取得 Animal 的名字,然后再存到 DB,蛮合理也蛮好用的。

但是,假设今天我把 DB 从 SQLite(Relational) 换成 MongoDB(NoSQL),是一个完全不同的储存实作方式,那么我是不是就要来 Animal 这个物件改 save() 这个方法 ? 听起来好像也还好吧,那如果有十个类似的 class,如果再大一点的专案,一百个类似的 class 怎么办 ? 加班生活直接 online。

还有另一点是,仔细想想,在 Animal 这样的一个物件里,有 save() 的方法好像有点奇怪 ?,这件事应该是跟 DB 相关的吧,怎么会直接隶属在 Animal 底下呢 ?

那么肯定是有解决方法的吧 !?

我第一个想到的方法是,那就把储存的方式独立出来啰,我可以实作出 SQLiteMongoDB 两种时储存方法,再独立出抽象介面,让所有类似的 class 都透过抽象界面来储存资料,那么当我下次要新增另一种资料库的时候,我所有类似 Animal 的 class 都不用动,只要新增新资料库的实作方法,再改动抽象介面就好。这样做的话,可以让 Animal 负责 Animal 相关的属性及功能,而跟资料储存相关的实作,就给 DB 去负责就好,是不是听起来有比原本的方法好呢 ?

而这位作者提供的方法如下,和我的想法类似,是用 AnimalDB 来处理资料库储存的功能、Animal 本身持有一个 DB 的实例 (instance),Animal 本身的 save() 实际上是 call DB 实例的 save(),如果要实作不同的 DB 的话,也可以再抽象一层,让 AnimalDB 去 call SqliteDBsave()MongoDBsave() 之类的。

class AnimalDB:    def get_animal(self, id) -> Animal:        pass    def save(self, animal: Animal):        passclass Animal:    def __init__(self, name: str):        self.name = name        self.db = AnimalDB()    def get_name(self):        return self.name    def get(self, id):        return self.db.get_animal(id)        def save(self):        self.db.save(animal=self)

上面这段 code 来自 SRP | solid.python

透过以上讲的解法,就可以将原本 Animal class 处理储存细节的这个功能,给分开出来到另一个专门负责储存细节的 class,达到一个 class 就负责它该负责的目标,以上就是 Single Responsibility Principle 的概念,对于程式的理解逻辑上可以更加清晰,同时,也 减少牵一髮动全身的惨况,讚讚。顺口提一下,上面这段 code 也就是 Facade Pattern (念法是 ㄈㄜ˙ 萨的,是法文,参考 How to Pronounce Facade) 的展现。

Open/Closed Principles (OCP)

开放封闭原则,那么是开放什么、封闭什么呢?

开放 扩充 的可能性封闭 修改 的可能性

简单来说,就是我已经写好可以跑的 code 了,那么我要新增一个功能的话,就是在不更动我原本写好的部分,只要另外写我需要扩充的功能就好,来看看下面的栗子

现在要在动物的类别上,加上一个动物叫声的方法。

class Animal:    def __init__(self, name: str):        self.name = name        def get_name(self) -> str:        passanimals = [    Animal('lion'),    Animal('mouse')]def animal_sound(animals: list):    for animal in animals:        if animal.name == 'lion':            print('roar')        elif animal.name == 'mouse':            print('squeak')animal_sound(animals)

上面这段 code 来自 OCP | solid.python

一样花个十秒看看有没有什么"异味"。

乍看之下好像也是没问题是吧 ? 那么如果要新增一种动物以及它的叫声呢 ? 就要再来这边新增一个 if animal.name == 'new animal name',可是在 OCP 这个原则,希望我们要 新增 的话,不要更改到旧的 code,同时这也是一位重视效率的 programmer 会希望的一件事情,让我们的程式保有一个 可扩充 的弹性,不要写死,也能再次 避免牵一髮动全身的窘境,只要专注在新功能的逻辑上即可。

那么有建议的解法吗 ?

有der,可以利用 继承(inherit) 并 重写(override) 每个动物叫声的实作。

class Animal:    def __init__(self, name: str):        self.name = name        def get_name(self) -> str:        pass# abstract method    def make_sound(self):        pass"""python 的继承是在 class name 旁边加小括号"""class Lion(Animal):# override make_sound() method    def make_sound(self):        return 'roar'class Mouse(Animal):    def make_sound(self):        return 'squeak'class Snake(Animal):    def make_sound(self):        return 'hiss'def animal_sound(animals: list):    for animal in animals:"""取用只需要同样的介面好处是即使新增不同的动物这边也不用改"""        print(animal.make_sound())animal_sound(animals)

上面这段 code 来自 OCP | solid.python

补充,有个很容易搞混的名词解释问题:Override、Overwrite、Overload

Liskov Substitution Principle (LSP)

里氏替换原则,想必是个叫做 Liskov 的人想出来的啰,替换是替换什么呢 ?

一句话来说,就是 子类(Child Class/Sub Class) 能够替换掉 父类(Parent Class/Super Class),其中 能替换 的意思包含「父类有的 method,子类也要有一样的 method,而且 method 参数也必须相同」,先看看範例吧。

现在要算动物有几只脚。

def animal_leg_count(animals: list):    for animal in animals:        if isinstance(animal, Lion):            print(lion_leg_count(animal))        elif isinstance(animal, Mouse):            print(mouse_leg_count(animal))        elif isinstance(animal, Pigeon):            print(pigeon_leg_count(animal))        animal_leg_count(animals)

上面这段 code 来自 LSP | solid.python

上面这段需要先判断动物是什么类别,才呼叫对应的数脚的方法,首先,这样的写法犯了 Open/Closed 原则,每次新增一个新的动物时,都还要来这边加个 elif,第二是明明都是动物,这些动物都可以被数出有几只脚,为什么不呼叫一样的数脚方法,最后再导到不同数脚实作方法呢 ? 这就是 LSP 所讲的,再来看看可以怎么改善吧。

解法

class Animal:def leg_count(self):passclass Lion(Animal):def leg_count(self):passclass Mouse(Animal):def leg_count(self):passclass Pigeon(Animal):def leg_count(self):passdef animal_leg_count(animals: list):    for animal in animals:"""不用检查动物是什么动物只要每一种动物都有实作自己的 leg_count() 即可"""        print(animal.leg_count())        animal_leg_count(animals)

上面这段 code 来自 LSP | solid.python

值得注意的点是,Python 没有像 Java 的 InterfaceAbstract 关键字,强制子类一定要 override 父类的 method,所以在实作 LSP 时,必须要自己多加注意,否则程式很容易出错。

可以参考 (2017) Force child class to override parent's methods,强制子类一定要 override 父类的 method

Interface Segregation Principle (ISP)

介面隔离原则,从字面上看起来,应该是跟 介面 还有 隔离 相关吧 XD,那么是要对介面做什么隔离呢? 简单来说,就是介面中的 methods 会随着功能增长,越加越多而肥大,然而介面中的某些 methods,对于实作这个介面的 class 来说,可能不完全需要实作全部的 method,觉得越讲越饶口,直接看範例吧。

假设有些动物会走、会飞、会跳。

class IAnimal:def walk(self):raise NotImplementErrordef fly(self):raise NotImplementErrordef jump(self):raise NotImplementError

raise NotImplementError,可以迫使每个实作 IAnimal 介面的 class 们,都必须要实作 method 才可以,不然会有 NotImplementError,这个方法也就是 LSP 最后提到强迫子类一定要 override 父类的 method 的方法之一。

class Dock(IAnimal):def walk(self):passdef fly(self):passdef jump(self):passclass Dog(IAnimal):def walk(self):passdef fly(self):passdef jump(self):pass...

可是,并不是每个动物都一定同时会走、会飞又会跳啊,但是我又想要强迫子类一定要实作这些 method,该怎么办呢?

还有一种情况,就算是每一种动物都同时会走、会飞又会跳,现在假设已经有十种动物都符合这条件,如果突然又发现,原来他们不是同时都会走、会飞又会跳,甚至还会游泳,那我在 IAnimal 新增一个 def swim(self),其他已经有实作 IAnimal 的子类,也就需要各自实作 swim() 才可以。现在只有十种需要个别实作 swim(),大腿捏一下把它做完还能忍受,那规模放大到五十、一百个,光是用想像的,就会觉得瞬间想放弃新增功能 XD。

class IAnimal:def walk(self):raise NotImplementErrordef fly(self):raise NotImplementErrordef jump(self):raise NotImplementError# 新增游泳功能def swim(self):raise NotImplementError
class Dock(IAnimal):def walk(self):passdef fly(self):passdef jump(self):pass# 每个子类都要跟着实作def swim(self):passclass Dog(IAnimal):def walk(self):passdef fly(self):passdef jump(self):pass# 才新增两个 class 我就想放弃了def swim(self):pass...

相信看到这里,你一定会觉得「乾,谁会写出这么白癡的程式码阿」,别不信邪,你同事、你同学甚至你自己,也会不知不觉写出这样的 code,我先承认,我就写过这样的 code ...

当初我在实作一个血压血糖纪录系统,需要从 Android 端,上传血压血糖资料到云端储存,因为同事后端设计的关係,舒张压、收缩压、心律、血糖值必须分别透过不同 API 上传,所以我就开了一个介面来让 Retrofit 使用,每个值都必须要有 POST 和 GET 两个 method,这个介面包含了 8 个 method,代表每个实作这个介面的地方都需要实作这 8 个 method,可是明明上传血糖的地方,我只需要血糖,却必须要连血压的一起实作;这故事还没完,后来需要新增一个「今天有没有吃药」的功能,我在介面一样多开了 POST 和 GET 两个 method,这时候我就必须硬着头皮,在上传血压的地方也要实作吃药的方法、在上传血糖的地方也要实作吃药的方法,更好笑的是,我还要在上传吃药的地方也实作血压、血糖的方法,明明不需要,却还是要放一个空的在那边,那个时候我就觉得我自己是白癡 ...

所以,痛过才知道 SOLID 的重要性,不要不信邪 XD。

那么解法呢 !! (敲碗

class IAnimal():def behavior(self):passclass Dock(IAnimal):def behavior(self):passclass Dog(IAnimal):def behavior(self):pass

其实就是把原本定得很细的 method,给它再抽象一层,不要把功能给写死,实际作的事就交给十作的 class 去决定就好。

那么我的血泪故事可以怎么解决呢 ? 就把它分成 post()get() 就好惹,唉 ...。

Dependency Inversion Principle (DIP)

依赖反转原则,什么是依赖(Dependency)? 又要反转? 你可以把 依赖 想像成,我每天早上都要喝一杯咖啡,那么就是我 依赖 咖啡,那么用程式的逻辑来解释依赖,就是有一个 Person class 和 Coffee Class,直接看看程式码。

class Person:def behavior_in_morning(self, coffee):print("Drink", coffee.name, ".")class Coffee:def __init__(self, name):self.name = namePerson().behavior_in_morning(Coffee("City Cafe"))# 输出为 Drink City Cafe .class SoyMilk:def __init__(self, name):self.name = name

这样了解了依赖的关係了吗 ? 其实再讲得更简单点,可以把 依赖 想像成 在你的 method 里有用到某个物件就是一种依赖,不论是取得物件的属性、修改物件的属性,还是执行物件的功能等等,都是一种依赖,还有继承与实作也是一种依赖。

那么今天有另一个人它习惯早上喝豆浆呢 ? 我这个 Person class 就必须得修改,这样就违反了 OCP,所以这样不是个好设计。

解法

class Drink:    def __init__(self, name):        self.name = nameclass SoyMilk(Drink):    def __init__(self, name):        self.name = nameclass Coffee(Drink):    def __init__(self, name):        self.name = nameclass Person:    def behavior_in_morning(self, drink):        print("Drink", drink.name, ".")Person().behavior_in_morning(SoyMilk("IMei Sugarless"))# 输出为 Drink IMei Sugarless .Person().behavior_in_morning(Coffee("City Coffee"))# 输出为 Drink City Coffee .

可以将再抽象一层 Drink class,然而 Personbehavior_in_morning 的参数改成 Drink,这样身为一个人,就可以自由地选择他早上想喝的饮料了,耶呼~

你或许有个疑问:「奇怪,你不是说依赖反转吗? 反转在哪里?」,这有点难理解,依照原文:

Dependency should be on abstractions not concretions

A. High-level modules
should not depend upon low-level modules. Both should depend upon abstractions.

B. Abstractions should not depend on details. Details should depend upon
abstractions.

直接翻译的话,就是「高阶不依赖低阶,高阶与低阶都应该依赖抽象,抽象不依赖细节,细节应该依赖抽象」,简单来说,就是 在需要有弹性的地方,不应该直接定死细节,而是需要再抽象一层。

其实你会感觉这个範例,怎么好像跟前面提到的每一个原则都适用阿 XD,没错,所以 一段好的程式码,就是都符合 SOLID 五原则,但也不是说要无限的抽象下去。

像在 Person class 的 behavior_in_morning 就还可以再改善,本身这个 method 在命名上就已经很细节了,还定到时间点,再加上 早上的行为 有可能是 运动 或 工作,而我在範例的设计却只有喝饮料,所以还能再深一层地抽象下去,可是 ! 如果当我的系统不需要这么有弹性的话,那我其实不需要抽象到这么多层,只要把 behavior_in_morning 改成 drink 并吃下 时间饮料 的参数,就可以了,如果我无限地抽象下去的话,就是 overdesign,你必须自己考量 扩充弹性 与 开发效益,来决定你怎么设计这支程式。

单日心得总结

好的,我第二次晚了一天发文 QQ,昨天下午有两场专题分享会,晚上还有一场读书会,原本想说不作进度,改发篇技术研究,应该会比较省时间,但其实没有呜呜,昨天写到 OCP 结束就已经晚上十一点半了,但又不想把 SOLID 拆成两篇来发,只好,恩... 但我心念一转,反正只要我的时间不要浪费掉就可以了,不用拘泥这种死规定,毕竟这个规定还是我自己设定的,不需要给自己绑死~

RS 建议,今天讲的 SOLID 五个原则,最好多花点时间把它搞懂,并且把例子熟记起来,熟到你可以说到其中一个原则,就能直接把例子想出来或写出来,可以帮你省了很多 debug、改 code 的时间,但有时候就是要痛过才会知道,没有被严重的 bug 拖过时间,你就不会知道 SOLID 给的建议有多重要。

我是 RS,这是我的 不做怎么知道系列 文章,我们 明天见。


喜欢我的文章吗? 赶快来看看我都发了什么文章吧:我的文章目录欢迎阅读我的上一篇: [不做怎么知道系列之Android开发者的30天后端养成故事 Day9] - 真正的连接前后端 #前端资料从后端捞 #串过MVT #Models-to-Templates欢迎阅读我的下一篇: [不做怎么知道系列之Android开发者的30天后端养成故事 Day11] - 测试可以吃吗? #高品质程式 #单元测试 #整合测试

关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章