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
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")orrender_template_hooks("events.list.hero"), and your feature registers a callback withregister_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 intrywhen the other feature is optional, and the contract lists it underfeatures_optional. - Signals.
connect_signalreacts 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.modelsnames 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.routesnames 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.templatesreplaces a template of the base with your copy.overrides.servicesreplaces a service with a class merged from yours and the base’s.overrides.hookstakes 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:validateand by thefeature:refinewizard. - 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.
- Can it work through what
eventsoffers? Its ownEditionmodel, public archive and admin screens need nothing fromevents, so they are composition. So is what it shows on the events pages, onceeventsopens slots there. - Does it need the data of
events? Yes. An event has to know its edition, soeditionsextendsEventwithedition_idthroughEventEditionMixin. - Could
eventsopen slots instead of being overridden? Yes.eventsopens seven slots onevents/list.html,events/detail.html,events/admin/list.htmlandevents/admin/form.html, andeditionsfills them from itshooks.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
- Template hooks. Slots, fallback slots and hook replacement
- Refinement. Declaring and writing a refiner
- Extension points. What a base lets refiners touch
- Validation. The rules and where they run
feature:refine. The wizard that writes a refinement