Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ruby mixins looking for a best practice

Tags:

ruby

mixins

I'm writing Ruby Gem where I have Connection module for Faraday configuration

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

Second module which makes API requests looks like this:

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

Class Cusomer where I use Request looks like this:

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

So here you see in Customer#all class method I'm calling Request#get method which is available because I extended Request in Customer. then I'm using self.extended method in Request module to be make Request#put available in Customer class, so I have question is this good approach to use mixins like this, or do you have any suggestion?

like image 239
user525717 Avatar asked Jul 20 '16 10:07

user525717


1 Answers

Mixins are a strange beast. Best practices vary depending on who you talk to. As far as reuse goes, you've achieved that here with mixins, and you have a nice separation of concerns.

However, mixins are a form of inheritance (you can take a peek at #ancestors). I would challenge you saying that you shouldn't use inheritance here because a Customer doesn't have an "is-a" relationship with Connection. I would recommend you use composition instead (e.g. pass in Connection/Request) as it makes more sense to me in this case and has stronger encapsulation.

One guideline for writing mixins is to make everything end in "-able", so you would have Enumerable, Sortable, Runnable, Callable, etc. In this sense, mixins are generic extensions that provide some sort of helpers that are depending on a very specific interface (e.g. Enumerable depends on the class to implement #each).

You could also use mixins for cross-cutting concerns. For example, we've used mixins in the past in our background jobs so that we could add logging for example without having to touch the source code of the class. In this case, if a new job wants logging, then they just mixin the concern which is coupled to the framework and will inject itself properly.

My general rule of thumb is don't use them if you don't have to. They make understanding the code a lot more complicated in most cases

EDIT: Adding an example of composition. In order to maintain the interface you have above you'd need to have some sort of global connection state, so it may not make sense. Here's an alternative that uses composition

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
like image 179
Josh Bodah Avatar answered Oct 15 '22 16:10

Josh Bodah