设计模式-六大设计原则

设计模式是系统服务设计中针对常⻅场景的⼀种解决⽅案;是一种开发设计的指导思想,每⼀种设计模式都是解 决某⼀类问题的概念模型;设计模式并不局限于最终的实现⽅案,⽽是在这种概念模型下,解决系统设计中的代码逻辑问题。

PS:博客根据it老齐大话设计模式课程课件进行整理,IT老齐 视频学习网站: https://www.itlaoqi.com

设计模式共有23种,按⽬的分类可以分为三类:

创建型模式:提供创建对象的机制,提升已有代码的灵活性和 可复⽤性。

  ⼯⼚⽅法模式、抽象⼯⼚模式、单例模式、建造者模式、原型模式。

结构型模式:介绍如何将对象和类组装成较⼤的结构,并同时 保持结构的灵活和⾼效。

  适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、 享元模式。

⾏为模式:负责对象间的⾼效沟通和职责传递委派。

  策略模式、模板⽅法模式、观察者模式、迭代⼦模式、责任链模式、命令 模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

六大设计原则:

  开闭原则,单一职责原则,里式替换原则,迪米特法则,接口隔离原则,依赖倒置原则

开闭原则

  开闭原则规定软件中的对象、类、模块和函 数对扩展应该是开放的,但对于修改是封闭的

代码场景:

  编写数据库链接程序,将原链接方式为jdbc,改为jndi

接口类

public interface UserDAO {
  public void insert();
}

jdbc实现类

public interface UserDAOImpl implements UserDAO{
  public void insert(){
    //基于JDBC实现数据插⼊
    pstmt.executeUpdate()
  }
}

错误代码

public interface UserDAOImpl implements UserDAO{
  public void insert(){
    //pstmt.executeUpdate() 这里在需求发生变化时,将原代码注释掉,改为了jndi的方式,这样操作违背了开闭原则
    jndi.insert();
  }
}

注意:以上代码,虽然实现了jdbc到jndi的转换,但是却改变了jdbc的代码,如果后续要还原jdbc的代码,又需要将jndi的代码注释或删除掉,重新编写jdbc的代码;而且也不一定清楚这样的变动会不会造成其他地方的调用出现问题;

正确代码:

  新建jndi的操作类 UserDAOJndiImpl 并实现UserDAO 接口类,重写jndi的insert方法

public interface UserDAOJndiImpl implements UserDAO{
 public void insert(){
  //基于JNDI实现数据插⼊
  jndi.insert();
 }
}

分析:以上代码通过修改之后,在拥有jndi链接方式的同时还拥有jdbc的链接方式,虽然代码量增加,但是这样对jdbc的代码不会进行修改,后续如果存在新的需求,只需再创建一个对于的class类,就可以在不修改原有代码的基础上实现功能的拓展

总结:开闭原则的核⼼思想也可以理解为⾯向抽象编程,在尽可能不修改原始代码的基础上,完成程序的拓展(将上层接口提出来形成一个接口类,用实现类的方式完成程序的设计)

 

单一职责原则

  ⼜称单⼀功能原则,单个class类的功能尽量有针对性,功能明确,不要形成大锅烩(将很多功能都放到一个class类或一个方法中);

代码场景:

  视频网站会员程序,游客可以看480P视频,并需要查看广告;会员可以查看高清视频,可以关闭广告;

错误代码:将所有的情况都放到一个方法中完成

复制代码
public class VideoUserService {
 public void serveGrade(String userType){
   if ("会员".equals(userType)){
     System.out.println("会员,视频高清,可关闭广告");
   } else if ("游客".equals(userType)){
     System.out.println("游客,视频480P,不可关闭广告");
   }
 }
}
复制代码

注意:以上代码,虽然将游客和会员的功能实现,但是代码过于杂乱,而且没有共性,如果需要添加更多会员类型,就会无限制的if else下去,降低程序可阅读能力,随着后期业务拓展,可维护性也降低很多;

正确代码:

  将游客和会员的功能提出来,编写为接口类,通过各自的实现,来完成游客和会员之间的独立;

接口类

public interface IVideoUserService {
 // 视频清晰级别;480P、720P、1080P
 void definition();
 // ⼴告播放⽅式;⽆⼴告、有⼴告
 void advertisement();
}

游客类

public class GuestVideoUserService implements IVideoUserService {
 public void definition() {
   System.out.println("游客,视频480P⾼清");
 }
 public void advertisement() {
   System.out.println("游客,视频有⼴告");
 }
}

会员类

