65.9K
CodeProject 正在变化。 阅读更多。
Home

SOLID 原则详解

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (7投票s)

2015 年 7 月 14 日

CPOL

18分钟阅读

viewsIcon

45453

SOLID 原则与软件系统的设计和维护有关。

引言

SOLID 原则是一系列由 Robert.C.Martin 最先提出的原则。SOLID 原则顾名思义,是一套能够构建 SOLID 软件系统的原则(例如:应用程序、应用程序模块),如果一个人按照这套原则来实现软件的话。

SOLID 软件系统意味着它允许构建这样的系统:

  1. 易于维护
  2. 易于扩展
  3. 易于理解
  4. 易于实现
  5. 易于解释

SOLID 原则与软件系统的设计和维护有关。大多数开发人员会将 SOLID 原则与 OOD(面向对象设计)原则和设计模式混淆。下图消除了 OOD 原则和设计模式的混淆。

注意:这是我对事物排列方式的个人解读。

所以根据上图:

  1. OOD 原则(抽象、封装、继承、多态)
  2. SOLID 原则
  3. 软件设计模式(GOF 模式、依赖注入和模式)
  4. Martin Flower 的企业应用架构模式(附加但必需)
  5. 领域驱动设计架构(附加但必需)

S.O.L.I.D 是的缩写

  1. 单一职责原则

    该原则与设计应仅执行一项任务的软件模块、类或函数有关。所以这个原则是关于创建的。

  2. 开闭原则

    该原则应用于第 1 条(单一职责原则)之后,此原则也与设计模块、类或函数有关。但它关于关闭已设计的实体以进行修改,同时对扩展保持开放,即扩展功能。所以这个原则是关于扩展的。

  3. 里氏替换原则

    该原则与子类替换父类有关。所以这个原则是关于关系的,即继承。

  4. 接口隔离原则

    该原则与接口的设计有关,因为设计的基本规则之一是依赖抽象。该原则是关于以这样的方式设计接口,以至于接口的客户端不必实现不需要的东西。所以这个原则是关于高效接口设计的。

  5. 依赖倒置原则

    该原则与设计软件系统中解耦的模块、类有关。此原则大多在第 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 类具有以下职责:

  1. ValidateOrder:验证客户下的订单,如有错误则返回错误消息。
  2. SaveOrder:保存客户下的订单,并返回 true/false。
  3. 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;  
}

前面的方法具有以下职责:

  1. 方法首先处理所有订单。
  2. 它遍历集合中的所有订单并执行一些操作。

遵循单一职责原则

要使类或函数遵循单一职责原则,请像下面这样通过创建新类或函数来划分职责。

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;  
}

注意

遵循单一职责并不意味着可以创建一个只有一种方法的类。

未遵循单一职责原则的缺点

在编程中,如果开发人员创建了一个执行多个任务的类/函数,那么在提供高质量方面总是会遇到问题。以下问题与执行多个任务的类有关:

  1. 其他开发人员(即不熟悉该类的开发人员)很难理解该类/函数。
  2. 其他开发人员很难维护或修改该类/函数。
  3. 为类/函数编写测试用例也变得困难。

如何确定单一职责原则未被遵循

  1. 尝试为类或方法写一个单行描述,如果描述中包含“并且”、“或”、“但”或“如果”等词语,那么这就是一个问题。如上所述,一个不遵循单一职责原则的类的描述示例是:“一个 Order 类,它执行订单保存、客户通知和订单验证”。
  2. 一个类的构造函数接受超过三个参数,或者一个方法包含过多的参数。
  3. 一个类或方法作为一个实现,其长度过长。
  4. 一个内聚性低的类。阅读更多关于内聚性的信息:内聚性

开闭原则

开闭原则是由 Robert C. Martin 定义的 SOLID 原则之一。该原则指出“软件实体(类、模块、函数等)应该对扩展开放,对修改关闭”。

所以简单来说,它表示一个实现(类或函数)一旦创建,就应该关闭以进行进一步修改,换句话说,不应该修改实现的逻辑和/或功能。可以进行重构或解决实现的错误(类或函数),但实现是对扩展开放的,换句话说,可以扩展实现(类或函数)的逻辑和/或功能。

开闭原则的现实生活示例

电源适配器是该原则的一个很好的例子。

正如你在图中所看到的

  1. 墙上的适配器总是关闭以供修改,换句话说,一旦安装好,我们就不能改变它,或者如果我们想要更多,就不能扩展它。
  2. 但是适配器总是提供扩展的方法,所以我们可以插入适配器的扩展板以获得更多适配功能。
  3. 所以你插入一个扩展板,扩展了安装在墙上的现有电源适配器。

应用程序开发中未遵循原则的示例

银行提供各种类型的储蓄账户(工资储蓄、普通储蓄等),以满足各种客户的需求。银行有不同的规则集,每种储蓄账户类型都有不同的规则来计算利息。

