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

使用简单的C#示例的SOLID架构原则

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.69/5 (13投票s)

2017年7月30日

CPOL

5分钟阅读

viewsIcon

20573

这是“使用简单的C#示例的SOLID架构原则”的替代方案

引言

我读了 CodeProject 上 Shivprasad Koirala 关于 SOLID 的文章 (https://codeproject.org.cn/Articles/703634/SOLID-architecture-principles-using-simple-Csharp),我想做一些改进。我喜欢 Shivprasad 保持简单愚蠢的想法,所以我也会使用他的代码示例。为了更好地理解我的文章,请先阅读他的文章。

什么是 SOLID?

定义很简单。它是一个首字母缩略词,其中:-

  • S 代表 SRP (单一职责原则)
  • O 代表 OCP (开闭原则)
  • L 代表 LSP (里氏替换原则)
  • I 代表 ISP (接口隔离原则)
  • D 代表 DIP (依赖倒置原则)

 

但我们为什么需要它?因为业务在不断变化,软件也需要随之变化以服务于业务。它能帮助你减少错误,提高灵活性,并在进行更改时付出更少的努力。我将在每个示例后进一步解释。

理解“S” - SRP (单一职责原则)

好的,让我们来看看 Shivprasad 的例子。

class Customer
    {
        public void Add()
        {
            try
            {
                // Database code goes here
            }
            catch (Exception ex)
            {
                System.IO.File.WriteAllText(@"c:\Error.txt", ex.ToString());
            }
        }
    }

Shivprasad 说“上面的客户类正在做它**不应该做的事情**”,我完全同意。然而,对于新程序员来说,什么该做什么不该做并不十分清楚。我以前也写过这样的代码,至少在我上大学以及之后的一段时间里。这类代码对我来说很直接,也很自然,因为它能完成工作。为了编写遵循 SRP 的代码,你必须改变你的思维方式。不再是按照“逻辑”编码,而是按照“职责”(责任)编码。现在看看上面的代码,我们将深入研究它。

首先

            catch (Exception ex)
            {
                System.IO.File.WriteAllText(@"c:\Error.txt", ex.ToString());
            }

如果抛出异常,它会将异常记录到 C 盘的“Error.txt”文件中。如果你的程序中没有其他地方执行相同的操作,那么这段代码是可以的,不需要 SRP。然而,不太可能只有客户类在记录错误。在这种情况下,你必须在

                System.IO.File.WriteAllText(@"c:\Error.txt", ex.ToString());          

到处重复。然后有一天,由于某些原因(可能是安全原因),文件位置需要更改,例如更改为“d:\Error.txt”,那么我们就必须在代码中将所有“c:\Error.txt”更改为“d:\Error.txt”。但这存在人为错误的风险,例如拼写错误,如“f:\Error.txt”。这就是为什么我们必须重新测试所有内容,这会花费时间。所以,最好将所有执行日志记录的代码组合到一个类中,如果需要更改,我们只需在一个地方进行。那个类负责日志记录,任何需要记录内容的类都将引用该日志类,可能通过类的构造函数进行实例化。这意味着我们的代码将变成

class FileLogger
    {
        public void Log(string error)
        {
            System.IO.File.WriteAllText(@"c:\Error.txt", error);
        }
    }
class Customer
    {
       private readonly FileLogger _logger;
       public Customer(FileLogger logger)
        {
         _logger = logger;
        }

       public void Add()
        {
            try
            {
                // Database code goes here
            }
            catch (Exception ex)
            {
                _logger.Log(ex.ToString());
            }
        }
    }

现在,另一个类,例如 Product,需要使用日志,这很简单。

class Product
    {
       private readonly FileLogger _logger;
       public Product(FileLogger logger)
        {
         _logger = logger;
        }
       public void DeleteProduct()
        {
                // Deleteing code goes here
                _logger.Log(“Successful deleted”);
        }
    }

现在你可以看到将事物视为职责(责任)的好处了。回到我们第一个例子,该类有 3 个职责:

  1. 添加
  2. 日志记录
  3. 错误处理:“try catch”块

现在我们的代码改进如下,每个类都负责自己的职责。

class Customer
    {
       public void Add()
        {
                // Database code goes here
        }
    }

class FileLogger
    {
        public void Log(string error)
        {
            System.IO.File.WriteAllText(@"c:\Error.txt", error);
        }
    }
class SimpleErrorHandler
    {
       public delegate void InpuFunction();
       private readonly InpuFunction _inputFunction;
       private readonly FileLogger _logger;    
       public SimpleErrorHandler(InpuFunction inputfunction, FileLogger logger)
        {
            _inputFunction = inputFunction;
            _logger = logger;
        }
       public void ErrorHandle()
        {
            try
            {
                _inputFunction();
            }
            catch (Exception ex)
            {
                _logger.Log(ex.ToString());
            }
        }
    }

现在我们将它们组合在一起。

var customer = new Customer();
var fileLogger = new FileLogger();
var customerWithErrorHandling = new SimpleErrorHandler(customer.Add,fileLogger);
customerWithErrorHandling.ErrorHandle();

如果我们有更多非托管代码,SimpleErrorHandler 可以轻松处理。

class FileRead
    {
       public void ReadFile()
        {
                // file reading code goes here
        }
    }
var fileRead = new FileRead();
var fileLogger = new FileLogger();
var customerWithErrorHandling = new SimpleErrorHandler(fileRead.ReadFile,fileLogger);
customerWithErrorHandling.ErrorHandle();

理解“O” - 开闭原则

对扩展开放,对修改关闭。通常需要花费大量时间才能使代码正确且无 bug,因此最好进行扩展而不是修改。这是 Shivprasad 的例子。

class Customer
{
        public virtual double getDiscount(double TotalSales)
        {
            return TotalSales;
        }
}
class SilverCustomer : Customer
    {
        public override double getDiscount(double TotalSales)
        {
            return base.getDiscount(TotalSales) - 50;
        }
    }
class goldCustomer : SilverCustomer
    {
        public override double getDiscount(double TotalSales)
        {
            return base.getDiscount(TotalSales) - 100;
        }
    }

我不喜欢这段代码,尽管它遵循了开闭原则。因为除了 OCP 之外,还有其他原则,例如:*优先组合而非继承*或*不要重复自己*。这里我们可以看到 getDiscount 的逻辑被重复了。假设我们有一个不同的 bronzeCustomer 类,但它与 silverCustomer 拥有相同的折扣,那么我们将有两个完全相同的 getDiscount 函数。现在让我们看看接口和组合的力量。

 

interface IDiscount
{
        double getDiscount(double TotalSales);
}

class Customer
{
        private readonly IDiscount _discount;
        private readonly string _customerClass;
        public string CustomerClass { get { return _customerClass; } }
        public Customer(string customerClass, IDiscount discount)
        {
            _customerClass = customerClass;
             _discount = discount;
        }
        public double getDiscount(double TotalSales)
        {
            return _discount.getDiscount(TotalSales);
        }
}
class GetFixedDiscount : IDiscount
{
       private readonly double _fixednumber;
       public GetFixedDiscount(double fixednumber)
       {
           _fixednumber = fixednumber;
       }
       public double getDiscount(double TotalSales)
       {
           return TotalSales - _fixednumber;
       }
}

gold、silver 和 bronze 客户的 getDiscount 函数不再重复。

var getFixed100Discount = new GetFixedDiscount(100);
var getFixed50Discount = new GetFixedDiscount(50);
var goldCustomer = new Customer(“Gold Class”, getFixed100Discount);
var silverCustomer = new Customer(“Silver Class”, getFixed50Discount);
var bronzeCustomer = new Customer(“Bronze Class”, getFixed100Discount);

理解“L” - LSP (里氏替换原则)

什么是 LSP?你可以通过谷歌轻松找到答案。

“程序中的对象应该能够被其子类的实例替换,而不会改变该程序的正确性。”

这只是对继承的良好设计。但我们为什么需要它?让我们来看看 Shivprasad 的例子。

class Enquiry : Customer
    {
        public override double getDiscount(double TotalSales)
        {
            return base.getDiscount(TotalSales) - 5;
        }
        public override void Add()
        {
            throw new Exception("Not allowed");
        }
    }

并且

List<Customer> Customers = new List<Customer>();
Customers.Add(new SilverCustomer());
Customers.Add(new goldCustomer());
Customers.Add(new Enquiry());
 foreach (Customer o in Customers)
 {
                o.Add();
 }
}

