Composite Pattern in UVM: One Interface for Leaves and Whole Trees

The Bridge post left you at the register layer, swapping access mechanisms under one read/write API. Now turn from a single register to the shape of the whole testbench: configuration that nests, hierarchies that grow, subtrees that need the same handling as the leaves inside them. You already live in one such tree every day — UVM's own component hierarchy, the biggest Composite there is, where uvm_root phases a leaf driver and a whole env the same way. That uniform handling is the Composite pattern: how does one validate() call check a single driver's config and an entire SoC's config tree without the caller knowing which it's holding?

The Problem: Config That Won't Scale

Start with the config object every testbench grows. A single env_config carries everything the environment needs: clock periods, agent enables, address maps, timeout budgets, error-injection knobs. It begins as a handful of fields and, over a project's life, swells to forty. Worse than its size is its shape — it is flat, and you thread it down the hierarchy by hand.

class env_config extends uvm_object;
  `uvm_object_utils(env_config)

  // Top-level knobs
  int unsigned clk_period_ns;
  bit          has_usb_agent;
  bit          has_pcie_agent;

  // USB sub-block fields
  bit          usb_is_active;
  int unsigned usb_n_outstanding;
  int unsigned usb_timeout_ns;

  // ... 30+ more fields, every sub-block flattened into one object ...
endclass

The threading is the part that hurts. The test sets the object at the top; every level below reaches in, carves off the slice it cares about, and hands a hand-built sub-config to the level under it:

// In the test
env_config cfg = env_config::type_id::create("cfg");
uvm_config_db #(env_config)::set(this, "env", "cfg", cfg);

// In the env's build_phase
uvm_config_db #(env_config)::get(this, "", "cfg", cfg);
usb_agent_cfg ucfg = usb_agent_cfg::type_id::create("ucfg");
ucfg.is_active     = cfg.usb_is_active;       // copy fields across by hand
ucfg.n_outstanding = cfg.usb_n_outstanding;
ucfg.timeout_ns    = cfg.usb_timeout_ns;
uvm_config_db #(usb_agent_cfg)::set(this, "usb_agent", "cfg", ucfg);

// In the agent's build_phase: get ucfg, repeat the carve-and-set for the driver...

This works, and it is how a great many testbenches look. It also creates three problems that compound the moment the testbench grows past one agent:

  • Adding a sub-block touches every level. Bring up a second PCIe agent and you do not just write the agent — you add its fields to env_config, a carve-and-set in the env, a get-and-carve in the agent, and wire the driver below. A new node forces edits at every level that threads config down: the Open/Closed principle inverted.
  • "Is this configuration legal?" has no single answer. Validating the whole setup means hand-writing a cascade of if checks at the top — clock sane, USB outstanding within range, PCIe lane count matching the address map. Because the config is flat, you cannot hand one agent's slice to someone and say "validate your subtree." There is no subtree, only the flat blob and a wall of conditionals.
  • Every hierarchy-visiting operation re-walks it by hand. Print the active configuration, validate it, sanity-check it before run_phase — each re-implements the same descent through the same fields, because nothing in the config knows how to visit itself. Three operations, three near-identical traversals, synced by discipline alone.

What you want is a config that is a tree. A single leaf field-set and a whole nested configuration should answer the same validate() and summarize() call, so the caller never branches on "is this one driver's config or the entire SoC's?" — it just calls, and the structure handles its recursion. That shape is the Composite pattern. The next sections pin down the Gang of Four version, then show you already use it in UVM's own component tree.

Gang of Four: The Composite Pattern

Take the Gang of Four version first, before any UVM, so the structure stands on its own.

"Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly."

— Design Patterns: Elements of Reusable Object-Oriented Software (Gamma et al., 1994)

The key word is uniformly. Composite hangs both the simple things and their containers off a single shared interface, so a caller holding it cannot tell — and never needs to ask — whether it points at one object or a whole subtree.

classDiagram
    class Component {
        <<interface>>
        +operation()*
        +add(Component)*
        +getChild(int)*
    }
    class Leaf {
        +operation()
    }
    class Composite {
        -children : Component[]
        +operation()
        +add(Component)
        +getChild(int)
    }
    Component <|.. Leaf
    Component <|.. Composite
    Composite o-- Component : contains

The participants:

  • Component — The common interface the client holds. It declares operation() (the real work) and the child-management methods. Both leaves and composites implement it, which is what makes them interchangeable.
  • Leaf — A node with no children. Its operation() is the actual unit of behavior, the bottom of the recursion; nothing below it to delegate to.
  • Composite — A node that holds children and implements operation() by iterating that collection and delegating to each child's operation(). It also implements add() and getChild() so the tree can be assembled and navigated. Its own work is mostly aggregation — combining what children report.
  • Client — Holds a Component reference and calls operation() on it. It never branches on leaf-vs-composite, never type-checks, never asks "how many children does this have?" first. It calls.

The whole pattern turns on one move: the recursive operation(). When the client calls operation() on a Composite, that composite walks its children and calls operation() on each — and any child may itself be a composite, which walks its children in turn. A single top-level call fans out across the entire subtree without the client writing a loop, a recursion, or a single if. The client treats a lone leaf and a thousand-node subtree identically — both are just a Component answering operation().

That uniform recursion through a shared base is why Composite gets confused with Decorator — both wrap a common base type and call through it recursively, so on a class diagram they look almost identical. Decorator has its own place in this series; pin the distinction down now:

AspectCompositeDecorator
Child countMany — represents a wholeExactly one — wraps a single target
IntentPart-whole hierarchy; uniform treatmentAdd behavior without changing interface
RecursionBranches over a list of childrenLinear chain of single wrappers
Mental model"Treat a leaf and a subtree the same""Stack more on top of one thing"

Rule of thumb: many children representing a whole → Composite; one child you are decorating → Decorator.

With the abstract shape settled, the next section shows you have been living inside exactly this structure all along: UVM's own component hierarchy.

UVM Implementation: The Component Tree

You already know this part in your hands, even if you never named it. Every testbench you have written is a Composite. The test instantiates an env, the env one or more agents and maybe a scoreboard, each agent a driver, monitor, and sequencer. That nesting — test contains env contains agent contains driver — is a part-whole hierarchy, and what makes it one is that every node in it is the same type: uvm_component.

uvm_component is the Component role from the Gang of Four diagram, made concrete: the shared base that both leaves and containers extend. A driver, a monitor, a sequencer, a scoreboard — these are Leaves: terminal nodes that do the real per-node work, nothing below them to delegate to. An agent, an env, a test — these are Composites: container nodes that hold children and exist mostly to aggregate them. The crucial Composite move — that leaves and composites are the same type to the client — is exactly what uvm_component gives you: there is no separate uvm_leaf_component and uvm_composite_component, just one base class a driver and an env both extend.

Watch what the phasing machinery does with that tree. As each phase runs, it invokes that phase's method — build_phase, connect_phase, run_phase, and the rest — on every node, identically whether the node is a leaf driver or a whole env subtree. The phaser never asks "is this a container or a terminal node?" before calling run_phase; it holds a uvm_component reference and it calls. The phase callbacks ARE the recursive operation() from the GoF diagram. When you override build_phase in your env to construct its children, and their build_phase runs to construct their children, that is operation() fanning out across a subtree — each composite delegating to children that may themselves be composites. You wrote the recursion the moment you nested a component; you called it phasing.

The child-management half is there too, where Composite says it should be: in the base class uvm_component. Every component — leaf or container — carries the API to navigate its children:

  • get_child(name) — fetch one child by instance name
  • get_children(q) — populate a queue with all direct children
  • get_num_children() — how many children this node has
  • get_first_child(name) / get_next_child(name) — iterate them one at a time

Putting these on the base class — not only the containers — is a deliberate choice we'll revisit at transparency versus safety. The consequence now: because the navigation API is uniform, a traversal written against uvm_component works on any node. Here is §2's recursive operation(), made concrete and runnable:

function void print_tree(uvm_component c, string pad = "");
  uvm_component children[$];
  $display("%s%s (%s)", pad, c.get_name(), c.get_type_name());
  c.get_children(children);
  foreach (children[i])
    print_tree(children[i], {pad, "  "});   // same call, leaf or subtree
endfunction

Look at what is not in that function. There is no if (is_composite), no type check, no branch on driver-versus-env, no special case for the bottom of the tree. A leaf simply has an empty children queue, so the foreach body never runs and the recursion stops on its own. Hand print_tree the uvm_root singleton and it dumps your entire testbench; hand it one agent and just that subtree; hand it a lone driver and a single line. Same call, any node. That is the Composite payoff in five lines you could paste into a debug task this afternoon.

Mapping the Gang of Four roles onto UVM:

GoF RoleUVM Class / Mechanism
Component (abstract)uvm_component
Leafterminal components (driver, monitor, sequencer, scoreboard)
Compositecontainer components (agent, env, test) — same base class
children collectioninternal m_children, exposed via get_children()
operation()the phase callbacks (build_phase, run_phase, …)
client walking the treeuvm_root / uvm_phase traversal

Notice the most telling row: Component and Composite both map to uvm_component. UVM merged the two roles into one base class — the transparent variant, which we'll pull apart later. The bigger point: you did not learn Composite from this post. You learned it the first time you nested an agent inside an env and watched build_phase cascade down. Every UVM user has used Composite — called it "the component hierarchy."

Build Your Own: A Self-Validating Config Tree

§1 left you with a flat env_config — forty fields, no subtree to ask "is this legal?" §3 showed the answer already running in your testbench: uvm_component is a Composite, and one print_tree walks a leaf or the whole tree with no branching. Now build the same shape for config — a parallel hierarchy that nests itself, so a single validate() checks one driver's fields or an entire SoC's configuration through the same call. You have seen UVM do this; here you do it yourself, with nothing but uvm_object.

The Component: cfg_node

The base class is the shared interface every config node answers — the Component role made concrete. It declares two operations as pure virtual, so leaves and groups each define them:

virtual class cfg_node extends uvm_object;
  string node_name;

  function new(string name = "cfg_node");
    super.new(name);
    node_name = name;
  endfunction

  // Returns 1 if this node (and, for a group, its subtree) is valid.
  // Appends one "path: message" string to errors[$] for each failure.
  pure virtual function bit validate(ref string errors[$], string path = "");

  // Pretty-prints this node (and, for a group, its subtree) at the given depth.
  pure virtual function void summarize(int indent = 0);
endclass

Two design notes carry the section. First, validate() returns ok/not-ok and accumulates strings keyed by the node's path — so a failure deep in the hierarchy reports env.usb.driver: n_outstanding=99 out of range [1..16], not a bare boolean. Second, both methods are §2's recursive operation() made concrete: a leaf does the per-node work, a group folds its children. The caller never learns which it is holding.

The Leaf: driver_cfg

A leaf is a terminal node with real fields and no children. Its validate() checks only its own invariants and appends a path-qualified error per failure; summarize() prints one line. To indent safely we build the pad by concatenation in a loop — replicating a two-space string indent times is a zero-replication trap when indent is 0:

class driver_cfg extends cfg_node;
  `uvm_object_utils(driver_cfg)

  int unsigned n_outstanding;
  int unsigned timeout_ns;
  bit          is_active;

  function new(string name = "driver_cfg");
    super.new(name);
  endfunction

  function bit validate(ref string errors[$], string path = "");
    string here = (path == "") ? node_name : {path, ".", node_name};
    bit ok = 1;
    if (n_outstanding < 1 || n_outstanding > 16) begin
      errors.push_back($sformatf("%s: n_outstanding=%0d out of range [1..16]",
                                 here, n_outstanding));
      ok = 0;
    end
    if (timeout_ns == 0) begin
      errors.push_back($sformatf("%s: timeout_ns must be > 0", here));
      ok = 0;
    end
    return ok;
  endfunction

  function void summarize(int indent = 0);
    string pad = "";
    for (int i = 0; i < indent; i++) pad = {pad, "  "};
    $display("%s%s [outstanding=%0d timeout=%0dns active=%0b]",
             pad, node_name, n_outstanding, timeout_ns, is_active);
  endfunction
