设计模式-六大设计原则
设计模式是系统服务设计中针对常⻅场景的⼀种解决⽅案;是一种开发设计的指导思想,每⼀种设计模式都是解 决某⼀类问题的概念模型;设计模式并不局限于最终的实现⽅案,⽽是在这种概念模型下,解决系统设计中的代码逻辑问题。
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
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 因为Apifox不支持离线,我果断选择了Apipost!
· 通过 API 将Deepseek响应流式内容输出到前端