SOLID 原则:编写可扩展且可维护的代码

本文翻译自国外论坛 medium,原文地址:https://forreya.medium.com/the-solid-principles-writing-scalable-maintainable-code-13040ada3bca


有没有人告诉过你,你写的是 “糟糕的代码” ?

如果你写过,其实也没什么好羞愧的。在学习的过程中,我们都会编写有缺陷的代码。但是好消息是对于 “糟糕的代码” 进行改进是相当简单的,但前提是你愿意改。

改进代码的最佳方法之一是学习一些编程设计原则。我们可以将编程原则视为成为一名更好的程序员的进阶指南或者可以说这是代码的原始哲学。现在我将介绍五个基本原则,它们将被涵盖缩写在 SOLID 单词下。

我将在示例中使用 Python,但这些概念可以轻松转移到其他语言(例如 Java)。

1. SOLID 第一个单词“S”代表单一职责

SOLID 原则:编写可扩展且可维护的代码
单一职责

这个原则告诉我们:

将我们的代码分解成模块,每个模块有一个职责。

让我们看一下这个 Person 类,它会执行和 Person 类不相关的任务,例如发送电子邮件和计算税金。

class Person:
  def __init__(self, name, age):
    self.name = name
    self.age = age

    def send_email(self, message):
        # 用于向此人发送电子邮件的代码
        print(f"Sending email to {self.name}{message}")

    def calculate_tax(self):
        # 为此人计算税费的代码
        tax = self.age * 100
        print(f"{self.name}'s tax: {tax}")

根据单一职责原则,我们应该将 Person 类拆分为几个更小的类,以避免违反该原则。

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

class EmailSender:
    def send_email(person, message):
        # 向此人发送电子邮件的代码
        print(f"Sending email to {person.name}{message}")

class TaxCalculator:
    def calculate_tax(person):
        # 为该人计算税费的代码
        tax = person.age * 100
        print(f"{person.name}'s tax: {tax}")

虽然代码量变得更多,但现在我们可以更容易地识别代码的每个部分试图完成的任务,可以更干净地测试代码,并在其他地方重用代码的一部分(而不需要担心不相关的方法)。

2. 第二个单词“O”代表开闭原则

SOLID 原则:编写可扩展且可维护的代码
开闭原则

这一原则建议我们设计的模块遵循:

将来添加新功能而无需直接修改我们现有的代码。

一旦模块被使用,它基本上就被锁定了,这减少了任何新添加破坏代码的机会。

由于其自相矛盾的性质,这是 5 个原则中最难完全掌握的原则之一,所以让我们看一个例子:

class Shape:
    def __init__(self, shape_type, width, height):
        self.shape_type = shape_type
        self.width = width
        self.height = height

    def calculate_area(self):
        if self.shape_type == "rectangle":
            # 计算并返回矩形的面积
        elif self.shape_type == "triangle":
            # 计算并返回三角形的面积

在上面的示例中,Shape 类直接在其 calculate_area() 方法中处理不同的形状类型。这违反了开闭原则,因为我们正在修改现有代码而不是扩展它。

这种设计是有问题的,因为随着添加更多形状类型,calculate_area() 方法变得更加复杂且难以维护。它违反了职责分离的原则,并使代码的灵活性和可扩展性降低。让我们看一下解决这个问题的一种方法。

class Shape:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def calculate_area(self):
        pass

class Rectangle(Shape):
    def calculate_area(self):
        # 为矩形实现calculate_area()方法

class Triangle(Shape):
    def calculate_area(self):
        # 实现三角形的calculate_area()方法

在上面的例子中,我们定义了基类 Shape,它的唯一目的是让更具体的形状类继承它的属性。例如,Triangle 类扩展为 calculate_area() 方法来计算并返回三角形的面积。

通过遵循开闭原则,我们可以在不修改现有 Shape 类的情况下添加新形状。这使我们能够扩展代码的功能,而无需更改其核心实现。

3. 第三个单词“L”代表里氏替换原则(LSP)

SOLID 原则:编写可扩展且可维护的代码
里氏替换原则

这个原则告诉我们以下内容:

子类应该能够与父类互换使用,而不会破坏程序的功能。

这到底是什么意思呢?让我们考虑一个带有名为 start_Engine() 方法的 Vehicle (车辆)类。

class Vehicle:
    def start_engine(self):
        pass