endclass

No recursion, no child handling — a leaf is the bottom of the tree, answering the same two calls a group does.

The Composite: cfg_group

A group holds children and exists to aggregate them. It adds one assembly method, add_child(), and folds the two operations over children:

class cfg_group extends cfg_node;
  `uvm_object_utils(cfg_group)

  cfg_node children[$];

  function new(string name = "cfg_group");
    super.new(name);
  endfunction

  function void add_child(cfg_node c);
    children.push_back(c);
  endfunction

  // Optional: this group's own cross-field rules, before recursing.
  protected function bit check_own_invariants(ref string errors[$], string here);
    return 1;  // override in a refined group to add rules
  endfunction

  function bit validate(ref string errors[$], string path = "");
    string here = (path == "") ? node_name : {path, ".", node_name};
    bit ok = check_own_invariants(errors, here);   // this node's rules
    foreach (children[i])
      ok &= children[i].validate(errors, here);     // self AND every child
    return ok;
  endfunction

  function void summarize(int indent = 0);
    string pad = "";
    for (int i = 0; i < indent; i++) pad = {pad, "  "};
    $display("%s%s/", pad, node_name);
    foreach (children[i])
      children[i].summarize(indent + 1);            // same call, deeper indent
  endfunction
endclass

The two lines that matter are inside validate(). ok starts as this group's own verdict, then ok &= children[i].validate(...) folds in every child's result — and any child may itself be a cfg_group, recursing into its children. A single top-level call fans out across the subtree, no caller-written if. The summarize() half reuses §3's accumulating-pad idiom: print this node, then recurse one indent deeper.

Assembling and Walking the Tree

Now build a small tree — an env group holding a usb agent group holding a driver leaf — and validate it. The payoff is the last two calls:

// Assemble the tree
driver_cfg drv = driver_cfg::type_id::create("driver");
drv.n_outstanding = 99;          // out of range on purpose
drv.timeout_ns    = 500;
drv.is_active     = 1;

cfg_group usb = cfg_group::type_id::create("usb");
usb.add_child(drv);

cfg_group env = cfg_group::type_id::create("env");
env.add_child(usb);

// One call validates the WHOLE tree...
string errs[$];
if (!env.validate(errs))
  foreach (errs[i]) `uvm_error("CFG", errs[i])

// the SAME call on a subtree validates just that subtree.
string sub_errs[$];
void'(usb.validate(sub_errs));

env.validate(errs) walks env, the usb agent, and the driver leaf; usb.validate(sub_errs) walks just the usb subtree. Same call, any node — the caller never branches on "is this the whole config or one agent's slice?", what §1 could not do with the flat blob. Because each error is keyed by its tree path, the report names the culprit precisely:

UVM_ERROR ... [CFG] env.usb.driver: n_outstanding=99 out of range [1..16]

Hand one team their agent's cfg_group and they validate their subtree in isolation, with the same validate() the top-level test calls on the root. New sub-block, no edits to existing nodes — build a node, add_child it, and the recursion already visits it.

Pitfalls

A few traps turn this clean recursion into a debugging afternoon:

  • Child-management on the leaf, left unimplemented. If you push add_child() up onto cfg_node so every node "looks uniform," driver_cfg inherits a method with no children to manage — call it and you get a runtime surprise, not a compile error. That is the transparency-versus-safety question §6 pulls apart; for now, keep add_child() on cfg_group only.
  • Mutating a node mid-traversal. Adding children to a group while a foreach (children[i]) walks it gives undefined or duplicated visits — the loop may skip the new node, revisit one, or walk a half-built subtree. Finish assembling the tree before validating or summarizing it.
  • A validate() that forgets to fold. If a group checks only its own invariants and returns that — dropping the ok &= children[i].validate(...) line — a broken leaf sails through while the subtree reports OK. The composite's verdict is meaningless unless it combines every child's result with its own. The fold is not an optimization; it is the pattern.

Scaling Up: Navigating the Tree Generically

§4 gave you a tree that validates itself, but validation is a push — you call the root and the structure fans out. At scale you also need to reach into the tree: configure one node by name, override a field deep in the hierarchy, or collect every leaf for a sweep — without knowing the concrete types or hand-walking the children queue. Three small methods turn the self-validating tree into a navigable one.

Reaching One Node: get_child and get_by_path

First, fetch a direct child by name. §4's cfg_group exposed children but had no lookup; add one method:

function cfg_node get_child(string name);
  foreach (children[i])
    if (children[i].node_name == name)
      return children[i];
  return null;
endfunction

That handles one level. To address a node several levels down — "usb.driver" under env — add a second method that splits the dotted path and descends child by child, using UVM's own uvm_split_string rather than rolling a tokenizer:

function cfg_node get_by_path(string dotted);
  string parts[$];
  cfg_node cur = this;
  byte sep = ".";                           // a real byte (8'h2E), not a string
  uvm_split_string(dotted, sep, parts);
  foreach (parts[i]) begin
    cfg_group grp;
    if (!$cast(grp, cur)) return null;        // hit a leaf mid-path
    cur = grp.get_child(parts[i]);
    if (cur == null) return null;             // no such child
  end
  return cur;
endfunction

The $cast guard is the load-bearing line. Descending one more level requires the current node to be a cfg_group — only a group has children. If the path tries to descend through a leaf ("usb.driver.foo" where driver is a driver_cfg), the cast fails and the lookup returns null rather than crashing. A missing name returns null too — two clean exits, no half-walked state.

One note on uvm_split_string: its separator argument is a byte, not a string. Pass an actual byte — byte sep = "." — not the bare literal ".". A one-character literal happens to be an 8-bit value, but relying on that conversion is simulator-dependent, and a longer literal truncates silently.

get_by_path is relative to the group it is called on, so on env, "usb.driver" reaches the driver leaf:

cfg_node n = env.get_by_path("usb.driver");
driver_cfg d;
if (n != null && $cast(d, n))
  d.n_outstanding = 8;          // tweak one field, no hand-walking

The caller named the node and got it — never touching a children queue or branching on node type until it cast the result to the leaf it meant to edit.

Operating on Every Node: collect_leaves

Path lookup reaches one node. The other half runs a uniform operation over many — count the leaves, collect them for a sweep, print them all — without knowing concrete types. The simplest form is a recursive collect_leaves: a leaf pushes itself, a group recurses. Add it to the base cfg_node as a virtual (not pure virtual, so existing nodes need no change):

// In cfg_node (base): default is "I am a leaf, push myself."
virtual function void collect_leaves(ref cfg_node out[$]);
  out.push_back(this);
endfunction
// In cfg_group (override): a group is not a leaf; recurse into children.
function void collect_leaves(ref cfg_node out[$]);
  foreach (children[i])
    children[i].collect_leaves(out);          // same call, leaf or subtree
endfunction

driver_cfg needs no override — it inherits the base definition that pushes itself. The group folds the call down to its children. Usage is the payoff:

cfg_node leaves[$];
env.collect_leaves(leaves);
`uvm_info("CFG", $sformatf("env has %0d leaf nodes", leaves.size()), UVM_LOW)
foreach (leaves[i])
  leaves[i].summarize();                       // uniform op over every leaf

One method, any subtree. Call it on env and you get every leaf in the SoC; on usb, just that agent's leaves. There is no if (is_leaf) at the call site — the branch lives once, in which class overrides the method.

Declare It, Don't Hand-Code It

This is the same move as the registries in earlier posts — the strategy table mapping a name to a handler, the chain that delegates until one link claims the request. You describe the node you want — by path, or "every leaf" — and the structure handles the how, the traversal written once.

Notice one deliberate asymmetry: get_child and add_child live on cfg_group only, while collect_leaves lives on the base — navigation that assumes children on the composite, a tree-walk that every node answers. That was a safety choice, and §6 examines the transparency-versus-safety trade-off head-on, with the recursion hazards a navigable tree quietly introduces.

Advanced: Transparency, Safety, and Recursion Hazards

Two questions decide how a Composite feels to use, and neither has a clean answer. The first is the pattern's signature tension — where the child-management API lives. The second is the family of hazards that comes free with any structure built from references pointing at each other. This section settles the §3 promise, then walks the three traps every tree-walk quietly carries.

Transparency versus Safety

The Gang of Four pose this as an explicit fork. The child-management methods — add_child, get_child, and friends — have to live somewhere, and you get two choices:

  • Transparent. Put child-management on the base Component, so every node — leaf or composite — exposes the identical interface. Clients treat them perfectly uniformly: anything you can ask a subtree, you can ask a leaf, no cast in sight. The leaf pays the cost: it must implement add_child() as a no-op or a runtime error, since it has no children to manage. Asking a leaf to add a child is legal at compile time and fails only at run time. You trade type safety for uniformity.
  • Safe. Put child-management on the composite only. A leaf has no add_child() to call, so the compiler stops you before the simulation starts — the type system enforces the tree shape. The cost lands on the client, which must $cast to the composite type before composing, reintroducing the type check the transparent variant erased.

GoF are blunt that this is a genuine trade-off with no universally right answer. Uniformity for the client or safety from the compiler — pick per use case and live with the other side's cost.

How This Post Chose Both

The two trees sit on opposite sides of that fork — the §3 promise, paid off.

uvm_component chose transparency. get_child and get_num_children live on the base class, so a leaf driver answers get_num_children() — returning 0 — as readily as an env does. The benefit is §3's print_tree: zero casting, leaf or whole subtree. The cost is the price tag — get_child("foo") on a driver is legal and simply returns null, a question that arguably should never have compiled.

Our cfg_group chose safety. add_child lives only on the group, so driver_cfg has none — the compiler refuses to compose a child onto a leaf. The price is the one §5 already paid: get_by_path must $cast its current node to cfg_group before descending, because only a group is known to have children.

Same pattern, opposite call: UVM optimizes for uniform traversal of a tree clients walk constantly; the config tree optimizes for a compiler that refuses to build a malformed hierarchy.

Recursion Hazards

A navigable tree is just a graph of handles, and three failure modes ride along the moment it stops being a strict tree — each with a one-line fix.

Shared leaf nodes. Nothing stops the same cfg_node handle from being add_child'd into two parents — SystemVerilog handles are references, so both groups now point at one object. A read-only walk (validate, summarize, collect_leaves) simply visits it twice, usually harmless: a duplicate line, not a wrong answer. But a state-mutating walk — an apply that increments a counter or flips a field — runs against that shared node twice and corrupts it. Fix: keep tree-walks read-only, or enforce single-parent ownership.

Cycles. If a group contains, directly or transitively, an ancestor of itself, the recursive validate() never bottoms out — it descends until the stack overflows. The defense is a build-time guard that walks children while remembering every node it has seen, reporting failure the instant it revisits one:

function bit validate_acyclic(cfg_node n, ref bit visited[cfg_node]);
  cfg_group g;
  if (visited.exists(n)) return 0;     // already on this path -> cycle
  visited[n] = 1;
  if ($cast(g, n))                     // only groups have children
    foreach (g.children[i])
      if (!validate_acyclic(g.children[i], visited)) return 0;
  visited.delete(n);                   // off this path; allow legit re-share
  return 1;                            // no cycle through this node
endfunction

The associative array keyed by handle is the whole trick: visited.exists(n) is true only when the same object is reached twice on one descent — precisely a cycle. Deleting n on the way back up keeps a legitimately shared leaf from being misread as a cycle. Run it once after assembly, before the first real walk.

Depth. Even an acyclic tree recurses as deeply as it is tall, each level a stack frame. A config tree is rarely more than a handful deep, so this is academic here — but a pathologically deep structure can exhaust the call stack on validate() alone. Fix: when depth is genuinely unbounded, swap the recursion for an explicit work-queue — an iterative BFS or DFS that pushes children onto a queue you own, not the call stack.

The Pattern in the Series

Composite is the fifteenth pattern in this series, earning its place the way every other did — by owning a single concern no other pattern covers.

Fifteen patterns. Fifteen orthogonal concerns. Factory builds, Abstract Factory keeps families consistent, Builder assembles, Prototype clones, Singleton guards the one instance, Adapter translates, Bridge decouples, Decorator adds, Facade simplifies, Proxy mediates, Observer broadcasts, Strategy swaps, Chain of Responsibility delegates until claimed, Command encapsulates — and Composite treats a leaf and a whole subtree the same way. None replaces another.

Quick Reference

Everything in one place:

GoF Role → UVM Mapping

GoF RoleUVM Component TreeYour cfg_node Tree
Component (base)uvm_componentcfg_node
Leafdriver / monitor / sequencerdriver_cfg
Compositeagent / env / testcfg_group
operation()phase callbacks (build_phase, run_phase)validate() / summarize()
child accessget_child / get_children (on base — transparent)get_child / children (on group — safe)
walk the treeuvm_root phasingvalidate() / collect_leaves() recursion

cfg_node API Cheatsheet

MethodDefined InPurpose
validate(ref string errors[$], string path="")cfg_node (pure virtual)Returns 1 if valid; appends path: message per failure. A group folds its children.
summarize(int indent=0)cfg_node (pure virtual)Pretty-print this node (and, for a group, its subtree).
collect_leaves(ref cfg_node out[$])cfg_node (virtual)Append every leaf in this subtree; base pushes self, group recurses.
add_child(cfg_node c)cfg_groupAppend a child node (groups only — the safe variant).
get_child(string name)cfg_groupDirect child by node_name, or null.
get_by_path(string dotted)cfg_groupDescend a dotted path (e.g. "usb.driver"), or null on a miss / leaf mid-path.

Common Mistakes

MistakeFix
Child-management API on the leaf, unimplementedDecide transparency vs safety deliberately; if transparent, a leaf returns empty/zero and never errors
A composite's validate() ignores its children's resultsFold every child: ok &= children[i].validate(...), not just the node's own invariants
A shared leaf mutated by a tree-wide walkKeep tree-walks read-only, or guarantee single-parent ownership
A cycle in the tree (a node contains an ancestor)Validate at build time — walk children tracking visited nodes, fail on a repeat
Branching on leaf-vs-composite in client codeIf you write if (is_composite) everywhere, the abstraction failed — push the behavior into cfg_node
Confusing Composite with DecoratorMany children forming a whole = Composite; one wrapped child adding behavior = Decorator

Previous: Bridge Pattern — One register operation, two access paths

Next: Coming soon

Author
Milan Kubavat
Sharing knowledge about silicon verification, hardware design, and engineering insights.

Comments (0)

Leave a Comment