Book review: A philosophy of software design (From Ch.5 to Ch.10)

Posted by Sungguk's lab on June 15, 2021

My helpful screenshot

Chapter 5. Information Hiding (and Leakage)

Discussing techniques for creating deep modules.

5.1 Information hiding

The most important technique for achieving deep module. The basic idea is that each module should encapsulated a few piece of knowledge. The knowledge is embedded in the module’s implementation but does not appear in its interface.

The information hidden usually consists of detilas about how to implement some mechanism.

어떻게 구현햇냐에 대한것은 숨긴다

Usually - hidden information includes ‘data structures’ and ‘algorithms’, ‘low level details’

Information hiding reduces complexity in two ways. - hides details. abstract view of the module’s functionality. -> Reduces the congnitive load

For example, a developer doens’t needs to worry about how b-tree implemented.

Second, information hiding makes it easier to evolve the system. -> If a piece of infmration is hidden. there are no dependencies on the information outisde the module containing the information. -> 모듈화가 되어있다는 뜻

5.2 Information leakage

The opposite of information hiding is information leakage. This creates a dependency between the modules. -> 하나의 모듈이 바뀌었지만 여러가지 모듈이 같이 바껴야하는경우

Information leakage is one of the most important red flags in software design.

정의에 따르면 interface를 통해 정보가 공유되는것도 information leakage다. 그래서 simpler interfaces를 하는게 좋다.

if you encounter information leakage between classes, ask yourself “how can I reorganize these classes so that this particular piece of knowledge only affects a single class?” If the affected classes are relatively small and closely tied to the leaked information, it may make sense to merge them into a single class. -> 영향받는 두 클레스가 작은 사이즈라면, 클레스를 합치는것.

or pull the information out of all of the affected classes and create a new class that encapsulates just that information. -> 공통된 정보를 빼내서 클래스를 따로 만드는것. 그 새로만든 인터페이스가 간단하고 encapsulate할수있어야한다.

5.3 Temporal decomposition(시간적 분해)

Temporal decomposition is one common cause of information leakage. In temporal decomposition, the structure of a system corresponds to the time order in which operation will occur.

시간순서대로 시스템 구조가 만들어져있다.

For example, reads a file, modifies the contents of the file, and then writes the file out again. With temporal decomposition, this applicaiton might be broken into three classes: read, modifiy, write.

The solution is to combine the core mechanism for reading and writing files into a single class.

읽기 쓰기를 하나의 클래스로 만든다.

It’s easy to fall into the trap of temporal decomposition, because the order in which operations must occur is often on your mind when you code. However, most design decisions manifest htemselves at several different times over the life of the application.

시간순서대로 클래스를 만들지만 실제로는 어플리케이션 동작동안 여러번 나타난다.

Order usually does matter, so it will be reflected somewhere in the application. However, it shouldn’t be reflected in the module structure unless that struct is consistent with information hiding.

When designing module, focus on the knowledge that’s needed to perform each task, not the order in wwhich tasks occur.

5.4 Example: HTTP server

5.5 Example: too many classes

The most common mistake made by students was to divide their code into a large number of shallow class, which led to information leakage between the class.

The first class read the request from the network connection into a string. The second class parsed the string.

This is good example of a temproal decomposition (First we read the request, then we parse it.)

an http request can’t be read without parsing much of the message. -> 즉 그냥 읽을려고해도 메시지를 파싱해야한다. headers must be parsed into order to compute the toal request lenght.

parsing code was duplicated in both classes. this approach also created extra complexity for caller, who had to invoke two methods in different classes, in a particular order, to receive a request.

Because the classes shared so much information, it would have been better to merge them into a single class that handles both request reading and parsing.

It isolates all knowledge of the request format in one class, and it also provide a simpler interface to caller.

Information hiding can often be improved by making a class slightly larger

5.6 Example: HTTP Parameter handling

5.8 Information hiding with a class

Information hidning can also be appplied at other levels in the system, such as within a class. Try to design the private methods hide it from the most of classes. Try to minimize the number of places where each instance variable is used. If you can reduce the number of places wwhere a variable is used, you will eliminiate dependencies within the class and reduce its complexity.

변수가 사용되는곳을 줄여라

5.9 Taking it too far

If the information is needed outside the module then you must not hide it.

작동에 진짜 영향을 주는 중요한거라면 parameter로 expose하여야한다.

Chapter 6. General-Purpose Modules are Deepper

A new module should be general-purpose or special-purpose.

a broad range of problems 를 처리해야한다고 할수도있다. 이 경우는 예상치못한 케이스를 발견할수있다. 이런경우 investment mindset으로 접근하는것이고 나중을 위해 시간을 더 쓰는것이다.

On the other hand, we know that it’s hard to predict the future needs of a software system, so a general-purposoe solution might include facilities that are never actually needed. 그리고 너무 제너럴하게 하면 오늘당장 해결해야할 문제를 푸는데 적절하지않을수있다.

