1.概述
设计模式(Design Pattern)是软件设计领域的一棵长青树,它是一套被反复使用、多人知晓、经过分类编目的、优秀代码设计经验的总结。使用设计模式是为了设计出易理解、可复用、可靠的代码,增强程序的可复用性。设计模式由Erich Gamma等人从建筑领域引入软件设计领域,成为了软件开发的指导思想,目前已成为高级软件工程师不可或缺的知识。本文将分系列讲述设计模式的原理以及使用场景,来帮助大家更好地理解和使用设计。第一系列主要讲述设计模式的七大设计原则(也有说六大设计原则,本文将合成复用原则算入)。
2.设计模式七大原则
2.1 单一职责原则(Single Responsibility Principle,SRP)
定义:一个类或者模块应该有且只有一个改变的原因。如果一个类A负责两个不同的职责:职责1和职责2,当职责1需求变更而改变A时,可能会造成职责2执行错误,所以需要将类A拆分成A1和A2。
案例:有一个交通类,它有一个run方法,表示交通工具正在运行。
public class SingleResponsibility {
public static void main(String[] args) {
Vehicle vehicle = new Vehicle();
vehicle.run("汽车");
vehicle.run("摩托车");
vehicle.run("卡车");
}
}
class Vehicle {
public void run(String name) {
System.out.println(name + "在公路上跑!");
}
}
此时出现一个新的问题,就是飞机和轮船这种交通工具,都不是在公路上跑的,按照单一职责进行修改,需要将Vehicle拆分成AirVehicle和RoadVehicle 和WaterVehicle,代码如下:
public class SingleResponsibility {
public static void main(String[] args) {
RoadVehicle roadVehicle = new RoadVehicle();
roadVehicle.run("汽车");
AirVehicle airVehicle = new AirVehicle();
airVehicle.run("飞机");
WaterVehicle waterVehicle = new WaterVehicle();
waterVehicle.run("轮船");
}
}
class RoadVehicle {
public void run(String name) {
System.out.println(name + "在路上跑!");
}
}
class AirVehicle {
public void run(String name) {
System.out.println(name + "在天空中飞!");
}
}
class WaterVehicle {
public void run(String name) {
System.out.println(name + "在水里跑!");
}
}
这种修改的方式在时间开发中会导致子类的激增,而且要修改大量地方。而如果直接修改Vehicle的run方法(添加if…else…语句进行判断),又违背了单一职责原则。因此可以考虑方法级别的单一职责原则(不修改原来的类,在方法层面进行区分)。修改代码如下:
public class SingleResponsibility {
public static void main(String[] args) {
Vehicle vehicle = new Vehicle();
vehicle.run("汽车");
vehicle.runAir("飞机");
vehicle.runWater("轮船");
}
}
class Vehicle {
public void run(String name) {
System.out.println(name + "在路上跑!");
}
public void runAir(String name) {
System.out.println(name + "在天空中飞!");
}
public void runWater(String name) {
System.out.println(name + "在水里跑!");
}
}
这种方式一定程度上违背了单一职责,但是相对修改地方较少,而且在方法层面仍然遵守单一职责原则,日常开发中也使用较多。
单一职责的优势和缺陷如下:
优势: 降低了类的复杂度,提高了代码可读性,提高了系统的可维护性;
缺陷:若处理不当,可能会导致子类激增且需要修改大量地方。
2.2 接口隔离原则(Interface Segregation Principle,ISP)
定义:客户端不应该依赖它不需要的接口,一个类对另一个类的依赖应该建立在最小的接口上。
案例:类A通过抽象接口依赖类B,类C通过抽象接口依赖于类D,而类B的核心方法只需实现抽象接口中的operation1(params)、operation2(params);类D中的核心方法只需实现抽象接口中的operation3()和operation4()。由于抽象接口不是最小接口,因此类B和D必须去实现他们不需要的方法。
按照接口隔离原则改进:需要将抽象接口拆分成几个独立的接口,类A和类C分别与他们需要的接口建立关系,拆分后类图如下:
上图中将抽象接口拆分成了3个接口,类A通过抽象接口1与类B建立联系,类C通过抽象接口2与类D建立联系。
优势:在一定程度上补充了单一职责原则,降低了耦合度;
缺点:可能由于场景的复杂性导致拆分接口过多,导致接口数量爆增;
2.3 开闭原则(Open-Close Principle,OCP)
定义:一个软件实体(如类),模块和函数应该扩展对外开放,对修改关闭。用抽象构建框架,用实现扩展细节。当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。开闭原则是编程中最基础、最重要的设计原则。
案例:有一个绘图工具类,根据传入的type来绘制不同的形状,代码如下:
public class OpenCloseRule {
public static void main(String[] args) {
GraphicEditor graphicEditor = new GraphicEditor();
graphicEditor.drawShape(new Circle());
graphicEditor.drawShape(new Rectangle());
}
}
class GraphicEditor {
public void drawShape(Shape shape) {
if (shape.type == 1) {
drawCircle();
} else if (shape.type == 2) {
drawRectangle();
}
}
public void drawCircle() {
System.out.println("绘制圆形");
}
public void drawRectangle() {
System.out.println("绘制三角形");
}
}
class Shape {
int type;
}
class Circle extends Shape {
public Circle() {
super.type = 1;
}
}
class Rectangle extends Shape {
public Rectangle() {
super.type = 2;
}
}
上述代码的问题在于,当需要添加一个绘制正方形的方法时,需要实现Shape类,并修改GraphicEditor 类中的drawShape方法。如下:
class Square extends Shape {
public Square() {
super.type = 3;
}
}
class GraphicEditor {
public void drawShape(Shape shape) {
if (shape.type == 1) {
drawCircle();
} else if (shape.type == 2) {
drawRectangle();
} else if (shape.type == 3) {
drawSquare();
}
}
public void drawCircle() {
System.out.println("绘制圆形");
}
public void drawRectangle() {
System.out.println("绘制三角形");
}
public void drawSquare() {
System.out.println("绘制正方形");
}
}
以此类推,如果再新增其它的形状,需要不断进行扩充和修改,严重违背开闭原则。修改思路如下:将Shape类变成一个抽象类或者接口,子类去继承或实现Shape中的方法,当新增图形时,只需继承或实现Shape类即可,满足开闭原则。修改结果如下:
public class OpenCloseSuccess {
public static void main(String[] args) {
GraphicEditor graphicEditor = new GraphicEditor();
graphicEditor.drawShape(new Circle());
graphicEditor.drawShape(new Triangle());
}
}
class GraphicEditor {
public void drawShape(Shape shape) {
shape.draw();
}
}
abstract class Shape {
int type;
public void draw() {
}
}
class Circle extends Shape {
@Override
public void draw() {
System.out.println("绘制圆形!");
}
}
class Triangle extends Shape {
@Override
public void draw() {
System.out.println("绘制三角形!");
}
}
优势:使系统具有良好的可扩展性,可维护性;
缺点:如果使用不好可能会导致类数量的递增。
2.4 里氏替换原则(Liskov Substitution Principle,LSP)
定义: 里氏替换原则主要使用来解决继承所带来的耦合性,一个类被另一个类继承,则就与其它类产生耦合,一但修改了父类,则可能影响其大多数子类。比如有一个类A,内部有一个方法method1()实现了一个功能,现在A的子类B要对该功能进行扩展,于是再method1()方法内添加了部分功能,此时新的method1就可能会影响原来A的method1()方法。
案例:有一个类A,内部有一个方法add(),完成两数相加,其子类B不小心重新了add()方法,变成了两数相减,B内部另一个方法addTen()实现两数相加,再加10。
public class A {
protected int add(int a, int b) {
return a + b;
}
}
class B extends A {
public int add(int a, int b) {
return a - b;
}
public int addTen(int a, int b) {
return add(a, b) + 10;
}
}
当测试B的addTen()方法时出现了异常,原本两数相加后再加10,变成了两数相减后再加10。
里氏替换原则的本质强调的是:子类可以扩展父类的功能,但不能改变父类的功能。
2.5 依赖倒置原则(Dependence Inversion Principle,DIP)
定义:高层模块不应该依赖于低层模块,二者都应该依赖于其抽象;抽象不应该依赖于细节,细节应该依赖于抽象;依赖倒置的核心思想是面向接口编程。
案例:有一个Person类,能够接收邮件信息并读取邮件信息。代码如下:
public class DIP {
public static void main(String[] args) {
Email email = new Email();
Person person = new Person();
person.read(email);
}
}
class Email {
public String getInfo() {
return "发送邮件信息:hello world";
}
}
class Person {
public void read(Email email) {
String info = email.getInfo();
System.out.println(info);
}
}
如果此时需要接收微信信息,则需要修改代码如下:
public class DIP {
public static void main(String[] args) {
Email email = new Email();
Person person = new Person();
person.read(email);
final WeiXin weiXin = new WeiXin();
person.readWeiXin(weiXin);
}
}
class Email {
public String getInfo() {
return "发送邮件信息:hello world";
}
}
class WeiXin {
public String getInfo() {
return "发送微信信息:baby";
}
}
class Person {
public void read(Email email) {
String info = email.getInfo();
System.out.println(info);
}
public void readWeiXin(WeiXin weiXin) {
String info = weiXin.getInfo();
System.out.println(info);
}
}
这种改法很明显违背单一职责原则和开闭原则,结合依赖倒置原则,修改代码如下:
public class DIP {
public static void main(String[] args) {
Email email = new Email();
Person person = new Person();
person.read(email);
person.read(new WeiXin());
}
}
interface ReadInfo {
String getInfo();
}
class Email implements ReadInfo {
public String getInfo() {
return "发送邮件信息:hello world";
}
}
class WeiXin implements ReadInfo {
public String getInfo() {
return "发送微信信息:baby";
}
}
class Person {
public void read(ReadInfo readInfo) {
String info = readInfo.getInfo();
System.out.println(info);
}
}
这样修改之后,所有要被读取的信息类型(比如QQ、钉钉等),形成子类继承ReadInfo接口即可,然后传入Person类的read方法中即可被读取。这里Person代表的则是高层模块,完成主要的业务逻辑,一旦对其修改,则可能引入巨大风险,所以遵循依赖倒置会降低类之间的耦合度,减少风险。
2.6 迪米特法则(Demeter Principle)
定义:一个对象应该对其它对象保持最少的了解,类与类的关系越密切,则耦合度越大。迪米特法则又叫最少知道原则,即一个类对自己依赖的类知道的越少越好。一个类被依赖的类不管多复杂,都尽量将自己的逻辑封装在类的内部,对外除了提供public方法,不对外泄漏任何信息。
案例:有一个学院管理类CollegeManager,内部拥有打印员工信息的方法,同时有一个学校管理类SchoolManager,内部拥有打印学校员工信息和学院员工信息的方法,代码如下:
public class SchoolManager {
public List<Employee> getAllEmployees() {
List<Employee> employees = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Employee employee = new Employee();
employee.setId(Long.valueOf(i));
employee.setName(getName());
employees.add(employee);
}
return employees;
}
public void printAllEmployee(CollegeManager sub) {
//此处违反迪米特法则,具体原因如下:
//1.这里的CollegeEmployee不是SchoolManager 的直接朋友
//2.CollegeEmployee是以局部变量方式出现在SchoolManager 中
List<CollegeEmployee> allEmployees = sub.getAllEmployees();
System.out.println("打印学院人员信息---------");
allEmployees.forEach(employee -> {
System.out.println(employee.getId() + ":" + employee.getName());
});
System.out.println("打印学校人员信息---------");
List<Employee> allEmployees1 = this.getAllEmployees();
allEmployees1.forEach(employee -> {
System.out.println(employee.getId() + ":" + employee.getName());
});
}
public String getName() {
String str = "abcdefghijklmnopqrstuvwxyz";
Random random = new Random();
StringBuffer sb = new StringBuffer();
sb.append("college-");
for (int j = 0; j < 5; j++) {
int i = random.nextInt(str.length());
char c = str.charAt(i);
sb.append(c);
}
return sb.toString();
}
}
class CollegeEmployee {
private Long id;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
class Employee {
private Long id;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
class CollegeManager {
public List<CollegeEmployee> getAllEmployees() {
List<CollegeEmployee> employees = new ArrayList<>();
for (int i = 0; i < 10; i++) {
CollegeEmployee employee = new CollegeEmployee();
employee.setId(Long.valueOf(i));
employee.setName(getName());
employees.add(employee);
}
return employees;
}
public String getName() {
String str = "abcdefghijklmnopqrstuvwxyz";
Random random = new Random();
StringBuffer sb = new StringBuffer();
sb.append("school-");
for (int j = 0; j < 5; j++) {
int i = random.nextInt(str.length());
char c = str.charAt(i);
sb.append(c);
}
return sb.toString();
}
}
按照迪米特法则(最少知道原则),需要将打印员工的方法封装在CollegeManager 中,只对外提供public类型的打印方法,修改代码如下:
public void printAllEmployee(CollegeManager sub) {
System.out.println("打印学院人员信息---------");
sub.printEmployees();
System.out.println("打印学校人员信息---------");
List<Employee> allEmployees1 = this.getAllEmployees();
allEmployees1.forEach(employee -> {
System.out.println(employee.getId() + ":" + employee.getName());
});
}
class CollegeManager {
public List<CollegeEmployee> getAllEmployees() {
List<CollegeEmployee> employees = new ArrayList<>();
for (int i = 0; i < 10; i++) {
CollegeEmployee employee = new CollegeEmployee();
employee.setId(Long.valueOf(i));
employee.setName(getName());
employees.add(employee);
}
return employees;
}
public void printEmployees() {
List<CollegeEmployee> allEmployees = this.getAllEmployees();
allEmployees.forEach(collegeEmployee -> {
System.out.println(collegeEmployee.getId() + ":" + collegeEmployee.getName());
});
}
public String getName() {
String str = "abcdefghijklmnopqrstuvwxyz";
Random random = new Random();
StringBuffer sb = new StringBuffer();
sb.append("school-");
for (int j = 0; j < 5; j++) {
int i = random.nextInt(str.length());
char c = str.charAt(i);
sb.append(c);
}
return sb.toString();
}
}
迪米特法则中强调直接朋友:每个对象都会与其它对象产生耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系,耦合的方式有很多,依赖、关联、组合、聚合等。其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类为非直接朋友,也就是说,陌生的类不要以局部变量的形式出现在类的内部。
2.7 合成复用原则(Composite Reuse Principle,CRP)
定义:尽量使用合成/聚合的方式,而不是使用继承。
案例:A要使用B的方法,尽量不要使用继承的方式,优先使用合成、聚合方式。代码如下:
public class CompositeReuse {
public static void main(String[] args) {
System.out.println("------依赖------");
B b = new B();
b.Operation1(new A());
System.out.println("------聚合------");
b.setA(new A());
b.Operation2();
System.out.println("------组合------");
b.Operation3();
}
}
class A {
void Operation1() {
System.out.println("A Operation1");
}
void Operation2() {
System.out.println("A Operation2");
}
void Operation3() {
System.out.println("A Operation3");
}
}
//如果只是需要用到 A类的方法,尽量不要使用继承。而是使用:依赖,聚合,组合的方式
class B {
//通过参数传递形式,将A注入到B的某个方法中(依赖)
void Operation1(A a) {
a.Operation1();
a.Operation2();
a.Operation3();
}
//----------------------------------------------------------------------
//通过set注入方式,将A变成B的属性成员(聚合)
A a;
public void setA(A a) {
this.a = a;
}
void Operation2() {
a.Operation1();
a.Operation2();
a.Operation3();
}
//----------------------------------------------------------------------
//通过对象创建方式,将A变成B的属性(组合)
A a1 = new A();
void Operation3() {
a1.Operation1();
a1.Operation2();
a1.Operation3();
}
}
设计模式的核心思想:
(1) 将应用中可能出现的变化之处进行独立,不要和那些不需要变化的代码混合在一起;
(2) 针对接口编程,而不是针对实现编程;
(3)为了交互对象之间的松耦合设计而努力。
2.8 类之间的关系
类之间的关系包括:依赖、泛化(继承)、实现、关联、聚合与组合。
2.8.1 依赖
一个使用了另一个类,那么他们之间就存在依赖关系。如果没有对方,就连编译都无法通过。类之间依赖包括以下几种情形: (1)在类中使用到了对方;
(2)一个类作为另一个类的成员属性;
(3)方法接收的参数类型;
(4)方法的返回类型;
(5)一个类在方法中使用到了另一个类。
类图如下:
2.8.2 泛化(继承)
泛化其实就是继承关系,是依赖关系的一种特例。
2.8.3 实现
实现关系实际上就是 一个类实现一个接口,依赖关系的特例。
2.8.4 关联
类与类之间的关系,依赖关系的特例,关联包括单向或者双向。
2.8.5 聚合
聚合表示的是整体和部分的关系,整体和部分的关系,也是关联关系的一种特例,因此它具有关联关系的导航性与多重性。
2.8.6 组合
组合也是整体和部分的关系,但是整体和部分不可以分开,关联关系的特例。
3.小结
1.设计模式的本质就是要降低程序耦合程度,提高可复用性;
2.设计模式的原则是设计模式的根本指导思想,本质就算指导设计出可复用更强的程序;
3.类之间的关系共有六种:依赖、泛化(继承)、实现、关联、聚合与组合。
4.参考文献
1.《设计模式-可复用面向对象软件的基础》-Erich Gamma、Richard Helm、Ralph Johnson、John Vlissides
2.《可复用物联网Web3D框架的设计与实现》-程亮(知网)
3.https://www.bilibili.com/video/BV1G4411c7N4-尚硅谷设计模式