Lessons in OOP derived from a program written to produce the classic song.

Rediscovering Simplicity

Simplifying code

4 examples of different ways to solve a problem

  1. Incomprehensibly concise

    Brevity is prioritized. Difficult to understand, even to original programmer

  2. Speculatively general

    Optimizes too soon. Spends time abstracting it ways that might not be necessary. Wastes time, solves wrong problems.

  3. Concretely abstract

    Prioritizes abstraction without consideration for the problem domain. Example: Creating a beer method that returns the string ‘beer’. Problematic, because the concept that’s relevant isn’t ‘beer’. It’s a beverage.

  4. Shameless green

    Straightforward. Passes tests. Not optimized. Reduendant. If nothing changes, it will be good enough. Prioritizes understandability over changeability.

Judging Code

How do you know which is best? There are many opinions about what good code looks like, but they aren’t very useful. There are metrics that can be used to compare different approaches.

  1. SLOC or LOC (Source lines of code)

    Very generally, less is better. However, it’s such an ambiguous number that it borders on uselessness. A bad programmer will probably solve a problem with more lines than a good one. But overly concise code can be problematic, as well. Still, it’s a point of reference.

  2. Cyclomatic Complexity

    A metric created by Thomas J. McCabe, Sr. to identify code that is difficult to test or maintain. The CC algo counts unique execution paths in a program. Related to number of conditionals. Makes no claims beyond its objective count of execution paths.

  3. Assignments, branches and conditions (ABC)

    “Cognitive size” of code. The more complex code is (in more dimensions than execution paths), the more difficult it is to reason about. Flog is a popular Ruby library to measure ABC.

With these metrics, shameless green emerges as best choice

Summary

  • Use TDD to find shameless green.
  • Don’t waste time solving problems that are not confirmed to be the right problem
  • Start with ‘good enough’ solutions, and they’ll remain so until they need to be changed.

Test Driving Shameless Green

  • Red/green/refactor is the process of writing a failing test, then getting it to pass, then refactoring when necessary
  • Writing first tests are the hardest, and they decrease with difficulty the more (good) tests are written. That’s because you learn something about the problem domain with each test, so every new test means you know more about the problem.
  • Tests should be small
  • Tests should drive out very incremental changes
  • Resist the temptation to code or test ahead. Respect the process.
    • This often means writing “dumb” solutions.
  • Code forward. Make progress on the current problem and don’t get sidetracked by seeming low hanging fruit or good ideas. Respect the process.

Removing duplication

  • Isolate what changes and what doesn’t
  • Dumb solution
def	verse(number)
  if	number	==	99
    "99	bottles	of	beer	on	the	wall,	"	+
    "99	bottles	of	beer.\n"	+
    "Take	one	down	and	pass	it	around,	"	+
    "98	bottles	of	beer	on	the	wall.\n"
  else
    "3	bottles	of	beer	on	the	wall,	"	+
    "3	bottles	of	beer.\n"	+
    "Take	one	down	and	pass	it	around,	"	+
    "2	bottles	of	beer	on	the	wall.\n"
  end
end
  • Separating what changes and what doesn’t
def	verse(number)
  if	number	==	99
    n	=	99
  else
    n	=	3
  end

  "#{n}	bottles	of	beer	on	the	wall,	"	+
  "#{n}	bottles	of	beer.\n"	+
  "Take	one	down	and	pass	it	around,	"	+
  "#{n	-	1}	bottles	of	beer	on	the	wall.\n"
end
  • Using the knowledge from above to create a smart solution
def	verse(number)
  "#{number}	bottles	of	beer	on	the	wall,	"	+
  "#{number}	bottles	of	beer.\n"	+
  "Take	one	down	and	pass	it	around,	"	+
  "#{number-1}	bottles	of	beer	on	the	wall.\n"
end

Transformations

This blog lists different types of transformations and ranks them in order of simplicity. When there are multiple ways to accomplish something, choose the simplest transformation.

