背景

在我们日常工作中,代码写着写着就出现下列的一些臭味。但是还好我们有SOLID这把‘尺子’, 可以拿着它不断去衡量我们写的代码,除去代码臭味。这就是我们要学习SOLID原则的原因所在。

设计的臭味

  • 僵化性

    • 具有联动性,动一处,会牵连到其他地方
  • 脆弱性
    • 不敢改动,动一处,全局瘫痪
  • 顽固性
    • 不易改动
  • 粘滞性
    • 耦合性太高
  • 不必要的复杂性
    • 代码设计过于复杂
  • 不必要的重复
    • 提高复用性,减少重复
  • 晦涩性
    • 代码设计不易理解

SRP-单一职责原则

  • 一个类只做一件事情。当然一件事情,不是说类中只有一个方法。而是类中的方法都是属于同一种职责。
  • 不能因为第二职责的原因去改动这个类。

一个很好的例子:在我们封装request库时,我们需要实现以下4个方法.

class MyRequestClient:

    def post(self):
pass
def get(self):
pass
def update(self):
pass
def delete(self):
pass #上面的方法就是属于同一职责。 如何还有其他的方法,那么这个类就不符合单一职责原则。
#例增加以下方法:
def get_db_data(self):
pass
def to_object(self):
pass

OCP-开放封闭原则

  • 对扩展开放,对修改封闭。
  • 无需改动自身代码,就可以扩展它的行为。
  • 对类的改动往往是新增代码就可以了,而不是去修改原有的代码。
  • 使用子类继承、依赖注入、数据驱动的方法可以实现OCP原则。

首先我们来看一个违反OCP原则的例子。

#bad code
def circle_draw():
print(f"this is circle draw") def square_draw():
print(f"this is square draw") def draw_all_shape(shapes):
for shape in shapes:
if shape == "circle":
circle_draw()
if shape == "square":
square_draw()

这段代码的问题是如果再有新的类型需要draw, 我们需要修改draw_all_shape函数来适配新的类型。

依赖注入实现OCP原则

我们定义了一个抽象类Shape, 子类Square和Circle继承Shape. 并且在子类中重写了父类的方法。函数draw_all_shape是绘制所有图形。

from typing import List
from abc import ABCMeta, abstractmethod class Shape(metaclass=ABCMeta): @abstractmethod
def draw(self):
pass class Square(Shape): def draw(self):
print(f"this is square draw") class Circle(Shape): def draw(self):
print(f"this is circle draw") def draw_all_shape(shapes: List[Shape]):
for shape in shapes:
shape.draw()

我们定义了一个抽象类Shape, 子类Square和Circle继承Shape. 并且在子类中重写了父类的方法。函数draw_all_shape是绘制所有图形。

参数注入实现OCP原则

def circle_draw():
print(f"this is circle draw") def square_draw():
print(f"this is square draw") def draw_all_shape_by_function(data: Dict[str,Callable]):
for key,value in data.items():
value() data = {
"circle": circle_draw,
"square": square_draw
} draw_all_shape_by_function(data=data)

Conclusion

  • 这样的设计的好处是,如果需要再绘制一个三角形,那么我们只需要增加一个新类并继承Shape.无需修改shape类和draw_all_shape就可以实现三角形类的绘制。
  • 当我们在类中或函数中需要使用大量的if-else逻辑判断时,很有可能代码就违反了OSP原则。

LSP:Liskov 替换原则

  • 派生类应该可以替换父类中的方法使用,而不会改变程序原本的功能。
  • 派生类重写方法的参数应该和父类的保持一致或多于父类,不能少于父类。
  • 派生类重写方法的返回值必须和父类返回值类型一致。
  • 违反LSP原则,通常也会违反OSP原则。

首先我们来看一段违法LSP的例子

from typing import Iterable
class User():
    def __init__(self, user: str) -> None:
        self.user = user
    def disable(self) -> None:
        print(f"{self.user} disable!")        
  class Admin(User):
    def __init__(self, user: str = "Admin") -> None:
        self.user = user
    def disable(self):
        raise "Admin do not disable!"
    def delete_user(users: Iterable[User]):
    for user in users:
        user.disable()

当执行delete_user时,就会抛出TypeError 错误,Admin类中disable方法违法了LSP替换原则。

Optimize

#Good
from typing import Iterable class User():
def __init__(self, user: str) -> None:
self.user = user def allow_disable(self):
return True def disable(self) -> None:
print(f"{self.user} disable!") class Admin(User):
def __init__(self, user: str = "Admin") -> None:
self.user = user def allow_disable(self):
return False def delete_user(users: Iterable[User]) -> None:
for user in users:
if user.allow_disable:
user.disable()

Conclusion

  • 上例中通过添加allow_disable 的方法,解决了Admin类不能disable的问题。
  • 当派生类不正确的重写父类方法的时候,就会违反LSP原则,我们在继承类的时候重写方法的时候,尤其- 要注意是否违反了LSP原则。

