六大设计原则

单一职责原则(SRP)

  • 设计原则分类:SOLID、KISS、YAGNI、DRY、LOD 等
  • SOLID:单一职责原则(S)、开闭原则(O)、里式替换原则(L)、接口隔离原则(I)和依赖反转原则(D)

如何理解单一职责原则(SRP)?

  • 单一职责原则(Single Responsibility Principle):一个类或者模块只负责完成一个职责(或者功能)。即不要设计大而全的类,要设计粒度小、功能单一的类。换个角度来讲就是,一个类包含了两个或者两个以上业务不相干的功能,那我们就说它职责不够单一,应该将它拆分成多个功能更加单一、粒度更细的类。
  • 单一职责原则是为了实现代码高内聚、低耦合,提高代码的复用性、可读性、可维护性。
  • 举例:一个类里既包含订单的一些操作,又包含用户的一些操作。而订单和用户是两个独立的业务领域模型,我们将两个不相干的功能放到同一个类中,那就违反了单一职责原则。为了满足单一职责原则,我们需要将这个类拆分成两个粒度更细、功能更加单一的两个类:订单类和用户类。

如何判断类的职责是否足够单一?

  • 不同的应用场景、不同阶段的需求背景下,对同一个类的职责是否单一的判定,可能都是不一样的。在某种应用场景或者当下的需求背景下,一个类的设计可能已经满足单一职责原则了,但如果换个应用场景或着在未来的某个需求背景下,可能就不满足了,需要继续拆分成粒度更细的类。
  • 类中的代码行数、函数或属性过多,会影响代码的可读性和可维护性,我们就需要考虑对类进行拆分;
  • 类依赖的其他类过多,或者依赖类的其他类过多,不符合高内聚、低耦合的设计思想,我们就需要考虑对类进行拆分;
  • 私有方法过多,我们就要考虑能否将私有方法独立到新的类中,设置为 public 方法,供更多的类使用,从而提高代码的复用性;
  • 比较难给类起一个合适名字,很难用一个业务名词概括,或者只能用一些笼统的 Manager、Context 之类的词语来命名,这就说明类的职责定义得可能不够清晰;
  • 类中大量的方法都是集中操作类中的某几个属性。

类的职责是否设计得越单一越好?

  • 单一职责原则通过避免设计大而全的类,避免将不相关的功能耦合在一起,来提高类的内聚性。同时,类职责单一,类依赖的和被依赖的其他类也会变少,减少了代码的耦合性,以此来实现代码的高内聚、低耦合。但是,如果拆分得过细,实际上会适得其反,反倒会降低内聚性,也会影响代码的可维护性。

接口隔离原则(ISP)

理解接口隔离原则

  • 接口隔离原则:客户端不应该被强迫依赖它不需要的接口。其中的“客户端”,可以理解为接口的调用者或者使用者
  • 理解:
    • 一组 API 接口集合
    • 单个 API 接口或函数
    • OOP 中的接口概念

把“接口”理解为一组 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 {
        //...
    }
    

    这是微服务用户系统提供了一组跟用户相关的 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 interface RestrictedUserService {
      boolean deleteUserByCellphone(String cellphone);
      boolean deleteUserById(long id);
    }
    
    public class UserServiceImpl implements UserService, RestrictedUserService {
      // ...省略实现代码...
    }
    

    这时,我们就可以把接口隔离原则中的接口,理解为一组接口集合

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

  • 例子:

    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;
    }
    

    在上面的代码中,count() 函数的功能不够单一,我们应该把 count() 函数拆成几个更小粒度的函数,每个函数负责一个独立的统计功能,让调用者只依赖它需要的那个细粒度函数。代码如下:

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

    你应该已经发现,接口隔离原则跟单一职责原则有点类似,不过稍微还是有点区别。单一职责原则针对的是模块、类、接口的设计。而接口隔离原则相对于单一职责原则,一方面它更侧重于接口的设计,另一方面它的思考的角度不同。它提供了一种判断接口是否职责单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一

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

  • 如果把“接口”理解为 OOP 中的接口,也可以理解为面向对象编程语言中的接口语法。那接口的设计要尽量单一,不要让接口的实现类和调用者,依赖不需要的接口函数。

  • 例子:

    public class RedisConfig {
        private ConfigSource configSource; //配置中心(比如zookeeper)
        private String address;
        private int timeout;
        private int maxTotal;
        //省略其他配置: maxWaitMillis,maxIdle,minIdle...
    
        public RedisConfig(ConfigSource configSource) {
            this.configSource = configSource;
        }
    
        public String getAddress() {
            return this.address;
        }
        //...省略其他get()、init()方法...
    
        public void update() {
            //从configSource加载配置到address/timeout/maxTotal...
        }
    }
    
    public class KafkaConfig { //...省略... }
    public class MysqlConfig { //...省略... }
    

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

    public interface Updater {
      void update();
    }
    
    public class RedisConfig implemets Updater {
      //...省略其他属性和方法...
      @Override
      public void update() { //... }
    }
    
    public class KafkaConfig implements Updater {
      //...省略其他属性和方法...
      @Override
      public void update() { //... }
    }
    
    public class MysqlConfig { //...省略其他属性和方法... }
    
    public class ScheduledUpdater {
        private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
        private long initialDelayInSeconds;
        private long periodInSeconds;
        private Updater updater;
    
        public ScheduleUpdater(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 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);
        kafkaConfigUpdater.run();
      }
    }
    

    加入我们现在又有新的需求:监控功能需求。不过,出于某些原因,我们只想暴露 MySQL 和 Redis 的配置信息,不想暴露 Kafka 的配置信息。改造代码如下:

    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);
            kafkaConfigUpdater.run();
            
            SimpleHttpServer simpleHttpServer = new SimpleHttpServer(127.0.0.1, 2389);
            simpleHttpServer.addViewer("/config", redisConfig);
            simpleHttpServer.addViewer("/config", mysqlConfig);
            simpleHttpServer.run();
        }
    }
    

    ScheduledUpdater 只依赖 Updater 这个跟热更新相关的接口,不需要被强迫去依赖不需要的 Viewer 接口,满足接口隔离原则。同理,SimpleHttpServer 只依赖跟查看信息相关的 Viewer 接口,不依赖不需要的 Updater 接口,也满足接口隔离原则。

开闭原则(OCP)

  • 开闭原则(Open Closed Principle):软件实体(模块、类、方法等)应该“对扩展开放、对修改关闭”。即:添加一个新的功能应该是,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。

  • 例子:

    这是一段 API 接口监控告警的代码,中,AlertRule 存储告警规则,可以自由设置。Notification 是告警通知类,支持邮件、短信、微信、手机等多种通知渠道。NotificationEmergencyLevel 表示通知的紧急程度,包括 SEVERE(严重)、URGENCY(紧急)、NORMAL(普通)、TRIVIAL(无关紧要),不同的紧急程度对应不同的发送渠道

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

    我们发现,一方面,我们对接口进行了修改,这就意味着调用这个接口的代码都要做相应的修改。另一方面,修改了 check() 函数,相应的单元测试都需要修改,不遵循开闭原则的 “对扩展开放、对修改关闭”

  • 遵循开闭原则实现:

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

    在这里,我们进行了两部分修改:

    第一部分是将 check() 函数的多个入参封装成 ApiStatInfo 类;

    第二部分是引入 handler 的概念,将 if 判断逻辑分散在各个 handler 中。

    使用 代码如下:

    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 Demo {
        public static void main(String[] args) {
            ApiStatInfo apiStatInfo = new ApiStatInfo();
            // ...省略设置apiStatInfo数据值的代码
            ApplicationContext.getInstance().getAlert().check(apiStatInfo);
        }
    }
    

    这时,我们再思考怎样改动实现上述的需求:

    第一处改动是:在 ApiStatInfo 类中添加新的属性 timeoutCount。

    第二处改动是:添加新的 TimeoutAlertHander 类。

    第三处改动是:在 ApplicationContext 类的 initializeBeans() 方法中,往 alert 对象中注册新的 timeoutAlertHandler。

    第四处改动是:在使用 Alert 类的时候,需要给 check() 函数的入参 apiStatInfo 对象设置 timeoutCount 的值。

    改动之后的代码如下:

    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() 函数的逻辑。但是有的小伙伴会说:改动一、三、四貌似不是基于扩展而是基于修改的方式来完成的,那改动一、三、四不就违背了开闭原则吗?实际上,我们也没必要纠结某个代码改动是“修改”还是“扩展”,更没必要太纠结它是否违反“开闭原则”。我们回到这条原则的设计初衷:只要它没有破坏原有的代码的正常运行,没有破坏原有的单元测试,我们就可以说,这是一个合格的代码改动。

里式替换原则(LSP)

里式替换原则概述

  • 里氏替换原则:子类对象(object of subtype/derived class)能够替换程序(program)中父类对象(object of base/parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。

  • 例子:

    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(/*省略参数*/););
    

    在上面的代码中,子类 SecurityTransporter 的设计完全符合里式替换原则,可以替换父类出现的任何位置,并且原来代码的逻辑行为不变且正确性也没有被破坏。

  • 违背了 LSP 的例子:

    // 改造前:
    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);
        }
    }
    

    改造前,如果 appId 或者 appToken 没有设置,我们就不做校验;改造后,如果 appId 或者appToken 没有设置,则直接抛出 NoAuthorizationRuntimeException 未授权异常。从设计思路上来讲,SecurityTransporter 的设计是不符合里式替换原则的。

多态和里式替换的区别

  • 虽然从定义描述和代码实现上来看,多态和里式替换有点类似,但它们关注的角度是不一样的。多态是面向对象编程的一大特性,也是面向对象编程语言的一种语法。它是一种代码实现的思路。而里式替换是一种设计原则,是用来指导继承关系中子类该如何设计的,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑以及不破坏原有程序的正确性。

哪些代码明显违背了 LSP?

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

  • 父类中提供的 sortOrdersByAmount() 订单排序函数,是按照金额从小到大来给订单排序的,而子类重写这个 sortOrdersByAmount() 订单排序函数之后,是按照创建日期来给订单排序的。那子类的设计就违背里式替换原则。

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

  • 在父类中,某个函数约定:运行出错的时候返回 null;获取数据为空的时候返回空集合(empty collection)。而子类重载函数之后,实现变了,运行出错返回异常(exception),获取不到数据返回 null。那子类的设计就违背里式替换原则。在父类中,某个函数约定,输入数据可以是任意整数,但子类实现的时候,只允许输入数据是正整数,负数就抛出,也就是说,子类对输入的数据的校验比父类更加严格,那子类的设计就违背了里式替换原则。在父类中,某个函数约定,只会抛出 ArgumentNullException 异常,那子类的设计实现中只允许抛出 ArgumentNullException 异常,任何其他异常的抛出,都会导致子类违背里式替换原则。

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

  • 父类中定义的 withdraw() 提现函数的注释是这么写的:“用户的提现金额不得超过账户余额……”,而子类重写 withdraw() 函数之后,针对 VIP 账号实现了透支提现的功能,也就是提现金额可以大于账户余额,那这个子类的设计也是不符合里式替换原则的。

依赖反转原则

控制反转(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.");
        }
      }
    }
    
    // 框架提供了一个可扩展的代码骨架,用来组装对象、管理整个执行流程。程序员利用框架进行开发的时候,只需要往预留的扩展点上,添加跟自己业务相关的代码,就可以利用框架来驱动整个程序流程的执行。
    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();
        }
      }
    

依赖注入(DI)

  • 依赖注入:不通过 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);
    

依赖反转原则(DIP)

  • 依赖反转原则:也叫作依赖倒置原则,高层模块(high-level modules)(调用者)不要依赖低层模块(low-level)(被调用者)。高层模块和低层模块应该通过抽象(abstractions)来互相依赖。除此之外,抽象(abstractions)不要依赖具体实现细节(details),具体实现细节(details)依赖抽象(abstractions)
  • 例子:Tomcat 是运行 Java Web 应用程序的容器。我们编写的 Web 应用程序代码只需要部署在 Tomcat 容器下,便可以被 Tomcat 容器调用执行。按照之前的划分原则,Tomcat 就是高层模块,我们编写的 Web 应用程序代码就是低层模块。Tomcat 和应用程序代码之间并没有直接的依赖关系,两者都依赖同一个“抽象”,也就是 Servlet 规范。Servlet 规范不依赖具体的 Tomcat 容器和应用程序的实现细节,而 Tomcat 容器和应用程序依赖 Servlet 规范。

迪米特法则(LOD)

  • 迪米特法则:不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口

