何为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.
呼,希望大家喜欢文章风格,也希望能批评指正。