public class VipVideoUserService implements IVideoUserService {
 public void definition() {
   System.out.println("会员,视频高清");
 }
 public void advertisement() {
   System.out.println("会员,视频⽆⼴告");
 }
}

分析:代码修改后,将原本在一个方法中实现的功能,拆分为功能明确,职责单一的class类,这样的修改虽然增加了代码量,但是能让代码变得更加简单易懂,并且随着后期会员类型的增加,原有代码的修改也变得简单,只需创建一个对于的会员代码类即可,这样也减少了因不拆分而带来的持续扩张问题,也减少了后期维护上的麻烦

总结:单一职责原则核心思想在于单一两字,将大锅烩的代码进行职责拆分,使得拆分出来的代码,针对性强,功能明确

 

里式替换原则

  继承必须确保超类所拥有的性质在⼦类中仍然成⽴(当⼦类继承⽗类时,除添加新的⽅法且完成新增功能外,尽量不要重写⽗类的⽅法)

目的:使⽤约定的⽅式,让使⽤继承后的代码具备良好 的扩展性和兼容性。

作用

  1、⾥⽒替换原则是实现开闭原则的重要⽅式之⼀。

  2、解决了继承中重写⽗类造成的可复⽤性变差的问题。

  3、是动作正确性的保证,即类的扩展不会给已有的系统引⼊新的错误, 降低了代码出错的可能性

  4、加强程序的健壮性,同时变更时可以做到⾮常好的兼容性,提⾼程序 的维护性、可扩展性,降低需求变更时引⼊的⻛险。

注意事项:

  1、⼦类可以实现⽗类的抽象⽅法,但不能覆盖⽗类的⾮抽象⽅法。

  2、⼦类可以增加⾃⼰特有的⽅法。

  3、当⼦类的⽅法重载⽗类的⽅法时,⽅法的前置条件(即⽅法的输⼊参 数)要⽐⽗类的⽅法更宽松。

  4、当⼦类的⽅法实现⽗类的⽅法(重写、重载或实现抽象⽅法)时,⽅ 法的后置条件(即⽅法的输出或返回值)要⽐⽗类的⽅法更严格或与 ⽗类的⽅法相等。

 

迪米特法则

  ⼜称为最少知道原则,是指⼀个对象类对于其他对象类来说,知道得越少越好,两个类之间不要有过多的耦合关 系,保持最少关联性。

代码场景:

  校长管理老师,老师管理学生,老师需要负责每个学生的学习情况,校长只会关心老师所在班级的总体成绩;

学生类

public class Student {
  private String name; // 学⽣姓名
  private int rank; // 考试排名(总排名)
  private double grade; // 考试分数(总分)
}

错误代码

老师类

复制代码
public class Teacher {
  private String name; // ⽼师名称
  private String clazz; // 班级
  private static List<Student> studentList; // 学⽣
  public Teacher() {
  }
  public Teacher(String name, String clazz) {
    this.name = name;
    this.clazz = clazz;
  }
  static {
    studentList = new ArrayList<>();
    studentList.add(new Student("花花", 10, 589));
    studentList.add(new Student("⾖⾖", 54, 356));
    studentList.add(new Student("秋雅", 23, 439));
    studentList.add(new Student("⽪⽪", 2, 665));
    studentList.add(new Student("蛋蛋", 19, 502));
 }
}
复制代码

校长类

复制代码
public class Principal {
 private Teacher teacher = new Teacher("丽华", "3年1班");
 // 查询班级信息,总分数、学⽣⼈数、平均值
 public Map<String, Object> queryClazzInfo(String clazzId) {
   // 获取班级信息;学⽣总⼈数、总分、平均分
   int stuCount = clazzStudentCount();
   double totalScore = clazzTotalScore();
   double averageScore = clazzAverageScore();
   // 组装对象,实际业务开发会有对应的类
   Map<String, Object> mapObj = new HashMap<>();
   mapObj.put("班级", teacher.getClazz());
   mapObj.put("⽼师", teacher.getName());
   mapObj.put("学⽣⼈数", stuCount);
   mapObj.put("班级总分数", totalScore);
   mapObj.put("班级平均分", averageScore);
   return mapObj;
 }
 // 总分
 public double clazzTotalScore() {
   double totalScore = 0;
   for (Student stu : teacher.getStudentList()) {
     totalScore += stu.getGrade();
   }
   return totalScore;
 }
 // 平均分
 public double clazzAverageScore(){
   double totalScore = 0;
   for (Student stu : teacher.getStudentList()) {
     totalScore += stu.getGrade();
   }
   return totalScore / teacher.getStudentList().size();
 }
 // 班级⼈数
 public int clazzStudentCount(){
   return teacher.getStudentList().size();
 }
}
复制代码

