面向对象六大原则
-
单一职责原则--SRP(Single Responsibility Principles)
-
每个类都有唯一的职责
设计一个类,首先要明白这个类要去干什么,即为这个类的职责。
发动机是一个复杂的系统,但是无论它多复杂,它也只有一个职责:输出动力。
-
-
开闭原则--OCP(Open Close Principles)
-
对扩展开放,对修改关闭
即设计的类在满足其职责的基础上,在不修改类中代码的情况下,能够良好的扩展.
单一职责原则和开闭原则一起,构成了设计一个类的指导思想
-
-
里式替换原则--LSP(Liskov Substitution Principles)
-
所有的基类出现的地方,都能透明的被其子类替换
里式替换原则是对扩展开放的基础,A依赖于B,那么A就是可以使用B的子类,来实现功能,从而让A具有了扩展性。
看下面的例子:
-
/**
* 厨师
*/
public class Chef {
private Cai cai;
public void cook() {
if (cai == null) {
System.out.print("我是厨师,我没菜做,我在和Lance吹牛逼...\n");
} else {
System.out.print("我是厨师,我在做" + cai.getNameToCook()+"\n");
}
}
public void setCai(Cai cai){
this.cai = cai;
}
}
/**
* 菜的父类
*/
public class Cai {
public String getNameToCook() {
return "菜";
}
}
/**
* 辣椒炒肉
*/
public class LaJiaoChaoRou extends Cai {
public String getNameToCook() {
return "辣椒炒肉";
}
}
/**
* 西红柿炒蛋
*/
public class XiHongShiChaoDan extends Cai {
public String getNameToCook() {
return "西红柿炒蛋";
}
}
@Test
public void testChef(){
Chef chef = new Chef();
chef.cook();
Cai cai = new Cai();
chef.setCai(cai);
chef.cook();
XiHongShiChaoDan xiHongShiChaoDan = new XiHongShiChaoDan();
chef.setCai(xiHongShiChaoDan);
chef.cook();
LaJiaoChaoRou laJiaoChaoRou = new LaJiaoChaoRou();
chef.setCai(laJiaoChaoRou);
chef.cook();
}
//打印结果如下
我是厨师,我没菜做,我在和Lance吹牛逼...
我是厨师,我在做菜
我是厨师,我在做西红柿炒蛋
我是厨师,我在做辣椒炒肉
可以看出,由于里式替换原则的存在,厨师类Chef虽然依赖于Cai这个类,但是它一样可以使用Cai的子类,做出西红柿炒蛋,辣椒炒肉等等。
-
依赖倒置原则--DIP(Dependence Inversion Principles)
高层模块不应该依赖低层模块,他们都应该依赖其抽象
-
抽象不应该依赖细节,细节应该依赖抽象
这是一个解耦原则,依据该原则对类进行设计,能够使类与被其使用的其他类之间最大限度的解耦。
什么是抽象? 接口或者抽象类是抽象 什么是细节? 实现类是细节
在里式替换的列子中,类Chef依赖于类Cai,Chef是高层模块,Cai是低层模块,高层模块直接依赖了低层模块。这样做有什么坏处呢?即Chef的cook()方法必须使用Cai或者Cai的子类,其他类即使有getNameToCook()这个方法,但是也一无法提供给A使用,例如下面的类
public class CaiBaoZi{
public String getNameToCook(){
return "菜包子";
}
}
CaiBaoZi这个类虽然有getNameOfFood()方法,但是它不是Cai的子类,无法被Chef使用 也就是说,低层模块Cai限制了高层模块Chef的能力,这就是耦合对一个类带来的伤害。 下面我们对上面的例子做一下修改:
/**
* 提供菜名
*/
public interface IName{
String getNameToCook();
}
/**
* 厨师
*/
public class Chef {
private IName cookedObject;
public void cook() {
if (cookedObject == null) {
System.out.print("我是厨师,我没菜做,我在和Lance吹牛逼...\n");
} else {
System.out.print("我是厨师,我在做" + cookedObject.getNameToCook()+"\n");
}
}
public void setCai(IName cookedObjec){
this.cookedObject = cookedObjec;
}
}
/**
* 菜的父类
*/
public class Cai implements IName {
public String getNameToCook() {
return "菜";
}
}
/**
* 辣椒炒肉
*/
public class LaJiaoChaoRou extends Cai {
public String getNameToCook() {
return "辣椒炒肉";
}
}
/**
* 西红柿炒蛋
*/
public class XiHongShiChaoDan extends Cai {
public String getNameToCook() {
return "西红柿炒蛋";
}
}
/**
* 菜包子
*/
public class CaiBaoZi implements IName{
@Override
public String getNameToCook() {
return "菜包子";
}
}
@Test
public void testChef(){
Chef chef = new Chef();
chef.cook();
Cai cai = new Cai();
chef.setCai(cai);
chef.cook();
XiHongShiChaoDan xiHongShiChaoDan = new XiHongShiChaoDan();
chef.setCai(xiHongShiChaoDan);
chef.cook();
LaJiaoChaoRou laJiaoChaoRou = new LaJiaoChaoRou();
chef.setCai(laJiaoChaoRou);
chef.cook();
CaiBaoZi caiBaoZi = new CaiBaoZi();
chef.setCai(caiBaoZi);
chef.cook();
}
//打印结果如下:
我是厨师,我没菜做,我在和Lance吹牛逼...
我是厨师,我在做菜
我是厨师,我在做西红柿炒蛋
我是厨师,我在做辣椒炒肉
我是厨师,我在做菜包子
通过上面的更改,Chef可以使用CaiBaoZi了,显然CaiBaoZi不是Cai的子类,Chef的扩展性得到更大的发展,对应到依赖倒置原则的定义: Chef是高层模块,是细节 Cai是低层模块,是细节 CaiBaoZi也是低层模块,是细节 IName就是抽象 Chef、Cai、CaiBaoZi都依赖于IName 有了IName这个抽象,A就再也不会受到具体的对象的限制了,只要是实现了IName接口的类,都能够被Chef使用,Chef具有了无限可能,可以说是可以使用任意的类来完成其cook()功能。
那有同学就说了,我有一个第三方SDK提供的类HongJiu:
public class HongJiu{
public String getNameOfDrink(){
return "红酒";
}
}
这个类封装在SDK中,我没办法去更改,怎么让被Chef使用呢?我们这么干:
public class HongJiuForChef implements IName{
HongJiu hongJiu = new HongJiu();
@Override
public String getNameToCook() {
return hongJiu.getNameOfDrink();
}
}
@Test
public void testChef() {
Chef chef = new Chef();
chef.cook();
Cai cai = new Cai();
chef.setCai(cai);
chef.cook();
XiHongShiChaoDan xiHongShiChaoDan = new XiHongShiChaoDan();
chef.setCai(xiHongShiChaoDan);
chef.cook();
LaJiaoChaoRou laJiaoChaoRou = new LaJiaoChaoRou();
chef.setCai(laJiaoChaoRou);
chef.cook();
CaiBaoZi caiBaoZi = new CaiBaoZi();
chef.setCai(caiBaoZi);
chef.cook();
HongJiuForChef hongJiuForChef = new HongJiuForChef();
chef.setCai(hongJiuForChef);
chef.cook();
}
//打印结果
我是厨师,我没菜做,我在和Lance吹牛逼...
我是厨师,我在做菜
我是厨师,我在做西红柿炒蛋
我是厨师,我在做辣椒炒肉
我是厨师,我在做菜包子
我是厨师,我在做红酒
牛逼了,我们的厨师能够做红酒了,这就是高层模块和低层模块的解耦。
那么问题来了,我怎么知道我要使用一个什么样的抽象呢? 答案是:根据高层模块完成功能H,需要低层模块提供什么样的功能L,要使用的抽象,就是这个功能L的抽象。 拿上面的例子来说,Chef要顺利的完成cook()方法需要什么?需要外界提供一个名字,那么我们对这个需求进行抽象,就抽象出来了一个接口IName,只要实现了这个接口的模块,就都可以被Chef作为低层模块使用了
-
接口隔离原则--ISP(Interface Segregation Principles)
类不应该依赖它不需要的接口。
-
类间的依赖关系,应该建立在最小接口上。
了解了以上四个原则之后,我们已经能够确定,接口即抽象对一个良好的设计的重要性。而接口隔离原则就是设计一个良好的抽象的指导原则。
看下面的例子:
public interface B {
void hear();
void talk();
void read();
void write();
}
public class A implements B{
@Override
public void hear() {
System.out.print("A hear");
}
@Override
public void talk() {
System.out.print("A talk");
}
@Override
public void read() {
//A 不需要这个方法
}
@Override
public void write() {
//A 不需要这个方法
}
}
public class C implements B{
@Override
public void hear() {
System.out.print("C hear");
}
@Override
public void talk() {
//C 不需要这个方法
}
@Override
public void read() {
System.out.print("C read");
}
@Override
public void write() {
System.out.print("C write");
}
}
可以看出,A被迫实现了它不需要的方法read、write,而C被迫实现它不需要的方法talk。方法就是实现的功能,本来A是没有read和write的功能的,但是由于实现了接口B,它就多出了这两个功能,这是和设计的初衷所不符的,也容易造成使用者的误解,对于后期的必要的修改也会造成干扰,例如我要对A增加一项功能run,如果将该功能抽象到B接口中,那么C也会被迫跟着修改,这显然不是一个良好的设计。
下面是修改后的设计:
public interface B {
void hear();
}
public interface B2A extends B{
void talk();
void run();
}
public interface B2C extends B{
void read();
void write();
}
public class A implements B2A{
@Override
public void hear() {
System.out.print("A hear");
}
@Override
public void talk() {
System.out.print("A talk");
}
@Override
public void run() {
System.out.print("A run");
}
}
public class C implements B2C{
@Override
public void hear() {
System.out.print("C hear");
}
@Override
public void read() {
System.out.print("C read");
}
@Override
public void write() {
System.out.print("C write");
}
}
修改之后,将接口进行了拆分隔离,再需要修改的时候,公共方法就在接口B中修改,其他的根据需要分别在接口B2A和接口B2C中修改,再也不会对其他的类造成不必要的影响;虽然接口的数量增加了,但是无论是对于以后代码的维护拓展,还是对于使用者的理解,都是非常友好的。
当然,对于最小接口的设计要注意把控好度,拆分的粒度不够细腻,就会维护困难,拆分的太细又会大量的增加接口数量,也会造成困扰。
-
迪米特原则(最少知识原则)--LOD(Law of Demeter)
一个对象,应该对其他的对象有最少的了解。
-
维基百科上定义如下: 得墨忒耳定律(Law of Demeter,缩写LoD)亦称为“最少知识原则(Principle of Least Knowledge)”,是一种软件开发的设计指导原则,特别是面向对象的程序设计。得墨忒耳定律是松耦合的一种具体案例。该原则是美国东北大学在1987年末在发明的,可以简单地以下面任一种方式总结:
每个单元对于其他的单元只能拥有有限的知识:只是与当前单元紧密联系的单元;
每个单元只能和它的朋友交谈:不能和陌生单元交谈;
只和自己直接的朋友交谈。
这个原理的名称来源于希腊神话中的农业女神,孤独的得墨忒耳。
很多面向对象程序设计语言用"."表示对象的域的解析算符,因此得墨忒耳定律可以简单地陈述为“只使用一个.算符”。因此,a.b.Method()违反了此定律,而a.Method()不违反此定律。一个简单例子是,人可以命令一条狗行走(walk),但是不应该直接指挥狗的腿行走,应该由狗去指挥控制它的腿如何行走。
-
百度百科上有如下描述:
狭义的迪米特法则
如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中的一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。
朋友圈的确定
“朋友”条件:
1)当前对象本身(this)
2)以参量形式传入到当前对象方法中的对象
3)当前对象的实例变量直接引用的对象
4)当前对象的实例变量如果是一个聚集,那么聚集中的元素也都是朋友
5)当前对象所创建的对象
任何一个对象,如果满足上面的条件之一,就是当前对象的“朋友”;否则就是“陌生人”。
狭义的迪米特法则的缺点:
在系统里造出大量的小方法,这些方法仅仅是传递间接的调用,与系统的商务逻辑无关。
遵循类之间的迪米特法则会是一个系统的局部设计简化,因为每一个局部都不会和远距离的对象有直接的关联。但是,这也会造成系统的不同模块之间的通信效率降低,也会使系统的不同模块之间不容易协调。
门面模式和调停者模式实际上就是迪米特法则的应用。
广义的迪米特法则在类的设计上的体现:
优先考虑将一个类设置成不变类。
尽量降低一个类的访问权限。
谨慎使用Serializable。
尽量降低成员的访问权限。