设计模式01-七大设计原则

设计模式01-七大设计原则

文章目录

开闭原则-Open Close

一个软件实体如类、模块、函数应该对扩展开放,对修改关闭。用抽象构建框架,用实现扩展细节。提高程序的复用性和可维护性

案例:一个课程类,具有:课程类型、课程名称、售价3个属性,代码实现:

课程接口:

public interface ICourse {
    // 获取类型
    String getCategory();
    // 获取名称
    String getName();
    // 获取价格
    Double getPrice();
}

Java课程实现类:

public class JavaCourse implements ICourse {

    private String name;
    private Double price;
    private String category;

    public JavaCourse(String name, Double price, String category) {
        this.name = name;
        this.price = price;
        this.category = category;
    }

    @Override
    public String getCategory() {
        return category;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public Double getPrice() {
        return price;
    }
}

测试类:

public static void main(String[] args) {
    JavaCourse course = new JavaCourse("架构师", 998.00, "Java");
    System.out.println("课程:" + course.getName() 
                       + " 分类:" + course.getCategory() 
                       + " 价格:" + course.getPrice());
}

问题:此时要根据情况修改价格,如双11打8折,春节打五折,那我们直接在getPrice()方法中修改显然违背了开闭原则中对修改关闭的定义,比如下面这样:

@Override
public Double getPrice() {
    return price * 0.8;// 双11打8折
}

这样会带来的问题

  1. 频繁修改已有代码逻辑,如春节、双11、双12各有不同的折扣力度,每种会员可能又有不同的折扣计算方式,这时候都要来修改我们已有的程序,降低了程序的可维护性
  2. 违背了开闭原则

这时候该怎么办呢?当然是通过对扩展开放的定义来扩展我们的程序:

比如我们可以定义一个JavaCourse的折扣类来实现这个逻辑:

public class JavaDiscountCourse extends JavaCourse {

    public JavaDiscountCourse(String name, Double price, String category) {
        super(name, price, category);
    }

    public Double getDiscountPrice() { // 获取折扣价格
        return super.getPrice() * 0.8;
    }
}

然后修改我们的主程序来进行测试:

public static void main(String[] args) {
    JavaCourse course = new JavaDiscountCourse("架构师", 998.00, "Java");
    System.out.println("课程:" + course.getName() +
            " 分类:" + course.getCategory() +
            " 折扣价格:" + course.getPrice() +
            "原价:" + ((JavaDiscountCourse) course).getDiscountPrice()
    );
}

这样即符合了开闭原则的理念,又提高了我们程序的复用性和可维护性。

依赖倒置原则-Dependence Inversion

定义高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象;针对接口编程,不针对实现编程

优点可以减少类之间的耦合性、提高系统稳定性,提高代码可读性和可维护性,可降低修改程序所造成的风险

高层低层的概念: 离调用者越近,层次越高。离被调用者越近,层次越低。如我去调用别人的接口的某个方法,那我的代码就是高层模块,被调用的就是底层。

高层依赖低层模块可能造成的问题:低层方法参数修饰符、返回值类型等发生了变化,那么高层调用者也要做出相应的变化。

案例:Tom类需要学习Java、Python两门课程

Tom类(被调用方:低层):

public class Tom {

    public void studyJava() {
        System.out.println("学习Java课程");
    }
    public void studyPython() {
        System.out.println("学习Python课程");
    }

}

测试类(也就是我们的高层调用):

public class DIDemo {
    public static void main(String[] args) {
        Tom tom = new Tom();
        tom.studyJava();
        tom.studyPython();
    }
}

这时,程序看起来没什么问题。但是如果将Tom中的studyJava()方法改成javaStudy() 那么高层的调用就也需要修改了。这样的话就提高了修改造成的风险,而且程序的耦合性强。

接下来做一下修改:高层和低层都依赖抽象

首先建立抽象(课程的抽象接口):

public interface ICourse {
    void study();
}

然后创建Java和Python的学习类,并实现课程接口:

public class JavaStudy implements ICourse {
    @Override
    public void study() {
        System.out.println("学习Java");
    }
}

public class PythonStudy implements ICourse {
    @Override
    public void study() {
        System.out.println("学习Python");
    }
}

这时Tom类(低层)依赖抽象:

public class Tom {
	// 依赖了抽象ICourse
    public void study(ICourse iCourse) {
        iCourse.study();
    }
}

这时我们的测试类(高层)就可以修改为依赖抽象而不是细节:

public static void main(String[] args) {
    Tom tom = new Tom();
    tom.study(new JavaStudy());
    tom.study(new PythonStudy());
}

这样就降低了我们程序的耦合度,提高了程序稳定性和可读性降低了修改程序带来的风险

版本3:当然我们也可以通过构造方法来优化Tom类:

public class Tom {

