What’s the ‘simplest thing?’

The conventional wisdom when doing programming is to start with the ‘simplest thing’. But what is the ‘simplest thing’? How do you know something is ‘simple’? And are there different ‘simple things’ that work better with different approaches?

I see four main candidates for the ‘simplest thing’:

The simplest interaction involves building one small interaction with one object: one whose interface you have designed. The simplest rule involves identifying some constraint on the system and building that. The simplest component involves building the smallest, most atomic part of the system. The simplest chunk involves identifying one large-ish object in the program, building it, and breaking it down into smaller objects.

Using the Bank Tech Test, here are four different first steps you could take.

The simplest interaction.

The simplest interaction is my favourite first building block: a behavioural one. An interaction is an atomic transaction with a program object. For instance: ‘an Account can store money’.

One benefit to this approach is that it allows us to think about a high-level interface first. We can start by writing a test for the code we wish worked:

  
  # An account can store money
  my_barclays_account = Account.new
  my_barclays_account.store(10) # => 10
  my_barclays_account.store(10) # => 20
  

Then we can make it work.

  
  class Account
    def initialize
      @balance = 0
    end

    def store(amount)
      @balance += amount
    end
  end
  

Because we're starting with an interface, starting from the simplest interaction is amenable to outside-in TDD. This means we can use an integration test to build out the Account interface:

  
  RSpec.describe 'Interacting with an Account' do
    describe '#store' do
      it 'stores money' do
        my_barclays_account = Account.new

        my_barclays_account.store(10)

        expect(my_barclays_account.store(10)).to eq 20
      end
    end
  end
  