DIP 依赖倒置原则

  • 程序中所有的依赖都应该终止于抽象类或接口。
  • 任何类都不应该从具体类派生。
  • 任何方法都不易应该重写它的任何基类已经实现了的方法。
  • 高层模块不应该依赖于低层模块,二者都应该依赖于抽象。

首先看一个违反DIP原则的例子:

class Lamp:
def turn_on(self):
print("turn on the lamp") def turn_off(self):
print("turn off the lamp") class Button(): def __init__(self) -> None:
self.lamp = Lamp() def turn_on(self):
return self.lamp.turn_on() def turn_off(self):
return self.lamp.turn_off()

当有一天,button需要控制televsion时,就需要修改Button类。Button和Lamp 具有强耦合关系。所以,当Lamp变动时,会影响到Button类。违法了DIP原则的高层模块依赖于底层模块。

Optimize

定义一个抽象类ElectricAppliance Button 和 Lamp 都依赖这个抽象类。 解决了Button和Lamp 具有强耦合的问题。

class ElectricAppliance(metaclass=ABCMeta):

    @abstractmethod
def turn_on(self):
pass @abstractmethod
def turn_off(self):
pass class Lamp(ElectricAppliance):
def turn_on(self):
print("turn on the lamp") def turn_off(self):
print("turn off the lamp") class Television(ElectricAppliance):
def turn_on(self):
print("turn on the televison") def turn_off(self):
print("turn off the televison") class Button: def __init__(self, electric_appliance: ElectricAppliance) -> None:
self.electric_appliance = electric_appliance def turn_on(self):
self.electric_appliance.turn_on() def turn_off(self):
self.electric_appliance.turn_off()

conclusion

  • 要确定代码是否违反了DIP原则,需要观察一个类中是否嵌入了调用其他类或函数。如果是,那么很可能是违反了DIP原则。

ISP 接口隔离原则

  • 客户应该不依赖它不使用的方法。
  • 一个类只做一件事。

首先来看一个违反ISP原则的例子:

class Animal(metaclass=ABCMeta):

    @abstractclassmethod
def run(self):
pass @abstractclassmethod
def speak(self):
pass @abstractclassmethod
def fly(self):
pass class Dog(Animal): def run(self):
return "Dog Running" def speak(self):
return "Dog Speaking" def fly(self):
raise TypeError("Dog can not fly") class Bird(Animal): def run(self):
raise TypeError("Bird can not run") def speak(self):
return "Bird Speaking" def fly(self):
return "Bird fly" def fly_animal(animals: Iterable[Animal]):
for animal in animals:
animal.fly()

当我们执行fly_animal时,就会抛出TypeError的错误。此时Animal抽象类是一个胖类,违法了ISP原则。

Optimize

  • 将Animal抽象类分解为三个新抽象类,FlyingAnimal, TalkingAnimal, RunningAnimal, 底层代码按需继承。
#good
class FlyingAnimal(metaclass=ABCMeta): @abstractclassmethod
def fly(self):
pass class RunningAnimal(metaclass=ABCMeta): @abstractclassmethod
def run(self):
pass class TalkingAnimal(metaclass=ABCMeta): @abstractclassmethod
def talk(self):
pass class Dog(RunningAnimal,TalkingAnimal): def run(self):
return "Dog Running" def talk(self):
return "Dog Speaking" class Bird(FlyingAnimal, TalkingAnimal): def talk(self):
return "Bird Speaking" def fly(self):
return "Bird fly" def fly_animal(animals: Iterable[FlyingAnimal]):
for animal in animals:
print(animal.fly())

conclusion

  • 接口隔离原则看似和单一职责原则相似,单一职责原则是针对模块,类,方法的设计。接口隔离原则更注重在调用者的角度,按需提供接口。
  • 写更小的类,大多数情况下是个好主意。
  • 违反ISP原则也可能会违反LSP原则和SRP原则。
  • 当子类重写了一个不需要的方法时,很可能违反了ISP原则。

想学会SOLID原则,看这一篇文章就够了!的更多相关文章

  1. Linux内核链表——看这一篇文章就够了

    本文从最基本的内核链表出发,引出初始化INIT_LIST_HEAD函数,然后介绍list_add,通过改变链表位置的问题引出list_for_each函数,然后为了获取容器结构地址,引出offseto ...

  2. 2300+字!在不同系统上安装Docker!看这一篇文章就够了

    辰哥准备出一期在Docker跑Python项目的技术文,比如在Docker跑Django或者Flask的网站.跑爬虫程序等等. 在Docker跑Python程序的时候不会太过于细去讲解Docker的基 ...

  3. .Net vs .Net Core,我改如何选择?看这一篇文章就够了

    前言 .Net目前支持构建服务器端应用程序的两种实现主要有两种,.NET Framework和.NET Core.两者共享许多相同的组件,并且您可以在两者之间共享代码.但是,两者之间存在根本差异,在我 ...

  4. Android:学习AIDL,这一篇文章就够了(下)

    前言 上一篇博文介绍了关于AIDL是什么,为什么我们需要AIDL,AIDL的语法以及如何使用AIDL等方面的知识,这一篇博文将顺着上一篇的思路往下走,接着介绍关于AIDL的一些更加深入的知识.强烈建议 ...

  5. (转载)Android:学习AIDL,这一篇文章就够了(下)

    前言 上一篇博文介绍了关于AIDL是什么,为什么我们需要AIDL,AIDL的语法以及如何使用AIDL等方面的知识,这一篇博文将顺着上一篇的思路往下走,接着介绍关于AIDL的一些更加深入的知识.强烈建议 ...

  6. (转载)Android:学习AIDL,这一篇文章就够了(上)

    前言 在决定用这个标题之前甚是忐忑,主要是担心自己对AIDL的理解不够深入,到时候大家看了之后说——你这是什么玩意儿,就这么点东西就敢说够了?简直是坐井观天不知所谓——那样就很尴尬了.不过又转念一想, ...

  7. 面试题-关于Java线程池一篇文章就够了

    在Java面试中,线程池相关知识,虽不能说是必问提,但出现的频次也是非常高的.同时又鉴于公众号"程序新视界"的读者后台留言让写一篇关于Java线程池的文章,于是就有本篇内容,本篇将 ...

  8. (转) TensorFlow深度学习,一篇文章就够了

    TensorFlow深度学习,一篇文章就够了 2016/09/22 · IT技术 · TensorFlow, 深度学习 分享到:6   原文出处: 我爱计算机 (@tobe迪豪 )    作者: 陈迪 ...

  9. 真的,Kafka 入门一篇文章就够了

    初识 Kafka 什么是 Kafka Kafka 是由 Linkedin 公司开发的,它是一个分布式的,支持多分区.多副本,基于 Zookeeper 的分布式消息流平台,它同时也是一款开源的基于发布订 ...

随机推荐

  1. ApplicationContext通常的实现是什么?

    FileSystemXmlApplicationContext :此容器从一个XML文件中加载beans的定义,XML Bean 配置文件的全路径名必须提供给它的构造函数. ClassPathXmlA ...

  2. Effective Java —— 多字段下考虑使用建造者模式构建实例

    本文参考 本篇文章参考自<Effective Java>第三版第二条"Consider a builder when faced with many constructor pa ...

  3. SVN在拉取(更新)代码的时候出现Error:svn: E155037: Previous operation has not finished; run 'cleanup' if it was interrupted问题 ---window版

    简易方法1 今天朋友看到朋友报错这个错误,偷偷学习了下他的方法并做记录以防忘记 简易方法2 今天使用svn时报了一个这个错,网上搜索时都说是要使用sqllite来删除svn队列. 其实可以直接使用id ...

  4. 《剑指offer》面试题3:二维数组中的查找

    面试题3:二维数组中的查找 面试题3:二维数组中的查找题目:在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序.请完成一个函数,输入这样的个二维数组和一个整数,判 ...

  5. task0002(四)- 练习:数据处理、轮播及交互

    转载自我的个人博客 欢迎大家批评指正 包括5部分: 小练习1-处理用户输入 小练习2-日期对象的使用 小练习3:轮播图 小练习4:输入提示框 小练习5:界面拖拽交互 源码地址task0002 在线De ...

  6. canvas离屏、旋转效果实践——旋转的雪花

    效果展示理论基础--"常见的canvas优化--模糊问题.旋转效果" 用离屏canvas画基础部分 1.封装画线函数 function drawLine(ctx,x1,y1,x2, ...

  7. Idea中创建maven项目(超详细)

    Idea中创建maven项目 提示:前提条件时maven已经安装好,并且环境变量也配置完成,maven没安装好或者环境变量没有配置好的请参考我上一篇文章--maven的安装和配置 上篇博文链接:htt ...

  8. QGIS源码编译步骤详解——官方新方案

    目录 源码下载 环境下载 Cygwin64 OSGeo4W CMAKE Visual Studio 2017 环境配置 配置 编译   方案详细可见源码文件中INSTALL.md. 源码下载   QG ...

  9. SpringMVC-开启静态资源访问权限

    1.配置 <mvc:resources mapping="/js/**" location="/js/"/> mapping:代表js目录下的所有文 ...

  10. Educational Codeforces Round 108 (Div. 2), C map套vector存储

    地址  Problem - C - Codeforces 题目 题意 一个学校有n个人参加比赛,他们分别属于ui队,每个人的能力值为si 当每个队需要1~n个人的时候,这个学校能参加的人的能力值和最大 ...