EXAMPLES - Understanding the Four Rules of Simple Design and other lessons from watching thousands of pairs work on Conway's Game of Life (2014)

Understanding the Four Rules of Simple Design and other lessons from watching thousands of pairs work on Conway's Game of Life (2014)

EXAMPLES

While running coderetreats, I have the opportunity to see a lot of people working on Conway’s Game of Life. As we go through the day, I make comments about design, both in the large and in the small. Over the years, I’ve seen similar patterns pop up across many different developers.

This section contains some of these concrete examples for the 4 rules of simple design and other lessons from coding Conway’s Game of Life.

Test Names Should Influence Object’s API

The idea of naming, and how it relates to the intent of your code, can be seen when looking at the symmetry between test names and the test code. When talking about test descriptions, we often say that they can stand in for documentation. Unfortunately, it is easy to lose sight of this when writing the code inside the test.

In Conway’s Game of Life, a common approach is to start with a World class. Since one of the techniques we practice at coderetreat is test-driven development, we start with a test. A common starting point is that a living cell can be added. I see the following two tests quite often.

def test_a_new_world_is_empty

world = World.new

assert_equal 0, world.living_cells.count

end

def test_a_cell_can_be_added_to_the_world

world = World.new

world.set_living_at(1, 1)

assert_equal 1, world.living_cells.count

end

On the surface, these seem like reasonably well-written tests. However, if we look at it from the idea that the tests should express intent, then there is an obvious mismatch between the test names and the code in the test.

Let’s look at the first one, since this is the simple one that we might write first.

def test_a_new_world_is_empty

world = World.new

assert_equal 0, world.living_cells.count

end

The test name talks about an empty world. The test code, though, has no concept of an empty world, no mention of an empty world. Instead, it is brutally reaching into the object, yanking out some sort of collection (only a lack of living cells represents that the world is empty?) and counting it.

When we write our tests, we should be spending time on our test names. We want them to describe both the behavior of the system and the way we expect to use the component under test. When starting a new component, we can use our test names to influence and mold our API. Think of the test as the first consumer of the component, interacting with the object the same way as the rest of the system. Do we want the rest of the system to be reaching in and grabbing the internal collection? No, of course we don’t. Instead, think about letting the code in the test be a mirror of the test description. How about something like this.

def test_a_new_world_is_empty

world = World.new

assert_true world.empty?

end

This hides the internals of the object, while building up a usable API for the rest of the system to consume.

Now, let’s look at the second test.

def test_a_cell_can_be_added_to_the_world

world = World.new

world.set_living_at(1, 1)

assert_equal 1, world.living_cells.count

end

After the discussion around the first test, we can see the lack of symmetry here. The test name talks about adding to the world, but the verification step isn’t looking for the cell that was added. It is simply looking to see if a counter was incremented on some internal collection. Let’s apply the symmetry again and have the test code actually reflect what we say is being tested.

def test_a_cell_can_be_added_to_the_world

world = World.new

world.set_living_at(1, 1)

assert_true world.alive_at?(1, 1)

end

This now adds to our API. Additional tests, of course, will flesh out the behavior of these methods, but we now have begun to build up the usage pattern for this object.

We also could add a test around the empty? method using set_living_at.

def test_after_adding_a_cell_the_world_is_not_empty

world = World.new

world.set_living_at(1, 1)

assert_false world.empty?

end

This is another way of slowly building up the API, especially the beginnings of the set_living_at behavior.

Focusing on the symmetry between a good test name and the code under tests is a subtle design technique. It is definitely not the only design influence that our tests can have on our code, but it can be an important one. So, next time you are flying through your TDD cycle, take a moment to make sure that you are actually testing what you say you are testing.

Duplication of Knowledge about Topology

Given that we are building with a cell abstraction, we can start to think about their locations. A common next step is to set certain cells to be alive at a given location, check for living cells at a location, etc.

A common, and pretty reasonable, implementation is to have something like a World class that contains these behaviors. A naive implementation might look at our 2-d grid and build the methods directly.

classWorld

def set_living_at(x, y)

#...

end

def alive_at?(x, y)

#...

end

end

And, of course, we might decide to add the coordinates to our Cell classes. After all, the cells are placed at a certain location.

classLivingCell

attr_reader :x, :y

end

classDeadCell

attr_reader :x, :y

end

On the surface, this seems okay. But, there is a subtle, not always obvious duplication of knowledge here: knowledge of our topology.

A good way to detect knowledge duplication is to ask what happens if we want to change something. What effort is required? How many places will we need to look at and change? For example, what if we want to change our topology to 3 dimensions? In our design, we would have quite a few places to change. This is duplication of knowledge; we have spread the knowledge of our topology — the fact that we are working on a 2-dimensional grid — all over the codebase. Eliminating this duplication relies on a strategy of reification. This is the act of taking a concept and making it real by extraction. So, let’s extract the x,y to create a Location abstraction.

classLocation

attr_reader :x, :y

end

Now, doing this gives us a way to eliminate our duplication.

classWorld

def set_living_at(location)

#...

end

def alive_at?(location)

#...

end

end

classLivingCell

attr_reader :location

end

classDeadCell

attr_reader :location

end

By isolating this knowledge, we have made it easier to handle any change in our topology. Our code becomes more adaptable, along with making it much more clear.

While we looked at this refactoring from the perspective of duplication, we can also approach this as a naming problem: a lack of effectively expressing our intent.

To start with, the parameters x and y have horrible names. The fact that we “know” what they mean is a convention, rather than a result of being explicit. When encountering poor names, we often can find a missing abstraction by thinking about what the poorly-named variables represent.

classWorld

def set_living_at(x, y)

#...

end

end

In our case, as a pair, the x and y represent a location in our system. A better way to represent it, to be explicit, is to name the pair.

classWorld

def set_living_at(location)

#...

end

end

This then tells us that this parameter represents a single object, an instance of something.

Of course, we could take a small, interim step by making this a tuple on the caller side.

world.set_living_at([x, y])

A good step, but this does not solve the naming issue, it really just pushes it elsewhere in the code. That can be good as a small step solution, but we’ll want to clean it up there, too. Applying some judicious name fixing at that level could push us closer to a Location object.

Behavior Attractors

A common and reasonable starting point for building Game of Life is to set living cells at locations. As per the rules, cells can be either living or dead. Here’s an example of how this could be implemented.

classWorld

def set_living_at(x, y)

#...

end

def alive_at?(x, y)

#...

end

end

classCell

attr_reader :x, :y

def alive_in_next_generation?

# run rules

end

end

Imagine that we are happily moving along with this design when we find ourselves in need of asking for the neighbor locations for a given x, y. Perhaps we want something like the following method.

def neighbors_of(x, y)

# calculate the coordinates of neighbors

end

Whenever we have a new method — a new behavior — an important question is “where do we put it?” What type does this belong to? Unfortunately it isn’t always immediately clear where to put it. Determining the right place for a new behavior can often be very challenging.

Should this go on the Cell class? It could make sense to ask a cell for its neighbors’ coordinates. After all, the cell does have knowledge about where it is positioned on the grid. Of course, it is also focused on implementing the rules for evolving to the next generation. That feels like we are starting to put unrelated responsibilities onto the Cell class.

Perhaps a better place would be the World class? After all, this represents the larger world, where we can set living cells and query whether certain locations are alive. It clearly has knowledge of the grid. But, just thinking about the name “World,” we can kind of get a sense that it has the makings of a God Class. Perhaps we should stay away from putting more things there.

How many times have you run into this problem? You know you need a behavior, but there is a bit of confusion around its proper place. Too often, our solution is to punt on really analyzing the problem. Instead, we just put it in whatever file is open at the time. After all, we can always justify it later. Or, we tell ourselves we’ll move it later, once we have more information. Once it starts getting used, though, moving it becomes less and less likely.

However, there could be a much more natural place. In an earlier example, we eliminated the knowledge duplication around the location, reifying a Location concept. Had we done this here, we might find that we already have a place that is just right.

classLocation

attr_reader :x, :y

end

Our other classes reference this, the Location, and rely on it to be entirely focused on the topology. What better place to put a behavior than the type that is concerned about the topology? Our behavior is really about asking for what locations constitute the neighborhood around a given location. Sounds like a natural behavior for the Location class.

classLocation

attr_reader :x, :y

def neighbors

# calculate a list of locations

# that are considered neighbors

end

end

This is an example of what I call a Behavior Attractor.

By aggressively eliminating knowledge duplication through reification, we often find that we have built classes that naturally accept new behaviors that arise. They not only accept, but attract them; by the time we are looking to implement a new behavior, there is already a type that is an obvious place to put it.

As a corollary to this, we can use this idea to notice potentially missing abstractions. If we are working on a new behavior, but are not sure where to place it — what object it belongs to — this might be an indication that we have a concept that isn’t expressed well in our system.

Testing State vs Testing Behavior

When starting a problem, especially when we are taking an outside-in approach, it is common to begin with the outermost object. We might start with some form of coordinator object for the use case we are writing. In Conway’s Game of Life, we might think of a World class; an object that might coordinate the grid, the changing state of the cells, etc.

In our case, a great first test might be to see whether a World starts empty.

def test_a_world_starts_out_empty

world = World.new

assert_true world.empty?

end

This is reasonable. It is simple. It establishes that we have a world. And, it establishes that there are no living cells in a new one.

The next test, of course, continues down this path. The previous test was looking at the idea of emptiness, so it seems natural to continue in that vein. How about placing a living cell? After all, that is how we start the game: placing cells. Since we’ve established an empty? method, we might make a simple test that the world isn’t empty after placing a living cell.

def test_world_is_not_empty_after_setting_a_living_cell

world = World.new

location = Location.random

world.set_living_at(location)

assert_false world.empty?

end

Nice and simple tests, for sure. They seem to follow naturally. You probably can see the path forward now. “Nice and simple” is true. It is worth noting, though, that they are leading to a very state-focused test suite. We are doing something, then checking what, if any, state change occurred.

An alternate way to develop a system is to focus on the behavior rather than the state of the objects. Think about what behaviors you expect and have our tests center around those. The idea of “focusing on behavior” is a common topic in software development conversations, but it isn’t always that clear how to do it.

Building our system in a behavior-focused way is about only building the things that are absolutely needed and only at the time they are needed. This way, we end up with a system that has just enough code to support our use cases. When doing this, there is a handy tool I use to keep myself building only what is needed.

When I think there is something I want to build, I ask myself a simple question: “What behaviour of my system requires this?” Once I answer that question, I move to building that behavior.

In our case above, this formula generates two questions:

· How do we know that we want to set an individual cell?

· How do we know that we want to check that the world is empty?

Once we answer these questions — usually with a statement that “this behaviour will need it” — we can take a step back and build our tests around that behaviour.

Why do we need to set an individual cell? Above, we said that this might be how we set up the initial pattern. This leads to another question.

Why do we need the initial pattern? The point of the game is to calculate the next generation.

And there is where we have identified a fundamental 'margin-top:6.0pt;margin-right:0cm;margin-bottom:12.0pt; margin-left:0cm;line-height:normal'>In our system, this fundamental behaviour happens with the tick, moving to the next generation. This is what triggers everything. So, let’s start testing that. Then, as it needs behaviors, we can build those.

So, what is a very simple thing we say about a tick? The empty world should tick into another empty world.

def test_an_empty_world_stays_empty_after_a_tick

end

Now a question has come up. We just had a question about having our first test be about checking that a new world was empty. And, it seems like we’ve moved ourselves into a position where we need to do this. Since the test dictates that we start with an empty world, we probably should postpone this test and make sure that a new world is empty, so we can write the original test.

def test_a_new_world_is_empty

assert_true World.new.empty?

end

After this, we can move to our original test. We know that a new world is empty. So, we can fill our behavior-focused test with that knowledge.

def test_an_empty_world_stays_empty_after_a_tick

world = World.new

next_world = world.tick

assert_true next_world.empty?

end

Huzzah! We’re now a little more behavior-focused!

Don’t Have Tests Depend on Previous Tests

Let’s look at our previous example’s “behavior-based” test.

def test_an_empty_world_stays_empty_after_a_tick

world = World.new

next_world = world.tick

assert_true next_world.empty?

end

Unfortunately, there is a subtle problem here.

How do we know that a newly-initialized World is empty? The test name indicates we are starting with an empty world, but the test code does not specify this explicitly. We talked about having our test names correspond to the test code in a previous example. Is this a problem here, though? We do have another test verifying this.

And there is our problem.

This test implicitly depends on the validity of a different, previous test: there is an assumption here that new worlds are empty. This causes a subtle, but important, problem; that lack of explicitness, combined with the coupling to the previous test, makes this test contribute to a fragile test suite. What happens if we change the parameters around a new world? What if we decide to make it not empty, but rather start with a stable structure, such as the block? In that case, our original “new world is empty” test fails, as it should. However, we’ll get another failure “an empty world stays empty after a tick”. We’ll look at that test and wonder why it is failing. That’s not good. We want test failures to be explicit, quickly and effectively pointing us to the problem. How should we resolve this?

Let’s look back at the idea of letting the test name influence the test code and use that to make the test code a bit more explicit. Rather than riding with the assumption that a new world is empty, let’s explicitly ask for an empty world.

def test_an_empty_world_stays_empty_after_a_tick

world = World.empty

next_world = world.tick

assert_true next_world.empty?

end

Now, if we change the default constructor to return something other than an empty world, this test will continue to pass. Only if we change what we mean by an empty world, created by World.empty, will this test fail. And, if we do that in such a way that the next world isn’t empty, then this test will fail. And it should, because the statement we are verifying will no longer be true.