if you take special-purpose approach, you can always refactor it to make it general-purpose. The special-purpose approach seems consistent with an incremental approach to software development.

6.1 Make classes somewhat general-purpose

Somewhat general-purpose fashion. - The module’s functionality should reflect your current needs, but its interface should not. Instead, the interface should be general enough to support multiple uses.

interface는 제너를하게 해서 나중을 대비한다.

Don’t build so general-pupose that it is difficult to use for your current needs.

6.4 Generality leads to better information hiding

The general-purpose approach provides a cleaner separation between the text and user interface classes.

General-purpose로 하니 이터페이스는 일반적인 것을 구현하게되고, 자연스럽게 구현 클래스와 분리가 된다. 예를들면 Text class는 interface 가 어쩌든 관심이 없다. 백스페이스키를 어떻게 다루던.

a developer working on the user interface only needs to learn a few simple methods, which cna be reduced for a variety of puprposes.

Backspace를 어떻게 할것인가 하는건 general하지않지않은가?

When the details are important, it is better to make them explicit and as obvious as possible, such as the revised implementation of the backspace operation. Hiding this information behind an interface just creates obscurity.

디테일이 진짜 중요한거면 노출하는게 혼란(obscurity) 를 줄여준다.

6.5 Questions to ask yourself

It is easiler to recognize a clean general-purpose class design than it is to create one.

내가 직접하는것보다 옆에서 보는게 더 쉽게 판단할수있다

What is the simplest interface that will cover all my current needs?

Reducing the number of methods makes sense only as long as the API for each individual method stays simple; if you have to introduce loots of additional arguments in order to reduce the number of methods, then you may not really be simplifying things.

In how many situations will this methods be used?

딱 한번 사용된다면 의심해야한다. special-purpose이므로

Is this API easy to use for my current needs?

너무 general할 경우 발생

6.6 Conclusion

General-purpose interfaces have many advantages over special-purpose ones. They tend to be simpler, with fewer methods that are deeper. They also provide a cleaner separation between classes, whereas special-purpose interfaces tend to leak information between classes. Making your modules somewhat general-purpose is one of the best ways to reduce overall system complexity.

Chapter 7. Different Layer, Different Abstraction

Higher layer use the facilities provided by lower layers. In a well-designed system, each layer provides a different abstraction from the layers above and below it.

If a system contains adjacent layers with similar abstraction, this is a read flag that suggests a problem with the class decomposition.

layer의 관계가 불분명하면 complexity가 증가한다. 인접한 layer가 같은 레벨의 abstraction을 제공하는경우

7.1 Passing-through methods

A pass-through method is one that does little except invoke another method. This typically indicates that there is not a clean division of responsibility between the classes.

단순히 변수전달만하는 함수들

Pass-through methods make classes shallower: they increase the interface complexity of the class, which adds complexity, but they don’t increase the total functionality of the system.

Pass-through methods indicate that there is confusion over the division of responsibility between classes.

“Exactly which features and abstractions is each of these classes responsible for?” You will probably notice that there is an overlap in responsibility between the classes.

Teh solution is to refactor the classes so that each class has a distinct and coherent set of responsibilities.

Solution: Expose the lower level class directly to the callers of the higher level class.

My helpful screenshot

7.2 When is interface duplication OK?

Not always bad. The important thing is that each new method should contribute significant functionality. Pass-through methods are bad because they contribute no new functionality.

One example where it’s useful is ‘dispatcher’. A dispatcher is a method that uses its arguments to select one of several other methods to invoke.

Dispatcher는 argument 를 판단해서 각각의 함수들을 호출해주는 함수이다.

1
2
3
4
5
6
func MyDispatcher(code) {
  if transport == 'php'
    phpInterpreter(code)
  if transport == 'javascript'
    javascriptInterpreter(code)
}

Another example is interfaces with multiple implementations.

SSD, Hard disk 처럼 구현은 다르지만 interface는 같은경우

7.3 Decorators

The decorator design pattern is one that encourages API duplication across layers.

A decorator object takes an existing object and extends its functionality. 기존것을 변경하지않고 확장하는것

The motivation for decorators is to separate special-purpose extensions of a class from more generic core.

작은 기능을 더하기 때문에 shallow할수밖에 없다.

Before creating a decorator class:

  • Could you add the new functionality directly to the underlying class.
  • If the new functionality is specialized for a particular use case, would it make sense to merge it with the use case, rather than creating a separate class?
  • Could you merge the new functionality with an existing decorator, rather than creating a new decorator? - deeper decorator rather multiple shallow ones.

7.5 Pass-through variables

Pass-through variable is a variable that is pass down through a long chain of methods. The information is only needed by a low-level methods, but it is passed down through all the method on the path.

