向ruby类添加可互换行为的最佳方式是什么?
本文关键字:最佳 方式 是什么 换行 ruby 添加 互换 | 更新日期: 2023-09-27 18:14:25
我来自c#背景,这可能是我犹豫的原因,但是想象一下我们有以下场景。我们有一个类接收一些原始数据,然后把它推送到一些外部API。在这样做的过程中,我们需要根据一些业务规则来处理数据。问题是,这些业务规则并不总是相同的。在c#中,我会为"数据处理"业务规则使用一个接口,然后在不同的时间使用不同的实现:
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
}
}
在上面的例子中,我将使用不同的IDataProcessor
实现来处理不同的数据。
现在,在Ruby世界里,我的第一个想法是做类似的事情:
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
我的问题有两个:首先,这在Ruby生态系统中"感觉"不太对。这是Ruby的方式吗?其次,如果我沿着这条路径走下去,data_processor会是什么样子?感觉它应该是一个模块,因为它只是一些行为,但如果它是只是一个模块,我如何在我的SomeBuilder类互换使用不同的模块?
提前感谢!
感觉应该是一个模块
我不知道。模块用于向类(或其他模块)注入相同的行为,而你想要不同的行为。
在ruby中,你将得到一个运行时错误,而不是编译器强制你的接口类有一个Process方法的规则。因此,您可以将任何类与SomeBuilder类结合使用,但如果该类没有Process方法,那么您将获得运行时错误。换句话说,在ruby中,您不必向编译器声明与SomeBuilder类一起使用的类必须实现IDataProcessor接口。如何在我的SomeBuilder中互换使用不同的模块课吗?
评论的例子:
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
你做对了。Ruby模块和类都可以使用。在这个场景中,我倾向于使用一个DataProcessor模块来包装处理器的各种具体实现。因此,这将得到:
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
然后使用:
实例化处理器processor = DataProcessor::Apple.new
基本上,模块DataProcessor"命名空间"你的处理器类,更有用的是在较大的库中,你可能会有一个名称冲突,或者如果你正在生产一个宝石,可能会被包含在任何数量的项目,其中名称冲突是可能的。
然后对于你的Builder类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
…和使用:
some_processor = DataProcessor::Apple.new
builder = SomeBuilder.new(some_data, some_processor)
builder.build
结合其他答案的想法:
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
这在您的示例SomeBuilder
类中工作得很好。
这是Michael Lang和7stud的答案之间的一种折衷。所有的数据处理器都是一个类的实例,这允许你执行共同的行为,并且每个处理器都使用一个简单的块来定义,这在定义新处理器时最大限度地减少了代码重复。
你的接口只有一个方法只有一个方法的对象与函数/过程同构。(具有一些字段和单个方法的对象与闭包是同构的)。因此,实际上应该使用一级过程。
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)
实际上,你可能也会在c#中做同样的事情。(另外,你可能应该让它通用。)
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());