Python 中的 SOLID 设计原则详解与实战

Noah
Noah
发布时间:2026-10-15 | 4人浏览过
原创

本文深入解析 Python 编程中的 SOLID 五大设计原则,包括单一职责、开闭、里氏替换、接口隔离和依赖倒置。通过具体代码示例,展示如何在实际开发中应用这些原则,提升代码的可读性、可维护性和可扩展性,帮助开发者写出更高质量的 Python 代码。

编写能够运行的代码相对容易,但编写高质量、易于维护的代码则具有挑战性。

高质量的代码通常遵循特定的设计准则,这些准则被称为设计原则,主要包括以下五项:

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

将这五个原则英文名称的首字母组合起来,便构成了著名的 SOLID 原则。下文将逐一介绍这些原则,并结合 Python 语言展示具体的应用方法。

1、单一职责原则 SRP

单一职责原则(Single Responsibility Principle,简称 SRP)的英文定义是:A class or module should have a single responsibility。将其翻译为中文,即一个类或模块应当只负责一项职责(或功能)。

为了更直观地理解,我们来看一个简单的例子:假设有一个数字列表 L = [n1, n2, …, nx],我们需要对其执行一些数学运算,例如计算最大值、平均值等。

一种不够理想的做法是让一个函数承担所有计算任务:


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

在实际开发中,你可以将 math_operations 视为一个庞大且功能杂糅的函数,其中混杂了多种逻辑。

为了符合单一职责原则,首要步骤是将 math_operations 函数拆分为更细粒度的子函数,确保每个函数仅执行单一任务:


def get_mean(list_):
    '''Compute Max'''
    print(f"the mean is {np.mean(list_)}") 
def get_max(list_):
    '''Compute Max'''
    print(f"the max is {np.max(list_)}") 
def main(list_): 
    # Compute Average
    get_mean(list_)
    # Compute Max
    get_max(list_)
main([1,2,3,4,5])
# the mean is 3.0
# the max is 5

这种重构带来的好处包括:

  • 代码更易读且易于调试,错误定位更加准确。
  • 代码具备可复用性,各个功能模块可以在其他部分被重复调用。
  • 代码更易于测试,为每个功能单独创建测试用例变得更加简单。

然而,若需增加新功能(如计算中位数),main 函数仍可能变得难以维护,因此需要引入第二个原则:OCP。

2、开闭原则 OCP

开闭原则(Open Closed Principle,简称 OCP)的核心思想是“对扩展开放,对修改关闭”。这一原则能显著提升代码的可维护性。具体而言,当需要增加新功能时,只需添加新代码,而无需修改原有代码。这样做不仅简单,而且不会影响现有的单元测试,降低了出错风险;即使出现错误,也只需检查新添加的代码部分。

针对上述代码问题,可以通过将所有函数封装为一个类的子类来解决。具体代码如下:


import numpy as np
from abc import ABC, abstractmethod
class Operations(ABC):
    '''Operations'''
    @abstractmethod
    def operation():
        pass
class Mean(Operations):
    '''Compute Max'''
    def operation(list_):
        print(f"The mean is {np.mean(list_)}") 
class Max(Operations):
    '''Compute Max'''
    def operation(list_):
        print(f"The max is {np.max(list_)}") 
class Main:
    '''Main'''
    def get_operations(list_):
        # __subclasses__ will found all classes inheriting from Operations
        for operation in Operations.__subclasses__():
            operation.operation(list_)
if __name__ == "__main__":
    Main.get_operations([1,2,3,4,5])
# The mean is 3.0
# The max is 5

如果未来需要添加新操作(例如计算中位数 median),只需创建一个继承自 Operations 类的 Median 类即可。新形成的子类将立即被 __subclasses__() 接收,无需对代码的其他任何部分进行修改。

3、里氏替换原则 (LSP)

里氏替换原则的英文全称为 Liskov Substitution Principle,缩写为 LSP。该原则由 Barbara Liskov 于 1986 年首次提出,其原始描述如下:

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。

直译成中文即为:客户端不应被强迫依赖其不需要的接口。其中的“客户端”指的是接口的调用者或使用者。

举例说明:


from abc import ABC, abstractmethod
class Mammals(ABC):
    @abstractmethod
    def swim(self) -> bool:
        pass
    @abstractmethod
    def walk(self) -> bool:
        pass
class Human(Mammals):
    def swim(self)-> bool:
        print("Humans can swim")
        return True 
    def walk(self)-> bool:
        print("Humans can walk")
        return True 
class Whale(Mammals):
     def walk(self) -> bool:
        print("Whales can't walk")
        return False
     def swim(self):
        print("Whales can swim")
        return True 
human = Human()
human.swim()
human.walk()
whale = Whale()
whale.swim()
whale.walk()
 

执行结果:

Humans can swim Humans can walk Whales can swim Whales can't walk

事实上,子类 Whale(鲸鱼)不应依赖其不需要的接口 walk。针对此情况,需要对接口进行拆分,具体代码如下:


from abc import ABC, abstractmethod
class Swimer(ABC): 
    @abstractmethod
    def swim(self) -> bool:
        pass
class Walker(ABC):
    @abstractmethod
    def walk(self) -> bool:
        pass
class Human(Swimer,Walker):
    def swim(self)-> bool:
        print("Humans can swim")
        return True
    def walk(self)-> bool:
        print("Humans can walk")
        return True
class Whale(Swimer):
    def swim(self):
        print("Whales can swim")
        return True
human = Human()
human.swim()
human.walk()
whale = Whale()
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 编程应用设计原则的资料请关注本站其它相关文章!

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表