Refine, extend or compose

There are three ways for your feature to change what another feature shows or does. Compose works through what the other feature already offers, extend adds data or URLs to it, and refine replaces part of it. Pick the first that works, because each step down the list ties your feature closer to the other and leaves less room for a third feature to do the same.

Table of contents

The rule

Compose first, extend when you need the base's data, refine last Three questions in order. If the feature can work through the base's slots, services and signals, compose. If it needs a column on the base's model or a route on its blueprint, extend. If the base could open a slot instead, open it and compose. Only otherwise refine, replacing a template, service or hook. Your feature has to change another feature Can it work through what the base offers? its hook slots, its services, its signals Yes no refinement Compose Fills the base's slots, calls its services. Any number can coexist. No Does it need the base's own data or URLs? a column on its model, a route on its blueprint Yes refines + extends Extend Adds a mixin or routes to the base. Names must not clash. No Could the base open a slot for it instead? a hook slot or a setting, as events does for editions Yes Open the slot in the base then compose No refines + overrides Refine Replaces a template, service or hook of the base. One refiner per target, so keep it rare.
Ask the three questions in order and stop at the first yes. A feature may land on more than one box, as editions composes and extends at once.

The three outcomes differ in what they ask of the base and in how many features can do the same thing at once.

Outcome Declared in What it may do Several features at once
Compose Nothing beyond the UVL dependency (b => a) Fill the base’s hook slots, call its services with service_proxy, listen to its signals, link to its routes Yes, without limit
Extend [tool.splent.refinement] with refines and extends Add columns, relationships and methods to a model of the base, add routes to its blueprint Yes, as long as no two of them add the same attribute or URL
Refine [tool.splent.refinement] with refines and overrides Replace a template, a service or the base’s callbacks in a hook slot No, one refiner per target

Extend and refine both make your feature a refiner of the base. A refiner loads after its base, its declaration is checked against the base’s extension points, and a refiner cannot be refined in turn. Compose leaves your feature an ordinary feature.


Compose

Composition is the default. Your feature has its own models, routes and templates, and reaches the other feature only through what that feature already publishes.

  • Hook slots. The base renders get_template_hooks("events.detail.before_meta") or render_template_hooks("events.list.hero"), and your feature registers a callback with register_template_hook. Hooks are additive, so any number of features can fill one slot. See Template hooks.
  • Services. service_proxy("EventsService") gives you the base’s service, refined or not. Wrap the call in try when the other feature is optional, and the contract lists it under features_optional.
  • Signals. connect_signal reacts to what the base emits without the base knowing who listens.

Composition needs no refinement section and cannot collide with another feature that composes the same base. events filling home.section on the public homepage is composition, and so is everything editions adds to the events pages.


Extend

Extend when your feature needs the base’s own records to carry something new, or the base’s blueprint to answer a new URL.

  • extends.models names a model of the base and a mixin from your feature. The framework injects the mixin’s columns, relationships and methods into the model, and your feature owns the migration of those columns. See Refinement.
  • extends.routes names a module of your feature that adds routes to the base’s blueprint.

Extensions are additive, so several refiners can extend one base. What they add must not clash.

  • Two refiners may not add an attribute of the same name to the same model, nor two columns that map to the same table column.
  • A refiner may not reuse the name of something the base model defines. The only exception is a method (or property, classmethod or staticmethod) marked with @typing.override, meant to replace the base’s method. A column or relationship of the base can never be replaced.
  • A route may not repeat a URL the base or another refiner already serves for the same HTTP method.

editions extends Event with a nullable edition_id, and edition_staff extends the same model with created_by. Both coexist because the names differ.


Refine

Refine only when something the base does has to be replaced, and the base cannot offer a slot or a setting for the change.

  • overrides.templates replaces a template of the base with your copy.
  • overrides.services replaces a service with a class merged from yours and the base’s.
  • overrides.hooks takes the base’s own callbacks out of a slot and puts yours in their place. Callbacks other features put there stay.

An override is exclusive. A template path or a service name has one replacement for the whole product, and a hook slot has one replacement among the refiners of the same base. Two refiners claiming the same target collide. Keep the override in one of them, or tell the SPL that they never go together with a UVL constraint such as !(editions & seasons).

A template override also costs upkeep. Your copy must follow every change the base makes to its template, and it hides every slot the base opens there. That is why the third question asks whether the base could open a slot instead. A slot is a small change to the base that every later feature can use, while an override serves only yours.


Keep refinement shallow

  • One level. A refiner refines a base that refines nothing. If you need to change a refiner, refine its base instead, or move the change into the refiner itself. Chains of refiners are refused at boot, by product:validate and by the feature:refine wizard.
  • Few targets. Declare only what you replace. Every declared override is a claim that no other refiner of the product can make.
  • Additive first. A mixin column, a route or a filled slot leaves room for the next feature. A replaced template, service or hook does not.

How SPLENT keeps it safe

The same rules run in feature:refine while you configure a refiner, in product:validate before boot, and at boot, from the shared module splent_framework.refinement.conflicts. A collision makes the wizard refuse the target, fails validation, and stops the app from starting. Validation lists the rules and their messages.


Worked example of events and editions

editions turns a recurring event into editions, each with its own programme. Asked in order, the three questions sort out what it does.

  1. Can it work through what events offers? Its own Edition model, public archive and admin screens need nothing from events, so they are composition. So is what it shows on the events pages, once events opens slots there.
  2. Does it need the data of events? Yes. An event has to know its edition, so editions extends Event with edition_id through EventEditionMixin.
  3. Could events open slots instead of being overridden? Yes. events opens seven slots on events/list.html, events/detail.html, events/admin/list.html and events/admin/form.html, and editions fills them from its hooks.py.
Page of events Slot What editions puts there
events/list.html events.list.title, events.list.hero, events.list.content With a current edition, the page title, the hero and the programme day by day. Without one it returns nothing and events renders its own page
events/detail.html events.detail.before_meta (called with the event) The edition the event belongs to
events/admin/list.html events.admin.list.actions Buttons to the editions screens
events/admin/list.html events.admin.list.event_meta (called with each event) The edition of each event as a badge
events/admin/form.html events.admin.form.fields (called with the event, None on create) The edition selector

Its refinement section holds only the extension.

[tool.splent.refinement]
refines = "splent_feature_events"

[tool.splent.refinement.extends]
models = [{ target = "Event", mixin = "EventEditionMixin" }]

editions claims no template of events, so another feature can add to the same pages through the same slots, and a change to the events templates needs nothing from editions.


See also


Back to top

splent. Distributed by an LGPL license v3. Contact us: drorganvidez@us.es