C#中面向对象编程的SOLID原理

作者 : IT 大叔 本文共11944个字,预计阅读时间需要30分钟 发布时间: 2020-10-29

SOLID原则是2000年代初期以来面向对象的开发人员使用的一组黄金规则。他们设定了如何使用OOP语言进行编程的标准,现在已经超出了敏捷开发的范围。SOLID程序可扩展性更好,花费的时间更少,并且可以更轻松地响应更改。雇主将始终喜欢对SOLID原则有深刻理解的候选人。

今天,我们将深入研究这5条原则,并使用代码,插图和易于理解的描述来解释它们的工作原理。

这是我们今天要介绍的内容:

  • SOLID原则是什么?
  • S:单一责任原则
  • O:开闭原理
  • L:李斯科夫替代原理
  • I:接口隔离原理
  • D:依赖倒置原则
  • 接下来要学什么

SOLID原则是什么?

SOLID是一种助记符设备,可用于5种面向对象程序(OOP)的设计原理,从而产生可读,可调整和可扩展的代码。SOLID可以应用于任何OOP程序。

SOLID的5条原则是:

  • 小号英格尔责任原则
  • O封闭式原理
  • L iskov替代原理
  • 覆盖整个院落隔离原则
  • d ependency倒置原则

SOLID原理是由计算机科学老师和作者Robert C. Martin(有时称为“ Bob叔叔”)于2000年开发的,并迅速成为现代面向对象设计(OOD)的主要内容。当这些原理在编程世界中广为流行时,SOLID的缩写变得司空见惯。

现在,SOLID已被敏捷开发和自适应软件开发所采用。

理解SOLID的最好方法是分解5条原则中的每一项,并查看它们在代码中的外观。所以,让我们做到这一点!

S:单一责任原则

SRP:拆分非SRP类

“一个类应该只负责一个职责,也就是说,只有对软件规范的一部分进行更改才能影响该类的规范。”

单一职责原则(SRP)项规定,每类模块,或者在你的程序的功能应该只能做一项工作。换句话说,每个人应对程序的单个功能负全部责任。该类应仅包含与其功能相关的变量和方法。

类可以协同工作以完成较大的复杂任务,但是每个类必须先完成一个函数,然后再将输出传递给另一个类。

马丁通过说“一个阶级应该只有一个改变的理由”来解释这一点。这里的“原因”是我们要更改此类追求的单个功能。如果我们不希望更改单个功能,则将永远不要更改此类,因为该类的所有组件都应与该行为相关。

因此,我们可以更改程序中除一个类之外的所有类,而不会破坏原始类。

SRP使遵循OOP的另一个广受尊重的原则即封装变得容易。当作业的所有数据和方法都在同一个单一职责类中时,很容易向用户隐藏数据。

如果将getter和setter方法添加到单一职责类,则该类满足封装类的所有条件。

遵循SRP的程序的好处是,您可以通过编辑负责该功能的单个类来更改其行为。另外,如果单个功能中断,您将知道错误在代码中的位置,并且可以相信只有该类会中断。

这个因素还有助于提高可读性,因为您只需要阅读一个类,直到确定其功能即可。

实作

让我们看一个如何应用SRP来使我们的RegisterUser类更具可读性的示例。

// does not follow SRP
public class RegisterService
{
    public void RegisterUser(string username)
    {
        if (username == "admin")
            throw new InvalidOperationException();
 
        SqlConnection connection = new SqlConnection();
        connection.Open();
        SqlCommand command = new SqlCommand("INSERT INTO [...]");//Insert user into database. 
 
        SmtpClient client = new SmtpClient("smtp.myhost.com");
        client.Send(new MailMessage()); //Send a welcome email. 
    }
}

上面的程序没有遵循SRP,因为RegisterUser它执行三个不同的工作:注册用户,连接到数据库以及发送电子邮件。

这种类型的类会在较大的项目中引起混乱,因为出乎意料的是,在与注册相同的类中生成电子邮件。

还有很多可能导致此代码更改的事情,例如,如果我们在数据库模式中进行了切换,或者我们采用了新的电子邮件API发送电子邮件。

相反,我们需要将该类划分为三个特定的类,每个类分别完成一项工作。这是我们同一个班级的样子,将所有其他作业重构为单独的班级:

 
public void RegisterUser(string username)
{
    if (username == "admin")
        throw new InvalidOperationException();
 
    _userRepository.Insert(...);
    
    _emailService.Send(...);
}

这样就达到了SRP,因为RegisterUser仅注册了一个用户,并且更改的唯一原因是如果添加了更多的用户名限制。所有其他行为都保留在程序中,但现在可以通过调用userRepository和来实现emailService

O:开闭原理

OCP:修改OldClass而不是扩展

“软件实体……应为扩展而开放,但应为修改而封闭。”

最初,该语句似乎是矛盾的,因为它要求您对实体(类/函数/模块)进行编程以使其处于打开和关闭状态。在开放-封闭原则(OCP)要求,可广泛适应,但也保持不变的实体。这导致我们通过多态性创建具有特殊行为的重复实体。

通过多态,我们可以扩展父实体以适合子实体的需求,同时保持父实体不变。

我们的父实体将作为一个抽象基类,可以通过继承与附加的专业化一起重用。但是,原始实体被锁定以允许程序被打开和关闭。

OCP的优点是,当您为实体添加新用途时,它可以最大程度地降低程序风险。您无需重新创建基类以适应正在进行的工作,而是创建一个与整个程序中当前存在的类分开的派生类。

然后,我们可以处理这个唯一的派生类,确信我们对其所做的任何更改都不会影响父类或任何其他派生类。

实作

OCP实现通常依靠多态和抽象来在类级别上对行为进行编码,而不是在某些情况下进行硬编码。让我们看看如何纠正面积计算器程序以遵循OSP:

// Does not follow OSP
public double Area(object[] shapes)
{
    double area = 0;
    foreach (var shape in shapes)
    {
        if (shape is Rectangle)
        {
            Rectangle rectangle = (Rectangle) shape;
            area += rectangle.Width*rectangle.Height;
        }
        else
        {
            Circle circle = (Circle)shape;
            area += circle.Radius * circle.Radius * Math.PI;
        }
    }
 
    return area;
}
 
public class AreaCalculator
{
    public double Area(Rectangle[] shapes)
    {
        double area = 0;
        foreach (var shape in shapes)
        {
            area += shape.Width*shape.Height;
        }
 
        return area;
    }
}

这个程序不遵循OSP因为Area()开放,以延伸和永远只能处理RectangleCircle形状。如果我们要添加的支持Triangle,我们不得不修改方法,所以它不是封闭的,以修改。

我们可以通过添加Shape所有类型的形状都继承的抽象类来实现OSP 。

public abstract class Shape
{
    public abstract double Area();
}
 
public class Rectangle : Shape
{
    public double Width { get; set; }
    public double Height { get; set; }
    public override double Area()
    {
        return Width*Height;
    }
}
 
public class Circle : Shape
{
    public double Radius { get; set; }
    public override double Area()
    {
        return Radius*Radius*Math.PI;
    }
}
 
public double Area(Shape[] shapes)
{
    double area = 0;
    foreach (var shape in shapes)
    {
        area += shape.Area();
    }
 
    return area;
}

现在,每个形状的子类型都可以通过多态处理自己的面积计算。这Shape可以扩展类的扩展范围,因为可以通过自己的面积计算轻松添加新形状而不会出错。

此外,程序中没有任何东西可以修改原始形状,将来也不需要修改。结果,该程序现在达到了OCP原则。

继续学习C#中的OOP

不要仅仅停留在SOLID。掌握C#中的所有OOP基础知识。

Educative的互动式,基于文本的课程可让每个C#面试官都在寻求技能的持续学习。

学习C#中的面向对象编程

L:李斯科夫替代原理

LSP:无缝替换现有代码中的B类

“程序中的对象应该可以用其子类型的实例替换,而不会改变程序的正确性。”

