第7章 不同层不同抽象

不同层不同抽象

软件系统由不同的层构成, 上层使用下层提供的服务。
如果系统设计的好,每一层会提供和在它只上和之下的层的不同的抽象
比如:

  • 文件系统,最上层实现了文件的抽象。一个文件由任意大小的字节数组组成。下一层实现了内存中对磁盘块的缓存。最底层实现了设备驱动,在二级存储设备和内存之间移动磁盘块
  • 网络传输层协议,比如tcp,最上层提供的抽象就是一个从一台设备流向其他设备的字节流。底层的抽象提供了传输指定大小数据包的能力

如果一个系统,相邻层之间具有相同的抽象,那么此时就需要注意。

直穿方法

当相邻的两层具有相似的抽象时,就会导致直穿方法的问题。直穿方法就是指一个方法体内除了调用另外一个方法签名和当前方法相似的方法外,没有做太多其他事的方法。

public class TextDocument ... {
private TextArea textArea;
private TextDocumentListener listener;
...
public Character getLastTypedCharacter() {
return textArea.getLastTypedCharacter();
}
public int getCursorOffset() {
return textArea.getCursorOffset();
}
public void insertString(String textToInsert,
int offset) {
textArea.insertString(textToInsert, offset);
}
public void willInsertString(String stringToInsert, int offset) {
if (listener != null) {
listener.willInsertString(this, stringToInsert, offset);
}
}

}

这里很明显就产生了直穿方法,只是做了参数的传递,将参数传递给另外一个方法,这通常就意味着没有很好地划分类之间的职能
直穿方法会使类更加浅,它们增加了接口的复杂性,但是并没有增加系统的功能。
另外,直穿方法增加了类之间的依赖性,如果修改了TextArea中的insertString的签名,那么TextDocument中的insertString的签名也得跟着改。
当产生直穿方法的时候,应当问自己,这个功能应该由哪个类来负责?通常这个时候就会发现类的职责之间有重叠。
解决方法是重构类,使每个类负责不同的功能。

在这里插入图片描述
第一种方法是,如b所示,将底层的类直接暴露给高层的调用者。
第二种方法是,如c所示在类之间重新分配功能
第三种方法是,如d所示,将类进行合并

什么时候接口的重复是可行的

多个接口有相同的签名不一定是坏事。重要的是,每个新的方法应当贡献有意义的功能。直穿方法之所以不好,是因为他们没有贡献新的功能。
最常见接口重复的例子就是分发器。分发器使用它的参数从一堆方法中选择一个来调用;然后将参数传递给指定的方法。通常分发器的参数和待选择的方法的参数是相同的。即使是这样,分发器仍然贡献了新的功能:选择方法来调用
如果多个具有相同签名的类之间,具有不同的功能,那么接口的重复是允许的。

装饰者

装饰者设计模式鼓励在不同层之间具有相同的接口。装饰者本身持有一个已存在的对象,然后扩展该对象的功能。装饰者的接口和其持有的对象的接口是一致的,比如java中的io BufferedInputStream等。
装饰者的动机是在普通功能外,增加特殊功能,比如BufferedInputStream。但是装饰者通常会比较浅:它们通常包含大量的样本文件,通常包含直穿方法。
在创建装饰者之前,应该问问自己几个问题:

  • 是否可以将新的方法直接添加到底层使用的类中,而不是创建一个新的装饰者。如果新的方法是相对通用的方法,或者底层的类通常都会使用新的方法,比如BufferedInputStream,大多数的使用者在创建InputStream的时候都会创建BufferedInputStream
  • 是否可以直接创建一个新的类,而不是创建一个装饰者
  • 如果新的功能是专用于某个特殊用例的,是否能将该功能和用例进行合并,而不是创建一个分开的类
  • 是否能将新的方法合并到已有的装饰者中,而不是创建一个新的装饰者
  • 最后,应该问自己,新的功能是否需要包括已有的功能

接口和实现

接口和实现应当不同,如果两者有相同的抽象,类可能不会很深。在之前的文本编辑器设计中,有些人会围绕行类来设计文本类,在文本类中提供getLine和putLine,这就会使文本类难以使用,并且很浅。在上层api中,可能会在一行的中间来插入文本,或者删除一段跨越多行的文本,如果使用面向行的文本api,调用者必须切分行和合并行来实现上面的操作

直穿变量

另外一种在不同层之间api重复的表现是直穿变量,如下图所示
在这里插入图片描述
在方法的调用链中,该参数一直被传递。该参数只有在底层的api中才会使用到,但是却需要从高层api一直向下传递。直穿参数会增加系统的复杂性,它会强制方法调用链中的所有方法都需要知道它的存在,即使这些方法不会使用这个变量。如果此时需要在底层的api中多传入一个参数,那么此时就需要修改大量方法调用链中的方法来添加一个参数。
解决直穿变量比较困难,解决方法有:

  • 如果在不同方法之间存在一个共享的对象,那么可以将参数存放在该变量对象中
    在这里插入图片描述
  • 将信息存放在全局变量中,但是全局变量有一个问题,那就是无法在同一个进程中创建某个系统该全局变量的两个实例。
    在这里插入图片描述
  • 创建一个上下文变量,上下文变量中存放应用中所有的全局状态,为了避免每个函数都需要将context作为参数传入,可以在系统的主要对象中存放context的引用,比如下图,包含m3方法的类中包含一个纸箱context的引用
    在这里插入图片描述
    上下文对象统一了系统全局信息的处理并且减少了直穿变量的数量。如果需要添加一个新的全局变量,那么可以将该变量添加到上下文对象中,只有上下文类的构造方法会受到影响,其他的代码不会受到影响。另外使用上下文对象也利用代码的测试,测试代码可以直接修改上下文对象中的配置来进行测试。
    但是使用上下文对象也会引起问题,比如使用者可能会不明白为什么使用这个变量以及这个变量哪里会用到,也会引起线程安全问题。使用上下文对象的最好方法就是令上下文中的所有属性都是不变的。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值