Hewing to the Plan

  • Use key words and abstractions to reveal intention.
  • Example: You can use if statements to accomplish what case statements can do, but they tell different stories. if chains compare distinct data. Case will make decisions based on the outcome of a single comparison.

  • Tests can reveal responsibilities in code,
  • Tests should be dumb. Tests are not the place for abstractions or DRY. Example: Hardcode the expected result for all 99 verses for the song methods test.

Unearthing Concepts

Shameless Green prioritizes understandability over changeability. So what happens when you need to change the code?

Imagine a new requirement rolls in to replace every appearance of ‘6 bottles’ with ‘a 6 pack’.

  • Still don’t over optimize. Only solve for the new requirement.
  • Extending the case statement introduces untenable duplication.
  • So how do we know what to change and how to change it?

Open/Closed Principle

  • One of the pillars of SOLID.
  • Says an object should be open for extension, but closed for modification. In other words, keep the processes of refactoring and adding features separate – Don’t refactor and add a feature at the same time.
  • Before adding a feature, the code must be ‘open’ to the change. If it’s not open, make it open. If you don’t know how to make it open, fix the easiest code smell. Repeat until you can open it.

Code smells

  • Anti patterns with prescribable refactors
  • Some are obvious (Duplication)
  • Others less so (Shotgun Surgery)
  • Read Martin Fowler’s refactoring book.
  • If you don’t know code smells, list what you don’t like about the code. It probably corresponds to a smell.
  • You don’t need to fix all of them. Choose the easiest to fix, fix it, then see if you can open the code to the new feature.
  • Current smells in our code are the switch statement and duplication. Duplication is easier, so let’s fix that first by refactoring.

Refactoring Systematically

  • Refactoring improves codes internal structure without changing its behavior.
  • Refactoring is how you open existing code to new features.
  • Tests are the guardrails that allow safe, quick and effective refactoring.
  • If a refactor breaks a test, then either
    • You accidentally changed the code behvior, and you need to try again
    • Or, your tests are too tightly coupled to your code
    • Tests measure code’s output, not its internals. Therefore, ‘Never change tests during refactoring’

Flocking Rules

  • The duplication in the shameless green is a result of an unidentified abstraction.
  • The question is, how are these different verses actually the same?
  • Flocking rules are steps to that help identify unclear abstractions
    • Name comes from behavior of animal flocks. Small, individual decisions reveal larger abstractions (Movement of a flock of birds looks cohesive but is product of repeated application of small decisions from individual members.)
  1. Select the things that are most alike.
  2. Find the smallest difference between them.
  3. Make the simplest change that will remove the difference.
    1. Parse new code
    2. Parse and execute it
    3. Parse, execute and use it
    4. Delete unused code
  • Make small changes so errors are helpful.
  • If you encounter an error you don’t understand, back up and make smaller changes.

Converging on Abstractions

  • DRYing out sameness has some value.
  • DRYing out differences has more value.
  • If two examples of an abstraction have a difference, that difference is a smaller abstraction.
  • Programmers have a bad habit of merely believing they understand the abstraction then inventing a solution.
  • A better habit is to use small, iterative refactors to find a solution. Like the flocking rules.

It is common to find that hard problems are hard only because the easy ones haven’t been solved yet. Therefore, don’t discount the value of solving easy problems.

  • The most alike parts of the code are verse 2 and all the others. The only difference is ‘bottle’ vs ‘bottles’.
  • That difference is the concept that needs an abstraction.
  • General rule: The name of the concept is often one level of abstraction higher than the the thing.
  • Since the diff is bottle/bottles (And we know we have a new requirement to replace them with ‘six-pack’ when there are 6 bottles) ‘container’ is a reasonable abstraction.
  • Tool to find name for an abstraction. Lay out knowns in a table. The name for the unknown column might be a usable abstraction.