In fact, over time I’ve developed a guideline for myself that external callers can’t actually use the base constructor for an object. Put another way: the outside world can’t use new to instantiate an object with an expectation of a specific state. Instead, there must be an explicitly named builder method2 on the class to create an object in a specific, valid state.

Breaking Abstraction Level

Automated unit test suites can have a tendency towards fragility, breaking for reasons not related to what the test is testing. This can be a source of pain when maintaining or making changes to a system. Some people have even gone to the extreme of moving away from unit- or micro-tests and only writing full-stack integration tests. Of course, this is the wrong reaction. Instead, we should investigate the source of the fragility and react with changes to our design.

It isn’t always a problem with our system design, though. Sometimes fragility can come about because of problems in our tests. Let’s take a look at a test we had in an earlier example. It is fairly small, and the assertion matches the test description.

def test_world_is_not_empty_after_adding_a_cell

world = World.empty

world.set_living_at(Location.new(1,1))

assert_false world.empty?

end

But, there is a problem. Can you see it?

Our test talks about the world being empty and adding cells. However, looking at the test code, we can see details about the topology: the (1,1) tuple. We want to strive to have our tests be concise and clear about the behavior we are describing. However, in this case, our test code is implying that the empty? method is somehow dependent on the coordinates, themselves.

This is an example of breaking the level of abstraction. We are testing the behavior of the world, but we are including details that it isn’t concerned with. If the actual topology knowledge is encapsulated in the location object, then the world should be relying on that object to manage those particulars. By tying this test to concrete implementation of 2 dimensions, via the (1,1) tuple, rather than the Location abstraction, we are laying the groundwork for fragile tests: change the topology and tons of tests fail that are not related to the coordinate system. This coupling can be seen as another example of duplication: spreading the knowledge of the topology not just throughout the code, but also throughout the test suite.

To improve this, we work to hide the details of the topology from the world object. One way to do this is to use a stand-in, a test double for the location object. This can be as simple as creating a new, plain object.

def test_world_is_not_empty_after_adding_a_cell

world = World.empty

world.set_living_at(Object.new)

assert_false world.empty?

end

Or, if you don’t like the use of test doubles, you can use a builder method that provides a location without exposing implementation details.

def test_world_is_not_empty_after_adding_a_cell

world = World.empty

world.set_living_at(Location.random)

assert_false world.empty?

end

Note: We could have used a more concrete location, like Location.center, but we aren’t guaranteed that our grid has a center, especially if it is infinite.

By isolating ourselves from changes to the topology, the internals of the Location, we help ensure that this test won’t break if we change something about the underlying coordinate system. We also emphasize that the actual coordinates of the location are irrelevant in this test.

Personally, I like to use a test double in this case, as it highlights that we aren’t using any specific attributes of the location object. And, if we find that we need some interaction with the location, we can specify it as constraints on the double. The result is that our test clearly expresses what behaviors of the location object we depend on. If we want to be even more explicit, we can give the test double a name. This can increase the readability of the test.

def test_world_is_not_empty_after_adding_a_cell

world = World.empty

world.set_living_at(double(:location_of_cell))

assert_false world.empty?

end

By using a test double, we gain feedback that can help minimize the coupling of the behavior under test: we must be explicit about every interation. Because we have to specify the coupling points, we can be clear and confident about how many touch points our objects have with each other. This helps identify any abstraction problems; for example, if this test needs 3 methods stubbed on the location double, then that is a potential indication that we are missing an abstraction, or perhaps set_living_at is doing too much.

Naive Duplication

Let’s look at encoding the actual rules of a cell’s evolution, how it transforms from generation to generation. A common design is to have a cell class with some sort of state to specify being alive or dead. We give it some sort of method to calculate its next state according to the evolution rules.

classCell

attr_reader :alive # true | false

def alive_in_next_generation?

if alive

number_of_neighbors == 2 ||

number_of_neighbors == 3

else

number_of_neighbors == 3

end

end

end

Let’s start refactoring this. Any noticeable duplication?

Aha! That check around whether number of neighbors is 3 looks suspicious. Let’s get rid of the duplication.

classCell

# ...

def alive_in_next_generation?

(alive && number_of_neighbors == 2) ||

number_of_neighbors == 3

end

end

We definitely got rid of the two instances of the number 3, but we have introduced new issues. This is due to what I consider naive, mechanical elimination of duplication: a refactoring that stems from a fundamental misunderstanding of the idea of DRY.

Let’s review the idea of duplication. The Don’t Repeat Yourself, or DRY, principle states:

Every piece of knowledge has one and only one representation

