设计模式中的原则 本文是【GoF设计模式】系列的前置篇前言本篇可以当做设计模式学习前的入门也可以当成设计模式学习后的复习。先看一段典型的面条代码// 一个臃肿的 UserService校验、持久化、通知、日志全堆在一起publicclassUserService{publicvoidregister(Useruser){if(user.getName()null||user.getName().isEmpty()){thrownewIllegalArgumentException(用户名不能为空);}ConnectionconnDriverManager.getConnection(jdbc:mysql://localhost/db,root,root);// 保存、发邮件、记日志全在这里换数据库要改源码也无法单独测试}}这段代码职责过多、强依赖具体实现、难以替换和测试。随着代码增长这类结构会越来越难维护。设计原则正是从这些痛点中提炼出的经验总结能让代码组织得更灵活、可维护、可测试。面向对象设计中有几个经典原则前五个常被合称为SOLID缩写原则核心要义S单一职责原则SRP一个类只承担一个职责O开闭原则OCP对扩展开放对修改关闭L里氏代换原则LSP子类必须能替换父类I接口隔离原则ISP不依赖不需要的接口D依赖倒转原则DIP依赖抽象不依赖细节此外还有迪米特法则LoD和合成/聚合复用原则CARP。下面逐一介绍。单一职责原则SRP单一职责原则Single Responsibility Principle就一个类而言应该仅有一个引起它变化的原因。如果一个类承担的职责过多就等于把这些职责耦合在一起一个职责的变化可能削弱或抑制它完成其他职责的能力导致脆弱的设计。软件设计的重要内容之一就是发现职责并把它们相互分离。判断一个类是否职责过多可以看是否能想到多个动机去改变它或用一句话描述它的职责若出现和或等连接词往往说明违反了该原则。❌违反 SRP 的设计// 一个类承担了数据管理、权限验证、消息通知三个职责publicclassUserService{publicvoidsaveUser(Useruser){/* 保存用户 */}publicbooleancheckPermission(Useruser,Stringresource){/* 检查权限 */}publicvoidsendEmail(Useruser,Stringmessage){/* 发送邮件 */}}✅遵循 SRP 的设计// 按职责拆分每个类只负责一件事publicclassUserService{publicvoidsaveUser(Useruser){/* 保存用户 */}}publicclassPermissionService{publicbooleancheckPermission(Useruser,Stringresource){/* 检查权限 */}}publicclassNotificationService{publicvoidsendEmail(Useruser,Stringmessage){/* 发送邮件 */}}优点缺点降低类的复杂度每个类只负责一项职责增加类的数量系统复杂度上升提高代码可读性与可维护性过度拆分会增加类间依赖修改一个职责不影响其他职责需要合理判断职责边界开闭原则OCP开闭原则Open-Closed Principle软件实体类、模块、函数等应该可以扩展但是不可修改。即对扩展开放、对修改封闭。无论模块多么封闭都会存在无法封闭的变化设计人员需要猜测最可能发生的变化再构造抽象来隔离它们。开闭原则是面向对象设计的核心遵循它能带来可维护、可扩展、可复用、灵活性好等好处。但拒绝不成熟的抽象和抽象本身一样重要不要对每个部分都刻意抽象。实现开闭原则的常见方式抽象与多态用接口或抽象类定义抽象层客户端依赖抽象而非具体实现参数化把变化的部分抽象为参数策略对象、回调配置化可变部分提取到配置文件运行时读取配置决定实现元数据驱动用注解驱动框架行为新增逻辑靠加注解而非改框架❌违反 OCP 的设计// 每次新增支付方式都要修改这里的 if/elsepublicclassPaymentProcessor{publicvoidprocessPayment(Stringtype,BigDecimalamount){if(alipay.equals(type)){/* 支付宝 */}elseif(wechat.equals(type)){/* 微信 */}}}✅遵循 OCP 的设计// 通过抽象扩展新增支付方式只需新增类不改老代码publicinterfacePaymentStrategy{voidpay(BigDecimalamount);}publicclassAlipayStrategyimplementsPaymentStrategy{publicvoidpay(BigDecimalamount){/* 支付宝 */}}publicclassPaymentProcessor{privatePaymentStrategystrategy;publicPaymentProcessor(PaymentStrategystrategy){this.strategystrategy;}publicvoidprocessPayment(BigDecimalamount){strategy.pay(amount);}}优点缺点提高系统稳定性减少回归测试需要预判变化增加设计难度提高代码复用性与可维护性增加抽象层提高复杂度新功能通过扩展实现降低修改风险过度抽象会让代码难懂依赖倒转原则DIP依赖倒转原则Dependency Inversion Principle高层不应依赖低层两者都依赖抽象抽象不应依赖细节细节应依赖抽象依赖倒转可以说是面向对象设计的标志如果编写时考虑的都是针对抽象编程而非细节编程即所有依赖关系都终止于抽象类或接口就是面向对象的设计反之就是过程化设计。传统过程化设计中高层依赖低层依赖倒转把这个关系倒转过来让两者都依赖抽象从而解耦。实现方式是高层模块定义接口、低层模块实现接口再用依赖注入把低层模块注入高层模块做到面向接口编程而非面向实现编程。❌违反 DIP 的设计// 高层直接 new 低层具体实现换数据库要改源码publicclassUserService{privateMySQLUserDaouserDaonewMySQLUserDao();publicUsergetUser(intid){returnuserDao.findById(id);}}✅遵循 DIP 的设计// 高层依赖抽象低层实现抽象通过构造器注入publicinterfaceUserDao{UserfindById(intid);}publicclassMySQLUserDaoimplementsUserDao{publicUserfindById(intid){/* MySQL 查询 */}}publicclassUserService{privateUserDaouserDao;publicUserService(UserDaouserDao){this.userDaouserDao;}publicUsergetUser(intid){returnuserDao.findById(id);}}优点缺点降低类间耦合提高系统稳定性增加抽象层提高复杂度提高可扩展性便于替换具体实现需要较好的抽象能力便于并行开发与单元测试轻松 Mock 依赖依赖注入框架增加学习成本里氏代换原则LSP里氏代换原则Liskov Substitution Principle子类型必须能替换父类型。只有当子类可以替换父类、软件单位的功能不受影响时父类才能真正被复用子类也能在父类基础上增加新行为。里氏代换是继承复用的基石只有子类能完全替换父类时继承才是合理的设计。经典反例是正方形继承矩形正方形重写setWidth/setHeight强制宽高相等导致在期望矩形的地方替换为正方形时面积计算出错。正确做法是让矩形和正方形共同实现一个Shape接口而非用继承强行关联。Java 集合框架中任何使用List接口的地方都能无缝替换为ArrayList、LinkedList就是该原则的体现。❌违反 LSP 的设计// 正方形继承矩形重写方法强制宽高相等替换后面积计算出错publicclassRectangle{protectedintwidth,height;publicvoidsetWidth(intw){widthw;}publicvoidsetHeight(inth){heighth;}publicintgetArea(){returnwidth*height;}}publicclassSquareextendsRectangle{publicvoidsetWidth(intw){widthw;heightw;}publicvoidsetHeight(inth){widthh;heighth;}}✅遵循 LSP 的设计// 矩形和正方形共同实现 Shape 接口不再用继承强行关联publicinterfaceShape{intgetArea();}publicclassRectangleimplementsShape{privateintwidth,height;publicRectangle(intw,inth){widthw;heighth;}publicintgetArea(){returnwidth*height;}}publicclassSquareimplementsShape{privateintside;publicSquare(intside){this.sideside;}publicintgetArea(){returnside*side;}}优点缺点保证继承复用的正确性限制继承的灵活性提高可维护性子类不破坏父类行为需仔细设计继承关系便于统一测试父类行为可能导致类层次变深迪米特法则LoD迪米特法则Law of Demeter又叫最少知识原则如果两个类不必直接通信就不应直接相互作用需要调用时通过第三者转发。它强调每个类都应尽量降低成员的访问权限一个对象应该对其他对象有尽可能少的了解只与直接朋友通信不跟陌生人说话。直接朋友对象自身、成员对象、方法参数、方法内创建的对象陌生人通过方法返回值间接获得的对象等a.getB().doSomething()这种链式调用属于和陌生人说话违反该法则。Java 三层架构中 Controller 只调用 Service、Service 只调用 Repository就是迪米特法则的典型应用。❌违反 LoD 的设计// 房客直接与房东、银行等多个“陌生人”交互publicclassTenant{publicvoidrentRoom(){LandlordlandlordnewLandlord();BankbanknewBank();landlord.negotiate();// 直接调用房东bank.transfer();// 直接调用银行}}✅遵循 LoD 的设计// 房客只与中介直接朋友交互由中介转发调用publicclassTenant{privateAgentagent;publicvoidrentRoom(){agent.rentRoom();}}publicclassAgent{privateLandlordlandlord;privateBankbank;publicvoidrentRoom(){landlord.negotiate();bank.transfer();}}优点缺点降低类间耦合提高模块独立性可能产生大量中介类提高可读性与可维护性过度使用会使结构复杂修改一个类影响范围更小可能增加方法调用层数接口隔离原则ISP接口隔离原则Interface Segregation Principle客户端不应该依赖它不需要的接口一个类对另一个类的依赖应建立在最小接口上。要建立单一的接口不要建立庞大臃肿的接口接口中的方法应尽量少只包含客户端需要的方法。接口过大时实现类被迫实现用不到的方法产生冗余代码。这里的隔离不是物理隔离而是通过拆分接口让客户端只依赖需要的方法与不需要的方法隔离。与单一职责原则的区别对比维度单一职责原则SRP接口隔离原则ISP关注点类的职责业务功能接口的依赖范围判断依据引起变化的原因客户端需要的方法拆分对象类接口两者经常配合SRP 保证类职责单一ISP 保证接口粒度合适。典型例子是 Java 集合的Iterable/Collection/List/RandomAccess接口层次客户端可按需依赖最小接口。❌违反 ISP 的设计// 臃肿接口机器人被迫实现用不到的 eat、sleeppublicinterfaceWorker{voidwork();voideat();voidsleep();}publicclassRobotWorkerimplementsWorker{publicvoidwork(){/* 工作 */}publicvoideat(){}// 空实现机器人不吃饭publicvoidsleep(){}// 空实现机器人不睡觉}✅遵循 ISP 的设计// 拆分接口机器人只实现需要的接口publicinterfaceWorkable{voidwork();}publicclassRobotWorkerimplementsWorkable{publicvoidwork(){/* 工作 */}}优点缺点降低接口耦合客户端只依赖需要的方法接口数量增多复杂度上升提高内聚性接口职责单一拆分过细可能导致类爆炸减少冗余实现扩展不影响已有接口需合理判断接口粒度合成/聚合复用原则CARP合成/聚合复用原则Composite/Aggregate Reuse Principle优先用合成/聚合少用类继承。继承关系在编译时就固定下来无法在运行时改变子类与父类紧密耦合父类的任何变化都会波及子类限制了灵活性和复用性。合成/聚合则保持每个类的封装性让类继承层次保持较小规模。合成与聚合都是关联的特殊种类聚合是弱的拥有关系部分可独立存在如学生与班级合成是强的拥有关系部分与整体同生命周期如人与心脏。❌违反 CARP 的设计// 用继承复用引擎汽车 IS-A 引擎逻辑不对且引擎变化会波及汽车publicclassCarextendsEngine{// 引擎的任何改动都会影响汽车}✅遵循 CARP 的设计// 用合成复用引擎汽车 HAS-A 引擎运行时可切换publicinterfaceEngine{voidstart();}publicclassCar{privateEngineengine;publicCar(Engineengine){this.engineengine;}publicvoidstart(){engine.start();}}对比维度继承合成/聚合耦合度高编译时绑定低运行时可替换灵活性低高复用性受限于父类实现可组合多个对象封装性破坏封装保持封装适用场景IS-A 关系狗是动物HAS-A 关系汽车有引擎优点缺点降低类间耦合度可能产生大量小类提高灵活性与可扩展性对象组合可能让结构复杂支持运行时动态组合、保持封装初期设计成本较高原则与设计模式的对应设计原则是设计模式的灵魂每个 GoF 模式背后都对应着一条或多条原则。理解这层映射能在遇到问题时快速定位该用哪个模式原则典型对应的设计模式体现方式单一职责SRP门面模式、桥接模式按职责拆分类与接口开闭OCP策略、装饰、观察者、模板方法通过新增类扩展功能不改已有代码里氏代换LSP策略、模板方法、状态子类安全替换父类多态有意义接口隔离ISP门面模式、适配器模式为客户端提供最小接口依赖倒转DIP工厂方法、抽象工厂、依赖注入高层依赖抽象低层实现抽象迪米特法则中介者、外观模式通过中介/门面减少直接交互合成/聚合复用CARP桥接、装饰、策略、代理、组合用组合代替继承原则之间的关系这些原则并非孤立存在而是相互支撑、形成完整的面向对象设计思想体系开闭原则核心目标 ↑ ┌──────────────┼──────────────┐ │ │ │ 单一职责原则 依赖倒转原则 里氏代换原则 类的拆分 抽象层解耦 继承的正确使用 │ │ │ └──────────────┼──────────────┘ ↓ 接口隔离原则接口拆分 ↓ 迪米特法则降低耦合 ↓ 合成/聚合复用原则复用方式开闭原则是核心目标让系统易于扩展、难以修改单一职责通过职责拆分为开闭原则创造条件依赖倒转通过抽象层解耦实现开闭原则里氏代换保证继承的正确性使多态有意义接口隔离通过拆分臃肿接口让依赖更精确迪米特法则通过减少通信降低耦合合成/聚合复用通过组合提高灵活性和复用性。这些原则有时也会相互掣肘需要权衡接口隔离拆接口 vs 合成复用导致类爆炸只有当不同客户端确实只用到接口的一部分时才拆分依赖倒转抽象层 vs 简单性抽象的前提是变化真实存在或可预见变体只有一种时直接依赖具体实现反而更清晰迪米特减少通信 vs 中介类泛滥中介只在需要降低跨层耦合时引入不为内部协作加壳原则不是强制规定不要为了遵循原则而过度设计。先让代码简单正确地跑起来等到变化真正出现时再重构引入抽象——重复三次的代码才考虑抽象。记忆口诀拆分靠 SRP一个类只做一件事扩展靠 OCP加功能不改老代码换实现靠 DIP都依赖抽象运行时替换继承看 LSP子类能替父类多态才安全接口靠 ISP用不到的方法别塞过来通信靠 LoD只跟直接朋友说话复用靠 CARP能用组合就别用继承一句话总纲先简单正确再按需抽象拆得清职责换得动实现扩得起新功能。