Ruby混合模块寻找最佳实践

5

我正在编写一个Ruby Gem,其中有一个名为Connection的模块用于Faraday配置

module Example
  module Connection
    private

    def connection
      Faraday.new(url: 'http://localhost:3000/api') do |conn|
        conn.request  :url_encoded             # form-encode POST params
        conn.response :logger                  # log requests to STDOUT
        conn.adapter  Faraday.default_adapter  # make requests with Net::HTTP
        conn.use      Faraday::Response::ParseJson
        conn.use      FaradayMiddleware::RaiseHttpException
      end
    end
  end
end

第二个模块用于进行API请求,代码如下:
module Example
  module Request
    include Connection

    def get(uri)
      connection.get(uri).body
    end

    def post(url, attributes)
      response = connection.post(url) do |request|
        request.body = attributes.to_json
      end
    end

    def self.extended(base)
      base.include(InstanceMethods)
    end

    module InstanceMethods
      include Connection

      def put(url, attributes)
        response = connection.put(url) do |request|
          request.body = attributes.to_json
        end
      end
    end
  end
end

我使用RequestCustomer类如下:

module Example
  class Customer
    extend  Request

    attr_accessor :id, :name, :age

    def initialize(attrs)
      attrs.each do |key, value|
        instance_variable_set("@#{key}", value)
      end
    end

    def self.all
      customers = get('v1/customer')
      customers.map { |cust| new cust }
    end

    def save
      params = {
        id:   self.id,
        age:  self.age
        name: self.name,
      }

      put("v1/customers/#{self.id}", params)
    end
  end
end

在这里,你可以看到在Customer#all类方法中,我调用了Request#get方法,这是因为我在Customer中扩展了Request。然后我在Request模块中使用了self.extended方法,使Request#putCustomer类中可用。那么,这种使用mixin的方式是否好,或者你有什么建议呢?

1个回答

6
混入是一种奇怪的东西,最佳实践因人而异。就重用而言,你已经通过混入实现了这一点,并且具有良好的关注点分离。
但是,混入是一种继承形式(可以查看#ancestors)。我要质疑你在这里不应该使用继承,因为"客户"与"连接"之间没有"is-a"关系。我建议你改用组合(例如传递"连接/请求"),因为在这种情况下更有意义并且具有更强的封装性。
编写混入的一个指导原则是使所有内容都以"-able"结尾,因此您将拥有Enumerable、Sortable、Runnable、Callable等。从这个意义上说,混入是通用扩展,提供某些助手,这些助手取决于非常特定的接口(例如,Enumerable依赖于类实现each)。
您还可以使用混入来处理横切关注点。例如,在我们的后台作业中过去使用过混入,以便我们可以添加日志记录,例如,而无需触及类的源代码。在这种情况下,如果新作业需要日志记录,则只需混入与框架耦合并将自己正确注入的关注点。
我的一般经验法则是,如果不必使用它们,则不要使用它们。在大多数情况下,它们会使理解代码变得更加复杂。
编辑:添加组合示例。为了保持上面的接口,您需要有某种全局连接状态,因此可能没有意义。这里是一种使用组合的替代方法。
class CustomerConnection
  # CustomerConnection is composed of a Connection and retains isolation
  # of responsibilities. It also uses constructor injection (e.g. takes
  # its dependencies in the constructor) which means easy testing.
  def initialize(connection)
    @connection = connection
  end

  def all_customers
    @connection.get('v1/customers').map { |res| Customer.new(res) }
  end
end

connection = Connection.new
CustomerConnection.new(connection).all_customers

1
谢谢Josh的回答,但是你的意思有点不太清楚:我建议你使用组合(例如传递Connection/Request),因为在这种情况下它对我来说更有意义,并且具有更强的封装性。 - user525717
Josh的意思是你应该创建一个Connection/Request类,使其在你的Customer类中可用。当你实例化你的Customer时,你可以传入你的Request类,或者你可以在Customer的初始化器中实例化一个Request。个人而言,我更喜欢使用组合的方式,因为这样我可以轻松地测试单个对象。 - unflores
Unflores,你能否提供一个示例? - user525717
@user525717 我添加了一个例子。为了保留Customer.all接口,您必须在某个时候将连接绑定到类上(这可能比值得的努力更费力,因此混合方法可能会使事情变得更简单)。 - Josh Bodah

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接