Uncle Bob 的 SOLID 软件设计原则——Python 实例讲解

SOLID 是 5 种软件设计原则的首字母缩写,由美国的软件工程师 Robert C. Martin(习惯上被称为 Uncle Bob)总结。可以帮助程序员写出更加灵活、容易理解、可维护性强、方便扩展的健壮代码。

  • S 代表 Single Responsibility Principle (SRP),一个类应该只包含一种单一的职责,有且仅有一种原因能促使其变更。通俗点说,让一个类只做一件事。如果需要承担更多的工作,那么分解这个类。
  • O 代表 Open/Closed Principle (OCP),软件实体应该对扩展是开放的,同时对修改是封闭的。如果需要添加额外的功能,应该优先扩展某个类而不是修改它。
  • L 代表 Liskov Substitution Principle (LSP),程序中的对象应该能够替换为其子类型的实例,仍不影响代码的正确性。
  • I 代表 Interface Segregation Principle (ISP),多个专门的基于客户端的接口要好于只有一个通用的接口。一个类对另一个类的依赖性应该建立在最小的接口上,客户端不应该被强迫实现一些他们不会使用的接口。
  • D 代表 Dependency Inversion Principle (DIP),抽象不应该依赖于细节,细节应当依赖于抽象。即要针对抽象(接口)编程,而不是针对实现细节编程。

实例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Order:
items = []
quantities = []
prices = []
status = "open"

def add_item(self, name, quantity, price):
self.items.append(name)
self.quantities.append(quantity)
self.prices.append(price)

def total_price(self):
total = 0
for i in range(len(self.prices)):
total += self.quantities[i] * self.prices[i]
return total

def pay(self, payment_type, security_code):
if payment_type == "debit":
print("Processing debit payment type")
print(f"Verifying security code: {security_code}")
self.status = "paid"
elif payment_type == "credit":
print("Processing credit payment type")
print(f"Verifying security code: {security_code}")
self.status = "paid"
else:
raise Exception(f"Unknown payment type: {payment_type}")


order = Order()
order.add_item("Keyborad", 1, 50)
order.add_item("SSD", 1, 150)
order.add_item("USB cable", 2, 5)

print(order.total_price())
order.pay("debit", "0372846")

上述 Python 代码实现了一个简单的“购物车”(订单)应用。

  • add_item 方法可以向订单中添加新的货物
  • total_price 方法可以计算订单的总价
  • pay 方法实现了订单的支付功能,支持借记卡、信用卡等支付方式

Single Responsibility Principle

单一职能原则。
将支付功能从 Order 类中分离出来,在另一个 PaymentProcessor 类中实现。同时去掉 pay 方法中的 if-else 判断,分别用两个函数 pay_debitpay_credit 实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Order:
items = []
quantities = []
prices = []
status = "open"

def add_item(self, name, quantity, price):
self.items.append(name)
self.quantities.append(quantity)
self.prices.append(price)

def total_price(self):
total = 0
for i in range(len(self.prices)):
total += self.quantities[i] * self.prices[i]
return total


class PaymentProcessor:
def pay_debit(self, order, security_code):
print("Processing debit payment type")
print(f"Verifying security code: {security_code}")
order.status = "paid"

def pay_credit(self, order, security_code):
print("Processing credit payment type")
print(f"Verifying security code: {security_code}")
order.status = "paid"


order = Order()
order.add_item("Keyborad", 1, 50)
order.add_item("SSD", 1, 150)
order.add_item("USB cable", 2, 5)

print(order.total_price())
processor = PaymentProcessor()
processor.pay_debit(order, "0372846")

Open/Closed Principle

在最新的支付功能的实现中,如果我们需要添加一个新的支付方法(比如 PayPal),就必须修改 PaymentProcessor 类的原始代码。这就违反了 Open/Closed 原则,额外的功能应该通过扩展而不是修改原来的类来实现。
改进的方法是用一个基类(PaymentProcessor)来定义基本的支付逻辑,再通过子类(如 DebitPaymentProcessor)来实现具体的支付方法。这样每当添加一种新的支付方式,直接实现一个新的子类即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
from abc import ABC, abstractmethod


class Order:
items = []
quantities = []
prices = []
status = "open"

def add_item(self, name, quantity, price):
self.items.append(name)
self.quantities.append(quantity)
self.prices.append(price)

def total_price(self):
total = 0
for i in range(len(self.prices)):
total += self.quantities[i] * self.prices[i]
return total


class PaymentProcessor(ABC):
@abstractmethod
def pay(self, order, security_code):
pass


class DebitPaymentProcessor(PaymentProcessor):
def pay(self, order, security_code):
print("Processing debit payment type")
print(f"Verifying security code: {security_code}")
order.status = "paid"


class CreditPaymentProcessor(PaymentProcessor):
def pay(self, order, security_code):
print("Processing credit payment type")
print(f"Verifying security code: {security_code}")
order.status = "paid"


order = Order()
order.add_item("Keyborad", 1, 50)
order.add_item("SSD", 1, 150)
order.add_item("USB cable", 2, 5)

print(order.total_price())
processor = DebitPaymentProcessor()
processor.pay(order, "0372846")

Liskov Substitution Principle

假设我们现在需要添加一种新的支付方式 PayPalPaymentProcessor,它在支付时并不依赖于 security_code 而是需要 email_address 进行验证。即 pay 方法的定义是 pay(self, order, email_address),与基类中虚拟方法的签名冲突。
改进的方法是将 pay 方法依赖的参数 security_codeemail_address 移动到支付类的 __init__ 方法中,将基类和子类的 pay 方法签名都改为 pay(self, order)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
from abc import ABC, abstractmethod


class Order:
items = []
quantities = []
prices = []
status = "open"

def add_item(self, name, quantity, price):
self.items.append(name)
self.quantities.append(quantity)
self.prices.append(price)

def total_price(self):
total = 0
for i in range(len(self.prices)):
total += self.quantities[i] * self.prices[i]
return total


class PaymentProcessor(ABC):
@abstractmethod
def pay(self, order):
pass


class DebitPaymentProcessor(PaymentProcessor):
def __init__(self, security_code):
self.security_code = security_code

def pay(self, order):
print("Processing debit payment type")
print(f"Verifying security code: {self.security_code}")
order.status = "paid"


class CreditPaymentProcessor(PaymentProcessor):
def __init__(self, security_code):
self.security_code = security_code

def pay(self, order, security_code):
print("Processing credit payment type")
print(f"Verifying security code: {security_code}")
order.status = "paid"


class PaypalPaymentProcessor(PaymentProcessor):
def __init__(self, email_address):
self.email_address = email_address

def pay(self, order):
print("Processing paypal payment type")
print(f"Verifying email address: {self.email_address}")
order.status = "paid"


order = Order()
order.add_item("Keyborad", 1, 50)
order.add_item("SSD", 1, 150)
order.add_item("USB cable", 2, 5)

print(order.total_price())
processor = PaypalPaymentProcessor('hi@example.com')
processor.pay(order)

Interface Segregation Principle

假设我们需要在支付组件中添加一个验证短信的功能。直观的想法是直接在 PaymentProcessor 基类中添加一个 auth_sms 虚拟方法:

1
2
3
4
5
6
7
8
class PaymentProcessor(ABC):
@abstractmethod
def pay(self, order):
pass

@abstractmethod
def auth_sms(self, code):
pass

对于需要验证短信的支付方式比如借记卡,改为如下形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class DebitPaymentProcessor(PaymentProcessor):
def __init__(self, security_code):
self.security_code = security_code
self.verified = False

def auth_sms(self, code):
print(f"Verifying SMS code {code}")
self.verified = True

def pay(self, order):
if not self.verified:
raise Exception("Not authorized")
print("Processing debit payment type")
print(f"Verifying security code: {self.security_code}")
order.status = "paid"

对于不需要短信验证的支付方式比如信用卡,就改为如下形式:

1
2
3
4
5
6
7
8
9
10
11
12
class CreditPaymentProcessor(PaymentProcessor):
def __init__(self, security_code):
self.security_code = security_code

def auth_sms(self, code):
raise Exception(
"Credit card payments don't support SMS code authorization.")