class Car(Vehicle):
    def start_engine(self):
        # 启动汽车发动机
        print("Car engine started.")

class Motorcycle(Vehicle):
    def start_engine(self):
        # 启动摩托车发动机
        print("Motorcycle engine started.")

根据里氏替换原则Vehicle 的任何子类也应该能够毫无问题地启动发动机。

但是,如果我们添加了 Bicycle(自行车)类。显然我们将无法再启动发动机,因为自行车没有发动机。下面演示了解决此问题的错误方法。

class Bicycle(Vehicle):
    def ride(self):
        # 骑自行车
        print("Riding the bike.")

    def start_engine(self):
         # 引发错误
        raise NotImplementedError("Bicycle does not have an engine.")

为了正确遵守 LSP,我们可以采取两条路线。我们来看看第一个。

解决方案 1Bicycle 成为自己的类(无继承),以确保所有 Vehicle 子类的行为与其超类一致。

class Vehicle:
    def start_engine(self):
        pass

class Car(Vehicle):
    def start_engine(self):
        # Start the car engine
        print("Car engine started.")

class Motorcycle(Vehicle):
    def start_engine(self):
        # Start the motorcycle engine
        print("Motorcycle engine started.")

class Bicycle():
    def ride(self):
        # Rides the bike
        print("Riding the bike.")

解决方案 2:将父类 Vehicle 分为两部分,一种用于带发动机的车辆,另一种用于后者。然后所有子类都可以与其父类互换使用,而不会改变预期行为或引入异常。

class VehicleWithEngines:
    def start_engine(self):
        pass

class VehicleWithoutEngines:
    def ride(self):
        pass

class Car(VehicleWithEngines):
    def start_engine(self):
        # 启动汽车发动机
        print("Car engine started.")

class Motorcycle(VehicleWithEngines):
    def start_engine(self):
        # 启动摩托车发动机
        print("Motorcycle engine started.")

class Bicycle(VehicleWithoutEngines):
    def ride(self):
        # 骑自行车
        print("Riding the bike.")

4. 第四个单词“I”代表接口隔离原则

SOLID 原则:编写可扩展且可维护的代码
接口隔离原则

这个原则指出,我们的模块不应该被迫担心它们不使用的功能。解释如下:

特定于客户端的接口比通用接口更好。这意味着类不应该被迫依赖于它们不使用的接口。相反,他们应该依赖更小、更具体的接口。

假设我们有一个 Animal 接口,其中包含 walk()swim()Fly() 等方法。

class Animal:
    def walk(self):
        pass

    def swim(self):
        pass

    def fly(self):
        pass

这里有个问题,并不是所有的 Animal 都能完成所有这些动作。

例如:狗不会游泳或飞翔,因此这两种从 Animal 接口继承的方法都是多余的。

class Dog(Animal):
    # 狗只能走路
    def walk(self):
        print("Dog is walking.")

class Fish(Animal):
    # 鱼只会游泳
    def swim(self):
        print("Fish is swimming.")

class Bird(Animal):
    # 鸟不会游泳
    def walk(self):
        print("Bird is walking.")

    def fly(self):
        print("Bird is flying.")

我们需要将 Animal 接口分解为更小、更具体的子类别,然后我们可以使用这些子类别来组成每种动物所需的一组精确功能。

class Walkable:
    def walk(self):
        pass

class Swimmable:
    def swim(self):
        pass

class Flyable:
    def fly(self):
        pass

class Dog(Walkable):
    def walk(self):
        print("Dog is walking.")

class Fish(Swimmable):
    def swim(self):
        print("Fish is swimming.")

class Bird(Walkable, Flyable):
    def walk(self):
        print("Bird is walking.")

    def fly(self):
        print("Bird is flying.")

通过这样做,我们实现了一种设计,其中类只依赖它们需要的接口,减少了不必要的依赖。这在测试时变得特别有用,因为它允许我们仅模拟每个模块所需的功能。

5. 第五个单词“D”代表依赖倒置原则

SOLID 原则:编写可扩展且可维护的代码
依赖倒置原则

这个解释起来非常简单,它指出:

高层模块不应该直接依赖于低层模块。相反,两者都应该依赖于抽象(接口或抽象类)

让我们再来看一个例子。假设我们有一个 ReportGenerator 类,它可以自然地生成报告。要执行此操作,需要首先从数据库中获取数据。

class SQLDatabase:
    def fetch_data(self):
        # 从 SQL 数据库获取数据
        print("Fetching data from SQL database...")

class ReportGenerator:
    def __init__(self, database: SQLDatabase):
        self.database = database

    def generate_report(self):
        data = self.database.fetch_data()
        # 使用获取的数据生成报告
        print("Generating report...")

在此示例中,ReportGenerator 类直接依赖于具体的 SQLDatabase 类。

目前这工作正常,但如果我们想切换到不同的数据库(例如 MongoDB)怎么办?这种紧密耦合使得在不修改 ReportGenerator 类的情况下更换数据库实现变得困难。

为了遵守依赖倒置原则,我们将引入 SQLDatabaseMongoDatabase 类都可以依赖的抽象(或接口)。

class Database():
    def fetch_data(self):
        pass

class SQLDatabase(Database):
    def fetch_data(self):
        # 从 SQL 数据库获取数据
        print("Fetching data from SQL database...")

class MongoDatabase(Database):
    def fetch_data(self):
        # 从 Mongo 数据库获取数据
        print("Fetching data from Mongo database...")

请注意 ReportGenerator 类现在还通过其构造函数依赖于新的数据库接口。

class ReportGenerator:
    def __init__(self, database: Database):
        self.database = database

    def generate_report(self):
        data = self.database.fetch_data()
        # 使用获取的数据生成报告
        print("Generating report...")

高级模块(ReportGenerator)现在不直接依赖于低级模块(SQLDatabaseMongoDatabase)。相反,它们都依赖于接口(数据库)。

依赖倒置意味着我们的模块不需要知道它们正在获得什么实现 — 只需要知道它们将接收某些输入并返回某些输出。

个人思考

SOLID 原则:编写可扩展且可维护的代码
SOLID

现在我在网上看到很多关于 SOLID 设计原则以及它们是否经受住时间考验的讨论。在这个多范式编程、云计算和机器学习的现代世界中,SOLID 仍然有意义吗?

就我个人而言,我相信 SOLID 原则永远是好的代码设计的基础。有时在使用小型应用程序时,这些原则的好处可能并不明显,但一旦开始处理较大规模的项目,代码质量的差异就值得我们努力学习它们。SOLID 所提倡的模块化仍然使这些原则成为现代软件体系结构的基础,我个人认为这种情况短期内不会改变。


博主总结

这里博主在对 SOLID 原则做一个总结输出。

SOLID 原则是一组编程设计原则,旨在提高软件的可扩展性、可维护性和质量。它们分别是:

  • 单一职责原则SOLID原则:一个类或模块应该只有一个职责,且该职责应该由该类或模块完全封装。
  • 开闭原则:一个类或模块应该对扩展开放,对修改关闭。也就是说,应该可以在不修改原有代码的基础上,增加新的功能或行为。
  • 里氏替换原则:一个类或模块的子类型应该能够替换其父类型,并且保持程序的正确性。也就是说,子类型应该遵守父类型的契约,不改变父类型的预期行为。
  • 接口隔离原则:一个类或模块不应该依赖于它不需要的接口,而应该只依赖于它需要的接口。也就是说,接口应该尽可能小而专一,避免过度泛化或臃肿。
  • 依赖倒置原则:一个类或模块应该依赖于抽象而不是具体实现。也就是说,高层模块不应该依赖于低层模块,而应该依赖于它们共同的抽象。

通过遵循这些原则,我们可以编写出更加清晰、灵活和可复用的代码,降低耦合度和代码腐化的风险,提高代码的可测试性和可读性。当然,这些原则并不是铁律,而是指导性的建议,我们需要根据具体的场景和需求来灵活地运用它们。希望本文能够对你有所帮助和启发。😎

·END·

因公众号更改推送规则,关注公众号主页点击右上角”设为星标第一时间获取博主精彩技术干货


往期原创热门文章推荐:

  1. 这四个开源项目太经典了

  2. 何时使用Kafka而不是RabbitMQ

  3. 万字详解常用设计模式

  4. 软件开发人员必须阅读的20本书

  5. 一图讲清楚公众号扫码关注绑定手机号自动登录流程

原文始发于微信公众号(waynblog):SOLID 原则:编写可扩展且可维护的代码

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/157817.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!