• 设计模式 00 设计原则


    参考源

    https://www.bilibili.com/video/BV1u3411P7Na?spm_id_from=333.999.0.0&vd_source=299f4bc123b19e7d6f66fefd8f124a03


    面向对象设计原则

    在进行软件开发时,不仅需要将基本的业务完成,还要考虑整个项目的可维护性可复用性

    因此在编写代码时,应该尽可能的规范,不然随着项目的不断扩大,整体结构只会越来越遭。

    为了避免这种情况的发生,应该尽量遵守面向对象设计原则

    设计原则

    单一职责原则

    单一职责原则(Simple Responsibility Pinciple,SRP)是最简单的面向对象设计原则,它用于控制类的粒度大小。

    一个对象应该只包含单一的职责,并且该职责被完整地封装在一个类中。

    // 人类
    public class People {
    
        // 写代码
        public void coding(){
            
        }
    
        // 打螺丝
        public void work(){
            
        }
    
        // 送外卖
        public void ride(){
            
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    可以看到,这个 People 类可以说是十八般武艺样样精通了,啥都会。

    但是实际上,每个人最终都是在自己所擅长的领域工作,正所谓术业有专攻

    会写代码的应该是程序员,会打螺丝的应该是工人,会送外卖的应该是骑手。

    显然这个 People 类太过臃肿(修改任意一种行为都需要修改 People 类,它拥有不止一个引起它变化的原因)。

    根据单一职责原则,我们需要进行更明确的划分,同种类型的操作才放在一起:

    // 程序员
    class Coder{
        
        // 编程
        public void coding(){
            
        }
        
    }
    
    // 工人
    class Worker{
        
        // 打螺丝
        public void work(){
            
        }
        
    }
    
    // 骑手
    class Rider {
        
        // 送外卖
        public void ride(){
            
        }
        
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    我们将类的粒度进行更近一步的划分,这样就很清晰了。

    在设计 Mapper、Service、Controller 等都应该采用单一职责原则根据不同的业务划分,作为实现高内聚低耦合的指导方针。

    实际上微服务也是参考了单一职责原则,每个微服务只应担负一个职责。

    开闭原则

    开闭原则(Open Close Principle)也是重要的面向对象设计原则。

    软件实体应当对扩展开放,对修改关闭。

    一个软件实体,比如类、模块和函数应该对扩展开放,对修改关闭。

    其中,对扩展开放是针对提供方来说的,对修改关闭是针对调用方来说的。

    比如程序员分为前端程序员、后端程序员,他们要做的都是去打代码,具体如何打代码根据不同语言的程序员决定。

    将程序员打代码的行为抽象成统一的接口或抽象类,就满足了开闭原则的第一个要求:对扩展开放

    哪个程序员使用什么语言怎么编程,是自己在负责,不需要其他程序员干涉,就满足第二个要求:对修改关闭

    // 程序员
    public abstract class Coder {
    
        public abstract void coding();
    
        // Java程序员
        class JavaCoder extends Coder{
            @Override
            public void coding() {
                
            }
        }
        
        // Python程序员
        class PythonCoder extends Coder{
            @Override
            public void coding() {
                
            }
        }
    
        // PHP程序员
        class PHPCoder extends Coder{
            @Override
            public void coding() {
                
            }
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    通过提供一个 Coder 抽象类,定义出编程的行为,但是不进行实现,而是开放给其他具体类型的程序员来实现。

    这样就可以根据不同的业务进行灵活扩展了,具有较好的延续性。

    里氏替换原则

    里氏替换原则(Liskov Substitution Principle)是对子类型的特别定义。

    它由芭芭拉·利斯科夫(Barbara Liskov)在 1987 年在一次会议上名为 “数据的抽象与层次” 的演说中首先提出。

    所有引用基类的地方必须能透明地使用其子类的对象。

    简单的说就是,子类可以扩展父类的功能,但不能改变父类原有的功能:

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

    • 子类可以增加自己特有的方法。

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

    • 子类的方法实现父类的方法时(重写/重载/实现抽象方法),方法的后置条件(方法的输出/返回值)要比父类更严格或与父类一样。

    // 程序员
    public abstract class Coder {
    
        // 写代码
        public void coding() {
            
        }
    
    	// Java程序员
        class JavaCoder extends Coder{
    
            // 打游戏
            public void game(){
                
            }
            
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    可以看到 JavaCoder 虽然继承自 Coder,但是并没有对父类方法进行重写,并是在父类的基础上进行额外扩展,符合里氏替换原则。

    // 程序员
    public abstract class Coder {
    
        // 写代码
        public void coding() {
            
        }
    
    	// Java程序员
        class JavaCoder extends Coder{
            
            // 打游戏
            public void game(){
                
            }
    
            // 写代码
            @Override
            public void coding() {
                
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    这里对父类的方法进行了重写,父类的行为就被子类覆盖了,这个子类已经不具备父类的原本的行为,违背了里氏替换原则。

    对于这种情况,我们不需要再继承自 Coder 了,可以提升一下,将此行为定义到 People 中:

    // 人类
    public abstract class People {
    
        // 写代码。这个行为还是定义出来,但是不实现
        public abstract void coding();   
        
        // 程序员
        class Coder extends People{
            
            // 写代码
            @Override
            public void coding() {
                
            }
        }
    
    	// Java程序员
        class JavaCoder extends People{
            
            // 打游戏
            public void game(){
                
            }
    
            // 写代码
            @Override
            public void coding() {
                
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    里氏替换也是实现开闭原则的重要方式

    依赖倒转原则

    依赖倒转原则(Dependence Inversion Principle)也是我们一直在使用的,最明显的就是 Spring 框架了。

    高层模块不应依赖于底层模块,它们都应该依赖抽象。抽象不应依赖于细节,细节应该依赖于抽象。

    回顾一下在使用 Spring 框架之前的情况:

    public class UserController {
    
        UserService service = new UserService();
    	// 调用服务
        
    
        static class UserService {
            UserMapper mapper = new UserMapper();
            // 业务代码......
        }
    
        static class UserMapper {
            // CRUD......
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    突然有一天,公司业务需求变化,现在用户相关的业务操作需要使用新的实现:

    public class UserController {
    
        UserServiceNew service = new UserServiceNew();
    	// 调用服务
        
    	// 服务发生变化,新的方法在新的服务类中
        static class UserServiceNew {
            UserMapper mapper = new UserMapper();
            // 业务代码......
        }
    
        static class UserMapper {
            // CRUD......
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    各个模块之间是强关联的,一个模块是直接指定依赖于另一个模块。

    虽然这样结构清晰,但是底层模块的变动,会直接影响到其他依赖于它的高层模块。

    如果项目很庞大,这样的修改将是一场灾难。

    而有了 Spring 框架之后,我们的开发模式就发生了变化:

    public class Main {
    
        public static void main(String[] args) {
            UserController controller = new UserController();
        }
    
        interface UserMapper {
            // 接口中只做 CRUD 方法定义
        }
    
        static class UserMapperImpl implements UserMapper {
            // 实现类完成 CRUD 具体实现
        }
    
        interface UserService {
            // 业务接口定义......
        }
    
        static class UserServiceImpl implements UserService {
            // 现在由Spring来为我们选择一个指定的实现类,然后注入,而不是由我们在类中硬编码进行指定
            @Resource   
            UserMapper mapper;
            
            // 业务代码实现......
        }
    
        static class UserController {
            // 直接使用接口,就算你改实现,我也不需要再修改代码了
            @Resource
            UserService service;   
    
            // 业务代码......
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    通过使用接口,将原有的强关联给弱化,只需要知道接口中定义了什么方法然后去使用即可。

    而具体的操作由接口的实现类来完成,并由 Spring 来为我们注入,而不是我们通过硬编码的方式去指定。

    接口隔离原则

    接口隔离原则(Interface Segregation Principle, ISP)实际上是对接口的细化。

    客户端不应依赖那些它不需要的接口。

    我们在定义接口的时候,一定要注意控制接口的粒度,比如下面的例子:

    // 电子设备
    interface Device {
        // 获取 CPU 信息
        String getCpu();
        
        // 获取类型
        String getType();
        
        // 获取内存
        String getMemory();
    }
    
    // 电脑是一种电子设备,那么就实现此接口
    class Computer implements Device {
    
        @Override
        public String getCpu() {
            return "i9-12900K";
        }
    
        @Override
        public String getType() {
            return "电脑";
        }
    
        @Override
        public String getMemory() {
            return "32G DDR5";
        }
    }
    
    // 电风扇也算是一种电子设备
    class Fan implements Device {
    
        @Override
        public String getCpu() {
            // 风扇没有 CPU
            return null;   
        }
    
        @Override
        public String getType() {
            return "风扇";
        }
    
        @Override
        public String getMemory() {
            // 风扇没有内存
            return null;   
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    虽然定义了一个 Device 接口,但是由于此接口的粒度不够细,虽然比较契合电脑这种设备,但是不适合风扇这种设备。

    因为风扇压根就不需要 CPU 和内存,所以风扇完全不需要这些方法。

    这时我们就必须要对其进行更细粒度的划分:

    // 智能设备
    interface SmartDevice {
        // 获取 CPU 信息
        String getCpu();
        
        // 获取类型
        String getType();
        
        // 获取内存
        String getMemory();
    }
    
    // 智能设备
    interface NormalDevice {
        // 获取类型
        String getType();
    }
    
    // 电脑是一种智能设备,继承智能设备接口
    class Computer implements SmartDevice {
    
        @Override
        public String getCpu() {
            return "i9-12900K";
        }
    
        @Override
        public String getType() {
            return "电脑";
        }
    
        @Override
        public String getMemory() {
            return "32G DDR5";
        }
    }
    
    // 电风扇是一种普通设备,继承普通设备接口
    class Fan implements NormalDevice {
        @Override
        public String getType() {
            return "风扇";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    这样,就将接口进行了细粒度的划分,不同类型的电子设备根据划分去实现不同的接口。

    当然,也不能划分得太小,还是要根据实际情况来进行决定。

    合成复用原则

    合成复用原则(Composite Reuse Principle)的核心就是委派

    优先使用对象组合,而不是通过继承来达到复用的目的。

    在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分,新的对象通过向这些对象的委派达到复用已有功能的目的。

    在考虑将某个类通过继承关系在子类得到父类已经实现的方法时,应该先考虑使用合成的方式来实现复用。

    比如下面这个例子:

    class A {
        public void connectDatabase(){
            System.out.println("我是连接数据库操作!");
        }
    }
    
    // 直接通过继承的方式,得到 A 的数据库连接逻辑
    class B extends A{    
        public void test(){
            System.out.println("我是B的方法,我也需要连接数据库!");
            // 直接调用父类方法
            connectDatabase();   
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    这样看起来没啥毛病,但还是存在之前说的问题,耦合度太高了。

    通过继承的方式实现复用,是将类 B 直接指定继承自类 A 的。

    如果有一天,由于业务的更改,数据库连接操作不再由A来负责,而是由C去负责。

    就不得不将需要复用 A 中方法的子类全部进行修改,这样是费时费力的。

    并且还有一个问题,通过继承子类会得到一些父类中的实现细节,比如某些字段或是方法,这样直接暴露给子类,并不安全。

    所以,当需要实现复用时,可以优先考虑以下操作:

    class A {
        public void connectDatabase(){
            System.out.println("我是连接数据库操作!");
        }
    }
    
    // 不进行继承,而是在用的时候给我一个 A,当然也可以抽象成一个接口,更加灵活
    class B {   
        public void test(A a){
            System.out.println("我是B的方法,我也需要连接数据库!");
            // 通过对象 A 去执行
            a.connectDatabase();   
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    或是:

    class A {
        public void connectDatabase(){
            System.out.println("我是连接数据库操作!");
        }
    }
    
    class B {
        A a;
        // 在构造时就指定好
        public B(A a){   
            this.a = a;
        }
        
        public void test(){
            System.out.println("我是B的方法,我也需要连接数据库!");
            // 通过对象 A 去执行
            a.connectDatabase();   
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    通过对象之间的组合,我们就大大降低了类之间的耦合度,并且 A 的实现细节也不会直接得到了。

    迪米特法则

    迪米特法则(Law of Demeter)又称最少知识原则,是对程序内部数据交互的限制。

    每一个软件单位对其他单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。

    一个类/模块对其他的类/模块有越少的交互越好。

    当一个类发生改动,与其相关的类需要尽可能少的受影响。

    这样我们在维护项目的时候会更加轻松一些。

    其实本质还是降低耦合度。

    public class Main {
        public static void main(String[] args) throws IOException {
            // 假设我们当前的程序需要进行网络通信
            Socket socket = new Socket("localhost", 8080);   
            Test test = new Test();
            // 现在需要执行 test 方法来做一些事情
            test.test(socket);   
        }
    
        static class Test {
            // 比如 test 方法需要得到我们当前 Socket 连接的本地地址
            public void test(Socket socket){
                System.out.println("IP地址:" + socket.getLocalAddress());
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    虽然这种写法没有问题,直接提供一个 Socket 对象供使用,然后再由 test 方法来取出 IP 地址。

    但是这样显然违背了迪米特法则,实际上这里的 test 方法只需要一个 IP 地址即可。

    完全可以只传入一个字符串,而不是整个 Socket 对象,这样就保证了与其他类的交互尽可能的少。

    就像在餐厅吃完了饭,应该是自己扫码付款,而不是直接把手机交给老板来帮你操作付款。

    要是某一天,Socket 类中的这些方法发生修改了,那我们就得连带着去修改这些类,很麻烦。

    所以,来进行改进:

    public class Main {
        public static void main(String[] args) throws IOException {
            Socket socket = new Socket("localhost", 8080);
            Test test = new Test();
            // 在外面解析好再传入
            test.test(socket.getLocalAddress().getHostAddress());  
        }
    
        static class Test {
            // 一个字符串就搞定了
            public void test(String str){   
                System.out.println("IP地址:"+str);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    这样,类与类之间的耦合度再次降低。

    设计模式

    由以上设计原则,可以总结出一些实用的设计模式

    设计模式(design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。

    使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。

    设计模式对自己对他人对系统都是多赢的,设计模式使代码编制真正工程化。

    项目中合理的运用设计模式可以完美的解决很多问题,每种模式在现在中都有相应的原理来与之对应。

    每一个模式描述了一个在周围不断重复发生的问题,以及该问题的核心解决方案,这也是它能被广泛应用的原因。

    设计模式可以分为三个大类:创建型、结构型、行为型

    创建型模式:对象实例化的模式,创建型模式用于解耦对象的实例化过程。

    结构型模式:把类或对象结合在一起形成一个更大的结构。

    行为型模式:类和对象如何交互,及划分责任和算法。

    img

    以上 23 种设计模式是在 1994 年,由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 四人合著出版的一本名为
    Design Patterns - Elements of Reusable Object-Oriented Software(设计模式 - 可复用的面向对象软件元素) 的书中首次提出,并一直沿用至今,没有新增,也没有删改,可见其经典。

    四位作者合称 GOF(四人帮,全拼 Gang of Four)

  • 相关阅读:
    java毕业设计艾灸减肥管理网站Mybatis+系统+数据库+调试部署
    Vue props实现父组件给子组件传递数据
    C高级day2
    Mysql和Oracle的语法区别?
    Java之方法
    过拟合学习理解
    ChatGPT研究报告:AIGC带来新一轮范式转移
    echarts:通过自定义工具栏来实现一些功能
    fpga_图像处理
    USB协议层数据格式
  • 原文地址:https://blog.csdn.net/qq_37770674/article/details/125636098