Information hiding(and leakage)
隐藏信息
在设计深模块时,最重要的技术就是隐藏信息。基本思想是在设计模块的时候,应该带有为什么这么设计的信息,这些信息应该出现在实现上,而不是在接口上。
一个模块背后的隐藏信息通常包括如何实现一些机制的细节。下面是一些常见的隐藏信息:
- 如何在B-Tree中存储信息,如何高效地访问
- 如何在一个文件中进行物理块和逻辑块的映射
- 如何实现TCP协议
- 如何在多核处理器上调度线程
- 如何转换json文档
隐藏信息主要包括数据结构和算法。
也可以包含底层信息,比如一页的大小;也可以包含一些高层的信息,比如预计大多数文件都很小
隐藏信息在下面两个方面减少复杂性:
1.简化了接口,比如使用B-Tree的开发者就不需要管如何使树平衡 - 如果信息被隐藏,在该模块的外部就不会对该信息产生依赖,所以和该信息相关的修改就不会影响到其他模块;比如需要修改TCP协议,我们只需要修改实现即可,不会对其他的模块产生影响,因为我们隐藏了信息
当我们设计新的模块时,应该尽量地隐藏信息,从而使接口足够深
信息泄露
信息隐藏的反面就是信息泄露
信息泄露的原因就是在多个模块中都涉及到了同一个设计决定,这就使这些模块之间产生了依赖,当需要修改设计决定时,我们就需要修改和该设计决定相关的全部模块。
比如一个类用来读取指定格式的文件,另外一个类用来向指定格式的文件进行写入,如果此时文件的格式变化,那么两个类都需要重新设计。
解决信息泄露的比较好的方法有
- 如果受到影响的类都比较小,那么可以考虑将他们合并到同一个类中
- 将导致影响的那部分信息抽取出来,单独创建一个类,来包含这个信息,但是这种方法只有在你能找到一个简单的接口来抽象信息的时候才有效,如果新创建的类通过接口来暴露大部分的信息,那么仍然会造成信息泄露
时间分解
导致信息泄露的原因中,最常见的就是时间分解。举一个例子,一个系统包括读取文件,修改文件内容,然后写入文件,如果使用时间分解,那么就会产生三个类:一个类用来读取文件,一个类用来修改文件内容,一个类用来写入文件
这就会导致读取文件和写入文件会和文件的格式产生耦合,从而导致消息泄露。这种情况下的解决方法就是将读文件和写文件的核心机制放到一个类中,在读取和写入过程中去调用那个类。
在设计系统的时候,应该重点关注执行每个任务需要的信息而不是这些任务执行的顺序
例子:http服务器
举一个例子,实现一个http服务器来接收请求和回送响应
在实现过程中,常见的错误主要有:
- . 使用两个不同的类来接收http请求,其中一个类使用字符串来接收来自网络的连接,第二个类解析这个字符串,这就是典型的时间分解问题:首先接收,然后解析。在读取http请求的时候,如果完全不对http请求的内容进行转化,是无法接收http请求的,Content-length字段表示请求体的长度,所以在接收http请求时,我们必须知道这个http请求的内容有多长,因此必须对http的请求头进行解析。这就导致了两个类中都需要了解http请求的格式,并且解析代码会重复使用。
解决这个问题的方法就是将读取http请求和解析http请求放在一个类中,这样做的好处有:
- . 将http请求头格式这部分的信息隐藏在了当前类中
- . 并且对使用人来说更加方便,只需要调用一个类的方法即可
通过让一个类稍微变大,会提高消息隐藏的效果
这样做的原因是: - 将和某种能力相关的代码都放到一个类中,这样该类就包含该种能力的所有代码
- 和将计算步骤中的不同步骤都放到不同的方法中,将全部的计算步骤都放到一个方法中会使接口更加简单,使得接口更加深
处理http请求的参数
在获得解析后的http请求参数时,很多人会直接返回一个map
public Map<String, String> getParams() {
return this.params;
}
首先这个接口很浅,并且暴露了内部用来存储参数的数据结构,如果改变了内部的数据结构,那么涉及到的接口都需要重新修改。另外,调用者还需要注意不能修改map中的内容,因为会改变http请求的状态
改进方法是将数据结构部分的信息隐藏在接口中,
public String getParameter(String name) { ... }
public int getIntParameter(String name) { ... }
隐藏底层使用到的数据结构和算法
http响应的默认值
http响应的http协议的版本必须和http请求协议的版本一致。
一种常见的错误方法是在响应接口中包含http协议的版本号,由调用者来指定版本号。并且一般调用者并不知道需要指定什么版本,如果调用者指定了版本,那么就在http库和调用者之间产生了泄露。因此针对一般情况,应该使用默认值
接口的设计应当使常见的调用情况尽量简单,一些较少情况,再由调用者来覆盖这个默认值
在任何时候,尽管调用者没有调用,类都应该做正确的事,比如io中的BufferedInputStream,就是一个反面例子