设计原则之【依赖反转原则】

设计原则是指导我们代码设计的一些经验总结,也就是“心法”;面向对象就是我们的“武器”;设计模式就是“招式”。

以心法为基础,以武器运用招式应对复杂的编程问题。

来吧,通过生活中一个小场景,一起系统学习这6大设计原则。

SOLID原则--SRP单一职责原则

SOLID原则--OCP开放封闭原则

SOLID法则--LSP里式替换原则

SOLID原则--ISP接口隔离原则

SOLID原则--DIP依赖反转原则

LOD迪米特法则

表妹让我帮她修收音机,把我给整懵了...

表妹:哥啊,我的电脑蓝屏死机了😓

我:我帮你看看。

一顿操作...

我:你这里有两个内存条,但是其中一根坏了,我现在把它给卸了,暂时用那根好的。

表妹:哇!果然可以了,真​棒👍对了,哥啊,我老爸有台82年的收音机,坏了好一阵子了,你也帮他修修呗?

我:哈...?这,我就不会啦。

表妹:为啥?电脑都会,小收音机怎么可能难倒你呢?

我:哈哈,因为,它违背了依赖反转原则。

表妹:哈?啥意思?


 

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

如下图所示:

 

为什么收音机很难修呢?

因为各个部件相互依赖,如左图所示,任何问题都可能涉及其他部件,不懂的人根本没法修。但是电脑不同,因为无论主板、CPU、内存、硬盘都是针对接口设计的,如右图所示,那么,不管哪一个出问题,都可以在不影响别的部件的前提下,进行修改或替换。

比如,现在我们需要实现一个披萨店,该店售有芝士披萨、海鲜披萨、超级至尊等品种。可能我们很容易想到,披萨店是上层模块,具体的披萨品种是下层模块,如果我们把披萨店和披萨品种的关系画成一张图,应该是这样的:

 

接下来,我们来看一下代码实现:

public class PizzaStore {

   public void cheesePizza() {
       System.out.println("芝士披萨");
  }

   public void seafoodPizza() {
       System.out.println("海鲜披萨");
  }
   
   public void superSupreme() {
       System.out.println("超级至尊");
  }
}

我们来模拟上层调用:

public static void main(String[] args) {
   PizzaStore pizzaStore = new PizzaStore();
   pizzaStore.cheesePizza();
   pizzaStore.superSupreme();
   pizzaStore.seafoodPizza();
}

如果该店业务的扩展,新增很多品种,那么,这个时候,就需要从底层实现到高层调用依次地修改代码。

我们需要在PizzaStore类中新增披萨的品种,也需要在高层调用中增加调用,这样一来,系统发布后,其实是非常不稳定的。虽然这个例子比较简单,修改也不会引入新的bug,但是,实际项目会复杂很多。

最理想的情况是,我们已经编写好的代码可以“万年不变”,这就意味着已经覆盖的单元测试可以不用修改,已经存在的行为可以保证保持不变,这就意味着稳定。任何代码上的修改带来的影响都是有未知风险的,不论看上去多么简单。

如何降低高低层代码之间的耦合?

比如,商品经济的萌芽时期,出现了物物交换。如果你要买一台手机,卖手机的人让你拿一头猪来换,但是你手里没有猪,这时,你就要去找一个卖猪的老板,但是他要你拿羊来跟他换,你也没有羊,继续去找卖羊的人...

你看,这一连串的对象依赖,造成了严重的耦合灾难。解决这一问题的最好办法就是,买卖双方都依赖一个抽象--货币,通过货币来进行交换,这样一来耦合度就大为降低了。

我们现在的代码是上层直接依赖底层实现,现在我们需要定义一个抽象的IPizza接口,来对这种强依赖进行解耦。如下图所示:

 

我们再来看一下代码:

先定义一个Pizza的抽象接口IPizza:

public interface IPizza {
   void pizza();
}

接下来就是不同的品种的实现类:

public class cheesePizza implements IPizza {
   @Override
   public void pizza() {
       System.out.println("芝士披萨");
  }
}

public class seafoodPizza implements IPizza {
   @Override
   public void pizza() {
       System.out.println("海鲜披萨");
  }
}

public class superSupreme implements IPizza {
   @Override
   public void pizza() {
       System.out.println("超级至尊");
  }
}

这时上层PizzaStore类依赖IPizza接口即可:

public class PizzaStore {
   public void sellPizza(IPizza p) {
       p.pizza();
  }
}

接下来就是上层调用:

public static void main(String[] args) {
   PizzaStore pizzaStore = new PizzaStore();
   pizzaStore.sellPizza(new cheesePizza());
   pizzaStore.sellPizza(new seafoodPizza());
   pizzaStore.sellPizza(new superSupreme());
}

你看,在这种设计下,无论该披萨店的品种怎么扩增,只需新建一个该品种的实现类即可,而不需要修改底层的代码。后面,如果该披萨店的业务继续扩大,除了卖披萨,还卖其他小吃,饮料酒水等,同样,只需分别抽象出小吃和饮料酒水两个接口,让上层调用只依赖这些接口即可。

总结

以抽象为基准比以细节为基准搭建起来的架构要稳定得多。

因此,在拿到需求后,要面向接口编程,先顶层设计再细节地设计代码结构。

好啦,每一种设计原则是否运用得当,应该根据具体业务场景,具体分析。

参考

极客时间专栏《设计模式之美》

《大话设计模式》

https://www.jianshu.com/p/c3ce6762257c

https://www.wmyskxz.com/2019/11/18/tan-yi-tan-yi-lai-dao-zhi-yuan-ze/

posted @ 2022-02-25 11:55  Gopher大威  阅读(416)  评论(2编辑  收藏  举报