设计模式
设计模式的六大原则
1、开闭原则(Open Close Principle)
开闭原则的意思是:对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类。
通俗来讲就是一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化。软件实体指的就是代码,例如一个模块、类、方法。
用例子说明开闭原则,需求是销售饮料:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| package org.principle.OCP.version1;
public interface Water { public String getName(); public String getColor(); public int getPrice(); }
package org.principle.OCP.version1;
public class Cola implements Water{
private String name = "可口可乐"; private String color = "黑色"; private int price = 4;
public Cola(String name, String color, int price){ this.name = name; this.color = color; this.price = price; } public String getName() { return name; }
public String getColor() { return color; }
public int getPrice() { return price; }
@Override public String toString() { return "【" + getName() + "】是【" + getColor() + "】颜色的,价格为【" + getPrice() + "】元"; } }
public class Customer { public static void main(String[] args) { Water cola = new Cola("可口可乐","黑色",4); System.out.println(cola.toString());
Water baishi = new Cola("百事可乐","黑色",5); System.out.println(baishi.toString()); } }
|
上述代码是Customer类创建可乐对象,从而获得价格,这是一个很简单的代码。但需求变了,需要打8折销售,请问如何实现?
- 修改Cola的getPrice方法代码,加入打八折功能,这是最直接的,但有问题,我想看原价怎么办?
- 在Cola类中,添加打折方法discountPrice,也可以,但需要修改Water接口,这带来的影响就太大了,如果Water接口被10个类实现,那么就会影响10个类,这显然是不现实的,接口是约束、契约,是稳定的,不能随意修改
- 增加一个DiscountCola类,继承Cola,然后重写getPirce方法,这样无需改动已有代码只需要增加类即可,这就满足了**【扩展开发,对修改关闭】**
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| package org.principle.OCP.version1;
public class DiscountCola extends Cola{ public DiscountCola(String name, String color, int price) { super(name, color, price); }
@Override public int getPrice() { return (int) (super.getPrice() * 0.8); } }
package org.principle.OCP.version1;
public class Customer { public static void main(String[] args) { Water cola = new Cola("可口可乐","黑色",4); System.out.println(cola.toString());
Water baishi = new Cola("百事可乐","黑色",5); System.out.println(baishi.toString());
Water discountCola = new DiscountCola("百事可乐","黑色",5); System.out.println(discountCola.toString()); } }
|
使用开闭原则有什么好处呢?
- 对测试友好:上线之后的代码都是有意义的,而且经过了严格的测试,如果因为业务的增加,却修改了低层次模块(接口、抽象类),或者原有代码,就意味着原有的测试代码需要重新写,如果这个代码历史悠久,请问,你敢动吗?
- 提高复用性:代码功能越是单一,越能提高复用性,单一职责、接口隔离都是这样的。这避免了修改A功能时,对B功能产生影响
- 提高维护性:你退出项目组后,接手你工作的同事更喜欢扩展类,而不是修改原类,无论你写的多优秀,让别人读懂都是很痛苦的事情
面向对象的开发要求:万物皆对象,每个对象都有变化的特点,而如何应对变化就需要策略,这就需要再设计之初考虑所有变化因素,然后留下接口,等待“可能”变为“现实”
————————————————
2、里氏代换原则(Liskov Substitution Principle)
里式替换原则有两层定义:
定义一:
所有引用其父类对象方法的地方,都可以透明的替换为其子类对象
定义二:
如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有对象o1都替换为o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型
第一种定义:
基类是父类、子类通过extends关键字完成继承,从而建立父子类关系,以下面代码为例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
|
public interface Car { public void drive(); }
public class Jingji implements Car{ public void drive() { System.out.println("经济型"); } }
public class Vip implements Car{ public void drive() { System.out.println("VIP车型" + ",额外收费10元"); } }
public class Driver { private Car car; public void setCar(Car car){ this.car = car; }
public void drive(){ this.car.drive(); } }
public class Test { public static void main(String[] args) { Car car = new Vip(); Driver driver = new Driver(); driver.setCar(car); driver.drive(); } }
|
父类:Car
子类:Jingji、Vip
引用基类的地方:Driver
引用基类的地方必须透明的使用其子类对象,在代码中我们可以使用Car的Jingji、Vip两个子类,只需要修改类名,而无需修改其它代码。
第二种定义:
S类型对象o1:Vip
T类型对象o2:Car
程序P:Driver
当对象o1替换为o2:就是Vip变为Car
程序P的行为没有发生变化:都在调用drive方法,不需要修改代码
那么此时VIP是Car的子类
通过拆解发现,上面的代码是使用了里氏替换原则,使用extends、implements实现父子关系
里氏替换原则的优缺点,具有继承的所有优缺点:
优点:
- 提高代码重用性
- 子类可扩展自我特性
缺点:
- 具有侵入性,必须具有父类的所有属性和方法
- 降低灵活性,必须拥有父类的属性和方法,让子类多了约束
- 增强耦合性,如果修改父类的属性和方法,会对所有的子类产生影响
里氏替换原则为继承定义的规范,包含4层含义:
- 子类必须完全实现父类的方法
- 子类可以有自己个性
- 重载或者实现父类的方法时,输入参数可以放大
- 重写或者实现父类的方法时,输出结果可以被缩小
3、依赖倒转原则(Dependence Inversion Principle)
这个原则是开闭原则的基础,具体内容:针对接口编程,依赖于抽象而不依赖于具体。
我们先来看一个例子,某天项目经理要求写一个羊了个羊游戏,在线养羊
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| package org.principle.DIP;
class Sheep{ String name = "HappyShepp"; public Sheep(){ } public Sheep(String name){ this.name = name; } public void eat(String food){ System.out.println(name + "在吃" + food); } }
class Breeder{ Sheep sheep; public void setSheep(Sheep sheep){ this.sheep = sheep; } public void feed(String food){ System.out.println("饲养员在喂HappyShepp食物:" + food); } } public class Test { public static void main(String[] args) { Sheep sheep = new Sheep(); Breeder breeder = new Breeder(); breeder.setSheep(sheep); breeder.feed("青草");
} }
饲养员在喂HappyShepp食物:青草
|
但是后来游戏火爆,有客户反应想养哈士奇,这时候怎么办?
- 增加一个哈士奇类
- 给饲养员类增加setHashiqi方法
有人会说这还不简单,但不要忘了,我们现在写的功能及其简单,而且修改原本代码后,要重新测试Breeder与Sheep,这显然是重复工作,是绝对不允许的。
这就是典型的依赖正置,说白了这就是典型的面向实现编程,你要什么,我实现什么,不考虑事情的本质和扩展性,如果从依赖倒置的角度去写,应该将Sheep抽象化,将Breeder也抽象化,抽象化是依赖倒置的基础条件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| package org.principle.DIP;
abstract class Animal{ String name = "Animal"; public Animal(){ } public Animal(String name){ this.name = name; } public abstract void eat(String food); }
class Hashiqi extends Animal{ public Hashiqi(String name){ super(name); }
public void eat(String food) { System.out.println(name + "在吃" + food); } }
abstract class People{ String name = "快乐饲养员"; Animal animal;
public People(String name){ this.name = name; }
public void setAnimal(Animal animal){ this.animal = animal; } }
class HappyBreeder extends People{ public HappyBreeder(String name) { super(name); }
public void feed(String food){ System.out.println(name + "在喂" + this.animal.name +"食物:" + food); } }
public class Test2 { public static void main(String[] args) { Hashiqi hashiqi = new Hashiqi("哈士奇"); HappyBreeder breeder = new HappyBreeder("快乐饲养员"); breeder.setAnimal(hashiqi); breeder.feed("青草");
} }
快乐饲养员在喂哈士奇食物:青草
|
上面的代码用到了典型的依赖倒置,他将最终实现的功能进行抽象为Animal与People,而且setAnimal、setPeople参数也都是父类,这就保证了该类的稳定性,无论传什么子类,我都可以接受,这是不是就是里氏替换原则?
所以,依赖倒置的定义就是:
高级模块不应该依赖于低级模块。两者都应该依赖抽象。
抽象不应该依赖细节。
细节应该依赖于抽象。
依赖倒置应该遵循下面规则
- 每个类都尽量有接口或者抽象类,或者两者都具备
- 变量的表面类型尽量是接口或者抽象类
- 任何类都不应该从具体类派生
- 尽量不要重写父类的方法,以及特殊的重载(例如违反里氏替换原则)
4、接口隔离原则(Interface Segregation Principle)
什么是接口?
- 实例接口(Object Interface):Person zhangsan = new Person,Person类就是zhangSan的接口,Java中类也是一种接口
- 类接口(Class Interface):Java中经常用interface定义接口
什么是接口隔离?
- 客户端不需要依赖它不需要的接口
- 类的依赖关系应该建立在最小的接口上
总结来说:建立单一的接口,不要建立臃肿庞大的接口,因为实现类中可能只需要其中的某几个方法,不需要的方法,不要实现。
例如对于汽车的描述,汽车有座椅加热、涡轮增压、自动驾驶这些功能,代码可以这样实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| package org.principle.ISP;
interface Car{ public void seatHeating(); public void autoPilot(); public void turbo(); }
class Benz implements Car {
public void seatHeating() { System.out.println("座椅加热"); }
public void autoPilot() { System.out.println("自动驾驶"); }
public void turbo() { System.out.println("涡轮增压"); } }
class SanLing implements Car {
public void seatHeating() { }
public void autoPilot() { System.out.println("自动驾驶"); }
public void turbo() { System.out.println("涡轮增压"); } }
|
Car的三个功能奔驰都有,但三菱只有两个,但受限于语法问题,三个方法都得实现,只不过SeatHeating是个空方法,是不是很奇怪?太多余了!不优雅
所以应当拆分Car接口,例如下面代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| package org.principle.ISP;
interface SeatHeating{ public void seatHeating(); }
interface AutoPilot{ public void autoPilot(); }
interface Turbo{ public void turbo(); }
class BenzX implements SeatHeating,AutoPilot,Turbo{
public void seatHeating() { System.out.println("座椅加热"); }
public void autoPilot() { System.out.println("自动驾驶"); }
public void turbo() { System.out.println("涡轮增压"); } }
class SanLingX implements SeatHeating,AutoPilot{
public void seatHeating() { System.out.println("座椅加热"); }
public void autoPilot() { System.out.println("自动驾驶"); } }
|
现在就变的非常优雅,非常清晰,没有多余,但并不是所有接口都只有一个方法,一个接口可以多个方法,这里只是演示使用。
那接口的隔离做的粒度越小越好吗?当然不是。虽然增加了灵活度,但降低了维护性,所以接口的设计要因项目而已,把握好”度“是关键。
5、迪米特法则,又称最少知道原则(Demeter Principle)
也称为最少知识原则(Least Knowledge Principle , LKP),对于初学者来说,这样的定义实在是太晦涩了。
- 一个类对自己耦合的类或者调用的类知道的越少越好,你内部如何复杂和我没关系
- 只和直接朋友交流(直接朋友是:耦合关系、组合、聚合、依赖)
朋友的定义是这样的:出现在成员变量、方法的输入输出参数中的类称为朋友类,而方法内部的类不属于朋友类,迪米特法则认为,一个对象或方法,只能调用以下对象:
当A类的方法调用了B类,B类调用C类完成业务,如果A类中也出现了C类,那么三者之间的耦合性将大大增加,例如C类出现代码变更,那么可能会影响A类和B类的代码,这无疑是增加了代码风险。
模拟一个超时购物的场景
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| package org.principle.LOD;
class Wallet {
private Float value;
public Wallet(Float value) { this.value = value; }
public Float getMoney(){ return this.value; }
public void reduceMoney(Float money){ this.value -= money; } }
class Customer {
private Wallet wallet = new Wallet(50f);
public Wallet getWallet() { return wallet; } }
class PaperBoy { public void charge(Customer customer,Float money){ Wallet wallet = customer.getWallet(); if (wallet.getMoney() >= money){ System.out.println("顾客付账:" + money +"元"); wallet.reduceMoney(money); System.out.println("钱包里还剩:"+wallet.getMoney()+"元"); } else { System.out.println("钱包里的金额不够......"); } } }
public class Test { public static void main(String[] args) { PaperBoy paperBoy = new PaperBoy(); Customer customer = new Customer(); paperBoy.charge(customer,20f); } }
|
在PaperBoy中的charge方法中出现了Wallet类,在Customer中也出现了Wallet类,而且观察PaperBoy.charge方法,PaperBoy竟然调用了wallet.reduceMoney(money);也就是拿着客户的钱包付钱,这反常态,你怎么可以动我钱包呢。所以他们之间的关系太乱,需要修改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| // 顾客 class Customer {
private Wallet wallet = new Wallet(50f);
// 顾客自己付钱 public void pay(Float money){ if (wallet.getMoney() >= money){ System.out.println("顾客付账:" + money +"元"); // 减去 应付的钱 wallet.reduceMoney(money); System.out.println("钱包里还剩:"+wallet.getMoney()+"元"); } else { System.out.println("钱包里的金额不够......"); } } }
// 收银员 class PaperBoy {
// 收银员收钱 public void charge(Customer customer,Float money){ customer.pay(money); } }
|
这样PaperBoy只是提出收费的要求,但具体客户怎么付,是刷卡还是现金,这都与我没有关系。通过分析:
- 顾客Customer和钱包Wallet是朋友
- 顾客Customer和收银员PaperBoy是朋友
- 钱包Wallet和收银员PaperBoy是陌生人
这就符合了迪米特法则:只和朋友交流,不和陌生人说话。该法则的观念就是类之间的解耦,只有解耦了,类的复用率才高,但也会导致与单一职责、依赖倒置、接口隔离相同的问题:产生大量的类,提高了系统的复杂性,所以在使用时也需要因项目而异,反复斟酌。
6、单一职责原则(Single Responsibility Principle,SRP)
定义:应该有且仅有同一类原因引起类的变更
理解:类承担的职责要聚焦某一个或者某一类,例如用户管理功能中不应该有订单管理的功能。
来举个例子
业务:要实现一个APP,打车可以选择经济型、快捷性、方便型等
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| public class DiDi {
public void drive(String type){ if (type == "jingji"){ System.out.println("经济型"); } if (type == "kuaijie"){ System.out.println("快捷型"); } if (type == "fangbian"){ System.out.println("方便型"); }
} }
public class Test { public static void main(String[] args) { DiDi jingji = new DiDi(); jingji.drive("jingji");
DiDi fangbian = new DiDi(); fangbian.drive("fangbian"); } }
|
第一个版本的代码完成了主要的业务功能,但如果产品经理要增加VIP乘车类型来完成对VIP的收费模式,就必须要修改DiDi类,而该类可能已经被十几个类使用,并且经过测试,业务上线运行了。此时修改DiDi类,估计测试部门的同事要疯了。这增加了测试的工作量,也增加了业务的风险
所以要使用单一职责的设计模式,看一下改进之后的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
|
public interface Car { public void drive(); }
public class Jingji implements Car{ public void drive() { System.out.println("经济型"); } }
public class Vip implements Car{ public void drive() { System.out.println("VIP车型" + ",额外收费10元"); } }
public class Test { public static void main(String[] args) { Jingji jingji = new Jingji(); jingji.drive();
Vip vip = new Vip(); vip.drive(); } }
|
代码升级之后,是不是可以灵活的应对需求变更?无论是增加车型,还是修改车型对应的服务,只需要修改对应的类就可以了,也只需要测试该类的相关类,做到了对代码的最小修改。
这里面的Car接口对应现在的需求其实并不是必须的,但对类的抽象是一种“默认的规则”,它在其它设计模式(策略、装饰)中是必须要进行抽象的。