面向对象(OO)的基本特征:
这几个特征是贯穿于所有的设计原则和所有的设计模式背后的思想,你会发现无论是哪一种设计原则或者设计模式都是在为了满足了其中的某些特征而努力。
定义:对于一个类,有且仅有一个引起它变化的原因。
单一职责的英文全称是Single Responsibility Principle, 简称SRP。
从这句定义我们很难理解它的含义,通俗讲就是我们不要让一个类承担过多的职责。如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化可能会导致类的行为功能发生变化。这种耦合会导致脆弱的设计,当变化发生时,设计会遭受到破坏。我们要做的就是要分离这种多职责的变化,从而降低耦合度。
比如一些初级的Android开发者往往喜欢在Activity中写Bean文件、网络请求数据处理,如果有列表的话Adapter 也会写在Activity中,这将导致Activity过于臃肿行数过多,如果我们要修改Bean文件,网络处理和Adapter都需要来修改这个Activity,这就会导致引起这个Activity变化的原因太多,我们在版本维护时也会比较头疼,也就严重违背了SRP原则。
单一职责适用于类、接口,以及方法,即不管是类、接口还是方法都要做到只干一件事。
举例 NO.1
下面的类图展示了用户相关的一些信息的操作行为,而且我们也会经常习惯性的去这样写一个类:
那么这个类存在什么问题呢? 乍一看没什么,其实是将用户属性和用户的行为混在了一起,对于用户属性一般我们应该是封装一个bean类,而用户行为应该独立出来,为什么?因为从业务逻辑来讲这在很大程度上会是变化的东西,比如现在是有login()、deleteUser()、addUser()和changePassword()几个行为,那么将来某一天老板说我加一个用户签到功能,好,于是你就要到IUserInfo接口中添加一个sign()方法, 结果隔了一天老板又说我要加一个用户评论功能,于是你又要加一个comment()方法。。这是不是在反复的修改这个类(接口)?所以这就没有做到有且仅有一个引起变化类的原因,所以我们要将那些变化的原因提取出来单独封装,而那些基础的属性像userName,userId的操作基本是共用而且不会变的。
所以我们按照单一职责,应该这个接口改造如下:
可以看到现在变成了两个接口,一个负责用户属性,一个负责用户行为,当然这里只是单独针对这个接口的设计而言按照单一职责进行示例,实际当中可能更倾向于使用两个类来实现这两个不同的接口进行操作,因为要做到代码复用。所以进一步改造如下:
举例 NO.2
这里举另外一个简单的例子来自于经典的《HeadFirst设计模式》,也是此书中的开篇例子,鸭子类的设计:
可以看到我们现在的类图设计是这样的,有一个鸭子的基类Duck,它具有quack(呱呱叫)、swim(游泳)、fly(飞行)、display(外观展示)几个方法,然后我们现在要用这个基类来生成一些鸭子,比如绿头鸭MallardDuck、红头鸭RedDuck以及玩具橡皮鸭RubberDuck,其中绿头鸭和红头鸭都是真实的鸭子,它们具有鸭子的基本的呱呱叫、会游泳和会飞本领,只是展示外观不同,所以覆写display()方法,而橡皮鸭也是一种鸭子但是它是一种玩具,只会吱吱叫并且不会飞,所以我们除了覆写display()以外,我们还要覆写quack()和fly()方法。
好,那么现在问题来了,并不是所有的鸭子类都是会呱呱叫和会飞的,假如boss现在要求再添加10种鸭子,其中有一些会叫但是不会飞,另一些会飞但是不会叫,还有一些是既不会飞也不会叫,那你是不是要到每一个子类中去覆写quack()和fly()实现不同的代码?所以这个就是变化的部分,就要按照单一职责,将变化的与不变的隔离开来,很显然我们需要将quack和fly这两个具有个体变化的行为独立分离出来,改进如下:
到这里,我们已经把Duck类中两个变化的个体行为职责从Duck类中剥离了出来,现在fly和quack行为作为两个独立的接口,只有需要他们的那些子类才去实现它们。这里你可能疑问,即便我的子类实现了fly和quack相关接口,还是要到每一个子类中去实现相关的方法,对的,因为java接口没有实现代码,所以不能做到代码复用,要解决这个问题,我们需要进一步做改造,这个改造涉及到了另一个设计原则,所以我们放到后面去讲。总之,到这里我们的Duck类已经完成了单一职责的改造,fly和quack的行为不会再影响它了(尽管有点问题,但我们在后面的原则中会很好的解决它)。
如果用一句话总结SRP遵循的OO设计法则就是:**封装变化的部分。**找出应用中那些会变化的地方,把它们独立取出并封装起来,将其和固定不变的代码相分离。这样保证了系统其他部分不受影响,代码引起的变化变少,系统更加有弹性。
单一职责体现的 OO特征:封装、抽象。
单一职责的好处:
其实单一职责是一个很有争议的职责,因为在实际项目当中最难确定的就是"职责",一个类到底负责的是哪些职责?有时我们往往难以确定。在实际当中很多的情况下,最终妥协的结果就是破坏了单一职责。
因为实际项目往往要受制于工期、成本、环境、开发人员的技术水平等诸多要素影响。试想,如果现在用户有一个需求明天就要上线,当你得知把所有可用的东西组合在一起就能瞬间立马满足用户的需求,你还会去考虑单一职责吗?或者跟第三方的开发平台进行合作,对方直接扔过来一句话告诉你:调用这个方法就OK,其他什么都不用管。这个时候我们也很难保证设计的合理性。
尽管如此,实际困难种种,但良好的设计是我们永远追求的目标,所以只能尽量保证你的类、接口以及方法满足SRP原则,但这并不一定是最符合你的实际的,我们只能做到尽量。
定义:一个软件实体应该对扩展开放,对修改关闭。
开闭原则的英文全称是Open-Close Principle, 简称OCP。
这里所说的软件实体包括:项目中按照一定逻辑规则划分的模块、类、抽象类以及方法。
开放封闭有两个含义,一个是对于拓展是开放的,另一个是对于修改是封闭的。一个软件在生命周期内总要面对需求的变化,那么对于开发者来说怎么处理变化的需求,在实际当中往往是新需求一来,我们就要把类重新改一遍, 这显然是令人头疼的。开闭原则告诉我们的是,尽量通过扩展软件的实体来实现变化,而不是通过修改已有的代码来完成变化。 可以说开闭原则是对软件实体的未来变化的一种约束性的原则。
举例
以书店售书为例,其类图如下:
其中BookStore是书店,IBook是书籍的抽象接口,它拥有三个方法分别返回书的名称、售价和作者,NovelBook是实现IBook接口的的小说类书籍。
相关代码:
public interface IBook {
/**获取书籍名称*/
public String getName();
/**获取书籍价格*/
public int getPrice();
/**获取书籍作者*/
public String getAuthor();
}
public class NovelBook implements IBook {
private String name;
private int price;
private String author;
public NovelBook(String name, int price, String author) {
this.name = name;
this.price = price;
this.author = author;
}
@Override
public String getName() {
return name;
}
@Override
public int getPrice() {
return price;
}
@Override
public String getAuthor() {
return author;
}
}
public class BookStore {
private final List<IBook> mBookList = new ArrayList<>();
public BookStore() {
setBooks();
}
private void setBooks() {
mBookList.add(new NovelBook("天龙八部", 3200, "金庸"));
mBookList.add(new NovelBook("巴黎圣母院", 5600, "雨果"));
mBookList.add(new NovelBook("悲惨世界", 3500, "雨果"));
}
public void printSellingBooks() {
for (IBook iBook : mBookList) {
System.out.println( "书籍名称:" + iBook.getName() + ", 书籍售价:"
+ iBook.getPrice()/100.0 + "元, 书籍作者:" + iBook.getAuthor() );
}
}
}
public class Main {
public static void main(String[] args) {
BookStore bookStore = new BookStore();
System.out.println("---------------书店卖出的书籍记录如下---------------");
bookStore.printSellingBooks();
}
}
上面代码中我们添加了几本小说当做书店销售的书籍,并在main函数中将它打印出来,输出如下:
现在代码写好了,书店开始按照这个代码销售书籍,过了一段时间之后,营销状况不是很好,书店决定对书籍进行打折促销活动:40元以上的书9折销售,其它的8折销售。那么,我们的代码该如何来修改呢:
public class OffNovelBook extends NovelBook {
public OffNovelBook(String name, int price, String author) {
super(name, price, author);
}
/**
* 重写getPrice()实现打折处理
*/
@Override
public int getPrice() {
int price = super.getPrice();
int offPrice = 0;
if (price > 4000) {
//原价大于40元打9折
offPrice = price * 90 / 100;
} else {
offPrice = price * 80 / 100;
}
return offPrice;
}
}
public class BookStore {
private final List<IBook> mBookList = new ArrayList<>();
public BookStore() {
setBooks();
}
private void setBooks() {
mBookList.add(new OffNovelBook("天龙八部", 3200, "金庸"));
mBookList.add(new OffNovelBook("巴黎圣母院", 5600, "雨果"));
mBookList.add(new OffNovelBook("悲惨世界", 3500, "雨果"));
}
public void printSellingBooks() {
for (IBook iBook : mBookList) {
System.out.println( "书籍名称:" + iBook.getName() + ", 书籍售价:"
+ iBook.getPrice()/100.0 + "元, 书籍作者:" + iBook.getAuthor() );
}
}
}
可以看到代码修改比较简单,在不影响原来NovelBook的前提下我们只是增加子类覆写了getPrice()方法进行打折处理,通过扩展完成了任务,相应的BookStore中将NovelBook替换成OffNovelBook就可以,运行程序,打印如下:
开闭原则告诉我们的思想是:一旦我们写出了可以正常工作的代码,就要努力保证这段代码可以一直正常工作,而不被破坏掉。当我们的编程水平和代码质量到了一定的水平,就需要提高代码自我的克制能力,当遇到问题时,优先保证之前的系统是可以work的然后再去扩展,而不是采取比较浪的方法去任意的修改,一旦你破坏了原有的结构,就引入了风险,而一旦你陷入了修改原有代码的思维模式,当你改的越来越多,在原有结构中插入越来越多的逻辑和碎片时,你会发现系统稳定性变得十分脆弱,不堪一击,稍有bug系统就会陷入尴尬。
如何应用开闭原则:
public interface IComputerBook extends IBook {
//范围领域
public String getScope();
}
public class ComputerBook implements IComputerBook {
private String name;
private int price;
private String author;
private String scope;
@Override
public String getName() {
return name;
}
@Override
public int getPrice() {
return price;
}
@Override
public String getAuthor() {
return author;
}
@Override
public String getScope() {
return scope;
}
}
public class BookStore {
private final List<IBook> mBookList = new ArrayList<>();
public BookStore() {
setBooks();
}
private void setBooks() {
mBookList.add(new NovelBook("天龙八部", 3200, "金庸"));
mBookList.add(new NovelBook("巴黎圣母院", 5600, "雨果"));
mBookList.add(new NovelBook("悲惨世界", 3500, "雨果"));
mBookList.add(new ComputerBook("Think in Java", 4300, "Bruce Eckel", "编程语言"));
}
public void printSellingBooks() {
for (IBook iBook : mBookList) {
System.out.println( "书籍名称:" + iBook.getName() + ", 书籍售价:"
+ iBook.getPrice()/100.0 + "元, 书籍作者:" + iBook.getAuthor() );
}
}
}
运行程序,打印如下:
这个代码中我们可以看到ComputerBook必须实现来自IBook接口的三个方法约束,因为IComputerBook继承了IBook接口,也就是说IBook对我们新增加的ComputerBook类产生了约束力,还有我们的mBookList是定义成接口的持有而不是实现类,如果你改成List
你会发现根本无法往里面添加ComputerBook对象,再者我们的getScope()方法能不能直接添加到IBook接口上而不用新增ComputerBook子类呢,当然不能,因为IBook接口影响的子类NovelBook已经在销售运行中,如果动IBook那么所有NovelBook势必要受到影响。所以这就是所谓的通过抽象约束来实现开闭原则。
开闭原则体现的OO特征:封装、继承、抽象。
开闭原则的好处:
- 提高可复用性 , 因为我们是对修改关闭,也就是说可以工作的逻辑代码高度集中并且基本不会变的,这部分代码就可以拿来复用的,这也正是我们封装的目标之一。
- 提高可维护性,遵循开闭原则的一个结果就是每次的修改都不会对之前的代码造成任何影响,那么维护人员也就无需关心之前的代码会出问题,只需要把精力放到本次的扩展修改的代码上。
定义:所有引用基类(父类)的地方必须能透明地使用其子类的对象。
里氏替换原则英文全称是Liskov Substitution Principle,简称LSP。
对定义简单的理解:凡是父类出现的地方子类就可以出现,而且替换为子类也不会产生任何错误和异常。使用者可能根本不需要关心使用的是父类还是子类,但是,反过来就不行,有子类出现的地方,父类不一定能适应。
举例
我们平时写一个接口或者抽象类,然后编码实现,调用的时候传入接口或者抽象类作为参数,其实这个时候我们已经应用了LSP原则了。下面的例子是经典的射击类游戏CS游戏当中涉及到场景,类图如下:
士兵使用枪来杀敌人,具体是使用什么枪要调用的时候才知道,而枪的职责是负责射击,不同的枪射击方法不一样,具体要在在枪的各个子类中去实现。相关代码实现:
public abstract class AbstractGun {
public abstract void shoot();
}
public class HandGun extends AbstractGun {
@Override
public void shoot() {
System.out.println("手枪射击");
}
}
public class Rifle extends AbstractGun {
@Override
public void shoot() {
System.out.println("步枪射击");
}
}
public class MachineGun extends AbstractGun {
@Override
public void shoot() {
System.out.println("机枪扫射");
}
}
public class Soldier {
//定义士兵的枪
private AbstractGun gun;
//给士兵一支枪
public void setGun(AbstractGun gun) {
this.gun = gun;
}
public void killEnemy() {
System.out.println("士兵开始杀敌....");
this.gun.shoot();
}
}
public class Client {
public static void main(String[] args) {
//创造一个士兵
Soldier soldier = new Soldier();
//给士兵一支步枪
soldier.setGun(new Rifle());
soldier.killEnemy();
//给士兵一支手枪
soldier.setGun(new HandGun());
soldier.killEnemy();
//给士兵一支机枪
soldier.setGun(new MachineGun());
soldier.killEnemy();
}
}
运行程序,打印如下:
注意,我们的Soldier类当中定义的gun是AbstractGun类型的,是一把抽象的枪,具体是一把什么样的枪在上战场前setGun才能确定,而在Client类当中我们给士兵一把步枪,士兵便使用步枪开始杀敌,士兵要使用手枪我们就set一个HandGun对象进去,士兵便使用手枪开始杀敌,士兵要使用机枪我们就set一个MachineGun对象进去,士兵便使用机枪开始杀敌。可以看到,在实现Soldier类的代码时我们根本就不需要关心士兵到底使用的是一把什么样的枪,只要是一把枪AbstractGun就行,在实际使用的场景当中它会被替换为一把真正的枪。
假如现在我们有一把玩具枪,把它添加到上面的设计中去,理所当然的,类图被修改如下:
因为玩具枪是不能射击杀人的所以我们不能实现shoot方法,代码修改如下:
public class ToyGun extends AbstractGun {
@Override
public void shoot() {
//玩具枪不能射击杀人,所以这个方法空实现
}
}
假如我们在Client中使用这把玩具枪,结果会如何呢
public class Client {
public static void main(String[] args) {
Soldier soldier = new Soldier();
soldier.setGun(new ToyGun());
soldier.killEnemy();
}
}
运行程序,打印结果:
可以看到我们的士兵在使用玩具枪开始杀敌了,但是玩具枪是不具备射击杀人功能的呀,所以士兵会干着急。那么怎么解决这个问题呢:
instanceof
判断,如果是玩具枪就不用来杀敌。if (this.gun instanceof ToyGun) {
//do nothing
} else {
System.out.println("士兵开始杀敌....");
this.gun.shoot();
}
这样可以解决问题,但是这样以后每增加一个类型的枪就可能要去修改Soldier类,很显然这违反了OCP原则。
上面的例子体现了LSP原则的一个特点:子类必须完全实现父类的方法。如果子类不能完全实现父类的方法,那么最好不再继续使用继承关系,使用依赖、组合关系来代替继承。
前面提到,父类出现的地方可以使用子类代替,但是反过来就不行,子类出现的地方,父类未必能胜任,下面举例说明为什么反过来不行:
我们在步枪的下面添加两种型号的枪,AK47和AUG狙击步枪,类图设计如下:
其中AUG是Rifle的子类,而狙击手Snipper射击严重依赖于AUG狙击步枪。相关代码:
public class AUG extends Rifle {
public void zoomOut() {
System.out.println("使用望远镜观察敌人...");
}
@Override
public void shoot() {
System.out.println("使用AUG射击...");
}
}
public class Snipper {
private AUG aug;
public void setGun(AUG aug){
this.aug = aug;
}
public void killEnemy() {
this.aug.zoomOut();
this.aug.shoot();
}
}
public class Client {
public static void main(String[] args) {
Snipper snipper = new Snipper();
snipper.setGun(new AUG());
snipper.killEnemy();
}
}
运行程序,打印如下:
好,现在我们将使用子类AUG的地方替换为父类试试,修改Client代码如下:
public class Client {
public static void main(String[] args) {
Snipper snipper = new Snipper();
snipper.setGun((AUG) new Rifle());
snipper.killEnemy();
}
}
运行发现报java.lang.ClassCastException
异常,即向下转型是不安全的,所以LSP原则反过来是不行的,子类出现的地方,父类未必可以出现。
里氏替换原则的两个约束条件:
1. 覆写或重载父类方法时输入参数可以被放大
覆写或重载时同名方法的输入参数,子类必须要比父类更加宽松而不能小于父类的参数范围,否则就会出现父类出现的地方子类不能代替,从而违背LSP原则。
public class Father {
public Collection doSomething(HashMap map) {
System.out.println("父类被执行");
return map.values();
}
}
public class Son extends Father {
public Collection doSomething(Map map) {
System.out.println("子类被执行");
return map.values();
}
}
2. 覆写或重载父类方法时输出结果可以被缩小
父类的一个方法返回值的类型为T,子类相同方法的返回类型为S,LSP原则要求S必须小于等于T,即要么S和T是同一类型,要么S是T的子类。
public class Father {
public List<String> getList() {
System.out.println("父类被执行");
List<String> list = new ArrayList<>();
list.add("a");
return list;
}
}
public class Son extends Father {
@Override
public ArrayList<String> getList() {
System.out.println("父类被执行");
ArrayList<String> list = new ArrayList<>();
list.add("a");
return list;
}
}
里氏替换原则体现的OO特征:继承、多态。
里氏替换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。
定义:高层模块不应该依赖低层模块,两个都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。
依赖倒置原则英文全称是Dependence Inversion Principle, 简称DIP。
高层模块和低层模块的理解: 每一个逻辑的实现都是由原子逻辑组成的,不可分割的原子逻辑就是低层模块,原子逻辑的再组装就是高层模块。
在Java中,抽象就是指接口或者抽象类,两者都是不能直接被实例化的;细节就是实现类,实现接口或者继承抽象类而产生的就是细节,也就是可以通过new产生的对象。高层模块就是调用端,低层模块就是具体实现类。
依赖倒置原则在Java中的体现就是:
一句话总结依赖倒置原则就是:面向接口编程,而不是面向实现编程。
举例 NO.1
一个司机驾驶奔驰的例子,类图如下:
其中司机Driver有一个drive方法来驾驶奔驰车,奔驰汽车Benz有一个run方法来启动汽车。Driver类依赖于Benz类, 相关代码:
public class Benz {
public void run() {
System.out.println("奔驰汽车开始运行...");
}
}
public class Driver {
public void drive(Benz benz) {
benz.run();
}
}
public class Client {
public static void main(String[] args) {
Driver zhangSan = new Driver();
Benz benz = new Benz();
//张三开奔驰
zhangSan.drive(benz);
}
}
运行程序,我们创造的张三司机可以顺利的驾驶奔驰汽车了,如果现在张三司机不仅要开奔驰还要开宝马,怎么做呢,先生成宝马汽车:
public class BMW {
public void run() {
System.out.println("宝马汽车开始运行...");
}
}
现在BMW宝马汽车有了,但是张三却无法驾驶它,为啥,因为司机类Driver的drive方法耦合的是Benz类的对象参数,没法传递新生成的BMW对象,这时你可能会去Driver里添加一个可以专门传递BMW对象的方法,很多时候你都会这样去做,随着业务的增加,我们的Driver会不断的修改,这导致我们的系统不稳定且越来越难以维护,这就说明系统设计存在问题,Driver和Benz严重耦合导致新需求来时只能再去修改Driver类。而依赖倒置就是为了解决这类问题的,我们按照依赖倒置原则重新设计类图如下:
在该类图当中IDriver接口依赖ICar接口,drive方法传递ICar接口类型的参数。子类Driver实现IDriver接口,奔驰和宝马汽车的具体类则实现ICar接口。相关代码:
public interface IDriver {
//是司机就可以驾驶汽车
public void drive(ICar car);
}
public interface ICar {
//是汽车就可以跑
public void run();
}
public class Benz implements ICar {
@Override
public void run() {
System.out.println("奔驰汽车开始运行...");
}
}
public class BMW implements ICar {
@Override
public void run() {
System.out.println("宝马汽车开始运行...");
}
}
public class Driver implements IDriver {
@Override
public void drive(ICar car) {
car.run();
}
}
public class Client {
public static void main(String[] args) {
IDriver zhangSan = new Driver();
ICar benz = new Benz();
//张三开奔驰
zhangSan.drive(benz);
}
}
可以看到我们的张三司机可以开奔驰汽车,其中IDriver接口的drive方法现在不依赖于具体的Benz或者BMW的实现类,即“抽象不依赖于细节”,而我们的Driver实现类的drive方法也是依赖于ICar而不是具体的Benz或者BMW对象,即“细节依赖于抽象”。抽象的Driver依赖于抽象的Car, 司机开的是抽象的车,在构造接口的阶段我们根本都不需要关心司机将来要开的具体是什么样的鸟车,我们只需要知道司机会驾驶车就可以了。这样的做到的好处就是,假如现在司机要开宝马汽车,好,客户端代码就可以轻松的应对:
public class Client {
public static void main(String[] args) {
IDriver zhangSan = new Driver();
ICar bmw = new BMW();
//张三开宝马
zhangSan.drive(bmw);
}
}
这样宝马车就可以开动起来了,注意在Client代码当中司机和汽车对象变量的引用我们都是使用的接口类型IDriver和ICar, 而不是具体的Driver和BMW对象,在创建对象以后的操作都是完全针对接口类型进行的,这就是“高层模块不依赖于低层”。
举例 NO.2
在前面单一职责中提到的鸭子类设计的例子时候留下了一个坑,这里就利用依赖倒置原则把这个坑填上。在单一职责中把鸭子呱呱叫和飞行两个变化的行为单独提取到了两个接口当中,每个Duck实现类都需要去选择是否实现相应的接口,比较麻烦,现在我们为每一种接口建立一组实现子类。类图设计如下:
现在我们为鸭子类添加呱呱叫和飞行这两种行为的抽象依赖,完整的类图设计如下:
相关实现代码如下:
public interface FlyBehavior {
public void fly();
}
public class FlyWithWings implements FlyBehavior {
@Override
public void fly() {
System.out.println("I'm Flying");
}
}
public class FlyNoWay implements FlyBehavior {
@Override
public void fly() {
//do nothing
}
}
public interface QuackBehavior {
public void quack();
}
public class Quack implements QuackBehavior {
@Override
public void quack() {
System.out.println("Quack");
}
}
public class Squeak implements QuackBehavior {
@Override
public void quack() {
System.out.println("Squeak");
}
}
public class MuteQuack implements QuackBehavior {
@Override
public void quack() {
//do nothing
}
}
public abstract class Duck {
FlyBehavior flyBehavior;
QuackBehavior quackBehavior;
public abstract void display();
public void performFly() {
flyBehavior.fly();
}
public void performQuack() {
quackBehavior.quack();
}
public void setFlyBehavior(FlyBehavior fb) {
this.flyBehavior = fb;
}
public void setQuackBehavior(QuackBehavior qb) {
this.quackBehavior = qb;
}
public void swim() {
System.out.println("All ducks float, event decoys!");
}
}
public class MallardDuck extends Duck{
@Override
public void display() {
System.out.println("I'm real Mallard duck");
}
}
public class DuckClient {
public static void main(String[] args) {
Duck mallardDuck = new MallardDuck();
QuackBehavior quack = new Quack();
FlyBehavior flyWithWings = new FlyWithWings();
mallardDuck.setFlyBehavior(flyWithWings);
mallardDuck.setQuackBehavior(quack);
mallardDuck.performQuack();
mallardDuck.performFly();
}
}
运行结果:
按照依赖倒置原则,不管我们的抽象类Duck还是实现类MallardDuck都是对FlyBehavior和QuackBehavior进行了抽象的依赖。在更换鸭子行为的时候,我们的Duck实现类跟具体的行为实现完全解耦,Duck类只需要接收抽象的行为接口,任意实现接口的行为都可以快速的完成修改。
这个例子告诉我们一个OO法则:多用组合,少用继承。组合尽量持有对抽象的依赖,这样才能够解耦具体实现类。
依赖倒置中依赖传递的三种方式:
1. 构造函数依赖注入
public interface IDriver {
//是司机就可以驾驶汽车
public void drive();
}
public class Driver implements IDriver {
private ICar car;
public Driver(ICar car) {
this.car = car;
}
@Override
public void drive() {
car.run();
}
}
2. Setter依赖注入
public interface IDriver {
//设置车辆
public void setCar(ICar car);
//是司机就可以驾驶汽车
public void drive();
}
public class Driver implements IDriver {
private ICar car;
@Override
public void setCar(ICar car) {
this.car = car;
}
@Override
public void drive() {
car.run();
}
}
3. 接口依赖注入
public interface IDriver {
//是司机就可以驾驶汽车
public void drive(ICar car);
}
public class Driver implements IDriver{
@Override
public void drive(ICar car) {
car.run();
}
}
很显然,在三种依赖注入方式当中setter依赖注入方式应该是最灵活的,因为可以在任意阶段进行注入,也可以随时切换注入的对象类型,实现动态插拔替换,这正是策略模式的原型。
依赖倒置原则体现的OO特征:抽象、多态。
如何使用依赖倒置原则,遵循以下几个原则:
依赖倒置原则是开闭原则的强化原则,满足依赖倒置原则也就做到了对扩展开放,对修改关闭,不能满足依赖倒置就很难做到开闭原则。这个原则也是6个设计原则当中最难以实现的原则,但是一旦你实现了依赖倒置就能够打破传统思维,摆脱对具体实现的耦合性依赖,做到以不变应万变。
依赖倒置原则固然好,但设计原则不是万能的,实际当中还是会依赖一些细节,所以不要为了遵循设计原则而去遵循设计原则。就像不要为了考试而去学习一些知识,为了考试而学习知识的结果往往就是应试教育的漩涡,这也是中国现阶段教育体制的严重问题。唯物辩证法告诉我们事物都是普遍联系的,而矛盾是对立统一的,个性和共性是对立统一的,可能性和现实性是对立统一的,现象和本质是对立统一的,相对和绝对是对立统一的,所以要具体问题具体分析,实践是检验真理的唯一标准。
定义:一个类对另一个类的依赖应该建立在最小的接口上,客户端不应该依赖它不需要的接口。
接口隔离原则英文全称Interface Segregation Principle,简称ISP。
这里接口的含义有两种:
对定义的理解:接口要做到细化单一,不用建立庞大臃肿的接口。接口中的方法尽量少。为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。如果有一个接口里面包含了各种模块的方法,那么就需要进行细化拆分,但细化拆分并不是无限制的细化每一个方法就对应写一个接口,细化拆分的下限和底线是做到满足单一职责,如果都不满足单一职责了那么接口的细化就没有意义了。在为一个类提供定制服务的时候,只暴露那些客户需要的方法,隐藏那些客户不需要的方法,做到以最少的方法完成最多的事,提高内聚。
举例
现在设计一个接口描述人类的一些特性,比如人会吃饭、睡觉、步行、讲话、工作、思考等等,一个真实的人会具备所有的这些特性,而机器人只具备其中的某些特性,类图设计如下:
可以看到我们在Human接口中的方法描述了人的基本特性,真实的人Man类实现Human接口并实现接口的全部方法,其它的假人Dummy、扫地机器人SweepRobot、双足机器人BipedRobot以及AI机器人AIRobot它们也实现了Human接口,机器人只具备某一些人类的特性,比如扫地机器人它只会扫地别的啥也不会干,AI机器人可能会讲笑话唱歌与人进行简短的对话,双足机器人会像人类一样直立行走,假人则只具备人类的形状,但是由于它们都实现了Human接口,所以Humman中的所有方法即便它们不需要也不得不去实现。这个就是违反接口隔离原则的例子,用代码来实现该设计,你会更容易发现问题:
public interface Human {
public void talk();
public void sleep();
public void walk();
public void eat();
public void work();
public void think();
public void display();
}
/** 真实的人 */
public class Man implements Human {
@Override
public void talk() {
System.out.println("讲话");
}
@Override
public void sleep() {
System.out.println("睡觉");
}
@Override
public void walk() {
System.out.println("行走");
}
@Override
public void eat() {
System.out.println("吃饭");
}
@Override
public void work() {
System.out.println("工作");
}
@Override
public void think() {
System.out.println("思考");
}
@Override
public void display() {
System.out.println("拥有人类外形");
}
}
/** 假人 */
public class Dummy implements Human {
@Override
public void display() {
System.out.println("拥有人类外形");
}
@Override
public void talk() {}
@Override
public void sleep() {}
@Override
public void walk() {}
@Override
public void eat() {}
@Override
public void work() {}
@Override
public void think() {}
}
/** 扫地机器人 */
public class SweepRobot implements Human {
@Override
public void display() {
System.out.println("我是圆形的");
}
@Override
public void work() {
System.out.println("打扫卫生");
}
@Override
public void talk() {}
@Override
public void sleep() {}
@Override
public void eat() {}
@Override
public void walk() {}
@Override
public void think() {}
}
/** 双足机器人 */
public class BipedRobot implements Human {
@Override
public void display() {
System.out.println("拥有人类外形");
}
@Override
public void walk() {
System.out.println("双足直立行走");
}
@Override
public void talk() {}
@Override
public void sleep() {}
@Override
public void eat() {}
@Override
public void work() {}
@Override
public void think() {}
}
/** AI机器人 */
public class AIRobot implements Human {
@Override
public void talk() {
System.out.println("我能讲笑话、还能聊天哦");
}
@Override
public void think() {
System.out.println("我还能回答问题、下围棋");
}
@Override
public void display() {
System.out.println("我是虚拟程序,没有外形");
}
@Override
public void walk() {}
@Override
public void sleep() {}
@Override
public void eat() {}
@Override
public void work() {}
}
以上就是实现代码,你会发现Dummy、SweepRobot、BipedRobot和AIRobot几个类只实现了Human接口的几个方法,其它方法都做了空实现,只有Man类实现了全部方法,也就是说对于机器人相关的实现类而言,接口方法是存在冗余的,所以按照接口隔离原则我们这种情况下就要对接口进行细化拆分,修改类图如下:
可以看到我们将接口进行了拆分,相关方法独立成为接口,每一个实现类只依赖跟它直接相关的接口。相关代码修改如下:
public interface Intelligence {
public void talk();
public void think();
}
public interface Work {
public void work();
}
public interface Display {
public void display();
}
public interface Action {
public void walk();
}
public interface Human extends Intelligence, Work, Display, Action {
public void sleep();
public void eat();
}
/** 真实的人 */
public class Man implements Human {
@Override
public void talk() {
System.out.println("讲话");
}
@Override
public void sleep() {
System.out.println("睡觉");
}
@Override
public void walk() {
System.out.println("行走");
}
@Override
public void eat() {
System.out.println("吃饭");
}
@Override
public void work() {
System.out.println("工作");
}
@Override
public void think() {
System.out.println("思考");
}
@Override
public void display() {
System.out.println("拥有人类外形");
}
}
/** 假人 */
public class Dummy implements Display {
@Override
public void display() {
System.out.println("拥有人类外形");
}
}
/** 双足机器人 */
public class BipedRobot implements Display, Action {
@Override
public void display() {
System.out.println("拥有人类外形");
}
@Override
public void walk() {
System.out.println("双足直立行走");
}
}
/** 扫地机器人 */
public class SweepRobot implements Display, Work {
@Override
public void display() {
System.out.println("我是圆形的");
}
@Override
public void work() {
System.out.println("打扫卫生");
}
}
/** AI机器人 */
public class AIRobot implements Display, Intelligence {
@Override
public void talk() {
System.out.println("我能讲笑话、还能聊天哦");
}
@Override
public void think() {
System.out.println("我还能回答问题、下围棋");
}
@Override
public void display() {
System.out.println("我是虚拟程序,没有外形");
}
}
现在机器人相关的实现类代码变的简洁了许多,再也不用实现跟它没有关系的方法做空实现了。
接口隔离原则体现的OO特征:封装、抽象。
接口隔离原则可以给系统带来灵活性的优点,但是接口不能无限制的细分,细化拆分必须在满足单一职责的条件下进行,这个细化的颗粒度的把握往往要凭经验常识或者根据业务逻辑来判断。接口隔离原则要求我们的接口或者类尽量使用原子接口和原子类来组装,一个接口只服务于一个模块或者业务逻辑。
定义:一个软件实体应当尽可能少地与其他实体发生相互作用。
迪米特法则(Law of Demeter,LoD)又叫作最少知识原则(Least Knowledge Principle 简写LKP)。
迪米特法则要求我们尽量或者减少类之间的直接交互,如果两个对象之间不必彼此直接通信,那么这两个对象就不应当发生任何直接的相互作用,如果一个对象的某一个方法需要调用另一个对象的诸多方法,可以通过第三者转发这个调用,调用者对被调用者内部的情况知道的越少越好。
举例
小明在app上点外卖,他选择了一家披萨店,并向披萨店下了一份订单,披萨店接收了订单并开始为小明准备披萨,类图设计如下:
实现代码:
public class PizzaStore {
public void takeOrder() {
System.out.println("披萨店接收订单");
}
public void prepareSource() {
System.out.println("披萨店准备披萨制作原料");
}
public void cookPizza() {
System.out.println("披萨店烹制披萨");
}
public void packPizza() {
System.out.println("披萨店打包披萨");
}
public void deliveryPizza() {
System.out.println("披萨店配送披萨");
}
}
public class Customer {
public void orderPizza(PizzaStore pizzaStore){
pizzaStore.takeOrder();
pizzaStore.prepareSource();
pizzaStore.cookPizza();
pizzaStore.packPizza();
pizzaStore.deliveryPizza();
}
public static void main(String[] args){
Customer xiaoming = new Customer();
PizzaStore pizzaStore = new PizzaStore();
xiaoming.orderPizza(pizzaStore);
}
}
运行程序结果:
可以看到小明发起订单后披萨店的一个整个过程,包括接收订单、准备原材料、制作披萨、打包、配送等,这样写代码貌似没有问题,但是对于小明来说这些过程其实他根本就不需要关心,小明跟披萨店的交互其实很简单,就是我给你订单,你给我披萨就可以,可是现在小明知道了过多的披萨店的方法,这个就是违反迪米特法则的例子,我们按照迪米特法则进行修改,设计如下:
代码修改:
public class PizzaStore {
private void takeOrder() {
System.out.println("披萨店接收订单");
}
private void prepareSource() {
System.out.println("披萨店准备披萨制作原料");
}
private void cookPizza() {
System.out.println("披萨店烹制披萨");
}
private void packPizza() {
System.out.println("披萨店打包披萨");
}
private void deliveryPizza() {
System.out.println("披萨店配送披萨");
}
public void orderPizza() {
takeOrder();
prepareSource();
cookPizza();
packPizza();
deliveryPizza();
}
}
public class Customer {
public void orderPizza(PizzaStore pizzaStore){
pizzaStore.orderPizza();
}
public static void main(String[] args){
Customer xiaoming = new Customer();
PizzaStore pizzaStore = new PizzaStore();
xiaoming.orderPizza(pizzaStore);
}
}
执行结果跟前面一样,但是现在代码发生了一些变化,我们在PizzaStore类中新增了一个orderPizza()方法来封装所有的披萨定制流程,同时PizzaStore类中的其他方法都被声明为了private,对于小明来说他只需要调用PizzaStore的orderPizza()方法来预定披萨就可以了,不需要知道其内部的运转流程,并且现在实际上小明也无法调用PizzaStore的其他方法了,因为他看不到了。这就是迪米特法则所要求的尽量减少两个实体之间的交互。
这里我也可以使用另外一种方式来满足迪米特法则,那就是引入中间类,通过中间类来解耦调用者和被调用者,这里我们可以引入一个PizzaStoreProxy来解耦Customer和PizzaStore, 类图设计如下:
可以看到我们现在的Customer只跟PizzaStoreProxy进行依赖,而PizzaStoreProxy内部关联PizzaStore,相关代码:
public class PizzaStoreProxy {
private PizzaStore pizzaStore;
public PizzaStoreProxy(PizzaStore pizzaStore) {
this.pizzaStore = pizzaStore;
}
public void orderPizza() {
pizzaStore.takeOrder();
pizzaStore.prepareSource();
pizzaStore.cookPizza();
pizzaStore.packPizza();
pizzaStore.deliveryPizza();
}
}
public class PizzaStore {
public void takeOrder() {
System.out.println("披萨店接收订单");
}
public void prepareSource() {
System.out.println("披萨店准备披萨制作原料");
}
public void cookPizza() {
System.out.println("披萨店烹制披萨");
}
public void packPizza() {
System.out.println("披萨店打包披萨");
}
public void deliveryPizza() {
System.out.println("披萨店配送披萨");
}
}
public class Customer {
public void orderPizza(PizzaStoreProxy pizzaStoreProxy){
pizzaStoreProxy.orderPizza();
}
public static void main(String[] args){
Customer xiaoming = new Customer();
PizzaStore pizzaStore = new PizzaStore();
PizzaStoreProxy pizzaStoreProxy = new PizzaStoreProxy(pizzaStore);
xiaoming.orderPizza(pizzaStoreProxy);
}
}
可以看到通过引入一个代理类,小明现在只跟披萨店代理打交道,完全不关披萨店的事,也不知道披萨店的内部实现,实现了解耦。
我们用一句话总结迪米特法则的第一个含义就是:知道的越少越好,知道的越多对你没有好处。
迪米特法则还有另外一个含义:只和直接的朋友交流(only talk to your immedate frineds)。这是什么意思呢,就是说一个对象在调用另一个对象的过程中,或者在依赖另一个对象的方法中,不应该出现被依赖对象以外的其他对象与依赖者产生交互影响。
举例
为了激励公司员工工作的积极性,老板准备给公司的员工发红包,当然发红包这种小事怎么可能劳驾老板亲自动手呢,于是老板找来部门负责人,让他来代为处理,类图设计如下:
相关代码:
public class Boss {
public void sendRedPacket(GroupLeader groupLeader) {
List<Employee> memberList = groupLeader.getMemberList();
for (Employee employee : memberList) {
groupLeader.giveRedPacket("100元大红包", employee);
}
}
}
public class GroupLeader {
private List<Employee> memberList;
public List<Employee> getMemberList() {
return memberList;
}
public void setMemberList(List<Employee> memberList) {
this.memberList = memberList;
}
public void giveRedPacket(String packet, Employee employee) {
employee.setRedPacket(packet);
}
}
public class Employee {
private String packet;
public void setRedPacket(String packet){
this.packet = packet;
}
}
在代码中,Boss类通过sendRedPacket方法传递GroupLeader对象,然后GroupLeader对象获取到员工列表并调用giveRedPacket()方法给每个员工发了一百元红包。这样貌似很正常,其实在这个例子中与Boss类直接相关的是GroupLeader对象,但是在sendRedPacket()方法中却出现了Employee对象,这里的Employee就属于第三者,它跟Boss不是“直接的朋友”,只有GroupLeader跟Boss才算“直接的朋友”,所以这个例子也是违反迪米特法则的,如果代码是上面这样写的那么前面的设计类图当中应该有一条虚线从Boss指向Employee, 也就是说Boss依赖了不相关的Employee,按照迪米特法则应该去掉Employee对Boss类的依赖影响,修改如下:
代码修改:
public class Boss {
public void sendRedPacket(GroupLeader groupLeader) {
groupLeader.sendRedPacket("100元大红包");
}
}
public class GroupLeader {
private List<Employee> memberList;
public List<Employee> getMemberList() {
return memberList;
}
public void setMemberList(List<Employee> memberList) {
this.memberList = memberList;
}
public void giveRedPacket(String packet, Employee employee) {
employee.setRedPacket(packet);
}
public void sendRedPacket(String packet) {
List<Employee> memberList = getMemberList();
for (Employee employee : memberList) {
giveRedPacket(packet, employee);
}
}
}
修改后的代码中Boss类的sendRedPacket只跟groupLeader对象有关,移除了Employee对象的影响,降低了耦合性,而将发红包的逻辑移到了GroupLeader内部,现实当中也是如此Boss可能只需要给你传一句话即可,具体怎么执行Boss压根不关心,那是你要做的事。
好了,红包发完了,过了一段时间Boss想检查一下公司员工的工作积极性到底有没有提高,准备抽查一下出勤率,但是公司部门有几百号人抽查不过来,所以准备只抽查部门的某一个组的出勤情况。我们修改一下GroupLeader类并添加小组负责人对象:
public class GroupLeader {
private TeamLeader teamALeader;
private TeamLeader teamBLeader;
private TeamLeader teamCLeader;
public TeamLeader getTeamALeader() {
return teamALeader;
}
public void setTeamALeader(TeamLeader teamALeader) {
this.teamALeader = teamALeader;
}
public TeamLeader getTeamBLeader() {
return teamBLeader;
}
public void setTeamBLeader(TeamLeader teamBLeader) {
this.teamBLeader = teamBLeader;
}
public TeamLeader getTeamCLeader() {
return teamCLeader;
}
public void setTeamCLeader(TeamLeader teamCLeader) {
this.teamCLeader = teamCLeader;
}
}
public class TeamLeader {
private List<Employee> memberList;
public List<Employee> getMemberList() {
return memberList;
}
public void setMemberList(List<Employee> memberList) {
this.memberList = memberList;
}
}
我们的GroupLeader 现在并不直接管理员工了,GroupLeader的手下现在是TeamLeader,总共有三个组的组长分别负责teamA、teamB、teamC,并由三个TeamLeader分别管理各自组的员工。
好了,都闪开,Boss来点人了:
public class Boss {
public void count(GroupLeader groupLeader) {
//检查B组的人数
System.out.println(groupLeader.getTeamBLeader().getMemberList().size());
}
}
仔细想一下,这里有没有违法迪米特法则呢?
当然有违反,关键是在这一句代码groupLeader.getTeamBLeader().getMemberList().size()
,这句代码实际上耦合了多个类,而迪米特告诉我们与直接的朋友交流,这里直接的朋友只有GroupLeader,其他都不是,然而这里的方法调用还返回了TeamLeader对象,这是迪米特法则不希望看到的,即你是我的朋友,但你的朋友不一定是我的朋友。我们应该怎么修改呢,很简单,在GroupLeader里面再封装一个方法:
public class GroupLeader {
.....
public int getTeamBMemberCount() {
return this.teamBLeader.getMemberCount();
}
}
同时在TeamLeader 中也添加统计数量的方法:
public class TeamLeader {
.....
public int getMemberCount() {
return memberList.size();
}
}
现在,Boss中的方法就只依赖一个对象了:
public class Boss {
public void count(GroupLeader groupLeader) {
//检查B组的人数
System.out.println(groupLeader.getTeamBMemberCount());
}
}
这个例子告诉我们应该尽量避免getA().getB()getC().getD()
这种链式调用的的写法(返回同一个对象类型的除外),类与类的关系是建立在类之间的,不是方法间,一个方法尽量不要引入一个类中不存在的对象。
怎么才能很好的遵循迪米特法则表达的“与直接的朋友交流”呢,在一个对象的方法内,只调用属于以下范围的方法:
一个很好的遵循迪米特法则的示范代码:
public class Car {
Engine engine;
public Car(Engine engine) {
this.engine = engine;
}
public void start(Key key) {
Doors doors = new Doors();
boolean anthorized = key.turn(); //方法参数传入对象的方法
if (anthorized) {
engine.start(); //成员对象的方法
updateDisplay(); //对象本身的方法
doors.lock(); //本方法内实例的对象的方法
}
}
//更新显示
public void updateDisplay() {
}
}
迪米特法则体现的OO特征:封装。
迪米特法则的优点是可降低系统的耦合度,使类与类之间保持松耦合关系,松耦合的关系同时也提高了复用性。但也有缺点,有可能造成大量中转类或跳转次数的增加,增加系统的复杂性,跳转次数越多系统就越复杂从而越难以维护。所以在实际项目中要具体问题具体分析,在松耦合和可维护性之间做一个合理的取舍。
参考: