interface Rule { isSatisfiedBy(ctx: Ctx): boolean }
class HasRole implements Rule { /* leaf */ }
class IsOwner implements Rule { /* leaf */ }
class AnyOf implements Rule {
constructor(private rules: Rule[]) {}
isSatisfiedBy(ctx: Ctx) { return this.rules.some(r => r.isSatisfiedBy(ctx)); }
}
const canEdit = new AnyOf([new HasRole("admin"), new IsOwner()]);
Composite lets you build a tree of objects. You can treat one object and a group of objects the same way, through one shared interface.
In the example:
HasRoleandIsOwnerare leaves. They do the actual check.AnyOfis a composite, and so is anAllOfnext to it. A composite holds child rules. It implementsisSatisfiedByby passing the call to its children.
The composite is itself a Rule. So composites can be nested inside each other: new AllOf([new IsActive(), new AnyOf([...])]). The guard calls canEdit.isSatisfiedBy(ctx). It doesn't know or care whether it holds one rule or a whole tree.
Other backend uses:
- nested menus or category trees
- pricing rules made of sub-rules
- validation of nested forms
The trade-off: one shared interface can be too general when leaves and composites need different operations.
- Composite builds a tree of objects behind one shared interface.
- Leaves do the work, and composites pass the call to their children.
- Clients treat one object and a group of objects the same way.
- Composites can be nested to any depth.
Read more