Notes from A Philosophy Of Software Design
Chapter #2: The nature of complexity
Software complexity can manifest itself in three different ways:
- Amplification of change: When a small change requires changing multiple parts of the code base
- Cognitive load: The amount of information required to make a change without regression (fat APIs, dependencies, inconsistancies)
- Unknown unknowns: The things that would change as a result of the intended change without being apparent in the first place.
The things that cause complexity:
- Dependencies (between modules)
- Obscurity (bad naming, unknown unknowns, unclear intentions or side effects, lack of documentation etc)
The method signature creates a dependency between the implementation and the invokation
Chapter #4: Modules should be deep
A module is a combination of its interface and its implementation.
The interface is composed of a formal (signature, API) and an informal (what’s the required knowldge to understand the interface behavior) information.
Abstraction is a simplified view of a software entity that hides the unimportant details and shows the important ones.
The interface should have a balance in the amount of details/information it exposes to avoid either obscurity or cognitive load; both causes of complexity.
A deep module is a software entity that has a small interface with a deep set of functionality. A deep module allows us to think of the ratio of the cost of learning the interface vs the benefit we reap from its functionality.
The interface of a module represents the complexity that the module imposes on the rest of the system. Modules that have a large interface for little functionality contribute negatively to increasing the complexity of the system.
Chapter #5: Information hiding & leakage
A module encapsulates a piece of knowledge/details of a mechanism that should not surface in an interface. (e.g TCP implementation, B-Tree) Hiding unimportant information helps to design a deeper module. Information leakage is a design decision that is reflected in different modules. This increases the complexity because every change will affect the modules where this design decision is visible (amplification of change). Information leakage is when an information surfaces in the interface or two modules share a dependency that is not apparent in its interface (a backdoor). Splitting code in different entities that handle separate logic/information based on their execution order will result in information leakage (implicit coupling). Avoid exposing internal state/structure in the interface because any change made to the internal details will require a change in all consumers of the module. Private methods should also hide information from the rest of the module.
Chapter #6: General purpose modules are deeper
Keep the following in mind while designing a new module:
• Write special purpose modules with generic interfaces (Open-Closed principle). • General purpose modules help us hide information. • We need to determine which modules need to know what & when while designing software. • Find the simplest interface that covers all needs without changing arguments. • How many situations will this public method be used in. • Is the general purpose API easy to use?
Chapter #7: Different layer, different abstraction
Pass through methods indicate that there is not enough/good separation of concerns between the entitites. When in such situation, consider either grouping two classes into one or make the caller access the lower-level class directly (without the extra layer). Dispatchers are good use case for pass through classes/methods though because they allow us to separate modules cleanly. The decorator pattern, on the other hand, is a prime example of why pass-throughs should be avoided. Instead, try to either implement the feature of the module as a standalone or merge it with an existing decorator or class (through composition?) Pass through modules add complexity because, remember(!), each module contributes to the system complexity via its interface. And pass-through modules add complexity without any benefit.
Chapter #8: Pull complexity downwards
A simple interface is more important than a simple implementation because it’s better for the author to “suffer” than the consumers of the modules. Configuration parameters indicate that the system does not solve the problem fully. Pulling the complexity down (from the interface to the implementation) reduces the overall system complexity.
Chapter #9: Better together or better apart?
Separating code can increase the system complexity as you add more interfaces. Group together code the shares the same information. Group together to simplify interfaces. No special purpose code should be in general purpose (adding exceptions/conditions) It’s ok to have long methods if they have small/clean signatures/interfaces A method should do one thing, its interface is simpler than its implementation and a clean interface. Split methods only if doing so doesn’t change the interface of the original method and both the “children” and original method are not aware of each other. Joining methods together can bring separated information together which reduces the complexity by encapsulating the knowldge. If the split methods cannot be understood independently, then splitting shouldn’t happen in the first place. The only reason for splitting units of code is to reduce complexity.
Chapter #10: Define errors out of bound
Exceptions alter the flow of code and add complexity. They can invite even more exception handling (handling exceptions of exceptions). Exception handling code is too verbose Exception code can be hard to test and unreliable (im not sure about this). Throwing errors to the caller only passes the problem to someone else who likely won’t know what to do with the exception either. Exceptions are part of the interface, thus they complicate the system especially that they are bubbled up in the system, which adds complexity to the caller in higher level. Throwing exceptions makes it harder to use the API because you have to add error handling which increases complexity. Exception masking is when exception is handled deeper in the system like in TCP (retrying connection establishment). This reduces complexity by pulling it down. Exception aggregation is when exceptions are handled in one spot. This is good for encapsulation and information hiding. Therefore, top-level exception handling encapsulates the knowledge about error generation without knowing about specific error messages. You can also just crash and abort the application.
Chapter #11: Design it twice
Consider multiple designs for every major design decision. WHen designing a class, think of the different interfaces it can have. Make props and cons of each design based on how the system will be used and how much complexity the new entity will add to the overall system. Design-it-twice approach teaches how to design better software and recognize good design from a bad one.
Chapter #12: Why write comments? The four excuses
In-code documentation improves the system design and without it the design loses its value. Comments are important for abstraction as they hide the complexity of the system from the maintainer (you don’t need to read the entity body to understand what it does - remember that code holds the truth though). Comments should be part of the design process. Large changes to the code or design should include changes to the documentation as well. Comments/documentation should capture the details that cannot be expressed in code. Comments help reduce complexity by reducing cognitive load to allow developers to work accurately and to reduce unknown-unknowns by giving some context.
Chapter #13: Comments should describe things that aren’t obvious from the code
Comments should not repeat waht the code does. Do not use the same words in the comments as in the code. Comments that describe the code from a higher level provide information on the intuition behind the code. Whereas comments that describe code from a lower level provide precision on the exact meaning of the code.
Chapter #14: Choosing names
Good names should be able to convey the entity description even without reading the documentation or the code. Names are a form of abstraction (they show only important information) If you cannot find a good name for a variable, it might not have a clear responsibility. Variable names should be precise but not too specific.
Rest of chapters:
Not much that I didn’t know so I didn’t feel like taking notes.