def pay(self, order, security_code):
print("Processing credit payment type")
print(f"Verifying security code: {security_code}")
order.status = "paid"

上述实现的问题在于,我们定义了一个通用的支付接口(PaymentProcessor),包含 payauth_sms 两种验证逻辑。但这两种逻辑并不总是被具体的支付方式(比如 CreditPaymentProcessor)所需要。
这违反了接口分离原则。即接口的实现应该依赖于具体的客户端(子类)需求,而不能不管客户端是否需要,就将所有的功能都放在一个胖接口中。
可以额外再实现一个 PaymentProcessor_SMS 基类来定义短信验证的逻辑,让不需要短信验证的支付方式继承 PaymentProcessor 基类,需要短信验证的支付方式继承 PaymentProcessor_SMS 基类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
from abc import ABC, abstractmethod


class Order:
items = []
quantities = []
prices = []
status = "open"

def add_item(self, name, quantity, price):
self.items.append(name)
self.quantities.append(quantity)
self.prices.append(price)

def total_price(self):
total = 0
for i in range(len(self.prices)):
total += self.quantities[i] * self.prices[i]
return total


class PaymentProcessor(ABC):
@abstractmethod
def pay(self, order):
pass


class PaymentProcessor_SMS(PaymentProcessor):
@abstractmethod
def auth_sms(self, code):
pass


class DebitPaymentProcessor(PaymentProcessor_SMS):
def __init__(self, security_code):
self.security_code = security_code
self.verified = False

def auth_sms(self, code):
print(f"Verifying SMS code {code}")
self.verified = True

def pay(self, order):
if not self.verified:
raise Exception("Not authorized")
print("Processing debit payment type")
print(f"Verifying security code: {self.security_code}")
order.status = "paid"


class CreditPaymentProcessor(PaymentProcessor):
def __init__(self, security_code):
self.security_code = security_code

def pay(self, order, security_code):
print("Processing credit payment type")
print(f"Verifying security code: {security_code}")
order.status = "paid"


class PaypalPaymentProcessor(PaymentProcessor_SMS):
def __init__(self, email_address):
self.email_address = email_address
self.verified = False

def auth_sms(self, code):
print(f"Verifying SMS code {code}")
self.verified = True

def pay(self, order):
if not self.verified:
raise Exception("Not authorized")
print("Processing paypal payment type")
print(f"Verifying email address: {self.email_address}")
order.status = "paid"


order = Order()
order.add_item("Keyborad", 1, 50)
order.add_item("SSD", 1, 150)
order.add_item("USB cable", 2, 5)

print(order.total_price())
processor = PaypalPaymentProcessor('hi@example.com')
processor.auth_sms(123456)
processor.pay(order)

Composition over Inheritance

在软件设计的大部分场景中,组合要优于继承。因为继承总是意味着更紧密的耦合性。
实际上短信认证并不一定通过继承来实现(PaymentProcessor_SMS),还可以通过组合来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
from abc import ABC, abstractmethod


class Order:
items = []
quantities = []
prices = []
status = "open"

def add_item(self, name, quantity, price):
self.items.append(name)
self.quantities.append(quantity)
self.prices.append(price)

def total_price(self):
total = 0
for i in range(len(self.prices)):
total += self.quantities[i] * self.prices[i]
return total


class SMSAuth:
authorized = False

def verify_code(self, code):
print(f"Verifying code {code}")
self.authorized = True

def is_authorized(self) -> bool:
return self.authorized


class PaymentProcessor(ABC):
@abstractmethod
def pay(self, order):
pass


class DebitPaymentProcessor(PaymentProcessor):
def __init__(self, security_code, authorizer: SMSAuth):
self.security_code = security_code
self.authorizer = authorizer
self.verified = False

def pay(self, order):
if not self.authorizer.is_authorized():
raise Exception("Not authorized")
print("Processing debit payment type")
print(f"Verifying security code: {self.security_code}")
order.status = "paid"


class CreditPaymentProcessor(PaymentProcessor):
def __init__(self, security_code):
self.security_code = security_code

def pay(self, order, security_code):
print("Processing credit payment type")
print(f"Verifying security code: {security_code}")
order.status = "paid"


