SOLIDPrinciple基础入门
- 其他
- 2025-09-21 21:18:01

(Robert C. Martin (Uncle Bob))
什么是SOLID原则?SOLID原则是面向对象编程(OOP)中编写高质量代码的指导方针。实际上,即使不使用SOLID原则,仅通过类、继承、封装和多态性,也可以让程序正常运行。那么为什么会出现SOLID原则呢?
SOLID原则是为了提高代码的可维护性 、可扩展性 以及管理耦合度 而设计的一种指导方针。换句话说,SOLID原则是编写高质量OOP代码的指南。
Agile Software Development: Principles, Patterns, and Practices (2002)
SOLID原则的起源SOLID原则最早出现在罗伯特·C·马丁(Robert C. Martin,也被称为Uncle Bob)于2002年出版的《敏捷软件开发:原则、模式与实践》一书中。虽然每个具体的原则在此之前已经存在,但罗伯特·C·马丁将它们整合在一起,并由迈克尔·C·费瑟斯(Michael C. Feathers)建议将其命名为“SOLID”,形成了我们今天熟知的形式。
罗伯特·C·马丁是敏捷开发和清洁代码领域的传奇人物,他对全球程序员社区产生了深远的影响。
在了解这些原则之前,我们需要先了解 敏捷(Agile)的概念。只有理解了敏捷,才能明白这本书为何如此具有革命性,甚至被视为程序员的“圣经”。
敏捷宣言与SOLID的关系2001年,程序员们发表了《敏捷宣言》。这份宣言强调了编程中的哲学价值,但并没有提供具体的执行方法。罗伯特·C·马丁在《敏捷软件开发:原则、模式与实践》中提供了具体的实施方案,将敏捷的“哲学”转化为“方法论”。这一成果帮助敏捷从理论走向了实际标准。
在初版中,SRP、OCP、LSP、ISP、DIP是分别介绍的,之后在这本书出版后,根据迈克尔·C·费瑟斯(Michael C. Feathers)的建议,这些原则被命名为“SOLID”。
(具体时间线推测大约在2004年左右正式命名为“SOLID”)
这些原则被描述为敏捷设计的方法论。
敏捷原则并不仅仅是快速编码,而是以具备长期可维护性和适应变化能力的设计为目标进行说明的。
因此,遵循SOLID原则对内化敏捷设计方法论有很大的帮助。
之后,罗伯特·马丁(Uncle Bob)根据迈克尔·C·费瑟斯的建议,在博客和演讲中将其命名为“SOLID”,形成了我们熟知的“SOLID原则”。
(“SOLID”有固体、坚固的意思,所以这里也包含了一种文字游戏的趣味)
第七章总结通过代码中的设计异味(Smells) ,如僵化性(Rigidity)、脆弱性(Fragility)、不流动性(Immobility)、粘滞性(Viscosity)等问题识别问题,并通过渐进式的改进逐步完善设计。这种改进并非一开始就追求完美,而是通过反复重构来优化,而SOLID原则正是这一过程中的实际方法论。
书中提到的“有异味的代码”的特征如下: 僵化性(Rigidity)定义
软件难以修改。也就是说,代码变得僵硬,当试图修改某一部分时,修改会对系统的其他多个部分产生影响,导致需要比预期更多的工作量。特点
代码高度耦合(Tightly Coupled),小的改动会引发连锁修改需求。难以根据需求变化进行调整。示例
修改一个类的方法时,发现需要同时修改调用它的数十个其他类。更改数据库模式时,需要对UI、业务逻辑、数据访问层进行全面修改。问题原因 :高耦合度(High Coupling)和低内聚性(Low Cohesion)。
解决方法 :通过SOLID中的SRP(单一职责原则)和DIP(依赖倒置原则)降低模块间的耦合度,减少修改对其他部分的影响。
脆弱性(Fragility)定义
软件容易崩溃或出现错误的状态。修改某一部分时,意想不到的其他部分出现问题,或者系统变得不稳定。特点
修改代码后频繁出现Bug。在与修改部分无直接关联的区域出现错误。示例
修改支付模块后,登录系统突然无法正常工作。改进某个方法的逻辑后,使用该方法的其他模块发生运行时错误。问题原因 :不恰当的继承使用(例如违反LSP)或依赖管理失败。
解决方法 :遵守LSP(里氏替换原则)确保继承结构的稳定性,并通过ISP(接口隔离原则)移除不必要的依赖。
不流动性(Immobility)定义
软件组件难以复用或移动的状态。当尝试将特定模块或代码用于其他项目或上下文时,由于过度依赖,难以分离或无法复用。特点
代码过于紧密地绑定到特定环境。如果要复用,需要进行大量修改。示例
数据库查询逻辑与UI代码纠缠在一起,导致无法在其他项目中单独复用查询逻辑。依赖特定硬件的代码无法在其他平台上运行。问题原因 :模块间高耦合度和缺乏抽象。
解决方法 :通过DIP(依赖倒置原则)设计为依赖抽象而非具体实现,并通过OCP(开闭原则)创建可复用的结构。
粘滞性(Viscosity)定义
软件开发环境或代码使任务变得困难且缓慢的状态。粘滞性高意味着“正确的方式”比“错误的方式”更难操作。特点
主要分为两种形式: 软件粘滞性(Viscosity of the Software) :代码本身难以维护或难以应用良好设计的情况。环境粘滞性(Viscosity of the Environment) :构建、测试、部署等开发环境效率低下,导致工作速度变慢。示例
复制粘贴添加代码比重构更容易(软件粘滞性)。构建时间过长,导致代码修改后验证变慢(环境粘滞性)。问题原因 :未遵守设计原则,开发流程复杂。
解决方法 :通过SRP保持代码简洁性,并通过持续重构和自动化测试/构建环境降低粘滞性。
不过,仅靠SOLID原则并不能消除所有设计异味。
它只是一个高质量的指导方针,这些代码异味还取决于个人的架构哲学、主观判断以及实际需求。
例如,典型的“霰弹枪手术(Shotgun Surgery)”问题仅靠SOLID原则很难解决。
“霰弹枪手术”的原因通常是责任分配过多。
“霰弹枪手术”:这是一种反模式(Anti-Pattern),虽然属于错误代码的案例,但经常出现。通常,“霰弹枪手术”是因为责任分散过多而导致的问题。
SOLID与敏捷(Agile)如何关联? SRP 减少代码的僵化性(Rigidity)。OCP 在不修改现有代码的情况下扩展功能,从而降低代码间的粘滞性(Viscosity)。LSP 保证继承结构的安全性,从而减少脆弱性(Fragility)。DIP 和 ISP 降低耦合度,从而减少不流动性(Immobility)。(img ref: .instagram /techwithisha/reel/C1Ws1ZDt8_j/)
SOLID原则?那么,我们已经了解了上述问题的原因,现在让我们来详细探讨一下SOLID原则。
我对所有智能思维特征的理解这指的是深入研究某一主题特定方面的态度。
这种研究不仅是为了保持该方面的一致性,同时也意识到自己所处理的内容只是整体的一部分。
我们知道程序必须正确运行,
因此我们可以从这个角度研究程序。
此外,我们也知道程序必须高效,
这一分析可以在其他时间单独进行。
有时,我们会思考程序是否真的必要,如果是,为什么。
然而,同时处理这些不同方面并不会带来任何好处,反而只会造成干扰。
我称之为“关注点分离(Separation of Concerns)”,
虽然无法完全实现,但这是有效整理思路的唯一方法。
当我说“专注于某一方面”时,
这并不意味着忽略其他方面。
相反,这意味着从某个特定方面的角度来看,其他方面暂时并不重要。
也就是说,这是一种既能专注于一件事,又能同时考虑多件事的思维方式。
—Edsger Wybe Dijkstra(On the role of scientific thought, 1982)
1. 单一职责原则(SRP, Single Responsibility Principle)提出者 : 罗伯特·C·马丁(Robert C. Martin)
定义
“一个类应该只有一个职责。” 也就是说,类应该只有一个修改的理由。意义
如果一个类承担多个角色,一个角色的变化会影响其他角色,导致僵化性和脆弱性增加。分离职责可以使代码更简单,维护更容易。示例代码
using System; using System.Data.SqlClient; public class Employee_BadExample // 通过类名标明 BadExample { public string Name { get; set; } public double BaseSalary { get; set; } public string Department { get; set; } private SqlConnection _dbConnection; // Employee 类竟然还负责数据库连接! public Employee_BadExample(string name, double baseSalary, string department, SqlConnection dbConnection) { Name = name; BaseSalary = baseSalary; Department = department; _dbConnection = dbConnection; // Employee 类接受数据库连接对象 } public double CalculateSalary() { """计算薪资的方法 (假设不同部门的奖金比例不同)""" double bonusRate = 0; if (Department == "Sales") { bonusRate = 0.1; } else if (Department == "Marketing") { bonusRate = 0.05; } return BaseSalary * (1 + bonusRate); } public void SaveToDatabase() { """将员工信息存储到数据库的方法""" double salary = CalculateSalary(); // 计算薪资逻辑竟然也在 Employee 内! try { _dbConnection.Open(); SqlCommand command = new SqlCommand("INSERT INTO Employees (Name, Salary, Department) VALUES (@Name, @Salary, @Department)", _dbConnection); command.Parameters.AddWithValue("@Name", Name); command.Parameters.AddWithValue("@Salary", salary); command.Parameters.AddWithValue("@Department", Department); command.ExecuteNonQuery(); } catch (Exception ex) { Console.WriteLine("数据库存储错误: " + ex.Message); } finally { _dbConnection.Close(); } } } public class BadExample_Program // 使用 BadExample 的主程序类 { public static void Main(string[] args) { // 错误的示例:Employee_BadExample 类承担了太多职责! SqlConnection dbConn = null; // 需替换为实际数据库连接对象 (此处用 null 代替) Employee_BadExample employee = new Employee_BadExample("홍길동", 3000000, "Sales", dbConn); employee.SaveToDatabase(); // Employee_BadExample 既计算薪资,又保存数据库! } }错误示例 : Employee类同时处理薪资计算和数据库存储,薪资逻辑变化会影响数据库代码。
using System; using System.Data.SqlClient; public class Employee // Employee 类仅负责数据 { public string Name { get; set; } public double BaseSalary { get; set; } public string Department { get; set; } public Employee(string name, double baseSalary, string department) { Name = name; BaseSalary = baseSalary; Department = department; } } public class SalaryCalculator // 负责薪资计算的类 { public double CalculateSalary(Employee employee) // 接收 Employee 对象作为参数 { """计算薪资的方法""" double bonusRate = 0; if (employee.Department == "Sales") { bonusRate = 0.1; } else if (employee.Department == "Marketing") { bonusRate = 0.05; } return employee.BaseSalary * (1 + bonusRate); } } public class EmployeeRepository // 负责数据库存储的类 { private SqlConnection _dbConnection; public EmployeeRepository(SqlConnection dbConnection) { _dbConnection = dbConnection; } public void Save(Employee employee, double salary) // 接收 Employee 对象和计算后的薪资 { """将员工信息存储到数据库的方法""" try { _dbConnection.Open(); SqlCommand command = new SqlCommand("INSERT INTO Employees (Name, Salary, Department) VALUES (@Name, @Salary, @Department)", _dbConnection); command.Parameters.AddWithValue("@Name", employee.Name); command.Parameters.AddWithValue("@Salary", salary); command.Parameters.AddWithValue("@Department", employee.Department); command.ExecuteNonQuery(); } catch (Exception ex) { Console.WriteLine("数据库存储错误: " + ex.Message); } finally { _dbConnection.Close(); } } } public class GoodExample_Program // 使用 GoodExample 的主程序类 { public static void Main(string[] args) { // 正确的示例:每个类只承担单一职责! SqlConnection dbConn = null; // 需替换为实际数据库连接对象 Employee employee = new Employee("김철수", 3500000, "Marketing"); SalaryCalculator calculator = new SalaryCalculator(); // 创建负责薪资计算的对象 double salary = calculator.CalculateSalary(employee); EmployeeRepository repository = new EmployeeRepository(dbConn); // 创建负责数据库存储的对象 repository.Save(employee, salary); } }正确示例 : 将其分为SalaryCalculator和EmployeeRepository。
优点 : 提高代码的内聚性(Cohesion),降低耦合度(Coupling)。 相关设计异味 : 僵化性、粘滞性缓解。
(Clean Coder Blog)
历史背景 : 根据罗伯特·C·马丁博客所述,SRP起源于大卫·L·帕纳斯的模块分解和戴克斯特拉的关注点分离概念。 结合当时编程社区中流行的耦合与内聚概念,最终形成了SRP。
正如罗伯特·C·马丁在博客中所说,SRP是关于人的。 现实中,软件会随着企业或组织的需求而变化,因此每个模块只负责单一业务功能,以便于明确哪个团队负责修改该功能。
2. 开闭原则(OCP, Open/Closed Principle)提出者 : 贝特朗·迈耶(Bertrand Meyer)
定义
“软件实体(类、模块等)应对扩展开放,对修改关闭。”意义
在不修改现有代码的情况下添加新功能。使用抽象(接口、抽象类)和多态性来实现。示例
using System; public class PaymentProcessor_BadExample // 通过类名标明 BadExample { public void ProcessPayment(string paymentMethod, double amount) { """根据支付方式处理支付的方法 (大量使用 if-else 语句)""" if (paymentMethod == "Card") { // 处理信用卡支付逻辑 Console.WriteLine($"使用信用卡支付 {amount} 元"); } else if (paymentMethod == "Cash") { // 处理现金支付逻辑 Console.WriteLine($"使用现金支付 {amount} 元"); } else if (paymentMethod == "MobilePay") // 新增支付方式!必须修改代码! { // 处理移动支付逻辑 Console.WriteLine($"使用移动支付 {amount} 元"); } else { Console.WriteLine("不支持的支付方式"); } } } public class BadExample_Program // 使用 BadExample 的主程序类 { public static void Main(string[] args) { // 错误的示例: PaymentProcessor_BadExample 违反开放-封闭原则 (OCP),无法轻易扩展! PaymentProcessor_BadExample processor = new PaymentProcessor_BadExample(); processor.ProcessPayment("Card", 10000); processor.ProcessPayment("Cash", 5000); processor.ProcessPayment("MobilePay", 7000); // 使用新的支付方式 } }错误示例 : 每次向PaymentProcessor类添加新的支付方式(如卡支付、现金支付)时,都需要修改if-else条件。
using System; // IPayment 接口: 适用于各种支付方式的通用接口 public interface IPayment { void ProcessPayment(double amount); } // 信用卡支付类 (实现 IPayment) public class CardPayment : IPayment { public void ProcessPayment(double amount) { Console.WriteLine($"[信用卡支付] 付款 {amount:N0} 元 完成"); } } // 现金支付类 (实现 IPayment) public class CashPayment : IPayment { public void ProcessPayment(double amount) { Console.WriteLine($"[现金支付] 付款 {amount:N0} 元 完成"); } } // 移动支付类 (实现 IPayment) public class MobilePayPayment : IPayment { public void ProcessPayment(double amount) { Console.WriteLine($"[移动支付] 付款 {amount:N0} 元 完成"); } } // 支付处理类: 符合开放-封闭原则 (OCP) public class PaymentProcessor { private readonly IPayment _paymentMethod; // 通过构造函数注入支付方式 (可应用依赖注入 DI) public PaymentProcessor(IPayment paymentMethod) { _paymentMethod = paymentMethod ?? throw new ArgumentNullException(nameof(paymentMethod)); } public void Process(double amount) { _paymentMethod.ProcessPayment(amount); } } public class Program { public static void Main() { // 创建各种支付方式对象 var cardPayment = new CardPayment(); var cashPayment = new CashPayment(); var mobilePayPayment = new MobilePayPayment(); // 符合 OCP: 新增支付方式时,无需修改 PaymentProcessor 代码 var processor1 = new PaymentProcessor(cardPayment); processor1.Process(10000); var processor2 = new PaymentProcessor(cashPayment); processor2.Process(5000); var processor3 = new PaymentProcessor(mobilePayPayment); processor3.Process(7000); } }正确示例 : 创建IPayment接口,并通过CardPayment和CashPayment类进行扩展。
优点 : 维持现有代码的稳定性,灵活应对新需求。 相关设计异味 : 僵化性、不流动性缓解。
出处 : 出自《面向对象软件构造》(Object-Oriented Software Construction, 1988),第2章“模块化”部分。
(Data Abstraction and Hierarchy) (1987, OOPSLA )
3. 里氏替换原则(LSP, Liskov Substitution Principle)提出者 : 芭芭拉·利斯科夫(Barbara Liskov)
定义
“子类应能在不干扰父类行为的情况下替代父类。”也就是说,在程序中用子类型替换父类型时,程序仍能正常运行。意义
在继承关系中,子类不应违反父类的契约(Contract)。这是安全使用多态性的原则。示例
public class Bird { public virtual void Fly() => Console.WriteLine("鸟在飞翔。"); } public class Penguin : Bird { public override void Fly() // 企鹅不能飞! { throw new NotImplementedException("企鹅无法飞行。"); } } public class Program { public static void MakeBirdFly(Bird bird) { bird.Fly(); // 如果传入的是 Penguin 对象,则会抛出异常! } static void Main() { Bird myBird = new Penguin(); MakeBirdFly(myBird); // 可能导致程序崩溃 } }错误示例 : Bird类有Fly()方法,而Penguin子类忽略或抛出异常。
// 将 Bird 抽象化,不允许直接使用 public abstract class Bird { } // 定义飞行接口 public interface IFlyable { void Fly(); } // 麻雀类 (实现 IFlyable 接口) public class Sparrow : Bird, IFlyable { public void Fly() => Console.WriteLine("麻雀在飞翔。"); } // 企鹅类 (不实现 IFlyable 接口,表示不能飞) public class Penguin : Bird { } public class Program { public static void MakeBirdFly(IFlyable bird) { bird.Fly(); } static void Main() { IFlyable sparrow = new Sparrow(); MakeBirdFly(sparrow); // 正常运行 } }正确示例 : 将Bird分为FlyingBird和WalkingBird,使Penguin不需要实现Fly()。
优点 : 确保继承结构的稳定性和可预测性。 相关设计异味 : 脆弱性缓解。
背景 : 该原则源自1987年OOPSLA会议论文,论文讨论了数据抽象和层次结构,为面向对象中的“继承(Inheritance)”提供了哲学和实用的指导方针。
数据抽象是指隐藏程序中数据的内部实现,仅通过接口访问。
归根结底,这是一个如何更好地抽象现实问题的问题。例如,如果将哺乳动物定义为“有腿的生物”,那么鲸鱼就难以被称为哺乳动物。因此,如何恰当地进行抽象才是关键。
4. 接口隔离原则(ISP, Interface Segregation Principle)提出者 : 罗伯特·C·马丁(Robert C. Martin)
定义
“客户端不应依赖于它不需要的接口。”也就是说,接口应尽可能小且具体。意义
设计只提供客户端所需功能的接口,而不是大型通用接口。移除不必要的依赖以降低耦合度。示例
using System; // IWorker_BadExample 接口: 包含了太多功能 (违反 ISP) public interface IWorker_BadExample { void Work(); // 工作功能 void Eat(); // 进食功能 - 但对 Robot 来说是不必要的! } // Robot_BadExample 类: 实现 IWorker_BadExample,必须强制实现不必要的 Eat() 方法 public class Robot_BadExample : IWorker_BadExample { public void Work() { Console.WriteLine("机器人正在努力工作。"); } public void Eat() // 机器人不需要进食,但仍然必须实现 { // 机器人不吃饭,因此只能什么都不做,或者抛出异常 Console.WriteLine("机器人无法进食。"); // 或者 throw new NotImplementedException(); } } // HumanWorker_BadExample 类: 实现 IWorker_BadExample,正确地实现 Work() 和 Eat() public class HumanWorker_BadExample : IWorker_BadExample { public void Work() { Console.WriteLine("人类正在努力工作。"); } public void Eat() { Console.WriteLine("人类正在吃午饭。"); } } public class BadExample_Program // 使用 BadExample 的程序类 { public static void Main(string[] args) { // 错误示例: Robot_BadExample 不应该有 Eat() 方法! IWorker_BadExample robot = new Robot_BadExample(); robot.Work(); robot.Eat(); // 机器人调用 Eat() 方法显得很奇怪 IWorker_BadExample human = new HumanWorker_BadExample(); human.Work(); human.Eat(); } }错误示例 : IWorker接口包含Work()和Eat()方法,导致Robot类需要实现不必要的Eat()方法。
using System; // IWorkable 接口: 仅包含工作功能 (遵循 ISP) public interface IWorkable { void Work(); // 工作功能 } // IEatable 接口: 仅包含进食功能 (遵循 ISP) public interface IEatable { void Eat(); // 进食功能 } // Robot_GoodExample 类: 仅实现 IWorkable 接口 (仅实现必要的功能) public class Robot_GoodExample : IWorkable // 机器人只能工作 { public void Work() { Console.WriteLine("机器人高效地执行任务。"); } // Eat() 方法未实现: 机器人不需要进食 } // HumanWorker_GoodExample 类: 实现 IWorkable 和 IEatable 接口 (拥有所有必要功能) public class HumanWorker_GoodExample : IWorkable, IEatable // 人类既能工作,也能进食 { public void Work() { Console.WriteLine("人类创造性地工作。"); } public void Eat() { Console.WriteLine("人类正在享受美味的午餐。"); } } public class GoodExample_Program // 使用 GoodExample 的程序类 { public static void Main(string[] args) { // 正确示例: Robot_GoodExample 只需要实现 IWorkable! IWorkable robot = new Robot_GoodExample(); // 机器人仅用作 IWorkable 类型 robot.Work(); // robot.Eat(); // Robot 未实现 IEatable,因此无法调用 Eat() 方法 (编译错误) IWorkable humanWorker = new HumanWorker_GoodExample(); // HumanWorker 可用作 IWorkable 类型 humanWorker.Work(); IEatable humanEater = new HumanWorker_GoodExample(); // HumanWorker 也可用作 IEatable 类型 humanEater.Eat(); } }正确示例 : 将接口拆分为IWorkable和IEatable,使Robot只需实现IWorkable。
优点 : 提高代码的灵活性和可重用性。 相关设计异味 : 脆弱性、粘滞性缓解。
出处 : 出自罗伯特·C·马丁1996年的文章。
(1996 Robert C. Martin Essay)
5. 依赖倒置原则(DIP, Dependency Inversion Principle)提出者 : 罗伯特·C·马丁(Robert C. Martin)
定义
“高层模块不应依赖于低层模块,二者都应依赖于抽象。”此外,“不要依赖具体实现,而是依赖抽象。”意义
通过接口或抽象类减少模块间的依赖。通过依赖注入(Dependency Injection)实现。示例
using System; // SqlDatabase 类: 具体数据库的实现 (低级模块,直接依赖于某个数据库) public class SqlDatabase_BadExample { public void Save(string data) { // 实际将数据存入 SqlDatabase 的逻辑 (省略实现) Console.WriteLine($"数据已存入 SqlDatabase: {data}"); } } // OrderService_BadExample 类: 直接依赖 SqlDatabase (违反 DIP,属于高级模块) public class OrderService_BadExample { private SqlDatabase_BadExample _database; // 直接依赖于具体的 SqlDatabase 类! public OrderService_BadExample() { _database = new SqlDatabase_BadExample(); // OrderService 直接创建 SqlDatabase 实例 } public void PlaceOrder(string orderData) { // 订单处理逻辑 (这里只是简单地存储数据) Console.WriteLine($"订单处理中: {orderData}"); _database.Save(orderData); // OrderService 直接调用 SqlDatabase 的 Save() 方法 Console.WriteLine("订单处理完成"); } } public class BadExample_Program // 使用 BadExample 的程序类 { public static void Main(string[] args) { // 错误示例: OrderService_BadExample 与 SqlDatabase 强耦合,难以扩展! OrderService_BadExample service = new OrderService_BadExample(); service.PlaceOrder("客户: 张三, 商品: 笔记本电脑"); } }错误示例 : OrderService直接依赖于SqlDatabase,更换数据库时需要修改代码。
using System; // 遵循 DIP (依赖倒置原则): OrderService 仅依赖 IDatabase 接口 public interface IDatabase { void SaveOrder(string orderDetails); } // SqlDatabase 实现 IDatabase 接口 public class SqlDatabase : IDatabase { public void SaveOrder(string orderDetails) { Console.WriteLine($"[SqlDatabase] 订单已保存: {orderDetails}"); } } // MongoDatabase 实现 IDatabase 接口 (可以添加新的数据库类型) public class MongoDatabase : IDatabase { public void SaveOrder(string orderDetails) { Console.WriteLine($"[MongoDatabase] 订单已保存: {orderDetails}"); } } // OrderService 依赖于接口 (IDatabase),不依赖具体实现 public class OrderService { private readonly IDatabase _database; // 依赖倒置原则 (DIP): OrderService 依赖接口,而不是具体实现 public OrderService(IDatabase database) { _database = database; // 依赖注入 (Dependency Injection) } public void PlaceOrder(string orderDetails) { _database.SaveOrder(orderDetails); // 通过接口存储订单 } } // OrderService 不再依赖特定数据库 → 可以轻松切换数据库 public class Program { public static void Main() { // 在不使用 DI 容器的情况下,直接创建对象并注入依赖 IDatabase sqlDatabase = new SqlDatabase(); IDatabase mongoDatabase = new MongoDatabase(); // 使用 SqlDatabase 的 OrderService OrderService orderService1 = new OrderService(sqlDatabase); orderService1.PlaceOrder("商品 A 订单"); // 使用 MongoDatabase 的 OrderService OrderService orderService2 = new OrderService(mongoDatabase); orderService2.PlaceOrder("商品 B 订单"); } }正确示例 : 创建IDatabase接口,OrderService依赖于接口,SqlDatabase作为具体实现。
优点 : 提高系统的灵活性和测试便利性。 相关设计异味 : 僵化性、不流动性缓解。
SOLID原则的整体意义提高可维护性和扩展性,从而构建能够灵活应对变化的软件。
原则
定义
提出者
S - 单一职责原则 (SRP)
一个类应该只有一个职责。
罗伯特·C·马丁 (2002)
O - 开闭原则 (OCP)
在不修改现有代码的情况下扩展功能。
贝特朗·迈耶 (1988)
L - 里氏替换原则 (LSP)
子类应能替代父类。
芭芭拉·利斯科夫 (1987)
I - 接口隔离原则 (ISP)
客户端不应依赖于它不需要的接口。
罗伯特·C·马丁 (2002)
D - 依赖倒置原则 (DIP)
高层模块不应依赖于低层模块,而是依赖于抽象。
罗伯特·C·马丁 (1996)
SOLID原则真的是绝对正确的答案吗?当然不是。SOLID只是一个指导方针。
示例:虚幻引擎中的Actor虚幻引擎的Actor负责物理、碰撞、光照、网格渲染等大量任务。
也就是说,单个类承担了太多的任务。那么,如果将这些任务分开,真的会变得方便吗?完全不会。相反,分离的成本可能远远高于收益。
Actor是虚幻引擎的核心基础类。如果为了遵循SRP(单一职责原则)而将其拆分,会对整个引擎产生影响。
那么,这是不是一个糟糕的设计呢?并不是。因为游戏是以对象为中心设计的,在这个单位下,这种设计是有充分理由的。
如果强行拆分,反而会导致在对象级别重新组合时需要付出巨大的成本。
LSP的情况同样地,LSP(里氏替换原则)也有例外。例如,在GUI框架中,当Button继承自Widget时,是否必须保证父类的按钮行为?
如果是这样,反而会导致实际问题。在这种情况下,如果需要多样化地设计按钮样式,可能会破坏设计的创造性。
因此,许多GUI框架(如C++的QT框架和GTK等)允许这样的例外。
此外,由于接口带来的开销,有时选择DOP(数据导向编程)而非OOP可能是更合适的。
虽然SOLID是面向对象编程(OOP)设计的一个很好的指导方针,但也需要了解OOP可能存在的性能限制和设计背景,并明确何时可以不遵守这些原则。
那么,什么时候可以违背SOLID原则? SRP(单一职责原则) :如果功能具有很强的内聚性,严格遵守SRP可能导致类之间频繁调用方法,从而引发性能开销。OCP(开闭原则) :虽然扩展频繁是好事,但如果性能优先,修改可能比扩展更好。LSP(里氏替换原则) :如果继承结构简单,则不需要强制遵守。ISP(接口隔离原则) :如果接口过多,反而会导致混乱。DIP(依赖倒置原则) :如果抽象化带来了额外开销,具体依赖可能是更好的选择。“假设维护你代码的人是一个知道你住址的暴力精神病患者,那么你就应该以这种方式编写代码。”-John F. Woods(1991年)
SOLID的本质与实用性平衡SOLID原则诞生于敏捷哲学,作为提升代码可维护性和扩展性的强大工具,已经占据了重要地位。
然而,它并不是适用于所有情况的“银弹”。
对于像虚幻引擎的Actor这样具有强烈领域特性的场景,或者在GUI框架中需要发挥创造力时,又或者在性能至关重要的系统中,与其勉强遵循SOLID原则,不如选择适合上下文的设计更为重要。
正如John F. Woods所说,“假设维护你代码的人是一个知道你住址的暴力精神病患者”,这不仅仅是在提醒我们写易读的代码。
其背后的真正含义是,无论在什么情况下,都要让处理代码的人能够轻松理解并适应代码。
SOLID只是实现这一目标的一种方式,而不是唯一的方式。有时,违反SRP保持一个整合的类,或者忽略DIP选择具体的依赖,可能是防止“精神病患者”采取极端行动的实用选择。
SOLIDPrinciple基础入门由讯客互联其他栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“SOLIDPrinciple基础入门”