    private ICourse iCourse;

    public Tom(ICourse iCourse) {
        this.iCourse = iCourse;
    }

    public void study() {
        iCourse.study();
    }

}

这样的话只需要在测试类中传入ICourse的实现类来构造Tom即可,同样也可以使用Set等方式进行注入。

一句话总结:面向接口编程

单一职责原则-Simple ResponsiBility

定义:不要存在多于一个导致类变更的原因。说白了:一个类、接口、方法只负责一项职责

优点:降低代码复杂度,提高程序可读性,提高系统可维护性,降低变更引起的风险

案例:观看两种课程,直播课不能快进,录播课可以快进

课程类(针对不同课程有不同处理逻辑):

public class Course {
    public void study(String courseName) {
        if ("直播课".equals(courseName)) {
            System.out.println("直播课不能快进");
        } else {
            System.out.println("录播课可以快进");
        }
    }
}

测试类:

public class StudyDemo {
    public static void main(String[] args) {
        Course course = new Course();
        course.study("直播课");
        course.study("录播课");
    }
}

这时候如果我们要对不同类型课程做不同的处理,比如编码解码处理,可能就需要针对Course#study方法进行修改,势必会增加代码复杂度,降低可读性。

接下来我们这样修改:分别在不同的类里面处理不同的课程:

直播课类

public class LiveCourse {
    public void study(String courseName) {
        System.out.println(courseName + "只能在线观看");
    }
}

录播课类

public class ReplayCourse {
    public void study(String courseName) {
        System.out.println(courseName + "可以反复观看");
    }
}

这样我们就可以直接调用不同的类进行处理。降低了复杂度,提高了可读性。

但是后面可能针对课程有很多新的职责:比如获取视频流、退款、学习课程、获取课程基本信息,这时候该怎么做呢?

接口级别

如用户接口,可以看视频和退款:

public interface ICourseManager {
    void readVideo();// 看视频
    void refundCourse();// 退款
}

课程信息接口,可以用来获取课程信息:

public interface ICourseInfo {
    String getCourseName();// 获取课程信息
}

这样的话,新增课程信息不会对其他职责造成影响,就满足了单一职责的定义

public class CourseImpl implements ICourseInfo, ICourseManager {
    @Override
    public String getCourseName() {
        return null;
    }

    @Override
    public void readVideo() {
    }

    @Override
    public void refundCourse() {
    }
}

方法级别

以修改用户信息为例

public class LoginMethod {
    private void updateUserName() {
        // 修改用户名
    }
    private void updateUserPhone() {
        // 修改用户手机号
    }
    private void updateUserAddress() {
        // 修改用户地址
    }
}

如上,较小颗粒度的拆分方法职责,使其看起来明了,并且职责清晰,不会相互影响。

接口隔离原则-Interface Segregation

定义:用多个专门的接口,而不是使用单一的总接口,客户端不应该依赖它不需要的接口

注意:一个类对应一个类的依赖应该建立在最小的接口上;建立单一接口,不要建立庞大臃肿的接口;尽量细化接口,接口中的方法尽量少;注意适度原则,一定要适度

优点:高内聚,低耦合,从而提高可读性,可扩展性和可维护性

比如我们有一个动物接口,内部提供了一些方法:

public interface IAnimal {

    // 跑
    void run();
    // 飞
    void fly();
    // 游泳
    void swim();
    // 吃东西
    void eat();

}

这时候假如我们有一个鸟类,实现该接口的话就需要实现一些不需要实现的方法,如游泳。

这时候我们可以针对:吃、跑、飞、游泳分别建立接口并提供方法,这时候鸟类只需要实现吃和飞的接口就可以了。

迪米特法则-Law of Demeter

定义:一个对象应该对其他对象保持最少的了解,又叫最少知道原则

强调只和朋友交流,不和陌生人说话

朋友的概念:出现在成员变量、方法输入、输出参数中的类称为成员朋友类,而出现在方法体内部的类不属于朋友类

作用:一定程度上解耦

案例:团队领导让员工查询现有的课程数量

课程类:

public class Course {
    private String name;

    public String getName() {
        return name;
    }

    public Course setName(String name) {
        this.name = name;
        return this;
    }
}

员工类(用来查询课程数量):

public class Employee {
    public int getCourseNumber(List<Course> courses) {
        return courses.size();
    }
}

团队领导类:

public class TeamLeader {
    public int getNumbersByEmployee(Employee employee) {
        List<Course> courses = new ArrayList<>();
        for (int i = 0; i < 20; i++) {
            courses.add(new Course());
        }
        return employee.getCourseNumber(courses);
    }
}

接下来是测试方法:

public static void main(String[] args) {
    Employee employee = new Employee();
    TeamLeader teamLeader = new TeamLeader();
    System.out.println(teamLeader.getNumbersByEmployee(employee));
}

这里就发现问题了:TeamLeader引用的Course类并不满足迪米特法则,即Course在TeamLeader中并不是其“朋友”。

里氏替换原则-Liskov Substitution

定义:如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都替换成o2时,程序P的行为没有发生变化,那么类型T2就是类型T1的子类型

定义扩展:一个软件实体如果适用一个父类的话,那一定适用其子类,所有引用父类的地方必须能透明地使用其子类的对象,子类对象能替换父类对象,而程序逻辑不变

引申意义:子类可以扩展父类的功能,但不能改变父类原有的功能,比如前面开闭原则的案例,打折课程类新写了一个打折方法:

public class JavaDiscountCourse extends JavaCourse {

    public JavaDiscountCourse(String name, Double price, String category) {
        super(name, price, category);
    }
    
    public Double getDiscountPrice() { // 获取折扣价格
        return super.getPrice() * 0.8;
    }

}

含义1:子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法

含义2:子类中可以增加自己特有的方法

含义3:子类方法重载父类的方法时,方法的前置条件(即方法的输入、入参)要比父类的输入参数更宽松

含义4:当子类的方法实现父类的方法时(重写、重载或实现抽象方法),方法的后置条件(即方法的输出、返回值)要比父类更严格或相等

优点1:约束继承泛滥,开闭原则的一种体现

优点2:加强程序的健壮性,同时变更时也可以做到非常好的兼容性提高程序的维护性,扩展性。降低需求变更时引入的风险

例子:以正方形长方形为例,在测试类中获取长方形的宽高,如果宽大于高就修改参数,直至宽小于等于高,代码如下:

长方形类:

public class Rectangle {

    private Integer width;// 宽

    private Integer height;// 高

    public Integer getWidth() {
        return width;
    }

    public Rectangle setWidth(Integer width) {
        this.width = width;
        return this;
    }

    public Integer getHeight() {
        return height;
    }

    public Rectangle setHeight(Integer height) {
        this.height = height;
        return this;
    }
}

正方形类:

public class Square extends Rectangle {

    private Integer length;

    public Integer getLength() {
        return length;
    }

    public Square setLength(Integer length) {
        this.length = length;
        return this;
    }

    @Override
    public Integer getWidth() {
        return this.length;
    }

    @Override
    public Rectangle setWidth(Integer width) {
        return setLength(width);
    }

    @Override
    public Integer getHeight() {
        return this.length;
    }

    @Override
    public Rectangle setHeight(Integer height) {
        return setLength(height);
    }
}

可以看到为了满足正方形的边长相等的属性,我们修改了其父类的width height的get和set方法。

接下来是测试类,定义一个resize()方法,如果长方形的宽大于高,就在while中将高度+1:

public class TestDemo {
    private static void resize(Rectangle rectangle) {
        while (rectangle.getWidth() >= rectangle.getHeight()) {
            rectangle.setHeight(rectangle.getHeight() + 1);
            System.out.println("当前宽度:" + rectangle.getHeight());
        }
    }

    public static void main(String[] args) {
        Rectangle rectangle = new Rectangle();
        rectangle.setWidth(20);
        rectangle.setHeight(10);
        resize(rectangle);
    }
}

这里我们用父类进行测试,控制台打印如下:

当前宽度:11
当前宽度:12
当前宽度:13
当前宽度:14
当前宽度:15
当前宽度:16
当前宽度:17
当前宽度:18
当前宽度:19
当前宽度:20
当前宽度:21

Process finished with exit code 0

接下来用子类Square进行测试:

当前宽度:407744
当前宽度:407745
当前宽度:407746
当前宽度:407747
当前宽度:407748
当前宽度:407749
...无限循环

可以看到这个resize()由于传的是子类,从而破坏了该方法的正常逻辑,也不满足里氏替换原则。

再回顾一下定义:使得以T1定义的所有程序P在所有的对象o1都替换成o2时,程序P的行为没有发生变化

那么上面的问题如何解决呢?

定义一个四边形接口:

public interface Quadrangle {
    Integer getHeight();// 获取高度
    Integer getWidth();// 获取宽度
}

定义正方形类:

public class Square implements Quadrangle {

    private Integer length;

    public Square setLength(Integer length) {
        this.length = length;
        return this;
    }

    @Override
    public Integer getHeight() {
        return this.length;
    }

    @Override
    public Integer getWidth() {
        return this.length;
    }
}

定义长方形类:

public class Rectangle implements Quadrangle {

    private Integer width;// 宽

    private Integer height;// 高

    public Rectangle setWidth(Integer width) {
        this.width = width;
        return this;
    }

    public Rectangle setHeight(Integer height) {
        this.height = height;
        return this;
    }

    @Override
    public Integer getHeight() {
        return this.height;
    }

    @Override
    public Integer getWidth() {
        return this.width;
    }
}

测试程序:

public class TestDemo {

    private static void resize(Quadrangle quadrangle) {
        while (quadrangle.getWidth() >= quadrangle.getHeight()) {
            // 由于四边形没有提供set方法所以这里会报错
            quadrangle.setHeight(quadrangle.getHeight() + 1);
            System.out.println("当前宽度:" + quadrangle.getHeight());
        }
    }

    public static void main(String[] args) {
        Square square = new Square();
        square.setLength(10);// 边长为10的正方形
        resize(square);
    }
}

这里由于四边形类并没提供setHeight()方法,所以这里的第五行代码会报错,从一定程度上避免了继承泛滥。

合成(组合)、复用原则-Composite&Aggregate Reuse

定义:尽量使用对象组合、聚合,而不是继承关系达到软件复用的目的

聚合:has - a , 比如电脑和U盘,可以在一起工作,电脑也可以单独工作

组合:contains - a,比如人体的各个部位,组合在一起才能有完整的生命周期

继承:is - a

优点:可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少

rangle.getWidth() >= quadrangle.getHeight()) {
// 由于四边形没有提供set方法所以这里会报错
quadrangle.setHeight(quadrangle.getHeight() + 1);
System.out.println(“当前宽度:” + quadrangle.getHeight());
}
}

public static void main(String[] args) {
    Square square = new Square();
    square.setLength(10);// 边长为10的正方形
    resize(square);
}

}


这里由于四边形类并没提供setHeight()方法,所以这里的第五行代码会报错,从一定程度上避免了继承泛滥。



## 合成(组合)、复用原则-Composite&Aggregate Reuse

定义:**尽量使用对象组合、聚合,而不是继承关系达到软件复用的目的**

聚合:**has - a** , 比如电脑和U盘,可以在一起工作,电脑也可以单独工作

组合:**contains - a**,比如人体的各个部位,组合在一起才能有完整的生命周期

继承:**is - a**

优点:**可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少**

**一句话总结:能不用继承就不用继承**

最后总结:设计模式是对我们开发中做的一些规范和约束,在实际的开发中并非要追求完美,而是在时间、成本等各方面允许的情况下尽量遵守规范。
上一篇:装箱和拆箱,字符串数字转换


下一篇:springboot 整合 shiro 安全框架