Number ???
1 bottle
6 six-pack
n bottles

Making Methodical Transformations

Making a slew of simultaneous changes is not refacotring - it’s rehacktoring.

  • Running with this new found abstraction and writing the method, then implementing it by changing existing code introduces too many changes.
  • Refactor by following the flocking rules and the substeps
  • Generally
    • Change one line at a time
    • Run tests after every change
    • If a test fails, find a better change (Don’t change tests during refactoring)
  • To keep changes small and code deployable, employ ‘Gradual Cutover Refactoring’
    • Allow changes to be gradually adopted. Example: Use default argument values to avoid having to update sender and receiver.

There are plenty of hard problems in programming, but [refactoring] isn’t one of them.

Summary

  • Separate refactoring and adding new features
    • First, open code to extension
    • Then, extend it
  • If it is not obvious how to open code, start identifying and resolving code smells until a path opens.
  • Opening code often requires identifying abstractions. Use flocking rules to do so.

Ch. 4 Practicing Horizontal Refactoring

  • Follow flocking rules!
    1. Find most similar cases
    2. Find the smallest difference between them
    3. Make the smallest change to remove the difference
  • ‘Sameness’ increases with abstraction

Finding good names

  • Good names reflect concepts. Some concepts are simple, like ‘containers’. Some are not.
  • When concept is not easy to name, consider one of these strategies:
    1. Spend 10 minutes with a thesaurus to find a good name. Use the best option found on the grounds that it’s good enough, and you can change it in the future.
    2. Use a meaningless placeholder name now, like ‘foo’, and assume (hope) that a more appropriate name will reveal itself as code evolves.
    3. Ask someone else for help, maybe domain experts or people who are good at naming things.

Code is read many more times than it is written, so anything that increases understandability lowers costs.

Liskov Substitution Principle

Subclasses should be substitutable for their superclasses

(It’s the ‘L’ in SOLID.)

  • Every piece of knowledge is a dependency.
  • Dependencies make code harder to maintain.
  • We should strive to reduce dependencies.
  • We can reduce dependencies by writing code that requires little knowledge.

Don’t refactor under red. Return to green and make incremental changes until you regain clarity.

  • The more confused you are, the slower and more incrementally you should move.
  • Refactoring exposes concepts which can be turned into abstractions
  • Abstractions can be dinged by static analysis tools, but recall “Code is read many more times than it is written, so anything that increases understandability lowers costs.”

Ch. 5 Separating Responsibilities

  • Previously used flocking rules to reduce duplication and aid refactoring.
  • Motivation was incoming 6 pack requirement.
  • Required code to be flexible to expansion, so we refactored to ‘open’ it.
    • Open/Close principle. Code should be open for extension and closed for modification, and vice versa. Do one at a time.

5.1.1

  • Ponder the existing code. What do you like, hate or not understand?
    • There’s no safety net in verses. Starting could be greater than ending.
    • There’s no separation of private methods. Maybe author’s effort to keep focus on refactoring?
    • Love the verse method, especially in contrast to shameless green.
    • Every method we abstracted previously follows the same pattern. It has a single argument (of the same name) and two execution paths.
    • Everything except sucessor returns a string.
    • Class is stateless
  • Sameness and difference should indicate information.
    • Following flocking rules leads to predictably similar/different code.

      Superfluous difference raises the cost of reading code, and increases the difficulty of future refactorings.

    • While many forms of code can accomplish a task, choosing the right expression of code makes it more understandable, expandable and lowers costs.
      • For example, using equality operators for number == 0 instead of number < 1 since number will never be negative.
  • Names should reflect concepts, even if they point to the same value.
    • number sent to verse is a different concept than number sent to container.

Insisting upon messages

  • OO should avoid conditionals that control behavior, as in the flocked five.
  • The flocked five come from shameless green and still favor understandability over changeability.
  • New requirements necessitate a ‘full-blown OO mindset’, including refactoring to reveal all the domain concepts, including classes.

Code is striving for ignorance, and preservin ignorance reqires minimizing dependencies.

Extracting Classes

  • The predominant code smell in the app at this point is ‘Primitive Obsession’, which means we are using a primitive to represent a concept. The cure is to follow the ‘Extract Class’ recipe.
  • The class we need to extract is for ‘bottle number’. It is not a type of bottle. It is a type of number, and that’s a very abstract concept.

    The power of OO is that it lets you model ideas … Model-able ideas often lie dormant in the interactions between other objects.

  • Consider a ticket management app:
    • Buyer and Ticket are two obvious classes that point to concrete things.
    • Discount and Refund are potential classes that model ideas.

Naming classes

  • Whereas methods should be named at one level of abstraction higher than the thing being named, classes should be named after what they are.
  • This can be revisited when new information arrives.
  • So we call the new class BottleNumber.

Steps for refactoring methods or classes

Don’t modify code internals during this process

  1. Parse the new code.
    1. Copy new code into new place.
    2. Don’t invoke it.
    3. Run tests.
    4. Demonstrates the parser can make sense of the new code.
  2. Parse and execute it.
    1. Inject calls to the new code alongside (before) the previous code.
    2. Don’t remove the previous usage. Still using result of original implementation.
    3. This demonstrates the code can execute without blowing up.
  3. Parse, execute and use it.
    1. Use result of new code by moving it after the previous code.
  4. Delete unused code.
    1. If tests pass, delete old code

This is an extremely mechanical, wonderfully boring, and deeply comforting refactoring process.

In real world applications, the same method name is often defined several times, and a message might get sent from many different places. Learning the art of transforming code one line at a time, while keeping the tests passing at every point, lets you undertake enormous refactorings piecemeal.

  • One tactic to safely remove existing code while keeping tests green:
    1. use temporary default values
    2. remove code that provides the value where there is a new default
    3. remove the default

5.3 Immutability

State is the particular condition of something at a specific time.

  • Functional programming paradigms are immutable, meaning that variables are unnecessary.
  • New values are created anew, instead of updating previous values.
  • Benefits:
    • Easier to reason about
    • Easier to test
    • Thread safe
  • Cons:
    • Less performant. Creating new objects is more costly than updating values.
    • Often this is incorrectly assumed to be an intolerable loss. Be sure to check whether it really is an unacceptable performance cost.

Assumptions about performance

The benefits of immutability are so great that, if it were free, yopu’d choose it every time.

  • Costs:
    • Accepting (to many programmers) new idea.
    • Creating many more objects
  • Immutability has parallels to caching.
  • Storing a value that is expensive to retrieve.
  • Assumptions:
    • Will improve performance
    • Will reduce costs
  • Sometimes true, not always.
  • Trying to predict those outcomes before writing code is often a fool’s errand.
  • Metz’ Strategy:
    • Write understandable, maintainble, ‘fast enough’ code.
    • Collect real metrics.
    • When metrics reveal unacceptable performance, improve.
  • The alternative is to waste resources optimizing the wrong thing.

The first solution to any problem should avoid caching, use immutable objects, and treat object creation as free.

Ch. 5 Summary

  • The goal is to open the code to a new feature
  • Identify code smells, fix them.
  • Refactor horizontally before embarking on vertical tangents.
  • Follow recipes, even though the result is not ‘perfect’ code.

Ch. 6 Achieving Openness

  • After a lot of work, code still isn’t open to refactoring. Question becomes, continue on path or retreat?
  • There are signs the code is moving in a good direction. Concepts are isolated, and the things that need to change have mostly been extracted.
    • Some metrics are lower, but outweighed by benefit of changeable, understandable code.