里氏替换原则(LSP)是由里氏巴巴拉和周以真创建的子类型关系的具体定义。该原理指出,任何类都必须可以被其任何子类直接替换而没有错误。

换句话说,每个子类都必须维护基类的所有行为以及该子类独有的任何新行为。子类必须能够处理与其父类相同的所有请求并完成所有相同的任务。

在实践中,程序员倾向于根据行为来开发类,并随着类变得更加具体而增强行为能力。LSP的优点在于,由于相同类型的所有子类共享一致的用法,因此它加快了新子类的开发。

您可以相信所有新创建的子类都可以使用现有代码。如果您决定需要一个新的子类,则可以在不修改现有代码的情况下创建它。

一些批评家认为,该原理并不适用于所有程序类型,因为没有实现的抽象超类型不能被设计用于实现的子类替代。

实作

LSP的大多数实现都涉及多态性,以为相同的调用创建特定于类的行为。为了演示LSP原理,让我们看看如何更改水果分类程序以实现LSP。

此示例不遵循LSP:

namespace SOLID_PRINCIPLES.LSP
{
    class Program
    {
        static void Main(string[] args)
        {
            Apple apple = new Orange();
            Debug.WriteLine(apple.GetColor());
        }
    }
    public class Apple
    {
        public virtual string GetColor()
        {
            return "Red";
        }
    }
    public class Orange : Apple
    {
        public override string GetColor()
        {
            return "Orange";
        }
    }
}

这不遵循LSP,因为在不更改程序输出的情况下,Orange该类无法替换Apple该类。该GetColor()方法被Orange类覆盖,因此将返回一个苹果为橙色的信息。

为了改变这种情况,我们将添加一个抽象类Fruit,这两个AppleOrange将实施。

namespace SOLID_PRINCIPLES.LSP
{
    class Program
    {
        static void Main(string[] args)
        {
            Fruit fruit = new Orange();
            Debug.WriteLine(fruit.GetColor());
            fruit = new Apple();
            Debug.WriteLine(fruit.GetColor());
        }
    }
    public abstract class Fruit
    {
        public abstract string GetColor();
    }
    public class Apple : Fruit
    {
        public override string GetColor()
        {
            return "Red";
        }
    }
    public class Orange : Fruit
    {
        public override string GetColor()
        {
            return "Orange";
        }
    }
}

