使用C#实例解释SOLID原则

文章原址:

http://www.codeproject.com/Articles/703634/SOLID-architecture-principles-using-simple-Csharp


翻译一下,希望对大家有所帮助。


Contents

内容

Introduction介绍

I know there are 1000’s of articles on this subject and every month 10 new articles around SOLID would be injected more. My goal of writing this article is to understand SOLID with simple C# examples.

Any improvements on this article comment box is open below.

我知道现在已经有1000多篇的文章关于SOLID的了,并且每个月又增加十多篇。这篇文章的目的是用比较简单的C#例子,来解释SOLID原则。

What is SOLID?什么是SOLID

SOLID are five basic principles whichhelp to create good software architecture. SOLID is an acronym where:-

SOLD是五个基本原则,来帮助创建新的软件架构。SOLID是下面的缩写:

  • S stands for SRP (Single responsibility principle
  • S指单一责任原则
  • O stands for OCP (Open closed principle)
  • O指开放关闭原则
  • L stands for LSP (Liskov substitution principle)
  • L指L式替换原则
  • I stands for ISP ( Interface segregation principle)
  • I指接口隔离原则
  • D stands for DIP ( Dependency inversion principle)
  • D指依赖反转原则

So let’s start understanding each principle with simple c# examples.

让我们开始用简单的C#例子来理解每个原则

Understanding “S”- SRP (Single responsibility principle)

理解“S”-单一责任原则

The best way to understand SOLID is by understanding what problem it tries to solve. Have a look at the code below, can you guess what the problem is ?( You are not going to get BEER to guess it J , because it’s too simple).

OK, let me give a HINT look at the catch block code.

理解SOLID的最好方式,是理解它试图解决什么问题。看看下面的代码,你能猜出是什么问题吗?(你能猜出来也不会奖励啤酒的,因为太简单了)。

好吧,我给个提示,看看catch块。

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

The above customer class is doing things WHICH HE IS NOT SUPPOSED TO DO. Customer class should do customer datavalidations, call the customer data access layer etc , but if you see the catch block closely it also doing LOGGING activity. In simple words its over loaded with lot of responsibility.

So tomorrow if add a new logger like event viewer I need to go and change the “Customer”class, that’s very ODD.

It’s like if “JOHN” has a problem why do I need to check “BOB”.

上面的customer类,正在做一些它不应该做的事情。customer类应该检查customer数据的合理性,调用数据访问层,但是如果你看catch块,你会发现它在做日志记录。简单说它承载了很多工作。

所以如果明天增加了一个新的日志,例如event viewer。那么我需要修改customer类。这非常奇怪。

就像是“JOHN”出问题了,我需要去检查“BOB”

This also reminds me of the famous swiss knife. If one of them needs to be changed the whole set needs to be disturbed. No offense I am great fan of swiss knifes.

这让我想起了著名的瑞士军刀,如果其中的一个出问题了,那么其他的都会受到影响。这里没有冒犯瑞士军刀的意思,我还是它的大粉丝呢。

But if we can have each of those items separated its simple, easy to maintain and one change does not affect the other. The same principle also applies to classes and objects in software architecture.

如果我们能分离上述功能,让每个都比较简单。就会容易维护,修改其中一个,也不会影响其他的。同样的原则适用软件架构中的类和对象设计。

So SRP says that a class should have only one responsibility and not multiple.So if we apply SRP we can move that logging activity to some other class who will only look after logging activities.

所以SRP告诉我们一个类应该只有一个责任,而不是多个。如果我们应该SRP原则,我们应该把日志挪到其他类中,那个类只处理日志活动。

class FileLogger
    {
        public void Handle(string error)
        {
            System.IO.File.WriteAllText(@"c:\Error.txt", error);
        }
    }

Now customer class can happily delegate the logging activity to the “FileLogger” class and he can concentrate on customer related activities.

现在customer类可以引用日志活动类FileLogger,然后自己集中做customer相关的活动了。

class Customer
    {
        private FileLogger obj = new FileLogger();
        publicvirtual void Add()
        {
            try
            {
                // Database code goes here
            }
            catch (Exception ex)
            {
                obj.Handle(ex.ToString());
            }
        }
    }

Now architecture thought process is an evolution. For some people who are seniors looking at above SRP example can contradict that even the try catch should not be handled by the customer class because that is not his work.

Yes, we can create a global error handler must be in theGlobal.asax file , assuming you are using ASP.NET and handle the errors in those section and make the customer class completely free.

So I will leave how far you can go and make this solution better but for now I want to keep this simple and let your thoughts have the freedom to take it to a great level.

现在架构师认为处理过程是渐进的。有些人自己看上面的SRP例子,可能争辩说甚至try,catch都不应该由customer类来处理,因为不是它的工作。

是的,我们可以创建一个全局的error处理器,必须放在Global.asax文件中。假设你正在使用ASP.NET,并且在这些地方处理error.使customer类完全解放出来。

所有,我把这个问题留给你,来确定为了让解决方案更好,更够走多远。但是现在我只是想把事情简单点,你来想想还有多少空间更上一层楼。

Below is a great comment which talks about how we can take this SRP example to the next level.
http://www.codeproject.com/Articles/703634/SOLID-architecture-principles-using-simple-Csharp?msg=4729987#xx4729987xx


下面是一个很好的说明,来讨论SRP例子更进一步完善。

http://www.codeproject.com/Articles/703634/SOLID-architecture-principles-using-simple-Csharp?msg=4729987#xx4729987xx

Understanding “O” - Open closed principle

理解"O"-开放关闭原则

Let’s continue with our same customer class example. I have added a simple customer type property to the class. This property decided if this is a “Gold” ora “Silver” customer.

Depending on the same it calculates discount. Have a look at the “getDiscount” function which returns discount accordingly. 1 for Gold customer and 2 for Silver customer.

Guess, what’s the problem with the below code. Hahaha, looks like this article will make you a GUESS champion;-) .

