码农也要有原则 : SOLID via C#

简介: 让姑姑不再划拳 码农也要有原则 : SOLID via C#

 何为SOLID?

  S.O.L.I.D.是一组面对面向对象设计的最佳实践的设计原则。术语来自Robert C.Martin的著作Agile Principles, Patterns, and Practices in C#,代表了下面五个设计原则:

    1. SRP(Single Responsibility Principle) 单一责任原则,

    2. OCP(Open Closed Principle) 开放封闭原则,

    3. LSP(Liskov Substitution Principle) 里氏替换原则,

    4. ISP(Interface Segregation Principle) 接口分离原则,

    5. DIP(Dependency Inversion Principle) 依赖倒置原则,

  下面用C#例子来一一介绍。

 

 S:SRP, Single Responsibility Principle, 单一责任原则

  人类学习和理解最快的方式是实践,这点在编程上显得尤为突出。理解SOLID最好的方式就是先去了解它解决了什么问题。

  首先给大家出一道大家来找茬,下面这段代码中有一个很大的问题,你找到了吗?(停停停,不用去倒杯茶细细来看,因为这段代码已经简单到没朋友了)

  那我们现在就抛开和华生的基情,对这个"作案现场"来调查一番。

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

  相信大家都发现这个问题出在哪里了,一个顾客类竟然可以自主写log!Customer Class 应该是要做关于Customer Datavalidation或者访问顾客相关的数据库进行存储的相关操作,实现Log的记录实际上已经超出了其责任的范围。

  这就像小龙女不去做个安静的美姑姑而去学划拳和人斗酒一样,WTF!

  当明日需要你改造Log记录的实现或路径的时候而你却push了一段Customer类的改动,这会让人感到非常奇怪的。

  这也让我想起来了一个世界知名的工具-瑞士军刀。毫无疑问它很棒,但当你需要改动其中一个部分的时候其余部分要一起重新来排列保证不会互相干扰到。而且你可以尝试一个场景,用瑞士军刀掏耳朵,那种感觉真的是醉了。

  

  倒不如我们一一拆分,各司其职,剪子剪纸,耳勺掏耳,使部件功能简单化,互不影响。这个原则适用于软件架构中类和对象的设计。

  

  所以,简而言之,SRP就是指单个类应该有且仅有单个职能。所以我们可以对刚才案例朝这个目标进行初步改造,首先将记录log的逻辑在一个单独的FileLogger类上实现:

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

  现在Customer类可以欢快的抛弃“五魁首六六六”,FileLogger class 来负责记录log的具体实现,而customer class可以更专注的负责自己的模块。

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

  如果有一些SRP经验的朋友可能已经发现,其实这种解决方案并不能完全解决SRP的问题。因为try catch其实并不是Customer类需要关心的功能。

  在记录Log这一层,不同的语言和结构都会有一个类似Asp.Net中Global.asax或者WPF中App.xaml.cs这类文件可以集中来处理这些冒泡的错误,这样Customer类中便不会有TryCatch的方法。

  其实这个程序依然可以更好,也可以有更多的解决方案,但此文旨在使用足够简单的例子来用C#阐述SOLID,也希望可以不禁锢大家思维,有好的方案可以在下面回复和交流,来产出一个伟大的解决方案。

  在codeproject里有一个答案是很不错的,具体实现就不剧透了,如感兴趣,可以戳:http://www.codeproject.com/Articles/703634/SOLID-architecture-principles-using-simple-Csharp?msg=4729987#xx4729987xx

 

 O:OCP, Open Closed Principle 开放封闭原则

  上一个“场景”过了SRP阶段我们要继续开始OCP阶段了, OCP简单来说就是 对扩展是开放的,对修改是封闭的。

  在Customer类中我们现在添加一个属性来表示他是黄金用户还是银色用户。当CustType为1时为Gold用户,为2时为Silver用户.根据用户类型不同来返回不同的折扣。

  来继续来找茬了,这个节奏好像看起来大家看完本文后能在大家来找茬中无往不胜啊,haha。

  开启福尔摩斯模式,关注在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;
                }
        }
}

  “嫌疑人”出现了,当我们再添加一个用户类型时,我们还需要添加修改if中的折扣逻辑,也就是我们需要修改Customer Class。

  当我们一次次更改Customer Class,我们还需要确认之前的逻辑是没错的以及引用该Class的更多的逻辑也是没问题的,也就说需要一次又一次的测试。

  那么问题来了,挖掘...不对,是如何来避免多次的“Modify”而带来的恶果呢,那就是“EXTENSION”(扩展).

  当我们每次增加一个用户类型的时候,我们就增加一个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;
        }
    }

  这样也就解决了多次修改带来的问题,通过扩展基类,而不是修改。

  OCP原则 拥抱扩展,拒绝修改,保证了现有逻辑的稳定性。

  

  其实还有一张比较XXX的图来表示OCP,我这边就不镶嵌到文章里了,因为....好奇的小盆友可以戳戳,记得留言写下感悟...:戳我

 

 L: LSP Liskov Substitution Principle 里氏替换原则

  跨过前两个坎,现在我们来到了第三个原则,这次我们换一个模型。首先我们有一个Bird的Class,有一个Fly的方法:

class Bird
{
  public void Fly()  {    // Fly Logic  }  
}

  后来我们发现生物学上企鹅也属于鸟类,当然这只企鹅不在深圳,但它不会飞。

  

  于是我们设计一个Penguin的class 继承自Bird 重写其Fly方法,标明其不会飞

class Penguin: Bird
    {
        public override void Fly()
        {
            throw new Exception("Can Not Fly");
        }
    }

  眨眼一看是没有问题,但实际上问题可大了去了,于是,

  好吧,大家来找茬又开始了,这次的茬可是隐藏的很深。

  首先在程序开发中,你并不能保证你继承的父类重写了的方法是安全的,它里面可能包含其他逻辑。而且在后续的开发中,极容易出现下面的代码:

List<Bird> Birds = new List<Bird>();
// Add Bird Logic
foreact(var bird in Birds)
{
     bird.Fly();      
}

  这时企鹅君便要崩溃了。风险就在别人使用你设计的类的时候并没有想到并非所有的子类都符合父类的要求。这便不符合LSP的原则了。

  LSP俗语是:“老鼠的儿子会打洞。”,其实就是子类是和父类有相同的行为和状态,是可以完全替换父类的。这也是保护了OCP的开放扩展关闭修改的原则。

  前面例子更改方法可以采用父类高聚合,子类低耦合的原则来做,父类要精,子类可以采用接口实现的方法来进行扩展。

  比如可以增加一个IFly的接口,实现该接口的子类便可以飞,而父类中便不再包含Fly方法。

  还有其他方法,大家可以扩散思想回复在下面。

 

 I:ISP Interface Segregation Principle 接口分离原则

   还是企鹅和鸟的问题,我们现在有个鸟类接口:

interface IBird
    {
        void Fly();
        void Eat();
    }

  燕子实现了Fly和Eat,而企鹅只能实现Eat,所以代码如下:

class Swallow : IBird
    {
        public void Fly()
        {
            // Fly Logic
        }
        public void Eat()
        {
            // Eat
        }
    }
    class Penguin : IBird
    {
        public void Fly()
        {
            //No Implementation
        }
        public void Eat()
        {
            // Eat
        }
    }

  如果我们保持这个设计,在什么场景会出现问题呢?

  好了!5!4!3!2!1!

  揭晓答案!(这个流程会不会太综艺化..=。=)

  企鹅继承了IBird的接口,就必须实现其所以方法,虽然它在飞的方法里什么都没做。负面效果就是在统计会飞的鸟儿的时候,因为企鹅也实现了其Fly方法,但也会被归纳其中。

  其实它压根不会飞嘛!!!

  

  所以这样就违反了接口分离规则,接口分离规则旨在使用多个特定功能的接口来避开通用接口造成的富余化。

  也就是说 我们可以分离IBird为IFly和IEat,Swallow实现IFly和IEat,而呆呆的企鹅只需要实现IEat就好。

  这样让程序的设计更加灵活,当需求改变的时候,我们要做的工作便小很多也风险少很多,也会发现更多的乐趣。


 D:DIP, Dependency Inversion Principle, 依赖倒置原则

 依赖倒置原则在SOLID里是我认为最为出彩的原则和技术。简单来说就是面向接口编程。

  之前博客中已有文来介绍其所以然,这里引用来:http://www.cnblogs.com/xfuture/p/3682666.html ,下面做一下简单的介绍,也算是...大片预告片?...

  远古母系氏族,每个人都是一个独立的个体,需要什么工具就需要自己去打磨一件工具,自己需要了解所有的流程才能生存。比如打猎,从前期准备绳索,尖木,到中期做陷阱,后期收成,都需要了解的非常透彻。对应编程中便是new 绳索()new 尖木()new 陷阱()new X()。实例化所有需要的资源,然后再进行逻辑流程。

  人类逐渐在进步,工业革命的来袭,改变了整个社会的结构。人再不需要了解所有的流程,只需要去一个工厂或者采购平台,输入自己想要的东西,便能得到。对应编程中便是工厂模式,需要一个静态工厂类,一个抽象产品类型的类,一个你想拿到的可以具象化的产品类,从此便进入了全民淘宝年代,需要什么购买什么。

  当你为了修一个顶楼的灯泡购买了梯子,但当修好后,如何处理这个梯子便成了难题,扔掉不舍,不扔去卖二手又很麻烦。这时候就需要我们的主角:和谐社会登场了!主张不铺张不浪费,这便是一种回收机制,你需要它只需要说一声,秒秒钟就到你手里,你也不需要知道他来自哪里。不需要了你也不用管,我直接秒秒钟再变走。是不是有一种魔术的感觉?这便是依赖注入!依赖注入解除了对象和对象的依赖关系,需要其他对象,会有外部直接注入,而你自己不需要关心他如何而来。对象符合OCP原则(对外扩展开放,对修改关闭), 容器负责所有关系匹配。在此层,容器便是这个社会的规则,而对象只需要关心自己所完成的一部分。轻松惬意!

  

  

总结

