Splitting Cats and Dogs 1659 words published on January 05, 2019.
Often when you’re programming you find the need to split things up a bit. This is such a fundamental part of programming that almost every language gives you many tools to aid with this. Maybe it’s modules, classes, impls, or even packages. The list goes on… In fact, one of the best forms of splitting can be a simple function. Even comments can play a role in this if you’d like.
There’s a number of reasons you’d want to split up some part of a program. You might be looking at a class with the need for countless functions. Instead of defining them all one after another, it makes sense to group the functions and move them somewhere so it’s clear how they are related. Or, you might be looking to share some logic across a few classes. This is key to writing good software, since maintaining two versions of the same thing is cumbersome. Remember, stay DRY (Don’t Repeat Yourself).
Grouping
Grouping shouldn’t require too much thought, aside from a cohesive name for the way in which the functions belong together. It can often be all to easy to end up with needless wrapper classes, or other added machinery. Another good acronym to keep around is, KISS (Keep It Simple, Stupid).
Consider an example:
class Dog
def bark
# ...
end
def growl
# ...
end
def eat
# ...
end
def drink
# ...
end
def poop
# ...
end
def pee
# ...
end
end
I think it’s pretty clear here what functions should be grouped with each other
(this isn’t always so clear), and all we need to do is split them up in some
way. Methods like bark
and growl
are clearly concerned with dog
communications, while methods like eat
and poop
are concerned with the
sustaining life at the most basic level.
Here’s a couple options:
class Dog
# Communicative Methods #
#########################
def bark
# ...
end
def growl
# ...
end
# Consumptive Methods #
######################
def eat
# ...
end
def drink
# ...
end
def poop
# ...
end
def pee
# ...
end
end
# Or, even better in my book.
class Dog
module Communicative
def bark
# ...
end
def growl
# ...
end
end
module Consumptive
def eat
# ...
end
def drink
# ...
end
def poop
# ...
end
def pee
# ...
end
end
include Communicative
include Consumptive
end
People often view a module
as a way to share functionality, but here we’re
just using it to group similar functions. The main thing we’ve done is make it
clear which functions are related, and therefore a good idea to read
together. If you want to share some part of your program’s design, then
you’ll need to think more, though a module
may still be the solution.
Sharing
Sharing is about abstraction. It’s about defining an interface, and letting users rely on it. Sharing code lets you keep things DRY and maintainable, and also helps keep testing focused.
It’s a fun fact that in theoretical computer science, the term abstraction and function can be used interchangably. The dual, is application, or calling the function. With only these two things you’ve got yourself a turing complete language called the λ-calculus.
Let’s consider what happens when we want to add a new Cat
class to our
program. The first thing we should notice is that we can share all of the
Consumptive
functionality, since just like dogs, cats also eat, drink, and the
rest.
Here’s a complete implementation of the Consumptive
module we’ll share:
module Consumptive
def initialize
@stomach = []
super
end
def eat(food)
@stomach << { food: food }
end
def drink(drink)
@stomach << { drink: drink }
end
def poop
digest_to_completion(:food, "can't poop")
end
def pee
digest_to_completion(:drink, "can't pee")
end
private
def digest_to_completion(kind, error_message)
victuals = @stomach.select { |v| v[kind] }
if victuals.empty?
raise error_message
end
@stomach = @stomach - victuals
end
end
Notice how we’ve made use of a Ruby instance variable here. It’s a really good idea to keep the use of these as close to each other as possible. It’s very hard to reason about a module that’s touching another module’s state. Keep spooky action at a distance out of our programs!
Some languages even make it impossible to inspect the state of another object’s
guts (@stomach
if you will), this can be a very good property. Ruby is a
little more liberal, requiring you to be more disciplined. With great power
comes great responsibility.
Well, now that everything is wrapped up nicely inside this module, all we have
to do is include it. This looks just like it did when we used a module
for
grouping before. The only difference is that the Consumptive
module now must
be visible to both the Cat
and Dog
classes.
class Dog
include Consumptive
# ...
end
class Cat
include Consumptive
end
Testing
With all this in mind, let’s see how we test the Dog
class and the
Consumptive
module. Each should only test the parts they are concerned with
directly.
First, since all of this code should run, let’s require
Ruby’s testing
framework.
require 'minitest/autorun'
Next, we can define the Dog
spesific tests. These are free to do anything
they’d like with the dog, but shouldn’t test Consumptive
since that’s defined
elsewhere.
class TestDog < Minitest::Test
def setup
@dog = Dog.new
end
# Implemention needed, since we left it as "# ...".
def test_bark
assert_equal @dog.bark, 'Woof'
end
# Tests ommited...
end
Finally, we can test the Consumptive
module. We want to test this
functionality without relying on our Dog
or Cat
since they aren’t the
concern of this module. A dummy class can be created just for our test, making
it very clear what’s going on.
class TestConsumptive < Minitest::Test
class TestAnimal
include Consumptive
attr_reader :stomach
end
def setup
@animal = TestAnimal.new
end
def test_eat
assert_empty @animal.stomach
@animal.eat("pepper")
refute_empty @animal.stomach
end
def test_drink
assert_empty @animal.stomach
@animal.drink("slurppy")
refute_empty @animal.stomach
end
def test_poop
@animal.eat("sand")
refute_empty @animal.stomach
@animal.poop
assert_empty @animal.stomach
end
def test_pee
@animal.drink("soda")
refute_empty @animal.stomach
@animal.pee
assert_empty @animal.stomach
end
end
These tests don’t cover enough cases, but you get the idea. We seem to have a functioning set of animals on our hands!
Naming Conventions
OK, so depending on who you ask we may have broken some rules here… First of
all, people may be expecting to see a module
in it’s own file. Well that one
is easy to fix, just move it there… though remember to preserve it’s
namespace. For example, Dog::Communicative
could be relocated to
dog/communicative.rb
as:
class Dog
module Communicative
# ...
end
end
Another convention is to name your concerns (modules
) as something-able. This
is a great huristic, but may not always be reasonable. I’ve intentionally
broken this pattern here. Keep in mind, not everyone is as passionate as you
about finding the perfect name, and while the better job we do at naming the
easier it will be to read, it’s never going to be perfect.
Just try to remember this:
- A
class
is a noun - A
module
is an adjective - A
method
is a verb
Hopefully in the process of thinking through how to split up your programs
you’ll discover elegant abstractions, and clean implementations. Or at least as
clean as a bunch of functions that deal with poop
can ever be.