设计模式的六大原则

841 阅读10分钟

1.单一职责原则

基本介绍

对类来说,及一个类应该只负责一项职责,如类A负责两个不同的职责,职责1、职责2,当职责1需求变更而改变A时,可能造成职责2执行错误,所以需要将类A的粒度分为A1,A2。

单一职责注意事项及细节:

  • 降低类的复杂度,一个类只负责一项职责
  • 提高类的可读性,可维护性
  • 降低变更引起的风险
  • 通常情况下,我们应该遵守单一职责原则,只有逻辑足够简单,才可以在代码级违反单一职责原则:只有类中方法数量足够少,可以在方法级别保持单一职责原则。

应用举例

一个交通工具的例子

  • 方案1
public class SingleResponsibility1 {
    public static void main(String[] args) {
        Vehicle vehicle = new Vehicle();
        vehicle.run("小汽车");
        vehicle.run("飞机");
        vehicle.run("船");
    }
}

//交通工具类
//方案1
//1、在方式1的run方法中,违反了单一职责原则
//2、解决方法:根据交通工具运行方法的不同,分解成不同的类
class Vehicle {
    public void run(String vehicle) {
        System.out.println(vehicle + " 在公路上行走");
    }
}
  • 方案2
public class SigleResponsibility2 {
    public static void main(String[] args) {
        Vehicle vehicle = new Vehicle();
        vehicle.run("小汽车");
        AirVehicle airVehicle = new AirVehicle();
        airVehicle.run("飞机");
        WaterVehicle waterVehicle = new WaterVehicle();
        waterVehicle.run("船");
    }
}
//方案二分析
//1、遵循单一职责
//2、相对于方案1,改动很大,即将类分解,同时修改客户端
//3、改进:直接修改Vehicle类,改动的代码较少
class RoadVehicle {
    public void run(String vehicle) {
        System.out.println(vehicle + " 在公路上运行");
    }
}

class AirVehicle {
    public void run(String vehicle) {
        System.out.println(vehicle + " 在天上飞");
    }
}

class WaterVehicle {
###### public void run(String vehicle) {
        System.out.println(vehicle + " 在水里运行");
    }
}
  • 方案3:
public class SingleResponsibility3 {
    public static void main(String[] args) {
        Vehicle3 vehicle = new Vehicle3();
        vehicle.run("小汽车");
        vehicle.runAir("飞机");
        vehicle.runWater("船");
    }
}

//方案3
//1、这种修改方法没有对原来的类做太大的改动,只是增加了方法。
//2、没有严格意思上遵守单一职责,但在方法级别上遵守单一职责原则
class Vehicle3 {
    public void run(String vehicle) {
        System.out.println(vehicle + " 在公路上行走");
    }

    public void runAir(String vehicle) {
        System.out.println(vehicle + " 在天上飞");
    }

    public void runWater(String vehicle) {
        System.out.println(vehicle + " 在水中运行");
    }
}

2.接口隔离原则

基本介绍

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

应用举例

image.png

  • 如图:
  1. A通过Interface1会依赖(使用)B,但是A中只会用到接口的1,2,3三个方法
  2. C通过Interface1会依赖(使用)D,但是A中只会用到接口的1,4,5三个方法 按接口隔离原则:应该将Interface1拆分为独立的几个接口,类A和类C分别与它们需要的接口建立依赖关系,也就是采用接口隔离原则。

image.png

3.依赖倒转原则

基本介绍

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

应用举例

编程完成Person发送消息的功能 方案1

public class DependencyInversion {
    public static void main(String[] args) {
        new Person().receive(new Email());
    }
}

class Email {
    public String getInfo() {
        return "电子邮件信息:Hello World!";
    }
}
//方案1
//1.简单,容易想到
//2.如果要获取微信、短信等信息,则需要新增类,同时Person也要增加相应的接收方法。
//3.解决方案:引入一个抽象的接口IReceiver,表示接收者,这样Person类与接口IReceive发生依赖。email、微信、短信实现IReceiver接口
class Person {
    public void receive(Email email) {
        System.out.println(email.getInfo());
    }
}

方案2,改进方案1

public class DependencyInversionImprove {
    public static void main(String[] args) {
        new Person2().receive(new Email2());
        new Person2().receive(new Wechat());
    }
}

interface IReceiver {
    String getInfo();
}

class Email2 implements IReceiver {
    public String getInfo() {
        return "电子邮件信息:Hello World!";
    }
}

class Wechat implements IReceiver {
    public String getInfo() {
        return "微信信息:Hello World!";
    }
}

//方案2
class Person2 {
    public void receive(IReceiver receive) {
        System.out.println(receive.getInfo());
    }
}

依赖关系传递的三种方式

  1. 接口传递
