面向对象设计原则
网上看到对许多对面向对象设计原则的总结,有五大原则的,也有六大原则的,甚至还有更多的。于是我个人也学习了一下,可能是自己各方面的知识还比较贫瘠,感觉理解起来并没有想象的那么容易,自己也学着总结归纳了一下,来加深自己的印象,也能更好提升自己对面向对象更深层次的理解。
我这里用了最常用的五大原则
S = 单一责任原则
O = 开闭原则
L = Liscov替换原则
I = 接口隔离原则
D = 依赖倒置原则
原则一:单一职责原则
这里引用别人的一张图片,我觉得这张图片是最好说明单一职责原则的了,从现实世界的一个工具,再到虚拟世界的一个类(class),一个函数或方法(function)绝对不可能做到万能,如果把一个类或一个方法的功能做的太复杂化,那么它的设计就是充满挑战的,就算你完成了这项伟大的设计,但它也未必能达到你预期的效果,而且你后期对它的维护也将充满挑战,所以不如不把复杂的功能简单化,把简单的功能细节化,人性化,这才是你对程序设计最原始的初衷
原则二:开闭原则
这里不得不提到对开闭原则的一句最经典的话:对扩展开放,对修改关闭。在现实生活中我们会对一个人的穿着打扮做出一些评价,是否经常在耳旁会有人说“你今天这身打扮真漂亮”,“这衣服正适合你”。说了这么多,我最终想表达的就是我们在更换自己的衣服时,我们的身体是绝对不会做任何更改的,代码亦是如此,这里我想也不用我多说,对代码好的扩展就好比给程序加上了漂亮的衣服,使程序更好的发挥自己的功能,但是你程序不得不说你程序本身长得”漂不漂亮“,也要看你最初对封装代码设计了,当然这里只是举例,肯定没有只加衣服这么简单,^^。
原则三:Liscov替换原则(里氏替换原则)
这个原则起初是最让我懵比的原则,因为一听它那高大上的名字就把我震住了,里氏替换原则海报描述了:"子类型必须能够替换它们的基类."或者, 换句话说:"使用基类引用的函数必须能够使用派生类而无须了解派生类.",看完这句话后的我还是不太懂,于是我又查阅资料,得出了我对他的理解,其实它就是面向对象编程的基本原则. 叫做多态性,在基本的面向对象原则中, "继承" 通常被描述成 "is a" 的关系。怎么说呢,举个例子,我们说一句话:“麻雀是可以飞行的动物”,在这句话中麻雀是子类,动物是父类(或者说是接口),它实现了父类所定义的“飞”这个方法,但是它满足Liscov替换原则所说的子类型必须能够替换它们的基类这个原则吗?答案很简单,你反过来说这句话就好了:“动物是可以飞行的麻雀”,那人,狗,猫也是动物,它们也要实现飞这个方法,但是他们可以飞吗,很显然“麻雀”并不能替换它的基类“动物”,所以它是不满足里氏替换原则的,那“麻雀是可以飞行的动物”这样的设定会造成那些不良的后果呢?你可以像想一下,如果把”动物“作为接口,定义一个“飞”的方法,这样做所有的动物都要去实现飞,那这样的设定很显然是不符合逻辑的。那我们对这句话稍微做一点改进呢?改成:“麻雀是可以飞行的鸟类”,把鸟作为接口,定义飞方法,这样做结果又如何呢,反过来说:“鸟类是可以飞行的麻雀”,很明显,这样设计也不太符合里氏替换原则,那怎样的设计才能符合里氏替换原则呢,其实很简单,把这句话改成:“麻雀是可以飞行的”,这样就好了,哈哈,你是不是会说:什么鬼,你会问麻雀是可以飞行的什么呢?起始你并不需要说明他是可以飞行的什么,只要知道它可以飞行就好了,反过来说“可以飞行的是麻雀”,“可以飞行的是飞机”,“可以飞行的是竹蜻蜓”,,,都可以,我们定义了一个IFlyable(可以飞行的)接口,这样更抽象,但也更加灵活了,我们不必在担心人,狗,猫要不要实现飞的问题了,只要可以飞的东西都去实现IFlyable接口就好了,其实在C#中,我们会用到许多接口,比如说IEnumerable(可以列举的),IComparable(可比较的),它们对接口的定义是不是和IFlyable(可以飞行的)接口一样,其实那就是传说中的里氏替换原则,把接口定义的更加功能化,抽象化,能更好的处理继承之间的关系。
原则四:接口隔离原则
接口隔离原则海报的意思是这样的:“用户不应该被迫依赖他们不使用的接口。”
假设你想去买一台电视机并且有两种类型可以选择,其中一种有很多开关和按钮,但是多数对你来说用不到,另一种只有几个开关和按钮,并且看来你很熟悉怎么用。如果这两种电视机提供同样的功能,你会选择哪一种?当然是第二种了。因为你不需要看起来很麻烦而且对我也不必要的开关和按钮。同样的,假如你有一些类,你通过接口暴露了类的功能,这样外部就能够知道类中可用的功能,客户端也可以根据接口来设计。当然那,如果接口太大,或是 暴露的方法太多,从外部看也会很混乱。接口包含的方法太多也会降低可复用性, 这种包含无用方法的”胖接口“无疑会增加类的耦合。
这还会引起其他的问题。如果一个类视图实现接口,它需要实现接口中所有的方法,哪怕一点都用不到。所以,这样会增加系统复杂度,降低系统可维护性和稳定性。
接口隔离原则确保接口实现自己的职责,且清晰明确,易于理解,具有可复用性。
下面的接口是一个“胖接口”,这违反接口隔离原则:
违反接口隔离原则的接口示例
注意,IBird接口定义 Fly()的行为有许多鸟类的行为。现在,如果一只鸟类(比方说,鸵鸟)实现了这个接口,它将会实现不必要的 Fly()的行为(鸵鸟不会飞)。
“胖接口”应该分隔成两个不同的接口,IBird 和IFlyingBird,而IFlyingBird继承于IBird。
接口隔离原则的例子中正确版本的接口
如果有一只不会飞的鸟(比如,驼鸟),只要用IBird接口即可,如果有一保会飞的鸟(比如,翠鸟),只要用IFlyingBird接口即可。
原则五:依赖倒置原则
依赖倒置原则概念:高层次的模块不应该依赖于低层次的模块,而是,都应该依赖于抽象。我们用一个现实的例子来理解。你的汽车是用很多部件组成,比如发动机,车轮,空调和其他的部件,它们并没有严格的构建在一个部件里;就是说,它们都是“插件”,要是引擎或着车轮出了问题,你可以单独修理它,甚至换一个用。
替换时,你只需要保证沉沦符合汽车的设计(汽车能使用任何1500CC的引擎或任何18寸的车轮)。
当然,你可以在1500CC 的位置上安装2000 CC的引擎,对某些制造商都一样(丰田汽车)。
可如果你的汽车部件不是“可拔插”的呢?那太可怕了!这样的话,要是汽车引擎故障,你得整车修理,或者买一辆新车!那么怎么做到"可插拔"呢?关键是”抽象“
现实世界中,汽车是高层级的模块/实体,它依赖于底层级的模块/实体,例如引擎和轮子。
相较于直接依赖于实体的引擎或轮子,汽车应该依赖于抽象的引擎或轮子的规格,这样只要是符合这个抽象规格的引擎或轮子,都可以装到车里跑。
来看看下面的图:
依赖倒置原则的类层次结构
所以,如果代码不遵循依赖倒置,就有下面的风险:
- 使用低层级类会破环高层级代码;
- 当低层级的类变化时,需要太多时间和代价来修改高层级代码;
- 代码可复用性不高
好啦,总结完了,当时里面许多的概念和图片是从相关资料整理过来的,感觉自己对面向对象设计的理解又有所加强^^
除 SOLID 原则外还有很多别的面向对象原则。比如:
- “组合替代继承”:是说“用组合比用继承好”;
- “笛米特法则”:是说“类对其它类知道的越少越好”;
- “共同封闭原则”:是说“相关类应该一起打包”;
- “稳定抽象原则”:这是说"类越稳定,就越应该是抽象类";