news 2026/2/16 10:37:02

设计模式学习(3) 设计模式原则

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
设计模式学习(3) 设计模式原则

0.个人感悟

  • 设计原则类似修真世界里的至高法则,万法的源头。遵守法则造出的术法具有能耗低、恢复快、自洽性高等优点,类似遵守设计原则设计的出的程序,具有很多优点
  • 设计原则从不同的角度对软件设计提供了约束和指导。其中开闭原则、依赖倒置让人印象深刻。我第一次重构一个模块时对开闭原则有了一丝感悟
  • 设计原则的核心思想: 对象内部高内聚,对象之间交互松耦合,变化也不变隔离开
  • 建议有条件读原著。写博客过程中,这些原则的英文都是手打出来的,感觉确实原汁原味过一遍和听别人说出来不一样
  • 最后一点是写给自己。这个记录是目前写的最久的一次了,上次写这么久文档还是参加公司代码评选。主要是设计原则本来就很重要,网上的教程,特别是举的例子也参差不齐,想着结合自己所学,用一个大场景来覆盖所有原则。事实上还是能力有限,中间也想过放弃,反复纠结中还是坚持下来了。
    • 不要害怕不完美,不要害怕被喷,错就改
    • 不要总想着一步到位,可以一层一层目标实现
    • 谢谢坚持,谢谢老婆的支持

1.概念

设计模式原则,是程序员在编程时应当遵循的原则,也是各种设计模式的基础。

2.作用

  • 指导程序设计,也是具体的设计模式遵循的规则
  • 同[[[设计模式学习(1) 概述]]]中讲到的,也是有助于实现软件开发过程中1(高内聚,低耦合)核4性(复用性 可读性 可扩展性 稳定性)
  • 个人修行的心法

3. 分类

说起设计模式分类,网上流传的有5大原则、6大、7大原则,那到底是哪些,出处又是哪些呢。查阅资料发现其实也没有一个明确的官方说法,只是不同时期不同的作者提出。每个原则的提出都有自己的价值,后面会按7大原则就进行学习

  1. solid族5大设计原则 。
    罗伯特·C·马丁在2000年左右撰写了一系列文章来阐述这些原则,并最终在2003年的论文《Design Principles and Design Patterns》中将其整合并命名为“SOLID”
    • S 单一职责原则
    • O 开闭原则
    • L 里氏替换原则
    • I 接口隔离原则
    • D 依赖倒置原则
  2. 迪米特法则(最小知道原则)
    **伊恩·霍兰德 **等,在1987年美国东北大学在项目研究报告《The Demeter Project: Aspect-Oriented Programming》中提出
  3. 合成复用原则
    四人帮(Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides)在1994年出版的《设计模式:可复用面向对象软件的基础》中提到。这本书被誉为设计模式圣经,首次系统地提出了23中经典设计模式。也是这个系列文章重要的参考。建议大家有条件去读一读!

4.详细介绍

4.1 单一职责原则(Single Responsibility Principle,SRP)

4.1.1定义

A class should have one, and only one, reason to change.

字面意思:
一个类应该只有一个引起变化的原因
理解:
一个类应该只负责一种职责。如果负责多个职责,那么当一个职责变化时,可能影像其它职责变化,进而引起整个类的变化。

4.1.2 好处

某个原则的优点、包括违背原则的以及后续其它设计原则的介绍,都围绕软件开发的1核4性来说明。这也是为什么我写文章时在专门在开篇里提到“软件开发过程遇到的问题和想达到的目标”。因为设计模式,包括设计与原则都是绕不开这个目标。这里分析优缺点时只介绍突出的项。

  • 高内聚,低耦合 职责粒度内聚代码,职责分开了,耦合也就低了
  • 稳定性 从原则的定义上就很明显地体现出,将代码修改引起的变化控制到某个职责,某个类中

4.1.3 违背原则会存在的问题

  • 高内聚,低耦合 违背单一职责原则,结果上发现是反过来的,类承担过多职责,不内聚,多个职责耦合到一块,高耦合
  • 稳定性 多个职责耦合到一块,修改一个职责,就有影像其它职责的风险

