SOLID原则小记


前言

无论是应用设计原则还是设计模式,最终的目的都是为了提高代码的可读性,可拓展性,复用性和可维护性等。在判断应用某一个设计原则是否合理时,我们可以以此作为最终的评价标准。

单一职责原则

1.简单介绍

单一职责原则指:一个类或模块只负责完成一个职责(或功能)。

单一职责原则描述对象有两个:类(class) 与 模块(module)。 关于这两个概念我们有两种理解方式。

  • 一种理解方式是把模块看作比类更加抽象的概念,把类看作一种模块;(一个类做完成一个职责(或功能),例如用户密码类就只有用户密码属性)
  • 另一种理解方式是把模块看作比类更粗粒度的代码块,多个类组成一个模块。(一个模块只负责一个职责(或功能),例如,用户模块,就只处理用户信息的操作,订单,交易是其他模块做的事情 )

2.如何判断类,模块是否单一职责

注意一定要站在当前应用场景考虑是否职责统一,因为在不同的应用场景和不同阶段的需求背景下,对同一个类的职责是否单一的判定可能是不一样的。
例如:

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

当前产品定义为社交产品,业务场景只需要用户保存自己的信息就好了。那这种类的构建就没有违反职责单一原则。
而如果当前业务场景有电商功能需求,那就需要将UserInfo中的地址拆分一个物流信息类出来(如果以后产品做的很好需要拓展电商模块,那就需要重构该用户信息类)
而如果之后产品发展的更加好,那可能还需要拆分出用户的身份认证属性手机号,邮箱等

类的职责并不是越细化越好,得立足与当前业务场景进行取舍


模块的单一职责定义同样是站在当前业务场景考虑的,如果之后产品迭代有耦合场景则需要独立出模块,或者建立耦合模块处理类似业务

3.小结

评价一个类的职责是否单一,并没有一个明确的,可量化的标准。实际上,在软件开发中,我们没有必要过度设计(粒度过细)。我们可以先编写一个粗粒度的类,满足当下的业务需求即可。随着业务的发展,如果这个粗粒度的类越来越复杂,代码越来越多,那么我们在这时再将这个粗粒度的类拆分成几个细粒度的类即可。
对于职责是否单一的判定,存在一些判定原则,如下所示:

  1. 如果类中的代码行数,函数或属性过多,影响代码的可读性和可维护性,就需要考虑对类进行拆分。
  2. 如果某个类依赖的其他类过多,或者依赖某个类的其他类过多,不符合高内聚,低耦合的代码设计思想,就需要对该类进行拆分。
  3. 如果类中的私有方法过多,就需要考虑将私有方法独立到新的类中,并设置为public方法,供更多类使用,从而提高代码的复用性。
  4. 如果类很难准确命名(很难用一个业务名词概括),或者只能用Manager,Context之类的笼统词语来命名,就说明类的职责定义不够清晰。
  5. 如果类中大量方法集中操作其中几个属性(如上面的UserInfo类的例子中,假如很多方法只操作address信息),就可以考虑将这些属性和对应的方法拆分出来。

开闭原则

1.简单介绍

开闭原则指:对拓展开发,对修改关闭。添加一个新功能时应该是在已有代码基础上拓展代码(新增模块类和方法等),而非修改已有代码(修改模块,类和方法等)。

2.那么怎样的代码改动才算是拓展,怎样的代码改动才算是修改呢?

源码如下:

public class Alert {
    private AlterRule 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 duration) {
       // 触发告警通知接口的相关负责人或团队
       long tps = requestCount / duration;
       // 当接口的TPS超过预先设置的最大值时触发告警
       if (tps > rule.getMatchedRule(api).getMaxTps()) {
           notification.notify(NotificationEmergencyLevel.URGENCY, "...")
       }
   
      // 当接口请求出错数大于最大允许值时触发告警
      if(errorCount > rule.getMethodRule(api).getMaxErrorCount()) {
          notification.notify(NotificationEmergencyLevel.SEVERE, "...")
      }
    }
}

两份代码举个例子:

  1. 以"修改"的方式,完成新增告警需求:
public class Alert {
    private AlterRule rule;
    private Notification notification;
   
    public Alert(AlertRule rule, Notification notification) {
       this.rule = rule;
       this.notification = notification;
    }
    // 改动一:新增参数timeoutCount
    public void check(String api, long requestCount, long errorCount, long timeoutCount, long duration) {
       // 触发告警通知接口的相关负责人或团队
       long tps = requestCount / duration;
       // 当接口的TPS超过预先设置的最大值时触发告警
       if (tps > rule.getMatchedRule(api).getMaxTps()) {
           notification.notify(NotificationEmergencyLevel.URGENCY, "...")
       }
   
      // 当接口请求出错数大于最大允许值时触发告警
      if(errorCount > rule.getMethodRule(api).getMaxErrorCount()) {
          notification.notify(NotificationEmergencyLevel.SEVERE, "...")
      }
      // 修改二:当每秒接口超时请求个数超过预先设置的最大值时触发告警并发送通知
      long timeoutTps = timeoutCount / duration;
      if(timeoutTps > rule.getMethodRule(api).getMaxTimeoutTps()) {
          notification.notify(NotificationEmergencyLevel.URGENCY, "...")
      }
    }
}

       以上每次需要添加一条告警规则时都需要修改check()函数,甚至相应的单元测试也需要修改和重新测试

  1. 以"拓展"的方式,完成新增告警需求:
    重构代码如下:
public class Alert {
  // 声明告警器AlertHandler
  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);
     }
  }
}

定义Api告警参数对象ApiStatInfo

public class ApiStatInfo {
   private String api;
   private long requestCount;
   private long errorCount;
   private long timeoutCount;
   private long duration;
  //...省略constructor, get, set方法
}

定义告警器AlertHandler

public abstract class AlertHandler {
   protected AlterRule rule;
   protected Notification notification;

   public AlertHandler (AlterRule rule, Notification notification) {
       this.rule = rule;
       this.notification = notification;
   }

   public abstract void check(ApiStatInfo apiStatInfo);
}

定义"当接口的TPS超过预先设置的最大值时触发告警"规则

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

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

定义"当接口请求出错数大于最大允许值时触发告警"规则

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

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

定义"当每秒接口超时请求个数超过预先设置的最大值时触发告警并发送通知"规则

public class TimeoutAlertHandler extends AlertHandler {
  public TimeoutAlertHandler (AlterRule rule, Notification notification) {
    super(rule, notification);
  }

  @Override
  public void check(ApiStatInfo apiStatInfo) {
      long timeoutTps = apiStatInfo.getTimeoutCount() / apiStatInfo.getDuration();
      if(timeoutTps > rule.getMethodRule(api).getMaxTimeoutTps()) {
          notification.notify(NotificationEmergencyLevel.URGENCY, "...")
      }
  }
}

拓展重构后,调用方式如下:
构建ApplicationContext ,用于Alert类的创建,组装(AlertRule 和 notification 的依赖注入) 和初始化(添加handler)。

public class ApplicationContext {
   private AlterRule alertRule;
   private Notification notification;
   private Alert alert;
   
   public void initializeBeans() {
     alertRule = new AlterRule(/*省略参数*/);
     notification= new Notification(/*省略参数*/);
     alert = new Alert();
     alert.addAlertHandler(new TpsAlertHandler(alertRule, notification));
     alert.addAlertHandler(new ErrorAlertHandler(alertRule, notification));
     alert.addAlertHandler(new TimeoutAlertHandler(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 Demo {
  public static void main(String[] args){
     ApiStatInfo apiStatInfo = new ApiStatInfo();
     apiStatInfo.setApi(/*设置api*/);
     apiStatInfo.setRequestCount(/*设置requestCount*/);
     apiStatInfo.setErrorCount(/*设置errorCount*/);
     apiStatInfo.setTimeoutCount(/*设置timeoutCount*/);
     apiStatInfo.setDuration(/*设置duration*/);
     ApplicationContext.getInstance().getAlert().check(apiStatInfo );
  }
}

       以上每次需要添加一条告警规则时只需要定义对于的告警器规则就好,而单元测试无需修改和重新测试。添加新的告警规则时只需要拓展,不需要修改,完全满足开闭原则。

3.小结

  1. 拓展性是衡量代码质量的重要衡量标准。
  2. 只要是代码改动没有破坏原有代码的正常运行和原有的单元测试,我们就可以认为这是一个合格的代码改动。
  3. 对于一些短期内可能进行的拓展,需求改动对代码结构影响比较大的拓展,或者实现成本不高的拓展,在编写代码时,我们可以事先进行可拓展性设计;但对于一些不确定未来是否要支持的需求,或者实现起来比较复杂的拓展,我们可以等到有需求驱动时,再通过重构的方式来满足拓展的需求。
  4. 在平时开发时,我们需要权衡拓展性和代码可读性。一般情况下,面向用户的业务需求,改动比较频繁的需求,可读性需要着重考虑。而不面向用户的业务需求,则需要着重考虑可拓展性

里氏替换原则

1. 简单介绍

里氏替换原则指: 子类对象能够替换到程序中的父类对象出现的任何地方,并且保证程序原有的逻辑型位不变和正确性不被破坏。

里氏替换原则和多态的区别:多态时一种代码实现思路,而里氏替换原则是一种设计原则用来知道继承关系中的子类设计。

2. 违反里氏替换原则的反模式

  1. 子类违反父类声明要实现的功能。例如:父类定义一个订单排序函数sortOrdersByAmount(),该函数按照金额从小到大来给订单排序,而子类重写sortOrdersByAmount(),之后,按照创建日期来给订单排序。那么,这个子类的设计就违反了里氏替换原则。
  2. 子类违反父类对输入输出和异常的约定。
    输入:在父类中,某个函数约定输入数据可以是任意整数,但子类重载之后规定只能输入正数,否则抛出异常,这各子类就违反了里氏替换原则。
    输出:在父类中,某个函数约定只抛出ArgumentNullException异常,那么子类重载此函数之后,也只允许抛出ArgumentNullException异常,否则子类就违反了里氏替换原则。
  3. 子类违反父类注释中罗列的任何规则。例如:在父类中定义一个函数withdraw(),其注释是这样写的:“用户的提现金额不得超过账户余额…”,而子类重写withdraw()函数之后,针对VIP账号实现了透支提现功能,那么这个子类就不符合里氏替换原则。除非修改父类注释,或者子类调整代码让其符合父类注释

总结来说就是,子类继承父类后,重写函数,函数中规则完全重写(调整),有别与父类函数定义的意思,则属于违反里氏替换原则
如果用父类的单元测试验证子类代码,如果某些单元测试运行失败,就说明子类的设计实现没有完全遵守父类约定,子类有可能违反里氏替换原则

接口隔离原则

1.简单介绍

接口隔离原则:客户端不应该被强迫依赖它不需要的接口

这里的“接口”指,一组抽象的约定,又可以具体指系统之间相互调用的API,还可以特指面向对象编程语言中的接口等,总结来说主要有三种理解方式。

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

2. 把“接口”理解为一组API或函数

微服务用户系统向其他系统提供了一组与用户相关的API,如登录,注册,获取用户信息等函数。
接口如下:

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

实现类如下:

public class UserServiceImp implements UserService  {
  // ....省略实现代码...
}

当前后台系统需要实现删除用户功能,需要系统提供deleteUserByCellPhoen()或者delteUserById()接口而该操作只会在后台使用。
为了避免可能的安全隐患以及误操作,该方法不能在UserService中增加。
新增接口RestrictedUserService代码如下:

public interface RestrictedUserService {
  boolean deleteUserByCellPhoen(String cellPhone);
  boolean delteUserById(long id);
}

实现类如下:

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

在上面代码示例中,我们把接口隔离原则中的接口理解为一组接口,也就是说,它可以是某个微服务的接口,某个类库的函数等。
在设计微服务接口或类库函数时,如果部分接口或函数只被部分调用者使用,就需要将这部分接口或函数隔离出来,并单独提供给对应的调用者使用,而不是“强迫”其他调用者也依赖这部分不会被用到的接口或函数

3. 把“接口”理解为单个API或函数

把“接口”理解为单个API或函数,可以理解为:API或函数尽量功能单一,不要将多个不同的功能逻辑在一个函数中实现
代码如下:

public class Statistics {
  private Long max;
  private Long min;
  private Long average;
  private Long sum;
  private Long percentile99;
  private Long percentile999;
  //...省略constructor,get,set等方法
  
  public Statistics count(Collection<Long> dataSet) {
    //...省略计算逻辑...
  }
}

       在上述代码中,count()函数功能不够单一,应为包含多个不同的统计功能,比如包含求最大值,最小值和平均值等。按照接口隔离原则,我们应该把count()函数拆分成几个更小粒度的函数,每个函数负责实现一个独立的统计功能。
拆分代码如下:

public class Statistics {
  private Long max;
  private Long min;
  private Long average;
  private Long sum;
  private Long percentile99;
  private Long percentile999;
  //...省略constructor,get,set等方法
  
  public Long max(Collection<Long> dataSet) {
    //...省略计算逻辑...
  }
  public Long min(Collection<Long> dataSet) {
    //...省略计算逻辑...
  }
  public Long average(Collection<Long> dataSet) {
    //...省略计算逻辑...
  }
}

       不过换个角度,如果Statistics类定义的所有统计信息都会被用到,那么count()函数的设计就是合理的;如果对于每个统计需求,Statistics类定义的统计信息只会用到部分,如只需要用到max,min这两个统计西悉尼,那么count()函数仍然会把所有统计信息计算一遍,这种情况下,我们应该将count()函数拆分成粒度更细的多个统计函数。
       接口隔离原则与单一职责有些类似。接口隔离原则提供一种判断接口是否职责单一的方法:通过调用者如何使用接口来间接地判定接口是否职责单一。如果调用者只使用部分接口或接口部分功能,那么接口设计就不满足单一职责原则。

4. 把“接口”理解为OOP中的接口概念

这里个人认为和第一点使用方式一样,只是概念不太一样,欢迎大家讨论

场景描述:项目中用到3个外部系统Redis,MySQL和Kafka。每个系统对应一系列配置信息,如IP地址,端口和访问超时时间等。为了在内存中存储这些配置信息,以供项目中的其他模块使用,我们实现了3个配置类:RedisConfig,MysqlConfig和KafkaConfig,具体代码实现如下:
RedisConfig :

public class RedisConfig {
  // 配置中心(如Zookeeper)
  private ConfigSource configSource;
  private String address;
  private int timeoutl
  private int maxTotal;
  //...省略其他配置:maxWaitMillis,maxIdle,minIdle...
  
  public RedisConfig(ConfigSource configSource) {
    this.configSource = configSource;
  }
  //...省略get,set方法...
  public void init() {
    //从configSource加载配置到address,timeout和maxTotal
  }
}

MysqlConfig:

public class MysqlConfig {
  // 配置中心(如Zookeeper)
  private ConfigSource configSource;
  private String address;
  private int timeoutl
  private int maxTotal;
  //...省略其他配置:maxWaitMillis,maxIdle,minIdle...
  
  public MysqlConfig(ConfigSource configSource) {
    this.configSource = configSource;
  }
  //...省略get,set方法...
  public void init() {
    //从configSource加载配置到address,timeout和maxTotal
  }
}

KafkaConfig:

public class KafkaConfig {
  // 配置中心(如Zookeeper)
  private ConfigSource configSource;
  private String address;
  private int timeoutl
  private int maxTotal;
  //...省略其他配置:maxWaitMillis,maxIdle,minIdle...
  
  public KafkaConfig(ConfigSource configSource) {
    this.configSource = configSource;
  }
  //...省略get,set方法...
  public void init() {
    //从configSource加载配置到address,timeout和maxTotal
  }
}

现在有个新的功能需求,希望支持Redis和Kafka配置信息的热更新。但是因为某些原因,我们并不希望对MySQL的配置信息进行热更新。
为实现这样的一个功能需求,我们实现了ScheduledUpdater类,以固定时间频率调用RedisConfig,KafkaConfig的update()方法,更新配置信息。具体的代码实现如下:

public interface Updater {
  void update();
}

RedisConfig 实现Updater接口:

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

KafkaConfig实现Updater接口:

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

MysqlConfig 不调整

ScheduledUpdater 实现如下:

public class ScheduledUpdater {
  private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
  private long initialDelayInSeconds;
  private long periodInSeconds;
  private Updater updater;
  
  public ScheduledUpdater(Updater updater, long initialDelayInSeconds, long periodInSeconds) {
      this.updater = updater;
      this.initialDelayInSeconds = initialDelayInSeconds;
      this.periodInSeconds = periodInSeconds;
  }
  
  public void run(){
     executor.scheduleAtFixedRate(new Runnable() {
       @Override
       public void run() {
         updater.update();
       }
     }, this.initialDelayInSeconds, this.periodInSeconds, TimeUnit.SECONDS);
  }
}

调用

public class Application {
  ConfigSource configSource = new ZookeeperConfigSource(/*省略参数*/);
  public static final RedisConfig redisConfig = new RedisConfig(configSource);
  public static final KafkaConfig kafkaConfig = new KafkaConfig(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);
	   kafkaConfigUpdater.run();
	}
}

以上就是热更新需求的实现。当前又有一个监控的新需求,通过命令的方式查看ZooKeeper中的配置信息比较麻烦,因此我们希望有一种更加方便的查看配置信息的方式。
我们可以在项目中开发一个内嵌的SimpleHttpServer,输出项目的配置信息到一个固定的HTTP地址,如http://127.0.0.1:2389/config。我们只需要在浏览器中输入这个地址,就可以显示系统配置信息。不过因为某些原因,我们只想暴露MySQL和Redis的配置信息,不想暴露Kafka的配置信息。
为了实现这个监控功能,我们需要对代码进行改造。改造之后的代码如下所示:

public interface Updater {
  void update();
}

Viewer 接口实现配置信息暴露操作

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

RedisConfig 接口调整,新增实现Viewer接口操作:

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

MysqlConfig 接口调整,新增实现Viewer接口操作:

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

KafkaConfig 不调整

SimpleHttpServer 实现

public class SimpleHttpServer {
  private String host;
  private int port;
  private Map<String, List<Viewer>> viewers = new HashMap<>();
  
  public SimpleHttpServer(String host, int port) {
    this.host=host;
    this.port=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 KafkaConfig(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);
	   kafkaConfigUpdater.run();

       SimpleHttpServer simpleHttpServer = new SimpleHttpServer("127.0.0.1", 2389);
       simpleHttpServer.addViewer("/config", redisConfig);
       simpleHttpServer.addViewer("/config", mysqlConfig);
       simpleHttpServer.run();
	}
}

至此,热更新和监控的需求都已经实现。我们设计了两个功能单一的接口: Updater 和 Viewer。ScheduledUpdater类只依赖Updater这个与热更新相关的接口,不依赖不需要的Viewer接口,满足接口隔离原则。同理,SimpleHttpServer类只依赖与查看信息相关的Viewer接口,不需要依赖Updater接口,也满足接口隔离原则。
以上就是把“接口”理解为OOP中的接口概念个人认为与把“接口”理解为一组API或函数*类似,只是概念上有些差异,实际工作中使用,两者是差不太多的(欢迎大家讨论)。

依赖反转原则

1.简单介绍

依赖反转原则有时也被称为依赖倒置原则:高层模块不要依赖低层模块。高层模块和低层模块应该通过抽象相互依赖。除此之外,抽象不要依赖具体实现细节,具体实现细节依赖抽象。
依赖反转原则主要用来知道框架的设计。

2.那么如何划分高层模块和低层模块?

简单来说,调用者属于高层,被调用者属于低层。以Tomcat为例:
Tomcat是运行Java Web应用程序的容器。我们编写的应用程序实现细节(代码)只要部署在Tomcat容器之下,便可以被Tomcat容器调用并执行,所以Tomcat是高层模块,被调用执行的应用程序实现细节(代码)就是低层模块,它们之间的并没有直接的依赖关系,二者都依赖同一个"抽象",也就是Servlet规范。
Servlet规范不依赖具体的Tomcat容器和应用程序实现细节(代码),而Tomcat容器和应用程序依赖Servlet规范。

3. 控制反转IoC

一种比较笼统的设计思想,一般用来知道框架的设计。
这里的"控制"是指程序执行流程的控制,而“反转”是指在没有使用框架之前,程序员自己编写代码控制整个程序流程的执行。在使用框架之后,整个程序的执行流程由框架控制,流程的控制权从程序员“反转”给了框架。程序员通过框架区控制程序流程。

没有框架之前的"控制反转"代码如下:

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.");
    }
  }
}

通过doTest()"控制反转"程序流程

抽象出测试框架后的"控制反转"代码如下:

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 UserServiceTest extends TestCase {
  @Override
  public boolean doTest() {
    //...测试业务
  }
}

使用:

public class JunitApplication {
  private static final List<TestCase> testCases = ArrayList<>();
  
  public static void register(TestCase testCase) {
    testCases.add(testCase);
  }
  
  public static void main(String[] args) {
     // 注册操作还可以通过配置的方式实现,不需要手动调用register()
     JunitApplication.register(new UserServiceTest());
     for (TestCase testCase: testCases) {
        testCase.run();
     }
  }
}

通过框架预留的拓展点添加自己需要测试的相关代码,就可以利用框架驱动整个程序流程的执行。

4. 依赖注入DI

依赖注入指,不通过new的方式在类的内部创建依赖的类对象,而是将类依赖的类对象在外部创建好之后,通过构造函数,函数参数等方式传递(或称为注入)给类使用。一种具体的编程技巧。

代码如下:
定义消息发送类:

public class Notification {
  private MessageSender messageSender;

  public Notification(MessageSender messageSender) {
    this.messageSender = messageSender;
  }
  
  public void sendMessage(String cellphoen, String message) {
    this.messageSender.send(cellphone, message);
  }
}

定义消息发送接口

public interface MessageSender {
  void send(String cellphone, String message);
}

定义短信发送类

public class SmsSender implements MessageSender {
  
  @Override
  public void send(String cellphone, String message) {
    //...发送业务...
  }
}

定义站内信发送类

public class InboxSender implements MessageSender {
  
  @Override
  public void send(String cellphone, String message) {
    //...发送业务...
  }
}

使用:
定义短信发送接口

public class Demo {
   public static void main(String[] args) {
      MessageSender messageSender = new SmsSender();
      Notification notification = new Notification(messageSender);
      notification.sendMessage("手机号码", "发送内容");
   }
}

5. 依赖注入框架

依赖注入框架,就是将对象的创建和依赖注入本身与自身业务无关的逻辑抽象成框架,由框架自动完成。

将这步操作交由框架操作:

public class Demo {
   public static void main(String[] args) {
      MessageSender messageSender = new SmsSender();
      Notification notification = new Notification(messageSender);
   }
}

我们通过依赖注入框架提供的拓展点,简单的配置所有需要创建的类对象,类之间的依赖关系,就可以实现由框架自动创建对象,管理对象的生命周期,依赖注入等。

总结

反复咀嚼《设计模式之美》中SOLID原则这一段,还是有部分不太懂,通过这篇博文,还是有小部分不太懂。大家有啥不懂的可以在评论区写出来,大家一起沟通交流哈
最后,感谢争哥的书~
原书中干货很多,推荐大家去看原书

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值