什么是 SOLID 原则?
2023年3月7日
什么是 SOLID 原则?
SOLID 原则是一组软件设计原则,用于指导软件开发人员设计和实现高质量的、易于维护和扩展的软件。它是由罗伯特·C·马丁在其著作《Agile Software Development, Principles, Patterns, and Practices》中提出的,是目前软件工程界被广泛接受的一种软件设计理念。
SOLID 五个原则
SOLID 原则包括以下五个原则:
1. 单一职责原则(Single Responsibility Principle,SRP)
一个类别只应该有一个职责。也就是说,一个类别应该只有一个引起它变化的原因。以下范例表示,
class ShoppingCart:
def __init__(self):
self.items = []
self.total = 0
def add_item(self, item):
self.items.append(item)
self.total += item.price
def remove_item(self, item):
self.items.remove(item)
self.total -= item.price
def print_receipt(self):
print('Items:')
for item in self.items:
print(f'{item.name} - ${item.price}')
print(f'Total: ${self.total}')
这个 ShoppingCart
类别同时负责处理购物车相关的任务和输出相关的任务。它的print_receipt()
方法应该被拆分为一个独立的类别或方法,以实现单一职责原则。
2. 开放封闭原则(Open-Closed Principle,OCP)
软件实体(类别、模组、函数等)应该对扩展开放,对修改封闭。这意味着当需要添加新功能时,应该扩展现有的实体,而不是修改它们。
class ShoppingCart:
def __init__(self):
self.items = []
def add_item(self, item):
self.items.append(item)
def remove_item(self, item):
self.items.remove(item)
def get_total_price(self):
total_price = 0
for item in self.items:
total_price += item.price
return total_price
class DiscountedShoppingCart(ShoppingCart):
def get_total_price(self):
total_price = super().get_total_price()
return total_price * 0.9
在这个范例中,当我们需要新增一个带有不同折扣的购物车时,我们必须创建一个新的子类别 DiscountedShoppingCart
并重写 get_total_price()
方法,而这也违反了开放封闭原则。
以下是一个符合开放封闭原则的修正过后的 Python 代码范例:
class ShoppingCart:
def __init__(self):
self.items = []
def add_item(self, item):
self.items.append(item)
def remove_item(self, item):
self.items.remove(item)
def get_total_price(self):
total_price = 0
for item in self.items:
total_price += item.price
return total_price
class Discount:
def calculate_discount(self, total_price):
return total_price
class TenPercentDiscount(Discount):
def calculate_discount(self, total_price):
return total_price * 0.9
class ShoppingCartWithDiscount:
def __init__(self, discount: Discount):
self.items = []
self.discount = discount
def add_item(self, item):
self.items.append(item)
def remove_item(self, item):
self.items.remove(item)
def get_total_price(self):
total_price = 0
for item in self.items:
total_price += item.price
return self.discount.calculate_discount(total_price)
在这个修正过后的范例中,我们将 get_total_price()
方法重构为使用Discount
策略类别,而 Discount
策略类别定义了一个共通的介面,可以用来扩展任何类型的折扣。
TenPercentDiscount
是 Discount
的一个子类别,它实现了 10% 的折扣计算。 ShoppingCartWithDiscount
类别持有一个 Discount
实例,并在计算总价时调用 calculate_discount()
方法。
这样,当我们需要新增一种不同类型的折扣时,只需要创建一个新的策略类别并将其传递给 ShoppingCartWithDiscount
即可,而无需修改现有的代码。
3. 里氏替换原则(Liskov Substitution Principle,LSP)
所有引用基类别的地方必须能够透明地使用其子类别的对象。换句话说,子类别应该可以替换其父类别并且不会破坏系统的正确性。
下面这个案例违反了 LSP,因为Rectangle
和Square
类别之间继承关系是有问题的,因为Square
继承自Rectangle
,但是Square
的set_height()
和set_width()
方法让它可以更改两个边的长度,从而违反了Rectangle
的定义,因为Rectangle
的两边可以有不同的长度。这导致在使用 Rectangle
类别对象的代码中,不能正确地使用 Square
对象来代替,进而违反了 LSP。
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def set_width(self, width):
self.width = width
def set_height(self, height):
self.height = height
def area(self):
return self.width * self.height
class Square(Rectangle):
def __init__(self, size):
self.width = size
self.height = size
def set_width(self, width):
self.width = width
self.height = width
def set_height(self, height):
self.width = height
self.height = height
square = Square(5)
square.set_width(10)
square.set_height(5)
print(square.area())
简单来说,LSP 要求子类别可以替代父类别在任何情况下使用,如果子类别有新增或修改方法而不被父类别所拥有,这样就会破坏原本设计的抽象。在这个例子中,Square
的特殊性质和 Rectangle
的不同导致它不能完全替代 Rectangle
,违反了 LSP。简单来说,LSP 要求子类别可以替代父类别在任何情况下使用,如果子类别有新增或修改方法而不被父类别所拥有,这样就会破坏原本设计的抽象。在这个例子中,Square
的特殊性质和 Rectangle
的不同导致它不能完全替代 Rectangle
,违反了 LSP。下面再来看看修复过后的代码:
class Shape:
def area(self):
pass
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def set_width(self, width):
self.width = width
def set_height(self, height):
self.height = height
def area(self):
return self.width * self.height
class Square(Shape):
def __init__(self, size):
self.size = size
def set_size(self, size):
self.size = size
def area(self):
return self.size ** 2
def print_area(shape):
print(f"Area: {shape.area()}")
shapes = [Rectangle(5, 10), Square(5)]
for shape in shapes:
print_area(shape)
这个范例中,Rectangle
和 Square
都继承自 Shape
类别,并且都实作了 area()
方法。在 print_area()
函数中,接受一个 Shape
物件作为引数,然后呼叫 area()
方法取得其面积。
这个范例中并没有违反 LSP,因为 Square
物件可以成功地替换 Shape
物件,而不影响程式的正常执行。换句话说,Square
物件与 Shape
物件具有相同的行为,且能够替代 Shape
物件,因此符合里氏替换原则。
4. 介面隔离原则(Interface Segregation Principle,ISP)
客户端不应该被迫依赖于它不使用的接口。接口应该被拆分为更小和更具体的部分,以便客户端只需要知道它们所需的部分。
先来看一段违反 ISP 的代码:
class Machine:
def print(self, document):
pass
def fax(self, document):
pass
def scan(self, document):
pass
class MultiFunctionPrinter(Machine):
def print(self, document):
print("Printing")
def fax(self, document):
print("Faxing")
def scan(self, document):
print("Scanning")
上述代码中,Machine
是一个机器的介面,包含了 print
、fax
和 scan
三个方法。而 MultiFunctionPrinter
是一个具有多种功能的印表机,它继承了 Machine
介面并实现了所有方法。这段代码违反了 ISP,因为不是所有的机器都需要实现fax
和scan
方法,而MultiFunctionPrinter
强制实现了这两个方法,这样的设计对于其他只需要实现print
方法的机器来说是多余的,也让介面变得不清晰。
再来看一段修复过后的代码:
class Printer:
def print(self, document):
pass
class Fax:
def fax(self, document):
pass
class Scanner:
def scan(self, document):
pass
class MultiFunctionDevice(Printer, Fax, Scanner):
def print(self, document):
print("Printing")
def fax(self, document):
print("Faxing")
def scan(self, document):
print("Scanning")
在这个新的设计中,我们将原来的介面 Machine
拆成了三个独立的介面 Printer
、Fax
和 Scanner
。 MultiFunctionDevice
是一个具有多种功能的装置,它实现了 Printer
、Fax
和 Scanner
三个介面。这样的设计让每个介面只包含必要的方法,并让装置可以实现自己所需要的介面。这样的设计更符合 ISP。
5. 依赖反转原则(Dependency Inversion Principle,DIP)
高层模组不应该依赖于低层模组,两者都应该依赖于抽象。换句话说,高层模组和低层模组之间应该通过介面或抽象类别进行交互。这样可以减少类别之间的直接耦合,从而提高代码的灵活性和可重用性。
先来看一个错误的范例:
class Logger:
def log(self, message):
print(f"Log: {message}")
class UserService:
def __init__(self):
self.logger = Logger()
def register(self, username, password):
try:
# register user to database
print(f"User {username} registered successfully")
self.logger.log(f"User {username} registered successfully")
except Exception as e:
print(f"Error: {e}")
self.logger.log(f"Error: {e}")
在这个例子中, UserService
直接创建 Logger
物件。这个设计违反了依赖反转原则,因为 UserService
的高层次模组直接依赖 Logger
这个低层次模组。
以下为修复过后的范例:
from abc import ABC, abstractmethod
class Logger(ABC):
@abstractmethod
def log(self, message):
pass
class ConsoleLogger(Logger):
def log(self, message):
print(f"Log: {message}")
class UserService:
def __init__(self, logger: Logger):
self.logger = logger
def register(self, username, password):
try:
# register user to database
print(f"User {username} registered successfully")
self.logger.log(f"User {username} registered successfully")
except Exception as e:
print(f"Error: {e}")
self.logger.log(f"Error: {e}")
logger = ConsoleLogger()
service = UserService(logger)
在这个例子中, UserService
现在只依赖 Logger
的抽象介面,而不是直接依赖实际的 Logger
物件。这个修改符合依赖反转原则。
💡 何谓高层次模组、何谓低层次模组? 在软件系统中,我们常会区分系统的不同层次,例如资料存取层、商业逻辑层、介面层等,资料存取层可能包含了一些和资料库沟通的代码,而商业逻辑层则使用资料存取层中提供的方法来操作资料。在这种情况下,商业逻辑层可以被视为高层次模组,因为它使用了低层次模组的服务。