【设计模式】设计原则之S.O.L.I.D 五原则

SOLID 原则

S:单一职责原则 SRP(Single Responsibility Principle)
O:单一职责原则 OCP(Open Close Principle)
L:里氏替换原则 LSP(Liskov Substitution Principle)
I:接口隔离原则 ISP(Interface Segregationi Principle)
D:依赖反转原则 DIP(Dependency Inversion Principle)

1. 单一职责原则 SRP(Single Responsibility Principle)

1.1 定义

一个类或者模块只负责完成一个职责(或者功能)。

1.2 可以从哪几个维度判断当前类设计是否符合单一职责

public class UserInfo {
  private long userId;
  private String username;
  private String email;
  private String telephone;
  private long createTime;
  private long lastLoginTime;
  private String avatarUrl;
  private String provinceOfAddress; // 省
  private String cityOfAddress; // 市
  private String regionOfAddress; // 区 
  private String detailedAddress; // 详细地址
  // ...省略其他属性和方法...
}

从不同的应用场景

拿用户数据来说,如果只是简单的作展示,那么把地址等信息全部放在 UserInfo 中是符合单一职责的。

如果除了作展示外,比如:地址信息还会被用到物流等业务中去,那么,地址相关的信息就应该从 UserInfo 中抽离出来形成单独的类。

从不同阶段的需求背景

在当前的需求背景下,可能 UserInfo 是满足单一职责的;而随着业务的发展,新的需求使得 UserInfo 不在满足单一职责,如:业务需要实现进行统一鉴权功能,那么跟鉴权相关的字段(手机号、邮箱,用户名等)就需要抽离出来形成新的类才能满足新需求下,单一职责的类设计。

从不同的业务层面

如果你从“用户”这个业务层面来看,UserInfo 是满足单一职责要求的。

如果你从更加细分的“用户展示信息”、“地址信息”、“登录认证信息”等业务层面来看,UserInfo 是就需要继续进行拆分。

1.3 防止过度设计的思路

在设计过程中,我们可以先写一个相对粗粒度的类,满足业务需求。如果粗粒度的类越来越大,代码越来越多的时候,我们可以将粗粒度的类拆分成几个更细粒度的类。通过重构来完成这个操作。

1.4 判断类职责是否单一的几点原则

  1. 类中的代码行数、函数或属性过多,会影响代码的可读性和可维护性
  2. 类依赖的其它类过多,或者依赖类的其它类过多,不符合高内聚、低耦合的设计思想
  3. 私有方法过多,需要考虑能否将私有方法独立到新类中,设置 public 方法,供更多的类使用,从而提高代码的复用性
  4. 比较难取名字的类,一般只能用一些比较笼统的 Manager、Context 之类的名词来命名
  5. 类中大量方法都是集中操作类中的某几个属性,如:UserInfo 中很多方法都只是在操作和地址相关的字段

1.5 类的职责是否越单一越好

/**
 * Protocol format: identifier-string;{gson string}
 * For example: UEUEUE;{"a":"A","b":"B"}
 */
public class Serialization {
  private static final String IDENTIFIER_STRING = "UEUEUE;";
  private Gson gson;
  
  public Serialization() {
    this.gson = new Gson();
  }
  
  public String serialize(Map<String, String> object) {
    StringBuilder textBuilder = new StringBuilder();
    textBuilder.append(IDENTIFIER_STRING);
    textBuilder.append(gson.toJson(object));
    return textBuilder.toString();
  }
  
  public Map<String, String> deserialize(String text) {
    if (!text.startsWith(IDENTIFIER_STRING)) {
        return Collections.emptyMap();
    }
    String gsonStr = text.substring(IDENTIFIER_STRING.length());
    return gson.fromJson(gsonStr, Map.class);
  }
}

更加满足单一职责的版本

public class Serializer {
  private static final String IDENTIFIER_STRING = "UEUEUE;";
  private Gson gson;
  
  public Serializer() {
    this.gson = new Gson();
  }
  
  public String serialize(Map<String, String> object) {
    StringBuilder textBuilder = new StringBuilder();
    textBuilder.append(IDENTIFIER_STRING);
    textBuilder.append(gson.toJson(object));
    return textBuilder.toString();
  }
}

public class Deserializer {
  private static final String IDENTIFIER_STRING = "UEUEUE;";
  private Gson gson;
  
  public Deserializer() {
    this.gson = new Gson();
  }
  
  public Map<String, String> deserialize(String text) {
    if (!text.startsWith(IDENTIFIER_STRING)) {
        return Collections.emptyMap();
    }
    String gsonStr = text.substring(IDENTIFIER_STRING.length());
    return gson.fromJson(gsonStr, Map.class);
  }
}

更加单一职责的版本带来的问题:

  1. 代码的内聚性没有原来高。当修改协议后,或者有其它改变后,Serializer 和 Deserializer 两个类都需要修改
  2. 容易引发新的问题。如:当协议修改后,我们只修改了一个类,而忘记修改其它类,这样会导致程度出错,代码的可维护性变差

总的来说,单一职责主要是避免不相关的功能耦合在一起,提高类的内聚性的。但如果拆分的过细,反倒会降低内聚性,也会影响代码的可读性。

2. 开闭原则 OCP(Open Close Principle)

2.1 定义

软件实体(模块、类、方法等)应该“对扩展开放,对修改关闭”。
也就是在添加新的功能的时候,在已有代码基础上扩展代码(新增模块、类或方法等),而非修改已有代码(修改模块、类或方法等)。

2.2 开闭原则改造的例子

public class Alert {
  private AlertRule rule;
  private Notification notification;

  public Alert(AlertRule rule, Notification notification) {
    this.rule = rule;
    this.notification = notification;
  }

  public void check(String api, long requestCount, long errorCount, long durationOfSeconds) {
    long tps = requestCount / durationOfSeconds;
    if (tps > rule.getMatchedRule(api).getMaxTps()) {
      notification.notify(NotificationEmergencyLevel.URGENCY, "...");
    }
    if (errorCount > rule.getMatchedRule(api).getMaxErrorCount()) {
      notification.notify(NotificationEmergencyLevel.SEVERE, "...");
    }
  }
}

未改造前,添加新功能

public class Alert {
  // ...省略AlertRule/Notification属性和构造函数...
  
  // 改动一:添加参数timeoutCount
  public void check(String api, long requestCount, long errorCount, long timeoutCount, long durationOfSeconds) {
    long tps = requestCount / durationOfSeconds;
    if (tps > rule.getMatchedRule(api).getMaxTps()) {
      notification.notify(NotificationEmergencyLevel.URGENCY, "...");
    }
    if (errorCount > rule.getMatchedRule(api).getMaxErrorCount()) {
      notification.notify(NotificationEmergencyLevel.SEVERE, "...");
    }
    // 改动二:添加接口超时处理逻辑
    long timeoutTps = timeoutCount / durationOfSeconds;
    if (timeoutTps > rule.getMatchedRule(api).getMaxTimeoutTps()) {
      notification.notify(NotificationEmergencyLevel.URGENCY, "...");
    }
  }
}

该修改一共有两处改动,且都是在原有核心业务方法中添加新的参数或逻辑,这种操作显然是不符合开闭原则的。

改造之后的类组成

public class Alert {
  private List<AlertHandler> alertHandlers = new ArrayList<>();
  
  public void addAlertHandler(AlertHandler alertHandler) {
    this.alertHandlers.add(alertHandler);
  }

  public void check(ApiStatInfo apiStatInfo) {
    for (AlertHandler handler : alertHandlers) {
      handler.check(apiStatInfo);
    }
  }
}

public class ApiStatInfo {//省略constructor/getter/setter方法
  private String api;
  private long requestCount;
  private long errorCount;
  private long durationOfSeconds;
}

public abstract class AlertHandler {
  protected AlertRule rule;
  protected Notification notification;
  public AlertHandler(AlertRule rule, Notification notification) {
    this.rule = rule;
    this.notification = notification;
  }
  public abstract void check(ApiStatInfo apiStatInfo);
}

public class TpsAlertHandler extends AlertHandler {
  public TpsAlertHandler(AlertRule rule, Notification notification) {
    super(rule, notification);
  }

  @Override
  public void check(ApiStatInfo apiStatInfo) {
    long tps = apiStatInfo.getRequestCount()/ apiStatInfo.getDurationOfSeconds();
    if (tps > rule.getMatchedRule(apiStatInfo.getApi()).getMaxTps()) {
      notification.notify(NotificationEmergencyLevel.URGENCY, "...");
    }
  }
}

public class ErrorAlertHandler extends AlertHandler {
  public ErrorAlertHandler(AlertRule rule, Notification notification){
    super(rule, notification);
  }

  @Override
  public void check(ApiStatInfo apiStatInfo) {
    if (apiStatInfo.getErrorCount() > rule.getMatchedRule(apiStatInfo.getApi()).getMaxErrorCount()) {
      notification.notify(NotificationEmergencyLevel.SEVERE, "...");
    }
  }
}

主要有两点改造:

  1. 将之间耦合在一起的业务逻辑的处理,都抽离到对应的类中,让每个类只处理一种情况的业务逻辑,符合单一职责
  2. 使用对象对函数参数进行了统一封装,让后续的改动不会再引发函数参数的改变

改造之后,添加新功能

public class ApplicationContext {
  private AlertRule alertRule;
  private Notification notification;
  private Alert alert;
  
  public void initializeBeans() {
    alertRule = new AlertRule(/*.省略参数.*/); //省略一些初始化代码
    notification = new Notification(/*.省略参数.*/); //省略一些初始化代码
    alert = new Alert();
    alert.addAlertHandler(new TpsAlertHandler(alertRule, notification));
    alert.addAlertHandler(new ErrorAlertHandler(alertRule, notification));
  }
  public Alert getAlert() { return alert; }

  // 饿汉式单例
  private static final ApplicationContext instance = new ApplicationContext();
  private ApplicationContext() {
    initializeBeans();
  }
  public static ApplicationContext getInstance() {
    return instance;
  }
}

public class Alert { // 代码未改动... }
public class ApiStatInfo {//省略constructor/getter/setter方法
  private String api;
  private long requestCount;
  private long errorCount;
  private long durationOfSeconds;
  private long timeoutCount; // 改动一:添加新字段
}
public abstract class AlertHandler { //代码未改动... }
public class TpsAlertHandler extends AlertHandler {//代码未改动...}
public class ErrorAlertHandler extends AlertHandler {//代码未改动...}
// 改动二:添加新的handler
public class TimeoutAlertHandler extends AlertHandler {//省略代码...}

public class ApplicationContext {
  private AlertRule alertRule;
  private Notification notification;
  private Alert alert;
  
  public void initializeBeans() {
    alertRule = new AlertRule(/*.省略参数.*/); //省略一些初始化代码
    notification = new Notification(/*.省略参数.*/); //省略一些初始化代码
    alert = new Alert();
    alert.addAlertHandler(new TpsAlertHandler(alertRule, notification));
    alert.addAlertHandler(new ErrorAlertHandler(alertRule, notification));
    // 改动三:注册handler
    alert.addAlertHandler(new TimeoutAlertHandler(alertRule, notification));
  }
  //...省略其他未改动代码...
}

public class Demo {
  public static void main(String[] args) {
    ApiStatInfo apiStatInfo = new ApiStatInfo();
    // ...省略apiStatInfo的set字段代码
    apiStatInfo.setTimeoutCount(289); // 改动四:设置tiemoutCount值
    ApplicationContext.getInstance().getAlert().check(apiStatInfo);
}

重构之后,代码更加灵活和可扩展。如果需要添加新的告警逻辑,核心是只需要添加对应的 Handler 新逻辑处理类即可。不需要再去修改之前的 check() 中的逻辑。

同时,只需要对新添加的 handler 添加单元测试代码测试,而老的单元测试没有被改动,不需要再修改。

2.3 修改代码就意味着违背开闭原则吗?

改动一:为 ApiStatInfo 添加 timeoutCount

实际上,开闭原则可以应用到不同粒度的代码中,可以是模块、可以是类还可以是方法(及其属性)。同样一个代码改动,在粗代码粒度下,会被认为是修改;在细粒度下,会被认为是扩展。如:在类这个粒度层面来看,添加了 timeoutCount 这个属性,会被认为是修改;而在方法或属性的这粒度层面来看,由于并没有修改已有属性或方法,反而会被认为是扩展。

开闭原则的设计初衷是:只要它没有破坏原有代码的运行,没有破坏原有的单元测试,就可以被认定为一个合理的改动。

改动三和四:在方法内部添加代码

这种改动,无论在哪个粒度上来说,都是对原有代码的修改操作。而这种改动是否是可接受的呢?

我们要认识到,添加一个新功能,不可能任何模块、类、方法的代码都不被“修改”。我们的原则是:尽量让修改更集中、更少、更上层,尽量让最核心、最复杂的部分逻辑代码满足开闭原则。

对于上面的例子中,最核心的逻辑是和告警及其处理相关的功能(主要是 Alert 和 Handler 类)。如果把这部分的代码看能一个模块的话,该模块在添加新功能时,是符合开闭原则的。

2.4 如何做到符合“对扩展开放、对修改关闭”

最常用来提高代码可扩展性的方法有:多态、依赖注入、基于接口而非实现,以及部分设计模式(如:装饰、策略、模板、职责链和状态等)等。

// 这一部分体现了抽象意识
public interface MessageQueue { //... }
public class KafkaMessageQueue implements MessageQueue { //... }
public class RocketMQMessageQueue implements MessageQueue {//...}

public interface MessageFromatter { //... }
public class JsonMessageFromatter implements MessageFromatter {//...}
public class ProtoBufMessageFromatter implements MessageFromatter {//...}

public class Demo {
  private MessageQueue msgQueue; // 基于接口而非实现编程
  public Demo(MessageQueue msgQueue) { // 依赖注入
    this.msgQueue = msgQueue;
  }
  
