关注TechLead,复旦博士,分享云服务领域全维度开发技术。拥有10+年互联网服务架构、AI产品研发经验、团队管理经验,复旦机器人智能实验室成员,国家级大学生赛事评审专家,发表多篇SCI核心期刊学术论文,阿里云认证的资深架构师,上亿营收AI产品研发负责人。
有人曾告诉你,你写过“糟糕的代码”吗?
如果有,不必感到羞愧。我们在学习的过程中都会写出有缺陷的代码。不过好消息是,只要你愿意,改善代码其实很简单。
提升代码质量的最佳方式之一就是学习一些编程设计原则。可以把编程原则视为成为更优秀程序员的通用指南——代码的原始哲学。如今有各种各样的设计原则(有人可能会认为甚至太多了),但我将介绍五个核心原则,它们组成了一个首字母缩略词:SOLID。
注意: 我将在示例中使用Python,但这些概念可以轻松转移到其他语言,如Java。
1. 首先是SOLID中的’S’——单一职责原则
这个原则告诉我们:
将代码拆分为每个模块只负责一个职责。
让我们来看这个执行不相关任务的Person
类,它既发送电子邮件又计算税款。
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def send_email(self, message):
print(f"发送邮件给 {self.name}: {message}")
def calculate_tax(self):
tax = self.age * 100
print(f"{self.name} 的税款: {tax}")
根据单一职责原则,我们应该将Person
类拆分为多个小类,以避免违反该原则。
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
class EmailSender:
def send_email(self, person, message):
print(f"发送邮件给 {person.name}: {message}")
class TaxCalculator:
def calculate_tax(self, person):
tax = person.age * 100
print(f"{person.name} 的税款: {tax}")
虽然代码行数增多了,但我们现在可以更清楚地识别代码的每个部分所要完成的任务,测试时也更干净,同时可以在其他地方复用这些部分,而无需担心不相关的方法。
2. 接下来是’O’——开闭原则
这个原则建议我们设计的模块应当:
能够在未来添加新功能,而不直接修改现有代码。
一旦某个模块被使用,它就基本上是“锁定的”,这减少了新添加的功能破坏代码的风险。
这五个原则中,开闭原则可能是最难完全掌握的,因为它的性质带有一定的矛盾性。让我们通过一个例子来解释:
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":
return self.width * self.height
elif self.shape_type == "triangle":
return self.width * self.height / 2
在上面的例子中,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):
return self.width * self.height
class Triangle(Shape):
def calculate_area(self):
return self.width * self.height / 2
在上面的例子中,我们定义了一个Shape
基类,其唯一目的是允许更具体的形状类继承它的属性。例如,Triangle
类扩展了calculate_area()
方法来计算并返回三角形的面积。
通过遵循开闭原则,我们可以在不修改现有Shape
类的情况下添加新形状,从而扩展代码的功能,而无需改变其核心实现。
3. 现在是’L’——里氏替换原则 (LSP)
里氏替换原则告诉我们:
子类应该能够替换它们的超类,而不会破坏程序的功能。
这意味着什么呢?让我们来看一个Vehicle
类及其start_engine()
方法。
class Vehicle:
def start_engine(self):
pass
class Car(Vehicle):
def start_engine(self):
print("汽车发动机启动。")
class Motorcycle(Vehicle):
def start_engine(self):
print("摩托车发动机启动。")
根据里氏替换原则,Vehicle
的任何子类都应该能够启动引擎而不会出现问题。
但如果我们添加了一个Bicycle
类,很显然自行车没有引擎。因此,以下是不正确的解决方案示例:
class Bicycle(Vehicle):
def start_engine(self):
raise NotImplementedError("自行车没有引擎。")
为了正确遵守里氏替换原则,我们可以采取两个解决方案。让我们先看第一个。
解决方案1: Bicycle
成为它自己的独立类(没有继承),确保所有Vehicle
子类与超类行为一致。
class Vehicle:
def start_engine(self):
pass
class Car(Vehicle):
def start_engine(self):
print("汽车发动机启动。")
class Motorcycle(Vehicle):
def start_engine(self):
print("摩托车发动机启动。")
class Bicycle:
def ride(self):
print("骑自行车。")
解决方案2: 将Vehicle
超类拆分为两类,一类是有引擎的车辆,另一类是没有引擎的。这样所有子类都能与其超类保持一致,而不需要改变预期行为或引入例外。
class VehicleWithEngines:
def start_engine(self):
pass
class VehicleWithoutEngines:
def ride(self):
pass
class Car(VehicleWithEngines):
def start_engine(self):
print("汽车发动机启动。")
class Motorcycle(VehicleWithEngines):
def start_engine(self):
print("摩托车发动机启动。")
class Bicycle(VehicleWithoutEngines):
def ride(self):
print("骑自行车。")
4. 接下来是’I’——接口隔离原则
这个原则的定义有些模糊,但可以归纳为:
客户端特定的接口优于通用接口。
换句话说,类不应该被迫依赖它们不使用的接口。相反,它们应依赖于更小、更具体的接口。
假设我们有一个Animal
接口,包含walk()
、swim()
和fly()
方法。
class Animal:
def walk(self):
pass
def swim(self):
pass
def fly(self):
pass
问题是,并不是所有动物都能执行这些动作。例如,狗不能游泳或飞行,因此这两个从Animal
接口继承的方法对狗来说是多余的。
class Dog(Animal):
def walk(self):
print("狗在走路。")
class Fish(Animal):
def swim(self):
print("鱼在游泳。")
class Bird(Animal):
def walk(self):
print("鸟在走路。")
def fly(self):
print("鸟在飞行。")
我们需要将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("狗在走路。")
class Fish(Swimmable):
def swim(self):
print("鱼在游泳。")
class Bird(Walkable, Flyable):
def walk(self):
print("鸟在走路。")
def fly(self):
print("鸟在飞行。")
通过这种方式,我们实现了一个设计,使类仅依赖它们所需的接口,从而减少不必要的依赖。这在测试时尤为有用,因为它允许我们仅模拟每个模块所需的功能。
5. 最后是’D’——依赖倒置原则
这个原则非常简单明了:
高层模块不应直接依赖低层模块。相反,二者都应依赖于抽象(接口或抽象类)。
让我们来看一个例子。假设我们有一个ReportGenerator
类,用于生成报告。要执行此操作,它需要先从数据库中获取数据。
class SQLDatabase:
def fetch_data(self):
print("从SQL数据库获取数据...")
class ReportGenerator:
def __init__(self, database: SQLDatabase):
self.database = database
def generate_report(self):
data = self.database.fetch_data()
print("生成报告...")
在这个例子中,ReportGenerator
类直接依赖于具体的SQLDatabase
类。
这在目前运行良好,但如果我们想切换到不同的数据库(例如MongoDB),这种紧密耦合会使得难以在不修改ReportGenerator
类的情况下更换数据库实现。
为了遵守依赖倒置原则,我们引入一个抽象(或接口),让SQLDatabase
和MongoDatabase
都依赖于它。
class Database:
def fetch_data(self):
pass
class SQLDatabase(Database):
def fetch_data(self):
print("从SQL数据库获取数据...")
class MongoDatabase(Database):
def fetch_data(self):
print("从Mongo数据库获取数据...")
注意,ReportGenerator
类现在依赖于新的Database
接口。
class ReportGenerator:
def __init__(self, database: Database):
self.database = database
def generate_report(self):
data = self.database.fetch_data()
print("生成报告...")
高层模块(ReportGenerator
)现在不再直接依赖低层模块(SQLDatabase
或MongoDatabase
)。相反,它们都依赖于接口(Database
)。
依赖倒置意味着我们的模块不需要知道它们接收到的是哪种具体实现——只需确保它们接收到某些输入并返回某些输出即可。
结论
现在,网上有很多关于SOLID设计原则的讨论,探讨它们是否经受住了时间的考验。在这个多范式编程、云计算和机器学习的现代世界里,SOLID是否仍然适用?
其实,SOLID原则将永远是良好代码设计的基础。当处理小型应用时,这些原则的好处可能不那么明显,但一旦开始参与大规模项目,代码质量上的差异是非常值得学习这些原则的。SOLID所倡导的模块化设计仍然使这些原则成为现代软件架构的基石,我认为这种情况在短时间内不会改变。