An illustrative code smell

  • Our code sorta demonstrates another code smell, data clumping.
  • Data clumping occurs when data fields routinely occur together.
  • container and quantity emit this smell because they occur side by side in 3 out of 4 lines.
    • Data clumping definition counts 3 data fields appearing repeatedly, but this is a good example, so roll with it =)
  • Evidence that a new concept is waiting to emerge.
  • Metz’ solution is to override the default to_s method of BottleNumber with one that prints container and quantity.

6.2 Making Sense of Conditionals

  • The similarly structured if statements in BottleNumber reek of the Switch Statement code smell.
  • One curative recipe is to replace the conditionals with polymorphism.
  • Adds new objects that intelligently return values previously determined with conditionals.
    • Increases dependencies, so do so carefully
  • Assessing conditionals reveals they only care if bottle number is 0 or 1. Everything else is a default response.
  • Since there are 3 conditions, we can solve this problem with 3 classes: BottleNumber and two subclasses of it: BottleNumber0 and BottleNumber1.
  • Now the questions is, how do we select the correct class to use for each verse?
    • With an object factory
  • Isolate conditional logic to single function that selects appropriate class for the condition.
  • Leans on Liskov substitution principle
  • Recipe for replacing conditionals is
    1. Create a subclass for the value determining the different branches. In our case, BottleNumber1 for all conditials depending on if number == 1.
      1. Copy one method relying on that switch into the new subclass
      2. In the subclass, remove everything but the true branch of the conditional.
        1. Create a factory that will return the appropriate class. So a bottle_number_for method that returns either BottleNumber or BottleNumber1.
        2. If factory already exists, just add the subclass.
      3. In the superclass method, remove everything but the false branch.
      4. Repeat a-c until all methods relying on that conditional are transferred.
    2. Repeat until all methods relying on the switching value (So all methods that are conditionals based on what the number of the bottle is) are transferred into appropriate subclasses.

The Truth of Conditionals

It’s hard to avoid them. But you can restrict them to a single place. The code now, using a factory, no longer determines behavior based on a conditional, it simply routes the input to the appropriate polymorphic handler.

A conditonal-less solution

There are strategies for truly avoiding conditionals. Here’s a simple one:

class BottleNumber
  def self.for(number)
    begin
      const_get("BottleNumber#{number}")
    rescue NameError
      BottleNumber
    end.new(number)
  end
end

Is the increased complexity and unconventional use of error handling as control flow worth avoiding the conditional? Depends on the situation.

Emerging Concepts

When we started with this project, the emphasis was on, “How do the verses differ from each other?” Now, after identifying code smells and following refactoring recipes, the emphasis has shifted to handling differences in bottle numbers. The verses are treated as givens with differences based on bottle numbers injected. This was not an obvious reality of the problem domanin when we started.

Fixing the Liskov violation

  • The current implementation of successor violates the Liskov principle because the successor to a BottleNumber should be a BottleNumber, but instead it is a number.
  • To fix
    • Move the bottle_number_for factor to BottleNumber and turn it into a class method named for.
    • To prevent the factory from failing during the refactor, add a temporary guard that simply returns the argument if it is already a BottleNumber.
    • Modify the successor implementations to return a BottleNumber
    • Modify the successor senders to expect a BottleNumber
    • Delete the guard

Adding the feature

  • At this point the refactorings are complete.
  • The code is closed to refactoring, which opens it to expansion.
    • In other words, we will write our first failing test in awhile which needs to be passed with expansion.
  • Satisfying the six pack requirement requires adding a BottleNumber6 class and choosing it appropriately with the factory.
  • Done!

The Easy Change

This modification was extremely easy, especially in comparison to the amount of work we put into refactoring the code in preparation for the change. This is how things should be. Work hard to make code expandable, so the expanding is easy. She supplies this apt quote from Kent Beck:

make the change easy (warning: this may be hard), then make the easy change.

Further Reading

  • https://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612
  • http://blog.8thlight.com/uncle-bob/2013/05/27/TheTransformationPriorityPremise.html
  • http://martinfowler.com/books/refactoring.html