网站单页在线,品牌网站设计打造,贵州建设工程招标协会网站,如何给一个网站做压测(Robert C. Martin (Uncle Bob))
什么是SOLID原则#xff1f;
SOLID原则是面向对象编程#xff08;OOP#xff09;中编写高质量代码的指导方针。实际上#xff0c;即使不使用SOLID原则#xff0c;仅通过类、继承、封装和多态性#xff0c;也可以让程序正常运行。那么为…
(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:https://www.instagram.com/techwithisha/reel/C1Ws1ZDt8_j/)
SOLID原则
那么我们已经了解了上述问题的原因现在让我们来详细探讨一下SOLID原则。 我对所有智能思维特征的理解 这指的是深入研究某一主题特定方面的态度。 这种研究不仅是为了保持该方面的一致性同时也意识到自己所处理的内容只是整体的一部分。 我们知道程序必须正确运行 因此我们可以从这个角度研究程序。 此外我们也知道程序必须高效 这一分析可以在其他时间单独进行。 有时我们会思考程序是否真的必要如果是为什么。 然而同时处理这些不同方面并不会带来任何好处反而只会造成干扰。 我称之为“关注点分离Separation of Concerns” 虽然无法完全实现但这是有效整理思路的唯一方法。 当我说“专注于某一方面”时 这并不意味着忽略其他方面。 相反这意味着从某个特定方面的角度来看其他方面暂时并不重要。 也就是说这是一种既能专注于一件事又能同时考虑多件事的思维方式。 —Edsger Wybe DijkstraOn 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 的 OrderServiceOrderService orderService1 new OrderService(sqlDatabase);orderService1.PlaceOrder(商品 A 订单);// 使用 MongoDatabase 的 OrderServiceOrderService 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. Woods1991年) SOLID的本质与实用性平衡
SOLID原则诞生于敏捷哲学作为提升代码可维护性和扩展性的强大工具已经占据了重要地位。
然而它并不是适用于所有情况的“银弹”。
对于像虚幻引擎的Actor这样具有强烈领域特性的场景或者在GUI框架中需要发挥创造力时又或者在性能至关重要的系统中与其勉强遵循SOLID原则不如选择适合上下文的设计更为重要。
正如John F. Woods所说“假设维护你代码的人是一个知道你住址的暴力精神病患者”这不仅仅是在提醒我们写易读的代码。
其背后的真正含义是无论在什么情况下都要让处理代码的人能够轻松理解并适应代码。
SOLID只是实现这一目标的一种方式而不是唯一的方式。有时违反SRP保持一个整合的类或者忽略DIP选择具体的依赖可能是防止“精神病患者”采取极端行动的实用选择。