nixpulvis

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:

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.