设计模式(一)—— 设计原则
开闭原则
开闭原则是指一个软件实体(类,模块,函数等)应该对扩展开放,对修改关闭。所谓的开闭也是对拓展和修改行为的原则。它强调的是用抽象构建框架,用实现拓展细节,可以提高软件系统的可复用性和维护性。开闭原则是面向对象设计中最基础的设计原则,它指导我们如何建立稳定,灵活的系统。开闭原则的核心思想就是面向抽象编程。
我们以手机为例, 首先创建一个手机接口 Phone:
package com.chunqiu.ocp;
public interface Phone {
Integer getId();
Double getPrice();
}
手机有很多的品牌:小米,苹果,华为……我们来创建一个苹果手机的类:ApplePhone:
package com.chunqiu.ocp;
public class ApplePhone implements Phone{
private Integer id;
private Double price;
public ApplePhone(Integer id, Double price) {
this.id = id;
this.price = price;
}
@Override
public Integer getId() {
return this.id;
}
@Override
public Double getPrice() {
return this.price;
}
}
现在我们要给苹果手机打折,促进消费,但是如果修改ApplePhone 中的getPrice()方法,会存在一定的风险,因为其他的地方可能也会调用这个方法,违背了我们的初衷,会影响其他地方的调用结果。那我们要怎么样在不修改原有代码的前提下实现打折的功能呢?那么现在我们写一个处理打折逻辑的类:DiscountApplePhone
package com.chunqiu.ocp;
public class DiscountApplePhone extends ApplePhone{
public DiscountApplePhone(Integer id, Double price) {
super(id, price);
}
// 实现打折
public Double getPrice() {
return super.getPrice() * 0.8;
}
public Double getOldPrice() {
return super.getPrice();
}
}
这样就实现了对拓展开放有没有对原来的代码修改。
依赖倒置原则
依赖倒置原则是指设计代码结构时,高层模块不应该依赖低层模块,二者都应该依赖其抽象。抽象不应该依赖细节,细节应该依赖抽象。通过 依赖倒置可以减少类之间的耦合性,提高形同的稳定性,提高代码的可读性和可维护性,并且可以降低修改程序所造成的风险。
我们来看一个例子,小明喜欢收集收集,有了新品就会买。我们先来创建一个类Ming
package com.chunqiu.dip;
public class Ming {
public void buyXiaoMi() {
System.out.println("小明买了最新的小米手机");
}
public void buyApple() {
System.out.println("小明买了最新的苹果手机");
}
}
调用方法:
public static void main(String[] args) {
Ming ming = new Ming();
ming.buyApple();
ming.buyXiaoMi();
}
但是最近又出了新的华为,要实现这个逻辑要从低层到高层(调用层)依次修改代码。在Ming 类中加上buyHuawei()方法,在调用层也要追加调用。这样的话拓展性不仅受到了限制,对于我们的系统也是有很大的风险。其实想一下,买手机的这个动作变化的只是手机的品牌,这一部分我们其实是可以抽取出来的。所以我们这样来进行优化代码:
- 创建一个手机的抽象接口:Phone:
package com.chunqiu.dip;
public interface Phone {
void buy();
}
然后编写XiaomiPhone类:
package com.chunqiu.dip;
public class XiaomiPhone implements Phone{
@Override
public void buy() {
System.out.println("小明买了最新的小米手机!");
}
}
然后编写ApplePhone类:
package com.chunqiu.dip;
public class ApplePhone implements Phone{
@Override
public void buy() {
System.out.println("小明买了最新的苹果手机!");
}
}
修改Ming的购买方法:
package com.chunqiu.dip;
public class Ming {
public void buy(Phone phone) {
phone.buy();
}
}
调用方法:
public static void main(String[] args) {
Ming ming = new Ming();
ming.buy(new XiaomiPhone());
ming.buy(new ApplePhone());
}
这时候我们再来看,无论之后有什么品牌的手机再出新品,我们只需要新建一个类,通过传参的方式告诉小明就可以了,而不需要修改底层的代码。
单一职责原则
单一职责是指不要存在多于一个导致类变更的原因,也就是说一个类只负责一个职责。否则一旦需求变更,修改其中一个职责的代码可能会导致另一个职责的功能发生故障。如果一个类中存在两个职责,那么应该每一个职责应该对应一个类,把这个类进行拆分,解耦。后期需求变更维护互不影响。这样的话可以降低类的复杂度,提高代码的可读性,提高系统的可维护性,降低变更引起的风险。
我们来看一个例子:
package com.chunqiu.srp;
public class Animal {
public void move(String type) {
if ("bird".equals(type)) {
System.out.println("fly");
} else {
System.out.println("run");
}
}
}
Animal 承担了两种处理逻辑。假如现在有一条鱼,就必须要在修改代码。我们应该使得我们的代码是松耦合的。我们来建立两个类:Bird和Dog让他们可以自己做自己的事情,互相隔离:
package com.chunqiu.srp;
public class Bird {
public void move() {
System.out.println("fly");
}
}
package com.chunqiu.srp;
public class Dog {
public void move() {
System.out.println("run");
}
}
接口隔离原则
接口隔离原则是指用多个专门的接口,而不是使用单一的总接口,客户端不应该实现他不需要的接口。我们应该注意以下几点:
1.一个类对另一个类的依赖应该建立在最小接口之上
2.建立单一接口,不要建立庞大的臃肿的接口
3.尽量的细化接口,接口的方法尽量少,当然要结合自己的实际情况
接口隔离原则符合我们常说的高内聚,低耦合的设计思想,可以使类具有良好的可读性,可维护性和可拓展性。
还是以动物为例:
package com.chunqiu.srp;
public interface Animal {
void fly();
void eat();
void run();
}
其中有两个实现类Bird和Dog,但是Dog却不得不实现Animal 的所有方法,那么fly也要实现,不太现实的对吧?这时候我们就要对不同的动物行为来设计不同的接口。也就是说我们需要一个接口负责对的事,不要添加自己不负责的事情。所以我们可以拆分为三个接口:Eat,Fly, Run.
迪米特原则
迪米特原则又称最少知道原则,是指一个对象应该对其他的对象保持最少的了解。尽量降低类和类之间的耦合度。迪米特原则强调只和朋友交流,不喝陌生人说话。出现在成员变量,方法的输入输出参数中的类都可以成为成员朋友类,而出现在方法体内部的类不属于朋友类。
假如领导要我们统计加班情况,我们统计好之后再把结果告诉领导。我们来实现一下:
Leader:
package com.chunqiu.srp;
import java.util.List;
public class Leader {
public void countOT(List<OT> ot) {
System.out.println("加班人数:" + ot.size());
}
}
Boss:
package com.chunqiu.srp;
import java.util.ArrayList;
import java.util.List;
public class Boss {
public void count(Leader leader) {
List<OT> ot = new ArrayList<>();
// 模拟统计加班
for (int i = 1; i <10; i++) {
ot.add(new OT());
}
leader.countOT(ot);
}
}
测试:
public static void main(String[] args) {
Boss boss = new Boss();
Leader leader = new Leader();
boss.count(leader);
}
到这里我们的代码完成了,功能也已经实现,看上去没什么问题。但是根据上边我们说的,领导只想要leader统计好加班的情况,要一个汇总的情况,不需要和加班的人直接去交流。而leader统计是还需要使用OT对象的。所以领导和OT并不是朋友。所以我们应该优化:
leader:
package com.chunqiu.srp;
import java.util.ArrayList;
import java.util.List;
public class Leader {
public void countOT() {
List<Integer> ot = new ArrayList<>();
// 模拟统计加班
for (int i = 1; i <10; i++) {
ot.add(i);
}
System.out.println("加班人数:" + ot.size());
}
}
Boss:
package com.chunqiu.srp;
import java.util.ArrayList;
import java.util.List;
public class Boss {
public void count(Leader leader) {
leader.countOT();
}
public static void main(String[] args) {
Boss boss = new Boss();
Leader leader = new Leader();
boss.count(leader);
}
}
这样的话Boss和OT就没有关联了。
里氏替换原则
里氏替换原则是说如果对每一个类型为T1 的对象o1,都有类型为T2 的对象o2,使得T1定义的所有程序P在所有的对象o1都替换成o2,程序P 的行为没有发生变化,那么类型T2是类型T1的子类型。
上边的说法很抽象,我们可以理解为一个软件实体如果适用于一个父类,那么一定适用于其子类,所有引用父类的地方必须能透明的使用其子类的对象,子类对象能够替代父类对象,而程序的逻辑不变。也可以得出一个结论:子类可以拓展父类的功能,但不能改变父类原有的功能。
1.子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
2.子类可以增加自己特有的方法
3.当子类的方法重载父类的方法是,方法的前置条件要比父类方法的输入参数更宽松。
4,当子类的方法重载父类的方法时,方法的后置条件要比父类更严格或和父类一样。
使用里氏替换原则有两个优点:
1.约束继承泛滥,是开闭原则的一种体现
2.加强程序的健壮性,在变更的时候也可以做到非常好的兼容性,提高程序的可维护性和拓展性,降低需求变更时的风险。
合成复用原则
合成复用原则是指尽量使用对象组合、聚合而不是继承关系达到软件复用的目的。可以使系统更加灵活,降低类之间的耦合度。
继承叫做白箱复用,把所有的实现细节暴露给子类,组合聚合成为黑箱复用,无法获取到类以外的对象的实现细节的。
我们来模拟一个数据库连接的操作:
DBConnection :
package com.chunqiu.srp;
public class DBConnection {
public String getConnection() {
return "mysql 连接";
}
}
ProductDao:
package com.chunqiu.srp;
public class ProductDao {
private DBConnection connection;
public void setConnection(DBConnection connection) {
this.connection = connection;
}
public void add() {
String connection = this.connection.getConnection();
System.out.println("添加产品");
}
}
这是一种合成复用的场景。但是DBConnection 目前只有mysql,之后添加其他的数据库,要怎么做?在DBConnection 添加对其他数据库的支持,但是违背了开闭原则,所以我们需要对DBConnection 进一步抽象:
public abstract class DBConnection {
public abstract String getConnection();
}
然后将其他数据库的逻辑抽出来进行具体化:
package com.chunqiu.srp;
public class MysqlDBConnection extends DBConnection{
@Override
public String getConnection() {
return "mysql连接";
}
}
这样的话也可以支持其他的数据库。
总结
设计模式的原则带给我们更重要的是一种思想,对于我们在实际的开发中有深远的意义,有利于规范我们的开发习惯,提升我们的能力。我们要让我们的代码专于自己的职责,为了以后系统的可拓展性和维护性尽量要进行解耦。设计原则是基于抽象进行架构,要面向接口编程,先顶层在细节的设计代码结构。