写点什么

「架构师训练营」第 2 周作业 - 设计原则

用户头像
guoguo 👻
关注
发布于: 2020 年 06 月 17 日
「架构师训练营」第2周作业 - 设计原则

作业一:请描述什么是依赖倒置原则,为什么有时候依赖倒置原则又被称为好莱坞原则?



Dependence Inversion Principle (DIP)

High level modules should not depend upon low level modules. Both should depend upon abstractions. Abstractions should not depend upon details. Details should depend upon abstractions.



原文翻译过来,字面意思大概是说

  • 高层模块不应依赖于低层模块。他们都依赖于抽象。

  • 抽象不应依赖于细节。

  • 细节应依赖于抽象。



对于面向对象的语言而言,这里的抽象我们可理解为`interface`或`abstract class`。即高层模块中,依赖的是接口或抽象类。而低层模块也依赖于接口或抽象类。而低层模块中,完成对接口或抽象类的实现或继承。



举例说明

司机驾驶奔驰汽车的例子,一个不符合DIP原则的设计,如下所示:

不符合DIP原则的设计

问题1:

当BenzCar发生了变化,将直接影响到依赖它的Driver。



当司机需要驾驶宝马汽车、奥迪汽车的时候,如下所示:

遇到扩展时



问题2:

每增加一种汽车,就要修改Driver类。这也同时违反了开闭原则(对内部修改关闭,对外部扩展开放)



符合依赖倒置原则的实现:

符合依赖倒置原则的设计



回到前面翻译的依赖倒置原则的定义,高层模块(Driver)并不依赖于低层的模块(如BenzCar、BmwCar和AudiCar),而是都依赖于Car这个对车的抽象。Car这个抽象不依赖具体的实现,而具体的实现依赖Car这个抽象。



当发生扩展时,不管具体的BenzCar或BmwCar怎么修改,Driver及其实现均不受影响。而当新增加一种Car,比如增加HongqiCar,只需要HongqiCar实现Car接口,而无需修改Driver。



好莱坞原则

“不给我们打电话,我们会给你打电话(don‘t call us, we‘ll call you)”这是著名的好莱坞原则。



在程序的调用链上,我们把调用方称为高层,被调用方称为低层。而好莱坞原则,强调的是低层只需要管好自己的实现逻辑(即前面例子中,BenzCar只需要管理好自己的run()方法的具体实现),而不用对高层作出指示自己什么时候被调用(不用作出指示run()方法什么时候被调用)。而在具体的业务逻辑处理中,高层模块通过自己的逻辑,去涉入低层的实现。



所以,有时候依赖倒置原则也称为好莱坞原则。



作业二:请描述一个你熟悉的框架,是如何实现依赖倒置原则的。



依赖倒置原则几乎所有框架都有实现。这里就简单的拿Dubbo的SPI举例。



SPI 全称为 Service Provider Interface,是一种服务发现机制。Dubbo在运行时,动态的读取接口的实现类,因此我们对Dubbo的SPI进行扩展时,只需要实现SPI对应的接口,然后通过文本配置,完成接口的注入。



使用Dubbo中,最常见的扩展可能就是对拦截器的扩展了。比如我们做一些dubbo调用的日志、或设置一些通用的调用参数、或者跟踪调用链路等。

我们就以扩展Dubbo的拦截器作为切入点,来了解下Dubbo如何实现依赖倒置原则的。



当需要扩展注册中心时,需要完成以下3步:



  1. 实现接口

  • org.apache.dubbo.rpc.Filter



  1. 配置扩展

@Activate(group = CommonConstants.PROVIDER)
public class CustomFilter extends ListenableFilter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
// TODO 拦截相关代码
return invoker.invoke(invocation);

也可通过xml配置文件配置扩展。



  1. 配置dubbo的SPI注入

src
|-main
|-java
|-com
|-xxx
|-CustomFilter.java (实现Filter接口)
|-resources
|-META-INF
|-dubbo
|-org.apache.dubbo.rpc.Filter (纯文本文件,内容为:customFilter=com.xxx.CustomFilter)



Dubbo在启动过程中,provider在暴露服务时,会通过`ProtocolFilterWrapper`的`buildInvokerChain`生成包装后的`Invoker`,完成调用链的包装。

private static <T> Invoker<T> buildInvokerChain(final Invoker<T> invoker, String key, String group) {
Invoker<T> last = invoker;
// 读取内置的和扩展的Filter
List<Filter> filters = ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension(invoker.getUrl(), key, group);
if (!filters.isEmpty()) {
for (int i = filters.size() - 1; i >= 0; i--) {
final Filter filter = filters.get(i);
final Invoker<T> next = last;
last = new Invoker<T>() {
@Override
public Class<T> getInterface() {
return invoker.getInterface();
}
@Override
public URL getUrl() {
return invoker.getUrl();
}
@Override
public boolean isAvailable() {
return invoker.isAvailable();
}
@Override
public Result invoke(Invocation invocation) throws RpcException {
Result asyncResult;
try {
asyncResult = filter.invoke(next, invocation);
} catch (Exception e) {
// onError callback
if (filter instanceof ListenableFilter) {
Filter.Listener listener = ((ListenableFilter) filter).listener();
if (listener != null) {
listener.onError(e, invoker, invocation);
}
}
throw e;
}
return asyncResult;
}
@Override
public void destroy() {
invoker.destroy();
}
@Override
public String toString() {
return invoker.toString();
}
};
}
}
// 返回最终的链
return new CallbackRegistrationInvoker<>(last, filters);
}



这里的`ProtocolFilterWrapper`看作是上层模块,而`Filter`看作是下层模块,程序中上层模块并不关注某一个具体的Filter,他只依赖于抽象的`Filter`接口。

而下层的具体实现需要依赖抽象的Filter接口,并实现其接口。



因此,我们在扩展拦截器时,并没有去修改Dubbo的源码,而是通过外部配置完成了对象的注入。



作业三:请用接口隔离原则优化 Cache 类的设计,画出优化后的类图



问题分析:

图中所示,类Cache中包含了4个方法,分别是操作缓存数据的get/put/delete方法和管理缓存的reBuild方法。如果Cache类对外暴露,当应用程序依赖了Cache类操作缓存,就有错误调用reBuild方法的风险。我们设计时应考虑接口隔离原则。



设计思路:

使客户端调用的接口与管理系统调用的接口分离。



用户头像

guoguo 👻

关注

还未添加个人签名 2017.11.30 加入

还未添加个人简介

评论 (1 条评论)

发布
用户头像
前面2题的解答详细且清晰,非常好。最后一题的图的类之间的关系可以再优化一下。
2020 年 06 月 20 日 20:46
回复
没有更多了
「架构师训练营」第2周作业 - 设计原则