面向对象设计原则
设计类时先看职责:一个类只负责一块变化原因。职责单一不是「只能有一个方法」,而是「不把互不相关的事堆在一起」。订单算价和邮件通知就该拆开,否则改邮件文案也要动算价代码,谈不上高内聚。
下面几条是常见的面向对象原则,合称时往往说 SOLID,再加上组合复用和迪米特。它们不是教条,是改需求时少踩坑的方向。
单一职责原则(SRP)
一个类只有一个引起它变化的原因。
反例:UserService 里既做登录校验,又拼 HTML 邮件,又写 Excel。登录规则一变,邮件和导出都得回归。拆成 AuthService、MailSender、UserExporter 后,每一块可以单独测、单独换。
开放-封闭原则(OCP)
对扩展开放,对修改封闭。已经测过的代码,能不改就不改;新行为用加新类、新实现来接。
反例:每多一种支付,就在 if (type == ...) 里加一枝。改成 PayChannel 接口,支付宝、微信各自实现,新渠道只加类,旧分支不用再打开。改完仍要回归,但回归范围是新类,不是整棵 if 树。
里氏替换原则(LSP)
子类必须能出现在任何使用父类的地方,行为不让调用方吃惊。
「子类可以代父类」不是「子类随便重写」。如果父类 Bird.fly(),再弄一个 Ostrich 在 fly() 里抛异常,所有按「鸟能飞」来写的代码都会坏。这时不该硬继承,该拆模型:会飞的是 FlyingBird,鸵鸟不是它的子类。
重写时不要削弱前置条件、也不要偷偷改方法含义。父类说「传入正数」,子类不能改成「只能传偶数」。
依赖倒置原则(DIP)
依赖抽象,不要依赖具体实现。高层模块和低层模块都对接口负责。
反例:OrderService 里直接 new MysqlOrderRepo()。库一换,服务也得改。改成依赖 OrderRepository,具体 MySQL / 内存实现在组装处注入。Spring 的构造器注入就是在落实这条。
「面向接口编程」说的是调用方认接口;实现类当然还是具体的,只是别把具体类型写进业务核心。
接口隔离原则(ISP)
用多个小接口,胜过一个什么方法都有的胖接口。
反例:Animal 上同时有 fly()、swim()、run(),鱼和狗都被迫空实现。拆成 Flyable、Swimmable,需要谁实现谁。调用方也只依赖自己用得到的那几个方法,实现类少受牵连。
组合复用原则(CRP)
优先用组合获得复用,而不是为了复用去继承。
继承是最强耦合:子类绑死父类的实现和生命周期,父类一改,所有子类都可能裂。需要「有一份」能力时,把能力做成成员对象。ArrayList 内部组合数组,而不是继承数组;BufferedInputStream 包一层 InputStream,而不是继承某个具体文件流。
继承仍然适合真正的「is-a」,而且父类稳定。只是先问一句:是「是一种」,还是「用到一个」。
迪米特原则(LoD / 最少知识)
一个对象应该少知道别的对象内部。只和直接朋友说话:自己的字段、方法参数、自己创建的对象。
反例:a.getB().getC().getD().doSomething()。A 必须了解 B、C 的结构,中间任一层一改,A 就崩。让 B 提供一个有意义的方法,把「找 D 办事」藏起来。
依赖越多,耦合越高,模块越搬不走。不是禁止调用,是禁止穿过别人的内衣去摸口袋。
怎么一起用
接到一个类已经又长又难改时,可以按这个顺序问:
- 它是不是在为两件无关的事变化? → SRP
- 加需求是不是总在改旧 if? → OCP、DIP
- 子类是不是让父类契约失效? → LSP
- 接口是不是逼着实现用不到的方法? → ISP
- 复用是不是靠一串 extends? → 组合
- 有没有
getX().getY().getZ()? → 迪米特
原则是为了让变化局部化。生搬六条写出一堆空接口,也是另一种过度设计。