Ok, also let me add a HINT, look at the “IF” condition in the “getDiscount” function.

让我们继续customer class的例子,我增加了一个简单的客户类型customer type属性。这个属性决定客户是金卡还是银卡客户。

基于这个属性,来计算折扣。让我们看看getDiscount这个函数,它返回折扣值。1指金卡客户,2指银卡客户。

猜猜下面的代码有啥问题?哈哈,读这个文章能让你的猜谜大奖。

好吧,给个提示,看看getDiscount函数中的IF条件。

class Customer
{
        private int _CustType;

        public int CustType
        {
            get { return _CustType; }
            set { _CustType = value; }
        }

        public double getDiscount(double TotalSales)
        {
                if (_CustType == 1)
                {
                    return TotalSales - 100;
                }
                else
                {
                    return TotalSales - 50;
                }
        }
}

The problem is if we add a new customer type we need to go and add one more “IF” condition in the “getDiscount” function, in other words we need to change the customer class.

If we are changing the customer class again and again, we need to ensure that the previous conditions with new one’s are tested again , existing client’s which are referencing this class are working properly as before.

In other words we are “MODIFYING” the current customer code for every change and every time we modify we need to ensure that all the previous functionalities and connected client are working as before.

How about rather than “MODIFYING” we go for “EXTENSION”. In other words every time a new customer type needs to be added we create a new class as shown in the below. So whatever is the current code they are untouched and we just need to test and check the new classes.

问题在于如果我们增加了新的客户类型,我们需要在getDiscount函数中增加一个新的IF条件,也就是说,我们需要修改customer类。

如果我们一再的修改customer类,我们就需要确保原来的条件加上新条件工作还正常,现存的客户,他们正在引用当前的这个类也和以前一样工作正常。

不如我们不‘修改’,而是‘扩展’,换句话说每次增加一个新的客户类型,我们增加一个新的类。这样现有的代码没动,我们只需要测试和检查新的类就可以了。

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

Putting in simple words the “Customer” class is now closed for any new modification but it’s open for extensions when new customer types are added to the project.

简单说,现在Customer类对修改关闭了,但是欢迎扩展,如果出现了新的客户类型。

【私下说,我觉得这个例子不好,实际上他还是修改了原类,把getDiscout在基类中设置为所谓没有折扣,而子类各自有type折扣。但是如果真是原来只有金卡银卡,后面加上了铜卡石头卡,基类的IF--ELSE咋写?如果基类不变,生成铜卡子类,万一使用者是个铜卡,但是他不知道有铜卡的子类,然后直接定义了基类对象,调用基类的getDiscount来计算铜卡折扣,咋办?】

Understanding “L”- LSP (Liskov substitution principle)

理解L氏替换

Let’s continue with the same customer. Let’s say our system wants to calculate discounts for Enquiries. Now Enquiries are not actual customer’s they are just leads. Because they are just leads we do not want to save them to database for now.

So we create a new class called as Enquiry which inherits from the “Customer” class. We provide some discounts to the enquiry so that they can be converted to actual customers and we override the “Add’ method with an exception so that no one can add an Enquiry to the database.

让我们继续customer的例子。我们说系统打算计算中间人的折扣。中间人不是实际的客户,他们只是搭桥人。因为他们只是搭桥人,所以我们不打算把他们保存到数据库中。

所以我们现在建了一个类,叫Enquiry,继承于customer类。我们提供折扣给中间人,这样他们可以把折扣告诉实际客户。我们重写了Add方法,抛出一个异常。这样就没人可以把中间人存到DB中了。

class Enquiry : Customer
    {
        public override double getDiscount(double TotalSales)
        {
            return base.getDiscount(TotalSales) - 5;
        }

        public override void Add()
        {
            throw new Exception("Not allowed");
        }
    }

If you visualize the current customer inheritance hierarchy it looks something as shown below. In other word “Customer” is the parent class with “Gold” , “Silver” and “Enquiry” as child classes.

如果我们想象目前的customer继承层次,应该如下图。customer是基类。Gold,Silver和Enquiry是子类。

So as per polymorphism rule my parent “Customer” class object can point to any of it child class objects i.e. “Gold”, “Silver” or “Enquiry” during runtime without any issues.

So for instance in the below code you can see I have created a list collection of “Customer” and thanks to polymorphism I can add “Silver” , “Gold” and “Enquiry” customer to the “Customer” collection without any issues.

Thanks to polymorphism I can also browse the “Customer” list using the parent customer object and invoke the “Add” method as shown in the below code.

Now again let me tickle your brains, there is a slight problem here, THINK, THINK THINK.

HINT: -Watch when the Enquiry object is browsed and invoked in the “FOR EACH” loop.

 基于多态性的原则,父类对象可以指向它的任何子类对象。因此customer类对象,可以指向Gold,Silver或者Enquiry.在运行时应该没有任何问题。

所以如下代码,我创建了一个List,是customer的对象。拜多态性所赐,我可以增加Silver,Gold和Enquiry客户到这个Collection。这样做没有任何问题。

再次感谢多态性,我们也可以查看customer list,使用父类customer的对象,然后调用Add方法。

现在敲敲脑袋,这里有个小问题。。。

提示:看看在FOR EACH循环中当Enquiry对象被查看,并且调用的时候会咋样?

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

As per the inheritance hierarchy the “Customer” object can point to any one of its child objects and we do not expect any unusual behavior.

But when “Add” method of the “Enquiry” object is invoked it leads to below error because our “Equiry” object does save enquiries to database as they are not actual customers.

根据继承层次,父类customer对象能够指向它的任何一个子类对象。我们不期望这里有任何的异常行为。

但是当Enquiry的Add方法被调用时,会导致一个error,因为Enquiry对象不会保存Enquiry到DB。

Now read the below paragraph properly to understand the problem. If you do not understand the below paragraph read it twiceJ..

In other words the “Enquiry” has discount calculation , it looks like a “Customer” but IT IS NOT A CUSTOMER. So the parent cannot replace the child object seamlessly. In other words “Customer” is not the actual parent for the “Enquiry”class. “Enquiry” is a different entity altogether. 

现在读下面的段落来理解问题。如果不明白,可以读两遍。

说白了,Enquiry有Discount计算,它看起来像Customer。但是它不是一个custome。所以父类的对象,不能无缝的替换子类的对象。也就是说,customer不是Enquiry的真正的父亲。Enquiry是个完全不同的实体。

So LISKOV principle says the parent should easily replace the child object. So to implement LISKOV we need to create two interfaces one is for discount and other for database as shown below.

所以L氏原则说的是父类对象应该能够很容易的替换子类对象。因为为了实现L氏原则,我们需要创建两个接口一个用于折扣,一个用于DB。

【擦。。,重构了。这得重构多少次项目啊,有完没完。。。,鬼知道啥时候Enquiry冒出来,没准项目都快上线了客户才说】

interface IDiscount
{
        double getDiscount(double TotalSales);
}


interface IDatabase
{
        void Add();
}

Now the “Enquiry” class will only implement “IDiscount” as he not interested in the “Add” method.

现在Enquiry类只实现IDiscount接口,不实现Add方法。

class Enquiry : IDiscount
    {
        public  double getDiscount(double TotalSales)
        {
            return TotalSales - 5;
        }
    }

While the “Customer” class will implement both “IDiscount” as well as “IDatabase” as it also wants to persist the customer to the database.

而customer类既实现IDiscount又实现IDatabase,因为它需要存customer信息到DB.

【我咋那么烦持久化这个说法呢,把本来简单的事情,说得像干了个大事儿一样】

class Customer : IDiscount, IDatabase
   {


       private MyException obj = new MyException();
       public virtual void Add()
       {
           try
           {
               // Database code goes here
           }
           catch (Exception ex)
           {
               obj.Handle(ex.Message.ToString());
           }
       }

       public virtual double getDiscount(double TotalSales)
       {
           return TotalSales;
       }
   }

Now there is no confusion, we can create a list of “Idatabase” interface and add the relevant classes to it. In case we make a mistake of adding “Enquiry” class to the list compiler would complain as shown in the below code snippet.

现在没有歧义了,我们创建一个IDatabase接口类List对象。然后把相关的class加上去。这样如果Enquiry被加上去了,那么编译就出错了。

 

Understanding “I” - ISP (Interface Segregation principle)

理解“I”-接口分离原则

Now assume that our customer class has become a SUPER HIT component and it’s consumed across 1000 clients and they are very happy using the customer class.

现在假设我们的customer类变成了一个超级热销组件,被超过1000个客户使用。大家都很开心。

 

Now let’s say some new clients come up with a demand saying that we also want a method which will help us to “Read” customer data. So developers who are highly enthusiastic would like to change the “IDatabase” interfaceas shown below.

But by doing so we have done something terrible, can you guess ? 

HINT: - Think about the effect of this change on the above image.

现在我们有个新客户,提了一个需求,说想加一个方法,帮他读客户数据。所以开发人员就信心满满的打算修改IDatabase接口了。像下面这样改。

但是如果这样做,我们可能就把自己陷进去了,知道咋回事吗?

提示:想想上面那张图会受啥影响?

【但是,如果这个需求是上面的client2提出的咋办?或者说是client2,3,4提出的,但是client1,5说不用。咋办?】

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

If you visualize the new requirement which has come up, you have two kinds of client’s: -

  • Who want’s just use “Add” method.
  • The other who wants to use “Add” + “Read”.

Now by changing the current interface you are doing an awful thing, disturbing the 1000 satisfied current client’s , even when they are not interested in the “Read” method. You are forcing them to use the “Read” method.

So a better approach would be to keep existing clients in their own sweet world and the serve the new client’s separately.

So the better solution would be to create a new interface rather than updating the current interface. So we can keep the current interface “IDatabase” as it is and add a new interface “IDatabaseV1” with the “Read” method the “V1” stands for version 1.

如果你设想一下这个新需求,我们现在有两类客户:

1. 那些只用Add的客户

2.那些Add和Read都用的客户

如果修改当前运行的挺好的现有接口,会影响1000个已经很满意的客户。即使他们不打算使用新功能Read。你也强迫他们使用Read方法。

所以一个更好的方式,是保持现有的客户,继续享受他们的安乐窝。然后分别的去服务那些新的客户。【想想后期维护的代价吧,我们到底有多少个版本?】

所以更好的解决方案是创建一个新的接口,而不是更新现有的接口。我们可以保持现有的IDatabase接口,然后增加一个新接口IDatabaseV1,带Read方法。

interface IDatabaseV1 : IDatabase // Gets the Add method
{
Void Read();
}

You can now create fresh classes which implement “Read” method and satisfy demands of your new clients and your old clients stay untouched and happy with the old interface which does not have “Read” method.

这样你可以创建全新的类实现Read方法,来满足新客户的需求,而旧的客户,还用老接口。

class CustomerwithRead : IDatabase, IDatabaseV1
    {

public void Add()
{
	Customer obj = new Customer();
Obj.Add();
}
	Public void Read()
	{
	// Implements  logic for read
}
    }

So the old clients will continue using the “IDatabase” interface while new client can use “IDatabaseV1” interface.

所以老客户用旧的IDatabase接口,新客户用IDatabaseV1接口。

IDatabase i = new Customer(); // 1000 happy old clients not touched
i.Add();

IDatabaseV1 iv1 = new CustomerWithread(); // new clients
Iv1.Read();

Understanding “D”- Dependency inversion principle

理解"D"-依赖反转原则

In our customer class if you remember we had created a logger class to satisfy SRP. Down the line let’s say new Logger flavor classes are created.

在我们的customer类,你应该记得创建了一个logger类用于满足SRP原则。下面的代码创建了一个新口味的logger类。

class Customer
    {
        private FileLogger obj = new FileLogger();
        public virtual void Add()
        {
            try
            {
                // Database code goes here
            }
            catch (Exception ex)
            {
                obj.Handle(ex.ToString());
            }
        }
    }

Just to control things we create a common interface and using this common interface new logger flavors will be created.

为了可控,我们创建了一个通用接口,基于这个接口,创建新口味的logger。

interface ILogger
{
        void Handle(string error);
}

Below are three logger flavors and more can be added down the line.

下面是三种口味的logger,可能增加更多种。【欢迎品尝】

class FileLogger : ILogger
    {
        public void Handle(string error)
        {
            System.IO.File.WriteAllText(@"c:\Error.txt", error);
        }
    }
class EverViewerLogger : ILogger
    {
        public void Handle(string error)
        {
            // log errors to event viewer
        }
    }
class EmailLogger : ILogger
  {
      public void Handle(string error)
      {
          // send errors in email
      }
  }

Now depending on configuration settings different logger classes will used at given moment. So to achieve the same we have kept a simple IF condition which decides which logger class to be used, see the below code.

QUIZ time, what is the problem here.

HINT: - Watch the CATCH block code.

现在基于配置设置,同一时刻可能某个特定的logger被使用。为了实现调用不同的logger,我们用IF条件决定那个logger被使用。代码如下:

问题时间来了:这里有啥问题?

提示:看看catch块。

class Customer : IDiscount, IDatabase
    {
        private IException obj; 

public virtual void Add(int Exhandle)
        {
            try
            {
                // Database code goes here
            }
            catch (Exception ex)
            {
                if (Exhandle == 1)
                {
                    obj = new MyException();
                }
                else
                {
                    obj = new EmailException();
                }
                obj.Handle(ex.Message.ToString());
            }
        }

The above code is again violating SRP but this time the aspect is different ,its about deciding which objects should be created. Now it’s not the work of “Customer” object to decide which instances to be created , he should be concentrating only on Customer class related functionalities.

If you watch closely the biggest problem is the “NEW” keyword. He is taking extra responsibilities of which object needs to be created.

So if we INVERT / DELEGATE this responsibility to someone else rather the customer class doing it that would really solve the problem to a certain extent.

上面的代码,又违反了SRP原则。但是这次只不过方向不同。至此是决定那个对象应该被创建。这个不是customer类的工作。它应该专注于customer的工作。

如果再仔细看看,大问题是New关键字。它在承担决定哪个对象应该被创建的责任。

所以,如果我们反转/代表这个责任给个替死鬼,而不是用customer类做这个事儿,那么应该能在一定程度上解决这个问题。

 

So here’s the modified code with INVERSION implemented. We have opened the constructor mouth and we expect someone else to pass the object rather than the customer class doing it. So now it’s the responsibility of the client who is consuming the customer object to decide which Logger class to inject.

这里我们修改代码,来实现反转。我们开放了一个构造函数,希望有人能够传这个日志对象进来,而不是customer自己创建。所以现在是客户自己来决定调用(注入)哪个Logger类。

class Customer : IDiscount, IDatabase
 {
        private Ilogger obj;
        public Customer(ILogger i)
        {
            obj = i;
        }
}

So now the client will inject the Logger object and the customer object is now free from those IF condition which decide which logger class to inject. This is the Last principle in SOLID Dependency Inversion principle.

Customer class has delegated the dependent object creation to client consuming it thus making the customer class concentrate on his work.

这样现在客户自己注入Logger对象,这样customer对象就自由了。不用决定那个logger要使用了。

这是最后一个SOLID原则,依赖反转原则。

IDatabase i = new Customer(new EmailLogger());

Revising SOLID principles

S stands for SRP (Single responsibility principle):- A class should take care of only one responsibility.

一个类应该只关注一个责任。

O stands for OCP (Open closed principle):- Extension should be preferred over modification.

欢迎扩展而不是修改

L stands for LSP (Liskov substitution principle):- A parent class object should be able to refer child objects seamlessly during runtime polymorphism.

父类对象在运行时应该可以无缝指向子类对象【张三是人,王五也是人。把张三和王五插入到人的列表中多正常啊】

I stands for ISP (Interface segregation principle):- Client should not be forced to use a interface if it does not need it.

客户不应该被强迫使用一个它不需要的接口。

D stands for DIP (Dependency inversion principle) :- High level modules should not depend on low level modules but should depend on abstraction.

应该依赖于抽象,而不是对象。因此通过依赖注入来决定动态调用哪个实际的对象。

If you are already done with this article the next logical steps would be going through GOF Design Patterns, here’s an article for the same, Hope you enjoy it.

如果已经理解了这篇文章,那么下一步应该看看GOF设计模式。



  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值