然后会抛出异常。然而,在实际生活中,你可以让 Add() 函数什么都不做来使其工作。我认为违反 LSP 对编写代码的人来说不是什么大问题,但对后来维护代码的人来说却是个问题。如果你是一名新入职的员工,你会被搞糊涂。有了那些空洞无意义的函数,你就不知道一个函数是否有实际意义。所以,你必须深入研究类中的每个函数,这会花费很多时间。

 

如果你从头开始构建软件,可以遵循 Shivprasad 的方法。如果你是维护者,从一开始就这样做不是个好主意,因为它需要大量的代码重构。与其创建一个客户的查询,你还可以简单地“为客户做一个查询”。

class Enquiry
{
       private readonly Customer _customer;
       public Enquiry(Customer customer)
       {
             _customer = customer;
       }
       public double getDiscount(double TotalSales)
       {
              return _customer.getDiscount(TotalSales);
       }
}

理解“I” - ISP (接口隔离原则)

基本上,它只是让你的接口“瘦身”,以便你的代码可以重用,而无需强制实现不必要的行为,这有利于代码重用。让我们来看看 Shivprasad 的例子。

interface IDatabase
{
        void Add(); // old client are happy with these.
        void Read(); // Added for new clients.
}

依我看,这是不现实的。事实上,接口在某种程度上是不可变的。如果你改变它们,你就会破坏所有实现它们的类,即使是最糟糕的程序员也不会这样做。在现实生活中,当维护别人的代码时,你会看到带有工作实现的“肥大”接口。

interface IDatabase
{
        void Add(); 
        void Read(); 
}

现在你需要一个只需要读取的数据库。解决方案是让你的类拥有对 IDatabase 的引用并使用它,但如果有两个接口而不是一个,那就更好了。

理解“D” - 依赖倒置原则

应该依赖于抽象而不是具体。它为你的代码增加了大量的灵活性。正如你在我的 ORP 部分的示例中看到的,我使用了

class Customer
{
        private readonly IDiscount _discount;
        public Customer(string customerClass, IDiscount discount)
        {
            _customerClass = customerClass;
            _discount = discount;
        }
}

这被称为“依赖注入”,是实现 DIP 的一种方式。你可以将实际的折扣类更改为模拟折扣类,以测试你代码的其他部分。

© . All rights reserved.