类设计的5个基本原则
我们常说啥面向对象三大特性:封装,继承,多态.另一种说法是:抽象,继承,动态绑定
然后就是面向对象五大设计原则,面向对象的设计其实说到底就是类的设计嘛,没有了类就自然不能叫面向对象了.当然了像C#中还有所谓的接口(interface),把它理解成一个特殊的类好了.
我觉得 面向对象的应用中最难的就是类的设计,怎么设计好一些类没有固定标准,只有一些参考原则.所以设计类不只是技术活,而且是个艺术活.
类设计(或者面向对象设计)五大基本原则
(1)单一职责原则(Single-Resposibility Principle)与 接口隔离原则(Interface-Segregation Principle)
单一职责原则
一个类尽量只做一件事.不过什么叫一件事?
就像维特根斯坦说世界可以分解为事实,而事实又分解为原子事实,原子事实(由对象组成)不可再分.那这里很明显的就是我们无法有一个标准来确定啥是原子事实.有些人觉得一个原子事实实际上可以再分,另一些人可能觉得不可分.
所以单一职责我们别指望去精确的确定啥是一件事,一个类的界限.反正就这样简单的理解.通过一个类名我们凭常规思维来想象下它可能会具有的静态属性(成员变量),动态属性(成员函数).就像我们平时看到一个名词时会联想到跟该名词紧密相关的性质.
要举个体现单一职责原则的最常见的例子无疑就是STL中的迭代器的设计.
有些人觉得容器跟迭代器的分离是不好的设计,觉得添加了复杂度.还不如直接把迭代器放容器里更简洁.不过很多人还是不这样认为的.首先嘛类的数量越多并不代表就越复杂.另外嘛迭代器如果放到容器里面,就会暴露容器的一些内部结构,不太符合封装性的思想.还有就是可扩展性的问题.因为对容器的访问遍历会有多种需求,如果把迭代器隔离开来你可以不修改容器类,再定义些特制的迭代器就行了.这样不管你有啥奇怪的需求只要整个对应的迭代器出来就OK.
接口隔离原则
接口依赖使用多个小的专门的接口,而不要使用一个大的总接口
其实简单点的讲与前面说的单一职责类似嘛.在C++中一个接口就是一个类,所以更加可以直接说要体现单一职责原理.而C#中的有专门的接口interface,和类区分开来的.而且C#中不像C++支持类的多继承,只继承接口的多继承.所以这里可以把接口理解成功能更小更特殊的类,一个接口可能就只要那么几个很少的方法就OK了.
(2)开放封闭原则(Open-Closed principle)与 依赖倒置原则(Dependecy-Inversion Principle)
开放封闭原则
开放封闭指:对扩展开放,对修改封闭.
这样听着肯定觉得很迷糊.所谓修改封闭,就是你之前设计好的类,类里面的方法等就不要去修改,比如删除掉一个类,删除掉一个函数或者改变函数的形参表啊.
所谓扩展开放,就是在不改变之前存在的类和函数的前提下你可以添加很多功能.一般是通过继承和多态来实现的.这样父类可以保持原样.只要子类中添加些新功能.
当然有时一些应用的改变导致父类中的一些函数实现细节也难免要改(细节的实现总是要跟着需求变的).所以最好的办法是面对抽象编程,比如定义一个抽象类,它只涉及到函数的声明,表明要实现哪些功能.而不涉及任何的细节.于是以后不用去修改它.而只修改或添加继承自这个抽象类的子类.实际上凡事都是相对而言的,不涉及到细节的抽象类改动的可能性小点,实际项目中我们也可能必须得修改抽象类,违反开放封闭原则.设计原则只是起指导作用,而不是起约束我们的作用.我们在大部分时候尽量遵循,但如果一些特殊情况需要特殊处理就自然不用去管啥原则了.
体现开放封闭原则的例子:
一个比较特别的例子就是C#中的扩展方法.不管是已经存在的你没法去修改的类库,还是你自己写的类.假如完全不让你去改动之前的代码,这自然是很好的体现封闭原则.那又要给原有的类扩充一些功能可咋整? 你首先想到可能是继承下那些类,然后在子类中添加些方法.但一来嘛如果是只是要简单的添加一点点功能,这样再整个类出来有点奢侈,费事了.二来嘛C#中还有些特殊的sealed类,它根本不能让你继承的. 于是C#中出现了个特殊的特性叫扩展方法.就是你在随便在哪定义一个静态函数,把你想扩展的类的类名作为参数,前面记得加个this.这样一来你定义的函数就会"绑定"到了那个类上.就仿佛那个类多了个函数了,实例化一该类就能调用该函数了.比较有趣的一个功能吧.C++中是没有这功能的.
依赖倒置
依赖倒置指依赖于抽象,高层模块不依赖于底层模块,二者都同依赖于抽象;抽象不依赖于具体,具体依赖于抽象.
是不是听着有点云里雾里的啊.先来举个例子瞧瞧,假如你毕业要去找工作了,你可能要依赖某个技能吧,于是你就靠着会C,C++,C#,Java或者matlab之类的具体技能去找工作.假如说一些企业招人也是按这样具体的要求,就是要你具体掌握某个具体的编程语言来招人.这样你找工作的人与招聘的人两者都要依赖于编程语言这个细节的底层模块.这样一依赖就会出现找工作的人选择范围太小了,招聘的人可供选择的范围也小了.
而假如把那些具体的编程语言的一些基础思想抽象出来,比如对操作系统原理,编译原理,数据结构算法等的掌握.假如学生依照这个抽象出来的技能去找工作,而招聘者也根据这些抽象点的要求来招人.于是皆大欢喜,都有很大的选择范围.
在编程中就是假如有类AA与BB,而类AA与BB互相依赖,于是比较好的方法就是把AA抽象出来一个A,BB抽象出来一个B,这样就变成A,B互相依赖了.另外AA要依赖A,继承自它并实现具体细节嘛.
依赖倒置设计可以这样来理解,依赖就是刚开始是具体的细节间互相依赖,我们要变成抽象类间的依赖,降低了耦合度.然后就是有了抽象类,继承自它的实现类也要依赖它.那倒置两字咋理解呢? 一般情况我们是先关注细节,然后根据细节抽象出来一些概括的观念出来嘛.所以按常理一般是抽象要依赖于细节的.而现在是是倒过来了,确定一个抽象类后,那些细节的实现得以抽象出来的规范为基准.不然你要继承了一个抽象类,你不完全实现它的方法的话可不让你实例化对象的啊.
(3)Liskov替换原则(Liskov-Substituion Principle)
替换原则就是子类必须能够替换其基类.
看来直貌似蛮简单啊,你甚至可能觉得这不会废话嘛.我们平时用的类都是子类可以替换基类的啊.要不然多态可咋实现呢.实际上之所以你没碰到违反Liskov替换原则的类一来嘛是因为这样的场景确实不太多,另一个就是设计好的类库肯定不会让违反替换原则的类出现.所以你实际应用中不太容易接触到替换原则啊.
下面来举个违反替换原则的特殊例子:
就是正方形与长方形的问题.我们知道正方形是一个特殊的长方形.所以可以设计两个类,正方形类继承自长方形类.
然后有两变量分别表示长和宽,有个计算面积的公式.假如计算面积的方法是virtual的,这样能实现多态嘛. 在先设定长和宽后再调用计算面积的方法.我们知道正方形是长和宽相等的.如果你设定长和宽的时候不是一样的,然后呢又是调用了正方形的面积计算公式.这样肯定就错了. 你可能会问咋这么扯蛋啊,为啥把长和宽设成不一样啊.很多设计的思想是一来为了让你方便,二来为了让你少犯错误.就是不管你怎么使用都不会出错,要出错应该是在编译时就错,让意识到哪出错了.而如果出现上面说的情况编译器是没法让你知道出错了的.
所以你这样一个正方形类继承自长方形类的设计是不好的(注意的一点是你违反了Liskov可替换原则并不是说就写的代码就出错了,只是说设计不太合理.实际上你这样设计代码没准可以正常的跑得很好呢,如果没有出现一些特殊情况可能是一点bug也没有.只不过设计不合理为导致一些安全隐患而已)
概括地讲, 面向对象设计原则仍然是面向对象思想的体现。例如,
(1)单一职责原则与接口隔离原则体现了封装的思想,
(2)开放封闭原则体现了对象的封装与多态,依赖倒置原则,则是多态与抽象思想的体现而 .
(3)Liskov替换原则是对对象继承的规范,