《Java 设计模式精讲》笔记——第3章 软件设计七大原则

声明:

本博客是本人在学习《Java 设计模式精讲》后整理的笔记,旨在方便复习和回顾,并非用作商业用途。

本博客已标明出处,如有侵权请告知,马上删除。

1. 七大原则

  • 开闭原则
  • 依赖倒置原则
  • 单一职责原则
  • 接口隔离原则
  • 迪米特法则(最少知道原则)
  • 里氏替换原则
  • 合成/复用原则(组合/复用原则)

2. 开闭原则

  • 定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭
  • 用抽象构建框架,用实现扩展细节
  • 优点:提高软件系统的可复用性及可维护性

开闭原则 coding

  1. 创建一个课程的接口

    public interface ICourse {
          
          
        Integer getId();
        String getName();
        Double getPrice();
    }
    
  2. 创建 Java 课程的类并且实现课程接口

    public class JavaCourse implements ICourse {
          
          
        private Integer id;
        private String name;
        private Double price;
    
        public JavaCourse(Integer id, String name, Double price) {
          
          
            this.id = id;
            this.name = name;
            this.price = price;
        }
    
        @Override
        public Integer getId() {
          
          
            return this.id;
        }
    
        @Override
        public String getName() {
          
          
            return this.name;
        }
    
        @Override
        public Double getPrice() {
          
          
            return this.price;
        }
    }
    
  3. 创建测试类

    public class Test {
          
          
        public static void main(String[] args) {
          
          
            ICourse javaCourse = new JavaCourse(96, "Java 从零开始到企业级开发", 348d);
            System.out.println("课程Id:" + javaCourse.getId() + " 课程名称:" + javaCourse.getName() + " 课程价格:" + javaCourse.getPrice() + "元");
        }
    }
    

    运行结果:

    课程Id:96 课程名称:Java 从零开始到企业级开发 课程价格:348.0元
    

现在类图如下所示:

在这里插入图片描述

假如现在 Java 课程出了一个打折活动,我们需要新增一个打折方法该如何去做呢?

下面是一种不遵守开闭原则的这做法:

  1. 在课程的接口中新增一个打折方法

    public interface ICourse {
          
          
        Integer getId();
        String getName();
        Double getPrice();
        Double getDiscountPrice();
    }
    
  2. 在 Java 课程的类中实现这个方法

    public class JavaCourse implements ICourse {
          
          
        private Integer id;
        private String name;
        private Double price;
    
        public JavaCourse(Integer id, String name, Double price) {
          
          
            this.id = id;
            this.name = name;
            this.price = price;
        }
    
        @Override
        public Integer getId() {
          
          
            return this.id;
        }
    
        @Override
        public String getName() {
          
          
            return this.name;
        }
    
        @Override
        public Double getPrice() {
          
          
            return this.price;
        }
    
        @Override
        public Double getDiscountPrice() {
          
          
            return this.price * 0.8;
        }
    }
    
  3. 测试类

    public class Test {
          
          
        public static void main(String[] args) {
          
          
            ICourse javaCourse = new JavaCourse(96, "Java 从零开始到企业级开发", 348d);
            System.out.println("课程Id:" + javaCourse.getId() + " 课程名称:" + javaCourse.getName() + " 课程价格:" + javaCourse.getPrice() + "元" + " 打折后价格:" + javaCourse.getDiscountPrice() + "元");
        }
    }
    

    运行结果:

    课程Id:96 课程名称:Java 从零开始到企业级开发 课程价格:348.0元 打折后价格:278.40000000000003元
    

    分析:假如课程很多,那么所有的课程的实现类都要重写一下方法,接口应该是稳定的,不应该是经常修改的。

