如何在 Python 中应用设计原则

知新闻 2023-07-05 17:29:38
51阅读

写出能用的代码很简单,写出好用的代码很难。

什么是好用的代码呢?其实就是代码质量比较高,如何评价代码质量的高低呢?最常用的、最重要的评价标准,就是代码的可维护性、可读性、可扩展性、灵活性、简洁性、可复用性、可测试性。

好用的代码,也都会遵循一此原则,这就是设计原则,它们分别是:

  • 单一职责原则 (SRP)
  • 开闭原则 (OCP)
  • 里氏替换原则 (LSP)
  • 接口隔离原则 (ISP)
  • 依赖倒置原则 (DIP)

提取这五种原则的首字母缩写词,就是 SOLID 原则。下面分别进行介绍,并展示如何在 Python 中应用。

1、单一职责原则 SRP

单一职责原则(Single Responsibility Principle)这个原则的英文描述是这样的:A class or module should have a single responsibility。如果我们把它翻译成中文,那就是:一个类或者模块只负责完成一个职责(或者功能)。

让我们举一个更简单的例子,我们有一个数字 L = [n1, n2, …, nx] 的列表,我们计算一些数学函数。例如,计算最大值、平均值等。

一个不好的方法是让一个函数来完成所有的工作:

 
  1. import numpy as np 
  2.  
  3. def math_operations(list_): 
  4.     # Compute Average 
  5.     print(f"the mean is {np.mean(list_)}"
  6.     # Compute Max 
  7.     print(f"the max is {np.max(list_)}")  
  8.  
  9. math_operations(list_ = [1,2,3,4,5]) 
  10. # the mean is 3.0 
  11. # the max is 5 

实际开发中,你可以认为 math_operations 很庞大,揉杂了各种功能代码。

为了使这个更符合单一职责原则,我们应该做的第一件事是将函数 math_operations 拆分为更细粒度的函数,一个函数只干一件事:

 
  1. def get_mean(list_): 
  2.     '''Compute Max''' 
  3.     print(f"the mean is {np.mean(list_)}")  
  4.  
  5. def get_max(list_): 
  6.     '''Compute Max''' 
  7.     print(f"the max is {np.max(list_)}")  
  8.  
  9. def main(list_):  
  10.     # Compute Average 
  11.     get_mean(list_) 
  12.     # Compute Max 
  13.     get_max(list_) 
  14.  
  15. main([1,2,3,4,5]) 
  16. # the mean is 3.0 
  17. # the max is 5 

这样做的好处就是:

  • 易读易调试,更容易定位错误。
  • 可复用,代码的任何部分都可以在代码的其他部分中重用。
  • 可测试,为代码的每个功能创建测试更容易。

但是要增加新功能,比如计算中位数,main 函数还是很难维护,因此还需要第二个原则:OCP。

2、开闭原则 OCP

开闭原则(Open Closed Principle)就是对扩展开放,对修改关闭,这可以大大提升代码的可维护性,也就是说要增加新功能时只需要添加新的代码,不修改原有的代码,这样做即简单,也不会影响之前的单元测试,不容易出错,即使出错也只需要检查新添加的代码。

上述代码,可以通过将我们编写的所有函数变成一个类的子类来解决这个问题。代码如下:

 
  1. import numpy as np 
  2. from abc import ABC, abstractmethod 
  3.  
  4. class Operations(ABC): 
  5.     '''Operations''' 
  6.     @abstractmethod 
  7.     def operation(): 
  8.         pass 
  9.  
  10. class Mean(Operations): 
  11.     '''Compute Max''' 
  12.     def operation(list_): 
  13.         print(f"The mean is {np.mean(list_)}")  
  14.  
  15. class Max(Operations): 
  16.     '''Compute Max''' 
  17.     def operation(list_): 
  18.         print(f"The max is {np.max(list_)}")  
  19.  
  20. class Main: 
  21.     '''Main''' 
  22.     def get_operations(list_): 
  23.         # __subclasses__ will found all classes inheriting from Operations 
  24.         for operation in Operations.__subclasses__(): 
  25.             operation.operation(list_) 
  26.  
  27.  
  28. if __name__ == "__main__"
  29.     Main.get_operations([1,2,3,4,5]) 
  30. # The mean is 3.0 
  31. # The max is 5 

如果现在我们想添加一个新的操作,例如:median,我们只需要添加一个继承自 Operations 类的 Median 类。新形成的子类将立即被 __subclasses__()接收,无需对代码的任何其他部分进行修改。

3、里氏替换原则 (LSP)

里式替换原则的英文是 Liskov Substitution Principle,缩写为 LSP。这个原则最早是在 1986 年由 Barbara Liskov 提出,他是这么描述这条原则的:

If S is a subtype of T, then objects of type T may be replaced with objects of type S, without breaking the program。

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

实际上,里式替换原则还有另外一个更加能落地、更有指导意义的描述,那就是按照协议来设计,子类在设计的时候,要遵守父类的行为约定(或者叫协议)。父类定义了函数的行为约定,那子类可以改变函数的内部实现逻辑,但不能改变函数原有的行为约定。这里的行为约定包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至包括注释中所罗列的任何特殊说明。

4、接口隔离原则 (ISP)

接口隔离原则的英文翻译是 Interface Segregation Principle,缩写为 ISP。Robert Martin 在 SOLID 原则中是这样定义它的:Clients should not be forced to depend upon interfaces that they do not use。

直译成中文的话就是:客户端不应该被强迫依赖它不需要的接口。其中的 客户端 ,可以理解为接口的调用者或者使用者。

举个例子:

 
  1. from abc import ABC, abstractmethod 
  2. class Mammals(ABC): 
  3.  
  4.     @abstractmethod 
  5.     def swim(self) -> bool: 
  6.         pass 
  7.  
  8.     @abstractmethod 
  9.     def walk(self) -> bool: 
  10.         pass 
  11.  
  12.  
  13. class Human(Mammals): 
  14.     def swim(self)-> bool: 
  15.         print("Humans can swim"
  16.         return True 
  17.  
  18.     def walk(self)-> bool: 
  19.         print("Humans can walk"
  20.         return True 
  21.  
  22.  
  23. class Whale(Mammals): 
  24.  
  25.     def walk(self) -> bool: 
  26.         print("Whales can't walk"
  27.         return False 
  28.  
  29.     def swim(self): 
  30.         print("Whales can swim"
  31.         return True 
  32.  
  33.  
  34. human = Human() 
  35. human.swim() 
  36. human.walk() 
  37. whale = Whale() 
  38. whale.swim() 
  39. whale.walk() 

执行结果:

 
  1. Humans can swim 
  2. Humans can walk 
  3. Whales can swim 
  4. Whales can't walk 

事实上,子类鲸鱼不应该依赖它不需要的接口 walk,针对这种情况,就需要对接口进行拆分,代码如下:

 
  1. from abc import ABC, abstractmethod 
  2.  
  3. class Swimer(ABC): 
  4.  
  5.     @abstractmethod 
  6.     def swim(self) -> bool: 
  7.         pass 
  8.  
  9. class Walker(ABC): 
  10.     @abstractmethod 
  11.     def walk(self) -> bool: 
  12.         pass 
  13.  
  14.  
  15. class Human(Swimer,Walker): 
  16.     def swim(self)-> bool: 
  17.         print("Humans can swim"
  18.         return True 
  19.  
  20.     def walk(self)-> bool: 
  21.         print("Humans can walk"
  22.         return True 
  23.  
  24.  
  25. class Whale(Swimer): 
  26.  
  27.     def swim(self): 
  28.         print("Whales can swim"
  29.         return True 
  30.  
  31.  
  32. human = Human() 
  33. human.swim() 
  34. human.walk() 
  35. whale = Whale() 
  36. whale.swim() 

5、依赖反转原则 (DIP)

依赖反转原则的英文翻译是 Dependency Inversion Principle,缩写为 DIP。英文描述:High-level modules shouldn’t depend on low-level modules. Both modules should depend on abstractions. In addition, abstractions shouldn’t depend on details. Details depend on abstractions。

我们将它翻译成中文,大概意思就是:高层模块不要依赖低层模块。高层模块和低层模块应该通过抽象(abstractions)来互相依赖。除此之外,抽象不要依赖具体实现细节,具体实现细节依赖抽象。

在调用链上,调用者属于高层,被调用者属于低层,我们写的代码都属于低层,由框架来调用。在平时的业务代码开发中,高层模块依赖低层模块是没有任何问题的,但是在框架层面设计的时候,就要考虑通用性,高层应该依赖抽象的接口,低层应该实现对应的接口。如下图所示:

也就是说本来 ObjectA 依赖 ObjectB,但为了扩展后面可能会有 ObjectC,ObjectD,经常变化,因此为了频繁改动,让高层模块依赖抽象的接口 interface,然后让 ObjectB 也反过来依赖 interface,这就是依赖反转原则。

举个例子,wsgi 协议就是一种抽象接口,高层模块有 uWSGI,gunicorn等,低层模块有 Django,Flask 等,uWSGI,gunicorn 并不直接依赖 Django,Flask,而是通过 wsgi 协议进行互相依赖。

依赖倒置原则概念是高层次模块不依赖于低层次模块。看似在要求高层次模块,实际上是在规范低层次模块的设计。低层次模块提供的接口要足够的抽象、通用,在设计时需要考虑高层次模块的使用种类和场景。明明是高层次模块要使用低层次模块,对低层次模块有依赖性。现在反而低层次模块需要根据高层次模块来设计,出现了「倒置」的显现。

这样设计好处有两点:

  • 低层次模块更加通用,适用性更广
  • 高层次模块没有依赖低层次模块的具体实现,方便低层次模块的替换

最后的话

我去年(2020)年 2 月 3 号购买的《设计模式之美》专栏,将近一年半才把它学习完,再回看之前的代码,真是一堆垃圾。之前一天写完的代码,重构差不多花了一个星期,重构之后,感觉还可以再重构的更好,似乎无止境,正如小争哥说的那样,项目无论大小,都可以有技术含量,所谓代码细节是魔鬼,细节到处都存在在取舍,应该怎么样,不应该怎么样,都大有学问。

本文转载自微信公众号「Python七号」,可以通过以下二维码关注。转载本文请联系Python七号公众号。

the end
免责声明:本文不代表本站的观点和立场,如有侵权请联系本站删除!本站仅提供信息存储空间服务。