Notice that it doesn’t say anything about code. In fact, it has very little to do with code. Just looking at code that appears similar and combining them misses the point of the DRY principle.

With this clarity in hand, let’s analyze our example a bit more closely.

These 3s are not the same. Thinking they are is a result of seeing a magic number without some sense of what it represents in terms of our domain. When thinking about duplication, it can help to expand the scope of our view, in this case to include the equality check, and to think about what it represents. In our alive case, the 3 is more closely linked to the 2 in the concept of a “stable neighborhood,” while in the dead case, it is linked to something like a “genetically fertile neighborhood.”

One good technique to keep from mistaking similar-looking code as actual knowledge duplication is to explicitly name the concepts before you try to eliminate the duplication. In our case, we would end up with something like this.

classCell

# ...

def alive_in_next_generation?

if alive

stable_neighborhood?

else

genetically_fertile_neighborhood?

end

end

end

After this small refactoring, we can see clearly that the 3s represent different things. This is the power of paying close attention to the expressiveness of our code before blindly trying to eliminate duplication.

Procedural Polymorphism

Take a look at the code that we have around the cell’s evolution.

classCell

# ...

def alive_in_next_generation?

if alive

stable_neighborhood?

else

genetically_fertile_neighborhood?

end

end

end

Notice that it contains a bit too much implementation detail. The method name alive_in_next_generation? is more about implementation, the move from generation to generation, rather than a description of the behavior we want. It is more of a state-oriented statement “alive in next generation?” rather than a question about behavior.

When we find these very generic names, we are looking at an expressiveness problem. Why is “alive” the state we are interested in? What if we add another state?

However, if we think about a better name, we have a hard time. In the case of a living cell, this is really whether it stays alive. In the case of a dead cell, though, it is about the cell coming to life. How can we reconcile this inconsistency?

Before diving straight into tackling the reconciliation, let’s start at a lower level, inside the method, and see if we can gain any insight.

Starting at the top, let’s look at the branching variable, alive; there are a few different questions we could ask ourselves about it.

The name of this variable captures a default, or preferred, state: alive. Why is this the thing we highlight? Each cell is really in one of two states; why not highlight dead? What if we change the concept of living? What if it isn’t binary? Changing this means we have to change code also related to the other two states. We also are spreading the concept in several places: alive has to do with both the variable and the method that uses it.

A seemingly quick solution would be to make it something like state, but that masks our intention a bit. What are the possible states?

classCell

# ...

def alive_in_next_generation?

if state == ALIVE

stable_neighborhood?

elsif state == DEAD

genetically_fertile_neighborhood?

end

end

end

This isn’t much better; We now have even more of an expressiveness problem with this branching: do we really know these are the only ones? I also feel a bit uncomfortable when I see an if-elsif sequence without a raw else.

Variables named state are also a huge red flag for expressiveness. Does a cell really change state? Do dead cells change state into living cells? Or are living cells created? To be honest, too often state variables are usually just an indiciation that we’ve given up on really understanding and encoding our intention.

Resolving this requires us to talk a bit about polymorphism in general. Polymorphism is about being able to call a method/send a message to an object and have more than one possible behavior. This can be one of the most powerful techniques in programming.

In our case, we are providing a form of polymorphism with this method. When this method is called, the caller can expect one of two different behaviors: either the ruleset for living cells or the ruleset for dead cells. Which ruleset gets run is based on an internal state, hidden from the outside world. In a way, this is good; the caller shouldn’t have to care. But it is worth looking at the method we use to achieve the goal.

When we use a branching construct inside a method like this, we run into several problems. We’ve talked about the expressiveness problem, but we also have issues with changing this code. If we are going to add a state, or change rules around the states, we will find ourselves modifying existing code. Not just existing code, but code that is unrelated to the change we are making. If we add a state, why would we force ourselves to modify the code related to the other states? When we begin to overload concepts in our system, especially method names, we run into this “everything goes here” situation.

In general, if statements (or other branching constructs) are imperative, procedural mechanisms. While they do provide a form of polymorphism, they provide a form that I call Procedural Polymorphism. It satisfies our needs for selecting a behavior, but their procedural background leads to tightly-coupled code, joining these often unrelated behaviors together.

Luckily, object-based and object-oriented languages provide a preferred method for polymorphism, what I call Type-Based Polymorphism. The idea is one central to object-oriented design: use different types for the different branches. The general approach is to analyze what the branching condition is, identify the concepts, and reify them into first-class concepts in our system.

In our example, we can take our state and raise it to types: LivingCell and DeadCell.

classLivingCell

def alive_in_next_generation?

# neighbor_count == 2 || neighbor_count == 3

stable_neighborhood?

end

end

classDeadCell

def alive_in_next_generation?

# neighbor_count == 3

genetically_fertile_neighborhood?

end

end

At this point we have separated out the concepts. And, if we choose to, we can also inline the business rule methods without sacrificing too much.

classLivingCell

def alive_in_next_generation?

neighbor_count == 2 || neighbor_count == 3

end

end

classDeadCell

def alive_in_next_generation?

neighbor_count == 3

end

end

We also have higher-level names for our concepts, which makes it easier to find where changes need to occur.

A huge benefit of this is that we also have provided ourself a safer method for adjusting the different states a cell can be in. If we need to add a new one, we add a new class. We extend our system, rather than modify it. This is an example of the open-closed principle3.

classZombieCell

def alive_in_next_generation?

# new, possibly more complex rules

end

end

It also provides a clear method for fixing the names of our methods to match the actual concepts in our system, focusing on specific behaviors, rather than a generic idea of alive_in_next_generation.

classLivingCell

def stays_alive?

neighbor_count == 2 || neighbor_count == 3

end

end

classDeadCell

def comes_to_life?

neighbor_count == 3

end

end

At this point, we now have very explicit statements of the intent of the types and their behaviors. But, changing these names takes away the polymorphism! We no longer can call a single method and have the appropriate rules applied. This is true. This could be an indication that the idea of having the initially-desired polymorphism isn’t a good design. Naturally it depends on how we end up using the cells, but focusing heavily on explicitness in this fashion can raise flags about desired or “planned” designs.

Making Assumptions About Usage

Let’s look at the Cell classes.

classLivingCell

def stays_alive?(number_of_neighbors)

number_of_neighbors == 2 ||

number_of_neighbors == 3

end

end

classDeadCell

def comes_to_life?(number_of_neighbors)

number_of_neighbors == 3

end

end

It seems reasonable that those methods would be there. After all, the following reads okay. Or, at least, it feels familiar.

cell.stays_alive?(number_of_neighbors)

But, there are a couple possible flags here.

First, notice that we are talking about entity classes here. That is, we have objects representing concrete abstractions: Cells. Classes of this nature tend to encapsulate and provide behavior around state. Methods on them are generally involved in working with that state. For example, query methods provide a way to access the state. In this case, though, the methods are not accessing internal state, at all. In fact, they are primarily using the passed-in value, number_of_neighbors.

It is true that we could say that the rules, themselves, the comparisons are related to the cell and constitute cell-focused knowledge. While cell-focused, they really represent the rules. But why is Cell our abstraction around executing the rules? Why don’t we reify the idea of a rule? One of the key parts of being easier to change (i.e. a better design) is being able to more easily find where the changes need to occur; this is what good naming contributes to. So, if we were to come to a large system, and we wanted to change the rules for evolution, you might look at a Cell class. But imagine if there was a Rule class. That could probably be an even larger signpost. Let’s play with this a bit by just adding Rules to the class names.

classLivingCellRules

def stays_alive?(number_of_neighbors)

number_of_neighbors == 2 ||

number_of_neighbors == 3

end

end

classDeadCellRules

def comes_to_life?(number_of_neighbors)

number_of_neighbors == 3

end

end

This looks interesting. Of course, we’ve now lost an abstraction, the Cell. This will influence our location objects. Are the locations linked to the current rules depending on the state, or is there still some placeholder idea of a cell? Do we even need a reified cell abstraction? What is causing us to have it? In fact, if we think about it, the concept of a DeadCell has a potential trap in it. We are working with an infinite grid. So, which dead cells are we keeping track of? Which locations are we tracking? How do we know that we should instantiate a location object for a given (x,y) pair? We can’t keep track of all of them. Perhaps it does make sense to question the concept of a concrete cell class.

A lot of questions that arise have a “do we need this abstraction” flavor. This happens quite frequently when following an inside-out development style. We start somewhere in our domain, making a very large assumption that the abstractions we are building will be needed sometime. As we’ve seen, new abstractions can be developed and investigated through refactorings, but it can be easy to work yourself into a corner. The fundamental thought that is hidden in “do we need this abstraction” is “use influences structure.” So, should we have LivingCellRules and get rid ofLivingCell? Should location objects keep a link to the rule, rather than the cell? Perhaps the location object doesn’t actually contain this link at all. Perhaps the existence of an instantiated location object implies LivingCellRules. So many answers not just disappear but never come up when building abstractions and behaviors through actual usage. This is often what happens when using an outside-in development method.

Unwrapping an Object

A common (and perhaps one of my favorite) constraints in coderetreat is writing your code with “no return values.” Most of the time, in our daily lives, we build in a very imperative style, asking several objects for their data, perhaps some calculations, then we enact an algorithm and stuff the results back into other objects. By eliminating the ability to return values from our functions, we force ourselves to rely instead on telling objects to enact behaviors.

Another side effect of this constraint is that you no longer can have properties on your objects — no methods for querying the internal state. By eliminating the ability to query for data, we begin to build objects that are very tightly encapsulated. We can rely on the objects alone to manage their internal state.

Whenever this constraint comes up, there is an inevitable question: “How do you test for equality?” As people work on the problem, they notice that they need a way to compare whether two location objects represent the same place on the grid.

As an example, imagine we have the following two location objects.

classLocation

attr_reader :x, :y

end

location1 = Location.new(1, 1)

location2 = Location.new(1, 2)

if location1.equals?(location2)

# Do something interesting

end

Ordinarily, you’d write something like this.

classLocation

attr_reader :x, :y

def equals?(other_location)

self.x == other_location.x &&

self.y == other_location.y

end

end

location1.equals?(location2)

Of course, equals? as a method name here is a bit poor. This isn’t really looking to see if the locations are equal, as much as representing the same place in space. But, for our purposes here, this is good enough.

But, this equals? method doesn’t conform to our constraint: it is asking other_location to return its x and y. This isn’t allowed.

This can be a very common sticking point. Most of us have been trained to use properties to access internal state of an object. We pretend, of course, that we are using properties to encapsulate state, but really it is just a way to allow the outside world to reach inside us and do what they want. In a world where you can’t return anything, though, how do you get around this?

The key idea is in a technique that I call unwrapping. Take a look at the following alternate form of equals.

classLocation

attr_reader :x, :y

def equals?(other_location)

other_location.equals_coordinate?(self.x,

self.y)

end

def equals_coordinate?(other_x, other_y)

self.x == other_x && self.y == other_y

end

end

Look what this is doing. Inside the first object (location1), we have access to our own internals. Rather than taking the approach of asking the other object (location2) for its internals, let’s just pass our own to it. So, we are comparing internals without having to reach inside the other object.

Of course, in a language with signature-based overloading, you wouldn’t have to have two methods.

publicclassLocation

{

private int x;

private int y;

public boolean Equals(Location otherLocation) {

return otherLocation.Equals(this.x, this.y);

}

public boolean Equals(int otherX, int otherY) {

returnthis.x == otherX && this.y == otherY

}

}

But, wait, you say. Doesn’t equals? return a boolean? The constraint is that we can’t return anything. So, we are violating that. This is true. Now that we have a way to do the comparison without querying for an object’s state, we can tackle this aspect.

Let’s take a step back and look at this from a behavioral point of view, returning to the fundamental question “why do we need this behavior?” Or, “why do we care if they are equal?” In general, we look for equality in order to react in a certain way. So, if they are equal, we’ll do something. As a simple example, let’s increment a counter.

Since we can’t return the boolean, let’s rewrite our code to remove that. In Ruby, every method returns something, so we have to be explicit to get rid of the boolean return.

classLocation

attr_reader :x, :y

def equals?(other_location)

other_location.equals_coordinate?(self.x, self.y)

nil

end

def equals_coordinate?(other_x, other_y)

self.x == other_x && self.y == other_y

nil

end

end

So, now we can’t get access to it. That satisfies the constraint, but it doesn’t do us much good. We want to do something if they are equal. Since we are can’t react to the comparison outside the objects, we need to move the behavior inward closer to where the action is happening. Notice thatequals_coordinate? does the comparison. So, this is where we need to do the behavior.

Ordinarily, we would write something with a simple if statement.

count_of_locations = 0

if location1.equals?(location2)

count_of_locations++

end

Instead, let’s take the behavior, wrap it in a lambda, and move it to where the comparison is happening.

count_of_locations = 0

location1.equals?(location2, -> { count_of_locations++ \

})

In this code, we expect the lambda to be called if the locations turn out to be equal. Let’s fix our code to support this.

classLocation

attr_reader :x, :y

def equals?(other_location, if_equal)

other_location.equals_coordinate?(self.x, self.y, i\

f_equal)

nil

end

def equals_coordinate?(other_x, other_y, if_equal)

if self.x == other_x && self.y == other_y

if_equal.()

end

nil

end

end

Now, we have a situation where we are telling a location object (location1) “Here is another location object (location2). If you are equal to it, do this (if_equal).”

Note: In most languages, there is some form of first-class function which makes this technique fairly straight-forward. Sadly, Java only recently got these. So, you have to solve this using some form of a command object. Is this bad? Not necessarily, although it can be a bit cumbersome.