下面是一种遵守开闭原则的这做法:

  1. 拓展一个 Java 课程打折类并且继承于 Java 课程类,在这个类中重写 getPrice() 方法

    public class JavaDiscountCourse extends JavaCourse {
          
          
        public JavaDiscountCourse(Integer id, String name, Double price) {
          
          
            super(id, name, price);
        }
    
        @Override
        public Double getPrice() {
          
          
            return super.getPrice() * 0.8;
        }
    }
    
    
  2. 测试类

    public class Test {
          
          
        public static void main(String[] args) {
          
          
            ICourse javaCourse = new JavaDiscountCourse(96, "Java 从零开始到企业级开发", 348d);
            System.out.println("课程Id:" + javaCourse.getId() + " 课程名称:" + javaCourse.getName() + " 打折后价格:" + javaCourse.getPrice() + "元");
        }
    }
    

    运行结果:

    课程Id:96 课程名称:Java 从零开始到企业级开发 打折后价格:278.40000000000003元
    

    分析:我们通过继承了 Java 课程类这个父类,使对于扩展是开放的,而对于修改这个父类和接口是关闭的,提高软件系统的可复用性及可维护性

如果我们还想要原价可以采用以下做法:

  1. 在拓展的打折类中,新增 getOriginPrice() 方法

    public class JavaDiscountCourse extends JavaCourse {
          
          
        public JavaDiscountCourse(Integer id, String name, Double price) {
          
          
            super(id, name, price);
        }
    
        public Double getOriginPrice() {
          
          
            return super.getPrice();
        }
    
        @Override
        public Double getPrice() {
          
          
            return super.getPrice() * 0.8;
        }
    }
    
  2. 测试类

    public class Test {
          
          
        public static void main(String[] args) {
          
          
            ICourse ICourse = new JavaDiscountCourse(96, "Java 从零开始到企业级开发", 348d);
            // 如果调用实现类里面的方法,我们就必须要进行强转一下
            JavaDiscountCourse javaCourse = (JavaDiscountCourse) ICourse;
            System.out.println("课程Id:" + javaCourse.getId() + " 课程名称:" + javaCourse.getName() + " 课程价格:" + javaCourse.getOriginPrice() + "元" + " 打折后价格:" + javaCourse.getPrice() + "元");
        }
    }
    

    运行结果:

    课程Id:96 课程名称:Java 从零开始到企业级开发 课程价格:348.0元 打折后价格:278.40000000000003元
    

最终类图如下所示:

在这里插入图片描述

3. 依赖倒置原则

  • 定义∶高层模块不应该依赖低层模块,二者都应该依赖其抽象
  • 抽象不应该依赖细节;细节应该依赖抽象
  • 针对接口编程,不要针对实现编程
  • 优点∶可以减少类间的耦合性、提高系统稳定性,提高代码可读性和可维护性,可降低修改程序所造成的风险

依赖倒置原则 coding

  1. 创建一个 Gelly 的类,里面有两个方法,一个学习 Java 课程的方法,一个是学习 FE 课程的方法:

    public class Gelly {
          
          
        public void studyJavaCourse() {
          
          
            System.out.println("Gelly在学习Java课程");
        }
        
        public void studyFECourse() {
          
          
            System.out.println("Gelly在学习FE课程");
        }
    }
    
  2. 测试类

    public class Test {
          
          
        public static void main(String[]args){
          
          
            Gelly Gelly = new Gelly();
            Gelly.studyJavaCourse();
            Gelly.studyFECourse();
        }
    }
    
  3. 如果这个时候,我还想要添加一个学习 Python 课程的方法,我需要在基类里面进行添加方法,再在测试类中进行添加方法

    public class Gelly {
          
          
        public void studyJavaCourse() {
          
          
            System.out.println("Gelly在学习Java课程");
        }
    
        public void studyFECourse() {
          
          
            System.out.println("Gelly在学习FE课程");
        }
    
        public void studyPythonCourse() {
          
          
            System.out.println("Gelly在学习Python课程");
        }
    }
    
    public class Test {
          
          
        public static void main(String[]args){
          
          
            Gelly Gelly = new Gelly();
            Gelly.studyJavaCourse();
            Gelly.studyFECourse();
            Gelly.studyPythonCourse();
        }
    }
    

    运行结果:

    Gelly在学习Java课程
    Gelly在学习FE课程
    Gelly在学习Python课程
    

    分析:以上我们的做法就是在针对实现来进行编程,测试类作为高层模块依赖于 Gelly 这个低层模块,这是遵循依赖倒置原则的做法。

