第九章:Better Together Or Better Apart?

one of the most fundamental questions in software design is this: given two pieces of functionality, should they be implemented together in the same place, or should their implementations be separated?
This question applies at all levels in a system, such as functions, methods, classes, and services.
when deciding whether to combine or separate, the goal is to reduce the complexity of the system as a whole and improve its modularity.

It might appear that the best way to achieve this goal is to divide the system into a large number of small components: the smaller the components, the simpler each individual component is likely to be.


subdividing creates addition complexity

However, the act of subdividing creates additional complexity that was not present before subdivision:

  • some complexity comes just from the number of components
    the more components, the harder to keep track of them all and the harder to find a desired component within the large collection.
    Subdivision usually results in more interfaces, and every new interface adds complexity.
  • Subdivision can result in additional code to manage the components.
    one piece of code that used a single object before subdivision might now have to manage multiple objects.
  • Subdivision creates separation
    Separation makes it harder for developers to see the components at the same time, or event to be aware of their existence
    components are truly independent, then separation is good: focus on a single component
    there are dependenciees between the components, then separation is bad: will flipping back and forth between the components. Even worse, not be aware of the dependencies.
  • Subdivision can result in duplication: code that was present in a single instance before subdivision may need to be present in each of the subdivided components.

Bringing pieces of code together is most beneficial if they are closely related
If the pieces are unrelated, they are probably better off apart

a few indication that two pieces of code are related

  • They share information. ex. depend on the syntax of a particular type of document.
  • They are used together. counter-ex. a disk block cache will almost always involve a hash table, but hash tables can be used in many situations that don't involve block caches, these modules should be separate.
  • They overlap conceptually.
    ex. searching for a substring and case conversion both fall under the category of string manipulation
    ex. flow control and reliable delivery both fall under the category of network communication.
  • It is hard to understand one of the pieces of code without looking at the other.

Bring together if information is shared

implementing an HTTP service. the project used two different methods in different classes to read in and parse HTTP requests.
The first method read the text of an incoming request from a network socket and placed it in a string object
The second method parsed the string to extract the various components of the request.
With this decomposition, both of the methods ended up with considerable knowledge of the format of HTTP requests.
Read method couldn't identify the end of ther request without doing most of the work of parsing it. Because of this shared information, it is better to both read and parse the requests in the same place; when the two classes were combined into one, the code got shorter and simpler.

如果存在信息依赖的场景,需要放在一起

Bring together to eliminate duplication

If you find the same pattern of code repeated over and over, see if you can reorginaze the code to eliminate the repetition.

One approach is to factor the repeated code out into a separate method and replace the repeated code snippets with calls to the method.
This approach is most effective if the repeated code snippet is long and the replacement method has a simple signature.

如果只有一两行的代码,那就没有替换合并的价值
如果方法逻辑依赖自己的环境变量(有多个本地变量),那么替换方法需要复杂的签名(比如有很多引用传递参数),那就降低了它的价值

Another way to eliminate duplication is to refactor the code so that the snippet in question only needs to be executed in on place

将多段重复的代码移到一处。比如打印日志,如果每次return前都要都一个Log.info,这样Log的重复代码太多了。可以在方法收敛到方法结尾处,统一打一个log,然后return。
书中举了使用goto的例子,但是goto的语句在使用过程中可维护性和可阅读性较差,不推荐使用

Separate general-purpose and special-purpose code

If a module contains a mechanism that an be used for several different purposes, then it should provide just that one general-purpose mechanism.
It should not include code that specializes the mechanism for a particular use, nor should it contain other general-purpose mechanisms.
Special-purpose code associated with a general-purpose mechanism should normally go in a different module (typlically one associated with the particular purpose).

针对编辑器场景,
文本类提供通用的操作,当界面上需要有特殊操作时,在界面的模块中进行实现。

If the same piece of code (or code that is almost the same) appears over and over again, that's a red flag that you haven't found the right abstractions.

这是一个很好的方向来重构或者思考代码。

In general, the lower layers of a system tend to be more general-purpose and the upper layers more special-purpose.
The way to separate special-purpose code from general-purpose code is to pull the special-purpose code upwards, into the higher layers, leaving the lower layers general-purpose.
When you encounter a class that includes both general-purpose and special-purpose features, and the other layered on top of it to provide the special-purpose features.

核心魔都
encounter: 偶遇

Example: insertion cursor and selection

举了三个例子:
两个例子来说明拆分相关的代码;
一个例子说明最好合并在一起

relevant: 相关的
GUI编辑器中,插入光标和选择区
编辑器需要在文本输入的地方展示闪烁的竖直的线,表示用户输入的地方
用部分文字高亮来显示选择区,可以用来拷贝或者删除。
如果有选择区,光标会在选择去的最后位置。

这样看起来选择区和插入光标是有关联的。
举个例子,光标总是在选择区的后面,那么它们看起来可以一起被操作的。
点击并拖动它们,需要首先删除已经选择的文本,并将它们在光标位置插入进来。
因此,它们似乎从逻辑上可以用一个对象来同时管理选择区和光标。
在一个对象中保存两个位置,以及哪一个是末尾位置和是否存在选择区的布尔值。

It provided no benefit for higher-level code, since the high-level code still needed to be aware of the selection and cursor as distinct entities, and it manipulated them separatly

In this case, the selection and cursor were not closely enough related to combine them.
The cursor implementation also got simpler becouse the cursor position was represented directly, rather than indirectly through a selection and a boolean.
Instead, a new Position class was introduced to represent a location in the file.
The selection was represented with two Positions and the cursor with one.