public class DependencyTransmit {
    public static void main(String[] args) {
        ITV tv = new XiaoMiTV();
        IOpenAndClose openAndClose = new OpenAndClose();
        openAndClose.open(tv);
    }
}

interface IOpenAndClose {
    void open(ITV tv);
}

interface ITV {
    void play();
}

class OpenAndClose implements IOpenAndClose {
    @Override
    public void open(ITV tv) {
        tv.play();
    }
}

class XiaoMiTV implements ITV {
    @Override
    public void play() {
        System.out.println("小米电视打开了");
    }
}
  1. 构造器传递
public class DependencyTransmit2 {
    public static void main(String[] args) {
        ITV2 tv = new XiaoMiTV2();
        OpenAndClose2 openAndClose = new OpenAndClose2(tv);
        openAndClose.open();
    }
}

interface IOpenAndClose2 {
    void open();
}

interface ITV2 {
    void play();
}

class OpenAndClose2 implements IOpenAndClose2 {
    private ITV2 tv;

    public OpenAndClose2(ITV2 tv) {
        this.tv = tv;
    }

    @Override
    public void open() {
        tv.play();
    }
}

class XiaoMiTV2 implements ITV2 {
    @Override
    public void play() {
        System.out.println("小米电视打开了");
    }
}
  1. setter方法传递
public class DependencyTransmit {
    public static void main(String[] args) {
        ITV tv = new XiaoMiTV();
        IOpenAndClose openAndClose = new OpenAndClose();
        openAndClose.open(tv);
    }
}

interface IOpenAndClose {
    void open(ITV tv);
}

interface ITV {
    void play();
}

class OpenAndClose implements IOpenAndClose {
    @Override
    public void open(ITV tv) {
        tv.play();
    }
}

class XiaoMiTV implements ITV {
    @Override
    public void play() {
        System.out.println("小米电视打开了");
    }
}

4.里氏替换原则

基本介绍

  • 如果对每个类型为T1对对象t1,都有类型为T2的对象t2,使得以T1定义的程序P在所有的对象t1都替换成t2时,程序P的行为没有发生变化,那么类型T2是T1的子类型。换言之,所有引用基类的地方必须透明的使用其子类对象。
  • 使用继承时,遵循里氏替换原则,子类中尽量不要复写父类的方法。
  • 里氏替换原则告诉我们,继承实际上让两个类耦合性增强了,在适当的情况下,可以使用聚合、组合、依赖来解决问题。

应用举例

应用继承时遇到的一些问题

public class Liskov {
    public static void main(String[] args) {
        A a = new A();
        System.out.println("11-3=" + a.fun1(11, 3));
        System.out.println("1-8=" + a.fun1(1, 8));

        B b = new B();
        System.out.println("11-3=" + b.fun1(11, 3));
        System.out.println("1-8=" + b.fun1(1, 8));
        System.out.println("1-8+9=" + b.fun2(1, 8));
    }
}

class A {
    public int fun1(int a, int b) {
        return a - b;
    }
}

class B extends A {
    @Override
    public int fun1(int a, int b) {
        return a + b;
    }

    public int fun2(int a, int b) {
        return fun1(a, b) + 9;
    }
}

B类复写了父类的方法,造成原有功能出现错误。实际过程中,我们往往会通过重写父类的方法完成新的功能,这样看起来虽然简单,但整个集成体系的复用性会比较差,特别是运行多态比较频繁的时候。 通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,采用依赖、聚合、组合等关系替代。

image.png 代码实现

public class LiskovImprove {
    public static void main(String[] args) {
        A2 a = new A2();
        System.out.println("11-3=" + a.fun1(11, 3));
        System.out.println("1-8=" + a.fun1(1, 8));

        B2 b = new B2();
        //明确接口的功能 这里就不会误以为是11-3,会正确的写成11+3
        System.out.println("11+3=" + b.fun1(11, 3));
        System.out.println("1+8=" + b.fun1(1, 8));
        System.out.println("1-8+9=" + b.fun2(1, 8));
    }
}

interface Base {
    int fun1(int a, int b);
}

class A2 implements Base {
    public int fun1(int a, int b) {
        return a - b;
    }
}

class B2 implements Base {
    A2 a = new A2();

    @Override
    public int fun1(int a, int b) {
        return a + b;
    }

    public int fun2(int a, int b) {
        return this.a.fun1(a, b) + 9;
    }
}

5.开闭原则(Open Close Principle)

基本介绍

  • 软件设计应该对扩展开放(服务方), 对修改关闭(调用方);用抽象构建框架,用实现扩展细节。
  • 当软件需求变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。
  • 编程中遵循其他原则,以及使用设计模式就是遵循开闭原则

应用举例

完成一个画图形的功能 传统类图

image.png

public class GraphicEditor {
    public static void main(String[] args) {
        GraphicEditor graphicEditor = new GraphicEditor();
        graphicEditor.drawShape(new Rectangle());
        graphicEditor.drawCircle(new Circle());
    }
    public void drawShape(Shape shape) {
        if (shape.getType() == 1) {
            drawRectangle(shape);
        } else if (shape.getType() == 2) {
            drawCircle(shape);
        }
    }

    private void drawRectangle(Shape shape) {
        System.out.println("画矩形");
    }

    private void drawCircle(Shape shape) {
        System.out.println("画圆形");
    }
}

@Data
class Shape {
    int type;
}

class Rectangle extends Shape {
    public Rectangle() {
        super.type = 1;
    }
}

class Circle extends Shape {
    public Circle() {
        super.type = 2;
    }
}

代码分析

  • 优点是比较好理解,简单易操作
  • 确定是违反了设计模式开闭原则,即对扩展开放,对修改关闭。
  • 如果我们要增加一个图形种类,如:三角形,修改的地方较多,甚至于还会影响到现有的图形。 传统方案改进思路:把创建Shape类做成抽象类,并通过一个抽象的draw方法,让子类去实现即可,使用方的代码不需要修改,满足了开闭原则

image.png

public class GraphicEditorImprove {
    public static void main(String[] args) {
        GraphicEditorImprove graphicEditorImprove = new GraphicEditorImprove();
        graphicEditorImprove.drawShape(new Rectangle2());
        graphicEditorImprove.drawShape(new Circle2());
        graphicEditorImprove.drawShape(new Triangle());
    }

    public void drawShape(Shape2 shape) {
        shape.draw();
    }
}

@Data
abstract class Shape2 {
    abstract void draw();
}

class Rectangle2 extends Shape2 {

    @Override
    void draw() {
        System.out.println("画矩形");
    }
}

class Circle2 extends Shape2 {
    @Override
    void draw() {
        System.out.println("画圆形");
    }
}

class Triangle extends Shape2 {
    @Override
    void draw() {
        System.out.println("画三角形");
    }
}

6.迪米特法制

基本介绍

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

应用举例

有一个学校,下属有各个学院和总部,现要求打印出学校总部员工id和学院员工的id。 传统代码:

public class Demeter {
    public static void main(String[] args) {
        SchoolManager schoolManager = new SchoolManager();
        schoolManager.printAllEmployee(new CollegeManager());
    }
}

@Data
@AllArgsConstructor
class CollegeEmployee {
    private Long id;
}

@Data
@AllArgsConstructor
class Employee {
    private Long id;
}

class SchoolManager {
    public List<Employee> getAllEmployee() {
        return IntStream.rangeClosed(0, 10).mapToObj(i -> new Employee((long) i)).collect(Collectors.toList());
    }

    void printAllEmployee(CollegeManager collegeManager) {
        List<CollegeEmployee> collegeEmployees = collegeManager.getAllEmployee();
        System.out.println("分公司员工");
        collegeEmployees.stream().forEach(e -> System.out.println(e.getId()));

        List<Employee> employees = getAllEmployee();
        System.out.println("学校总部员工");
        employees.stream().forEach(s -> System.out.println(s.getId()));
    }
}

class CollegeManager {
    public List<CollegeEmployee> getAllEmployee() {
        return IntStream.rangeClosed(0, 10).mapToObj(i -> new CollegeEmployee((long) i)).collect(Collectors.toList());
    }
}
  • 传统代码分析:前面设计的问题在于schoolManager中,CollegeEmployee类并不是SchoolManager的直接朋友,按照迪米特法则,应该避免类中出现这样非直接朋友关系的耦合。
  • 代码改进:前面设计的问题在于SchoolManager中,CollegeEmployee类并不是SchoolManager类的直接朋友。按照迪米特法则,应该避免类中出现这样非直接朋友关系的耦合。

改进的代码

class SchoolManager {
    public List<Employee> getAllEmployee() {
        return IntStream.rangeClosed(0, 10).mapToObj(i -> new Employee((long) i)).collect(Collectors.toList());
    }

    void printAllEmployee(CollegeManager collegeManager) {
        collegeManager.printEmployee();

        List<Employee> employees = getAllEmployee();
        System.out.println("学校总部员工");
        employees.stream().forEach(s -> System.out.println(s.getId()));
    }
}

class CollegeManager {
    public List<CollegeEmployee> getAllEmployee() {
        return IntStream.rangeClosed(0, 10).mapToObj(i -> new CollegeEmployee((long) i)).collect(Collectors.toList());
    }

    void printEmployee() {
        List<CollegeEmployee> collegeEmployees = this.getAllEmployee();
        System.out.println("分公司员工");
        collegeEmployees.stream().forEach(e -> System.out.println(e.getId()));
    }
}

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

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

合成复用原则

合成复用原则是尽量使用聚合、组合的方式,而不是使用继承。

image.png