我们编写软件的过程中常常面临着需求变更,每一次的变更其实都是对我们代码的可重用性、可读性、可维护性、可靠性的一次考验。设计模式就是为了让我们的代码具备这些功能,并使程序呈现高内聚,低耦合的特性。
1、可重用性:相同的代码,不需要重复编写
2、可读性:代码规范,命名规范,便于理解
3、可维护性:新增功能容易,方便扩展
4、可靠性:新增的功能,不会对历史功能造成影响
一、单一职责原则
单一职责原则(Single responsibility principle)规定每个类都应该有一个单一的功能,并且该功能应该由这个类完全封装起来。所有这个类的服务都应该严密的和该功能平行(功能平行,意味着没有依赖)。
package com.ycdhz.design; public class SinglePrinciple { public static void main(String[] args) { Vehicle vehicle = new Vehicle(); vehicle.run("骑车"); vehicle.run("飞机"); } } class Vehicle { public void run(String vehicle){ System.out.println(vehicle + "在路上跑"); } }
飞机是在天上飞的,Vehicle 的run 方法不能满足需求,也违反了单一职责原则。我可以根据不同的交通工具拆分成不同的类。
package com.ycdhz.design; public class SinglePrinciple { public static void main(String[] args) { RoadVehicle roadVehicle = new RoadVehicle(); roadVehicle.run("公交"); AirVehicle airVehicle = new AirVehicle(); airVehicle.run("飞机"); } } class RoadVehicle { public void run(String vehicle){ System.out.println(vehicle + "在路上跑"); } } class AirVehicle { public void run(String vehicle){ System.out.println(vehicle + "在天上飞"); } }
我们将Vehicle 类拆分为RoadVehicle、AirVehicle 满足了单一职责原则,但在实际的开发中我们往往不会这么做,我们做了类分解同时还修改了客户端,这样的改动太大。
package com.ycdhz.design; public class SinglePrinciple { public static void main(String[] args) { Vehicle vehicle = new Vehicle(); vehicle.roadRun("公交"); vehicle.airRun("飞机"); } } class Vehicle{ public void roadRun(String vehicle){ System.out.println(vehicle + "在路上跑"); } public void airRun(String vehicle){ System.out.println(vehicle + "在天上飞"); } }
我们没有对Vehicle 类进行拆分,而是新增了一个方法。虽然从类上来说不满足单一职责原则,但是从方法上来说依旧是满足的。
总结:
1、降低类的复杂度,一个类只负责一项职责。
2、提高类的可读性,可维护性。
3、降低变更引起的风险。
4、通常情况下我们应当遵守单一职责原则,只有逻辑足够简单,才可以在代码级违反单一职责原则;例如类中方法数量足够少,可以在方法级别保持单一职责原则。
二、里氏替换原则
里氏替换原则(Liskov Substitution Principle) 子类型必须可以替换掉它们的父类。子类可以扩展父类的功能,但不能改变父类原有的功能。简单来说就是一个软件实体如果使用的是一个父类的话,那么一定适用于其子类,如果把软件中父类替换为子类,程序的行为没有任何变化。
案例:
package com.ycdhz.design; public class LiskovPrinciple { public static void main(String[] args) { A a = new A(); System.out.println("11-3=" + a.func1(11, 3)); B b = new B(); System.out.println("11-3=" + b.func1(11, 3));//这里本意是求出 11-3 System.out.println("11+3+9=" + b.func2(11, 3)); } } class A { public int func1(int num1, int num2) { return num1 - num2; } } class B extends A{ @Override public int func1(int a, int b) { return a + b; // 重写了A的方法 } public int func2(int a, int b) { return func1(a, b) + 9; } }
我们发现原来运行正常的功能,因为类 B 无意中重写了父类的方法,造成功能出现错误。在实际开发过程中我们经常会通过重写来实现新功能,这样虽然开发简单,但是整个体系的复用性会比较差。我们可以定义一个更通俗的基类,让原有的父类,子类通过这个基类,去掉他们的继承关系。采用聚合、组合、依赖的方式来代替。
package com.ycdhz.design; public class LiskovPrinciple { public static void main(String[] args) { B b = new B(); System.out.println("11+3=" + b.func1(11, 3)); System.out.println("11+3+9=" + b.func2(11, 3)); System.out.println("11-3=" + b.func3(11, 3)); } } class Base { } class A extends Base { public int func1(int num1, int num2) { return num1 - num2; } } class B extends Base { private A a = new A(); public int func1(int num1, int num2) { return num1 + num2; } public int func2(int num1, int num2) { return func1(num1, num2) + 9; } public int func3(int num1, int num2) { return a.func1(num1, num2); } }
总结:
1、里氏替换原则 所有引用基类的地方必须能透明地使用其子类的对象。
2、在使用继承时,遵循里氏替换原则,在子类中尽量不要重写父类的方法。
3、里氏替换原则告诉我们,继承实际上让两个类耦合性增强了,在适当的情况下,可以通过 聚合、组合、依赖来解决问题。
三、依赖倒置原则
依赖倒转原则(Dependence Inversion Principle) 需要满足两点:高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。相对于细节(实现类)的多变性,抽象(接口/抽象类)的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。
案例: 用户接收Email消息功能
package com.ycdhz.design; public class DependecyPrinciple { public static void main(String[] args) { Persion persion = new Persion(); persion.Recive(new Email()); } } class Email { public void getInfo() { System.out.println("Email Msg!"); } } class Persion { public void Recive(Email email){ email.getInfo(); } }
如果这个时候需求发生了变更,我们需要接收微信、短信、QQ等等,则我们需要为每一个功能新增一个类,同时Perons 也要增加相应的接收方法。这显然不符合我们的依赖倒转原则,这时我们只引入一个IRecive 接口,让Perons 与IRecive 接口发生依赖。
package com.ycdhz.design; public class DependecyPrinciple { public static void main(String[] args) { Persion persion = new Persion(); persion.Recive(new Email()); } } class Persion { public void Recive(IRecive recive){ recive.getInfo(); } } interface IRecive{ void getInfo(); } class Email implements IRecive { @Override public void getInfo() { System.out.println("Email Msg!"); } }
这个时候我们只需要根据需求添加具体的业务代码,而不需要改变客户端。
依赖关系的三种传递方式
1、接口传递
2、构造方法传递
3、setter 方式传递
总结:
1、低层模块尽量都要有抽象类或接口,或者两者都有,程序稳定性更好。
2、变量的声明类型尽量是抽象类或接口, 这样我们的变量引用和实际对象间,就存在一个缓冲层,利于程序扩展和优化。
3、继承时遵循里氏替换原则。
四、接口隔离原则
接口隔离原则(Interface Segregation Principle) 客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。
案例:
如图现在 ClientA 需要通过接口 Interface1 接口依赖ServiceA 中的Operation1、Operation2、Operation3 三个方法,ClientB 需要通过接口 Interface1 依赖ServiceB中的Operation1、Operation4、Operation5 三个方法。
package com.ycdhz.design; public class InterfacePrinciple { public static void main(String[] args) { ClientA a = new ClientA(); a.operation1(new ServiceA()); a.operation2(new ServiceA()); ClientB b = new ClientB(); b.operation1(new ServiceB()); b.operation4(new ServiceB()); } } interface Interface1{ void operation1(); void operation2(); void operation3(); void operation4(); void operation5(); } class ServiceA implements Interface1{ @Override public void operation1() { System.out.println("Class ServiceA operation 1"); } @Override public void operation2() { System.out.println("Class ServiceA operation 2"); } @Override public void operation3() { System.out.println("Class ServiceA operation 3"); } @Override public void operation4() { System.out.println("Class ServiceA operation 4"); } @Override public void operation5() { System.out.println("Class ServiceA operation 5"); } } class ServiceB implements Interface1{ @Override public void operation1() { System.out.println("Class ServiceB operation 1"); } @Override public void operation2() { System.out.println("Class ServiceB operation 2"); } @Override public void operation3() { System.out.println("Class ServiceB operation 3"); } @Override public void operation4() { System.out.println("Class ServiceB operation 4"); } @Override public void operation5() { System.out.println("Class ServiceB operation 5"); } } class ClientA { public void operation1(Interface1 interface1){ interface1.operation1(); } public void operation2(Interface1 interface1){ interface1.operation2(); } public void operation3(Interface1 interface1){ interface1.operation3(); } } class ClientB { public void operation1(Interface1 interface1){ interface1.operation1(); } public void operation4(Interface1 interface1){ interface1.operation4(); } public void operation5(Interface1 interface1){ interface1.operation5(); } }
我们发现Interface1 接口对于ServiceA、ServiceB 来说都不是最小接口,明显违背了接口隔离原则。我可以根据不同的需要对接口进行拆分,将接口 Interface1 拆分为独立的几个接口( Interface1、Interface2、Interface3),类 ServiceA 和类ServiceB 分别与他们需要的接口建立依赖关系。
package com.ycdhz.design; public class InterfacePrinciple { public static void main(String[] args) { ClientA a = new ClientA(); a.operation1(new ServiceA()); a.operation2(new ServiceA()); a.operation3(new ServiceA()); ClientB b = new ClientB(); b.operation1(new ServiceB()); b.operation4(new ServiceB()); b.operation5(new ServiceB()); } } interface Interface1{ void operation1(); } interface Interface2{ void operation2(); void operation3(); } interface Interface3{ void operation4(); void operation5(); } class ServiceA implements Interface1, Interface2{ @Override public void operation1() { System.out.println("Class ServiceA operation 1"); } @Override public void operation2() { System.out.println("Class ServiceA operation 2"); } @Override public void operation3() { System.out.println("Class ServiceA operation 3"); } } class ServiceB implements Interface1, Interface3 { @Override public void operation1() { System.out.println("Class ServiceB operation 1"); } @Override public void operation4() { System.out.println("Class ServiceB operation 4"); } @Override public void operation5() { System.out.println("Class ServiceB operation 5"); } } class ClientA { public void operation1(Interface1 interface1){ interface1.operation1(); } public void operation2(Interface2 interface1){ interface1.operation2(); } public void operation3(Interface2 interface1){ interface1.operation3(); } } class ClientB { public void operation1(Interface1 interface1){ interface1.operation1(); } public void operation4(Interface3 interface1){ interface1.operation4(); } public void operation5(Interface3 interface1){ interface1.operation5(); } }
总结:
1、将接口 Interface1 进行合理的拆分,让类ClientA 和类ClientB 分别与他们需要的接口建立依赖关系,这样就满足了接口隔离原则。
五、迪米特法则
迪米特法则(Demeter Principle)又叫 最少知道原则,即一个类 对自己依赖的类知道的越少越好。也就是说,对于被依赖的类不管多么复杂,都尽量将逻辑封装在类的内部。对外除了提供的 public 方法,不对外泄露任何信息。
迪米特法则还有个更简单的定义:只与直接的朋友通信。每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多依赖、关联、组合、聚合等。其中我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类不是直接的朋友。
package com.ycdhz.design; import java.util.ArrayList; import java.util.List; public class DemeterPrinciple { public static void main(String[] args) { CompanyManager manager = new CompanyManager(); manager.printAllEmployee(new SubCompanyManager()); } } //总公司员工 class Employee { private String id; public void setId(String id) { this.id = id; } public String getId() { return id; } } //分公司员工 class SubEmployee { private String id; public void setId(String id) { this.id = id; } public String getId() { return id; } } class SubCompanyManager { public List<SubEmployee> getAllEmployee() { List<SubEmployee> list = new ArrayList<SubEmployee>(); for (int i = 0; i < 100; i++) { SubEmployee emp = new SubEmployee(); //为分公司人员按顺序分配一个ID emp.setId("分公司" + i); list.add(emp); } return list; } } class CompanyManager { public List<Employee> getAllEmployee() { List<Employee> list = new ArrayList<Employee>(); for (int i = 0; i < 30; i++) { Employee emp = new Employee(); //为总公司人员按顺序分配一个ID emp.setId("总公司" + i); list.add(emp); } return list; } public void printAllEmployee(SubCompanyManager sub) { List<SubEmployee> list1 = sub.getAllEmployee(); for(SubEmployee e:list1){ System.out.println(e.getId()); } List<Employee> list = this.getAllEmployee(); for (Employee e : list) { System.out.println(e.getId()); } } }
我们发现在CompanyManager 类的printAllEmployee 方法中与子公司的员工发生了耦合,按照迪米特法则只与直接的朋友发生通信,而SubEmployee 类并不是CompanyManager 类的直接朋友(以局部变量出现的耦合不属于直接朋友),从逻辑上讲总公司只与他的分公司耦合就行了,与分公司的员工并没有任何联系,这样设计显然是增加了不必要的耦合。
class SubCompanyManager { public List<SubEmployee> getAllEmployee() { List<SubEmployee> list = new ArrayList<SubEmployee>(); for (int i = 0; i < 100; i++) { SubEmployee emp = new SubEmployee(); //为分公司人员按顺序分配一个ID emp.setId("分公司" + i); list.add(emp); } return list; } public void printEmployee() { List<SubEmployee> list = this.getAllEmployee(); for (SubEmployee e : list) { System.out.println(e.getId()); } } } class CompanyManager { public List<Employee> getAllEmployee() { List<Employee> list = new ArrayList<Employee>(); for (int i = 0; i < 30; i++) { Employee emp = new Employee(); //为总公司人员按顺序分配一个ID emp.setId("总公司" + i); list.add(emp); } return list; } public void printAllEmployee(SubCompanyManager sub) { sub.printEmployee(); List<Employee> list = this.getAllEmployee(); for (Employee e : list) { System.out.println(e.getId()); } } }
修改后,为分公司增加了打印人员ID的方法,总公司直接调用来打印,从而避免了与分公司的员工发生耦合。
总结:
1、迪米特法则的核心是降低类之间的耦合。
2、由于每个类都减少了不必要的依赖,因此迪米特法则只是要求降低类间(对象间)耦合关系, 并不是要求完全没有依赖关系。
六、开闭原则
开闭原则(Open Closed Principle)是编程中最基础、最重要的设计原则。软件实体(类,模块,函数)应该可以扩展,但是不可以修改。即对于扩展是开放的,对于更改是封闭的。面对需求,对程序的改动是通过增加新代码进行的,而不是更改现有的代码。开放-封闭原则是面向对象设计的核心所在,遵循这个原则可以使系统可维护,可扩展,可复用,灵活性好。
package com.ycdhz.design; public class OpenClosedPrinciple { public static void main(String[] args) { GraphicEditor editor = new GraphicEditor(); editor.drawCircle(new Circle()); editor.drawRectangle(new Rectangle()); } } //Shape 类,基类 class Shape { int m_type; } class Rectangle extends Shape { Rectangle() { super.m_type = 1; } } class Circle extends Shape { Circle() { super.m_type = 2; } } class GraphicEditor { public void drawShape(Shape s) { if (s.m_type == 1) { drawRectangle(s); } else if (s.m_type == 2){ drawCircle(s); } } public void drawRectangle(Shape r) { System.out.println("绘制矩形"); } public void drawCircle(Shape r) { System.out.println("绘制圆形"); } }
这个时候如果我们需要绘制一个三角形,我们发现需要做很多的修改,违背了开闭原则。其实我们可以将Shape 类做成抽象类,并提供一个抽象的 draw 方法,让子类去实现,这样我们有新的图形种类时,只需要让新的图形类继承 Shape,并实现 draw 方法即可。
package com.ycdhz.design; public class OpenClosedPrinciple { public static void main(String[] args) { GraphicEditor editor = new GraphicEditor(); editor.drawShape(new Circle()); editor.drawShape(new Rectangle()); } } abstract class Shape{ public abstract void draw(); } class Rectangle extends Shape { @Override public void draw() { System.out.println("绘制矩形"); } } class Circle extends Shape { @Override public void draw() { System.out.println("绘制圆形"); } } class GraphicEditor{ public void drawShape(Shape s) { s.draw(); } }
总结:
1、开闭原则(Open Closed Principle)是编程中 最基础、最重要的设计原则。
2、一个软件实体如类,模块和函数应该 对扩展开放( 对提供方),对 修改关闭( 对使用方)。用抽象构建框架,用实现扩展细节。因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。
3、当软件需要变化时,尽量 通过扩展软件实体的行为来实现变化,而不是 通过修改已有的代码来实现变化。