Part 2 · OOP for Verification · Intermediate

The Factory Pattern from Scratch

A creator registry in an associative array, create-by-name, type overrides, and how this is exactly the UVM factory.

The problem: new() hard-codes the type

Everywhere your environment writes txn t = new();, the concrete type is frozen at compile time . Now you want one test to run the same environment but with an error-injecting transaction subclass. Without a factory your options are bad: edit the generator (touching verified code), or copy-paste a parallel environment. The factory pattern fixes this by routing every construction through a level of indirection : components ask the factory for "a transaction", and the test — before the environment is built — tells the factory which concrete class that request should produce.

diagram
FACTORY INDIRECTION

  WITHOUT factory                    WITH factory
  ───────────────                    ────────────
  generator:                         generator:
    txn t = new();                     txn t;
         │                             $cast(t, factory::create("txn"));
         ▼                                  │
    always bus_txn                          ▼
                                   registry lookup: "txn"  ?
  to change type:                       │ default        │ after override
  EDIT the generator                    ▼                ▼
                                     bus_txn          err_txn
                                   to change type:
                                   test calls factory::override
                                   ("txn", "err_txn") — generator untouched

Building it: registry, creators, create-by-name

The machinery is three pieces. (1) A creator class per registered type whose only job is to construct one instance — a virtual build() on an abstract base gives every type a uniform construction API. (2) A registry : a static associative array mapping string names to creator objects. (3) An override table consulted before each lookup, mapping a requested name to a substitute name. Registration happens via a static-initializer trick so each class registers itself at elaboration with no central list to maintain.

systemverilog
// ---- (1) uniform construction API ----
virtual class creator_base;
  pure virtual function bus_txn build();
endclass

class creator #(type T = bus_txn) extends creator_base;
  virtual function bus_txn build();
    T obj = new();
    return obj;                    // returned as base handle
  endfunction
endclass

// ---- (2)+(3) registry and override table ----
class txn_factory;
  static creator_base registry [string];   // "name" → creator
  static string       override [string];   // requested → substitute

  static function void register(string name, creator_base c);
    registry[name] = c;
  endfunction

  static function void set_override(string from, string to);
    override[from] = to;
  endfunction

  static function bus_txn create(string name);
    string actual = override.exists(name) ? override[name] : name;
    if (!registry.exists(actual))
      $fatal(1, "factory: type '%s' not registered", actual);
    return registry[actual].build();
  endfunction
endclass

// ---- self-registration via static initializer ----
class bus_txn_reg;
  static bit done = register();
  static function bit register();
    creator #(bus_txn) c = new();
    txn_factory::register("bus_txn", c);
    return 1;
  endfunction
endclass

Test-time substitution: the error-injection example

systemverilog
class err_txn extends bus_txn;
  rand bit corrupt;
  constraint c_corrupt { corrupt dist {1 := 1, 0 := 4}; }
  virtual function string convert2string();
    return {super.convert2string(), corrupt ? " [CORRUPT]" : ""};
  endfunction
endclass

class err_txn_reg;              // register the subclass too
  static bit done = register();
  static function bit register();
    creator #(err_txn) c = new();
    txn_factory::register("err_txn", c);
    return 1;
  endfunction
endclass

// ---- the generator never changes ----
class generator;
  task run(mailbox #(bus_txn) mb, int n);
    repeat (n) begin
      bus_txn t = txn_factory::create("bus_txn");  // indirection point
      assert (t.randomize());
      mb.put(t);
    end
  endtask
endclass

// ---- the error test flips ONE switch before anything is built ----
module test_error_injection;
  initial begin
    txn_factory::set_override("bus_txn", "err_txn");  // FIRST
    // ...build env, run... every "bus_txn" create now yields err_txn,
    // and virtual methods (convert2string, etc.) dispatch to err_txn.
  end
endmodule

This IS the UVM factory

Map the pieces and UVM's machinery demystifies: registry[string] is uvm_factory's internal type table; the creator class is uvm_object_registry #(T) which the `uvm_object_utils macro instantiates (that macro is the self-registration trick); create("name") is type_id::create(); and set_override is set_type_override_by_type. UVM adds instance-path-specific overrides and component creation, but the spine is byte-for-byte this pattern.

Interview angle

  • "Implement a factory without UVM" — a top-three senior interview question; the registry + creator + override trio above is the expected answer.

  • "Why must the override be set before build?" — objects already constructed with the old type are not retroactively replaced.

  • "Why does substitution work through base handles?" — virtual methods; the factory returns derived objects that dispatch correctly.

Key takeaways

  • A factory is one level of indirection: ask by name, receive whatever the override table says.

  • Three pieces: parameterized creator class, static registry associative array, override map.

  • Static-initializer self-registration removes the central list — exactly what uvm_*_utils macros do.

  • Overrides must be installed before construction; they redirect future creates, not existing objects.

Common pitfalls

  • Calling new() directly somewhere in the env — that construction silently escapes every override.

  • Setting the override after the environment is built — too late, the old-type objects already exist.

  • Registering two classes under one name without noticing — last registration wins, first type vanishes.

  • Forgetting $cast when you need the derived view of a factory-returned base handle.