Stichword-Archiv: ruby

Ruby best practice: Implementing operator == and ensuring it doesn’t break

März 8, 2019 12:42 pm Veröffentlicht von

In ruby, comparing hashes, strings and objects is a complicated topic. Should you use equal?, eql? or ==? There is plenty of help on this topic, but in this post, we will focus on the interesting behavior of the == operator and how you can make it behave as you need it for your use case.

When comparing Hashes in Ruby, the == operator compares the content of a hash recursively.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
my_hash = {
    :sub_hash => {
        :value => 42
    }
}

my_second_hash = {
    :sub_hash => {
        :value => 42
    }
}

my_third_hash = {
    :sub_hash => {
        :value => 21
    }
}

puts "my_hash == my_second_hash? #{my_hash == my_second_hash}"
puts "my_hash == my_third_hash? #{my_hash == my_third_hash}"

1
2
my_hash == my_second_hash? true
my_hash == my_third_hash? false

Unfortunately, when comparing objects of arbitrary classes, the default operator only compares the object identity.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class MyClass
  def initialize(value)
    @value = value
  end
end

my_object = MyClass.new(42)
my_second_object = MyClass.new(42)

puts "my_object == my_second_object? #{my_object == my_second_object}"

1
my_object == my_second_object? false

If you want to do a deep comparison of objects of your class, you need to implement your own operator == by overriding the existing operator.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class MyClass
  attr_reader :value

  def initialize(value)
    @value = value
  end

  def ==(other)
    other.respond_to?("value") && value == other.value
  end
end

my_object = MyClass.new(42)
my_second_object = MyClass.new(42)

puts "my_object == my_second_object? #{my_object == my_second_object}"

1
my_object == my_second_object? true

That was easy. But imagine this was a bigger class and someone else needed to add a property, not being aware of the existence of this operator and some other code depending on it to ensure no public member of the object changed. How can you ensure such a change doesn’t sneak in unnoticed?

I stumbled across the following solution when implementing an operator == for a class in the BOSH code together with my colleague Max.

As BOSH code is written in TDD – and your code should be as well – writing a test that breaks with a change as the one described above should ensure the operator to keep working. But how can such a test look like?

Consider the following change to our code above:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class MyClass
  attr_reader :value
  attr_reader :value_new

  def initialize(value)
    @value = value
    @value_new = value
  end

  def ==(other)
    other.respond_to?("value") && value == other.value
  end
end

my_object = MyClass.new(42)
my_second_object = MyClass.new(42)

puts "my_object == my_second_object? #{my_object == my_second_object}"

To detect the variable @value_new has been added using rspec can be done with a test like the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
require './object_compare_op'

describe :MyClass do
  describe 'operator ==' do
    context 'when instance variables are modified' do
      let :obj do
        MyClass.new(42)
      end
      let :other_obj do
        MyClass.new(42)
      end

      all_members = MyClass.new(0).instance_variables.map { |var| var.to_s.tr('@', '') }
      all_members.each do |member|
        it "returns false when #{member} is modified" do
          eval <<-END_EVAL
            class MyClass
              def modify_#{member}
               @#{member} = 'foo'
              end
            end
          END_EVAL
          obj.send("modify_#{member}")
          expect(obj == other_obj).to(
            equal(false),
            "Modification of #{member} not detected by == operator.",
          )
        end
      end
    end
  end
end

The variable @value_new only has an attribute reader, so we cannot simply assign a new value. But this doesn’t stop you from changing the value. Not in Ruby. Using the eval in the test, we add a method for all existing instance variables of MyClass (one in each iteration) that modifies the member.

Afterwards, the newly added method is called to change the value of the member and the expect checks if the operator detects the modification. And – for our code above – will fail. Hence, whenever someone adds a new member to MyClass, he will be reminded to also it to the operator == by this test. Even if the code of test itself might not be as speaking, the output of the failing test is:

 Modification of value_new not detected by == operator.

In some situations you may want to exclude a member from this check as it is just internal or not important to the equality of two objects. To enable this, we added an exclude list for private members to the test. This adds a bit of complexity to adding new members to the class as the test will bother you and you also have to add the member to the exclude list, but it improves the safety of your operator ==.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
require './object_compare_op'

describe :MyClass do
  describe 'operator ==' do
    context 'when instance variables are modified' do
      let :obj do
        MyClass.new(42)
      end
      let :other_obj do
        MyClass.new(42)
      end

      all_members = MyClass.new(0).instance_variables.map { |var| var.to_s.tr('@', '') }
      private_members = %w[value_new]
      public_members = all_members - private_members
      public_members.each do |member|
        it "returns false when #{member} is modified" do
          eval <<-END_EVAL
            class MyClass
              def modify_#{member}
               @#{member} = 'foo'
              end
            end
          END_EVAL
          obj.send("modify_#{member}")
          expect(obj == other_obj).to(
            equal(false),
            "Modification of #{member} not detected by == operator.",
          )
        end
      end
    end
  end
end

With this kind of test, you can easily implement comparison operators for your classes that check for object equality rather than identity and ensure you do not forget to add new members of the class also to the comparison.
You can take a look at productive code in the BOSH code base hier kontaktieren. As you may see it’s not much different to what I presented here – it’s a universal approach to solve the problem.

Define interfaces in a duck typed language like ruby

Oktober 5, 2018 6:55 am Veröffentlicht von

In Java, it is very intuitive how interfaces are defined and used. You just create an interface in a similar way you would create a class and derive the classes, implementing the interface.

interface Drivable {
public void drive(int meters);
public void stop();
}

class Car implements Drivable {
public void drive(int meters) {
//start the engine
//go for it
}

public void stop() {
//stop the engine
}
}

class Bagger implements Drivable {
public void drive(int meters) {
//start engine
//start left track
//start right track
}

public void stop() {
//stop left track
//stop right track
}
}

class AutomatedDriver {
public void forward(Drivable vehicle, int meters) {
vehicle.drive();
vehicle.stop();
}
}

This results in a exlplicit class structure as depicted in the following class diagram.

However, in languages like ruby, interfaces are defined implicitly, which means that two classes implement the same interface as soon as they respond to the same interface. Take a look at our example as implemented in ruby:

class Car
public void drive(meters)
# start the engine
# go for it
end

def stop
# stop the engine
end
end

class Bagger
def drive(meters)
# start engine
# start left track
# start right track
end

def stop
# stop left track
# stop right track
end
end

class AutomatedDriver
def forward(vehicle, meters)
vehicle.drive();
vehicle.stop();
end
end

The classes Car and Bagger still both implement the Drivable interface. But as in ruby you use the so-called ducktyping, they both implement it implicitly by just responding to the same API, consisting of drive and stop. However, even in duck-typed languages, you might want to define and document your interfaces in a central point to make sure once you change it, all implementing classes do as well. You can do this by implementing unit tests to ensure the interface is fulfilled.

Following is an example of a rspec test to ensure our Drivable interface is implemented correctly.

shared_examples "a Drivable" do
it { expect(subject).to respond_to(:drive).with(1).argument }
it { expect(subject).to respond_to(:stop).with.no_args }
end

describe Car do
it_behaves_like "a Drivable"
end

describe Bagger do
it_behaves_like "a Drivable"
end

If the developer now changes something in the interface Drivable, he does so in the rspec test ensuring the interface. This test will fail for all classes that are expected to implement it but not yet do.

Even if it is not as intuitiv as it is in java, where your code just doesn’t compile if you fail to implement the interface, it is possible to define an interface and ensure it is implemented correctly in duck typed languages.

You might argue that you lose a bit of the flexibility of duck typing if you implement this for all your interfaces, and you are right! But in many cases, for example if the one defining the interface and the ones implementing it are different people, this is a very useful tool.

For example, imagine you are the author of a ruby library. A shared_example is a good and straight forward way to tell the users of your ruby gem what you expect their classes to behave like. Also, this will make them confident that if they upgrade to a newever version of your library, they will notice changes in the API by executing their test suite.