下面我们针对接口进行编程:

  1. 创建一个课程接口,里面有一个 studyCourse() 方法

    public interface ICourse {
          
          
        public void studyCourse();
    }
    
  2. 创建两个类 JavaCourse、FECourse 实现 ICourse 接口

    public class JavaCourse implements ICourse {
          
          
    
        @Override
        public void studyCourse() {
          
          
            System.out.println("Gelly在学习Java课程");
        }
    }
    
    public class FECourse implements ICourse{
          
          
    
        @Override
        public void studyCourse() {
          
          
            System.out.println("Gelly在学习FE课程");
        }
    }
    
  3. 创建一个 Gelly 的类,里面有一个学习课程方法,传入了学习课程的接口,由具体的实现类来进行实现

    public class Gelly {
          
          
        public void studyCourse(ICourse iCourse) {
          
          
            iCourse.studyCourse();
        }
    }
    
  4. 测试类

    public class Test {
          
          
        public static void main(String[]args){
          
          
            Gelly Gelly = new Gelly();
            Gelly.studyCourse(new JavaCourse());
            Gelly.studyCourse(new FECourse());
        }
    }
    
  5. 此时,如果要学习 Python 的课程的话,只需要新增 PythonCourse 实现 ICourse 接口,在测试中调用就可以了。

    public class PythonCourse implements ICourse {
          
          
    
        @Override
        public void studyCourse() {
          
          
            System.out.println("Gelly在学习Python课程");
        }
    }
    
    public class Test {
          
          
        public static void main(String[]args){
          
          
            Gelly Gelly = new Gelly();
            Gelly.studyCourse(new JavaCourse());
            Gelly.studyCourse(new FECourse());
            Gelly.studyCourse(new PythonCourse());
        }
    }
    

    运行结果:

    Gelly在学习Java课程
    Gelly在学习FE课程
    Gelly在学习Python课程
    

    分析:测试类作为高层模块不依赖于 Gelly 这个低层模块,Gelly 作为高层模块不依赖于具体的课程实现类,Gelly 只依赖于 ICourse 接口。这样就遵循了依赖倒置原则,降低了类之间的耦合性,提高了可拓展性

最终类图如下所示:

在这里插入图片描述

4. 单一职责原则

  • 定义∶不要存在多于一个导致类变更的原因
  • 一个类/接口/方法只负责一项职责
  • 优点︰降低类的复杂度、提高类的可读性,提高系统的可维护性、降低变更引起的风险

单一职责原则 coding

下面将类拆分为单一职责:

  1. 创建一个 Bird 类

    public class Bird {
          
          
        public void mainMoveMode(String birdName) {
          
          
            System.out.println(birdName + "用翅膀飞");
        }
    }
    
  2. 测试类

    public class Test {
          
          
        public static void main(String[] args) {
          
          
            Bird bird = new Bird();
            bird.mainMoveMode("大雁");
            bird.mainMoveMode("鸵鸟");
        }
    }
    

    运行结果:

    大雁用翅膀飞
    鸵鸟用翅膀飞
    
  3. 但是鸵鸟不是用翅膀飞的,我们需要在原来 Bird 类里面进行扩展。

    public class Bird {
          
          
        public void mainMoveMode(String birdName) {
          
          
            if ("鸵鸟".equals(birdName)) {
          
          
                System.out.println(birdName + "用脚走");
            } else {
          
          
                System.out.println(birdName + "用翅膀飞");
            }
        }
    }
    

    运行结果:

    大雁用翅膀飞
    鸵鸟用脚走
    
  4. 但这样 Bird 类是不遵循单一职责原则的,我们按照职责的不同来进行拆分为 FlyBird 和 WalkBird

    public class FlyBird {
          
          
        public void mainMoveMode(String birdName) {
          
          
            System.out.println(birdName + "用脚走");
        }
    }
    
    public class WalkBird {
          
          
        public void mainMoveMode(String birdName) {
          
          
            System.out.println(birdName + "用翅膀飞");
        }
    }
    
    public class Test {
          
          
        public static void main(String[] args) {
          
          
            FlyBird flyBird = new FlyBird();
            WalkBird walkBird = new WalkBird();
    
            flyBird.mainMoveMode("大雁");
            walkBird.mainMoveMode("鸵鸟");
        }
    }
    

    运行结果:

    大雁用脚走
    鸵鸟用翅膀飞
    

