标签:好的 err 结构 活动 ext 访问 log 连接 里氏代换原则
开发一个系统并不是一件困难的事,但是为何维护好一个系统却是一件让人头疼不以的事?
在笔者的观念中这一切都源自于需求。
如果在软件开发完成之后,需求就不再改变,那大部分程序都不需要维护了。但是,事实呢,需求是变化的,而我们呢?也需要去拥抱这些变化。
因为需求的变化无常,使得某些系统的设计无法与新的需求相容。当这些破坏式的需求越来越多,一个系统也就慢慢的变的难以维护了。真的就无法避免这样的事情发生吗?当然不!
如果说我们在设计之初就为日后的变化留出了足够的空间,或者说,我们的设计一开始就是一个具有良好的扩展性,灵活性和可插拔性的设计,系统必然能相容变化,按照正确的维护方案维护。怎么做出一个良好的设计呢?关键就在于恰当的提高软件的可维护性和复用性。
好了说了以上一堆废话,现在开始正式介绍面向对象设计中的六大原则
一、开闭原则(OCP)
定义:对扩展开放,对修改关闭。
举个例子:
你班里有班长,学习委员,体育委员等等一系列班干部。因为在一次集体活动中你的组织能力被老师看中了,想提拔你当班干部,但是又不想随便撤销了之前的干部,怎么办?班长向老师提议,他缺一个团支书,于是你就被提拔成了团支书。
在这个例子中,老师开设了新的职位-团支书,在扩展班干部的同时也保留了原先的人马,这就是所谓的对扩展开放,对修改关闭。即:不通过去修改原有的代码逻辑,来为系统添加新的功能。
当然这实际上一个非常虚的定义,他只告诉你目标,却没告诉你怎么去做。所以基于此,接下来介绍的设计原则都是围绕开闭原则出发,告诉你怎么遵守OCP
二、里氏代换原则(LSP)
定义:如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有对象o1都替换成o2时,程序P的行为没有任何变化,那么类型T2是类型T1的子类。简单概括就是,任何父类出现的地方,子类一定可以出现。
举个例子:
有两匹马,白马和黑马,你要骑马,你既可以骑黑马,又可以骑白马。这里马是父类,白马,黑马是子类,你骑马的动作对象是马,所以只要你会骑马,不管是白马还是黑马你都可以骑。
1 class Horse {
2 void ride(){}
3 }
4
5 class WhiteHorse extends Horse {
6
7 @Override
8 public void ride() {
9 System.out.println("骑白马");
10 }
11 }
12
13 class BlackHorse extends Horse {
14
15 @Override
16 public void ride() {
17 System.out.println("骑黑马");
18 }
19 }
20
21 class Men {
22 void ride(Horse horse) {
23 horse.ride();
24 }
25 }
在看下面的代码:
1 class Calculator {
2 void add(int x,int y) {
3 System.err.println(x+y);
4 }
5 }
6
7 class AdvCalculator extends Calculator {
8 @Override
9 void add(int x,int y) {
10 System.out.println(x*y);
11 }
12 void sub(int x,int y) {
13 System.out.println(x-y);
14 }
15 }
子类AdvCalculator重写了父类Calculator的add方法,我们可以看出在调用add方法时,期望输出是两者和,但是由于子类复写了add方法,所以我们调用子类的add的时候获得的结果是两者的乘积,和期望不符合。
实际上这个原则隐藏了这样一层约束,当类B继承类A时,不能改变父类的规范和约束(不随意重写父类已经实现好的方法),如果子类随意修改这些契约,那么会对整个继承体系造成破坏。
*以上原则反过来是不成立,因为一般情况下子类包含了父类没有的新特性。
三、依赖倒转原则(DIP)
定义:依赖抽象,不依赖实现。
举个例子:
同样以黑马白马为例子,镖师骑着白马去送行,代码如下:
1 class WhiteHorse {
2
3 void ride() {
4 System.out.println("骑白马");
5 }
6 }
7
8 class Men {
9
10 void ride(WhiteHorse horse) {
11 horse.ride();
12 }
13 }
但是有一天,镖局没有白马了,只有黑马,代码如下:
1 class BlackHorse {
2
3 void ride() {
4 System.out.println("骑黑马");
5 }
6 }
但是此刻发现,镖师居然只能骑白马送信,要是想让镖师骑黑马送信,就必须修改镖师,这显然有问题,镖师也太任性了,黑马不是马么(耦合性太高)?必须要让镖师什么马都骑,不能惯着。
我们引入一个接口马:
interface Horse{
void ride();
}
class Men {
void ride(Horse horse) {
horse.ride();
}
}
这样镖师类与接口马发生了依赖关系。而白马和黑马都实现了接口马。这样就符合了DIP,同时也让我们的镖师能骑所有的马送信了:
1 class WhiteHorse implements Horse {
2
3 public void ride() {
4 System.out.println("骑白马");
5 }
6 }
7
8 class BlackHorse implements Horse {
9
10 public void ride() {
11 System.out.println("骑黑马");
12 }
13 }
有这样一个事实,良好抽象层相对与实现层稳固多了。在笔者理解中,抽象是对具体对象抽离出本质特征的过程。它排除同属对象之前的差异,只留下普适的共同点。所以以良好抽象层为基础构建起来的系统一定比以具体实现类为基础搭建的系统稳定,DIP也正是基于此。
* 当然,有一些具体的类可能是相对稳定的,不会发生变化的,消费这个对象的类完全可以依赖这个具体实现,而不必在为此在创建一个抽象类
四、接口隔离原则(ISP)
定义:提供尽可能小的接口。
举个例子:
将接口理解为一个类所提供的所有方法的集合,也就是一种逻辑上才存在的概念,这样的话,对接口的划分实际上就是对类型的划分。也就是说一个接口可以理解为一个角色,看以下类图
从上面这个类图可以看出,由一个接口A给出了所有功能,同时可以看出类Client的下单属性里面包含了支付功能,而支付属性同时也包含了下单的功能,明显违反了ISP原则,这是一种糟糕的设计,那么依照ISP的做法是怎么样的呢。
从上图中可以看出,将接口划分成两个角色一个支付,一个下单。支付只针对支付,下单只针对下单。不建立单一臃肿庞大的接口,细分到角色,一个角色只负责一件事情。
五、合成/聚合复用原则(CAPP)
定义:尽量使用合成/聚合方式复用对象,而不是使用继承。
1、什么是合成/聚合
将已有的对象纳入到新对象中,使之成为新对象的一部分。实际上就是把已有对象当作新对象的一个属性,合成和聚合的区别在于,合成。新对象负责管理被合成对象的生命周期,如人和手,人没了手还会在么?聚合则么不是,如公司和员工,公司不再了,员工当然不会随着消失
2、为什么要尽量使用合成/聚合
a、破坏封装,将超类的实现细节暴露给子类。
b、超类发生改变,引发子类的改变
c、继承关系编译器就确定了,在运行期间无法改变,不够灵活
举个例子:
人有多个身份,如父亲,儿子。如果以继承的方式去描述这些信息,这意味着人要么是父亲要么是儿子。但是一个人既可以是父亲又可以是儿子啊。这显然不合理。
这种错误的设计源于,错误的把人的等级结构和角色的等级结构混淆了。在这里要引入has-a 和 is-a的概念。is-a 是严格分类学意义上的定义,意思是一个类是另一个类的一种。而has-a表示的是某一个具有一种责任。父亲或者儿子只是一种角色,一只小狗也可以是一只大狗的儿子,难道这只小狗有着人的所有行为特征?基于此对上述设计进行改造
*里氏替换原则是继承复用的基础。只有两个类满足里氏替换原则的时候,才可能是“Is-A”关系。如果两个类是“Has-A”关系而非“Is-A”关系,但是设计成了继承,那么肯定违反里氏替换原则。
六、迪米特法则(LoD)
定义:一个对象应当对其他对象有尽可能的少了解
对于这个法则还有一种简单的定义,不要和陌生人说话,只和朋友通信。那么什么叫朋友呢?
1、this
2、方法入参
3、对象属性
4、对象属性如果是集合,里面所有元素也是朋友
5、由当前对象所创建的对象
任何一个对象满足以上任何一个条件,就是对象朋友,其他都是陌生人。
举个例子,排长统计自己手下有多少士兵,代码:
1 class Platoon {
2 private Squad[] squads;
3
4 void gather() {
5 int count = 0;
6 for (Squad squad : squads) {
7 for (Man man : squad.gather()) {
8 count += man.gather();
9 }
10 }
11 System.out.println("一个排一共有"+count+"名士兵");
12 }
13 }
14
15 class Squad {
16 private Man[] mans;
17
18 Man[] gather() {
19 return this.mans;
20 }
21 }
22
23 class Man {
24 int gather() {
25 return 1;
26 }
27 }
从上述代码中可以看出,类Platoon的设计是有问题的,Platoon中直接与Man类发生了通信,使两个类发生了耦合,根据LoD原则,类只和自己的朋友交流,man对象明显是Platoon的陌生人。而且从现实的逻辑上看,排长只需知道自己有几个班就好了,班上成员只要交给班长就好了。如果事必躬亲,那还需要班长干什么。根据LoD原则,改进后的设计如下:
1 class Platoon {
2 private Squad[] squads;
3
4 void gather() {
5 int count = 0;
6 for (Squad squad : squads) {
7 count += squad.gather();
8 }
9 System.out.println("一个排一共有"+count+"名士兵");
10 }
11 }
12
13 class Squad {
14 private Man[] mans;
15
16 int gather() {
17 int count = 0;
18 for (Man man : mans) {
19 count += man.gather();
20 }
21 return count;
22 }
23 }
24
25 class Man {
26 int gather() {
27 return 1;
28 }
29 }
其实,迪米特法则谈论的就是对象之间的信息流量、流向以及信息的影响的控制。将实现的细节隐藏起来,让模块之间只通过api交互,这实际上就是信息的隐藏。它可以使模块之间脱耦,允许模块独立开发维护等一系列好处。所以在设计类的时候最好遵循以下几点
1、优先考虑将类设置成不变类
2、尽量降低一个类的访问权限
3、尽量降低成员的访问权限
--------------------------------------------------------end--------------------------------------------------------
以上粗略的介绍了设计模式的六大原则,是笔者自己个人的感悟,如有谬论,欢迎指出。如有兴趣深入了解,可以参考《java与模式》已经网上其他优秀的博文,
作者:燃点null
出处:http://www.cnblogs.com/lightnull/
本文为燃点null原创,欢迎转载,但未经同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
java设计模式(一)【六大原则】
标签:好的 err 结构 活动 ext 访问 log 连接 里氏代换原则
原文地址:http://www.cnblogs.com/lightnull/p/7617192.html