设计原则之【依赖反转原则】
设计原则是指导我们代码设计的一些经验总结,也就是“心法”;面向对象就是我们的“武器”;设计模式就是“招式”。
以心法为基础,以武器运用招式应对复杂的编程问题。
来吧,通过生活中一个小场景,一起系统学习这6大设计原则。
表妹让我帮她修收音机,把我给整懵了...
表妹:哥啊,我的电脑蓝屏死机了😓
我:我帮你看看。
一顿操作...
我:你这里有两个内存条,但是其中一根坏了,我现在把它给卸了,暂时用那根好的。
表妹:哇!果然可以了,真棒👍对了,哥啊,我老爸有台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 {
这时上层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