Creational Patterns: Factory and Builder in Verification
Every UVM engineer uses the factory. Few understand it. Even fewer realize there's a whole family of creational patterns - Factory Method, Abstract Factory, Builder - each solving different object creation problems. This post goes beyond type_id::create() to show you what these patterns really do and when to apply them.
The Problem: Why Object Creation Matters
Consider this seemingly innocent code:
class axi_scoreboard extends uvm_scoreboard;
function void check_transaction(axi_transaction t);
// Need a reference transaction for comparison
axi_transaction ref_txn = new("ref_txn"); // Direct instantiation
ref_txn.copy(expected_model.predict(t));
// ...
endfunction
endclass
What's wrong with new()? Several things:
- Tight coupling: The scoreboard is hardwired to
axi_transaction. Want to useaxi4_transactionoraxi_lite_transaction? Modify the scoreboard. - No extensibility: A test can't substitute a specialized transaction type.
- Violates Open/Closed: Adding new transaction types requires changing existing code.
The Gang of Four identified this in 1994:
"Program to an interface, not an implementation... Object creation often involves more than simply instantiating a class."
— Design Patterns, Gamma et al.
Factory Method: Delegation of Creation
The Factory Method pattern delegates object creation to a method that subclasses can override. It's the simplest creational pattern and appears throughout UVM.
classDiagram
class Creator {
+factoryMethod() Product
+operation()
}
class ConcreteCreator {
+factoryMethod() ConcreteProduct
}
class Product {
<<interface>>
}
class ConcreteProduct
Creator <|-- ConcreteCreator
Product <|.. ConcreteProduct
Creator ..> Product : creates
ConcreteCreator ..> ConcreteProduct : creates
Factory Method in UVM Sequences
You've been using Factory Method without knowing it:
class base_sequence extends uvm_sequence #(axi_transaction);
// Factory method - subclasses override to create different transaction types
virtual function axi_transaction create_transaction(string name);
axi_transaction t = axi_transaction::type_id::create(name);
return t;
endfunction
task body();
axi_transaction req;
repeat(num_transactions) begin
req = create_transaction("req"); // Calls factory method
start_item(req);
if (!req.randomize()) `uvm_fatal("SEQ", "Randomization failed")
finish_item(req);
end
endtask
endclass
// Subclass overrides factory method
class error_injection_sequence extends base_sequence;
virtual function axi_transaction create_transaction(string name);
// Create error-capable transaction instead
axi_error_transaction t = axi_error_transaction::type_id::create(name);
return t; // Polymorphism: returns base type
endfunction
endclass
The base sequence doesn't know (or care) what concrete type it's creating. The factory method provides a seam - a point where behavior can change without modifying the caller.
UVM Factory: Abstract Factory on Steroids
The UVM Factory is an implementation of the Abstract Factory pattern combined with a Registry. Understanding this reveals its full power.
What type_id::create() Actually Does
// This innocent line...
axi_transaction t = axi_transaction::type_id::create("t", this);
// ...is shorthand for approximately:
axi_transaction t;
uvm_factory factory = uvm_factory::get(); // Singleton
uvm_object_wrapper wrapper = factory.find_override_by_type(
axi_transaction::get_type(), get_full_name());
t = wrapper.create_object("t");
if (!$cast(t, obj)) `uvm_fatal(...)
The factory:
- Looks up whether any override exists for this type at this path
- Uses the (possibly overridden) type's wrapper to create the object
- Returns it as the requested type
Override Mechanisms
UVM provides two override dimensions:
| Override Type | Scope | Use Case |
|---|---|---|
set_type_override_by_type | Global | Replace all instances of a type |
set_inst_override_by_type | Path-specific | Replace only at specific hierarchy |
class error_test extends base_test;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
// Global: All axi_transaction become axi_error_transaction
axi_transaction::type_id::set_type_override(
axi_error_transaction::get_type());
// Instance-specific: Only agent1's driver gets error transactions
axi_transaction::type_id::set_inst_override(
axi_error_transaction::get_type(),
"env.agent1.driver.*");
endfunction
endclass
The Registration Mechanism
Ever wonder what `uvm_object_utils actually does?
// This macro...
`uvm_object_utils(axi_transaction)
// ...expands to approximately:
typedef uvm_object_registry #(axi_transaction, "axi_transaction") type_id;
static function type_id get_type();
return type_id::get();
endfunction
function uvm_object_wrapper get_object_type();
return type_id::get();
endfunction
The uvm_object_registry is a parameterized singleton that:
- Registers the type with the global factory at elaboration time
- Provides a wrapper that knows how to create instances
- Enables string-based and type-based lookup
Factory Power: Configuration-Driven Testbenches
// In your test, driven by a configuration file or plusarg:
function void configure_factory_from_plusargs();
string override_type;
if ($value$plusargs("TXN_TYPE=%s", override_type)) begin
case (override_type)
"error": axi_transaction::type_id::set_type_override(
axi_error_transaction::get_type());
"slow": axi_transaction::type_id::set_type_override(
axi_slow_transaction::get_type());
"minimal": axi_transaction::type_id::set_type_override(
axi_minimal_transaction::get_type());
endcase
end
endfunction
Now the same testbench supports multiple transaction types without recompilation.
Builder Pattern: Complex Object Construction
Factory patterns decide which class to instantiate. The Builder pattern addresses a different problem: how to construct complex objects step by step.
The Problem: Telescoping Constructors
// Anti-pattern: Too many constructor parameters
class pcie_tlp extends uvm_sequence_item;
function new(string name = "pcie_tlp",
tlp_type_e tlp_type = MEM_RD,
bit [63:0] address = 0,
bit [9:0] length = 1,
bit [7:0] tag = 0,
bit [15:0] requester_id = 0,
traffic_class_e tc = TC0,
bit poisoned = 0,
bit [3:0] first_be = 4'hF,
bit [3:0] last_be = 4'hF,
// ... 10 more parameters
);
// ...
endfunction
endclass
// Calling code is unreadable
pcie_tlp tlp = new("tlp", MEM_WR, 64'h1000, 10'd4, 8'd1, 16'h100,
TC0, 0, 4'hF, 4'hF, ...);
Which parameter is which? What if you want defaults for most but need to set one in the middle?
Builder Solution: Fluent Interface
class pcie_tlp_builder;
// Internal state
protected tlp_type_e m_type;
protected bit [63:0] m_address;
protected bit [9:0] m_length;
protected bit [7:0] m_tag;
protected bit [15:0] m_requester_id;
protected traffic_class_e m_tc;
protected bit m_poisoned;
protected bit [3:0] m_first_be;
protected bit [3:0] m_last_be;
protected bit [31:0] m_data[$];
// Fluent setters - each returns 'this' for chaining
function pcie_tlp_builder set_type(tlp_type_e t);
m_type = t;
return this;
endfunction
function pcie_tlp_builder set_address(bit [63:0] addr);
m_address = addr;
return this;
endfunction
function pcie_tlp_builder set_length(bit [9:0] len);
m_length = len;
return this;
endfunction
function pcie_tlp_builder set_tag(bit [7:0] tag);
m_tag = tag;
return this;
endfunction
function pcie_tlp_builder set_traffic_class(traffic_class_e tc);
m_tc = tc;
return this;
endfunction
function pcie_tlp_builder set_poisoned(bit p = 1);
m_poisoned = p;
return this;
endfunction
function pcie_tlp_builder add_data(bit [31:0] dw);
m_data.push_back(dw);
return this;
endfunction
function pcie_tlp_builder set_data(bit [31:0] data[$]);
m_data = data;
return this;
endfunction
// Terminal operation: build the object
function pcie_tlp build();
pcie_tlp tlp = pcie_tlp::type_id::create("tlp");
tlp.tlp_type = m_type;
tlp.address = m_address;
tlp.length = m_length;
tlp.tag = m_tag;
tlp.requester_id = m_requester_id;
tlp.tc = m_tc;
tlp.poisoned = m_poisoned;
tlp.first_be = m_first_be;
tlp.last_be = m_last_be;
tlp.data = m_data;
// Validation
if (m_type inside {MEM_WR, CPL_D} && m_data.size() == 0)
`uvm_warning("BUILDER", "Write/CplD TLP with no data")
return tlp;
endfunction
// Reset for reuse
function void reset();
m_type = MEM_RD;
m_address = 0;
m_length = 1;
m_tag = 0;
m_requester_id = 0;
m_tc = TC0;
m_poisoned = 0;
m_first_be = 4'hF;
m_last_be = 4'hF;
m_data.delete();
endfunction
function new();
reset();
endfunction
endclass
Using the Builder
class pcie_sequence extends uvm_sequence #(pcie_tlp);
pcie_tlp_builder builder;
function new(string name = "pcie_sequence");
super.new(name);
builder = new();
endfunction
task body();
pcie_tlp tlp;
// Readable, self-documenting construction
tlp = builder
.set_type(MEM_WR)
.set_address(64'h0000_1000)
.set_length(4)
.set_tag(8'h01)
.set_traffic_class(TC2)
.add_data(32'hDEAD_BEEF)
.add_data(32'hCAFE_BABE)
.add_data(32'h1234_5678)
.add_data(32'h9ABC_DEF0)
.build();
start_item(tlp);
finish_item(tlp);
builder.reset(); // Reuse builder
// Another TLP - only set what differs from defaults
tlp = builder
.set_type(MEM_RD)
.set_address(64'h0000_2000)
.set_length(1)
.build();
start_item(tlp);
finish_item(tlp);
endtask
endclass
Builder + Randomization
Combine Builder with constrained random for the best of both worlds:
class pcie_tlp_builder;
// ... previous code ...
// Build with constraints
function pcie_tlp build_randomized(int max_length = 128);
pcie_tlp tlp = pcie_tlp::type_id::create("tlp");
// Apply builder settings as constraints
if (!tlp.randomize() with {
tlp_type == local::m_type;
address == local::m_address;
length inside {[1:max_length]};
tc == local::m_tc;
// Let other fields randomize freely
}) `uvm_fatal("BUILDER", "Randomization failed")
return tlp;
endfunction
endclass
// Usage: Constrain what matters, randomize the rest
tlp = builder
.set_type(MEM_WR)
.set_address(64'h1000)
.build_randomized(16); // Random length 1-16, random data
Director Pattern: Reusable Build Sequences
When you find yourself building the same object configurations repeatedly, extract them into a Director:
class pcie_tlp_director;
pcie_tlp_builder builder;
function new();
builder = new();
endfunction
// Predefined configurations
function pcie_tlp build_config_read(bit [11:0] reg_addr);
return builder
.set_type(CFG_RD0)
.set_address({52'b0, reg_addr})
.set_length(1)
.build();
endfunction
function pcie_tlp build_config_write(bit [11:0] reg_addr, bit [31:0] data);
return builder
.set_type(CFG_WR0)
.set_address({52'b0, reg_addr})
.set_length(1)
.add_data(data)
.build();
endfunction
function pcie_tlp build_msi_write(bit [63:0] msi_addr, bit [31:0] msi_data);
return builder
.set_type(MEM_WR)
.set_address(msi_addr)
.set_length(1)
.set_traffic_class(TC0) // MSI always TC0
.add_data(msi_data)
.build();
endfunction
function pcie_tlp build_dma_read(bit [63:0] host_addr, int num_dwords);
return builder
.set_type(MEM_RD)
.set_address(host_addr)
.set_length(num_dwords)
.build();
endfunction
endclass
// Clean, intention-revealing code
pcie_tlp_director director = new();
pcie_tlp cfg_rd = director.build_config_read(12'h010); // Read Device ID
pcie_tlp msi = director.build_msi_write(msi_addr, msi_data);
pcie_tlp dma_req = director.build_dma_read(buffer_addr, 64);
When to Use Which Pattern
flowchart TD
A[Need to create objects?] --> B{One type or family?}
B -->|One type| C{Complex construction?}
B -->|Family of types| D[Abstract Factory]
C -->|Simple| E{Need runtime substitution?}
C -->|Complex, multi-step| F[Builder]
E -->|Yes| G[Factory Method]
E -->|No| H[Direct new]
style D fill:#d1fae5,stroke:#10b981
style F fill:#d1fae5,stroke:#10b981
style G fill:#d1fae5,stroke:#10b981
style H fill:#fef3c7,stroke:#f59e0b
| Pattern | Problem | UVM Example |
|---|---|---|
| Factory Method | Let subclasses decide which class to instantiate | Virtual create_transaction() in base sequence |
| Abstract Factory (UVM Factory) | Create families of related objects | type_id::create() with overrides |
| Builder | Construct complex objects step-by-step | TLP builder with fluent interface |
| Director | Reusable build configurations | Common TLP configurations |
Common Mistakes
1. Using new() When Factory is Available
// BAD: Bypasses factory, breaks overrides
axi_transaction t = new("t");
// GOOD: Uses factory, enables test customization
axi_transaction t = axi_transaction::type_id::create("t", this);
2. Forgetting to Register Types
// BAD: No registration - factory can't create or override
class my_transaction extends uvm_sequence_item;
// Missing `uvm_object_utils(my_transaction)
endclass
// GOOD: Properly registered
class my_transaction extends uvm_sequence_item;
`uvm_object_utils(my_transaction)
// ...
endclass
3. Override Order Matters
// BAD: Override after object creation
function void build_phase(uvm_phase phase);
env = my_env::type_id::create("env", this); // Created first
// Too late! env already created with original type
my_env::type_id::set_type_override(custom_env::get_type());
endfunction
// GOOD: Override before creation
function void build_phase(uvm_phase phase);
my_env::type_id::set_type_override(custom_env::get_type());
env = my_env::type_id::create("env", this); // Gets override
endfunction
4. Builder Without Reset
// BAD: Builder state accumulates
tlp1 = builder.set_type(MEM_WR).set_address(64'h1000).build();
tlp2 = builder.set_type(MEM_RD).build(); // Still has address 0x1000!
// GOOD: Reset between uses
tlp1 = builder.set_type(MEM_WR).set_address(64'h1000).build();
builder.reset();
tlp2 = builder.set_type(MEM_RD).set_address(64'h2000).build();
Key Takeaways
- Factory Method: Delegate object creation to overridable methods
- UVM Factory: Global Abstract Factory + Registry enabling test-time type substitution
- Always use
type_id::create(): Directnew()breaks the override mechanism - Builder Pattern: Construct complex objects with readable, step-by-step fluent interface
- Director: Encapsulate common build configurations for reuse
- Combine patterns: Builder + Factory + Randomization for maximum flexibility
Further Reading
- Design Patterns, Chapter 3 (Creational Patterns) - Gamma, Helm, Johnson, Vlissides
- Effective Java, Item 2 (Builder Pattern) - Joshua Bloch
- Head First Design Patterns, Chapter 4 (Factory Pattern) - Freeman & Robson
- UVM 1.2 Class Reference -
uvm_factory,uvm_object_registry
Interview Corner
Q: Why should you use type_id::create() instead of new() in UVM?
A: type_id::create() uses the UVM factory, which enables type overrides. Tests can substitute derived types without modifying the original code. Direct new() bypasses this mechanism.
Q: What's the difference between Factory Method and Abstract Factory?
A: Factory Method uses inheritance - a method creates one product, subclasses override to change the product type. Abstract Factory uses composition - an object creates families of related products, and the entire factory object can be substituted.
Previous: Part 0 - Introduction | Next: Structural Patterns
Return to Software Engineering for DV
Comments (0)
Leave a Comment