有人告诉过你,你写的是“糟糕的代码”吗?
如果你有,那真的没什么可羞愧的。我们在学习的过程中都会写出有缺陷的代码。好消息是,改进起来相当简单——但前提是你愿意。
改进代码的最佳方法之一是学习一些编程设计原则。你可以将编程原则视为成为更好程序员的一般指南 - 可以说是代码的原始哲学。现在,有一系列的原则(有人可能会说甚至可能过多),但我将介绍五个基本原则,它们都归为缩写 SOLID。
ps:我将在示例中使用 Python,但这些概念可以轻松转移到其他语言,例如 Java。
1. “S” 单一职责
这一原则教导我们:
将我们的代码分成每个负责一个职责的模块
让我们看一下这个Person执行不相关任务(例如发送电子邮件和计算税金)的类。
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def send_email(self, message):
# Code to send an email to the person
print(f"Sending email to {self.name}: {message}")
def calculate_tax(self):
# Code to calculate tax for the person
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):
# Code to send an email to the person
print(f"Sending email to {person.name}: {message}")
class TaxCalculator:
def calculate_tax(person):
# Code to calculate tax for the person
tax = person.age * 100
print(f"{person.name}'s tax: {tax}")
现在我们可以更轻松地识别代码的每个部分试图完成的任务,更干净地测试它,并在其他地方重用它(而不必担心不相关的方法)。
2. “O” 开放/封闭原则
该原则建议我们设计模块时应能够:
将来添加新的功能,而无需直接修改我们现有的代码
一旦模块被使用,它基本上就被锁定了,这减少了任何新添加的内容破坏您的代码的可能性。
由于其矛盾性,这是 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":
# Calculate and return the area of a rectangle
elif self.shape_type == "triangle":
# Calculate and return the area of a 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):
# Implement the calculate_area() method for Rectangle
class Triangle(Shape):
def calculate_area(self):
# Implement the calculate_area() method for Triangle
在上面的例子中,我们定义了基类Shape,其唯一目的是允许更具体的形状类继承其属性。例如,该类Triangle扩展了calculate_area()方法来计算并返回三角形的面积。
通过遵循开放/封闭原则,我们可以添加新形状而无需修改现有Shape类。这使我们能够扩展代码的功能,而无需更改其核心实现。
3. “L” 里氏替换原则(LSP)
在这个原则中,Liskov 基本上是想告诉我们以下内容:
子类应该能够与其超类互换使用,而不会破坏程序的功能。
那么这到底意味着什么呢?让我们考虑一个Vehicle有一个名为 的方法的类start_engine()。
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.")
根据里氏替换原则,的任何子类Vehicle也应该能够顺利启动引擎。
但是,如果我们添加一个Bicycle类,我们显然就无法启动引擎了,因为自行车没有引擎。下面演示了解决这个问题的错误方法。
class Bicycle(Vehicle):
def ride(self):
# Rides the bike
print("Riding the bike.")
def start_engine(self):
# Raises an error
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):
# Start the car engine
print("Car engine started.")
class Motorcycle(VehicleWithEngines):
def start_engine(self):
# Start the motorcycle engine
print("Motorcycle engine started.")
class Bicycle(VehicleWithoutEngines):
def ride(self):
# Rides the bike
print("Riding the bike.")
4. “I”代表接口隔离
一般定义指出,我们的模块不应该被迫担心它们不使用的功能。但这有点模棱两可。让我们将这句晦涩难懂的句子转换成一组更具体的指令:
客户端专用接口优于通用接口。这意味着类不应该被迫依赖于它们不使用的接口。相反,它们应该依赖于更小、更具体的接口。
假设我们有一个具有诸如、和等Animal方法的接口。walk()、swim()、fly()
class Animal:
def walk(self):
pass
def swim(self):
pass
def fly(self):
pass
问题是,并非所有动物都能完成所有这些动作。
例如:狗不会游泳或飞翔,因此从Animal接口继承的这两种方法都变得多余。
class Dog(Animal):
# Dogs can only walk
def walk(self):
print("Dog is walking.")
class Fish(Animal):
# Fishes can only swim
def swim(self):
print("Fish is swimming.")
class Bird(Animal):
# Birds cannot swim
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” 依赖倒置
这一点解释起来很简单,它指出:
高级模块不应该直接依赖于低级模块。相反,两者都应该依赖于抽象(接口或抽象类)。
再一次,我们来看一个例子。假设我们有一个ReportGenerator自然生成报告的类。要执行此操作,它需要首先从数据库获取数据。
class SQLDatabase:
def fetch_data(self):
# Fetch data from a SQL database
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()
# Generate report using the fetched data
print("Generating report...")
在这个例子中,ReportGenerator类直接依赖于具体SQLDatabase类。
目前这还不错,但如果我们想切换到不同的数据库(如 MongoDB)该怎么办?这种紧密耦合使得在不修改类的情况下更换数据库实现变得困难ReportGenerator。
SQLDatabase为了遵守依赖倒置原则,我们将引入和MongoDatabase类都可以依赖的抽象(或接口) 。
class Database():
def fetch_data(self):
pass
class SQLDatabase(Database):
def fetch_data(self):
# Fetch data from a SQL database
print("Fetching data from SQL database...")
class MongoDatabase(Database):
def fetch_data(self):
# Fetch data from a Mongo database
print("Fetching data from Mongo database...")
请注意,该类ReportGenerator现在还将Database通过其构造函数依赖于新的接口。
class ReportGenerator:
def __init__(self, database: Database):
self.database = database
def generate_report(self):
data = self.database.fetch_data()
# Generate report using the fetched data
print("Generating report...")
高级模块 ( ReportGenerator) 现在不再直接依赖于低级模块 (SQLDatabase或MongoDatabase)。相反,它们都依赖于接口 ( Database)。
依赖反转意味着我们的模块不需要知道它们得到了什么实现——只需要知道它们将接收某些输入并返回某些输出。
结论
如今,我看到网上有很多关于 SOLID 设计原则的讨论,以及它们是否经受住了时间的考验。在这个多范式编程、云计算和机器学习的现代世界中…… SOLID 是否仍然有意义?
我个人认为 SOLID 原则永远是良好代码设计的基础。有时,在处理小型应用程序时,这些原则的好处可能并不明显,但一旦你开始处理更大规模的项目,代码质量的差异就值得你去学习它们。SOLID 所倡导的模块化仍然使这些原则成为现代软件架构的基础,我个人认为这种情况不会很快改变。
本文译自The SOLID Principles: Writing Scalable & Maintainable Code
参考文档
https://forreya.medium.com/the-solid-principles-writing-scalable-maintainable-code-13040ada3bca