Template Method Pattern in UVM: Define the Flow Once, Override Only the Steps

The Composite post had one interface handle a single leaf and a whole subtree the same way. Now keep the inheritance but shift what varies: not the shape of the data, but the order of the work. Every test in your regression runs the same flow — configure, reset, ramp, drive traffic, drain, check — and every test hand-copies it, so each copy drifts a little. The pattern that fixes the flow in one place and lets each test fill in only the steps that differ is Template Method. The question it answers bites at 2 a.m.: how does a fix to your drain logic land in every test — including the three nobody remembered to update?

The Problem: Copy-Pasted Test Flows That Drift

Start with the run flow every test in your regression grows. A test configures the DUT, resets it, drives traffic, drains the bus, then checks the scoreboard — six or seven steps, in an order that matters. You wrote the first by hand; the second you copied and changed the middle. By the twentieth test, twenty run_phases carry the same skeleton, each hand-copied, each drifted a little from the original.

class smoke_test extends uvm_test;
  // ... constructor, registration ...
  task run_phase(uvm_phase phase);
    phase.raise_objection(this);
    cfg.n_outstanding = 4;            // configure
    reset_seq.start(env.vsqr);        // reset
    smoke_seq.start(env.vsqr);        // traffic
    #(DRAIN_NS * 1ns);                // drain — v1 of the drain logic
    check_scoreboard();               // final checks
    phase.drop_objection(this);
  endtask
endclass

class stress_test extends uvm_test;
  // ... constructor, registration ...
  task run_phase(uvm_phase phase);
    phase.raise_objection(this);
    cfg.n_outstanding = 16;           // configure — deeper queue
    reset_seq.start(env.vsqr);        // reset
    // ... same 20 lines again — ramp, then a long stress_seq ...
    check_scoreboard();               // final checks BEFORE drain
    wait (env.mon.outstanding == 0);  // drain — waits on outstanding count
    phase.drop_objection(this);
  endtask
endclass

These two tests are 80% the same source. They are also subtly, dangerously different — and the difference is invisible until it costs you a debug afternoon. Three problems compound the moment the regression grows past a handful:

  • A fix to the drain logic must be re-applied in every test — and misses three of them. A fixed delay (#(DRAIN_NS * 1ns)) is wrong: it drains too early under load. The correct drain waits on the outstanding count. You find this, fix it, and now you must hand-edit the drain line in all twenty run_phases. You patch seventeen. The three you miss — the ones nobody remembered — keep draining on a timer, and keep passing on a timer, until one fails in a nightly run at 2 a.m. with no obvious cause.
  • A new mandatory step means editing every test. Verification adds a rule: every test must run a register sanity check right after reset, before any traffic. There is no single place to add it. You open all twenty tests and insert the same post_reset_check() call by hand into each run_phase, hoping you put it in the same spot every time. Adding a required step forces an edit to every existing test — the Open/Closed principle inverted. The flow is closed for reuse and wide open for modification.
  • No structural guarantee of ordering invariants. Look again: smoke_test drains, then checks the scoreboard. stress_test checks the scoreboard, then drains. One is correct — you cannot check a scoreboard before traffic has finished draining out of the DUT — and one is a latent bug. Both compile. Both run. The compiler has no opinion on the order you call your own tasks in, so the ordering invariant lives nowhere but in the discipline of whoever copied the flow last. Discipline drifts; a wrong order rides in on the next copy-paste and sits there green until it isn't.

What you want is the flow defined once, in that fixed order, with each test filling in only the steps that differ. That's Template Method.

Gang of Four: The Template Method Pattern

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

"Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm's structure."

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

The key word is skeleton. One method in the base class fixes the algorithm's shape — the steps and the order they run in — and calls out to other methods for the parts that vary. Subclasses cannot touch the shape; they only fill in the steps the base class left open.

classDiagram
    class AbstractClass {
        +templateMethod()
        #primitiveOp1()*
        #primitiveOp2()*
        #hook()
    }
    class ConcreteClass {
        #primitiveOp1()
        #primitiveOp2()
    }
    AbstractClass <|-- ConcreteClass
    note for AbstractClass "templateMethod() {
primitiveOp1();
hook();
primitiveOp2();
}"

The two kinds of step the base class leaves open are not the same, and the difference matters. An abstract step — primitiveOp1(), primitiveOp2(), marked pure virtual (the * in the diagram) — has no body in the base class, so a subclass must implement it or the code will not compile. A hook — hook() — ships with a default body, often empty, so a subclass may override it but is free to inherit the default. UVM uses both, deliberately — and this abstract-versus-hook distinction is the one you'll reach for when you build your own skeleton later in the post.

This inverts who is in charge. In the copy-pasted flows of §1, your test held the control flow — it called configure, then reset, then traffic, in whatever order it pleased. Under Template Method the base class owns the control flow and calls down into your steps. That is the Hollywood Principle: "don't call us, we'll call you." The base class decides when primitiveOp1() fires and in what order — you decide only what it does.

That inversion — base class owns the flow, you supply the steps — is exactly why Template Method gets confused with Strategy. Both let behavior vary, but they vary different things by different means:

AspectTemplate MethodStrategy
MechanismInheritance — override stepsComposition — inject an object
What variesSteps inside a fixed algorithmThe entire algorithm
Binding timeCompile time (subclass choice)Runtime (swap the object)
Control flowBase class calls you (Hollywood)You call the strategy

Strategy earned its own post in this series. Rule of thumb: fixed flow, variable steps → Template Method. Variable flow behind a fixed interface → Strategy.

UVM Implementation: The Skeletons You've Been Filling In

You already know this part in your hands, even if you never named it. Every sequence, every component, every transaction you have written lives inside a Template Method. The skeleton in §2 is not an analogy for UVM — it is UVM, three times over. Start with the most literal sighting, the one you can print in seven lines.

When you call seq.start(sequencer), you are not running your own code first. You step into the middle of an algorithm uvm_sequence_base wrote, which calls down into your body() at the moment it chooses:

// What uvm_sequence_base::start() actually does — condensed:
task start(uvm_sequencer_base sequencer, ...);
  // ... arbitration, registration ...
  pre_start();             // hook  — default empty
  if (call_pre_post) pre_body();   // hook  — default empty
  body();                  // YOUR step — pure virtual in spirit
  if (call_pre_post) post_body();  // hook  — default empty
  post_start();            // hook  — default empty
  // ... cleanup ...
endtask

Read the comments down the right margin and the pattern names itself. The four pre_/post_ methods are hooks — they ship with empty bodies, so you override them only when you have setup or teardown to splice in. body() is the abstract step — the one method the algorithm cannot supply a default for, the one every sequence owes. Every sequence you've ever written was step 3 of someone else's algorithm. One caveat lives in the skeleton: the pre_body()/post_body() pair fires only when call_pre_post is set (the default), and is skipped when a parent sequence starts this one as a sub-sequence — so those two hooks are conditional, while pre_start()/post_start() always run.

The second sighting is the one you stopped seeing years ago, because it never looked like a method call: phasing. run_test() owns an algorithm too — build the component tree top-down, elaborate and connect it, then run every phase in its fixed order — and your components fill in the steps. Your build_phase(), connect_phase(), and run_phase() are each a step. Same pattern, far bigger skeleton: where start() fits in seven lines, the phasing algorithm sprawls across framework internals you never open. That is why it feels like "just how UVM works" rather than a pattern — the template method is buried so deep in the library that you only ever meet the hooks. You have been writing the steps of run_test()'s algorithm since your first testbench.

The third sighting is the smallest and most numerous. Every uvm_object carries a trio of fixed public algorithms — copy(), compare(), print() — each setting up its machinery and then calling down into a do_-prefixed hook you override:

function void copy(uvm_object rhs);  // fixed algorithm — you don't touch it
  // ... __m_uvm_field_automation, policy setup ...
  do_copy(rhs);                      // YOUR hook — the per-field copy
endfunction

You never call do_copy() yourself; copy() calls it for you, at the right time, with the policy already set up. Override do_compare()/do_print() the same way and the public algorithm wires your fragment into a flow you never manage.

Callbacks: The Same Idea via Composition

There is a fourth way the framework calls your fragment, swapping the mechanism without changing the intent. uvm_callbacks reaches the same place Template Method does — a framework-owned flow that pauses to run your code — but by composition instead of inheritance. Rather than override a method, you register a callback object, and the framework fires it at a published hook point:

class my_driver extends uvm_driver #(my_txn);
  `uvm_register_cb(my_driver, my_driver_cb)
  task drive_txn(my_txn t);
    `uvm_do_callbacks(my_driver, my_driver_cb, pre_drive(this, t))  // hook point
    // ... pin wiggling ...
  endtask
endclass

The trade-off is one sentence: callbacks compose across closed VIP and multiple independent parties — several teams can hang behavior off one pre_drive hook without touching the driver or each other — at the cost of implicit control flow you cannot read off the class hierarchy. The inheritance hazards that make the override-based variant fragile get their own treatment in §6.

Mapping the Gang of Four roles onto UVM:

GoF RoleUVM Class / Mechanism
AbstractClassuvm_sequence (start machinery), uvm_component (phasing), uvm_object (copy/compare/print)
templateMethod()start(), run_test()/phase traversal, copy()/compare()/print()
primitiveOperation() (abstract step)body()
hook() (optional step)pre_body()/post_body(), phase callbacks, do_copy()/do_compare()
ConcreteClassyour sequences, components, transactions

Notice that one GoF role maps to three different UVM base classes — uvm_sequence, uvm_component, uvm_object all play AbstractClass, each with its own template method. That is the tell: Template Method is not a corner of UVM you can opt out of, it is the load-bearing shape of the whole user-facing API. You learned it the first time you overrode body() and watched the framework call it without ever calling it yourself — you just called it "extending the base class."

Build Your Own: A Base Test Skeleton

§1 left you with twenty run_phases, each a hand-copied flow that drifted. §3 showed the answer already running in your testbench: uvm_sequence::start() fixes a five-step skeleton and calls down into your body(). Now build the same shape for the test flow itself — a base_test whose run_phase is the template method, fixing the order once so every test fills in only the steps that differ.

The Template Method: base_test::run_phase

The base class owns the flow. Its run_phase calls seven steps in a fixed order, and the order is the whole point — a subclass cannot reach in and reshuffle it. What a subclass can touch depends on which kind each step is: §2's abstract step and hook, plus a third the skeleton implies. Fixed steps are non-virtual — the invariants live in them. Abstract steps are pure virtual — every test owes you a body, or the code will not compile. Hooks are virtual with a sensible default — a test overrides them only when it has something to add.

Because a pure virtual task is legal only inside a virtual class, base_test stays abstract — correct anyway, since a base_test with no traffic is not a runnable test.

virtual class base_test extends uvm_test;     // virtual: it has a pure virtual step
  my_env env;
  // ... `uvm_component_utils, constructor, build_phase that creates env ...

  task run_phase(uvm_phase phase);            // THE template method — the fixed flow
    phase.raise_objection(this);
    configure_dut();                          // hook     — default implementation
    do_reset();                               // fixed    — not virtual, owns reset protocol
    post_reset_check();                       // hook     — default register sanity check
    ramp_up();                                // hook     — default empty
    main_traffic();                           // abstract — every test must define
    drain();                                  // fixed    — not virtual, owns the drain protocol
    final_checks();                           // hook     — default scoreboard drain check
    phase.drop_objection(this);
  endtask

  // ---- abstract step: pure virtual, no body — a subclass MUST implement it ----
  pure virtual task main_traffic();

  // ---- hooks: virtual with sensible defaults — a subclass MAY override ----
  virtual task configure_dut();
    `uvm_info("FLOW", "configure_dut (default)", UVM_HIGH)   // breadcrumb — see pitfalls
    env.cfg.n_outstanding = 4;                // sensible default the regression shares
  endtask

  virtual task post_reset_check();
    `uvm_info("FLOW", "post_reset_check (default)", UVM_HIGH)
    // ... read a known-reset register, assert its reset value ...
  endtask

  virtual task ramp_up();
    `uvm_info("FLOW", "ramp_up (default: empty)", UVM_HIGH)   // default empty — most tests need no ramp
  endtask

  virtual task final_checks();
    `uvm_info("FLOW", "final_checks (default)", UVM_HIGH)
    env.sb.check_drained();                   // default scoreboard drain check
  endtask

  // ---- fixed steps: NOT virtual — the invariants live here, no test overrides them ----
  task do_reset();                            // the ONE reset protocol
    env.reset_seq.start(env.vsqr);
  endtask

  task drain();                               // the ONE drain protocol — fixed in one place
    wait (env.mon.outstanding == 0);          // wait on outstanding, NOT a fixed #DRAIN_NS delay
  endtask
endclass

Read the right margin down and the three-way split is the design. main_traffic() is the abstract step: the base class has no sensible default for it, so it refuses to supply one — leave it out of a subclass and the compiler refuses the subclass. The four hooks ship with defaults the whole regression inherits. do_reset and drain are fixed: not virtual, so the reset and drain protocols exist in exactly one place and no test can fork them.

That single non-virtual drain() is §1's fix made structural. The drain that waits on the outstanding count — not the #(DRAIN_NS * 1ns) timer that drained too early under load — lives here, in one task. There is no drain line in any test to patch, so there are no three tests to miss.

The Concrete Tests: smoke_test and stress_test

A concrete test extends base_test and fills in only what differs. The minimum it owes is main_traffic() — the one abstract step — and a smoke test owes nothing else:

class smoke_test extends base_test;
  `uvm_component_utils(smoke_test)
  function new(string name, uvm_component parent); super.new(name, parent); endfunction

  // The ONLY thing a smoke test must supply: its traffic.
  task main_traffic();
    smoke_seq seq = smoke_seq::type_id::create("seq");
    seq.start(env.vsqr);
  endtask
endclass

smoke_test is its traffic and nothing more. It inherits the fixed reset, the fixed drain-on-outstanding, and the default post-reset and final checks — the entire flow, for free, in the order base_test fixed. A stress test needs two more seams: a deeper-queue configure_dut and a ramp_up that warms the DUT before pounding it — it overrides both hooks and supplies its traffic:

class stress_test extends base_test;
  `uvm_component_utils(stress_test)
  function new(string name, uvm_component parent); super.new(name, parent); endfunction

  // Override the deeper-queue config the stress run wants.
  task configure_dut();
    env.cfg.n_outstanding = 16;               // deeper queue than the default 4
  endtask

  // Override the ramp_up hook: warm the DUT with background noise first.
  task ramp_up();
    noise_seq nseq = noise_seq::type_id::create("nseq");
    nseq.start(env.vsqr);
  endtask

  // Supply the abstract step: the stress traffic itself.
  task main_traffic();
    stress_seq seq = stress_seq::type_id::create("seq");
    seq.start(env.vsqr);
  endtask
endclass

Now read §1's three pain points off these two classes. The drain fix lands in one place: both tests inherit the one non-virtual drain() — patch it once in base_test and every test, including the three nobody remembers, drains correctly the next morning. The new mandatory post-reset check appears in every test for free: it is a hook with a default body, called by run_phase between reset and ramp, so both tests run it without a single edit — the Open/Closed inversion of §1, righted. And the checks-before-drain bug is structurally impossible: run_phase calls drain() and then final_checks(), full stop. No subclass can reorder them, because the order lives in the non-virtual template method. The wrong order §1's stress_test rode in on can no longer be written.

Pitfalls

A few traps turn this clean skeleton back into the N divergent flows it replaced:

  • Making EVERYTHING virtual. It is tempting to mark every step virtual "for flexibility." Do it and there is no invariant left — a subclass can override do_reset and drain, and the template method melts back into the N hand-copied flows of §1, now wearing one base class as a disguise. Keep the steps that own invariants non-virtual; that non-virtuality is the guarantee.
  • Forgetting pure on must-implement steps. If main_traffic() ships with an empty virtual body instead of pure virtual, a test that forgets to define it still compiles and still "runs" — it resets, ramps, drains, and checks, sending no traffic at all, silently inheriting the empty default. It passes a green regression while testing nothing. pure virtual turns that silent no-op into a compile error.
  • SystemVerilog has no override keyword. Nothing checks that a method you meant as an override actually matches a base method's name. Type final_check() when the hook is final_checks() and you have not overridden anything — you have added a new method nobody calls, while the base default runs silently in its place. It compiles clean and your checks never fire. Mitigation: keep hook names few and grep-able, and breadcrumb each base hook with a `uvm_info at low verbosity — when the log shows final_checks (default) on a test you thought overrode it, the typo surfaces in the transcript instead of in triage.

Scaling Up: One Skeleton, a Whole Regression

§4 fixed the flow once and let two tests fill in only the steps that differ. The leverage shows up as the regression grows: every concern that must hold for all tests now has exactly one place to live. Drop it into the template method and it lands in every test — including the three nobody remembers — for free.

Two such concerns are worth wiring in straight away. A watchdog that fails the test if the whole flow runs long — one fork/join_none raised before the first step, so every test inherits a timeout without a line of its own. And per-step breadcrumbs — a `uvm_info at each step boundary, so every test emits the same structured logging transcript. Write that once in base_test and the whole regression speaks one log format.

The same single location is where ordering invariants stop being implied and start being checked. §4's non-virtual flow guarantees drain() runs before final_checks(), but it cannot guarantee a subclass's main_traffic() did not jump the gun before reset finished. Step guards — a precondition asserted at the boundary — close that gap. The breadcrumb says where you are; the guard says you had no business being there.

Fold all three into one helper pair and run_phase reads as a checked, logged ledger of the flow:

task run_phase(uvm_phase phase);
  phase.raise_objection(this);
  fork watchdog(test_timeout_ns); join_none           // one watchdog for EVERY test

  step_begin("configure");      configure_dut();      step_end("configure");
  step_begin("reset");          do_reset();           step_end("reset");
  guard(reset_done,  "reset must complete before traffic");   // ordering, CHECKED
  step_begin("post_reset");     post_reset_check();   step_end("post_reset");
  step_begin("ramp_up");        ramp_up();            step_end("ramp_up");
  step_begin("traffic");        main_traffic();       step_end("traffic");
  guard(bus_idle,    "bus must be idle before drain");        // ordering, CHECKED
  step_begin("drain");          drain();              step_end("drain");
  step_begin("final_checks");   final_checks();       step_end("final_checks");

  phase.drop_objection(this);
endtask

// breadcrumb pair — one place, every test logs the same way
protected task step_begin(string s); `uvm_info("FLOW", {"enter ", s}, UVM_MEDIUM) endtask
protected task step_end(string s);   `uvm_info("FLOW", {"leave ", s}, UVM_HIGH)   endtask
protected function void guard(bit ok, string why);
  if (!ok) `uvm_fatal("FLOW", {"precondition failed — ", why})
endfunction

The watchdog, the breadcrumbs, and the guards are all written once and inherited by every test. No leaf test carries a timeout, a log line, or an ordering assertion — the skeleton carries them, and a fix to any one lands everywhere.

The skeleton also stacks. A company-wide base_test fixes the universal flow; a block-level pcie_base_test extends base_test fixes more of it and exposes a narrower set of hooks to the leaf tests beneath it:

virtual class pcie_base_test extends base_test;
  task configure_dut();   // override the hook: PCIe-specific config
    env.cfg.set_link_speed(GEN4);
    // ... lane config, equalization presets ...
  endtask
  task ramp_up();         // override the hook: bring the link up before traffic
    link_up_seq lseq = link_up_seq::type_id::create("lseq");
    lseq.start(env.vsqr);
  endtask
endclass

Each layer fixes more of the flow and leaves a thinner seam below: a PCIe leaf test owes only main_traffic(). The discipline cost is that a change to pcie_base_test::ramp_up can ripple into tests it never names — the fragile base class that §6 takes head-on.

Advanced: The Fragile Base Class

The leverage of §5 has a signature failure mode, the one hazard every Template Method carries: the base class evolves, and subclasses break silently. No compile error — the signatures still match. No immediate failure — the test still runs. There is only changed behavior, riding into tests that never named the line that changed, surfacing weeks later as a flake nobody can trace to a base-class edit they did not make. The pattern that lets one fix land everywhere lets one mistake land everywhere too. This section walks the four forms that bite, each grounded in this post's own base_test and pcie_base_test.

An Inserted Step Changes the Ground Under a Subclass

Recall §4's flow: reset, then ramp. A subclass wrote its ramp_up() against that flow — its first line reads a register, safe in the assumption that nothing touches the bus between reset finishing and ramp starting. The assumption was true the day it was written, and never written down.

Now §1's new mandatory step lands. You add post_reset_check() to base_test's template method, between do_reset() and ramp_up(), and it reads a handful of known-reset registers. The base class is better for it — every test now sanity-checks reset for free. But that subclass's ramp_up() now starts the instant post_reset_check() returns, and its first register read races traffic it never knew existed: the base's own reset-check reads, possibly still draining out of the DUT. Nothing in the subclass changed; nothing failed to compile. The test passes nine runs in ten and fails the tenth, and the diff that caused it edited a file the subclass author has never opened.

That is the inserted-step hazard in one sentence: a fixed step added mid-flow rewrites the preconditions of every hook downstream — and the subclass that depended on the old preconditions has no way to know they moved.

The super Call Is a Per-Hook Contract, and SystemVerilog Will Not Enforce It

When a subclass overrides a hook, it faces a question with no default answer: does it call super or not? Get it wrong either way and behavior changes silently.

Some hooks require the super call. build_phase is the canonical one — it propagates config and resolves the factory, so a subclass that forgets super.build_phase(phase) silently skips that propagation, and a child component comes up unconfigured for reasons that point nowhere near the missing line. Others are designed not to be chained: body() has no base behavior you are meant to extend — calling super.body() is at best a no-op and at worst runs a parent sequence you did not intend.

Your own configure_dut shows why the rule must be documented, because it can go either way. The base sets env.cfg.n_outstanding = 4 — a default the whole regression shares. A subclass that overrides configure_dut to add one PCIe-specific knob and forgets super.configure_dut() silently drops the base's default: n_outstanding is now whatever the config object happens to initialize to, not the 4 every other test runs with. Yet §4's stress_test overrides the same hook to replace the default outright — deepening the queue to 16 — and correctly omits super, because chaining it would set 4 only to overwrite it. Same omitted call, opposite intent: one is a silent bug, the other exactly right. That is why the contract must say which one this hook expects.

The rule is the only defense, and it is a documentation rule because the language gives you nothing else: document, per hook, whether super is required, optional, or forbidden. SystemVerilog has no annotation that forces the call and no warning when you skip it — so the contract lives in a comment on the base hook, or in the tribal memory of whoever last debugged it.

Reordering Is an API Break With No Signature Change

The third hazard is the subtlest, because it changes nothing a tool can see. Suppose a later edit to base_test swaps two steps — ramp_up() now runs before post_reset_check(). No signature changed. No override broke. Every subclass still compiles and still runs every step it always did.

But the meaning of the flow changed. A subclass whose ramp_up() assumed reset had been sanity-checked first now ramps against an unchecked DUT. The call order was the contract — the whole point of §4 — and reordering it is a semantic API break wearing the disguise of a harmless refactor.

Treat the call order the way you treat a function signature: as a published contract. When it must change, version the flow — bump a documented flow version and announce the reorder the way you would announce a renamed argument, so the subclasses that depended on the old order find out at review time instead of at 2 a.m.

The Tension Named, and the Exit Sign

Step back and the four hazards share one root. The Open/Closed principle pulls you toward more hooks — every new seam is one more thing a subclass can vary without editing the base. A stable contract pulls the other way, toward fewer — every hook is a promise about call order and preconditions that you can break by accident. The two forces do not reconcile; you manage the tension with a handful of rules:

  • Hooks live at step edges, never mid-step. A hook fires between fixed steps, where the preconditions are stable — not partway through a step, where it can observe half-mutated state.
  • Fixed steps own all state mutation. Reset and drain are non-virtual for a reason: if the invariant-bearing work lives only in steps no subclass can touch, no override can corrupt it.
  • The call order is documented as a contract. Write the order and each hook's preconditions next to the template method, and version it when it changes — the reorder hazard is only silent when the order was never published.
  • When two teams both need the same hook point, that is the exit sign. Inheritance gives each subclass one override of a hook; the moment two independent parties both need to splice behavior into the same seam, you are past what a single override chain can express. That is where you reach for composition — the uvm_callbacks beat from §3.

Sixteen patterns. Sixteen 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, Composite treats a leaf and a whole subtree the same way, Observer broadcasts, Strategy swaps, Chain of Responsibility delegates until claimed, Command encapsulates — and Template Method fixes the flow while letting you override the steps. None replaces another.

Quick Reference

Everything in one place:

GoF Role → UVM Mapping

GoF RoleUVM's Template Method (uvm_sequence)Yours (base_test)
AbstractClassuvm_sequence (start machinery)base_test
templateMethod()start()run_phase()
abstract stepbody()main_traffic()
hookspre_body() / post_body(), pre_start() / post_start()configure_dut() / post_reset_check() / ramp_up() / final_checks()
ConcreteClassyour sequencessmoke_test / stress_test

base_test Skeleton API Cheatsheet

StepKindDefault behaviorsuper rule
configure_dut()hookSets shared defaults (n_outstanding = 4)optional — super keeps base defaults; omit only to replace them
do_reset()fixedThe ONE reset flown/a — not overridable
post_reset_check()hookRegister sanity checkoptional — super keeps the default check
ramp_up()hookEmptyoptional
main_traffic()abstractNone — must implementforbidden — nothing to chain
drain()fixedWait on outstanding countn/a — not overridable
final_checks()hookScoreboard drain checkoptional — super keeps the default check

Common Mistakes

MistakeFix
Every step virtual — no invariants leftFixed steps non-virtual; hooks only where variation is intended
Empty default inherited where traffic was requiredpure virtual for must-implement steps — fail at compile, not in triage
Typo'd hook name silently never called (no override in SV)Few, grep-able hook names; `uvm_info breadcrumb in every base hook
Subclass forgets super.build_phase()Document per hook: super required / optional / forbidden
Base class inserts or reorders steps, subclasses break silentlyTreat call order as a published contract; version the flow
Confusing Template Method with StrategyFixed flow + variable steps = Template Method; swap whole algorithm at runtime = Strategy

Previous: Composite Pattern — One interface for leaves and whole trees

Next: Coming soon

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

Comments (0)

Leave a Comment