> 文档中心 > 设计模式的七大原则

设计模式的七大原则


本篇为学习设计模式所作笔记,水平有限,如有错误,劳烦给予指正,万分感谢!

1、什么是设计模式

在软件工程中,设计模式是对软件设计中普遍存在,反复出现的问题,所提出来的解决方案。

2、设计模式的目的

编写软件过程中,程序员面临着来自耦合性,内聚性以及可维护性,可扩展性,重用性,灵活性等多方面的挑战,设计模式是为了让程序(软件),具有更好的

  1. 代码重用性 (即:相同功能的代码,不用多次编写)
  2. 可读性 (即:编程规范性, 便于其他程序员的阅读和理解)
  3. 可扩展性 (即:当需要增加新的功能时,非常的方便,称为可维护)
  4. 可靠性 (即:当我们增加新的功能后,对原来的功能没有影响)
  5. 使程序呈现高内聚,低耦合的特性

3、设计模式的七大原则

1、单一职责原则

一个类只负责一项职责,不能负责多项职责,比如:甲负责了A、B两个不同职责任务,这是不被允许的。因为当改变其中一个职责时,另一个可能会受到牵连而出现错误,应该把这两种职责进行粒度分解成不同的类。

举例说明

public class SingleResponsibility {    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类就违反了单一职责,因为从输出结果来看,飞机是在天上飞的,而并非在路上跑,解决的方案非常的简单,根据交通工具运行方法不同,分解成不同类即可。如下

public class SingleResponsibility {    public static void main(String[] args) { RoadVehicle roadVehicle = new RoadVehicle(); AirVehicle airVehicle = new AirVehicle(); roadVehicle.run("汽车"); airVehicle.run("飞机");    }}class RoadVehicle{    public void run(String RoadVehicle){ System.out.println(RoadVehicle+"在路上跑");    }}class AirVehicle{    public void run(String AirVehicle){ System.out.println(AirVehicle+"在天上飞");    }}/* 输出结果汽车在路上跑飞机在天上飞*/

上述改进遵守单一职责原则,但是这样做的改动很大,即将类分解,同时修改客户端。这种适合类的内容比较复杂的时候使用,如果类的内容比较简单,可以采取下面这种方式

public class SingleResponsibility {    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 + "在天上飞");    }}

这种修改方法没有对原来的类做大的修改,只是增加方法,虽然没有在类这个级别上遵守单一职责原则,但是在方法级别上,仍然是遵守单一职责的。这种适用于类比较简单的情况下使用

单一职责原则注意事项和细节

  1. 降低类的复杂度,一个类只负责一项职责
  2. 提高类的可读性、可维护性、降低变更引起的风险
  3. 通常情况下,我们要遵循单一职责原则,只有逻辑足够简单,才可以在代码级违反单一职责原则,若类中的方法比较少且足够简单,可以在方法上保持单一职责原则

2、接口隔离原则

客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口。

设计模式的七大原则

在上述图片中,类C和类D都实现了同一个接口。类A中的方法使用类C作为参数,但只使用其中的 operation1operation2operation3方法,而类B中的方法使用类D作为参数,但只使用其中的 operation1operation24operation5方法,则对类A和类B来说,这个接口都不是它们的最小接口。上图代码编写如下

// 声明接口public interface Segregation {    void operation1();    void operation2();    void operation3();    void operation4();    void operation5();}// 类C实现接口class C implements Segregation{    @Override    public void operation1() { System.out.println("C==========operation1");    }    @Override    public void operation2() { System.out.println("C==========operation2");    }    @Override    public void operation3() { System.out.println("C==========operation3");    }    @Override    public void operation4() { System.out.println("C==========operation4");    }    @Override    public void operation5() { System.out.println("C==========operation5");    }}// 类D实现接口class D implements Segregation{    @Override    public void operation1() { System.out.println("D==========operation1");    }    @Override    public void operation2() { System.out.println("D==========operation2");    }    @Override    public void operation3() { System.out.println("D==========operation3");    }    @Override    public void operation4() { System.out.println("D==========operation4");    }    @Override    public void operation5() { System.out.println("D==========operation5");    }}// 类A创建三个方法 调用 接口方法1、2、3。方法4、5没有用到,所以不是最小接口class A{    public void method1(Segregation segregation) { segregation.operation1();    }    public void method2(Segregation segregation) { segregation.operation2();    }    public void method3(Segregation segregation) { segregation.operation3();    }}// 类A创建三个方法 调用 接口方法1、4、5,方法2、3没有用到,所以不是最小接口class b{    public void method1(Segregation segregation) { segregation.operation1();    }    public void method4(Segregation segregation) { segregation.operation4();    }    public void method5(Segregation segregation) { segregation.operation5();    }}class test{    public static void main(String[] args) { // 创建A对象 A a = new A(); // 以下是以类C做参数调用方法 a.method1(new C()); a.method2(new C()); a.method3(new C()); B b = new B(); // 以下是以类D做参数调用方法 b.method1(new D()); b.method4(new D()); b.method5(new D());    }}

