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 ?