 Shivprasad koirala 用一句话总结的SOLID总结的很好,这里就不翻译了,大家来感受下大牛对SOLID精确的理解:

 

  S:SRP, A class should take care of only one responsibility.

  O: OCP, Extension should be preferred over modification.

  L: LSP, A parent class object should be able to refer child objects seamlessly during runtime polymorphism.

  I: ISP, Client should not be forced to use a interface if it does not need it.

  D: DIP, High level modules should not depend on low level modules but should depend on abstraction.


  呼,希望大家喜欢文章风格,也希望能批评指正。

目录
相关文章
|
前端开发 .NET C#
集DDD,TDD,SOLID,MVVM,DI,EF,Angularjs等于一身的.NET(C#)开源可扩展电商系统–Virto Commerce
今天一大早来看到园友分享的福利《分享一个前后端分离方案源码-前端angularjs+requirejs+dhtmlx 后端asp.net webapi》,我也来分享一个吧。以下内容由笔者写于昨天晚上[2015.10.08]。
1366 0
|
6月前
|
开发框架 前端开发 .NET
C#编程与Web开发
【4月更文挑战第21天】本文探讨了C#在Web开发中的应用,包括使用ASP.NET框架、MVC模式、Web API和Entity Framework。C#作为.NET框架的主要语言,结合这些工具,能创建动态、高效的Web应用。实际案例涉及企业级应用、电子商务和社交媒体平台。尽管面临竞争和挑战,但C#在Web开发领域的前景将持续拓展。
200 3
|
6月前
|
SQL 开发框架 安全
C#编程与多线程处理
【4月更文挑战第21天】探索C#多线程处理,提升程序性能与响应性。了解C#中的Thread、Task类及Async/Await关键字,掌握线程同步与安全,实践并发计算、网络服务及UI优化。跟随未来发展趋势,利用C#打造高效应用。
199 3
|
18天前
|
C# 开发者
C# 一分钟浅谈:Code Contracts 与契约编程
【10月更文挑战第26天】本文介绍了 C# 中的 Code Contracts,这是一个强大的工具,用于通过契约编程增强代码的健壮性和可维护性。文章从基本概念入手,详细讲解了前置条件、后置条件和对象不变量的使用方法,并通过具体代码示例进行了说明。同时,文章还探讨了常见的问题和易错点,如忘记启用静态检查、过度依赖契约和性能影响,并提供了相应的解决建议。希望读者能通过本文更好地理解和应用 Code Contracts。
30 3
|
1月前
|
安全 C# 数据安全/隐私保护
实现C#编程文件夹加锁保护
【10月更文挑战第16天】本文介绍了两种用 C# 实现文件夹保护的方法:一是通过设置文件系统权限,阻止普通用户访问;二是使用加密技术,对文件夹中的文件进行加密,防止未授权访问。提供了示例代码和使用方法,适用于不同安全需求的场景。
112 0
|
2月前
|
API C#
C# 一分钟浅谈:文件系统编程
在软件开发中,文件系统操作至关重要。本文将带你快速掌握C#中文件系统编程的基础知识,涵盖基本概念、常见问题及解决方法。文章详细介绍了`System.IO`命名空间下的关键类库,并通过示例代码展示了路径处理、异常处理、并发访问等技巧,还提供了异步API和流压缩等高级技巧,帮助你写出更健壮的代码。
42 2
|
2月前
|
安全 程序员 编译器
C#一分钟浅谈:泛型编程基础
在现代软件开发中,泛型编程是一项关键技能,它使开发者能够编写类型安全且可重用的代码。C# 自 2.0 版本起支持泛型编程,本文将从基础概念入手,逐步深入探讨 C# 中的泛型,并通过具体实例帮助理解常见问题及其解决方法。泛型通过类型参数替代具体类型,提高了代码复用性和类型安全性,减少了运行时性能开销。文章详细介绍了如何定义泛型类和方法,并讨论了常见的易错点及解决方案,帮助读者更好地掌握这一技术。
77 11
|
2月前
|
SQL 开发框架 安全
并发集合与任务并行库:C#中的高效编程实践
在现代软件开发中,多核处理器普及使多线程编程成为提升性能的关键。然而,传统同步模型在高并发下易引发死锁等问题。为此,.NET Framework引入了任务并行库(TPL)和并发集合,简化并发编程并增强代码可维护性。并发集合允许多线程安全访问,如`ConcurrentQueue&lt;T&gt;`和`ConcurrentDictionary&lt;TKey, TValue&gt;`,有效避免数据不一致。TPL则通过`Task`类实现异步操作,提高开发效率。正确使用这些工具可显著提升程序性能,但也需注意任务取消和异常处理等常见问题。
48 1
|
2月前
|
安全 数据库连接 API
C#一分钟浅谈:多线程编程入门
在现代软件开发中,多线程编程对于提升程序响应性和执行效率至关重要。本文从基础概念入手,详细探讨了C#中的多线程技术,包括线程创建、管理及常见问题的解决策略,如线程安全、死锁和资源泄露等,并通过具体示例帮助读者理解和应用这些技巧,适合初学者快速掌握C#多线程编程。
80 0