设计模式(零) - 前言

版权声明 :

本文为博主原创文章,如需转载,请注明出处(https://blog.csdn.net/F1004145107/article/details/94214662)

  • / 前言 /

           使用设计模式也大约有俩年了,对单个模式多个模式的组合运用都有一定的实操,本系列主要是想分享一下我对设计模式的理解,仅为个人观点,希望可以帮助大家多角度的去理解各个设计模式

    本系列内容会从三个方向出发

    1. 它是什么模式,怎么用

    2. 为什么要使用它

    3. 它的业务场景以及对应现实的具象(明白使用场景很重要,可以帮你更好的理解该设计模式并且去应用它,而不是学会了之后就一直放在脑海中了)

  • / 1 / 什么是设计模式

    设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。 使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样 -- 百度百科

    简单来说就是一套软件开发人员为了解决开发中普遍存在的问题,根据特有的规则来完善的一套解决方案,而这个解决方案可以使我们的系统具备高可用,可扩展,低耦合等特性

  • / 2 / 为什么要使用设计模式

           作为程序开发最让人崩溃的一点就是频繁的需求变更,今天plan A,明天Plan B,后天Plan C,如果我们在一开始就使用设计模式的话我们就可以在少量调整代码的情况下完成需求变更,但是如果俩个计划完全不相关的话,em......那你还是要大量的修改代码

           比如说我们今天需求是要增加一个发短信的功能,明天增加一个发邮件的,后天增加APP消息推送,而且一条消息可能要同时使用三种功能,那么这种前提下,我们最好先设计一个统一的接口,然后让这三种功能都去实现这个接口,方便我们的调用以及对功能的约束,不然三种功能三种接口调用起来可能会非常麻烦(eg:每个功能的形参列表都不同),也方便了以后增加新的发送消息的功能

          我们时常会在代码中发现,业务类S中可能依赖了10余个类来实现A功能,那10余个类中又依赖着其他的类,但是这个类中还有B功能,C功能,这些功能都不需要依赖那么多的类,并且如果我们需要修改A功能的某一块代码的话可能涉及到好多类,这绝对是让人非常崩溃的,我们可以把A功能中的代码尽可能的拿出来放到一个统一的接口中去,这样我们在修改A功能时就能尽可能避免去触碰到S类,我们可以在接口中进行修改

           以上是我们从较为突出的几个点来解释了为什么要使用设计模式,设计模式还有更多的好处,例如减少代码量,改变你的思维模式,以前可能我们只是想着怎么写业务代码,而现在我们在写之前要想,这个系统应该怎么设计才能尽可能应付未来的改动,我们无法预知'未来',但是我们要尽力做到当'未来'到来的时候我们并不会手忙脚乱

           but : 思想不要被设计模式所局限,不是所有的业务场景都有合适的设计模式来对应,也不是所有的场景都只能使用唯一的设计模式,这就是为什么我们在讲解设计模式的时候一定要说清楚我们为什么要用这个设计模式,它能给我们带来的好处,以及对应的使用场景,方便我们更好的去使用

    • 高可用

    • 可扩展

    • 低耦合

  • / 3 / 设计模式6大原则

    • 在讲述6大原则之前呢,我们先需要明白一个事情,我们不是为了实现这一原则而去实现,我们遵循该原则一定是因为该原则可以方便我们的开发

    • 1 ) 单一职责原则

      这个原则其实凡是工作过一段时间的程序员基本都会去自动遵守这一原则,因为大家都明白,当你将大量的职责放到一起时,但凡其中一个发生了改变,那就是灾难,如下图

      我们举一个简单的例子

      /**
       * @author wise
       */
      public class User {
          
          //姓名
          private String name;
          //性别
          private Integer sex;
          //身份证号
          private String idCard;
          //银行卡号
          private String bankCard;
      ​
          //余额(分)
          private Integer balance;
          //消费额(分)
          private Integer consumption;
      }

             例如如上一个User类,里面存有用户的基本信息以及账户信息,消费时我们需要修改User表中的余额及消费额的数据,修改个人信息时我们也需要修改User类,从程序的角度我们可以看出来,存储个人信息和存储账户信息并不属于同一功能,最合适的方式就是将其变成如下

      /**
       * @author wise
       */
      public class User {
          //姓名
          private String name;
          //性别
          private Integer sex;
          //身份证号
          private String idCard;
          //银行卡号
          private String bankCard;
      }
      ​
      class UserAccount {
          //余额(分)
          private Integer balance;
          //消费额(分)
          private Integer consumption;
      }

             but : 单一原则是一个看着简单实则比较复杂的原则,因为它的界限太过于模糊,我们只能根据实际场景来判断.比如在User表中的信息我们还可以细分为用户普通信息,用户私密信息,可能你感觉不需要区分那么详细吧,但是一旦我的私密信息和普通信息占用的字段各自都有20多个呢,那我们最好还是进行区分

             所以如果你可以根据现有的业务看到未来的业务场景,那么在设计的时候一定要注意了,需不需要再详细的划分是每一个程序设计者的烦恼

      • 一个类只负责单一的功能

    • 2 ) 里式替换原则

             这个原则确实有点绕,简单来说就是我创建了父类的变量,此时我即可以将父类的对象赋给父类的变量也可赋值子类的对象,但是我最终的运行结果不能发生改变,子类不能改变父类已有的逻辑

             注意,这里的行为就是指父类是1 + 1 = 2,子类不能改成1 - 1 = 0,而且子类是不能改变父类已有的逻辑,但是不代表是绝对不能覆写父类的方法

      //父类变量 : obj
      Parent obj = new Parent();
      obj = new Children();

             讲到里式替换原则就不得不提到多态了,对于从事Java语言的朋友来说,很容易搞混,从里式替换原则的定义上来看是不允许子类改变父类逻辑,而多态是要求子类必须覆写父类的方法,所以有了我上面说的不是不能覆写,只要你保证在覆写之后逻辑还一样不也可以吗

             而且还有一种可能性,如果父类是抽象类或者接口呢,这个方法没有方法体的话子类覆写了方法(子类自己的逻辑),那这样算不算遵循里式替换原则呢,这个问题网上讨论了很久了,仁者见仁智者见智吧

      我们看一个典型的里式替换原则的例子

      我们在Call引用类中引用了Parent的对象,那么我们在调用时就必须可以使用子类对象进行替换,并且保证程序正常运行

      //父类
      public class Parent {
          public void say () {
              System.out.println("我是父类,我正在运行父类的程序.......");
          }
      }
      ​
      //子类
      class Children extends Parent{}
      ​
      //引用类
      class Call {
          public void call (Parent parent) {
              parent.say();
          }
      }
      ​
      public class Client {
          public static void main(String[] args) {
              Call call = new Call();
              call.call(new Parent());
              call.call(new Children());
          }
      }

      打印结果

      我是父类,我正在运行父类的程序.......

      我是父类,我正在运行父类的程序.......

      • 定义 : 如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1代换o2时,程序P的行为没有变化,那么类型S是类型T的子类型

      • 我自己的理解就是所有引用父类的地方可以使用子类对象进行替换,且子类不会改变父类原有方法的运行逻辑

  • 3 ) 依赖倒置原则

    • 定义 : 高层模块不应该依赖底层模块,二者都应该依赖于抽象,抽象不应该依赖于细节,细节应该依赖于抽象

    • 我自己的理解就是所有的实现都应该依赖于抽象(接口、抽象类),也就是都被抽象所约束,这样我们在实现时不会影响到客户端的调用,也就是客户端只调用抽象,而具体的逻辑走的是具体的实现

             就拿上面里式替换原则代码来说,我们可以给Parent类增加一个IBase接口,让父类去实现,这样我们在调用的时候只需要调用IBase的say()方法,具体里面是怎么实现的交给底层

      public interface IBase {
          void say ();
      }
      ​
      public class Parent implements IBase{
          @Override
          public void say () {
              System.out.println("我是父类,我正在运行父类的程序.......");
          }
      }
      ​
      public class Client {
          public static void main(String[] args) {
              IBase obj = new Parent();
              obj.say();
          }
      }

      其实这个在我们的编程中是大量使用到的,比如面向接口编程,MybatisPlus的BaseMapper、IService等等

  • 4 ) 迪米特原则

    • 也称最小知道原则,即将属于自己的操作全部封装到自己的类中,不要给别的类来操作

             如果俩个类之间互相依赖对方的细节,这俩个类之间的耦合度就会非常的高,当你需要修改一个类的细节就需要俩个类一起修改

      我们要尽量做到高内聚,将属于自己的细节完全隐藏在自己的类中,只给别人现成的直接调用即可

      例如我们需要做一个配置类,我们不需要让别的类来设置这些配置的属性值,我们只需要给他配置好的即可

      import lombok.Getter;
      @Getter
      public class DBConfig {
          public DBConfig () {
              this.username = "root";
              this.password = "admin";
              this.dbType = "MySql";
              this.cacheEnabled = true;
          }
      ​
          private String username;
      ​
          private String password;
      ​
          private String dbType;
      ​
          private boolean cacheEnabled;
      }

             这样如果对方需要配置类中的信息,只需要创建配置表对象,调用getter()方法即可,即使我们后续需要修改配置表信息只需要在DBConfig的构造器中修改即可

  • 5 ) 接口隔离原则

    • 也称最小化原则,我们需要将接口设计的尽可能的小(方法尽可能的少),避免我们的实现类只需要几个方法,大多数的方法被空置

             比如JDK中的Lock接口,里面只有锁所必需的5个方法,它的子类如果需要一些特定的方法如ReentrantLockisLocked()方法,可以自己去定制

      举个简单的例子

      /**
       * @author wise
       */
      public interface Human {
          //工作
          void work();
      ​
          //学习
          void study();
      ​
          //演出
          void perform();
      }

             上面代码中三个功能不是所有人都必须要有的,上班族和学生可能都需要工作和学习的功能,但是大多数人是不需要演出功能的,那是属于演员等特定职业的功能

             但是接口定义了这个功能,其所有实现类就必须无条件去拥有该功能,这个是不公平的,解决办法就是将Human接口中的演出功能扩展到另一个接口中去

      public interface Human {
      ​
          //工作
          void work();
      ​
          //学习
          void study();
      }
      ​
      public interface Performer extends Human{
      ​
          //演出
          void perform();
      }

      我们让所有的演员全部去实现Performer这个接口就好,这样既能拥有演员独有的特性,也能拥有人类共有的特性

  • 6 ) 开闭原则

    • 对扩展开放,对修改关闭

             我们在大多数开发中,即使你使用的设计模式已经完美的符合了现有的业务场景,但是任然有些时候避免不了当需求改动时我们还是需要修改代码,而开闭原则则是希望我们在不修改任何代码的前提下对系统进行扩展

             当我们站在需求的角度上来看,有的需求我们可以完美实现开闭原则,有的不可以,但是站在程序的角度来看,我们是无法完美实现开闭原则的,就像当前系统的代码逻辑不可能适用于所有的需求一样

  • / 4 / 结语

           所有的设计模式都是围绕着这六大原则来进行设计,其实对于很多朋友来说,六大原则中很多原则即使不知道但日常也在使用,例如单一原则,迪米特原则等等,正是因为遵循这些原则可以简化我们的开发所以我们在不知不觉中已经在遵守了

           说这些只是希望大家能够深入的理解设计模式,理解设计模式背后的东西,在每一个场景中每个设计模式的具体表现都是不同的,我们将其互相组合并对其进行篡改为的就是我们能够享受设计模式带来的便利

    我们不是为了实现这一原则而去实现,我们遵循该原则一定是因为该原则可以方便我们的开发

原创文章 42 获赞 51 访问量 1万+

猜你喜欢

转载自blog.csdn.net/F1004145107/article/details/94214662