下面将接口拆分为单一职责:

  1. 创建课程接口,里面含有两个大块的功能:一个是获取课程的相关的信息,一个是对课程进行管理

    public interface ICourse {
          
          
        /** 获取课程的相关的信息 */
        String getCourseName();
        byte[] getCourseVideo();
    
        /** 对课程进行管理 */
        void studyCourse();
        void refundCourse();
    }
    
  2. 我们对课程接口按照职责的不同来进行拆分为 ICourseContent 和 ICourseManage

    public interface ICourseContent {
          
          
        /**
         * 获取课程的相关的信息
         */
        String getCourseName();
        byte[] getCourseVideo();
    }
    
    public interface ICourseManage {
          
          
        /**
         * 对课程进行管理
         */
        void studyCourse();
        void refundCourse();
    }
    
  3. 写一个实现类,来实现上面的两个接口

    public class CourseImp implements ICourseContent, ICourseManage{
          
          
        @Override
        public String getCourseName() {
          
          
            return null;
        }
    
        @Override
        public byte[] getCourseVideo() {
          
          
            return new byte[0];
        }
    
        @Override
        public void studyCourse() {
          
          
    
        }
    
        @Override
        public void refundCourse() {
          
          
    
        }
    }
    

下面将方法拆分为单一职责:

  1. 创建一个用户类,里面有一个更新用户信息的方法

    public class User {
          
          
        private void updateUserInfo(String userName, String address) {
          
          
            userName = "Tom";
            address = "beijing";
        }
    }
    
  2. 我们对 updateUserInfo 按照职责的不同来进行拆分为 updateUserName 和 updateUserAddress

    public class User {
          
          
        private void updateUserName(String userName) {
          
          
            userName = "Tom";
        }
    
        private void updateUserAddress(String address) {
          
          
            address = "beijing";
        }
    }
    

注意:在实际的开发当中,遵循单一职责原则,要看实际的情况。不能过少的运用单一职责原则,也不能过多的使用单一职责原则,如果类过多的话,会引起来类的爆炸的现象。

5. 接口隔离原则

  • 定义:用多个专门的接口,而不使用单一的总接口
  • 客户端不应该依赖它不需要的接口
  • 一个类对一个类的依赖应该建立在最小的接口上建立单一接口,不要建立庞大臃肿的接口
  • 尽量细化接口,接口中的方法尽量少
  • 注意适度原则,一定要适度
  • 优点:符合我们常说的高内聚低耦合的设计思想,从而使得类具有很好的可读性、可扩展性和可维护性

接口隔离原则 coding

  1. 创建一个动物接口

    public interface IAnimalAction {
          
          
        void eat();
        void fly();
        void swim();
    }
    
  2. 创建一个狗类,实现动物接口。狗不会飞,所以 fly 方法是空的实现。

    public class Dog implements IAnimalAction {
          
          
        @Override
        public void eat() {
          
          
            System.out.println("dog eat...");
        }
    
        @Override
        public void fly() {
          
          
    
        }
    
        @Override
        public void swim() {
          
          
            System.out.println("dog swim...");
        }
    }
    
  3. 为了让狗类不实现他不需要的 fly 方法,我们可以对上面的接口来进行细化,拆分成 3 个接口

    public interface IEatAnimalAction {
          
          
        void eat();
    }
    
    public interface ISwimAnimalAction {
          
          
        void swim();
    }
    
    public interface IFlyAnimalAction {
          
          
        void fly();
    }
    
  4. 修改狗类,实现 IEatAnimalAction 和 IFlyAnimalAction

    public class Dog implements IEatAnimalAction, ISwimAnimalAction {
          
          
        @Override
        public void eat() {
          
          
            System.out.println("dog eat...");
        }
    
        @Override
        public void swim() {
          
          
            System.out.println("dog swim...");
        }
    }
    

注意:我们在设计接口的时候,也不能分的太细,让接口过多;接口隔离原则在使用的时候,一定要适度,用的过多,或者过少都是不好的。

6. 迪米特原则(最少知道原则)

  • 定义∶一个对象应该对其他对象保持最少的了解,又叫最少知道原则
  • 尽量降低类与类之间的耦合
  • 优点:降低类之间的耦合
  • 强调只和朋友交流,不和陌生人说话
  • 朋友∶出现在成员变量、方法的输入、输出参数中的类称为成员朋友类,而出现在方法体内部的类不属于朋友类。

迪米特原则 coding

  1. 创建课程类

    public class Course {
          
          
    }
    
  2. 创建项目经理类,他有个方法可以在线查询课程数量

    public class TeamLeader {
          
          
        public void checkNumberOfCourses(List<Course> courseList) {
          
          
            System.out.println("在线课程的数量是:" + courseList.size());
        }
    }
    
  3. 创建 Boss 类,他有个方法可以命令项目经理在线查询课程数量

    public class Boss {
          
          
        public void commandCheckNumber(TeamLeader teamLeader) {
          
          
            List<Course> courseList = new ArrayList<>();
            for (int i = 0; i < 20; i++) {
          
          
                courseList.add(new Course());
            }
            teamLeader.checkNumberOfCourses(courseList);
        }
    }
    
  4. 测试类

    public class Test {
          
          
        public static void main(String[]args){
          
          
            Boss boss = new Boss();
            TeamLeader teamLeader = new TeamLeader();
            boss.commandCheckNumber(teamLeader);
        }
    }
    

    运行结果:

    在线课程的数量是:20
    

    分析:对于 Boss 类中,只有出现在成员变量、方法的输入、输出参数中的类称为成员朋友类,其他的都不能称为朋友。在 commandCheckNumber 这个方法里面不应该和 Course 的这个类有任何的交互,这里就是违背了迪米特法则

现在类图如下所示:

在这里插入图片描述

下面进行修改使其符合迪米特原则

  1. 修改 Boss 类

    public class Boss {
          
          
        public void commandCheckNumber(TeamLeader teamLeader) {
          
          
            teamLeader.checkNumberOfCourses();
        }
    }
    
  2. 修改 TeamLeader 类

    public class TeamLeader {
          
          
        public void checkNumberOfCourses() {
          
          
            List<Course> courseList = new ArrayList<>();
            for (int i = 0; i < 20; i++) {
          
          
                courseList.add(new Course());
            }
            System.out.println("在线课程的数量是:" + courseList.size());
        }
    }
    

    运行结果:

    在线课程的数量是:20
    

最终类图如下所示:

在这里插入图片描述

7. 里氏替换原则

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

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

  • 引申意义∶子类可以扩展父类的功能,但不能改变父类原有的功能。

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

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

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

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

里氏替换原则 coding

  1. 在之前讲开闭原则的时候,在获取打折价格,我们是这样来写的

    public class JavaDiscountCourse extends JavaCourse {
          
          
        public JavaDiscountCourse(Integer id, String name, Double price) {
          
          
            super(id, name, price);
        }
    
        public Double getOriginPrice() {
          
          
            return super.getPrice();
        }
    
        @Override
        public Double getPrice() {
          
          
            return super.getPrice() * 0.8;
        }
    }
    
  2. 这里的 getPrice() 方法已经是重写了父类里面的非抽象方法,我们可以这样来做

    public class JavaDiscountCourse extends JavaCourse {
          
          
        public JavaDiscountCourse(Integer id, String name, Double price) {
          
          
            super(id, name, price);
        }
        
        public Double getDiscountPrice() {
          
          
            return super.getPrice() * 0.8;
        }
    }
    
  3. 测试类

    public class Test {
          
          
        public static void main(String[] args) {
          
          
            ICourse ICourse = new JavaDiscountCourse(96, "Java 从零开始到企业级开发", 348d);
            // 如果调用实现类里面的方法,我们就必须要进行强转一下
            JavaDiscountCourse javaCourse = (JavaDiscountCourse) ICourse;
            System.out.println("课程Id:" + javaCourse.getId() + " 课程名称:" + javaCourse.getName() + " 课程价格:" + javaCourse.getPrice() + "元" + " 打折后价格:" + javaCourse.getDiscountPrice() + "元");
        }
    }
    

    运行结果:

    课程Id:96 课程名称:Java 从零开始到企业级开发 课程价格:348.0元 打折后价格:278.40000000000003元
    

里氏替换原则:

  • 优点1∶约束继承泛滥,开闭原则的一种体现。
  • 优点2∶加强程序的健壮性,同时变更时也可以做到非常好的兼容性提高程序的维护性、扩展性。降低需求变更时引入的风险。

8. 合成复用原则

  • 定义∶尽量使用对象组合/聚合,而不是继承关系达到软件复用的目的
  • 聚合 has-A 和组合 contains-A
  • 优点:可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少
  • 何时使用合成/聚合、继承
  • 聚合 has-A、组合 contains-A、继承 is-A

合成复用原则 coding

  1. 创建连接数据库的类

    public class DBConnection {
          
          
        public String getConnection() {
          
          
            return "MySQL的数据库连接";
        }
    }
    
  2. Dao 层

    public class ProductDao extends DBConnection {
          
          
        public void addProduct() {
          
          
            String conn = super.getConnection();
            System.out.println("使用"+conn+"增加产品");
        }
    }
    
  3. 测试类

    public class Test {
          
          
        public static void main(String[]args){
          
          
            ProductDao productDao = new ProductDao();
            productDao.addProduct();
        }
    }
    

    运行结果:

    使用MySQL的数据库连接增加产品
    

现在需要再用 Oracle 的数据库来进行连接:

  1. 我们对之前的连接的类进行修改,改成了抽象类

    public abstract class DBConnection {
          
          
        public abstract String getConnection();
    }
    
  2. 具体获取什么连接交给实现它的子类

    public class MySQLConnection extends DBConnection {
          
          
        @Override
        public String getConnection() {
          
          
            return "MySQL数据库连接";
        }
    }
    
    public class OracleConnection extends DBConnection{
          
          
        @Override
        public String getConnection() {
          
          
            return "Oracle数据库连接";
        }
    }
    
  3. Dao 层

    public class ProductDao {
          
          
        private DBConnection dbConnection;
    
        public void setDbConnection(DBConnection dbConnection) {
          
          
            this.dbConnection = dbConnection;
        }
    
        public void addProduct() {
          
          
            String conn = dbConnection.getConnection();
            System.out.println("使用"+conn+"增加产品");
        }
    }
    
  4. 测试类

    public class Test {
          
          
        public static void main(String[]args){
          
          
            ProductDao productDao = new ProductDao();
            productDao.setDbConnection(new OracleConnection());
            productDao.addProduct();
        }
    }
    

    运行结果:

    使用Oracle数据库连接增加产品
    

猜你喜欢

转载自blog.csdn.net/bm1998/article/details/113002524