Skip to content

Instantly share code, notes, and snippets.

@johnlane
Created April 16, 2016 19:48
Show Gist options
  • Save johnlane/1489c066a12286847c487c2cd743fccf to your computer and use it in GitHub Desktop.
Save johnlane/1489c066a12286847c487c2cd743fccf to your computer and use it in GitHub Desktop.
Can a Trailblazer concept inherit from another and be able to extend its operati ons (multiple inheritance) ?

The effect of multiple inheritance can be achieved by using modules.

First define the ActiveRecord objects like this:

class Topic < ActiveRecord::Base; end
class Location < ActiveRecord::Base; end

There is no longer a base Tag abstract class, allowing Tag to be defined as a module like this (app/concepts/tag/crud.rb):

module Tag
  module Create
    def self.included(base)
      base.send :include, Trailblazer::Operation::Model
      base.send :model, base.parent # e.g. Thing::Tag => Thing
      base.send :contract, Form
    end

    class Form < Reform::Form
      property ...
    end

    def process(params)
      ...
    end
  end
  module Update
    def self.included(base)
      base.send :action, :update
    end
  end
  module Delete
    def self.included(base)
      base.send :action, :find
    end
    def process(params)
      ...
    end
  end
end

Code that would normally be placed inside operation classes (such as include Model and contract) are placed inside a self.included method so that they are executed within the scope of the including class. The ruby send method needs to be used to invoke such methods on the including class from within the module's self.included method.

Using this Tag module, a Topic tag would look like this (app/concepts/tag/topic/crud.rb)

class Topic
  class Create < Trailblazer::Operation
    include Tag::Create
    contract do
      property ...
    end
  end
  class Update < Create
    include Tag::Update
  end
  class Delete < Create
    include Tag::Delete
    def process(params)
      ....
      super
    end
  end
end

This allows extension of the Tag contract by Topic::Create, which adds properties to the contract, and further customisation of Tag methods like the Delete::process example that calls super to invoke Tag::Delete::process. Aditionally, the contract will know it's a Topic so things like simple_form will work properly.

Consider an abstract Tag concept where there are different kinds of tags, say Topic and Location (amongst others), that are unrelated apart from being tags. They have the same base Tag properties in common but are otherwise different.

A Topic concept is based on a similar Tag concept. An operation like Topic::Update would usually inherit from Topic::Create, but such an operation also needs to inherit from Tag::Update. Ruby doesn't support multiple inheritance - can Trailblazer support this?

  • Trailblazer operations support inheritance through a builds block that allows them to instantiate a subclass based on the contents of the supplied params hash. This works where the base class (Tag) is public-facing and operations are invoked through the base class. However, in this example, the public-facing class is the Topic subclass.

  • Operations need to be invoked through the subclass (Topic) but have its operations be based off a common Tag base class (a reverse builder ?).

Here is one way that this can be achieved through single inheritance (but it illustrates the shortcomings of this approach)...

Each type of tag is stored in its own database table and has ActiveRecord classes like this:

class Tag < ActiveRecord::Base 
  self.abstract_class = true
end
class Topic < Tag; end

The Trailblazer concept would follow a similar design - a Tag operation would provide the base functionality and be subclassed by more a specific operation (Topic). The Tag operation would not be used directly - a Topic controller, for example, would use the Topic operation.

The Topic operation inherits from Tag but must specify its own Topic model which seems to be only possible within each operation, requiring each to be subclassed explicitly:

class Topic < Tag 
  class Create < Tag::Create
    model Topic
  end 
  class Update < Tag::Update
    model Topic
  end 
  class Delete < Tag::Delete
    model Topic
  end 
end

A problem with this is that the contract, being defined on the base operation, thinks that it is a Tag rather than a Topic and this leads to issues where it's used as a model. An example showing where this is a problem is in a cell's view: the Topic concept has a cell that presents views to manipulate its objects. It renders forms using simple_form_for, like this:

simple_form_for operation.contract

This doesn't work as expected because the contract thinks it is a Tag and this breaks the form:

  • its parameters are sent as params[:tag] instead of params[:topic]
  • the submit button's label is Create Tag instead of Create Topic.

The cell can't use operation.model (which would otherwise work) because it won't see any form errors when rendering after a submited operation fails.

A way to solve this is to be explicit with simple_form_for:

simple_form_for operation.contract, as: :topic, url: topics_path ...

Another problem occurs when adding properties to Topic, because this requires extending the Tag contract. The usual way to do this is to add a contract do..end block to the Topic::Create operation. The problem occurs because such a block would not be seen by Topic::Update and Topic::Delete because they inherit from their Tag counterparts and not from Topic::Create.

An alternative would be for a subclassed Topic::Update operation to inherit from Topic::Create. This would remove the need to specify the model (because Topic::Create does it) but would mean that anything added by the Tag::Update operation would be lost:

class Update < Create
  action :update
end

The action needs to be respecified because Tag::Update isn't inherited but, because Topic::Create is inherited, properties added in Topic::Create are available in Topic::Update.

Both of these styles work as long as changes are only in one base class. It breaks whe there are changes in both because Ruby does not support multiple inheritance. Consider the Delete operation which usually looks like this:

class Delete < Create
  action :find
  def process(params)
    # validate params and then delete
  end
end

If that is Tag::Delete then Topic::Delete could be either

  class Delete < Tag::Delete
    model Topic
  end 

or

class Delete < Create
  action :find
end

In the former case Topic::Delete would be unaware of properties added by Topic::Create and, in the latter case, Topic::Delete would lacks the process method defned in Tag::Delete.

How can a Trailblazer concept inherit another and be able to extend its operations ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment