什么是 SOLID 原则?

2023年3月7日

💎 加入 E+ 成長計畫 與超過 500+ 位軟體工程師一同在社群中成長,並且獲得更多的軟體工程學習資源

什么是 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 策略类别定义了一个共通的介面,可以用来扩展任何类型的折扣。

TenPercentDiscountDiscount 的一个子类别,它实现了 10% 的折扣计算。 ShoppingCartWithDiscount 类别持有一个 Discount 实例,并在计算总价时调用 calculate_discount() 方法。

这样,当我们需要新增一种不同类型的折扣时,只需要创建一个新的策略类别并将其传递给 ShoppingCartWithDiscount 即可,而无需修改现有的代码。

3. 里氏替换原则(Liskov Substitution Principle,LSP)

所有引用基类别的地方必须能够透明地使用其子类别的对象。换句话说,子类别应该可以替换其父类别并且不会破坏系统的正确性。

下面这个案例违反了 LSP,因为RectangleSquare 类别之间继承关系是有问题的,因为Square 继承自Rectangle,但是Squareset_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)

这个范例中,RectangleSquare 都继承自 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 是一个机器的介面,包含了 printfaxscan 三个方法。而 MultiFunctionPrinter 是一个具有多种功能的印表机,它继承了 Machine 介面并实现了所有方法。这段代码违反了 ISP,因为不是所有的机器都需要实现faxscan 方法,而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 拆成了三个独立的介面 PrinterFaxScannerMultiFunctionDevice 是一个具有多种功能的装置,它实现了 PrinterFaxScanner 三个介面。这样的设计让每个介面只包含必要的方法,并让装置可以实现自己所需要的介面。这样的设计更符合 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 物件。这个修改符合依赖反转原则。

💡 何谓高层次模组、何谓低层次模组? 在软件系统中,我们常会区分系统的不同层次,例如资料存取层、商业逻辑层、介面层等,资料存取层可能包含了一些和资料库沟通的代码,而商业逻辑层则使用资料存取层中提供的方法来操作资料。在这种情况下,商业逻辑层可以被视为高层次模组,因为它使用了低层次模组的服务。

🧵 如果你想收到最即時的內容更新,可以在 FacebookInstagram 上追蹤我們