为了计算账户的利息,开发人员开发了以下类,其中包含计算利息的方法。

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 方法根据账户类型(如 SalaryRegular)进行计算。

因此,该实现没有遵循开闭原则,因为如果明天银行引入新的储蓄账户类型,就需要修改此方法以添加新账户类型的条件。例如,如果银行引入“儿童储蓄账户类型”,则需要为该账户类型的新计算利息条件。这意味着该方法总是对修改开放。

还有一点需要注意:该方法也没有遵循单一职责原则。因为在这里,该方法做了不止一件事情,比如计算一种以上类型的利息。

如何实现开闭原则

继承只是实现开闭原则的一种方式。因为继承只是面向对象设计(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;  
  }  
}

在前面的代码中,创建了两个新类 RgularSavingAccountSalarySavingAccount,它们都继承自 IsavingAccount

因此,如果银行添加了一个新账户,就不需要修改现有类的逻辑,只需通过继承接口来扩展功能。

最后,前面的示例实现了开闭原则,因为不需要修改现有的已实现逻辑,并且它允许扩展以添加新逻辑。

而且前面的示例也实现了单一职责原则,因为每个类或函数只做一项任务。

注意: 接口在这里仅作为示例创建。可能有一个抽象类 SavingAccount,它由一种新的储蓄账户类型实现。

未遵循开闭原则的缺点

  1. 由于类或函数总是允许添加新逻辑,每当添加新逻辑时,总是需要对完整的功能进行测试。这需要为添加的功能添加新的测试用例,并且可能还需要修改现有的因添加功能而失败的测试用例。
  2. 它还破坏了单一职责原则,因为一个类或函数最终可能会执行多个任务。
  3. 类或函数的维护变得困难,因为一个类或函数可能包含数千行代码,难以理解。

如何识别未遵循单一职责原则

  • 一个类或函数总是对修改开放,换句话说,总是允许在其上添加更多逻辑。就像在前面的示例中一样。

里氏替换原则

里氏替换原则(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 接口被银行的不同类型的储蓄账户实现,例如 RegularSalaryFixDeposit 储蓄账户。

但是根据银行规定,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(正方形和矩形示例)。

在前面的图片中,创建了新的类 WithWithdrawalWithoutWithdrawal,子类继承自相应的父类。

因此,在前面的代码中:

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(接口隔离原则)违规方面,下图显示了一个大型垃圾桶,用于丢弃各种垃圾,而没有任何分类。

图 1:垃圾桶

遵循 ISP 的情况,下图是我们现实生活中的一个很好的分类示例。

图 2:现实生活中的 ISP

这是一个垃圾分类箱的图像,显示了我们在丢弃垃圾时应该使用哪一个。

ISP 在应用程序开发中的示例

这里有一个银行客户的例子,银行有以下类型的客户:

  1. 公司客户:适用于公司人员。
  2. 零售客户:适用于个人、日常银行业务。
  3. 潜在客户:他们只是银行的客户,但还没有持有银行的任何产品,这只是一个与公司和零售不同的记录。

系统开发人员为客户定义了一个接口,如下所示,这没有遵循 ISP 规则。

图 3:客户接口

乍一看似乎还可以,但这是一个大型的“胖接口”,存在问题,因为它迫使客户端类实现不需要的方法。

  1. 潜在客户(如上所述,不持有任何产品)被迫实现产品属性。
  2. 潜在客户和零售客户都必须拥有客户结构属性,但在实际场景中,公司客户拥有描述客户层次结构的客户结构。
  3. 潜在客户和零售客户都必须实现业务类型,但这只属于公司客户。
  4. 公司客户和潜在客户都必须实现职业属性,而这只是零售客户的属性。

解决上述问题的方法是将“胖接口”分解为有意义的部分,换句话说,是小型接口,因此客户类型只实现其所需的接口。

下图展示了一个遵循 ISP 的图像。

图 4:接口

缺点

  1. 提供一个大型“胖接口”,迫使客户端实现一个不需要的方法。
  2. 客户端最终实现了一个无用的方法,换句话说,一个对客户端没有意义的方法。这降低了代码的可读性,也使使用客户端代码的开发人员感到困惑。
  3. 客户端接口最终有时会违反 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 类或使用依赖项容器来接收依赖项。

注意:这里是一个类的例子,但对于软件中设计的模块也是一样的,因为依赖倒置是关于提供一组抽象策略,细节依赖于这些策略,并且策略为软件系统提供了灵活性。

缺点:应用程序模块变得紧密耦合,这意味着:

  1. 模块的可测试性变得困难。
  2. 模块的并行开发变得困难。
  3. 当模块发生修改以及当它依赖的模块发生变化时,需要进行许多更改。

注意:依赖注入与依赖倒置不同,因为依赖倒置是关于为软件模块定义抽象策略,而依赖注入是一套提供依赖的模式。

© . All rights reserved.