要想保证接口隔离原则,则需要如下改进:将接口拆分为几个独立的接口,类C和类D分别和需要的接口建立联系。

package com.tzf.sheji;class test{    public static void main(String[] args) { A a = new A(); // A类通过接口去依赖C类 a.method1(new C()); a.method2(new C()); a.method3(new C()); B b = new B(); // B类通过接口去依赖(使用)D类 b.method1(new D()); b.method4(new D()); b.method5(new D());    }}interface Segregation1 {    void operation1();}interface Segregation2 {    void operation2();    void operation3();}interface Segregation3 {    void operation4();    void operation5();}class C implements Segregation1,Segregation2{    @Override    public void operation1() { System.out.println("C==========operation1");    }    @Override    public void operation2() { System.out.println("C==========operation2");    }    @Override    public void operation3() { System.out.println("C==========operation3");    }}class D implements Segregation1,Segregation3{    @Override    public void operation1() { System.out.println("D==========operation1");    }    @Override    public void operation4() { System.out.println("D==========operation4");    }    @Override    public void operation5() { System.out.println("D==========operation5");    }}// A 类通过接口Segregation1,Segregation2依赖(使用) C类,但是只会用到1,2,3方法,保证最小依赖关系class A{    public void method1(Segregation1 segregation) { segregation.operation1();    }    public void method2(Segregation2 segregation) { segregation.operation2();    }    public void method3(Segregation2 segregation) { segregation.operation3();    }}// B 类通过接口Segregation1,Segregation3依赖(使用) D类,但是只会用到1,4,5方法,保证最小依赖关系class B{    public void method1(Segregation1 segregation) { segregation.operation1();    }    public void method4(Segregation3 segregation) { segregation.operation4();    }    public void method5(Segregation3 segregation) { segregation.operation5();    }}

3、依赖倒置原则

依赖倒置原则(Dependence Inversion Principle)是指:

  • 高层模块不应该依赖低层模块,二者都应该依赖其抽象
  • 抽象不应该依赖细节,细节应该依赖抽象
  • 依赖倒置的中心思想是面向接口编程
  • 依赖倒置原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。在java中,抽象指的是接口或抽象类,细节就是具体的实现类
  • 使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成

案例:完成Person接收消息的功能

public class DependecyInversion {    public static void main(String[] args) { Person person = new Person(); person.receive(new Email());    }}class Email {    public String getInfo() { return "电子邮件信息: hello,world";    }}class Person {    public void receive(Email email ){ System.out.println(email.getInfo());    }}

此种方法比较容易想到,但如果我们获取的对象是微信,短信等等,则新增类,同时Perons也要增加相应的接收方法。怎样才能避免这个问题呢?答案便是采用依赖倒置原则实现
解决思路:引入一个抽象的接口IReceiver 表示接收者, 这样Person类与接口IReceiver发生依赖,因为Email, WeiXin 等等属于接收的范围,他们各自实现IReceiver接口就可以了, 这样我们就符合依赖倒转原则。

public class DependecyInversion {    public static void main(String[] args) { Person person = new Person(); person.receive(new Email()); person.receive(new WeiXin());    }}interface IReceiver{    String getInfo();}class Email implements IReceiver{    public String getInfo() { return "hello,Email";    }}class WeiXin implements IReceiver{    @Override    public String getInfo() { return "hello,WeiXin";    }}class Person {    // 这里我们是对接口的依赖    public void receive(IReceiver receiver){ System.out.println(receiver.getInfo());    }}

依赖关系传递的三种方式

  1. 接口传递
public class DependecyInversion {    public static void main(String[] args) { ChangHong changHong = new ChangHong(); OpenAndClose openAndClose = new OpenAndClose(); openAndClose.open(changHong);    }}// 通过接口传递实现依赖// 开关的接口interface ITV { //ITV接口    public void play();}interface IOpenAndClose {    public void open(ITV tv); //抽象方法,接收接口}class ChangHong implements ITV {    @Override    public void play() { System.out.println("长虹电视机,打开");    }}// 实现接口class OpenAndClose implements IOpenAndClose {    public void open(ITV tv) { tv.play();    }}
  1. 通过构造方法实现依赖传递
public class DependecyInversion {    public static void main(String[] args) { ChangHong changHong = new ChangHong(); OpenAndClose openAndClose = new OpenAndClose(changHong); openAndClose.open();    }}// 开关的接口interface ITV { //ITV接口    public void play();}interface IOpenAndClose {    public void open(); //抽象方法,接收接口}class ChangHong implements ITV {    @Override    public void play() { System.out.println("长虹电视机,打开");    }}// 实现接口class OpenAndClose implements IOpenAndClose {    private ITV tv;    // 创建构造器并传入接口参数    public OpenAndClose(ITV tv){ this.tv = tv;    }    public void open() { this.tv.play();    }}
  1. 通过setter实现依赖传递
public class DependecyInversion {    public static void main(String[] args) { ChangHong changHong = new ChangHong(); OpenAndClose openAndClose = new OpenAndClose(); // 设置接口 openAndClose.setTv(changHong); // 调用方法 openAndClose.open();    }}// 开关的接口interface ITV { //ITV接口    public void play();}interface IOpenAndClose {    void open(); //抽象方法,接收接口    void setTv(ITV tv); }class ChangHong implements ITV {    @Override    public void play() { System.out.println("长虹电视机,打开");    }}// 实现接口class OpenAndClose implements IOpenAndClose {    private ITV tv;    @Override    public void open() { this.tv.play();    }    @Override// 通过setter注入依赖    public void setTv(ITV tv) { this.tv = tv;    }}

依赖倒转原则的注意事项和细节

  • 低层模块尽量都要有抽象类或接口,或者两者都有,程序稳定性更好.

  • 变量的声明类型尽量是抽象类或接口, 这样我们的变量引用和实际对象间,就存在一个缓冲层,利于程序扩展和优化

  • 继承时遵循里氏替换原则

4、里氏替换原则

基本介绍

  • 里氏替换原则(Liskov Substitution Principle)在1988年,由麻省理工学院的一位姓里的女士提出的。

  • 如果对每个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型T2是类型T1 的子类型。换句话说,所有引用基类的地方必须能透明地使用其子类的对象。

  • 在使用继承时,遵循里氏替换原则,在子类中尽量不要重写父类的方法

  • 里氏替换原则告诉我们,继承实际上让两个类耦合性增强了,在适当的情况下,可以通过聚合,组合,依赖来解决问题。

一个程序引出的思考

public class Liskov {public static void main(String[] args) {A a = new A();System.out.println("11-3=" + a.func1(11, 3));System.out.println("1-8=" + a.func1(1, 8));System.out.println("-----------------------");B b = new B();System.out.println("11-3=" + b.func1(11, 3));//这里本意是求出11-3,但现在为11+3System.out.println("1-8=" + b.func1(1, 8));// 1-8System.out.println("11+3+9=" + b.func2(11, 3));}}// A类class A {// 返回两个数的差public int func1(int num1, int num2) {return num1 - num2;}}// B类继承了A// 增加了一个新功能:完成两个数相加,然后和9求和class B extends A {//这里,重写了A类的方法, 可能是无意识public int func1(int a, int b) {return a + b;}public int func2(int a, int b) {return func1(a, b) + 9;}}

我们发现原来运行正常的相减功能发生了错误。原因就是类B无意中重写了父类的方法,造成原有功能出现错误。在实际编程中,我们常常会通过重写父类的方法完成新的功能,这样写起来虽然简单,但整个继承体系的复用性会比较差。特别是运行多态比较频繁的时候。

解决方法

通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉, 采用依赖,聚合,组合等关系代替.

public class Liskov {public static void main(String[] args) {// TODO Auto-generated method stubA a = new A();System.out.println("11-3=" + a.func1(11, 3));System.out.println("1-8=" + a.func1(1, 8));System.out.println("-----------------------");B b = new B();//因为B类不再继承A类,因此调用者,不会再func1是求减法//调用完成的功能就会很明确System.out.println("11+3=" + b.func1(11, 3));//这里本意是求出11+3System.out.println("1+8=" + b.func1(1, 8));// 1+8System.out.println("11+3+9=" + b.func2(11, 3));//使用组合仍然可以使用到A类相关方法System.out.println("11-3=" + b.func3(11, 3));// 这里本意是求出11-3}}//创建一个更加基础的基类class Base {//把更加基础的方法和成员写到Base类}// A类class A extends Base {// 返回两个数的差public int func1(int num1, int num2) {return num1 - num2;}}// B类继承了A// 增加了一个新功能:完成两个数相加,然后和9求和class B extends Base {//如果B需要使用A类的方法,使用组合关系private A a = new A();//这里,重写了A类的方法, 可能是无意识public int func1(int a, int b) {return a + b;}public int func2(int a, int b) {return func1(a, b) + 9;}//我们仍然想使用A的方法public int func3(int a, int b) {return this.a.func1(a, b);}}

5、开闭原则

基本介绍

  • 开闭原则(Open Closed Principle)是编程中最基础、最重要的设计原则

  • 一个软件实体如类,模块和函数应该对扩展开放 (只对提供方开放,也就是可以在提供方处对功能进行扩展),对修改关闭(对使用方,也就是提供方新扩展了功能后,使用方不用做任何修改)。用抽象构建框架,用实现扩展细节。

  • 当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。

  • 编程中遵循其它原则,以及使用设计模式的目的就是遵循开闭原则。

看看下面这段代码,会发现有什么问题?

public class Ocp {    public static void main(String[] args) { GraphicEditor graphicEditor = new GraphicEditor(); graphicEditor.drawShape(new Rectangle()); graphicEditor.drawShape(new Circle()); // graphicEditor.drawShape(new Triangle());    }}//这是一个用于绘图的类 [使用方]class GraphicEditor {    //接收Shape对象,然后根据type,来绘制不同的图形    public void drawShape(Shape s) { if (s.m_type == 1)     drawRectangle(s); else if (s.m_type == 2)     drawCircle(s);/* else if (s.m_type == 3)     drawTriangle(s); */    }    //绘制矩形    public void drawRectangle(Shape r) { System.out.println(" 绘制矩形 ");    }    //绘制圆形    public void drawCircle(Shape r) { System.out.println(" 绘制圆形 ");    }    //绘制三角形   /* public void drawTriangle(Shape r) { System.out.println(" 绘制三角形 ");    }   */    }//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 Triangle extends Shape {    Triangle() { super.m_type = 3;    }}*/

通过上面的代码,我们可以发现,当我们要扩展一个功能的时候(比如上面的新增画三角形的功能,注释掉的部分),我们不仅要在功能提供方处新增代码,而且在功能使用方处也要修改代码,这不符合开闭原则,即对扩展开放(提供方),对修改关闭(使用方)。

修改上述代码

public class Ocp {    public static void main(String[] args) { //使用看看存在的问题 GraphicEditor graphicEditor = new GraphicEditor(); graphicEditor.drawShape(new Rectangle()); graphicEditor.drawShape(new Circle()); graphicEditor.drawShape(new Triangle()); graphicEditor.drawShape(new OtherGraphic());    }}//这是一个用于绘图的类 [使用方]class GraphicEditor {    //接收Shape对象,调用draw方法    public void drawShape(Shape s) { s.draw();    }}//Shape类,基类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 Triangle extends Shape {    @Override    public void draw() { System.out.println(" 绘制三角形 ");    }}//新增一个图形class OtherGraphic extends Shape {    @Override    public void draw() { System.out.println(" 绘制其它图形 ");    }}

上述代码我们要想扩展功能只需要继承基类 Shape实现里面的方法就行。而不必再修改使用者处的代码,符合开闭原则

6、迪米特原则

基本介绍

  • 一个对象应该对其他对象保持最少的了解
  • 类与类关系越密切,耦合度越大
  • 迪米特法则(Demeter Principle)又叫最少知道原则,即一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类不管多么复杂,都尽量将逻辑封装在类的内部。对外除了提供的public方法,不对外泄露任何信息
  • 迪米特法则还有个更简单的定义:只与直接的朋友通信
  • 直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系, 我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖,关联,组合,聚合 等。其中,**我们称出现成员变量,方法参数,方法返回值中的类为直接的朋友,而出现在局部变量中的类不是直接的朋友。**也就是说,陌生的类最好不要以局部变量 的形式出现在类的内部。

应用实例:有一个学校,下属有各个学院和总部,现要求打印出学校总部员工ID和学院员工的id

// 客户端public class Demeter1 {public static void main(String[] args) {// 创建了一个 SchoolManager 对象SchoolManager schoolManager = new SchoolManager();// 输出学院的员工id 和 学校总部的员工信息schoolManager.printAllEmployee(new CollegeManager());}}// 学校总部员工类class Employee {private String id;public void setId(String id) {this.id = id;}public String getId() {return id;}}// 学院的员工类class CollegeEmployee {private String id;public void setId(String id) {this.id = id;}public String getId() {return id;}}// 管理学院员工的管理类class CollegeManager {// 返回学院的所有员工public List<CollegeEmployee> getAllEmployee() {List<CollegeEmployee> list = new ArrayList<CollegeEmployee>();for (int i = 0; i < 10; i++) { // 这里我们增加了10个员工到 listCollegeEmployee emp = new CollegeEmployee();emp.setId("学院员工id= " + i);list.add(emp);}return list;}}/*学校管理类 分析 SchoolManager 类的直接朋友类有哪些? Employee、CollegeManager CollegeEmployee 不是直接朋友 而是一个陌生类,这样违背了迪米特法则 */class SchoolManager {// 返回学校总部的员工public List<Employee> getAllEmployee() {List<Employee> list = new ArrayList<Employee>();// 这里我们增加了5个员工到 listfor (int i = 0; i < 5; i++) { Employee emp = new Employee();emp.setId("学校总部员工id= " + i);list.add(emp);}return list;}// 该方法完成输出学校总部和学院员工信息(id)void printAllEmployee(CollegeManager sub) {/*分析问题1. 这里的 CollegeEmployee 不是 SchoolManager的直接朋友2. CollegeEmployee 是以局部变量方式出现在 SchoolManager3. 违反了迪米特法则 */// 获取到学院员工List<CollegeEmployee> list1 = sub.getAllEmployee();System.out.println("------------学院员工------------");for (CollegeEmployee e : list1) {System.out.println(e.getId());}// 获取到学校总部员工List<Employee> list2 = this.getAllEmployee();System.out.println("------------学校总部员工------------");for (Employee e : list2) {System.out.println(e.getId());}}}

前面设计的问题在于SchoolManager中,CollegeEmployee类并不是 SchoolManager类的直接朋友 ,按照迪米特法则,应该避免类中出现这样非直接朋友关系的耦合。下面为遵循迪米特法则的改进版代码

public class Demeter1 {public static void main(String[] args) {System.out.println("~~~使用迪米特法则的改进~~~");// 创建了一个 SchoolManager 对象SchoolManager schoolManager = new SchoolManager();// 输出学院的员工id 和  学校总部的员工信息schoolManager.printAllEmployee(new CollegeManager());}}// 学校总部员工类class Employee {private String id;public void setId(String id) {this.id = id;}public String getId() {return id;}}// 学院的员工类class CollegeEmployee {private String id;public void setId(String id) {this.id = id;}public String getId() {return id;}}// 管理学院员工的管理类class CollegeManager {// 返回学院的所有员工public List<CollegeEmployee> getAllEmployee() {List<CollegeEmployee> list = new ArrayList<CollegeEmployee>(); // 这里我们增加了10个员工到 listfor (int i = 0; i < 10; i++) { CollegeEmployee emp = new CollegeEmployee();emp.setId("学院员工id= " + i);list.add(emp);}return list;}// 输出学院员工的信息public void printEmployee() {// 获取到学院员工List<CollegeEmployee> list1 = getAllEmployee();System.out.println("------------学院员工------------");for (CollegeEmployee e : list1) {System.out.println(e.getId());}}}// 学校管理类class SchoolManager {// 返回学校总部的员工public List<Employee> getAllEmployee() {List<Employee> list = new ArrayList<Employee>();// 这里我们增加了5个员工到 listfor (int i = 0; i < 5; i++) {Employee emp = new Employee();emp.setId("学校总部员工id= " + i);list.add(emp);}return list;}// 该方法完成输出学校总部和学院员工信息(id)void printAllEmployee(CollegeManager sub) {// 分析问题// 1. 将输出学院的员工方法,封装到CollegeManagersub.printEmployee();// 获取到学校总部员工List<Employee> list2 = this.getAllEmployee();System.out.println("------------学校总部员工------------");for (Employee e : list2) {System.out.println(e.getId());}}}

迪米特法则注意事项和细节

迪米特法则的核心是降低类之间的耦合。但是注意:由于每个类都减少了不必要的依赖,因此迪米特法则只是要求降低类间(对象间)耦合关系, 并不是要求完全没有依赖关系。

7、合成复用原则

一句话:原则是尽量使用合成/聚合的方式,而不是使用继承

设计原则核心思想

  • 找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。
  • 针对接口编程,而不是针对实现编程。
  • 为了交互对象之间的松耦合设计而努力