注意:以上就是通过校⻓管理所有学⽣,⽼师只提供了⾮常简单的信息。虽然 可以查询到结果,但是违背了迪⽶特法则,因为校⻓需要了解每个学⽣ 的情况。如果所有班级都让校⻓类统计,代码就会变得⾮常臃肿,也不易于维护和扩展;

正确代码:

  由⽼师负责分数统计,校长只需要调用老师提供的方法,获取统计好的分数信息即可,并不需要获取具体学⽣信息

老师类

复制代码
public class Teacher {
  private String name; // ⽼师名称
  private String clazz; // 班级
  private static List<Student> studentList; // 学⽣
  public Teacher() {
  }
  public Teacher(String name, String clazz) {
    this.name = name;
    this.clazz = clazz;
  }
  static {
    studentList = new ArrayList<>();
    studentList.add(new Student("花花", 10, 589));
    studentList.add(new Student("⾖⾖", 54, 356));
    studentList.add(new Student("秋雅", 23, 439));
    studentList.add(new Student("⽪⽪", 2, 665));
    studentList.add(new Student("蛋蛋", 19, 502));
  }
  // 总分
  public double clazzTotalScore() {
    double totalScore = 0;
    for (Student stu : studentList) {
      totalScore += stu.getGrade();
    }
    return totalScore;
  }
  // 平均分
  public double clazzAverageScore(){
    double totalScore = 0;
    for (Student stu : studentList) {
      totalScore += stu.getGrade();
    }
   return totalScore / studentList.size();
  }
  // 班级⼈数
  public int clazzStudentCount(){
   return studentList.size();
  }
}
复制代码

校长类

复制代码
public class Principal {
  private Teacher teacher = new Teacher("丽华", "3年1班");
  // 查询班级信息,总分数、学⽣⼈数、平均值
  public Map<String, Object> queryClazzInfo(String clazzId) {
    // 获取班级信息;学⽣总⼈数、总分、平均分
    int stuCount = teacher.clazzStudentCount();
    double totalScore = teacher.clazzTotalScore();
    double averageScore = teacher.clazzAverageScore();
    // 组装对象,实际业务开发会有对应的类
    Map<String, Object> mapObj = new HashMap<>();
    mapObj.put("班级", teacher.getClazz());
    mapObj.put("⽼师", teacher.getName());
    mapObj.put("学⽣⼈数", stuCount);
    mapObj.put("班级总分数", totalScore);
    mapObj.put("班级平均分", averageScore);
    return mapObj;
  }
}
复制代码

分析:代码经过修改后,老师将每个学生的学习成绩进行汇总统计,校长只需要获取老师统计好的数据即可,无需知道老师统计的具体细节,减少了校长的额外工作量;使得老师和校长各司其职,相互之间的耦合度降低;

总结:迪米特法则,能更好的让类之间各司其职,减低相互之间的耦合度,减少牵一发而动全身的问题;

 

接口隔离原则

  ⼀个类对另⼀个类的依赖应该建⽴在最⼩的接⼝上(尽 量将臃肿庞⼤的接⼝拆分成更⼩的和更具体的接⼝,让接⼝中只包含客户感兴趣的⽅法)

做法演示

  Servlet事件监听器可以监听ServletContext、HttpSession、 ServletRequest等域对象的创建和销毁过程,以及监听这些域对象属性的修改。

ServletContextListener接⼝

public void contextInitialized(servletContextEvent sce);
public void contextDestroyed(servletContextEvent sce);

HttpSessionListener接口

public void sessionCreated(HttpSessionEvent se);
public void sessionDestroyed(HttpSessionEvent se)

ServletRequestListener接⼝

public void requestInitialized(ServletRequestEvent sre);
public void requestDestroyed(ServletRequestEvent sre);

监听应⽤代码

复制代码
public class MyListener implements ServletRequestListener, HttpSessionListener, ServletContextListener {
  public void contextInitialized(ServletContextEvent arg0) {
    System.out.println("ServletContext对象被创建了");
  }
  public void contextDestroyed(ServletContextEvent arg0) {
    System.out.println("ServletContext对象被销毁了");
  }
  public void sessionCreated(HttpSessionEvent arg0) {
    System.out.println("HttpSession对象被创建了");
  }
  public void sessionDestroyed(HttpSessionEvent arg0) {
    System.out.println("HttpSession对象被销毁了");
  } 
  public void requestInitialized(ServletRequestEvent arg0) {
    System.out.println("ServletRequest对象被创建了");
  }
  public void requestDestroyed(ServletRequestEvent arg0) {
    System.out.println("ServletRequest对象被销毁了");
  }
}
复制代码

