99 Bottles of OOPOn July 4, 2019 in bookshelf • 7 minutes read
Table of Contents
What It’s About
99 Bottles of OOP is a practical guide to writing cost-effective, maintainable, and pleasing object-oriented code.
- Recognizing when code is “good enough”
- Getting the best value from Test-Driven Development (TDD)
- Doing proper refactoring, not random “rehacktoring”
- Locating concepts buried in code
- Finding names that convey deeper meaning
- Safely altering code by following the “Flocking Rules”
- Simplifying new additions with the Open/Closed Principle
- Avoiding conditionals by obeying the Liskov Substitution Principle
- Making targeted improvements by reducing Code Smells
- Improving changeability with polymorphism
- Manufacturing role-playing objects using Factories
My notes (still a WIP, will need to rephrase a bunch of stuff)
The basic promise of Object-Oriented Design (OOD) is that if you’re willing to accept increases in the complexity of your code along some dimensions, you’ll be rewarded with decreases in complexity along others. OOD doesn’t claim to be free; it merely asserts that its benefits outweigh its costs.
Each of these design choices has costs, and it only makes sense to pay these costs if you also accrue some offsetting benefits. In theory these abstractions make code easier to understand and change, but in practice they often achieve the opposite. One of the biggest challenges of design is knowing when to stop, and deciding well requires making judgments about code.
If you choose well, your code will be expressive, understandable and flexible, and everyone will love both it and you. However, if you get the abstractions wrong, your code will be convoluted, confusing, and costly, and your programming peers will hate you.
This book is about finding the right abstractions.
You’d think that by now, there would exist a universally agreed-upon definition of good code that could unambiguously guide our programming behaviour.
The unfortunate truth is that not only are there a multitude of definitions, but these definitions generally describe how code looks when it’s done without providing any concrete guidance about how to get there.
Any pile of code can be made to work; good code not only works, but is also simple, understandable, expressive and changeable.
The problem with these definitions is that although they accurately describe how good code looks once it’s written, they give no help with achieving this state, and provide little guidance for choosing between competing solutions. The attributes they use to describe good code are qualitative, not quantitative.
If you could identify and measure these qualities, you could seek after them diligently and deliberately. Therefore, although your opinions about code matter, you would be well served by facts.
When you first write a piece of code, you obviously know what it does. Therefore, during initial development, the price you pay for poor design choices is relatively low. However, code is read many more times than it is written, and its ultimate cost is often very high and paid by someone else.
Writing code is like writing a book; your efforts are for other readers. Although the struggle for good names is painful, it is worth the effort if you wish your work to survive to be read. Code clarity is built upon names.
Independent of all judgment about how well a bit of code is arranged, code is also charged with doing what it’s supposed to do now as well as being easy to alter so that it can do more later.
While it’s difficult to get exact figures for value and cost, asking the following questions will give you insight into the potential expense of a bit of code:
- How difficult was it to write?
- How hard is it to understand?
- How expensive will it be to change?
The past (“was it”) is a memory, the future (“will it be”) is imaginary, but the present (“is it”) is true right now. The very act of looking at a piece of code declares that you wish to understand it at this moment. Questions 1 and 3 above may or may not concern you, but question 2 always applies.
Code is easy to understand when it clearly reflects the problem it’s solving, and thus openly exposes that problem’s domain.
Don’t Repeat Yourself
The Don’t Repeat Yourself (DRY) principle promises that if you put a chunk of code into a method and then invoke that method instead of duplicating the code, you will save money later if the behaviour of that chunk changes.
Recognise, though, that DRYing out code is not free. It adds a level of indirection, and layers of indirection make the details of what’s happening harder to understand. DRY makes sense when it reduces the cost of change more than it increases the cost of understanding the code.
DRY also applies to method names; when you name a method after its current implementation, you can never change that internal implementation without ruining the method name. You should name methods not after what they do, but after what they mean, what they represent in the context of your domain.
DRY is important but if applied too early, and with too much vigour, it can do more harm than good. When faced with a situation like this, ask these questions:
Does the change I’m contemplating make the code harder to understand? When abstractions are correct, code is easy to understand. Be suspicious of any change that muddies the waters; this suggests an insufficient understanding of the problem.
What is the future cost of doing nothing now? Some changes cost the same regardless of whether you make them now or delay them until later. If it doesn’t increase your costs, delay making changes. The day may never come when you’re forced to make the change, or time may provide better information about what the change should be. Either way, waiting saves you money.
When will the future arrive, or how soon will I get more information? If you’re in the middle of writing a test suite, better information is as close as the next test. Squeezing all duplication out at the end of every test is not necessary. It’s perfectly reasonable to tolerate a bit of duplication across several tests, hoping that coding up a number of slightly duplicative examples will reveal the correct abstraction. It’s better to tolerate duplication than to anticipate the wrong abstraction.
Test Driven development
A generation ago, a handful of extreme programming (XP) practitioners began writing automated tests using a technique they called “test first development.” Their ideas were so influential that automated tests are now the norm, and these tests are often written first, in prelude to writing code.
The practice of writing tests before writing code become known as test-driven development (TDD). In its simplest form, TDD works like this:
- Write a test: Because the code does not yet exist, this test fails. Test runners usually display failing tests in red.
- Make it run: Write the code to make the test pass. Test runners commonly display passing tests in green.
- Make it right: Each time you return to green, you can refactor any code into a better shape, confident that it remains correct if the tests continue to pass.
TDD promises straightforward, bug-free software that can be confidently and easily changed. TDD does not claim to be free, merely that its benefits outweigh its costs.
The ideas of testing, and of testing first, have won the hearts and minds of programmers. However, a commitment to writing tests doesn’t make this easy. TDD presents a never-ending challenge. You must repeatedly decide which test to write next, how to arrange code so that the test passes, and how much refactoring to do once it does. Each decision requires judgment and has consequences.
If your TDD judgment is not yet fully developed, it’s reasonable to temporarily adopt that of a master. Here’s an excellent guiding principle:
Quick green excuses all sins. — Kent Beck Test-Driven Development by Example
Green means safety. Green indicates that, at least as evidenced by the tests at hand, you understand the problem. Green is the wall at your back that lets you move forward with confidence. Getting to green quickly simplifies all that follows.
Tightly Coupled tests
There’s nothing more frustrating than making a change that preserves the behaviour of an application but breaks apparently unrelated tests. If you change an implementation detail while retaining existing behaviour and are then confronted with a sea of red, you are right to be exasperated. This is completely avoidable, and a sign that tests are too tightly coupled to code. Such tests impede change and increase costs.
Tests are not the place for abstractions—they are the place for concretions. Abstractions belong in code. If you insist on reducing duplication by adding logic to your tests, this logic by necessity must mirror the logic in your code. This binds the tests to implementation details and makes them vulnerable to breaking every time you change the code.
DRY is a very good idea in code, but much less useful in tests. When testing, the best choice is very often just to write it down.
Testing, done well, speeds development and lowers costs. Unfortunately it’s also true that flawed tests slow you down and cost you money.
It is worth the effort, therefore, to get good at testing. TDD can prevent costly guesses, but only if you commit to writing code in small steps. Tests can make it safe and easy to refactor, but only if they are carefully de-coupled from the current code.
Good tests not only tell a story, but they lead, step by step, to a well-organised solution.
Intuition is merely an unconscious prodding to follow an unarticulated rule
Other Bookshelf Pages
- May 14, 2020: Superforecasting
- May 14, 2020: A Calendar of Wisdom
- November 17, 2018: The Art of Worldly Wisdom
- November 11, 2018: Made to Stick