现在,由于的特定于类的行为,该类的任何子类型(AppleOrangeFruit都可以被其他子类型替换而不会出错GetColor()。结果,该程序现在实现了LSP原理。

I:接口隔离原理

ISP:仅继承适用的方法

“许多特定于客户端的接口比一个通用接口要好。”

界面偏析原理(ISP)要求类只能够执行与实现其高端功能有用的行为。换句话说,类不包括不使用的行为。

这与我们的第一个SOLID原则有关,因为这两个原则共同消除了所有不直接影响其作用的变量,方法或行为。方法必须整体上有助于最终目标。

该方法的任何未使用部分均应删除或拆分为单独的方法。

ISP的优点是它将大型方法拆分为较小的,更具体的方法。由于以下三个原因,这使得程序更易于调试:

  1. 类之间携带的代码更少。更少的代码意味着更少的错误。
  2. 单一方法负责较小的行为。如果行为存在问题,则只需查看较小的方法。
  3. 如果将具有多种行为的通用方法传递给不支持所有行为的类(例如,调用该类不具有的属性),则如果该类尝试使用不受支持的行为,则会出现错误。

实作

要查看ISP原理在代码中的外观,让我们看看在遵循和不遵循ISP原理的情况下程序如何变化。

首先,不遵循ISP的程序:

// Not following the Interface Segregation Principle  
  
  public interface IWorker  
  {  
      string ID { get; set; }  
      string Name { get; set; }  
      string Email { get; set; }  
      float MonthlySalary { get; set; }  
      float OtherBenefits { get; set; }  
      float HourlyRate { get; set; }  
      float HoursInMonth { get; set; }  
      float CalculateNetSalary();  
      float CalculateWorkedSalary();  
  }  
  
  public class FullTimeEmployee : IWorker  
  {  
      public string ID { get; set; }  
      public string Name { get; set; }  
      public string Email { get; set; }  
      public float MonthlySalary { get; set; }  
      public float OtherBenefits { get; set; }  
      public float HourlyRate { get; set; }  
      public float HoursInMonth { get; set; }  
      public float CalculateNetSalary() => MonthlySalary + OtherBenefits;  
      public float CalculateWorkedSalary() => throw new NotImplementedException();  
  }  
  
  public class ContractEmployee : IWorker  
  {  
      public string ID { get; set; }  
      public string Name { get; set; }  
      public string Email { get; set; }  
      public float MonthlySalary { get; set; }  
      public float OtherBenefits { get; set; }  
      public float HourlyRate { get; set; }  
      public float HoursInMonth { get; set; }  
      public float CalculateNetSalary() => throw new NotImplementedException();  
      public float CalculateWorkedSalary() => HourlyRate * HoursInMonth;  
  }

该程序不遵循ISP,因为FullTimeEmployee该类不需要该CalculateWorkedSalary()功能,并且ContractEmployee该类不需要CalculateNetSalary()

这些方法都无法促进这些类的目标。相反,之所以实现它们是因为它们是IWorker接口的派生类。

这是我们如何重构程序以遵循ISP原理的方法:

// Following the Interface Segregation Principle  
  
    public interface IBaseWorker  
    {  
        string ID { get; set; }  
        string Name { get; set; }  
        string Email { get; set; }  
         
         
    }  
  
    public interface IFullTimeWorkerSalary : IBaseWorker  
    {  
        float MonthlySalary { get; set; }  
        float OtherBenefits { get; set; }  
        float CalculateNetSalary();  
    }  
  
    public interface IContractWorkerSalary : IBaseWorker  
    {  
        float HourlyRate { get; set; }  
        float HoursInMonth { get; set; }  
        float CalculateWorkedSalary();  
    }  
  
    public class FullTimeEmployeeFixed : IFullTimeWorkerSalary  
    {  
        public string ID { get; set; }  
        public string Name { get; set; }  
        public string Email { get; set; }  
        public float MonthlySalary { get; set; }  
        public float OtherBenefits { get; set; }  
        public float CalculateNetSalary() => MonthlySalary + OtherBenefits;  
    }  
  
    public class ContractEmployeeFixed : IContractWorkerSalary  
    {  
        public string ID { get; set; }  
        public string Name { get; set; }  
        public string Email { get; set; }  
        public float HourlyRate { get; set; }  
        public float HoursInMonth { get; set; }  
        public float CalculateWorkedSalary() => HourlyRate * HoursInMonth;  
    }  

在此版本中,我们将常规接口IWorker分为一个基本接口,IBaseWorker两个子接口IFullTimeWorkerSalaryIContractWorkerSalary

常规界面包含所有工作人员共享的方法。子接口按工人类型(FullTime有薪或Contract按小时支付)拆分方法。

现在,我们的类可以为该类型的worker实现接口,以访问基类中的所有方法和属性以及特定于worker的接口。

现在,最终类仅包含可进一步实现其目标并因此实现ISP原理的方法和属性。

D:依赖倒置原则

DIP:对对象A和B的更改不会影响其他对象

“一个人应该依靠抽象,而不是具体的东西。”

依赖倒置原则(DIP)有两个部分:

  1. 高级模块不应依赖于低级模块。相反,两者都应依赖于抽象(接口)
  2. 抽象不应依赖细节。细节(如具体实现)应取决于抽象。

该原理的第一部分颠倒了传统的OOP软件设计。如果没有DIP,程序员通常会在构建程序时将高级(较少细节,更抽象)的组件显式地与低级(特定)的组件相连接,以完成任务。

DIP解耦高级组件和低级组件,而是将两者连接到抽象。高级别和低级别的组件仍然可以相互受益,但是其中一个更改不应直接破坏另一个。

DIP的这一部分的优势在于,解耦的程序需要较少的更改工作。程序中的依存关系网意味着单个更改可以影响许多单独的部分。

如果最小化依赖关系,则更改将更加本地化,​​并且需要较少的工作来查找所有受影响的组件。

第二部分可以被认为是“如果更改细节,则抽象不会受到影响”。抽象是程序的面向用户的部分。

详细信息是使用户可见程序行为的特定幕后实现。在DIP程序中,我们可以全面检查该程序如何在用户不知情的情况下实现其行为的幕后实现。

此过程称为重构

这意味着您不必对接口进行硬编码即可仅使用当前的细节(实现)。这使我们的代码保持松散耦合,并允许我们灵活地重构我们的实现。

实作

我们将创建一个具有接口,高级,低级和详细组件的常规业务程序。

首先,让我们使用该getCustomerName()方法创建一个接口。这将面对我们的用户。

 
public interface ICustomerDataAccess
{
    string GetCustomerName(int id);
}

现在,我们将实现取决于ICustomerDataAccess接口的细节。这样做可以实现DIP原理的第二部分。

public class CustomerDataAccess: ICustomerDataAccess
{
    public CustomerDataAccess() {
    }
 
    public string GetCustomerName(int id) {
        return "Dummy Customer Name";        
    }
}

现在,我们将创建一个工厂类,该类实现抽象接口ICustomerDataAccess并以可用形式返回它。返回的CustomerDataAccess类是我们的底层组件。

public class DataAccessFactory
{
    public static ICustomerDataAccess GetCustomerDataAccessObj() 
    {
        return new CustomerDataAccess();
    }
}

最后,我们将实现CustomerBuisnessLogic还实现interface的高级组件ICustomerDataAccess。注意,我们的高级组件不会实现我们的低级组件,而只会使用它。

 
public class CustomerBusinessLogic
{
    ICustomerDataAccess _custDataAccess;
 
    public CustomerBusinessLogic()
    {
        _custDataAccess = DataAccessFactory.GetCustomerDataAccessObj();
    }
 
    public string GetCustomerName(int id)
    {
        return _custDataAccess.GetCustomerName(id);
    }
}

这是代码和可视图表中的完整程序:

public interface ICustomerDataAccess
{
    string GetCustomerName(int id);
}
 
public class CustomerDataAccess: ICustomerDataAccess
{
    public CustomerDataAccess() {
    }
 
    public string GetCustomerName(int id) {
        return "Dummy Customer Name";        
    }
}
 
public class DataAccessFactory
{
    public static ICustomerDataAccess GetCustomerDataAccessObj() 
    {
        return new CustomerDataAccess();
    }
}
 
public class CustomerBusinessLogic
{
    ICustomerDataAccess _custDataAccess;
 
    public CustomerBusinessLogic()
    {
        _custDataAccess = DataAccessFactory.GetCustomerDataAccessObj();
    }
 
    public string GetCustomerName(int id)
    {
        return _custDataAccess.GetCustomerName(id);
    }
}

数据访问程序的可视化图表

接下来要学什么

SOLID原理是改善代码并简化修改的绝佳方法。如果您刚入门,可能很难在一个程序中全部实现它们,因此,请一次专注于一个。最终,您将按习惯编写SOLID程序。

但是,SOLID仅是成为ace面向对象开发人员的一步。接下来,您应该习惯使用OOP四个组件:

  • 多态性
  • 抽象化
  • 遗产
  • 封装形式
免责声明:
1. 本站资源转自互联网,源码资源分享仅供交流学习,下载后切勿用于商业用途,否则开发者追究责任与本站无关!
2. 本站使用「署名 4.0 国际」创作协议,可自由转载、引用,但需署名原版权作者且注明文章出处
3. 未登录无法下载,登录使用金币下载所有资源。
IT小站 » C#中面向对象编程的SOLID原理

常见问题FAQ

没有金币/金币不足 怎么办?
本站已开通每日签到送金币,每日签到赠送五枚金币,金币可累积。
所有资源普通会员都能下载吗?
本站所有资源普通会员都可以下载,需要消耗金币下载的白金会员资源,通过每日签到,即可获取免费金币,金币可累积使用。

发表评论