(At the moment, we've only got one class: the Account. But later, Account might rely on other things. This integration test will grow to test the Account interface without mocking anything else.)

Repeated application of the simplest interaction will start to produce the simplest chunk (see the last section). When this starts to happen – that is, when you notice your class contains too many responsibilities – do the following:

That way repeatedly applying the simplest interaction builds towards a complete program manageably.

The simplest rule.

The simplest rule is another behavioural building block. A rule is something that is necessarily true about a system. For example: ‘an Account cannot be negative’. Another example: ‘an Account cannot store a negative amount’.

A rule is sometimes quite complicated to implement, and fulfilling it may involve many objects. Therefore, it's best to build the rule similarly to building a chunk (see the next section): as a bloated glob with multiple responsibilities. Then, use refactoring techniques to break it down.

Starting with the simplest rule involves both thinking about the interface at a high level and the objects involved, therefore it's amenable to both inside-out and outside-in TDD.

  
  # An account cannot be negative
  my_barclays_account = Account.new
  my_barclays_account.withdraw(10) # => Error: account cannot be negative
  

We can build the rule quite simply, to start:

  
  class Account
    def initialize
      @balance = 0
    end

    def withdraw(amount)
      raise 'Error: account cannot be negative'
    end
  end
  

We can then look at another rule:

  
  class Account
    def initialize
      @balance = 0
    end

    def store(amount)
      raise 'Error: cannot store a negative amount'
    end

    def withdraw(amount)
      raise 'Error: account cannot be negative'
    end
  end
  

In both of these examples, we've ended up programming away from the happy path, but we could have selected rules that kept us to it (for example, 'the balance is the sum of all transactions').

The simplest component.

The simplest component is a more advanced technique. In it, you start by fully-defining the most atomic building block of a system. For example: a quantity, a currency, or a transaction.

Building the simplest component is amenable to inside-out TDD. It works well with a Chicago approach to TDD.

A powerful example of this approach is using the quantity pattern.

  
  # Understands the presentation of a transaction
  class Transaction
    include Comparable

    def initialize(amount)
      @amount = amount
    end

    def +(other)
      @amount + other.amount
    end

    def -(other)
      raise 'Cannot have negative money' if other > self
      @amount - other.amount
    end

    def <=>(other)
      @amount <=> other.amount
    end

    def credit?
      @amount.positive?
    end

    def debit?
      @amount.negative?
    end

    protected

    attr_reader :amount
  end
  

Now that we have a thorough Transaction object, we can easily work with a variety of different kinds of transaction.

  
  cinema_tickets = Transaction.new(14)
  diet_coke      = Transaction.new(1.65)

  balance = cinema_tickets + diet_coke # => 15.65
  cinema_tickets > diet_coke # => true
  diet_coke - cinema_tickets # => 'Cannot have negative money'
  

From here, we can build outwards: perhaps we'd look for something to house the transactions, or something to do calculations with them.

I generally recommend against the simplest component unless you have a very solid understanding of the problem domain. It therefore works very well when used with programmer anarchy, a post-Agile philosophy developed by Fred George.

The simplest chunk.

The simplest chunk combines state and behaviour in a building block. For example: ‘an Account’. It's my least preferred first step, because it's too easy to accidentally build a whole program, then wind up with a painful and tedious refactor (which we'll do now). I really only advise starting with the simplest chunk when spiking part of a system.

Starting with the simplest chunk involves

Therefore, it's amenable to both inside-out and outside-in TDD (so long as the chunk is small). It works especially well with a mockist approach to TDD. I'm going to take deliberately large steps, so I won't be using TDD as I go. I rarely use TDD if I'm spiking: as it's exploratory code I'm going to throw away.

Let's start by spiking the sort of object that an Account could be. It should:

As we go, we can make interesting decisions, and refer to the spec for each one. For example: should we accept a starting balance? After withdrawing do we return the balance, the date-stamped withdrawal transaction, or just the withdrawn amount? Answering these questions with the spike with help us to move more swiftly later on.

We'd like to be able to use the Account like this:

  
  require 'account'

  account = Account.new
  account.store(10)
  account.store(20)
  account.withdraw(14)

  # should give an error
  account.store(-10) 

  # should give an error
  account.withdraw(100)

  # should print a statement 
  # (without the erroneous transactions)
  account.statement
  

Sticking this code in a script gives us a kind of integration test we can run to catch errors as we build out the Account functionality. We'll keep the Account interface the same as we go: so this 'test' shouldn't need to change.

We can use the simplest chunk approach to understand the ultimate structure of the system. For instance, it's apparent early on that we need Accounts to store date-stamped transactions: not just a simple @balance integer.

  
  class Account
    # The spec doesn't talk about starting balances,
    # so no starting transactions when creating an account
    def initialize
      @transactions = []
    end

    def store(amount)
      raise 'No storing negative money!' if amount < 0
      @transactions.push([amount, Time.now])

      # storing should probably give us the stored amount?
      amount
    end

    def withdraw(amount)
      raise 'Not enough money!' if amount > @transactions.map { |transaction| transaction[0] }.reduce(:+)
      @transactions.push([-amount, Time.now])

      # withdrawing should probably give us the withdrawn amount?
      amount
    end

    def statement
      print "date || credit || debit || balance\n"
      @transactions.inject(0) do |running_balance, transaction|
        running_balance += transaction[0]
        print "#{ transaction[1] }  || #{ transaction[0] if transaction[0].positive? } || #{ transaction[0] if transaction[0].negative? } || #{ running_balance }\n"
        running_balance
      end
    end
  end
  

Now that we meet the integration spec, we can use extract method on this spike to start investigating what responsibilities are hiding inside Account:

  
  class Account
    def initialize
      @transactions = []
    end

    def store(amount)
      raise 'No storing negative money!' if amount < 0
      @transactions.push(transaction(amount))
      amount
    end

    def withdraw(amount)
      raise 'Not enough money!' if amount > running_balance
      @transactions.push(transaction(-amount))
      amount
    end

    def statement
      print_header
      print_transactions
    end

    private

    def transaction(amount)
      [amount, Time.now]
    end

    def running_balance(transactions = @transactions)
      transactions.map { |transaction| transaction[0] }.reduce(:+)
    end

    def print_header
      print "date || credit || debit || balance\n"
    end

    def print_transactions
      @transactions.each { |transaction| print_transaction(transaction) }
    end

    def print_transaction(transaction)
      print "#{ transaction[1] }  || #{ transaction[0] if transaction[0].positive? } || #{ transaction[0] if transaction[0].negative? } || #{ running_balance(transactions_up_to(transaction)) }\n"
    end

    def transactions_up_to(transaction)
      @transactions.slice_after(transaction).to_a[0]
    end
  end
  

Using extract method exposed four extra sets of responsibilities, which now lurk in the private methods of Account:

That last responsibility could belong to the same object that manages and queries Transactions: we don't know yet. We should use extract class on each responsibility, checking as we go. We can come up with some names for objects that could be responsible for each of the above:

Let's use extract class a few times:

  
  # Understands the presentation of a single transaction
  class Transaction
    attr_reader :amount

    def initialize(amount)
      @amount = amount
      @time   = Time.now
    end

    def debit?
      @amount < 0
    end

    def credit?
      @amount >= 0
    end

    def time
      @time.strftime("%d/%m/%Y")
    end
  end

  # Understands the calculation of the sum of transaction amounts
  class Balance
    def self.of(transactions)
      transactions.map(&:amount).reduce(:+)
    end
  end

  # Understands the management and querying of transactions
  class Transactions
    include Enumerable

    def initialize
      @state = []
    end

    def each(&block)
      @state.each(&block)
    end

    def store(amount)
      @state.push(Transaction.new(amount))
      amount
    end

    def withdraw(amount)
      @state.push(Transaction.new(-amount))
      amount
    end

    def up_to(transaction)
      @state.slice_after(transaction).to_a[0]
    end
  end

  # Understands the presentation of statements
  class Statement
    HEADER = "date || credit || debit || balance\n".freeze

    def initialize(transactions)
      @transactions = transactions
    end

    def self.with(transactions)
      new(transactions).to_s
    end

    def to_s
      print HEADER
      print transactions
    end

    private

    def transactions
      @transactions.inject("") do |output, transaction| 
        output << " #{ transaction.time }  ||" 
        output << " #{ transaction.amount if transaction.credit? } ||"
        output << " #{ transaction.amount if transaction.debit? } ||" 
        output << " #{ Balance.of(@transactions.up_to(transaction)) }\n"
      end
    end
  end

  # Understands the filtering of appropriate activity
  class Account
    def initialize
      @transactions = Transactions.new
    end

    def store(amount)
      raise 'No storing negative money!' if amount < 0
      @transactions.store(amount)
    end

    def withdraw(amount)
      raise 'Not enough money!' if amount > Balance.of(@transactions)
      @transactions.withdraw(amount)
    end

    def statement
      Statement.with(@transactions)
    end
  end
  

I advise against picking the simplest chunk: it's too easy to get caught up in doing too much. People often accidentally build the simplest chunk when what they wanted to build was the simplest interaction. A good way to avoid that is to use a rigid 15-minute timebox. If you run over and you're not done (implementation and refactoring), start again with a smaller, simpler interaction.

On the other hand, doing this spike has opened me to the possibility of the existence of services, a statement object, the Account-as-a-filter, and so on. These are all valuable insights for design decisions I might take as a I move in a more step-by-step manner.

Summary

There are four different ways to define the 'simplest thing':

The simplest interaction involves building one small interaction with one object: one whose interface you have designed. The simplest rule involves identifying some constraint on the system and building that. The simplest component involves building the smallest, most atomic part of the system. The simplest chunk involves identifying one large-ish object in the program, building it, and breaking it down into smaller objects.

Mostly, my 'simplest step' starts with the simplest interaction. After a while, lots of simple interactions lead to an object that looks a bit like the simplest chunk: but at that point it's manageable to extract method and then extract class.

One thing to avoid is prematurely building out new simplest chunks halfway through building something else. Avoid introducing new objects until extract method and extract class tell you to, or until some deliberate new design step has begun. A good way to hold yourself to this is to use 15-minute design cycles.