A Philosophy of Software Design
Autor
John Ousterhout
Opinión
Este libro del creador de Tcl habla sobre la complejidad y el diseño de software, no basta con saber programar ya que lo mejor (y lo más divertido) es diseñar una pieza simple de código que consiga realizar el mejor trabajo.
Habla de la programación “táctica” versus la programación “estratégica”, la primera es ir trabajando de funcionalidad en funcionalidad (tipo agile) en donde se hace lo mínimo para que funcione (estilo TDD) y hace que el trabajo sea más rápido al inicio. Y la segunda trata de un diseño por cada abstracción donde se toma el tiempo para diseñar con detalle una solución profunda y simple lo que hace que al principio se invierta un poco más de tiempo. No sé si son términos de el pero me parecen muy acertados.
Habla de muchos más temas y usa suficientes ejemplos aunque casi todos los (creo que todos) están ilustrados con Java y como no soy un experto pues sólo lo básico comprendí de ellos, lo bueno es que todo va bien explicado en los párrafos y comentarios.
No sé si esto califique como una filosofía de software, lo que si creo es que es una excelente colección de guías y buenas prácticas para mejorar el diseño y desarrollo de software, además que este autor hace valer sus puntos.
Algunas citas
Programmers aren’t bound by practical limitations such as the laws of physics; we can create exciting virtual worlds with behaviors that could never exist in the real world.
Isolating complexity in a place where it will never be seen is almost as good as eliminating the complexity entirely.
One of the most important goals of good design is for a system to be obvious.
Complexity is caused by two things: dependencies and obscurity.
dependency exists when a given piece of code cannot be understood and modified in isolation
Dependencies lead to change amplification and a high cognitive load.
Obscurity creates unknown unknowns, and also contributes to cognitive load
The bottom line is that complexity makes it difficult and risky to modify an existing code base.
you should not think of “working code” as your primary goal, though of course your code must work. Your primary goal must be to produce a great design, which also happens to work. This is strategic programming.
The longer you wait to address design problems, the bigger they become; the solutions become more intimidating, which makes it easy to put them off even more. The most effective approach is one where every engineer makes continuous small investments in good design.
In order to manage dependencies, we think of each module in two parts: an interface and an implementation.
Typically, the interface describes what the module does but not how it does it
For the purposes of this book, a module is any unit of code that has an interface and an implementation.
The extreme of the “classes should be small” approach is a syndrome I call classitis, which stems from the mistaken view that “classes are good, so more classes are better.”
The opposite of information hiding is information leakage
Information leakage occurs when the same knowledge is used in multiple places, such as two different classes that both understand the format of a particular type of file.
information hiding can often be improved by making a class slightly larger.
The best features are the ones you get without even knowing they exist.
it’s important to recognize which information is needed outside a module and make sure it is exposed.
When decomposing a system into modules, try not to be influenced by the order in which operations will occur at runtime; that will lead you down the path of temporal decomposition, which will result in information leakage and shallow modules.
One of the most important elements of software design is determining who needs to know what, and when.
decorator classes tend to be shallow: they introduce a large amount of boilerplate for a small amount of new functionality.
Without discipline, a context can turn into a huge grab-bag of data that creates nonobvious dependencies throughout the system.
Contexts may also create thread-safety issues; the best way to avoid problems is for variables in a context to be immutable.
Ideally, each module should solve a problem completely; configuration parameters result in an incomplete solution, which adds to system complexity.
When developing a module, look for opportunities to take a little bit of extra suffering upon yourself in order to reduce the suffering of your users.
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.
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.
The decision to split or join modules should be based on complexity. Pick the structure that results in the best information hiding, the fewest dependencies, and the deepest interfaces.
Exception handling is one of the worst sources of complexity in software systems.
The best way to reduce the complexity damage caused by exception handling is to reduce the number of places where exceptions have to be handled.
Overall, the best way to reduce bugs is to make software simpler.
Exception aggregation works best if an exception propagates several levels up the stack before it is handled; this allows more exceptions from more methods to be handled in the same place. This is the opposite of exception masking: masking usually works best if an exception is handled in a low-level method.
One way of thinking about exception aggregation is that it replaces several special-purpose mechanisms, each tailored for a particular situation, with a single general-purpose mechanism that can handle multiple situations.
Special cases of any form make code harder to understand and increase the likelihood of bugs.
The design-it-twice approach not only improves your designs, but it also improves your design skills. The process of devising and comparing multiple approaches will teach you about the factors that make designs better or worse. Over time, this will make it easier for you to rule out bad designs and hone in on really great ones.
good comments can make a big difference in the overall quality of software; it isn’t hard to write good comments; and (this may be hard to believe) writing comments can actually be fun.
If users must read the code of a method in order to use it, then there is no abstraction
Developers should be able to understand the abstraction provided by a module without reading any code other than its externally visible declarations.
Every class should have an interface comment, every class variable should have a comment, and every method should have an interface comment.
Occasionally, the declaration for a variable or method is so obvious that there is nothing useful to add in a comment (getters and setters sometimes fall in this category), but this is rare; it is easier to comment everything rather than spend energy worrying about whether a comment is needed.
If the information in a comment is already obvious from the code next to the comment, then the comment isn’t helpful. One example of this is when the comment uses the same words that make up the name of the thing it is describing.
Comments augment the code by providing information at a different level of detail.
The main goal of implementation comments is to help readers understand what the code is doing (not how it does it).
In addition to describing what the code is doing, implementation comments are also useful to explain why.
When documenting variables, focus on what the variable represents, not how it is manipulated in the code.
The goal of comments is to ensure that the structure and behavior of the system is obvious to readers, so they can quickly find the information they need and make modifications to the system with confidence that they will work.
poor name choices increase the complexity of code and create ambiguities and misunderstandings that can result in bugs.
you shouldn’t settle for names that are just “reasonably close”. Take a bit of extra time to choose great names, which are precise, unambiguous, and intuitive. The extra attention will pay for itself quickly, and over time you’ll learn to choose good names quickly.
Take a bit of extra time to choose great names, which are precise, unambiguous, and intuitive. The extra attention will pay for itself quickly, and over time you’ll learn to choose good names quickly.
The process of choosing good names can improve your design by identifying weaknesses.
Consistency has three requirements: first, always use the common name for the given purpose; second, never use the common name for anything other than the given purpose; third, make sure that the purpose is narrow enough that all variables with the name have the same behavior.
Gerrand makes one comment that I agree with: “The greater the distance between a name’s declaration and its uses, the longer the name should be.” The earlier discussion about using loop variables named i and j is an example of this rule.
if you take a little extra time up front to select good names, it will be easier for you to work on the code in the future.
To write a good comment, you must identify the essence of a variable or piece of code: what are the most important aspects of this thing?
The tactical approach very quickly leads to a messy system design. If you want to have a system that is easy to maintain and enhance, then “working” isn’t a high enough standard; you have to prioritize design and think strategically. This idea also applies when you are modifying existing code.
A typical mindset is “what is the smallest possible change I can make that does what I need?” Sometimes developers justify this because they are not comfortable with the code being modified
If you’re not making the design better, you are probably making it worse.
The best way to ensure that comments get updated is to position them close to the code they describe
The best way to ensure that comments get updated is to position them close to the code they describe, so developers will see them when they change the code.
If you want to include a copy of this information in the commit message as well, that’s fine, but the most important thing is to get it in the code. This illustrates the principle of placing documentation in the place where developers are most likely to see it; the commit log is rarely that place.
Don’t redocument one module’s design decisions in another module.
If information is already documented someplace outside your program, don’t repeat the documentation inside the program; just reference the external documentation.
It’s important that readers can easily find all the documentation needed to understand your code, but that doesn’t mean you have to write all of that documentation.
If a system is consistent, it means that similar things are done in similar ways, and dissimilar things are done in different ways.
Consistency only provides benefits when developers have confidence that “if it looks like an x, it really is an x.”
Consistency is another example of the investment mindset. It will take a bit of extra work to ensure consistency:
software should be designed for ease of reading, not ease of writing.
The first form of inheritance is interface inheritance, in which a parent class defines the signatures for one or more methods, but does not implement the methods.
The second form of inheritance is implementation inheritance.
Class hierarchies that use implementation inheritance extensively tend to have high complexity.
Although the mechanisms provided by object-oriented programming can assist in implementing clean designs, they do not, by themselves, guarantee good design.
Developing incrementally is generally a good idea, but the increments of development should be abstractions, not features.
The problem with test-driven development is that it focuses attention on getting specific features working, rather than finding the best design.
One place where it makes sense to write the tests first is when fixing bugs.
Although it may make sense to use getters and setters if you must expose instance variables, it’s better not to expose instance variables in the first place. Exposed instance variables mean that part of the class’s implementation is visible externally, which violates the idea of information hiding and increases the complexity of the class’s interface.
Whenever you encounter a proposal for a new software development paradigm, challenge it from the standpoint of complexity: does the proposal really help to minimize complexity in large software systems?
The most important idea is still simplicity: not only does simplicity improve a system’s design, but it usually makes systems faster.
In many cases, a more efficient approach will be just as simple as a slower approach
It’s tempting to rush off and start making performance tweaks, based on your intuitions about what is slow. Don’t do this! Programmers’ intuitions about performance are unreliable. This is true even for experienced developers.
Programmers’ intuitions about performance are unreliable.
let’s assume that you have carefully analyzed performance and have identified a piece of code that is slow enough to affect the overall system performance. The best way to improve its performance is with a “fundamental” change, such as introducing a cache,
For each of these critical paths, we tried to identify the smallest amount of code that must be executed in the common case.
For each of these critical paths, we tried to identify the smallest amount of code that must be executed in the common case. Then we designed the rest of the class around these critical paths.
Clean design and high performance are compatible.
if you write clean, simple code, your system will probably be fast enough that you don’t have to worry much about performance in the first place.
In the few cases where you do need to optimize performance, the key is simplicity again: find the critical paths that are most important for performance and make them as simple as possible.
This book is about one thing: complexity. Dealing with complexity is the most important challenge in software design.
If the only thing that matters to you is making your current code work as soon as possible, then thinking about design will seem like drudge work that is getting in the way of your real goal.
A clean, simple, and obvious design is a beautiful thing.
The reward for being a good designer is that you get to spend a larger fraction of your time in the design phase, which is fun. Poor designers spend most of their time chasing bugs in complicated and brittle code.