BFree
BFree

Reputation: 103742

What's the best way to add interchangeable behavior to a ruby class?

I come from a C# background, and that's probably the reason for my hesitation, but imagine we have the following scenario. We have a class that takes in some raw data, and then pushes it up to some external API. In the process of doing so, we need to process the data according to some business rules. The catch is, these business rules aren't always the same. In C#, I would use an interface for the "data processing" business rules, and then use different implementations at different times:

public interface IDataProcessor
{
    object Process(object data);
}

public class SomeBuilder
{
    private object _data;
    private IDataProcessor _dataProcessor;
    public SomeBuilder(object data, IDataProcessor dataProcessor)
    {
        _data = data;
        _dataProcessor = dataProcessor;
    }

    public void Build()
    {
        var processedData = _dataProcessor.Process(_data);
        //do the fun building stuff with processedData
    }
}

In the above example, I would have different implementations of my IDataProcessor that process the data differently.

Now, in the Ruby world, my first inkling was to do something similar:

class SomeBuilder
    def initialize(some_data, data_processor)
        @some_date = some_date
        @data_processor = data_processor
    end

    def build
        processed_data = @data_processor.process(@some_data)
        #do build logic
    end
end

My issue is twofold: First, this somehow doesn't "feel" quite right in the Ruby ecosystem. e.g. is this the Ruby way? Second of all, if I were to go down this path, what would the data_processor look like? It feels like it should be a module, since it's just some behavior, but if it is just a module, how do I interchangeably use different modules in my SomeBuilder class?

Thanks in advance!

Upvotes: 0

Views: 253

Answers (4)

Jörg W Mittag
Jörg W Mittag

Reputation: 369428

Your interface has only a single method. An object with only a single method is isomorphic to a function/procedure. (And an object with some fields and a single method is isomorphic to a closure). Therefore, you would actually use a first-class procedure.

class SomeBuilder
  def initialize(some_data, &data_processor)
    @some_data, @data_processor = some_data, data_processor
  end

  def build
    processed_data = @data_processor.(@some_data)
    #do build logic
  end
end

# e.g. a stringifier-builder:
builder = SomeBuilder.new(42) do |data| data.to_s end
# which in this case is equivalent to:
builder = SomeBuilder.new(42, &:to_s)

Actually, you would probably do the same thing in C# as well. (Plus, you probably should make it generic.)

public class SomeBuilder<I, O>
{
    private I _data;
    private Func<I, O> _dataProcessor;
    public SomeBuilder(I data, Func<I, O> dataProcessor)
    {
        _data = data;
        _dataProcessor = dataProcessor;
    }

    public void Build()
    {
        var processedData = _dataProcessor(_data);
        //do the fun building stuff with processedData
    }
}

// untested

// e.g. a stringifier-builder
var builder = new SomeBuilder(42, data => data.ToString());

Upvotes: 0

Max
Max

Reputation: 22325

Combining ideas from other answers:

class DataProcessor
  def initialize &blk
    @process = blk
  end

  def process data
    @process[data]
  end
end

multiply_processor = DataProcessor.new do |data|
  data * 10
end

reverse_processor = DataProcessor.new do |data|
  data.reverse
end

This works just fine with your example SomeBuilder class.

This is sort of a happy medium between Michael Lang and 7stud's answers. All data processors are instances of one class, which allows you to enforce common behavior, and each processor is defined using a simple block, which minimizes code repitition when defining new ones.

Upvotes: 0

7stud
7stud

Reputation: 48599

It feels like it should be a module

I don't know about that. Modules are used for injecting identical behavior into classes(or other modules), and you want different behavior.

how do I interchangeably use different modules in my SomeBuilder class?

Instead of a compiler enforcing the rule that your interface class has a Process method, in ruby you will get a runtime error. So you can use any class you want in conjunction with your SomeBuilder class, but if the class doesn't have a Process method, then you will get a runtime error. In other words, in ruby you don't have to announce to a compiler that the class you use in conjunction with your SomeBuilder class must implement the IDataProcessor interface.

Comment example:

class SomeBuilder
  def initialize(some_data)
    @some_data = some_data
  end

  def build
    processed_data = yield @some_data
    puts processed_data
  end
end

sb = SomeBuilder.new(10)

sb.build do |data|
  data * 2
end

sb.build do |data|
  data * 3
end


--output:--
20
30

Upvotes: 1

Michael Lang
Michael Lang

Reputation: 1038

You're on the right track. Ruby Modules and Classes can both be used. In this scenario, I would tend to have a DataProcessor module that wraps the various concrete implementations of the processors. So, that would give:

banana_processor.rb:

module DataProcessor
  class Banana
     def process(data)
       # ...
     end

     # ...
  end
end

apple_processor.rb:

module DataProcessor
   class Apple
      def process(data)
        # ...
      end

      # ...
   end
end

Then you instantiate the processor with:

processor = DataProcessor::Apple.new

Basically, the module DataProcessor "namespaces" your processor classes and more useful in larger libraries where you might have a name collision or if you're producing a gem that might get included in any number of projects where name collisions are possible.

Then for your Builder class

class SomeBuilder
  attr_reader :data, :processor, :processed_data

  def initialize(data, processor)
    @data = data
    @processor = processor
  end

  def build
    @processed_data = @processor.process(@data)
    # do build logic
  end

...and used:

some_processor = DataProcessor::Apple.new
builder = SomeBuilder.new(some_data, some_processor)
builder.build

Upvotes: 0

Related Questions