第九章：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
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.
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.
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: 可想象地，可理解地
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.
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.
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.
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.