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.
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 untouchedBuilding 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.
// ---- (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
endclassTest-time substitution: the error-injection example
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
endmoduleThis 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.