Inverted Composition as a Replacement for Inheritance

Take a look at these cell classes.

classLivingCell

attr_reader :location

end

classDeadCell

attr_reader :location

end

We’ve extracted the location object. One benefit of this, of course, is that gives us a centralized place for our topology knowledge. This is very nice, but we can see another duplication here, as well. Both the living cell and the dead cell have a location attribute.

Is this knowledge duplication? What is the knowledge we are duplicating? Since these are two different objects, and this is “just” an attribute, we can be tempted to say it isn’t. As we look at this code in light of the 4 rules, we want to make sure that what we have is actual knowledge duplication, rather than just incidental, implementation similarity. After all, extracting the location object was about taking the “actual” knowledge and representing it in one place.

We can look at this as knowledge duplication, since this location attribute represents the fact that our cells are linked to a specific position on the grid. It is an interesting case here, where eliminating a specific duplication didn’t eliminate all the duplication, just part of it.

So, let’s look at ways to eliminate this duplication.

A common attempt at a solution to this is to jump to inheritance. We could do something like the following.

classCell

attr_reader :location

end

classLivingCell < Cell

end

classDeadCell < Cell

end

Wait, though, let’s look at this code a minute.

Now, it does seem to simplify our code a bit if we think in terms of lines of code. But, is it really simpler? It does add another type after all. I often say having more classes isn’t bad, as long as they are the correct abstractions. But, unlike the extraction of the Location class, this extraction doesn’t introduce a new domain concept; this abstraction increases the complexity without adding additional information about our domain. This feels like a violation of the fourth rule, “small.”

Inheritance is often used as way of creating “reuse” rather than eliminating duplication. We are assuming that both the LivingCell and DeadCell need to have access to their location (do they?), so we provide access through the base class. Even if we support our assumption, however, the objects don’t need access to their location necessarily, they really would need access to the behaviors that the location object exposes. And, of course, at this point, we haven’t even talked about whether they truly do.

So, let’s ask again: is it really eliminating the duplication? The location attribute is still there on the objects. Our two different types still contain the same knowledge.

Base classes of this nature, extracted entirely to eliminate apparent duplication can have a tendency to hide actual duplication. Also, it is very common for these base classes to become buckets of unrelated behavior.

So, if inheritance isn’t really eliminating the knowledge, what other options do we have?

In Ruby, we do have modules. This might be a good use for them.

classLivingCell

include HasLocation

end

classDeadCell

include HasLocation

end

And the HasLocation module adds attr_reader :location to the including class. Modules, when used this way, though, are just a way to implement multiple inheritance. The same arguments arise as in the above discussion of straight subclassing.

I do believe this is slightly better than using a base class, Cell. Modules are often used as a way of grouping aspects of different classes, and this can be useful for code organization. But this technique should be used very judiciously. Primarily, I use modules in this way as a step in the path towards a better design. Separating out aspects of a class into modules can help find hidden dependencies, as well as highlight all the different responsibilities a class has. But they are rarely the place to stop.

So, with that option off the table, how do we eliminate the duplication? Let’s look at what we are trying to accomplish. Our goal is to have a link between the Cell and the Location it is at. Or, rather, our system needs to know this link. We haven’t actually seen anything to indicate the Cellclasses, themselves, need the link. Our assumption here is that something needs to see the link.

When having two types containing a link to the same type (Living|Dead)Cell and Location, a useful technique is to reverse the dependency.

classLocation

attr_reader :x, :y

attr_reader :cell

end

classLivingCell

def stays_alive?(number_of_neighbors)

number_of_neighbors == 2 ||

number_of_neighbors == 3

end

end

classDeadCell

def comes_to_life?(number_of_neighbors)

number_of_neighbors == 3

end

end

At this point, our cell classes are indeed just focused on information related to the cell (for example, rules). The topology is also further abstracted from the rules of the game. We can start to see that the Location class is taking on a structural role, providing the link between the topology and the cell that exists there. The cell classes are now focused on rules around evolution.

While the refactoring is good, it highlights a potential naming issue. Is Location the correct name for this class? From a reading point of view, it seems like a Cell should have a Location, not the other way around. This is arguable, of course, but it seems like potentially we chose the wrong name for the Location class. Perhaps it is better as a Coordinate.

classCoordinate

attr_reader :x, :y

attr_reader :cell

end

I’m not saying it is, or not, at this point. I only wanted to mention it is interesting how eliminating the duplication highlighted a possible naming issue. This is a good example of how applying these rules can often lead to other refactoring opportunities and insight into our design.