Red Flag: Special-General Mixture

This red flag ovvures when a general-purpose mechanism also contains code specialized for a particular use of that mechanism. This makes the mechanism more complicated and creates information leakage between the mechanism and the particular use case: future modifications to the use case are likely to require changes to the underlying mechanism as well.

Example: separate class for logging

这个例子,将原来代码中打印日志的代码,单独抽出放在Logger类中,意图是将所有日志打印的部分放在一起。

相当于,原来是Logger.log()的地方,调用自己写的Logger类的方法。
而且日志的文案都隐藏在了自己写Logger类中,增加了复杂度
有人阅读到业务代码时,需要进入日志的方法确认打印的日志与业务一致;
有人看到了日志打印的方法,需要跳转到调用它的地方来理解方法的目的。

这种情况下,最好删除日志方法,将日志打印放到对应错误出现的地方。
回想自己之前的编码经历,确实产生过这样的想法:我想把打印日志相关的能力都封装到一个类中,以后关于日志的问题都在这里调整。看起来内聚了,但是真实写代码时会发现,增加了开发成本、理解成本和维护成本。所以这样的形式还不如不要。

Example: editor undo mechanism

In the GUI editor project, one of the requirements was to support multi-level undo/redo, not just for changes to the text ifself, but also for changes in the selection, insertion cursor, and view.

Some of the student projects implemented the entire undo mechanism as part of the text class. The text class maintained a list of all the undoable changes.
This approah resulted in an awkward set of features in the text class.

The core of undo/redo consists of a general-purpose mechanism for managing a list of actions that have been executed and stepping through them during undo and redo operations.

// TODO

Spliting and joining methods

Long methods tend to be more difficult to understand than shorter ones, so many people argue that length alone is a good justification for breaking up a method.
Howeverm length by itself is rarely a good reason for splitting up a method.

按照长度拆分方法不是一个好理由

In general, developers tend to break up methods too much.
Splitting up a method introduces additional interfaces, which add to complexity.
It also separates the pieces of the orginal method, which makes the code harder to read if the pieces are actually related.

根据长度拆分方法的弊端

You shouldn't break up a method unless it makes the overall system simpler;

方法拆分的目标

Spliting up a method only makes sense if it results in cleaner abstractions, overall.

best way

The best way is by factoring out a subtask into a separate method, as (b)
适用地方
This form of subdivision makes sense if there is a subtask that is cleanly separable from the rest of the original method.
the child method is relatively general-purpose: it could conceivably be used by other methods besides the parent.
如何判断
If you make a split of this form and then find yourself flipping back and forth between the parent and child to understand how they work together, that is a red flag indecating that the split was probably a bad idea.

最好地方式是从大方法中拆分出独立的小方法。
subdivision 细分
conceivably: 可想象地,可理解地

second way

The second way to break up a method is to split it into two separate methods, each visible to callers of the original mthod, as (c)
适用地方
This make sendse if the original method had an overly complex interface because it tried to do multiple things that were not closely related.
If you make a split like this , the interface for each of the resulting methods should be simpler than the interface of the original method.

如何判断

Ideally, most callers should only need to invoke one of the two new mthods; if callers must invoke both of the new methods, then that adds complexity, which makes it less likely that the split is a good idea.
The new methods will be more focused in what they do. It is a good sign if the new methods are more general-purpose than the origin method

劣势

Splits of the form shown in (c) don't make sense very often, because they result in callers having to deal with multiple methods instead of one.

avoid way

If the caller has to invoke each of the separate methods, passing state back and forth between them, then spliting is not a good idea.

如果c中的方式用着用着会最终拆分成多个浅方法,如果d

如果考虑和c的方式拆分,那么应该基于是否为调用者做了简化进行判断。

Methods containing handreds of lines of code are fine if they have a simple signature and are easy to read. These methods are deep (lots of functionality, simple interface), which is good.
If the blocks have complext interactions, it's even more important to keep them together so readers can see all of the code at once; if each block is in a separate method, readers will have to flip back and forth between these spread-out methods in order to understand how they work together.

长方法存在也没啥问题,但是核心是需要有简单的标签和易于阅读。

When designing methods, the most important goal is to provide clean and simple abstractions.
Each method should do one thing and do it completely.
The method should have a clean and simple interface, so that users don't need to have much information in their heads in order to use it correctly
The method should be deep: its interafce should be much simpler than its implementation.
If a method has all of these properties, then it probably doesn't matter whether it is long or not.

设计方法的要领:提供干净的和简单的抽象。
每个方法都是完成地独立地做一件事。

There are also situations where a system can be make simpler by joining methos together.

  • joining methods might replace two shallow methods with one deeper method;
  • it might eliminate duplication of code;
  • it might eliminate dependencies between the original methods, or intermediate data structures
  • it might result in better encapsulation, so that knowledge that was previously present in multiple palces is now isolated in a single palce;
  • or it might result in a simpler interface
intermediate: 居中的,中级的中间层的
encapsulation: 封装性
isolated: 独立的


Red Flag: Conjoined Methods

If two pieces of code are physically separated, but each can only be understood by looking at the other, that is a red flag.
If you can't understand the implementation of one method without also unserstanding the implementation of another, that's a red flag.

Conclusion

The decision to split or join medules should be based on complexity.
Pick the structure that results in the best information hiding, the fewest dependencies, and the deepest interfaces.