本文翻译自国外论坛 medium,原文地址:https://forreya.medium.com/the-solid-principles-writing-scalable-maintainable-code-13040ada3bca
有没有人告诉过你,你写的是 “糟糕的代码” ?
如果你写过,其实也没什么好羞愧的。在学习的过程中,我们都会编写有缺陷的代码。但是好消息是对于 “糟糕的代码” 进行改进是相当简单的,但前提是你愿意改。
改进代码的最佳方法之一是学习一些编程设计原则。我们可以将编程原则视为成为一名更好的程序员的进阶指南或者可以说这是代码的原始哲学。现在我将介绍五个基本原则,它们将被涵盖缩写在 SOLID 单词下。
我将在示例中使用 Python,但这些概念可以轻松转移到其他语言(例如 Java)。
这个原则告诉我们:
将我们的代码分解成模块,每个模块有一个职责。
让我们看一下这个 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}")
虽然代码量变得更多,但现在我们可以更容易地识别代码的每个部分试图完成的任务,可以更干净地测试代码,并在其他地方重用代码的一部分(而不需要担心不相关的方法)。
这一原则建议我们设计的模块遵循:
将来添加新功能而无需直接修改我们现有的代码。
一旦模块被使用,它基本上就被锁定了,这减少了任何新添加破坏代码的机会。
由于其自相矛盾的性质,这是 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
类的情况下添加新形状。这使我们能够扩展代码的功能,而无需更改其核心实现。
这个原则告诉我们以下内容:
子类应该能够与父类互换使用,而不会破坏程序的功能。
这到底是什么意思呢?让我们考虑一个带有名为 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,我们可以采取两条路线。我们来看看第一个。
解决方案 1:Bicycle
成为自己的类(无继承),以确保所有 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.")
这个原则指出,我们的模块不应该被迫担心它们不使用的功能。解释如下:
特定于客户端的接口比通用接口更好。这意味着类不应该被迫依赖于它们不使用的接口。相反,他们应该依赖更小、更具体的接口。
假设我们有一个 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.")
通过这样做,我们实现了一种设计,其中类只依赖它们需要的接口,减少了不必要的依赖。这在测试时变得特别有用,因为它允许我们仅模拟每个模块所需的功能。
这个解释起来非常简单,它指出:
高层模块不应该直接依赖于低层模块。相反,两者都应该依赖于抽象(接口或抽象类)。
让我们再来看一个例子。假设我们有一个 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
类的情况下更换数据库实现变得困难。
为了遵守依赖倒置原则,我们将引入 SQLDatabase
和 MongoDatabase
类都可以依赖的抽象(或接口)。
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
)现在不直接依赖于低级模块(SQLDatabase
或 MongoDatabase
)。相反,它们都依赖于接口(数据库)。
依赖倒置意味着我们的模块不需要知道它们正在获得什么实现 — 只需要知道它们将接收某些输入并返回某些输出。
现在我在网上看到很多关于 SOLID 设计原则以及它们是否经受住时间考验的讨论。在这个多范式编程、云计算和机器学习的现代世界中,SOLID 仍然有意义吗?
就我个人而言,我相信 SOLID 原则永远是好的代码设计的基础。有时在使用小型应用程序时,这些原则的好处可能并不明显,但一旦开始处理较大规模的项目,代码质量的差异就值得我们努力学习它们。SOLID 所提倡的模块化仍然使这些原则成为现代软件体系结构的基础,我个人认为这种情况短期内不会改变。
这里博主在对 SOLID 原则做一个总结输出。
SOLID 原则是一组编程设计原则,旨在提高软件的可扩展性、可维护性和质量。它们分别是:
通过遵循这些原则,我们可以编写出更加清晰、灵活和可复用的代码,降低耦合度和代码腐化的风险,提高代码的可测试性和可读性。当然,这些原则并不是铁律,而是指导性的建议,我们需要根据具体的场景和需求来灵活地运用它们。希望本文能够对你有所帮助和启发。😎
·END·
因公众号更改推送规则,关注公众号主页点击右上角"设为星标"第一时间获取博主精彩技术干货
往期原创热门文章推荐: