三年前我接了一个订单模块的维护,当时只是要加一种新的支付方式,结果改了五个类,还把另一个渠道的成功回调带出了问题。事后复盘,问题根本不在支付方式本身,而在于整个模块的扩展方式就是往上堆 if-else。类似经历踩多了,我才真正把软件设计七大原则当成一回事。它们不是用来应付软考中级软件设计师的条条框框,而是在你每次“加一个功能”的时候,帮你少踩一次坑。这篇文章结合代码实例,把这七条原则完整梳理一遍。不管是准备考试,还是日常写业务代码,都能对照着看看。核心目的只有一个:让程序改得动、测得了、敢上线。
1. 先看清楚:可维护性差,到底差在哪
1.1 三个典型的“坏味道”
很多项目刚上线时跑得挺好,半年之后就成了谁都不敢碰的代码。可维护性差,通常不是某个功能写错了,而是代码结构在一点点腐烂。我总结下来最典型的三个征兆:
第一个是“改一行崩三处”。比如全局有个订单状态字段,某天为了新需求给它加了一个枚举值,结果支付回调、对账报表、用户消息推送全都跟着出了状态判断问题。原因是这些逻辑都直接依赖一个可变的状态字段,而不是依赖稳定的行为接口。
第二个是“新功能无处安放”。想加一个会员折扣,翻半天代码才发现价格计算逻辑分散在 Service、工具类、甚至 SQL 里都有,真正动手的时候只能凭感觉到处补丁。第三个是“单元测试无处下笔”。方法里面又读数据库又发短信又扣库存,测试想 mock 其中一个依赖,必须把整个环境都搭起来。这种代码不是不能跑,而是没人敢重构,最后只能继续往上叠补丁。
1.2 七大原则不是规则,是观察视角
软件设计七大原则包括:单一职责原则、开闭原则、里氏替换原则、依赖倒置原则、接口隔离原则、迪米特法则和合成复用原则。很多人觉得它们是一个清单,写代码时逐条检查,其实不是。它们更像一组“观察视角”,帮助你从不同维度发现代码里的问题:
- 单一职责原则看的是“类内部是否太杂”;
- 开闭原则看的是“扩展时要不要改老代码”;
- 里氏替换原则看的是“继承关系是否安全”;
- 依赖倒置原则看的是“依赖方向是否合理”;
- 接口隔离原则看的是“接口大小是否合适”;
- 迪米特法则看的是“对象之间的社交范围”;
- 合成复用原则看的是“复用手段是否选对”。
这七条彼此有联动。开闭原则是目标,单一职责、依赖倒置、接口隔离是手段,里氏替换是继承的安全底线,迪米特法则管好耦合边界,合成复用则提醒你别动不动就继承。实际写代码时,不需要每一条都生搬硬套,但一旦出现“改不动、测不了、不敢上线”的苗头,就该用这些视角去检查代码。
2. 单一职责原则 + 接口隔离原则:先把“职责”划清楚
2.1 单一职责:一个类真的只有一个改变理由吗
单一职责原则是指一个类应该只有一个引起它变化的原因。听起来抽象,落到实处就是:当你需要修改一个类时,应该只有一个理由去改它。
举个最常见的反面例子,订单类:
public class Order { private double price; private int count; public double calculateTotal() { // 计算商品总价 return price * count; } public void saveToDb() { // SQL 插入订单表 // jdbcTemplate.update("insert into orders ..."); } public void sendNotification() { // 发短信提醒用户 // smsClient.send("您的订单已生成"); } }这个类里面有三个职责:价格计算、持久化、消息通知。以后改打折规则要改这里,改表结构要改这里,换短信服务商还要改这里。三个修改理由挤在一个类里,任何一个改动都可能影响另外两个功能。
重构思路是拆分:
public class Order { private double price; private int count; // 只保留订单自身的数据和行为 } public class OrderCalculator { public double calculateTotal(Order order) { return order.getPrice() * order.getCount(); } } public class OrderRepository { public void save(Order order) { // 只负责持久化 } } public class OrderNotifier { public void sendNotification(Order order) { // 只负责通知 } }这样每个类只有一个修改理由。以后价格规则变了,只改 OrderCalculator;数据库变了,只改 OrderRepository。拆完之后,每个类也更容易测试。
这里要提醒一句:单一职责不是“越细越好”。我曾经见过有人把User类拆成UserBasicInfo、UserDetailInfo、UserLoginInfo,结果反而让代码碎片化。判断标准很简单:问自己一句“这个类有几个引起变化的原因”,回答是一个,就够了。如果两个原因总是同进同退,拆开反而制造麻烦。
2.2 接口隔离:不要给实现类塞一堆用不上的方法
接口隔离原则说的是:客户端不应该被迫依赖它不需要的接口方法。翻译成大白话就是,你设计接口时,别把所有人都用不上的方法也塞进去。
看一个典型例子,把“工人”抽象成一个接口:
public interface Worker { void work(); void eat(); void sleep(); }人实现这个接口没问题,因为人需要吃和睡。但假如系统里还有机器人工人:
public class RobotWorker implements Worker { @Override public void work() { System.out.println("机器人工作"); } @Override public void eat() { throw new UnsupportedOperationException("机器人不需要吃饭"); } @Override public void sleep() { throw new UnsupportedOperationException("机器人不需要睡觉"); } }这就是典型的胖接口。调用RobotWorker的人用不上eat和sleep,但仍然必须实现它们,甚至还要抛异常。以后如果有人遍历所有 Worker 调用eat(),机器人就崩了。
按接口隔离原则,应该拆开:
public interface Workable { void work(); } public interface Restable { void eat(); void sleep(); }人实现两个接口,机器人只实现 Workable。这样一来,每个客户端只依赖它真正需要的方法,接口的变化不会波及无辜的实现类。
需要注意,接口隔离和单一职责容易混淆。单一职责关注的是“一个类该不该做太多事”,接口隔离关注的是“调用方需要什么能力”。实践中,我通常先想清楚调用方视角:谁会调用这个接口?他需要哪些能力?把不同客户端需要的不同能力拆开,而不是把所有可能的操作都写进一个通用接口。
3. 开闭原则 + 依赖倒置原则:让扩展不靠“改老代码”
3.1 开闭原则:加功能尽量不动已稳定的代码
开闭原则是七大原则里最出名的:对扩展开放,对修改关闭。意思是当系统需要增加新功能时,应该通过新增代码来实现,而不是修改已经测试通过的旧代码。
支付场景是最常见的反例。很多项目一开始只有支付宝:
public class PaymentService { public void pay(String type, Order order) { if ("alipay".equals(type)) { // 调用支付宝 SDK } else if ("wechat".equals(type)) { // 调用微信支付 SDK } } }第一次加微信支付,你在这段代码里加了一个 else if。第二次加银联,又加一个。每加一种支付方式,都要重新测试整个方法。而且随着渠道增多,这个方法的循环复杂度会越来越高,最终没人敢动。
用开闭原则改造,把支付行为抽象出来:
public interface Payment { void pay(Order order); } public class AlipayPayment implements Payment { @Override public void pay(Order order) { // 支付宝支付逻辑 } } public class WechatPayment implements Payment { @Override public void pay(Order order) { // 微信支付逻辑 } }支付入口不再关心具体类型:
public class PaymentService { private Payment payment; public PaymentService(Payment payment) { this.payment = payment; } public void checkout(Order order) { payment.pay(order); } }以后新增一种支付方式,只需要新增一个类实现 Payment 接口,然后在组装处换一个实现即可。老的支付类和 PaymentService 都不需要改,这就是“对修改关闭,对扩展开放”。
但开闭原则不是免费的。提前抽象需要成本,而且你不可能预判到所有变化点。我个人的判断标准是:同一个功能点已经出现第二次不同形态时,再抽象也不迟。第一次写死,第二次开始抽象,第三次扩展收益就很明显。
3.2 依赖倒置:高层模块不要依赖低层模块
依赖倒置原则的核心是:高层模块不应该依赖低层模块,两者都应该依赖抽象;抽象不应该依赖细节,细节应该依赖抽象。
这个“倒置”不好理解。举一个老式代码:
public class OrderService { private JdbcOrderRepository jdbcOrderRepository = new JdbcOrderRepository(); public void saveOrder(Order order) { jdbcOrderRepository.save(order); } }问题很明显:OrderService 是高层业务逻辑,直接依赖了底层数据库实现 JdbcOrderRepository。以后想换成 Redis 缓存库、或者换成内存存储做测试,就得改 OrderService 里的 new。更麻烦的是,订单数据来自 MySQL 还是文件,这个决策不该由高层业务逻辑来做。
引入抽象:
public interface OrderRepository { void save(Order order); } public class JdbcOrderRepository implements OrderRepository { @Override public void save(Order order) { // 数据库保存 } }OrderService 不再依赖具体实现,而是依赖接口:
public class OrderService { private final OrderRepository orderRepository; public OrderService(OrderRepository orderRepository) { this.orderRepository = orderRepository; } public void saveOrder(Order order) { orderRepository.save(order); } }“倒置”倒在哪?原来 OrderService 直接指挥 JdbcOrderRepository,依赖方向是高层指向低层;现在低层 JdbcOrderRepository 反而要去实现 OrderService 依赖的接口,依赖方向反过来了。这是在写代码时最应该培养的直觉:不要在高层业务逻辑里去 new 一个具体低层组件,把具体选择权交出去。
依赖倒置原则带来的直接好处就是可测试性。测试时我可以传一个内存版的 OrderRepository,不需要启动数据库,也不用 mock 一堆静态方法。真实项目里 Spring 的依赖注入只是实现手段,原则本身和框架无关。
3.3 这两个原则组合起来的威力
开闭和依赖倒置经常一起出现。因为要做到“扩展开放”,通常就得让上层依赖抽象接口,而不是依赖具体类。比如一个订单处理流程,既有支付,又有通知:
public interface OrderNotifier { void notify(Order order); }支付成功后,OrderService 既不直接 new SmsNotifier,也不直接 new EmailNotifier,而是构造时传入一个 OrderNotifier。这样一来,通知方式的变化不会污染订单主逻辑,主逻辑也不会被具体通知渠道绑架。两者配合,才能让系统像插卡一样换实现。
4. 里氏替换原则 + 迪米特法则:继承与“朋友”之间都要有边界
4.1 里氏替换原则:子类必须能替换父类而不出错
里氏替换原则说:所有引用父类的地方,必须能透明地使用子类对象。替换后程序行为不能变坏。如果替换后行为异常,说明继承关系设计有问题。
最经典的例子是矩形和正方形。
public class Rectangle { private int width; private int height; public void setWidth(int width) { this.width = width; } public void setHeight(int height) { this.height = height; } public int getWidth() { return width; } public int getHeight() { return height; } } public class Square extends Rectangle { @Override public void setWidth(int width) { super.setWidth(width); super.setHeight(width); } @Override public void setHeight(int height) { super.setWidth(height); super.setHeight(height); } }数学上正方形是特殊的矩形,但继承之后就出事。看这段“修改高度并重新计算面积”的逻辑:
public class AreaCalculator { public int calculateArea(Rectangle rectangle) { rectangle.setWidth(5); rectangle.setHeight(10); return rectangle.getWidth() * rectangle.getHeight(); } }如果传入 Rectangle,结果是 50;如果传入 Square,因为 setHeight 把宽度也改成 10,结果是 100。出现了明显错误。问题就出在 Square 继承 Rectangle 时,改变了父类对 setWidth、setHeight 的行为约束,破坏了父类的不变量。
正确的建模方式是用组合而不是继承:
public class Square { private int side; public void setSide(int side) { this.side = side; } public int getSide() { return side; } }不要用继承去表达“正方形是一种矩形”,除非父类的所有行为约束都能安全适用于子类。里氏替换原则不只是理论,实践里有一个特别有效的检查方式:把父类写好的单元测试原封不动地拿到子类上跑一遍,如果挂了,或者需要改测试才能过,那就说明继承关系有问题。
4.2 迪米特法则:只和直接朋友说话
迪米特法则又叫最少知识原则。核心意思是:一个对象应该尽量少了解其他对象,只和自己的直接朋友通信。直接朋友包括:当前对象本身、通过参数传入的对象、成员变量指向的对象、方法返回的对象。
看一个反例,订单处理里直接掏用户钱包:
public class Wallet { private double balance; public void subtract(double amount) { balance -= amount; } } public class Customer { private Wallet wallet; public Wallet getWallet() { return wallet; } } public class OrderProcessor { public void process(Customer customer, double amount) { // 反例:越过 Customer,直接操纵它的内部钱包 customer.getWallet().subtract(amount); } }OrderProcessor 知道得太多了。它知道 Customer 内部有一个 Wallet,还知道 Wallet 有个 subtract 方法。一旦 Customer 内部存储改为支付宝账户,或者钱包换成了信用卡,OrderProcessor 就得跟着改。
修正方式是让 Customer 提供一个行为:
public class Customer { private Wallet wallet; public void pay(double amount) { wallet.subtract(amount); } }OrderProcessor 只需要调用:
public class OrderProcessor { public void process(Customer customer, double amount) { customer.pay(amount); } }这样 OrderProcessor 不知道 Customer 内部是钱包还是信用卡,耦合度大幅降低。
迪米特法则应用时特别容易走极端。我见过有人为了避免链式调用,给每一层都写了转发方法,结果类数量翻倍,代码绕来绕去更难读。实际落地时,我的经验是不禁止点号,但警惕“一对多”的链式访问,比如a.getB().getC().getD().pay()。超过两层就要问一下:这个中间对象是不是被我当成了传话筒?是不是该由更靠近数据的对象提供行为?
4.3 继承误用与过度中介化
里氏替换和迪米特法则都容易误用。前者容易让人“为了继承而继承”,把父子之间的关系只停留在概念层面;后者容易让人“为了解耦而解耦”,加一堆中介类。两个问题的根源是一样的:没有想清楚对象之间的真实关系。继承只能用在真正的 is-a 关系上,同时要保证父类的行为契约能被遵守;通信边界则要按职责来划分,不是按层级来划分。
实际开发里,我会把继承当成一种重活,每写一个 extends 都问自己:如果明天父类改一个方法签名,哪些子类会遭殃?如果答案是“很多且互不相同”,那大概率应该把公共行为抽取成接口或者组合对象。反过来,迪米特法则也不是让你完全不调用 getter,而是不要让外部对象根据 getter 结果去编排内部逻辑。
5. 合成复用原则:优先组合,而不是继承
5.1 为什么“继承复用”有坑
合成复用原则说:尽量使用对象组合而不是类继承来达到复用的目的。因为继承是静态的,子类在编译期就绑定了父类行为;而组合是动态的,可以在运行时替换行为。
继承复用的坑,用鸭子例子上最直观。假设一个游戏里有一堆鸭子。有的会飞,有的不会飞;有的会叫,有的不会叫。如果纯用继承,先定义一个鸭子基类:
public class Duck { public void fly() { System.out.println("飞"); } public void quack() { System.out.println("叫"); } }不会飞的鸭子就得重写 fly 的方法:
public class RubberDuck extends Duck { @Override public void fly() { // 什么都不做 } @Override public void quack() { System.out.println("橡皮鸭吱吱叫"); } }如果再来一种“会飞但不会叫”的鸭子,又要创建一个子类去重写 quack。当行为维度变多,类数量会爆炸。而且父类一旦加了新行为,所有子类都被迫响应,哪怕它们根本不需要。
5.2 用组合解决“类爆炸”
组合的思路是把变化的行为抽成接口,在鸭子里持有接口对象:
public interface FlyBehavior { void fly(); } public class FlyWithWings implements FlyBehavior { @Override public void fly() { System.out.println("用翅膀飞"); } } public class FlyNoWay implements FlyBehavior { @Override public void fly() { System.out.println("不会飞"); } }鸭子类不再自己实现飞行逻辑,而是把飞行行为“组合”进来:
public abstract class Duck { protected FlyBehavior flyBehavior; public void setFlyBehavior(FlyBehavior flyBehavior) { this.flyBehavior = flyBehavior; } public void performFly() { flyBehavior.fly(); } }以后要造一只“会飞但不会叫”的鸭子,不需要新建类,只需要在创建实例时给它传入 FlyWithWings,再配上合适的叫声行为即可。这样类数量不会爆炸,行为也可以运行时替换。
合成复用原则的关键是区分“有一个”和“是一个”。一只鸭子“有一个”飞行行为,所以该用组合;一个订单“是一个”支付请求,才适合用继承。大多数业务代码里,组合都比继承更灵活,因为组合不破坏封装,也不依赖父类内部实现。
5.3 什么时候仍然要用继承
组合优于继承不是“永远不要继承”。框架编程里继承仍然很重要。比如 Spring 的抽象类、模板方法模式中的基类,这些场景父类定义了稳定的算法骨架,子类只需要填充少量步骤。这种继承是安全的,因为父类行为契约稳定,且子类不会改变核心流程。
我的判断标准是三条:一是确确实实存在 is-a 关系;二是父类行为足够稳定,不需要频繁变化;三是子类不会暴力和父类对着干。如果三条都满足,继承可以省不少代码。如果有一条不满足,优先考虑组合。
6. 综合实战:用七大原则重构一个“订单通知模块”
6.1 原始代码的问题分析
把前面几条原则串起来,看一段典型的“能跑但难维护”的订单服务代码:
public class OrderService { public void checkout(Order order, String payType) { double total = order.getPrice() * order.getCount(); if (total > 100) { total = total * 0.9; } // 扣库存 // 保存订单到数据库 // 发短信 // 记日志 if ("alipay".equals(payType)) { // 支付宝 } else if ("wechat".equals(payType)) { // 微信 } } }这段代码至少违反五条原则。第一,一个方法里混了价格计算、库存、持久化、通知、日志、支付,节点太多,违反单一职责。第二,新加支付方式要改这个老方法,违反开闭原则。第三,OrderService 直接依赖具体数据库和短信客户端,违反依赖倒置。第四,如果以后有人在继承 OrderService 时改变了价格计算方式,替换后行为可能不一致,违反里氏替换原则的潜在风险。第五,通知逻辑直接写死,无法动态组合,违反合成复用原则。
6.2 重构后的设计结构
把职责拆开之后,整体设计可以是这样:
DiscountCalculator接口:负责价格计算,比如满减、会员折扣;Payment接口:负责支付渠道;OrderRepository接口:负责持久化;Notifier接口:负责用户通知。
订单主流程只编排流程,不关心细节:
public interface DiscountCalculator { double calculate(Order order); } public interface Payment { void pay(Order order); } public interface OrderRepository { void save(Order order); } public interface Notifier { void notify(Order order); }实现类按需要补充:
public class DefaultDiscountCalculator implements DiscountCalculator { @Override public double calculate(Order order) { double total = order.getPrice() * order.getCount(); if (total > 100) { total = total * 0.9; } return total; } } public class SmsNotifier implements Notifier { @Override public void notify(Order order) { // 发短信 } } public class EmailNotifier implements Notifier { @Override public void notify(Order order) { // 发邮件 } }最终订单服务变成:
public class OrderService { private final DiscountCalculator discountCalculator; private final Payment payment; private final OrderRepository orderRepository; private final Notifier notifier; public OrderService(DiscountCalculator discountCalculator, Payment payment, OrderRepository orderRepository, Notifier notifier) { this.discountCalculator = discountCalculator; this.payment = payment; this.orderRepository = orderRepository; this.notifier = notifier; } public void checkout(Order order) { double total = discountCalculator.calculate(order); order.setFinalPrice(total); orderRepository.save(order); payment.pay(order); notifier.notify(order); } }6.3 重构后如何扩展
现在加一种新的通知方式,比如站内信,只需要新增一个InAppNotifier实现 Notifier,然后在组装 OrderService 时换掉或组合进去,一点也不碰 OrderService。
加一种新的支付方式,也只需要新增一个 Payment 实现类。加新的折扣策略,额外加一个 DiscountCalculator 实现就可以。如果某个实现想复用已有的通知能力,可以把多个 Notifier 组合成一个 CompositeNotifier:
public class CompositeNotifier implements Notifier { private final List<Notifier> notifiers; public CompositeNotifier(List<Notifier> notifiers) { this.notifiers = notifiers; } @Override public void notify(Order order) { for (Notifier notifier : notifiers) { notifier.notify(order); } } }这种组合方式比“在 SmsNotifier 里再调用 EmailNotifier”要干净得多。OrderService 自始至终只知道一个 Notifier,不需要知道里面有短信还是邮件还是站内信。这让单元测试也变得非常简单:测订单流程时传一个不做事的内存 Notifier,测短信时单独测 SmsNotifier 即可。
6.4 设计原则带来的可维护性变化
重构之后再看最初的问题。加支付方式时,不需要再改老的checkout方法;改短信渠道时,不影响订单计算;想换数据库,只动 OrderRepository 实现。每个类都只跟一个稳定的抽象协作,继承和组合边界也清楚。
这个例子基本可以把七大原则全部落地一遍:单一职责负责拆类,开闭负责扩展路径,依赖倒置负责高层和低层解耦,接口隔离保证 Notifier 和 Payment 各自独立,里氏替换通过接口实现而不是继承来规避行为破坏,迪米特法则让 OrderService 只跟四个抽象朋友协作,合成复用则体现在 CompositeNotifier 和策略组合上。说到底,这些原则不是互相孤立的,它们最后都在为一个目标服务:让代码在需求变来变去时,保持稳定和可预测。
7. 常见问题与排查技巧实录
7.1 考试和面试里常问的几个点
软考中级软件设计师和不少公司面试,都喜欢拿设计原则做文章。常见问法无非几种。
一种是给你一段代码,问违反了哪些原则。比如一个类里有业务逻辑又有数据库访问,那么答案通常是单一职责原则;一个方法里用 if-else 判断支付类型,那开闭原则大概率被违反;高层 Service 直接 new 低层 DAO,那就是依赖倒置原则的问题。这类题目本质上是考验你能否识别坏味道,而不是背原则定义。
另一种是对比题,比如“依赖倒置和开闭原则有什么区别”,或者“里氏替换原则在实际中怎么落地”。我的回答思路是:开闭关注扩展方式,依赖倒置关注依赖方向,里氏替换关注继承安全。各说一个例子,比单纯背定义有说服力得多。
还有一种情况,我看到有些备考者把“单一职责”和“接口隔离”弄混,急着背定义。其实只要记住,单一职责是类层面的,接口隔离是调用方视角的。这两个在面试里被单独拎出来的概率挺高,最好准备一个能同时说明两者的例子,比如前面那个 Worker 例子,就可以两处使用。
7.2 实际工作里的高频坑
原则听着好,用起来容易出问题。我踩过的坑和观察到的坑有这几个:
第一个坑是接口隔离过头。一个业务对象拆了十几个接口,每个接口只有一个方法,实现类 implements 一大堆。接口数量多到没人记得住,反而增加理解成本。正确做法是只对变化点拆,稳定的公共能力没必要拆太碎。
第二个坑是单一职责过度拆分。有的团队把“用户注册”拆成五个类,每个类只有一个方法,方法之间还得互相依赖。代码是听话了,但人看着崩溃。单一职责拆的是修改理由,不是把每个操作都拆成单独类。
第三个坑是盲目追求开闭。新项目刚开始,需求还不稳定,就急着为所有逻辑加接口、加工厂。等需求一变化,发现抽象层自己也要大改,抽象反而成了负担。我的习惯是让代码先简单跑起来,第二次出现相同变化点时再抽象。
第四个坑是里氏替换“能跑就当没事”。很多继承问题不是运行时马上报错,而是某个边界条件下行为不一致。比如子类重写父类方法时悄悄忽略了参数校验。排查这种问题最好的办法,是把父类的测试用例直接套到子类上跑。
第五个坑是迪米特法则变成了“层层转发”。每次要调一个方法,就加一个中间方法,最后形成一条很长的调用链。这没有降低耦合,只是把耦合藏得更深。迪米特法则关注的是“不要让外部对象操作内部细节”,不是“禁止一切跨层访问”。
第六个坑是合成复用变成了“凡是继承都反对”。框架里的模板方法、抽象基类其实还是在用继承,只是用得克制。不要为了“组合优于继承”这句话,把所有抽象基类都改成接口加组合,那样往往会丢失算法骨架复用的好处。
7.3 如何让七大原则平稳落地
想让这些原则真正改善代码,而不是变成口头禅,有四个实操建议。
第一,先写测试再重构。没有测试兜底,任何设计原则都是空谈。哪怕只是抽取一个方法,也要先确认现有行为被测试锁住。
第二,从坏味道出发找重构点。模型理不清时,先别急着套原则。看看有哪些类既算价格又发消息,哪些方法有超过三层以上的 getter 链路,哪些 if-else 分支还要继续加。找到了坏味道,再决定用哪条原则去修。
第三,把原则当成 code review 的检查项。Review 时看到 new 一个具体实现、看到子类重写父类后改变父类约束、看到接口里有无人使用的方法,都可以当场指出来。慢慢地,团队就会形成共同的设计语言。
第四,结合设计模式一起理解。策略模式的核心就是开闭原则和依赖倒置,模板方法模式是继承复用的典范,装饰器模式是组合复用和开闭原则结合的例子,适配器模式又常和接口隔离有关。模式是原则的具体招式,原则是模式背后的心法,两者一起学,理解会快很多。
我在实际项目里最深的一个体会是:设计原则不是写代码时束缚手脚的枷锁,而是重构时照亮方向的灯。没有灯的时候,新功能总是不知道往哪里放;有了灯,你至少知道哪条路更稳。每当你面对“又要加需求了,老代码却无从下手”的时候,想想这七条,绝大多数卡点都能找到出口。