class PaypalPaymentProcessor(PaymentProcessor):
def __init__(self, email_address, authorizer: SMSAuth):
self.email_address = email_address
self.authorizer = authorizer

def pay(self, order):
if not self.authorizer.is_authorized():
raise Exception("Not authorized")
print("Processing paypal payment type")
print(f"Verifying email address: {self.email_address}")
order.status = "paid"


order = Order()
order.add_item("Keyborad", 1, 50)
order.add_item("SSD", 1, 150)
order.add_item("USB cable", 2, 5)

print(order.total_price())
authorizer = SMSAuth()
processor = DebitPaymentProcessor('0372846', authorizer)
authorizer.verify_code(123456)
processor.pay(order)

定义一个 SMS_Auth 类来实现短信验证的逻辑,再通过组合的方式将其实例添加到具体的需要短信验证的支付方式中(比如 DebitPaymentProcessor)。

Dependency Inversion Principle

细节应该依赖于抽象,而不是抽象依赖于细节。上述实现中就违反了这个原则。
比如借记卡支付方式(DebitPaymentProcessor)的 __init__ 方法,签名是 __init__(self, security_code, authorizer: SMSAuth)。其中的 SMSAuth 是一个具体的短信验证类型,而不是一个通用的代表某种验证类型的抽象。
这样当支付方式需要的是另外一种验证方法(比如 NotARobot),这里的签名就需要修改。

可以创建一个 Authorizer 基类来代表通用的验证方式,具体的验证方式比如 SMSAuthNotARobot 则作为 Authorizer 的子类来实现。
在支付方式的实现中,则使用 Authorizer 作为验证方式的类型定义。这样在使用支付类的实例时,就可以灵活地传入 Authorizer 的子类 SMSAuth 或者 NotARobot 进行组合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
from abc import ABC, abstractmethod


class Order:
items = []
quantities = []
prices = []
status = "open"

def add_item(self, name, quantity, price):
self.items.append(name)
self.quantities.append(quantity)
self.prices.append(price)

def total_price(self):
total = 0
for i in range(len(self.prices)):
total += self.quantities[i] * self.prices[i]
return total


class Authorizer(ABC):
@abstractmethod
def is_authorized(self) -> bool:
pass


class SMSAuth(Authorizer):
authorized = False

def verify_code(self, code):
print(f"Verifying code {code}")
self.authorized = True

def is_authorized(self) -> bool:
return self.authorized


class NotARobot(Authorizer):
authorized = False

def not_a_robot(self):
print("Are you a robot? Naa")
self.authorized = True

def is_authorized(self) -> bool:
return self.authorized


class PaymentProcessor(ABC):
@abstractmethod
def pay(self, order):
pass


class DebitPaymentProcessor(PaymentProcessor):
def __init__(self, security_code, authorizer: Authorizer):
self.security_code = security_code
self.authorizer = authorizer
self.verified = False

def pay(self, order):
if not self.authorizer.is_authorized():
raise Exception("Not authorized")
print("Processing debit payment type")
print(f"Verifying security code: {self.security_code}")
order.status = "paid"


class CreditPaymentProcessor(PaymentProcessor):
def __init__(self, security_code):
self.security_code = security_code

def pay(self, order, security_code):
print("Processing credit payment type")
print(f"Verifying security code: {security_code}")
order.status = "paid"


class PaypalPaymentProcessor(PaymentProcessor):
def __init__(self, email_address, authorizer: Authorizer):
self.email_address = email_address
self.authorizer = authorizer

def pay(self, order):
if not self.authorizer.is_authorized():
raise Exception("Not authorized")
print("Processing paypal payment type")
print(f"Verifying email address: {self.email_address}")
order.status = "paid"


order = Order()
order.add_item("Keyborad", 1, 50)
order.add_item("SSD", 1, 150)
order.add_item("USB cable", 2, 5)

print(order.total_price())
authorizer = NotARobot()
processor = DebitPaymentProcessor('0372846', authorizer)
authorizer.not_a_robot()
processor.pay(order)

参考资料

Uncle Bob’s SOLID principles made easy 🍀 - in Python!