4.1.4 如何做

这个要求设计人员有很强的分析、设计能力和经验,能识别将系统中不同的职责分离开来,并封装到不同的类或者模块中

4.1.5 示例

这里假设一个业务场景,博客评论
流程简化为 用户验证->评论内容校验->评论存储
模型设计: 用户模块User 文章模块Article 评论模块Commmet
示例:
UserService和CommentService职责分开,UserService负责用户验证,CommmetService负责评论相关操作;而不是全放一个service中
UserService:

packageprinciple.blog.user.service;publicinterfaceUserService{booleancanPostComment();}

CommentService:

packageprinciple.blog.comment.service;importprinciple.blog.comment.model.Comment;importjava.util.List;publicinterfaceCommentService{voidpostComment(Stringcontent);voideditComment(StringcommentId,StringnewContent);voiddeleteComment(StringcommentId);voidreplyToComment(StringparentCommentId,Stringcontent);List<Comment>getReplies(StringcommentId);}

模拟评论流程

packageprinciple.blog.test.srp;importprinciple.blog.comment.model.Comment;importprinciple.blog.comment.service.CommentService;importprinciple.blog.user.service.UserService;publicclassSrpTest{privatefinalUserServiceuserService;privatefinalCommentServicecommentService;publicSrpTest(UserServiceuserService,CommentServicecommentService){this.userService=userService;this.commentService=commentService;}voidsrp(){Commentcomment=newComment();// 1.用户校验booleancanPostComment=userService.canPostComment();// 2.评论校验逻辑// 3.存储commentService.postComment(comment.getContent());// 4.其它逻辑}}

4.2 开闭原则(Open-Closed Principle,OCP)

4.2.1定义

Software entities (class, modules, functions,etc.) should be open for extension, but closed for modification.

字面意思:
软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。
理解:

  • 感觉这是最基础、最重要的设计原则。其它原则都是在为开闭原则服务,并且在具体设计模式学习中尤为感受深刻
  • 软件设计时允许在不改变先手代码的情况下扩展功能。对扩展开放(提供方),修改关闭(使用放)

4.2.2 好处

  • 高内聚,低耦合 高内聚,低耦合的代码易于扩展,遵循开闭原则倒逼设计者考虑如何提高内聚,降低耦合
  • 扩展性、稳定性 简直是量身定做,原则的定义中就提到了扩展性。对修改关闭,也就意味着改动不应该影像核心代码,进而增强系统稳定性。

4.2.3 违背原则会存在的问题

  • 高内聚,低耦合 不遵循开闭原则,很容易让人分不清哪些是变化的那些是不变的,代码的内聚性会很差
  • 扩展性、稳定性 如果不按拓展方式修改,有修改引入风险,也增加测试的复杂度

4.2.4 如何做

  • 这是一个系统性的设计问题。需要设计人员分析系统有哪些模块,模块间的关系和通信协议,模块中哪些是变化的那些是不变的
  • 利用抽象和多态实现

4.2.5 示例

还是博客评论业务,需要对文本进行校验,有不同的校验规则,比如长度校验、敏感词校验等。
设计时可以考虑抽取校验器接口,统一管理,后续新增校验规则,只用实现校验器接口(对扩展开放),不用动使用方的代码(对修改关闭)
校验器接口:

packageprinciple.blog.comment.validation;importprinciple.blog.comment.model.Comment;publicinterfaceCommentFilter{booleanshouldFilter(Commentcomment);StringgetReason();}

基于关键词的实现:

packageprinciple.blog.comment.validation.impl;importprinciple.blog.comment.model.Comment;importprinciple.blog.comment.validation.CommentFilter;importjava.util.Set;publicclassKeywordFilterimplementsCommentFilter{privatefinalSet<String>bannedKeywords;publicKeywordFilter(Set<String>bannedKeywords){this.bannedKeywords=bannedKeywords;}@OverridepublicbooleanshouldFilter(Commentcomment){Stringcontent=comment.getContent().toLowerCase();returnbannedKeywords.stream().anyMatch(content::contains);}@OverridepublicStringgetReason(){return"包含违禁关键词";}}

基于链接校验的实现:

packageprinciple.blog.comment.validation.impl;importprinciple.blog.comment.model.Comment;importprinciple.blog.comment.validation.CommentFilter;importjava.util.regex.Matcher;importjava.util.regex.Pattern;publicclassExcessiveLinkFilterimplementsCommentFilter{privatefinalintmaxLinks;publicExcessiveLinkFilter(intmaxLinks){this.maxLinks=maxLinks;}@OverridepublicbooleanshouldFilter(Commentcomment){Stringcontent=comment.getContent();longlinkCount=countLinks(content);returnlinkCount>maxLinks;}privatelongcountLinks(Stringcontent){PatternurlPattern=Pattern.compile("https?://[\\w./?=&]+");Matchermatcher=urlPattern.matcher(content);returnmatcher.results().count();}@OverridepublicStringgetReason(){return"包含过多链接";}}

客户端代码

packageprinciple.blog.test.ocp;importprinciple.blog.comment.model.Comment;importprinciple.blog.comment.validation.CommentFilter;importjava.util.List;publicclassOcpTest{// 注入所有过滤器实现privatefinalList<CommentFilter>filters;publicOcpTest(List<CommentFilter>filters){this.filters=filters;}voidocpTest(){Commentcomment=newComment();// 按个调用过滤器实现进行校验booleancanSave=filters.stream().noneMatch(item->item.shouldFilter(comment));// 其它逻辑实现}}

4.3 里氏替换原则(Liskov Substitution Principle,LSP)

4.3.1定义

Subtypes must be substitutable for their base typses.

字面意思: 子类型必须能够替换其基类型
理解:

  • 详细说法: 如果对每个类型为 T1 的对象 o1,都有类型为 T2 的对象 o2,使得以 TI定义的所有程序P在所有的对象 o1 都代换成 o2 时,程序P的行为没有发生变化,那么类型T2是类型T1的子类型。换句话说,所有引用基类的地方必须能透明地使用其子类的对象。
  • 出现这一原则的原因: 继承的双刃性(这一点在韩老师的视频中讲的非常精准,我之前学习这个原则时未get到这一点)
    • 继承的优点:
    1. 复用性: 父子族类有利于代码的组织和复用。这也是面向对象的优点。
    2. 灵活性: 面向对象的三大特点,封装、继承、多态。多态意味着代码可以更加灵活
    • 继承的弊端
    1. 侵入性: 运行基类类的代码,可以通过继承创建子类,进而用子类替换父类,如果子类中修改了基类功能(重写),那么势必会影响原代码功能。jdk中很多类都是final的。这也是这个原则提出背景。特别是无意识地修改,对于系统的稳定性是具体风险。
    2. 耦合性: 没错,继承意味着耦合,意味着你得清楚基类的功能和细节,不合理地使用和扩展会有引入风险。

4.3.2 好处

  • 稳定性 引用基类的地方可以透明地使用子类的对象,可以保证使用基类的代码不会受到影响

4.3.3 违背原则会存在的问题

  • 灵活性: 这一点其实算是优点。使用继承和多态,有利于代码灵活性
  • 稳定性: 子类如果重写父类方法,很容易影像父类代码正常运行

4.3.4 如何做

  • 使用继承时,在子类中尽量不要重写父类的方法
  • 对于变化的功能,在基类中建议抽取成抽象方法
    为什么对于接口不会提出里氏替换原则,因为接口定义的本来就是抽象的,不会存在覆盖一说。如果类中某些行为在类族中存在变化,建议提取抽象方法
  • 使用组合替代继承。后面合成复用原则会提到
  • 辩证使用继承和遵循里氏替换原则
    实际开发经验来说,建议均衡继承的灵活性和风险性。不是一棒子打死一定要遵循里氏替换原则。比如Jdk中的Object基类,其中toString方法,子类中也经常覆盖。
    • 建议遵循的场景: 功能模块、容易变化
    • 可以考虑灵活性的场景: 基础模块,不容易变化。比如你所负责模块是基础模块,类似Jdk中基础类型,很稳定,你可以自行设计对外不暴露细节。

4.3.5 示例

4.3.5.1示例1:

常用的例子,正方形和长方形
如果正方形继承长方形,为了代码正确,需要重写get、set。那么基类(长方形)的代码,使用子类(正方形)的对象去替换就会出问题,因为多态执行的是子类重写的方法。所以继承带来便捷的同时,有这种修改风险。
长方形:

packageprinciple.liskovsubstitution.v1;publicclassRectangle{protectedintlength;protectedintwidth;publicintgetLength(){returnlength;}publicvoidsetLength(intlength){this.length=length;}publicintgetWidth(){returnwidth;}publicvoidsetWidth(intwidth){this.width=width;}publicintgetArea(){returnlength*width;}}

正方形:

packageprinciple.liskovsubstitution.v1;publicclassSquareextendsRectangle{@OverridepublicvoidsetLength(intlength){this.length=length;this.width=length;}@OverridepublicvoidsetWidth(intwidth){this.length=width;this.width=width;}}

违背里氏替换原则

packageprinciple.liskovsubstitution.v1;publicclassShapeClient{publicstaticvoidmain(String[]args){// 原始类型Rectanglerectangle=newRectangle();rectangle.setLength(4);rectangle.setWidth(5);System.out.println(rectangle.getArea());// 子类rectangle=newSquare();rectangle.setLength(4);rectangle.setWidth(5);System.out.println(rectangle.getArea());}}

那是不是一定要遵循里氏替换原则呢。其实也不能一棒子打死。比如jdk中的Object的toString()方法,很多类型都重写了它。所以要结合业务区考虑,如果是基础、稳定的模块,做好封装,也可以考虑继承。

packageprinciple.liskovsubstitution.other;publicclassTypeTest{publicstaticvoidmain(String[]args){Objectobj1=newObject();System.out.println(obj1.toString());Objectobj2=newString();System.out.println(obj2.toString());}}

对于长方形、正方形问题,网上有很多讨论,有认为继承没问题,主要是没有提供构造方法导致太灵活;有干脆分成单独两个类;还有的采用合成复用方式,让正方形组合长方形。我这里参考obj类设计,采用这种设计,提出一个公共基类,抽象getArea()方法,两个类分别继承。

packageprinciple.liskovsubstitution.v3;publicclassShape{intDEFAULT_AREA=0;publicintgetArea(){returnDEFAULT_AREA;}}
packageprinciple.liskovsubstitution.v3;publicclassSquareextendsShape{privateintwide;publicintgetWide(){returnwide;}publicvoidsetWide(intwide){this.wide=wide;}publicSquare(intwide){setWide(wide);}@OverridepublicintgetArea(){returnwide*wide;}}
packageprinciple.liskovsubstitution.v3;publicclassRectangleextendsShape{protectedintlength;protectedintwidth;publicRectangle(intlength,intwidth){this.length=length;this.width=width;}publicintgetLength(){returnlength;}publicvoidsetLength(intlength){this.length=length;}publicintgetWidth(){returnwidth;}publicvoidsetWidth(intwidth){this.width=width;}publicintgetArea(){returnlength*width;}}
packageprinciple.liskovsubstitution.v3;importjava.util.ArrayList;importjava.util.List;publicclassShapeClient{publicstaticvoidmain(String[]args){// 长方形Rectanglerectangle=newRectangle(4,5);System.out.println(rectangle.getArea());// 正方形Squaresquare=newSquare(6);System.out.println(square.getArea());// 基类统一管理List<Shape>shapeList=newArrayList<>(2);shapeList.add(rectangle);shapeList.add(square);intsum=shapeList.stream().mapToInt(Shape::getArea).sum();System.out.println(sum);}}
4.3.5.2 示例二

还是博客评论业务,举一个正面例子,利用抽象
遇到这样一个场景,持久层,可以根据策略切换采用内存实现或者数据库实现
这里设计的话,持久层抽象为接口,有内存、数据库两个实现,根据配置进行切换。子类覆写的是抽象方法,就不会违背里氏替换原则

packageprinciple.blog.comment.repository;importprinciple.blog.comment.model.Comment;importjava.util.List;importjava.util.Optional;publicinterfaceCommentStorage{voidsave(Commentcomment);Optional<Comment>findById(Stringid);List<Comment>findByArticleId(StringarticleId);voiddelete(Stringid);// 基础统计方法intcountByArticleId(StringarticleId);intcountByUserId(StringuserId);}
packageprinciple.blog.comment.repository;importprinciple.blog.comment.model.Comment;importjava.util.List;importjava.util.Map;importjava.util.Optional;importjava.util.concurrent.ConcurrentHashMap;importjava.util.stream.Collectors;/** * 内存存储实现 */publicclassInMemoryCommentStorageimplementsCommentStorage{privatefinalMap<String,Comment>storage=newConcurrentHashMap<>();@Overridepublicvoidsave(Commentcomment){storage.put(comment.getId(),comment);}@OverridepublicOptional<Comment>findById(Stringid){returnOptional.ofNullable(storage.get(id));}@OverridepublicList<Comment>findByArticleId(StringarticleId){returnstorage.values().stream().filter(comment->articleId.equals(comment.getArticleId())).collect(Collectors.toList());}@Overridepublicvoiddelete(Stringid){storage.remove(id);}@OverridepublicintcountByArticleId(StringarticleId){return(int)storage.values().stream().filter(comment->articleId.equals(comment.getArticleId())).count();}@OverridepublicintcountByUserId(StringuserId){return(int)storage.values().stream().filter(comment->userId.equals(comment.getAuthorId())).count();}}
packageprinciple.blog.comment.repository;importprinciple.blog.comment.model.Comment;importjava.util.List;importjava.util.Optional;/** * 数据库存储实现 */publicclassDatabaseCommentStorageimplementsCommentStorage{// 使用JDBC@Overridepublicvoidsave(Commentcomment){}@OverridepublicOptional<Comment>findById(Stringid){returnOptional.empty();}@OverridepublicList<Comment>findByArticleId(StringarticleId){returnList.of();}@Overridepublicvoiddelete(Stringid){}@OverridepublicintcountByArticleId(StringarticleId){return0;}@OverridepublicintcountByUserId(StringuserId){return0;}}
packageprinciple.blog.test.lsp;importprinciple.blog.comment.repository.CommentStorage;importprinciple.blog.comment.repository.DatabaseCommentStorage;importprinciple.blog.comment.repository.InMemoryCommentStorage;publicclassLspTest{voidlsp(){// 仓存使用抽象定义。就不会存在子类继承后覆盖原来逻辑的场景CommentStoragestorage;// 仓存层可以根据配置切换内存实现还是数据库storage=newInMemoryCommentStorage();storage=newDatabaseCommentStorage();}}

4.4 接口隔离原则(Interfance Segregation Principle,ISP)

4.4.1定义

Clients should not be forced to depend on methonds they do not use.

字面意思:
客户端不应该被迫依赖于它们不使用的方法
理解:

  • 一个类对另一个类的依赖应该建立在最小的接口上
  • 接口隔离原则可以辅助单一职责原则。单一职责注重的是单个类职责的组织,接口隔离注重的是依赖类职责的隔离。一个被依赖的类,可以按被需要的功能来组织职责。

4.4.2 好处

  • 高内聚,低耦合 被依赖的类按需要去聚合职责,提高内聚; 不依赖不需要的方法,降低了类与类之间的耦合
  • 稳定性 同单一职责原则。将引起类变化的因素控制到最小。

4.4.3 违背原则会存在的问题

  • 高内聚,低耦合 提供过多的方法,提供方功能不够内聚; 依赖不需要的方法,增加了类与类之间的耦合
  • 稳定性 同单一职责原则。被依赖的类提供过多的方法,那么这些方法在修改时有影响其它方法的风险,进而影响使用方。

4.4.4 如何做

  • 接口尽量小,按需要去提供功能
  • 封装。封装细节,只提供需要对外提供的方法
  • 辩证分析。结合具体业务设计接口。这个要求设计人员要有很强的分析设计经验

4.4.5 示例

还是博客评论业务,业务变化,评论要区分普通用户权限和管理员权限 管理员还可以审核
设计的话考虑对原接口进行拆分,通过组合方式对外提供不同服务
将操作划分为3个粒度的接口:
粒度接口1: 基本的评论操作

packageprinciple.blog.comment.service.operations;/** * 细粒度接口1: 基本的评论操作 */publicinterfaceBasicCommentOperations{voidpostComment(Stringcontent);voideditComment(StringcommentId,StringnewContent);voiddeleteComment(StringcommentId);}

细粒度接口2: 评论回复操作

packageprinciple.blog.comment.service.operations;importprinciple.blog.comment.model.Comment;importjava.util.List;/** * 细粒度接口2: 评论回复操作 */publicinterfaceCommentReplyOperations{voidreplyToComment(StringparentCommentId,Stringcontent);List<Comment>getReplies(StringcommentId);}

细粒度接口3: 评论审核操作(仅审核员可用)

packageprinciple.blog.comment.service.operations;importprinciple.blog.comment.model.Comment;importjava.util.List;/** * 细粒度接口3: 评论审核操作(仅审核员可用) */publicinterfaceCommentModerationOperations{voidapproveComment(StringcommentId);voidrejectComment(StringcommentId,Stringreason);voidmarkAsSpam(StringcommentId);List<Comment>getPendingComments();}

对外服务基于角色功能进行组合
普通用户评论服务

packageprinciple.blog.comment.service;importprinciple.blog.comment.service.operations.BasicCommentOperations;importprinciple.blog.comment.service.operations.CommentReplyOperations;/** * 普通用户评论服务 */interfaceRegularUserCommentServiceextendsBasicCommentOperations,CommentReplyOperations{}

审核员评论服务

packageprinciple.blog.comment.service;importprinciple.blog.comment.service.operations.CommentModerationOperations;importprinciple.blog.comment.service.operations.CommentReplyOperations;/** * 审核员评论服务 */publicinterfaceModeratorCommentServiceextendsCommentService,CommentReplyOperations,CommentModerationOperations{}

4.5 依赖倒置原则(Dependency Inversion Principle,DIP)

4.5.1定义

High-level modules should not depend on low-level modules. Both should depend on absractions. Abstractions should not depend on detailds. Deatils should depend on abstractions.

字面意思:
高层模块不应该依赖于低层模块,两者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。
理解:

  • 抽象比细节更稳定。感觉这是编程思维的一种精华。
  • 依赖倒置原则是实现开闭原则的重要手段
  • 它的另一种解释也不陌生: 面向接口编程

4.5.2 好处

  • 高内聚,低耦合 以接口维度内聚职责;依赖抽象,而不是具体,可以替换实现,降低了耦合
  • 扩展性、稳定性: 同开闭原则。

4.5.3 违背原则会存在的问题

  • 高内聚,低耦合 依赖具体的类,增加了耦合
  • 扩展性、稳定性 同开闭原则。反过来,高层依赖底层并且依赖于具体,那么当业务变化时,需要直接去修改依赖的具体类,不易修改,而且引入风险极高。

4.5.4 如何做

  • 面向接口编程。 分析系统的模块及模块间的依赖关系,约定对外提供的接口。
  • 使用继承时,尽量遵循里氏替换原则

4.5.5 示例

同[[设计模式学习(3) 设计模式原则#4.2.5 示例]]

4.6 迪米特法则(Law of Demeter,LoD;Principle of Least Knowledge, PLK)

4.6.1定义

Eache unit should have only limited knowledge about other units: only units ‘closedly’ related to the current unit. Or: Each unit should only talk to its friends; don’t talk to strangers.

字面意思:
每个单元对于其他单元只应该拥有有限的知识:只了解与当前单元关系密切的单元。或者说:每个单元只应该与其朋友对话,不要与陌生人对话。
理解:

  • 最少知道原则。一个类对自己依赖的类知道的越少越好,只需要知道自己需要的输入输出,不用知道实现细节以及被依赖的类的依赖类。
  • 被依赖的类应该做好封装,仅提供需要使用方知道的功能
  • 和接口隔离原则相互补充。接口隔离原则约束了一个类对外应该提供的功能。最小知道原则约束一个类做好这些功能的封装;作为使用方时,不要依赖陌生类。

4.6.2 好处

  • 高内聚,低耦合 类封装细节,只暴露需要提供的方法,提高内聚; 仅与依赖的类通信,降低耦合
  • 稳定性 不依赖陌生的类,减少依赖,那么一个类的变化能传递的影像随之减小

4.6.3 违背原则会存在的问题

  • 高内聚,低耦合 类内部细节暴露,不够内聚;依赖增加,增加了耦合
  • 稳定性 依赖陌生的类,当被依赖的类修改时,增加了引入风险

4.6.4 如何做

  • 使用方,只依赖需要依赖的类,不去关注它的实现细节
  • 提供方,只暴露该暴露的方法,封装细节
  • 类关系设计上,减少网状依赖。循环依赖更使不得

4.6.5 示例

还是博客评论业务
假如有一个模块依赖评论模块,需要依赖CommentService,那么它只用和CommetService打交道就可以,不用在意内部实现。如果调用Commmet持久层接口,那就违背了迪米特法则

packageprinciple.blog.test.lod;importprinciple.blog.comment.model.Comment;importprinciple.blog.comment.repository.CommentStorage;importprinciple.blog.comment.service.CommentService;publicclassLodTest{privatefinalCommentServicecommentService;privatefinalCommentStoragecommentStorage;publicLodTest(CommentServicecommentService,CommentStoragecommentStorage){this.commentService=commentService;this.commentStorage=commentStorage;}voidlod(){// 假设这个类是service层类,依赖评论模块的commentServiceCommentcomment=newComment();// 正确示例 仅依赖commentService,不和commentStorage交互commentService.postComment(comment.getContent());// 错误示例 不仅和commentService交互,也和commentStorage交互commentStorage.save(comment);commentService.postComment(comment.getContent());}}

4.7 合成复用原则(Composite Reuse Principle,CRP)

4.7.1定义

Wherever possible, one shoud prefer composing classes out of componets that already exist, rather than creating new class by inheritance from existing clssses. This principle is known as composition over inheritance.

字面意思:
在可能的情况下,应该优先使用已有的组件来组合类,而不是通过继承现有类来创建新类。这一原则被称为组合优于继承。
理解:

  • 尽量使用组合、聚合的方式来依赖类,而不是继承
  • 不要为了使用某个类的方法而去继承它
  • 符合里氏替换原则,不使用继承就不会有继承的弊端问题

4.7.2 好处

  • 高内聚,低耦合 组合、聚合依赖性远远小于继承的方式
  • 稳定性 同里氏替换原则

4.7.3 违背原则会存在的问题

  • 高内聚,低耦合 继承的方式依赖性远远大于组合、聚合
  • 稳定性 同里氏替换原则

4.7.4 如何做

  • 尽量使用组合、聚合的方式来构建类的关系,而不是使用继承
  • 使用继承时,尽量遵循里氏替换原则

4.7.5 示例

还是博客评论业务,现在业务发现,可以带图进行评论
如果采用继承的方式,会增加耦合和代码复杂度;如果采用充血模型,这里假如校验方法,那么图片评论类会重写校验方法,违背里氏替换原则,增加风险

packageprinciple.blog.test.crp.v1;importjava.util.Date;publicclassComment{privateStringid;privateStringauthorId;privateStringarticleId;privateStringparentCommentId;privateStringcontent;privateDatecreatedAt;publicvoidvalidate(){// 校验文本content逻辑}// 省略}

继承的方式

packageprinciple.blog.test.crp.v1;publicclassImageCommentextendsComment{privateStringurl;privateStringtype;privateStringcaption;@Overridepublicvoidvalidate(){super.validate();// 图片校验逻辑}// 省略}

使用组合:提取图片信息类,组合到原评论类中

packageprinciple.blog.test.crp.v2;publicclassImageInfo{privateStringurl;privateStringtype;privateStringcaption;// 省略}
packageprinciple.blog.test.crp.v2;importprinciple.blog.comment.validation.CommentFilter;importjava.util.ArrayList;importjava.util.Date;importjava.util.List;publicclassComment{privateStringid;privateStringauthorId;privateStringarticleId;privateStringparentCommentId;privateStringcontent;privateDatecreatedAt;// 组合而不是继承privateList<ImageInfo>imageInfos;publicvoidvalidate(){// 注入所有校验器(包括 文本、图片校验器实现)List<CommentFilter>filters=newArrayList<>();// 循环校验}// 省略}
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/7 0:46:32

YOLO + Arduino 构建低成本智能门禁系统

YOLO Arduino 构建低成本智能门禁系统 在城市社区、共享办公空间甚至家庭住宅中&#xff0c;传统刷卡或密码门禁正暴露出越来越多的短板&#xff1a;卡片易丢失、密码可被窥探、无法识别真实身份。与此同时&#xff0c;边缘计算能力的跃升让“把AI放进门禁盒”成为可能——我们…

作者头像 李华
网站建设 2026/2/14 5:55:38

使用 Docker Compose 部署 LobeChat 数据版

使用 Docker Compose 部署 LobeChat 数据版 在构建私有化 AI 助手的实践中&#xff0c;持久化的会话存储、安全的身份认证和可靠的文件处理能力是生产环境部署的核心需求。LobeChat 作为一款现代化的开源 AI 聊天框架&#xff0c;不仅提供了媲美 ChatGPT 的交互体验&#xff0…

作者头像 李华
网站建设 2026/2/7 1:11:30

PaddlePaddle训练任务中断恢复:借助git版本控制系统还原状态

PaddlePaddle训练任务中断恢复&#xff1a;借助Git版本控制系统还原状态 在深度学习项目开发中&#xff0c;最令人沮丧的场景之一莫过于——经过连续36小时的训练&#xff0c;模型刚刚进入收敛阶段&#xff0c;却因为服务器断电、代码异常或误操作导致进程终止。重启训练&#…

作者头像 李华
网站建设 2026/2/12 5:33:33

Yolo-v5运行中thop安装与检测框问题解决

YOLOv5 实践避坑实录&#xff1a;thop 安装与检测框缺失的根源解析 在部署 YOLOv5 模型时&#xff0c;你有没有经历过这样的时刻&#xff1f; 明明代码跑通了&#xff0c;日志也输出了一堆张量信息&#xff0c;结果打开图像一看——干干净净&#xff0c;一个框都没有。再回头…

作者头像 李华
网站建设 2026/2/9 11:51:38

HarmonyOS 星闪快速实战

一、什么是星闪&#xff1f; 星闪&#xff08;NearLink&#xff09; 是华为研发的新一代短距离无线通信技术&#xff0c;可以理解为"华为版蓝牙"&#xff08;仅限我们目前用的&#xff0c;有对标WiFi的版本&#xff09;&#xff0c;但比蓝牙更快、更稳、更省电。 星…

作者头像 李华
网站建设 2026/2/8 20:30:03

一文带你入门智能体Agent开发——核心知识与学习路线

你是否也曾面对复杂的AI Agent项目&#xff0c;却只能照着README文档傻傻使用&#xff1f;这篇文章将帮你彻底打破这一局面&#xff0c;轻松掌握AI Agent开发技能&#xff01;从核心概念到实战框架&#xff0c;一文打尽&#xff01;一、什么是Agent&#xff1f;狭义上的Agent&a…

作者头像 李华