Reputation: 103742
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
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
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
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
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