面向对象的 SOLID 原则
隐藏对象的属性和实现细节,通过对外暴露的接口控制程序中属性的读写。
封装需要把所有属性私有化,对每个属性提供 getter 和 setter 方法。如果有一个带参的构造函数的话,还需要写一个不带参的构造函数。
通过继承可以实现代码的复用。
缺点是提高了类之间的耦合性。
基于对象所属类的不同,外部对同一个方法的调用,实际执行的逻辑不同。
要想实现多态,需要在继承+覆盖或重载。
面向对象的 SOLID 设计原则:
一个类只做一件事。
比如订单和账单,都包括订单号、创建时间等信息,但是如果只用一个类来表达,会导致:
实体应该对扩展是开放的,对修改是封闭的。即可扩展而不可修改。哪里可能变化,就在哪里抽象。
例如,对于支付场景,如果将多种支付方式整合到一个类中,每次添加一种新的支付方式都需要变更这个类,并可能导致已有的支付方式受到影响而无法使用。可以将其抽象为抽象类或接口,每种支付方式继承抽象类或实现接口。
interface PayProcessor {
Result handle(Param param);
}
public class AlipayProcessor implements PayProcessor {
...
}
public class WeChatPayProcessor implements PayProcessor {
...
}
在对象出现的任何地方,都可以用其子类实例进行替换,而不会导致程序错误,且软件功能不受影响。
对于继承关系,只有满足里式替换原则才算合理。里氏替换原则的关键在于不能覆盖父类的非抽象方法。父类实现好的方法设定了一系列规范和契约,虽然它不强制要求所有的子类必须遵从这些规范,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。
例如,B 在继承 A 时,修改了其非抽象方法:
class A {
public int func1(int a, int b) {
return a-b;
}
}
class B extends A {
public int func1(int a, int b) {
return a+b;
}
public int func2(int a, int b) {
return func1(a,b)+100;
}
}
public class Client {
public static void main(String[] args) {
A a = new A();
System.out.println("100-50="+a.func1(100, 50));
B b = new B();
System.out.println("100-50="+b.func1(100, 50));
System.out.println("100+20+100="+b.func2(100, 20));
}
}
客户不应被强迫依赖它不使用的方法。一个类实现的接口中,包含了它不需要的方法。将接口拆分成更小和更具体的接口,有助于解耦,从而更容易重构、更改。
例如,假设对于支付接口,支付宝支持收费和退费,微信只支持收费。如果只抽象一个支付接口,则必须包括收费和退费两个方法:
interface PayChannel {
void charge();
void refund();
}
class AlipayChannel implements PayChannel {
public void charge() {
...
}
public void refund() {
...
}
}
class WeChatChannel implements payChannel {
public void charge() {
...
}
public void refund() {
// 没有任何代码
}
}
此时微信不得不将退费实现成空方法。
要满足接口隔离原则,可以将支付接口拆成各包含一个方法的收费和退费两个接口。支付宝需要同时实现这两个接口,而微信只需要实现收费接口。
可以用于将业务和底层模块解耦(例如底层逻辑经常变化,或预计会在一段时间后替换底层模块)。
Button 对象用于感知外部环境。Lamp 对象用于影响外部环境。
下图直接使用继承。此时 Lamp 对象的任何修改都会影响到 Button 对象。这个模型可以通过反转 Lamp 对象的依赖性来改进。
public class Button {
private Lamp lamp;
public void Poll() {
if (/*some condition*/) {
lamp.TurnOn();
}
}
}
下图使用了依赖倒置原则。Button 对象现在拥有一个与 ButtonServer 相关的关联,它提供了 Button 可以用来打开或关闭某些东西的接口。Lamp 实现 ButtonServer 接口。此时,Button 对象可以控制所有实现了 ButtonServer 接口的对象,更灵活,同时还可以控制目前尚未创建的对象。
假设目前使用 MySQL,但是后面可能换成其它数据库。针对 MySQL 的实现类 MySQLDBUtil 编程的话,后续假设又创建了 OracleDBUtil,则替换的工作量相当大。而如果加一个抽象层接口 DBUtil,所有的数据库类(MySQLDBUtil、OracleDBUtil)都实现 MySQLDBUtil 接口,就可以轻松替换。
新设计,增加抽象层接口。注意,这里用关联关系代替了继承关系,具体参考下面的合成复用原则:
合成复用原则(Composite Reuse Principle, CRP):尽量使用对象组合和聚合关系,而不是继承。组合和聚合关系可以降低类之间的耦合度。如果使用继承,需要严格遵循里氏替换原则。
数据库工具类:
class DBUtil {
}
继承关系:
class UserDAO extends DBUtil {
...
}
聚合关系:
class UserDAO {
private DBUtil util;
...
}
迪米特法则又叫最少知道原则:一个对象应该对其他对象保持最少的了解。
迪米特法则特点:
举个例子:连长、班长、士兵之间布置任务,连长只需要传达给其直接下属班长,班长再传达给其负责的士兵(下图左侧示例)。如果违反了迪米特法则,将导致混乱及高耦合度(下图右侧示例)。