向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类添加可互换行为的最佳方式是什么?

感觉应该是一个模块

我不知道。模块用于向类(或其他模块)注入相同的行为,而你想要不同的行为。

如何在我的SomeBuilder中互换使用不同的模块课吗?

在ruby中,你将得到一个运行时错误,而不是编译器强制你的接口类有一个Process方法的规则。因此,您可以将任何类与SomeBuilder类结合使用,但如果该类没有Process方法,那么您将获得运行时错误。换句话说,在ruby中,您不必向编译器声明与SomeBuilder类一起使用的类必须实现IDataProcessor接口。

评论的例子:

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());