不该有直接依赖关系的类之间,不要有依赖

  • 例子:

    public class NetworkTransporter {
        // 省略属性和其他方法...
        public Byte[] send(HtmlRequest htmlRequest) {
          //...
        }
    }
    
    public class HtmlDownloader {
      private NetworkTransporter transporter;//通过构造函数或IOC注入
      
      public Html downloadHtml(String url) {
        Byte[] rawHtml = transporter.send(new HtmlRequest(url));
        return new Html(rawHtml);
      }
    }
    

    问题分析:首先,我们来看 NetworkTransporter 类。作为一个底层网络通信类,我们希望它的功能尽可能通用,而不只是服务于下载 HTML,所以,我们不应该直接依赖太具体的发送对象 HtmlRequest。从这一点上讲,NetworkTransporter 类的设计违背迪米特法则,依赖了不该有直接依赖关系的HtmlRequest 类

  • 重构:

    public class NetworkTransporter {
        // 省略属性和其他方法...
        public Byte[] send(String address, Byte[] data) {
          //...
        }
    }
    
    public class HtmlDownloader {
      private NetworkTransporter transporter;//通过构造函数或IOC注入
      
      // HtmlDownloader这里也要有相应的修改
      public Html downloadHtml(String url) {
        HtmlRequest htmlRequest = new HtmlRequest(url);
        Byte[] rawHtml = transporter.send(
          htmlRequest.getAddress(), htmlRequest.getContent().getBytes());
        return new Html(rawHtml);
      }
    }
    

    分析:这样,NetworkTransporter 类 与 HtmlRequest 类没有直接依赖关系了,NetworkTransporter 里的send()方法就可以发送其他的请求了

有依赖关系的类之间,尽量只依赖必要的接口

  • 例子:

    // 序列化类
    public class Serialization {
        // 序列化方法
        public String serialize(Object object) {
            String serializedResult = ...;
            //...
            return serializedResult;
        }
    
        // 反序列化方法
        public Object deserialize(String str) {
            Object deserializedResult = ...;
            //...
            return deserializedResult;
        }
    }
    

    问题分析:假设在我们的项目中,有些类只用到了序列化操作,而另一些类只用到反序列化操作。那基于迪米特法则后半部分“有依赖关系的类之间,尽量只依赖必要的接口”,只用到序列化操作的那部分类不应该依赖反序列化接口。同理,只用到反序列化操作的那部分类不应该依赖序列化接口

  • 重构:

    public class Serializer {
      public String serialize(Object object) {
        String serializedResult = ...;
        ...
        return serializedResult;
      }
    }
    
    public class Deserializer {
      public Object deserialize(String str) {
        Object deserializedResult = ...;
        ...
        return deserializedResult;
      }
    }
    

    分析:虽然这样比上面的代码更满足迪米特法则,但却违背了高内聚的设计思想

    高内聚:就是指相近的功能应该放到同一个类中,不相近的功能不要放到同一个类中

    松耦合:就是指类与类之间的依赖关系简单清晰。即使两个类有依赖关系,一个类的代码改动不会或者很少导致依赖类的代码改动

  • 再次重构:

    public interface Serializable {
      String serialize(Object object);
    }
    
    public interface Deserializable {
      Object deserialize(String text);
    }
    
    public class Serialization implements Serializable, Deserializable {
      @Override
      public String serialize(Object object) {
        String serializedResult = ...;
        ...
        return serializedResult;
      }
      
      @Override
      public Object deserialize(String str) {
        Object deserializedResult = ...;
        ...
        return deserializedResult;
      }
    }
    
    public class DemoClass_1 {
      private Serializable serializer;
      
      public Demo(Serializable serializer) {
        this.serializer = serializer;
      }
      //...
    }
    
    public class DemoClass_2 {
      private Deserializable deserializer;
      
      public Demo(Deserializable deserializer) {
        this.deserializer = deserializer;
      }
      //...
    }
    

    分析:往 DemoClass_1 的构造函数中,传入包含序列化和反序列化的 Serialization 实现类,但是,我们依赖的 Serializable 接口只包含序列化操作,DemoClass_1 无法使用 Serialization 类中的反序列化接口,对反序列化操作无感知,这也就符合了迪米特法则后半部分所说的“依赖有限接口”的要求。

此文章仅供学习,不可用于商业用途,如有侵权,立删

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值