  // msgFormatter:多态、依赖注入
  public void sendNotification(Notification notification, MessageFormatter msgFormatter) {
    //...    
  }
}

2.5 如何在项目中灵活运用开闭原则

  1. 对于一些比较确定的、短期内可能扩展,或者需求改动对代码结构影响比较大的情况,或者实现成本不高的扩展点,在编写代码的时候,我们是可以事先做好可扩展性设计的
  2. 对于一些未来不确定是否需要支持的需求,或者实现起来比较复杂的扩展点,可以等到后续需求来驱动,通过重构的方式支持可扩展的需求

2.6 开闭原则可能引发的问题

与代码可读性之间的冲突

一般情况下,重构之后符合开闭原则的代码,要比之前的代码复杂得多,理解起来也更加的有难度。

很多时候,需要在可扩展性和可读性之间做权衡。在某些情况下,可扩展性更重要,就牺牲一下可读性;而另一些时候,可可读性更重要,则需要牺牲一下可扩展性。

比如:如果核心逻辑中的判断逻辑不是很多,也就是功能不是很多的情况下,完全可以只使用 if-else 的方法来完成功能。相反,如果核心逻辑的判断逻辑比较多,本身的可读性和可维护性就会变差。此时,就需要将代码逻辑抽离出来,基于扩展性的思路来进行改造。

3. 里氏替换原则 LSP(Liskov Substitution Principle)

3.1 定义

能使用父类的地方,都可以用子类进行替换,并且保证原有程序的逻辑行为不变及正确性不被破坏。

主要用于继承关系中子类该如何设计的一个原则。

3.2 示例代码

public class Transporter {
  private HttpClient httpClient;
  
  public Transporter(HttpClient httpClient) {
    this.httpClient = httpClient;
  }

  public Response sendRequest(Request request) {
    // ...use httpClient to send request
  }
}

public class SecurityTransporter extends Transporter {
  private String appId;
  private String appToken;

  public SecurityTransporter(HttpClient httpClient, String appId, String appToken) {
    super(httpClient);
    this.appId = appId;
    this.appToken = appToken;
  }

  @Override
  public Response sendRequest(Request request) {
    if (StringUtils.isNotBlank(appId) && StringUtils.isNotBlank(appToken)) {
      request.addPayload("app-id", appId);
      request.addPayload("app-token", appToken);
    }
    return super.sendRequest(request);
  }
}

public class Demo {    
  public void demoFunction(Transporter transporter) {    
    Reuqest request = new Request();
    //...省略设置request中数据值的代码...
    Response response = transporter.sendRequest(request);
    //...省略其他逻辑...
  }
}

// 里式替换原则
Demo demo = new Demo();
demo.demofunction(new SecurityTransporter(/*省略参数*/););

3.3 面向对象多态和里氏替换区别

从定义描述和代码实现上来看,多态和里氏替换,但它们的关注角度是不一样的。

多态是面向对象编程的一大特性,也是面向对象编程语言的一种语法。它是一种代码层面的实现思路。

而里氏替换是一种设计原则,是用来指导继承关系中,子类该如何设计。子类的设计要保证在替换父类的时候,不改变原有程序的逻辑以及不破坏原有逻辑的正确性。

// 改造前:
public class SecurityTransporter extends Transporter {
  //...省略其他代码..
  @Override
  public Response sendRequest(Request request) {
    if (StringUtils.isNotBlank(appId) && StringUtils.isNotBlank(appToken)) {
      request.addPayload("app-id", appId);
      request.addPayload("app-token", appToken);
    }
    return super.sendRequest(request);
  }
}

// 改造后:
public class SecurityTransporter extends Transporter {
  //...省略其他代码..
  @Override
  public Response sendRequest(Request request) {
    if (StringUtils.isBlank(appId) || StringUtils.isBlank(appToken)) {
      throw new NoAuthorizationRuntimeException(...);
    }
    request.addPayload("app-id", appId);
    request.addPayload("app-token", appToken);
    return super.sendRequest(request);
  }
}

像上面这段改造后的代码,在使用时,是完全符合多态特性的,但是不满足里氏替换原则,为什么呢?

因为如果将父类替换成子类后,原本正常运行的代码,会抛出异常,导致代码无法正常运行。

3.4 哪些代码明显违反了 LSP?

里氏替换原则有一个更具指导意义的描述:按照协议来设计。也就是子类在设计的时候,要遵守父类的行为约定(协议)。子类只可以改变函数内部的实现逻辑,而不能改变函数原有的约定。这里主要包括:1. 函数声明要实现的功能;2. 对输入、输出、异常的规定;3. 注释中所罗列的任何特殊说明。

1. 子类违背父类声明要实现的功能

比如:父类定义的函数为 sortByAmount() 按照订单金额来进行排序,而子类使用的是按照订单生成日期来进行排序。

*2. 子类违背父类对输入、输出、异常的约定

比如:父类定义运行出错返回 null,数据为空返回空集合;而子类在运行出错后抛出异常,在数据为空返回 null。

3. 子类违背父类注释中所罗列的任何特殊说明

比如:对于提现功能来说,父类在注释中说明了:用户提现金额不能大于账户余额。而子类在实现中,对 VIP 客户实现了透支功能。

3.5 验证子类的设计是否符合里氏替换原则

使用父类的单元测试去验证子类的代码。如果某些单元测试运行失败,则有可能说明,子类的设计是没有完成遵守父类的约定,子类违反了里氏替换原则的。

4. 接口隔离原则 ISP(Interface Segregation Principle)

4.1 定义

客户端不应该被强迫依赖它不需要的接口。其中,客户端可以理解为接口的调用者或者使用者。

4.2 接口的理解

  1. 一组 API 接口集合
  2. 单个 API 接口或函数
  3. OOP 中接口的概念

1. 一组 API 接口集合

public interface UserService {
  boolean register(String cellphone, String password);
  boolean login(String cellphone, String password);
  UserInfo getUserInfoById(long id);
  UserInfo getUserInfoByCellphone(String cellphone);
}

public class UserServiceImpl implements UserService {
  //...
}

增加删除用户接口导致的问题

如果我们继续将 deleteById()deleteUserByCellphone() 接口直接添加到 UserService 接口中,会存在安全隐患。比如:这个删除用户的操作只有管理台才能使用,而如果将该方法直接添加到 UserService 接口后,其它没有被授权的系统也可以调用该方法。

使用 ISP 进行改造

public interface UserService {
  boolean register(String cellphone, String password);
  boolean login(String cellphone, String password);
  UserInfo getUserInfoById(long id);
  UserInfo getUserInfoByCellphone(String cellphone);
}

public interface RestrictedUserService {
  boolean deleteUserByCellphone(String cellphone);
  boolean deleteUserById(long id);
}

public class UserServiceImpl implements UserService, RestrictedUserService {
  // ...省略实现代码...
}

主要是将存在安全隐患的接口,通过创建单独的接口从原来的接口中隔离出来,避免被其它系统滥用。

如果部分接口只被部分调用者使用,那就需要将这部分接口隔离出来,单独给对应的调用者使用,而不是强迫其它调用者也依赖这部分用不到的接口。

2. 单个接口或函数

接口功能的定义或函数的实现在设计上要功能单一,不要将多个不同的功能逻辑在一个函数中实现。

public class Statistics {
  private Long max;
  private Long min;
  private Long average;
  private Long sum;
  private Long percentile99;
  private Long percentile999;
  //...省略constructor/getter/setter等方法...
}

public Statistics count(Collection<Long> dataSet) {
  Statistics statistics = new Statistics();
  //...省略计算逻辑...
  return statistics;
}

通过 ISP 原则改造后:

public Long max(Collection<Long> dataSet) { //... }
public Long min(Collection<Long> dataSet) { //... } 
public Long average(Colletion<Long> dataSet) { //... }
// ...省略其他统计函数...

将原有设计中职责不够单一的函数使用 ISP 原则进行了隔离,让其满足单一职责。

3. 把接口理解为 OOP 中的接口

public interface Updater {
  void update();
}

public interface Viewer {
  String outputInPlainText();
  Map<String, String> output();
}

public class RedisConfig implemets Updater, Viewer {
  //...省略其他属性和方法...
  @Override
  public void update() { //... }
  @Override
  public String outputInPlainText() { //... }
  @Override
  public Map<String, String> output() { //...}
}

public class KafkaConfig implements Updater {
  //...省略其他属性和方法...
  @Override
  public void update() { //... }
}

public class MysqlConfig implements Viewer {
  //...省略其他属性和方法...
  @Override
  public String outputInPlainText() { //... }
  @Override
  public Map<String, String> output() { //...}
}

public class SimpleHttpServer {
  private String host;
  private int port;
  private Map<String, List<Viewer>> viewers = new HashMap<>();
  
  public SimpleHttpServer(String host, int port) {//...}
  
  public void addViewers(String urlDirectory, Viewer viewer) {
    if (!viewers.containsKey(urlDirectory)) {
      viewers.put(urlDirectory, new ArrayList<Viewer>());
    }
    this.viewers.get(urlDirectory).add(viewer);
  }
  
  public void run() { //... }
}

public class Application {
    ConfigSource configSource = new ZookeeperConfigSource();
    public static final RedisConfig redisConfig = new RedisConfig(configSource);
    public static final KafkaConfig kafkaConfig = new KakfaConfig(configSource);
    public static final MySqlConfig mysqlConfig = new MySqlConfig(configSource);
    
    public static void main(String[] args) {
        ScheduledUpdater redisConfigUpdater =
            new ScheduledUpdater(redisConfig, 300, 300);
        redisConfigUpdater.run();
        
        ScheduledUpdater kafkaConfigUpdater =
            new ScheduledUpdater(kafkaConfig, 60, 60);
        redisConfigUpdater.run();
        
        SimpleHttpServer simpleHttpServer = new SimpleHttpServer(“127.0.0.1”, 2389);
        simpleHttpServer.addViewer("/config", redisConfig);
        simpleHttpServer.addViewer("/config", mysqlConfig);
        simpleHttpServer.run();
    }
}

通过 ISP 原则,这里设计了两个职责非常单一的接口:Updater 和 Viewer。ScheduledUpdater 只依赖 Updater 这个跟热更新相关的接口,不需要依赖自己用不到的 Viewer 接口。同样的,SimpleHttpServer 也只依赖跟查看信息相关的接口 Viewer 接口,不需要依赖 Updater 接口。

4.3 单一职责原则和接口隔离原则的区别

接口隔离原则更加侧重接口的设计。同时,它提供了一种判断接口是否职责单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的职责就不够单一。

单一职责原则是针对模块、类、接口的设计。涉及的范围更大。

4.4 不符合接口隔离原则的接口会存在的问题

  1. 会导致某些类依赖了一些它不需要的接口。
  2. 当接口增加一个方法,所以,之间实现了该接口的类,都需要实现这个方法。而如果是新建一个接口,则不会影响之前实现类的逻辑。

5. 依赖反转/依赖倒置原则 DIP(Dependency Inversion Principle)

5.1 定义

高层模块不要依赖低层模块,高层模块和低层模块应该通过抽象来相互依赖。此外,抽象不要依赖具体的实现细节,具体的实现细节依赖抽象。

高层模块和低层模块

在调用链上,调用者属于高层,被调用者属于低层。而在平时的开发中,高层模块依赖低层模块是没有任何问题的。

Tomcat 和 Java Web 程序的关系

Tomcat 就是高层模块,Java Web 应用程序属于低层模块。Tomcat 和 Java Web 之间没有直接的依赖关系,两者都是依赖于同一个“抽象”(Servlet 规范)。Servlet 规范不依赖 Tomcat 和 Java Web 程序的实现细节,而 Tomcat 和 Java Web 依赖于 Servlet 规范。

5.2 控制反转 IOC(Inversion of Control)

逻辑由程序员控制

public class UserServiceTest {
  public static boolean doTest() {
    // ... 
  }
  
  public static void main(String[] args) {//这部分逻辑可以放到框架中
    if (doTest()) {
      System.out.println("Test succeed.");
    } else {
      System.out.println("Test failed.");
    }
  }
}

逻辑由框架控制

public abstract class TestCase {
  public void run() {
    if (doTest()) {
      System.out.println("Test succeed.");
    } else {
      System.out.println("Test failed.");
    }
  }
  
  public abstract boolean doTest();
}

public class JunitApplication {
  private static final List<TestCase> testCases = new ArrayList<>();
  
  public static void register(TestCase testCase) {
    testCases.add(testCase);
  }
  
  public static final void main(String[] args) {
    for (TestCase case: testCases) {
      case.run();
    }
  }

框架通过 doTest() 预留扩展点,程序员只需要实现这个方法,而不需要再写 main 函数中的执行流程了。

只需要程序员实现的代码

public class UserServiceTest extends TestCase {
  @Override
  public boolean doTest() {
    // ... 
  }
}

// 注册操作还可以通过配置的方式来实现,不需要程序员显示调用register()
JunitApplication.register(new UserServiceTest();

控制反转的核心

框架只需要提供可扩展的代码骨架,用来封装对象,管理整个流程的执行。程序员只需要将业务有关的代码块添加到预留点上就可以利用框架来驱动整个程序流程的执行。

控制反转中的“控制”指的是对程序流程的控制,而“反转”指的是在没有使用框架前,程序员需要自己来控制流程的执行。在使用框架后,整个流程通过框架来控制。流程的控制权从程序员“反转”到了框架。

5.3 依赖注入 DI(Dependency Injection)

依赖注入是一种具体的编程技巧。该技术有一个非常形象的说法:依赖注入是一个标价 25 美元,实际只值 5 美分的概念。

依赖注入的概念:不通过 new() 的方式在类内部创建依赖对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类使用。

非依赖注入方式

// 非依赖注入实现方式
public class Notification {
  private MessageSender messageSender;
  
  public Notification() {
    this.messageSender = new MessageSender(); //此处有点像hardcode
  }
  
  public void sendMessage(String cellphone, String message) {
    //...省略校验逻辑等...
    this.messageSender.send(cellphone, message);
  }
}

public class MessageSender {
  public void send(String cellphone, String message) {
    //....
  }
}
// 使用Notification
Notification notification = new Notification();

依赖注入方式

// 依赖注入的实现方式
public class Notification {
  private MessageSender messageSender;
  
  // 通过构造函数将messageSender传递进来
  public Notification(MessageSender messageSender) {
    this.messageSender = messageSender;
  }
  
  public void sendMessage(String cellphone, String message) {
    //...省略校验逻辑等...
    this.messageSender.send(cellphone, message);
  }
}
//使用Notification
MessageSender messageSender = new MessageSender();
Notification notification = new Notification(messageSender);

5.4 控制反转与依赖注入的区别

控制反转是一种设计思想,是将程序由程序员控制“反转”到由框架控制。控制反转的实现主要有依赖注入、模板方法等方式。

而依赖注入是一种具体的编程技巧,是用来实现控制反转的一种方法。

说明

此文是根据王争设计模式之美相关专栏内容整理而来,非原创。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342