Pass-through variables add complexity because they force all of the intermediate methods to be aware of their existence even though the methods have no use for the variables.

  • 이미 공유되고 있는 object가 있는가?
  • global variable에 넣기 (테스트할때 유용)

  • A context stores all of the application’s global state. There is one context object per instance of the system. the context allows multiple instance of the system to context in a single progress. Each process has its own context. The problem is context itself a path-through variable.

When a new object is created, the creating method retrieves the context reference from its object and passes it to the constructor for the new object. The context makes it easy to identify and manage the global state of the system, since it is all stored in one place. The context is also convenient for testing.

Disadvantage: It may not be obvious why a particular variable is present, or where it is used. Contexts may also create thread-safety issues; make it immutable.

Chapter 8. Pull Complexity Downwards

Chapter 9. Better Together Or Better Apart?

Given two piece of functionality, should they be implemented together in the same place, or should their implemetations be separated?

When deciding whether to combine or separate, the goal is to reduce the complexity of the system.

The act of subdividing creates addtional 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.
  • 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. IF the components are truly independent, then separation is good: it allows the developer to focus on a single component at a time, without being destracted by the other components.
  • Subdivision can result in duplication.

Indications that two pieces of code are related:

  • They share information - 파라미터가 의존한다던지..
  • They are used together
  • They overlap conceptually - 예를들어 string maipulation하는 함수들
  • It is hard to understand one of the pieces of code without looking at the other.

9.1 Bring together if information is shared

예를들어, parsing과 reading을 다른곳에서 할필요가있나? read하더라도 parsing의 대부분을 해야한다.

9.2 Bring together if it will simplify the interface

A모듈에서 B모듈로 전달한다면, 합쳐질수있다. 그리고 합쳐지면 사용자들이 A다음에 B를 호출해야하는걸 몰라도된다.

9.3 Bring togehter to eliminate duplication

If you find the same pattern of code repeated over and over, see if you can reorganize the code to eliminate the repetitioon. One approoach is to factor the repeated coode out.

This appraoch is most effective if the repeated coode snippet is long and the replacement method has a simple signature. -> 한두줄이면 그냥 써라

파라미터가 많아진다면 가치가 떨어질수있다.

Another way to eliminate duplicaiton is to refactor the code so that the snippet in question only needs to be executed in one place. -> 예를들어 에러핸들링을 한 함수로

9.4 Separate general-purpose and special-purpose code

여러목적으로 구현이 되어있다면, General purpose 를 만들고 거기에 special purpose를 더한다.

Chapter 10. Define Errors Out Of Existence

Developers often define exceptions without considering how they will be handled.

The key is to reduce the number of places where exceptions must be handled; in many cases the semantics of operations can be modified so that the normal behavior handles all situations and here is no exceptional conditions to report.

구현을 수정함으로써 일반적인 처리루틴으로 예외적인 상황을 줄일수있다.

10.1 Why exceptions add complexity

Exception - any uncommon condition that alter the normal flow of control in a program.

Exceptions can occur even without using a formal exception reporting mechanism, such as when a method returns a special valuue indiciating that it didn’t complete its normal behavior.

Large systems have to deal with many exceptional conditions, particularly if they are distributed or need to be fault-tolerant. Exception handling can account for a significant fraction of all the code in a system.

Excepption handling code is inherently(태어날때부터) more difficult to write than normal-case code.

When an exception occurs,

A) Move forward and complete the work in progress in spite of the exception.

B) Abort the operation in progress and report the exception upwards.

However, aborting can be complicated because the exception may have occurrred at a point where system state is inconsistent; the exception handling code must restoer consistency, such as by unwinding any changes made before the exception occurred. (exception 발생전으로 복구시켜야하잖아..)

To prevent an unending cascade of exceptions, the developer must eventually find a way to handling exceptions without introducing more exceptions.

Language support for exceptions tends to be verbose and clunky, which makes exception handling code hard to read. (일이 점점 커진다)

It’s difficult to ensure that exception handling code really works. Some exceptions, such as I/O errors, can’t easily be generated in a test environment, so it’s hard to test the code that handles them. Exceptions don’t occur very often in running systems, so exception handling code rarely executes. Bugs can go undetected for a long time.

When exception handling code fails, it’s difficult to debug the problem, since it occurs so infrequently.

10.2 Too many exceptions

Programmers exacerbate the problems realted to execption handling by defining unnecessary exceptionos.

Most programmer are taught to detact and report errors it; they often interpret this to mean “the more errors detected, the better”

This leads to an over-defensive style where anything that looks even a bit suspicious is rejected with an excepption. and it increases the complexity of the system.

It’s tempting to use exceptions to avoid dealing with difficult situations; However, if you are having trouble figuring out what to do for the particular situation, there’s a good chance that the caller won’t know what to do either.

The exceptions thrown are part of its interface. -> The more exceptions, the more complex interface.

Throwing exceptions is easy; handling them is hard.

The best way is to reduce the number of places where exceptions have to be handled.

This leads to