Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save kostia/5604137 to your computer and use it in GitHub Desktop.
Save kostia/5604137 to your computer and use it in GitHub Desktop.
The is a pseudo-blogpost about why I think an instance level implementations is to prefere over a class level implementation.

Why an instance level implementations is to prefere over a class level implementation.

First of all, let's make clear what is ment by an instance level implementation and a class level implementation.

Here is an example of an instance level implementation:

# driver1.rb

class Driver
  cattr_accessor :drunk_driver
  
  attr_reader :blood_alc_level

  def initialize
    @blood_alc_level = 0
  end

  def drink!
    @blood_alc_level += 1
  end
  
  self.drunk_driver = new.tap do |driver|
    3.times { driver.drink! }
  end
end

And here is an example of a class level implementation:

# driver2.rb

clas DrunkDriver
  class << self
    attr_accessor :blood_alc_level
    
    def drink!
      @blood_alc_level ||= 0
      @blood_alc_level += 1
    end
    
    # Test purpose only.
    def reset_blood_alc_level!
      @blood_alc_level = nil # Reset blood alcohol level.
      3.times { drink! } # Encrease the level to the default one.
    end
    
    self.reset_blood_alc_level!  
  end
end

To state it clearly: it's NOT about class methods vs. instance methods. It's about putting an implementation (with some behavior AND a state) into a class or module vs. putting it into an instance.

The tests.

Now to the question: Why is the first one to prefere over the last one? Let's analyze both implementations...

Basically they do the same. The first one creates an object, which is an instance of a class, it has some state and it behaves somehow. The last one also (implicitly) creates an object, this object also has a state and it also behaves in some particular way. Both then create a singleton object with a particular state.

So the last implementation does almost the same as the first one (and it is, by the way, shorter). But here is the trap: the main difference is it's globality.

Let's now look at the tests. Here is a test for the first implementation.

# driver1_spec.rb

describe Driver do
  its(:blood_alc_level) { should eq(0) }
  
  describe '#drink!' do
    it 'increases the blood alcohol level by one' do
      expect { subject.drink! }.to change { subject.blood_alc_level }.by(1)
    end
  end
  
  describe '.drunk_driver' do
    subject { Driver.drunk_driver }
    it { should be_a(Driver) }
    its(:blood_alc_level) { should eq(3) }
  end
  
end

And a test for the second one.

# driver2_spec.rb

describe DrunkDriver do
  subject { DrunkDriver }
  before { DrunkDriver.reset_blood_alc_level! }
  
  its(:blood_alc_level) { should eq(3) }
  
  describe '.drink!' do
    it 'increases blood alcohol level' do
      expect { subject.drink! }.to change { subject.blood_alc_level }.by(1)
    end
  end
end

The last implementation's state is global. So it's behavior is tightly coupled to it's global state (the system level state). Whether in the first implementation the behavior is completely independent from the global state of the whole system.

Extending functionality.

Let's look what happens if we expand the functionality.

# driver1.rb

class Driver
  cattr_accessor :drunk_driver
  
  attr_reader :blood_alc_level

  def initialize
    @blood_alc_level = 0
  end

  def drink!
    @blood_alc_level += 1
  end
  
  def sleep!
    @blood_alc_level -= 1 if @blood_alc_level > 0
  end
  
  self.drunk_driver = new.tap do |driver|
    3.times { driver.drink! }
  end
end

# driver1_spec.rb

describe Driver do
  its(:blood_alc_level) { should eq(0) }
  
  describe '#drink!' do
    it 'increases the blood alcohol level by one' do
      expect { subject.drink! }.to change { subject.blood_alc_level }.by(1)
    end
  end
  
  describe '#sleep!' do
    context 'when drunk' do
      it 'decreases the blood alcohol level by one' do
        subject.drink!
        expect { subject.sleep! }.to change { subject.blood_alc_level }.by(-1)
      end
    end
    
    context 'when sobber' do
      it 'does nothing' do
        expect { subject.sleep! }.to_not change { subject.blood_alc_level }
      end
    end
  end
  
  describe '.drunk_driver' do
    subject { Driver.drunk_driver }
    it { should be_a(Driver) }
    its(:blood_alc_level) { should eq(3) }
  end
  
end

And the class level implementation.

# driver2.rb

clas DrunkDriver
  class << self
    attr_accessor :blood_alc_level
    
    def drink!
      @blood_alc_level ||= 0
      @blood_alc_level += 1
    end
    
    def sleep!
      if @blood_alc_level && @blood_alc_level > 0
        @blood_alc_level -= 1
      end
    end
    
    # Test purpose only.
    def reset_blood_alc_level!
      @blood_alc_level = nil # Reset blood alcohol level.
      3.times { drink! } # Encrease the level to the default one.
    end
    
    self.reset_blood_alc_level!
  end
end

# driver2_spec.rb

describe DrunkDriver do
  subject { DrunkDriver }
  before { DrunkDriver.reset_blood_alc_level! }
  
  its(:blood_alc_level) { should eq(3) }
  
  describe '.drink!' do
    it 'increases blood alcohol level' do
      expect { subject.drink! }.to change { subject.blood_alc_level }.by(1)
    end
  end
  
  describe '.sleep!' do
    context 'when drunk' do
      it 'decreases blood alcohol level' do
        subject.drink!
        expect { subject.sleep! }.to change { subject.blood_alc_level }.by(-1)
      end
    end
    
    context 'when sobber' do
      it 'does nothing' do
        # Ensure driver is sobber.
        subject.sleep! until subject.blood_alc_level == 0
        expect { subject.sleep! }.to_not change { subject.blood_alc_level }
      end
    end
  end
end

We clearly see, that in the first implementation we have a lot more code to write. But on the other hand, we don't have to think about pollution of the global state. We just use a new instance every time and then just through it away. Whether in the second implementation we have to carry about the global state and we reset that state before every test case.

Now assume our code system will become more complex (which systems with budgets are tending to) and our objects will get more complex states, with lot of instance variables and lot more methods to change them.

With the last implementation we will have to carry about resetting the whole environment for each test case. Our before block will have to handle complex functionalities and share knowledge with the implementation. We can also implement a reset class method, but it doesn't make it much better.

The core problem.

So what is the core problem? The core problem is that the last implementation mixes different abstraction levels: the behavior at the "component" (or "unit") level and the system level configuration (the global state).

And it gets really awkward when it comes to the tests, because we're doing BDD and BDD is basically unit testing (which is, yes - the "unit level testing"). By mixing and tightly coupling unit level functionality with system level functionality in the implementation, we're creating a system, where our unit tests have to handle aspects from a higher abstraction level. But unit tests are NOT concepted for this purpose. And so they are quickly getting very complex, hard to understand, they pollute the global state and affect each other. They are soon getting slow because every test has to reset the global state and so on...

Conclusion

At the end of they day you have to decide, whether you prefere:

  • an implementation with clearly separated levels of abstraction (and somehow longer implementations).
  • or a less verbose implementation with a complex and fragile test suite.

Personally, I would always prefere the first one, because the maintainment costs tend to grow exponentially in a system, where you have no clear separation of concerns and abstraction levels. But it's my personal opinion...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment