SOLID 原则详解






4.91/5 (7投票s)
SOLID 原则与软件系统的设计和维护有关。
引言
SOLID 原则是一系列由 Robert.C.Martin 最先提出的原则。SOLID 原则顾名思义,是一套能够构建 SOLID 软件系统的原则(例如:应用程序、应用程序模块),如果一个人按照这套原则来实现软件的话。
SOLID 软件系统意味着它允许构建这样的系统:
- 易于维护
- 易于扩展
- 易于理解
- 易于实现
- 易于解释
SOLID 原则与软件系统的设计和维护有关。大多数开发人员会将 SOLID 原则与 OOD(面向对象设计)原则和设计模式混淆。下图消除了 OOD 原则和设计模式的混淆。
注意:这是我对事物排列方式的个人解读。
所以根据上图:
- OOD 原则(抽象、封装、继承、多态)
- SOLID 原则
- 软件设计模式(GOF 模式、依赖注入和模式)
- Martin Flower 的企业应用架构模式(附加但必需)
- 领域驱动设计架构(附加但必需)
S.O.L.I.D 是的缩写
- 单一职责原则
该原则与设计应仅执行一项任务的软件模块、类或函数有关。所以这个原则是关于创建的。
- 开闭原则
该原则应用于第 1 条(单一职责原则)之后,此原则也与设计模块、类或函数有关。但它关于关闭已设计的实体以进行修改,同时对扩展保持开放,即扩展功能。所以这个原则是关于扩展的。
- 里氏替换原则
该原则与子类替换父类有关。所以这个原则是关于关系的,即继承。
- 接口隔离原则
该原则与接口的设计有关,因为设计的基本规则之一是依赖抽象。该原则是关于以这样的方式设计接口,以至于接口的客户端不必实现不需要的东西。所以这个原则是关于高效接口设计的。
- 依赖倒置原则
该原则与设计软件系统中解耦的模块、类有关。此原则大多在第 4 条(接口隔离原则)之后应用,因为接口是一种抽象形式,而此原则与细节(模块/类)应依赖于抽象,而抽象不应依赖于细节有关。所以这个原则是关于创建松耦合系统的。
以下详细描述了每个原则:
单一职责原则
单一职责原则是由 Robert C. Martin 定义的 SOLID 原则之一。原则上,它指出一个实现(类/函数)应该只执行一项任务或实现,并且应该只为一个原因而更改它(类/函数)。
所以简单来说,它表示开发人员在开发过程中在代码中实现的任何内容(类/函数)都应该只执行一项任务,并且开发人员应该只有一个原因来更改实现(类/函数)。
对原则的错误解读
大多数开发人员将其理解为类应该只执行一项任务。但不仅是类,开发过程中在代码中实现的函数也应该只执行一项任务。所以应该将其理解为实现应该只执行一项任务。
未遵循单一职责原则的现实生活示例
当一个人可以做不止一件事情时会发生什么?下面是它的一个例子图。
一个人可以执行多项任务,这一点毫无疑问,但这不会提供高质量/更好的产出。
因此,要获得高质量/更好的工作产出,一个人应该一次只做一件事。
应用程序开发中未遵循原则的示例
在编程中,换句话说,在开发以下代码时,Order
类没有遵循该原则。
Public class OrderManager
{
Public List < string > ValidateOrder()
{
//Code for validation
}
Public bool SaveOrder(OrderInfo order)
{
//Code for saving order
}
Public void NotifyCustomer()
{
//Code for notification
}
}
前面的 order 类具有以下职责:
ValidateOrder
:验证客户下的订单,如有错误则返回错误消息。SaveOrder
:保存客户下的订单,并返回 true/false。NotifyCustomer
:通知客户订单已下单。
未遵循原则的方法。
public int SumOfAllCustomerOrder(int customerId)
{
int sum = 0;
var query = "Select * from order where customerid = " + customerid;;
//query orders
foreach(Order in OrderCollection)
{
If(Order.Items.Count > 5)
Sum += Order.Price;
}
return sum;
}
前面的方法具有以下职责:
- 方法首先处理所有订单。
- 它遍历集合中的所有订单并执行一些操作。
遵循单一职责原则
要使类或函数遵循单一职责原则,请像下面这样通过创建新类或函数来划分职责。
Public Class OrderValidator
{
Public List < string > Validate(Order order)
{
//code for validation
}
Public Class Notifier
{
Public void Notify(string emailId)
{
//code for notification
}
Public Class OrderManager
{
Private readonly OrderValidator orderValidator;
Private readonly Notifier notifier;
Public OrderManager(OrderValidator oValidator, Notifier nFier) {
orderValidator = oValidator;
notifier = nFier;
}
Public bool SaveOrder(OrderInfo orderInfo)
{
//Validate order
orderValidator.Validate(orderInfo);
//code for saving order //this might be call to repository to save order
//notify after successful saving
notifier.Notify(orderInfo.EmailId);
}
Public List < OrderInfo > GetOrders(int customerId)
{
//code for getting order by cusotmerid
}
前面的代码显示了三个类,每个类只有一个单一的职责。
对于方法,它将是这样的:
public List < OrderInfo > GetOrder(int customerId)
{
int sum = 0;
var query = “Select * from order where customerid = ” + customerid;;
//query orders
return ordercollection;
}
public int SumOfAllCustomerOrder(int customerId)
{
var OrderCollection = GetOrder(customerId);
foreach(Order in OrderCollection)
{
If(Order.Items.Count > 5)
Sum += Order.Price;
}
return sum;
}
注意
遵循单一职责并不意味着可以创建一个只有一种方法的类。
未遵循单一职责原则的缺点
在编程中,如果开发人员创建了一个执行多个任务的类/函数,那么在提供高质量方面总是会遇到问题。以下问题与执行多个任务的类有关:
- 其他开发人员(即不熟悉该类的开发人员)很难理解该类/函数。
- 其他开发人员很难维护或修改该类/函数。
- 为类/函数编写测试用例也变得困难。
如何确定单一职责原则未被遵循
- 尝试为类或方法写一个单行描述,如果描述中包含“并且”、“或”、“但”或“如果”等词语,那么这就是一个问题。如上所述,一个不遵循单一职责原则的类的描述示例是:“一个 Order 类,它执行订单保存、客户通知和订单验证”。
- 一个类的构造函数接受超过三个参数,或者一个方法包含过多的参数。
- 一个类或方法作为一个实现,其长度过长。
- 一个内聚性低的类。阅读更多关于内聚性的信息:内聚性
开闭原则
开闭原则是由 Robert C. Martin 定义的 SOLID 原则之一。该原则指出“软件实体(类、模块、函数等)应该对扩展开放,对修改关闭”。
所以简单来说,它表示一个实现(类或函数)一旦创建,就应该关闭以进行进一步修改,换句话说,不应该修改实现的逻辑和/或功能。可以进行重构或解决实现的错误(类或函数),但实现是对扩展开放的,换句话说,可以扩展实现(类或函数)的逻辑和/或功能。
开闭原则的现实生活示例
电源适配器是该原则的一个很好的例子。
正如你在图中所看到的
- 墙上的适配器总是关闭以供修改,换句话说,一旦安装好,我们就不能改变它,或者如果我们想要更多,就不能扩展它。
- 但是适配器总是提供扩展的方法,所以我们可以插入适配器的扩展板以获得更多适配功能。
- 所以你插入一个扩展板,扩展了安装在墙上的现有电源适配器。
应用程序开发中未遵循原则的示例
银行提供各种类型的储蓄账户(工资储蓄、普通储蓄等),以满足各种客户的需求。银行有不同的规则集,每种储蓄账户类型都有不同的规则来计算利息。
为了计算账户的利息,开发人员开发了以下类,其中包含计算利息的方法。
Public class SavingAccount
{
//Other method and property and code
Public decimal CalculateInterest(AccountType accountType)
{
If(AccountType==”Regular”)
{
//Calculate interest for regular saving account based on rules and
// regulation of bank
Interest = balance * 0.4;
If(balance < 1000) interest -= balance * 0.2;
If(balance < 50000) interest += amount * 0.4;
}
else if(AccountType==”Salary”)
{
//Calculate interest for saving account based on rules and regulation of
//bank
Interest = balance * 0.5;
}
}
}
因此,在前面的代码中,SavingAccount
类的 CalculateInterest
方法根据账户类型(如 Salary
和 Regular
)进行计算。
因此,该实现没有遵循开闭原则,因为如果明天银行引入新的储蓄账户类型,就需要修改此方法以添加新账户类型的条件。例如,如果银行引入“儿童储蓄账户类型”,则需要为该账户类型的新计算利息条件。这意味着该方法总是对修改开放。
还有一点需要注意:该方法也没有遵循单一职责原则。因为在这里,该方法做了不止一件事情,比如计算一种以上类型的利息。
如何实现开闭原则
继承只是实现开闭原则的一种方式。因为继承只是面向对象设计(OOD)的一个基本支柱,它允许扩展现有类的功能。
为了实现开闭原则,可以使用接口、抽象类、抽象方法和虚拟方法,然后在需要扩展功能时继承它们。
因此,前面提到的储蓄账户问题可以像下面这样解决。
Interface ISavingAccount
{
//Other method and property and code
decimal CalculateInterest();
}
Public Class RegularSavingAccount : ISavingAccount
{
//Other method and property and code related to Regular Saving account
Public decimal CalculateInterest()
{
//Calculate interest for regular saving account based on rules and
// regulation of bank
Interest = balance * 0.4;
If(balance < 1000) interest -= balance * 0.2;
If(balance < 50000) interest += amount * 0.4;
}
}
Public Class SalarySavingAccount : ISavingAccount
{
//Other method and property and code related to Salary Saving account`
Public decimal CalculateInterest()
{
//Calculate interest for saving account based on rules and regulation of
//bank
Interest = balance * 0.5;
}
}
在前面的代码中,创建了两个新类 RgularSavingAccount
和 SalarySavingAccount
,它们都继承自 IsavingAccount
。
因此,如果银行添加了一个新账户,就不需要修改现有类的逻辑,只需通过继承接口来扩展功能。
最后,前面的示例实现了开闭原则,因为不需要修改现有的已实现逻辑,并且它允许扩展以添加新逻辑。
而且前面的示例也实现了单一职责原则,因为每个类或函数只做一项任务。
注意: 接口在这里仅作为示例创建。可能有一个抽象类 SavingAccount,它由一种新的储蓄账户类型实现。
未遵循开闭原则的缺点
- 由于类或函数总是允许添加新逻辑,每当添加新逻辑时,总是需要对完整的功能进行测试。这需要为添加的功能添加新的测试用例,并且可能还需要修改现有的因添加功能而失败的测试用例。
- 它还破坏了单一职责原则,因为一个类或函数最终可能会执行多个任务。
- 类或函数的维护变得困难,因为一个类或函数可能包含数千行代码,难以理解。
如何识别未遵循单一职责原则
- 一个类或函数总是对修改开放,换句话说,总是允许在其上添加更多逻辑。就像在前面的示例中一样。
里氏替换原则
里氏替换原则(Liskov Substitution Principle)– 是 SOLID 原则之一,由 Barbara Liskov 定义。该原则基于父子关系,换句话说,是 OOD(面向对象设计)的继承特性。该原则指出“当类 S 是类 T 的子类型时,类型 T 的对象可以用类型 S 的对象替换,而不会影响实现或程序的 [功能/正确性]”。
简单来说,它表示“在实现(类/函数)中使用基类的位置,换句话说,消耗基类服务的那些地方,当用子类(派生类)对象替换基类对象时,必须能够正常工作。”
里氏替换原则在现实生活中的应用
下面的例子是一个电灯泡,它实际上违反了替换原则。当灯泡损坏时,它会被一个新灯泡替换。在这个例子中,旧灯泡被新灯泡替换。
完美替换

注意
在这个灯泡家族的例子中,将旧灯泡视为所有灯泡类型的父类,并将节能灯视为继承自它的同一家族的子类。
当一个人替换一个坏掉的灯泡,换句话说,在编程术语中用新的替换旧的,它必须提供旧灯泡提供的光,换句话说,在不影响正确性或功能的情况下工作(为家庭提供恒定的光)。在前面的例子中,替换工作得很完美,因为功能没有改变。
违反示例
前面的图片显示了该原则的违反。因为用装饰灯泡(提供装饰性灯光)替换了只为照亮房屋而设计的灯泡。这实际上是一种功能修改的违规,因为装饰灯泡不为消费者提供相同的功能。
应用程序开发中未遵循原则的示例
继续前面文章中已解释的银行储蓄账户,关于开闭原则。
Interface ISavingAccount
{
//Other method and property and code
bool Withdrwal(decimal amount);
}
Public Class RegularSavingAccount : ISavingAccount
{
//Other method and property and code related to Regular Saving account
Public bool Withdrwal ()
{
Decimal moneyAfterWithdrawal = Balance-amount;
if(moneyAfterWithdrawal >= 1000)
{
//update balace
return true;
}
else
return false;
}
}
Public Class SalarySavingAccount : ISavingAccount
{
//Other method and property and code related to Salary Saving account`
Public bool Withdrwal ()
{
Decimal moneyAfterWithdrawal = Balance-amount;
if(moneyAfterWithdrawal >= 0)
{
//update balace
return true;
}
else
return false;
}
}
Public Class FixDepositSavingAccount : ISavingAccount
{
//Other method and property and code related to Salary Saving account`
Public bool Withdrwal ()
{
Throw New Excpetion("Not supported by this account type");
}
}
在前面的代码中,IsavingAccount
接口被银行的不同类型的储蓄账户实现,例如 Regular
、Salary
和 FixDeposit
储蓄账户。
但是根据银行规定,FixDeposit
储蓄账户不提供取款功能,而其他银行账户可能提供取款功能。
因此,开发人员可能会编写代码,在尝试从 FixDeposit
储蓄账户取款时抛出异常。
现在考虑一个类中的以下方法,该方法通过将实际对象强制转换为父类类型来调用取款。
Public class AccountManager
{
Public bool WithdrawFromAccount(IsavingAccount account)
{
account.Withdraw(amount);
}
}
以下代码调用该方法:
//works ok AccountManager.WidhdrawFromAccount(new RegularSavingAccount()); //works ok AccountManager.WidhdrawFromAccount(new SalarySavingAccount()); //throws exception as withdrawal is not supported AccountManager.WidhdrawFromAccount(new FixDepositSavingAccount());
违反里氏替换规则
因此,前面的代码违反了里氏替换规则,因为继承的 FixDepositSavingAccount
类修改了取款功能。因为储蓄账户应该提供取款功能而不抛出任何错误。
如何停止违反规则
要停止违反规则,必须验证继承树,换句话说,从父类继承的子类在子类对象替换父类对象时,不应破坏功能。
因此,类必须以这样的方式继承自正确的父类,使得当子类替换父类时,它不会破坏父类提供的实际功能。
注意
并非总是如此,一个人必须在继承树中进行更改,但对类和方法级别的更改也可以解决问题。要获得此类示例,请单击链接:Object Menter(正方形和矩形示例)。
在前面的图片中,创建了新的类 WithWithdrawal
和 WithoutWithdrawal
,子类继承自相应的父类。
因此,在前面的代码中:
Interface ISavingAccount
{}
Public Class SavingAccountWithWithdrawal : ISavingAccount
{
Public virtual bool Withdrwal () {}
}
Public Class SavingAccountWithoutWithdrawal : ISavingAccount
{
}
Public Class RegularSavingAccount : SavingAccountWithWithdrawal
{
Public bool Withdrwal ()
{
//implementation
}
}
Public Class SalarySavingAccount : SavingAccountWithWithdrawal
{
Public bool Withdrwal ()
{//implementation
}
}
Public Class FixDepositSavingAccount : SavingAccountWithoutWithdrawal
{
}
现在使用它:
Public class AccountManager
{
Public bool WithdrawFromAccount(SavingAccountWithWithdrawal account)
{
account.Withdraw(amount);
}
}
现在调用方法的代码:
//works ok AccountManager.WidhdrawFromAccount(new RegularSavingAccount()); //works ok AccountManager.WidhdrawFromAccount(new SalarySavingAccount()); //compiler gives error AccountManager.WidhdrawFromAccount(new FixDepositSavingAccount());
未遵循里氏替换原则的缺点
- 开发的代码会抛出运行时错误或异常,或者也可能无法按预期工作,这会导致程序失败或结果不正确。
- 前面的讨论显示了一个子类为不支持的方法抛出异常的例子。
- 阅读链接:Object Mentor,其中显示了一个示例(正方形和矩形示例),说明在未遵循该原则时会产生不正确的结果。
因此,不遵循此规则的最大缺点是:它会在运行时导致问题,而不是导致应用程序故障或结果不正确。
接口隔离原则
接口隔离原则(Interface Segregation Principle)是由 Robert C. Martin 定义的 SOLID 原则之一。它是软件开发的一条规则,指出始终根据契约(即接口)进行编码,而不是根据实现(即具体类)进行编码,因为根据接口编码提供了灵活性、松耦合、可测试代码等优势。此原则与为实现创建接口有关。
该原则指出:“客户端(类实现接口)不应被迫实现它们不使用的接口。” 简单来说,该原则就是说,不要设计一个大型的“胖接口”,迫使客户端实现它不需要的方法,而是设计一个小型接口。通过这样做,类只实现所需的一组接口。
如果存在一个大型的“胖接口”,就将其分解为一组小型接口,其中包含相关的方法。这类似于数据库规范化,将数据库从 1NF 规范化到 3NF,其中一个大表被分解为具有相关列的表。
接口隔离原则在现实生活中的应用
在 ISP(接口隔离原则)违规方面,下图显示了一个大型垃圾桶,用于丢弃各种垃圾,而没有任何分类。
遵循 ISP 的情况,下图是我们现实生活中的一个很好的分类示例。
这是一个垃圾分类箱的图像,显示了我们在丢弃垃圾时应该使用哪一个。
ISP 在应用程序开发中的示例
这里有一个银行客户的例子,银行有以下类型的客户:
- 公司客户:适用于公司人员。
- 零售客户:适用于个人、日常银行业务。
- 潜在客户:他们只是银行的客户,但还没有持有银行的任何产品,这只是一个与公司和零售不同的记录。
系统开发人员为客户定义了一个接口,如下所示,这没有遵循 ISP 规则。
乍一看似乎还可以,但这是一个大型的“胖接口”,存在问题,因为它迫使客户端类实现不需要的方法。
- 潜在客户(如上所述,不持有任何产品)被迫实现产品属性。
- 潜在客户和零售客户都必须拥有客户结构属性,但在实际场景中,公司客户拥有描述客户层次结构的客户结构。
- 潜在客户和零售客户都必须实现业务类型,但这只属于公司客户。
- 公司客户和潜在客户都必须实现职业属性,而这只是零售客户的属性。
解决上述问题的方法是将“胖接口”分解为有意义的部分,换句话说,是小型接口,因此客户类型只实现其所需的接口。
下图展示了一个遵循 ISP 的图像。
缺点
- 提供一个大型“胖接口”,迫使客户端实现一个不需要的方法。
- 客户端最终实现了一个无用的方法,换句话说,一个对客户端没有意义的方法。这降低了代码的可读性,也使使用客户端代码的开发人员感到困惑。
- 客户端接口最终有时会违反 SRP,因为它可能会执行一些与其无关的操作。
依赖倒置原则
依赖倒置原则(Dependency Inversion Principle)是由 Robert C. Martin 定义的 SOLID 原则之一。此原则涉及软件组件(如两个模块、两个类)之间的依赖关系。
该原则指出,高层模块应依赖于抽象,而不是低层模块的细节,换句话说,不是低层模块的实现。抽象不应依赖于细节。细节应依赖于抽象。简单来说,该原则表示软件组件(即两个模块、两个类)之间不应存在紧密耦合,为了避免这种情况,组件应依赖于抽象,换句话说,是契约(接口或抽象类)。
依赖倒置原则在现实生活中的应用
为了更好地理解第二个问题,让我们来看一个计算机或笔记本电脑的现实生活场景。
正如你在前面的图像中看到的,我们有一个端口用于连接每个外部设备,然后我可以关联一个外部设备并完成我们的工作。
但是这样做的问题是,我不能将我的键盘连接到打印机端口,反之亦然。其他设备也存在同样的问题。所以这就像一种紧密耦合,我不能在给定的接口上更改我的外部设备,换句话说,我依赖于它。
解决方案是 USB 端口。
如果我有一个 USB 端口,那么我可以轻松地将任何设备连接到我的机器并完成我的任务。
依赖倒置原则在应用程序开发中的示例
下图是一个不遵循该原则的紧密耦合的类图。
Public Class Customer
{
CustomerRepository CustomerRepository;
Public Customer
{
CustomerRepository = new CustomerRpository();
}
Public bool Save()
{
CustomerRepository.Save();
}
}
Public class CustomerRepository
{
Public bool Save(dattype data)
{
//Sql Connection object and Save data in Sql server
}
}
前面的代码是紧密耦合的,因为当前的存储库处理 SQL Server。因此,如果需求是使用 Oracle 服务器,那么 Customer 类就需要修改。
因此,为了避免这种情况,使 Customer 类依赖于抽象。下图是一个类图,其中 Customer 依赖于抽象,并支持 SQL 和 Oracle 服务器。
Public Class Customer
{
CustomerRepository CustomerRepository;
Public Customer
{
CustomerRepository = new CustomerRpository();
}
Public bool Save()
{
CustomerRepository.Save();
}
}
Public class CustomerRepository
{
Public bool Save(dattype data)
{
//Sql Connection object and Save data in Sql server
}
}
因此,在前面的代码中,Customer 类依赖于 ICustomerRepository
抽象,换句话说,是一个接口。另一件事是,Customer 类通过消耗 Customer 类或使用依赖项容器来接收依赖项。
注意:这里是一个类的例子,但对于软件中设计的模块也是一样的,因为依赖倒置是关于提供一组抽象策略,细节依赖于这些策略,并且策略为软件系统提供了灵活性。
缺点:应用程序模块变得紧密耦合,这意味着:
- 模块的可测试性变得困难。
- 模块的并行开发变得困难。
- 当模块发生修改以及当它依赖的模块发生变化时,需要进行许多更改。
注意:依赖注入与依赖倒置不同,因为依赖倒置是关于为软件模块定义抽象策略,而依赖注入是一套提供依赖的模式。