分析:以上代码,很好的将所需功能隔离,调用者只需要引入对于的接口,无需引入其他不需要的方法,减少不必要的引用

总结:接口隔离原则,简单来说就是吃多少拿多少,不需要的就不要

 

依赖倒置原则

  在设计代码架构时,⾼层模块不应该依赖于底层模块,⼆者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。

代码场景

  抽奖活动,有随机抽奖、权重抽奖等抽奖方式;

抽奖用户类

public class BetUser {
  private String userName; // ⽤户姓名
  private int userWeight; // ⽤户权重
}

错误代码

复制代码
public class DrawControl {
  // 随机抽取指定数量的⽤户,作为中奖⽤户
  public List<BetUser> doDrawRandom(List<BetUser> list, int count) {
    // 集合数量很⼩直接返回
    if (list.size() <= count) return list;
    // 乱序集合
    Collections.shuffle(list);
    // 取出指定数量的中奖⽤户
    List<BetUser> prizeList = new ArrayList<>(count);
    for (int i = 0; i < count; i++) {
      prizeList.add(list.get(i));
    }
    return prizeList;
  }
  // 权重排名获取指定数量的⽤户,作为中奖⽤户
  public List<BetUser> doDrawWeight(List<BetUser> list, int count) {
    // 按照权重排序
    list.sort((o1, o2) -> {
      int e = o2.getUserWeight() - o1.getUserWeight();
      if (0 == e) return 0;
      return e > 0 ? 1 : -1;
    });
    // 取出指定数量的中奖⽤户
    List<BetUser> prizeList = new ArrayList<>(count);
    for (int i = 0; i < count; i++) {
      prizeList.add(list.get(i));
    }
    return prizeList;
  }
}
复制代码

注意:以上代码虽然将两种抽奖方式以方法的形势编写,但是对于后续增加的抽奖方式,则需要继续在DrawControl 类中新增额外的抽奖方法,这样就不符合开闭原则;

正确代码

抽奖接⼝

public interface IDraw {
  // 获取中奖⽤户接⼝
  List<BetUser> prize(List<BetUser> list, int count);
}

随机抽奖实现

复制代码
public class DrawRandom implements IDraw {
  @Override
  public List<BetUser> prize(List<BetUser> list, int count) {
    // 集合数量很⼩直接返回
    if (list.size() <= count) return list;
    // 乱序集合
    Collections.shuffle(list);
    // 取出指定数量的中奖⽤户
    List<BetUser> prizeList = new ArrayList<>(count);
    for (int i = 0; i < count; i++) {
      prizeList.add(list.get(i));
    }
    return prizeList;
  }
}
复制代码

权重抽奖实现

复制代码
public class DrawWeightRank implements IDraw {
  @Override
  public List<BetUser> prize(List<BetUser> list, int count) {
    // 按照权重排序
    list.sort((o1, o2) -> {
      int e = o2.getUserWeight() - o1.getUserWeight();
      if (0 == e) return 0;
      return e > 0 ? 1 : -1;
    });
    // 取出指定数量的中奖⽤户
    List<BetUser> prizeList = new ArrayList<>(count);
    for (int i = 0; i < count; i++) {
      prizeList.add(list.get(i));
    }
    return prizeList;
  }
}
复制代码

开奖类

复制代码
public class DrawControl {
  public List<BetUser> doDraw(IDraw draw, List<BetUser> betUserList, int count) {
    return draw.prize(betUserList, count);
  }
 
  public static void main(String[] args){
    List<BetUser> userList = new ArrayList();
    //初始化userList
    //这⾥的重点是把实现逻辑的接⼝作为参数传递
    new DrawControl().doDraw(new DrawWeightRank() , userList , 3);
  }
}
复制代码

分析:使用接口作为参数传递,这样可以在不原有代码的情况下,将新的抽奖方式类作为参数类传递,符合开闭原则

总结:依赖倒置原则是实现开闭原则的重要途径之⼀,它降低了类之间的耦 合,提⾼了系统的稳定性和可维护性,同时这样的代码⼀般更易读,且 便于传承。

本博客借鉴于 IT老齐的视频教程课程<IT老齐的白话设计模式> 地址为 https://itlaoqi.com

posted @   程序菜小子  阅读(114)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 因为Apifox不支持离线,我果断选择了Apipost!
· 通过 API 将Deepseek响应流式内容输出到前端
点击右上角即可分享
微信分享提示