Decorator Pattern in UVM: Adding Behavior Without Changing the Class
Your AXI monitor works. It observes transactions on the bus and broadcasts them through an analysis port. Now verification demands coverage collection for regression, protocol checking for compliance, and transaction logging for debug — three concerns that have no business touching the monitor itself. The Decorator pattern lets you layer each concern onto the monitor's output independently, at runtime, without modifying or subclassing the monitor class.
- The Problem: When Every Test Needs Different Instrumentation
- Gang of Four: The Decorator Pattern
- UVM's Decorator: Analysis Subscribers and Transaction Wrappers
- Building AXI Monitor Decorators
- Scaling Up: Stacking Decorators and Runtime Control
- Advanced: Decorator + Factory
- Quick Reference
The Problem: When Every Test Needs Different Instrumentation
You have a working AXI monitor. It captures bus transactions and broadcasts them via an analysis port. It does one thing well. Now your verification plan asks for three more things: functional coverage during regression, protocol compliance checking for signoff, and detailed transaction logging during debug. Three different concerns, three different audiences, and none of them belong inside the monitor.
Naive Approach 1: Modify the Monitor
The most direct approach is to add everything inline. Coverage bins, protocol checks, and log formatting all go into the monitor itself:
class axi_monitor extends uvm_monitor;
`uvm_component_utils(axi_monitor)
uvm_analysis_port #(axi_txn) ap;
axi_txn txn;
// Coverage — bolted on
covergroup axi_cg;
cp_op: coverpoint txn.op;
cp_burst: coverpoint txn.burst_type;
cp_len: coverpoint txn.burst_len { bins short = {[0:3]}; bins long = {[4:255]}; }
cross_op_burst: cross cp_op, cp_burst;
endgroup
function new(string name, uvm_component parent);
super.new(name, parent);
ap = new("ap", this);
axi_cg = new();
endfunction
task run_phase(uvm_phase phase);
forever begin
collect_txn(txn);
// Coverage — always runs
axi_cg.sample();
// Protocol checking — always runs
if (txn.burst_type == WRAP && (txn.burst_len + 1) inside {1, 2, 4, 8, 16})
; // OK
else if (txn.burst_type == WRAP)
`uvm_error("PROTO", $sformatf("WRAP burst length %0d not power of 2",
txn.burst_len + 1))
// Logging — always runs
`uvm_info("AXI_LOG", $sformatf("[%0t] %s addr=0x%08h len=%0d",
$time, txn.op.name(), txn.addr, txn.burst_len), UVM_MEDIUM)
ap.write(txn);
end
endtask
endclass
Your monitor grew from 20 lines to 50. Every test gets coverage, checking, and logging whether it needs them or not. Want to disable logging during regression? You need a flag. Want to add latency tracking? More code in the same class. This is a Single Responsibility Principle violation — the monitor now does four jobs instead of one, and every change risks breaking all of them.
Naive Approach 2: Subclass Per Feature
You could keep the base monitor clean and create subclasses for each feature:
class axi_monitor_with_coverage extends axi_monitor;
// Add coverage
endclass
class axi_monitor_with_logging extends axi_monitor;
// Add logging
endclass
class axi_monitor_with_coverage_and_logging extends axi_monitor;
// Add both
endclass
class axi_monitor_with_coverage_and_checking extends axi_monitor;
// Add coverage + checking
endclass
// ... and so on
Three features. Seven possible combinations. Add a fourth feature (latency tracking) and you have fifteen subclasses. This is the class explosion problem — N features require up to 2N subclasses, and each one duplicates the combination logic. Maintaining this is a nightmare.
Both approaches fail because they try to put optional, combinable behavior into the monitor's inheritance tree. What you need is a way to attach behavior to the monitor's output externally, one concern at a time, with the ability to mix and match at runtime.
There is a pattern for exactly this.
Gang of Four: The Decorator Pattern
"Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality."
— Design Patterns: Elements of Reusable Object-Oriented Software (Gamma et al., 1994)
The Decorator pattern works by wrapping an object with another object that has the same interface. The wrapper delegates to the original, adding behavior before or after the delegation. Because the wrapper shares the interface, the client cannot tell the difference between the original and the decorated version — and you can stack multiple wrappers, each adding one concern.
classDiagram
class Component {
<<interface>>
+operation()
}
class ConcreteComponent {
+operation()
}
class Decorator {
<<abstract>>
-component : Component
+operation()
}
class ConcreteDecoratorA {
+operation()
+addedBehaviorA()
}
class ConcreteDecoratorB {
+operation()
+addedBehaviorB()
}
Component <|.. ConcreteComponent : implements
Component <|.. Decorator : implements
Decorator o-- Component : wraps
Decorator <|-- ConcreteDecoratorA
Decorator <|-- ConcreteDecoratorB
The key insight is the double relationship between Decorator and Component. The Decorator is a Component (same interface) and has a Component (composition). This is what enables stacking: ConcreteDecoratorA wraps a Component, and ConcreteDecoratorB wraps a Component — including a ConcreteDecoratorA. Each wrapper adds one behavior and passes everything else through.
Decorator vs. Adapter
The Adapter pattern (covered in the previous post) and the Decorator pattern look structurally similar — both use composition to wrap an existing object. The difference is in intent:
- Adapter changes the interface but preserves the behavior. Your
apb_reg_adapterconverteduvm_reg_bus_opintoapb_txn— different types, same operation. - Decorator preserves the interface but changes the behavior. A coverage subscriber receives the same
axi_txnand adds coverage sampling — same type, new behavior added.
If you are translating between two worlds, that is an Adapter. If you are adding responsibilities to an existing object without changing its type, that is a Decorator.
When Decorator Shines
- Cross-cutting concerns — Coverage, logging, protocol checking, performance monitoring. These concerns span multiple components and should not be baked into any single one.
- Optional and combinable features — Some tests need coverage but not logging. Others need logging but not checking. Decorators let you pick and choose at the test level.
- Runtime flexibility — The decision of which behaviors to attach can be deferred to
build_phaseor even controlled byuvm_config_dbflags, rather than being baked into class definitions at compile time. - Avoiding 2N explosion — Three decorators give you seven combinations. Four give you fifteen. You write N classes, not 2N.
UVM's Decorator: Analysis Subscribers and Transaction Wrappers
UVM does not have a class called uvm_decorator. But its analysis port ecosystem implements the Decorator pattern naturally. The analysis port broadcasts transactions to any number of connected subscribers, and each subscriber adds one behavior without modifying the original component or the transaction. This is the Decorator pattern — same interface, added behavior, stacking.
GoF Role Mapping
Map the GoF participants to UVM:
- Component interface —
write(T t). The analysis port defines this contract. Every subscriber must implement it. - ConcreteComponent — Your AXI monitor. It produces transactions and writes them to its analysis port.
- Decorator —
uvm_subscriber#(T). The abstract base that connects to an analysis port and implementswrite(). - ConcreteDecorators — Your coverage collector, protocol checker, and transaction logger. Each extends
uvm_subscriber#(axi_txn)and adds one concern.
The analysis port broadcasts to all connected subscribers. Every transaction that the monitor produces gets seen by every attached decorator. Each decorator independently processes the transaction — one samples coverage, another runs protocol checks, a third logs the details. The monitor does not know how many decorators are attached, or what they do. It just calls ap.write(txn).
Composition Over Inheritance
Classical Decorator pattern uses composition: the decorator wraps the component and delegates to it. UVM's analysis ecosystem achieves the same effect through the observer pattern — subscribers observe the component's output rather than wrapping the component itself. The result is the same: behavior is added externally, one concern at a time, without modifying the component's class.
This matters most when you cannot modify the base component. If your AXI monitor comes from a third-party VIP, you cannot add coverage to it, period. But you can connect a subscriber to its analysis port. Decorators through analysis ports work with any component that has an analysis port — whether you own the source code or not.
Transaction Wrappers
Sometimes the decoration applies to the transaction rather than the component. A transaction wrapper adds metadata — timestamps, trace IDs, source identifiers — without modifying the base transaction class. This is composition-based Decorator applied to data rather than components:
class traced_axi_txn extends uvm_sequence_item;
axi_txn inner; // Wrapped transaction — composition
string trace_id; // Added metadata
realtime capture_time; // Timestamp
string source; // Which monitor captured this
`uvm_object_utils(traced_axi_txn)
function new(string name = "traced_axi_txn");
super.new(name);
endfunction
static function traced_axi_txn wrap(axi_txn t, string src);
traced_axi_txn w = new("traced");
w.inner = t;
w.trace_id = $sformatf("TRC_%0d", $urandom());
w.capture_time = $realtime;
w.source = src;
return w;
endfunction
// Delegate core fields to inner transaction
function bit [31:0] get_addr(); return inner.addr; endfunction
function axi_op_e get_op(); return inner.op; endfunction
function bit [7:0] get_id(); return inner.id; endfunction
function axi_burst_e get_burst(); return inner.burst_type; endfunction
endclass
The wrapper does not extend axi_txn — it contains one. This is important. If axi_txn comes from a VIP, you cannot add fields to it. But you can wrap it in a new object that carries the extra metadata. Downstream subscribers that need the metadata use traced_axi_txn; those that don't can still access the inner axi_txn directly.
There is a SystemVerilog tension here. The language pushes you toward inheritance — class extended_txn extends axi_txn feels natural. But composition wins when you cannot modify the base class, when you want to wrap objects from different class hierarchies, or when the added data is orthogonal to the base transaction's purpose. Decorator favors composition for exactly these reasons.
Building AXI Monitor Decorators
Let's build three concrete decorators for the AXI monitor. Each one extends uvm_subscriber#(axi_txn) and adds exactly one concern. We will start with the base AXI transaction class and monitor, then build the coverage collector, protocol checker, and logger.
Base AXI Transaction and Monitor
typedef enum bit { AXI_READ = 0, AXI_WRITE = 1 } axi_op_e;
typedef enum bit [1:0] { OKAY = 2'b00, EXOKAY = 2'b01, SLVERR = 2'b10, DECERR = 2'b11 } axi_resp_e;
typedef enum bit [1:0] { FIXED = 2'b00, INCR = 2'b01, WRAP = 2'b10 } axi_burst_e;
class axi_txn extends uvm_sequence_item;
rand axi_op_e op;
rand bit [31:0] addr;
rand bit [31:0] data[];
rand axi_burst_e burst_type;
rand bit [7:0] burst_len; // Beats - 1
rand bit [2:0] burst_size; // Bytes/beat = 2^burst_size
rand bit [3:0] cache;
rand bit [2:0] prot;
rand bit [3:0] qos;
rand bit lock;
rand bit [7:0] id;
rand axi_resp_e exp_resp; // Expected response
`uvm_object_utils(axi_txn)
function new(string name = "axi_txn");
super.new(name);
endfunction
endclass
The monitor is simple. It collects transactions from the bus interface and writes them to its analysis port. This is the ConcreteComponent in GoF terms — the object being decorated:
class axi_monitor extends uvm_monitor;
`uvm_component_utils(axi_monitor)
uvm_analysis_port #(axi_txn) ap;
function new(string name, uvm_component parent);
super.new(name, parent);
ap = new("ap", this);
endfunction
task run_phase(uvm_phase phase);
axi_txn txn;
forever begin
// Collect transaction from bus interface
collect_txn(txn); // Implementation depends on your VIF
ap.write(txn); // Broadcast to all subscribers
end
endtask
endclass
The monitor does one thing: collect and broadcast. Everything else happens outside, in decorators.
Coverage Decorator
The coverage collector samples functional coverage on every transaction. It tracks operation types, burst types, burst lengths, and their crosses — the data you need for regression closure:
class axi_coverage_collector extends uvm_subscriber #(axi_txn);
`uvm_component_utils(axi_coverage_collector)
covergroup axi_txn_cg with function sample(axi_txn t);
cp_op: coverpoint t.op {
bins read = {AXI_READ};
bins write = {AXI_WRITE};
}
cp_burst: coverpoint t.burst_type {
bins fixed = {FIXED};
bins incr = {INCR};
bins wrap = {WRAP};
}
cp_len: coverpoint t.burst_len {
bins single = {0};
bins short_b = {[1:3]};
bins medium_b = {[4:15]};
bins long_b = {[16:255]};
}
cp_size: coverpoint t.burst_size {
bins byte_1 = {3'b000};
bins byte_2 = {3'b001};
bins byte_4 = {3'b010};
bins byte_8 = {3'b011};
}
cross_op_burst: cross cp_op, cp_burst;
cross_op_burst_len: cross cp_op, cp_burst, cp_len;
endgroup
function new(string name, uvm_component parent);
super.new(name, parent);
axi_txn_cg = new();
endfunction
function void write(axi_txn t);
axi_txn_cg.sample(t);
endfunction
endclass
The write() method is the Decorator interface. Every time the monitor broadcasts a transaction, this subscriber samples it for coverage. The monitor does not know this subscriber exists. The coverage collector does not know what other subscribers are connected. Each decorator is independent.
Protocol Checker Decorator
The protocol checker validates AXI specification rules on every observed transaction. It catches violations that your scoreboard would miss — illegal burst lengths, 4KB boundary crossings, and address alignment errors:
class axi_protocol_checker extends uvm_subscriber #(axi_txn);
`uvm_component_utils(axi_protocol_checker)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void write(axi_txn t);
check_wrap_length(t);
check_4kb_boundary(t);
check_burst_alignment(t);
endfunction
// AXI spec: WRAP burst length must be 2, 4, 8, or 16
function void check_wrap_length(axi_txn t);
int unsigned beats;
if (t.burst_type != WRAP) return;
beats = t.burst_len + 1;
if (!(beats inside {2, 4, 8, 16}))
`uvm_error("AXI_PROTO", $sformatf(
"WRAP burst length %0d is not 2/4/8/16. addr=0x%08h id=0x%02h",
beats, t.addr, t.id))
endfunction
// AXI spec: Burst must not cross a 4KB boundary
function void check_4kb_boundary(axi_txn t);
bit [31:0] start_addr, end_addr;
int unsigned total_bytes;
if (t.burst_type == FIXED) return; // FIXED does not cross boundaries
total_bytes = (t.burst_len + 1) * (1 << t.burst_size);
start_addr = t.addr;
end_addr = t.addr + total_bytes - 1;
if (start_addr[31:12] != end_addr[31:12])
`uvm_error("AXI_PROTO", $sformatf(
"Burst crosses 4KB boundary. start=0x%08h end=0x%08h id=0x%02h",
start_addr, end_addr, t.id))
endfunction
// AXI spec: Start address must be aligned to transfer size
function void check_burst_alignment(axi_txn t);
int unsigned align_mask;
align_mask = (1 << t.burst_size) - 1;
if ((t.addr & align_mask) != 0)
`uvm_error("AXI_PROTO", $sformatf(
"Address 0x%08h not aligned to burst_size %0d (align=%0d bytes)",
t.addr, t.burst_size, (1 << t.burst_size)))
endfunction
endclass
This decorator adds zero overhead to the monitor. The protocol checks run in the subscriber's context, completely decoupled from the monitor's transaction collection. If you don't connect this subscriber, you pay nothing.
Logger Decorator
The logger formats every transaction into a human-readable log line. Useful during debug, unnecessary during regression:
class axi_txn_logger extends uvm_subscriber #(axi_txn);
`uvm_component_utils(axi_txn_logger)
int unsigned txn_count;
function new(string name, uvm_component parent);
super.new(name, parent);
txn_count = 0;
endfunction
function void write(axi_txn t);
txn_count++;
`uvm_info("AXI_LOG", $sformatf(
"[#%0d] %s addr=0x%08h id=0x%02h burst=%s len=%0d size=%0d data[0]=0x%08h",
txn_count, t.op.name(), t.addr, t.id,
t.burst_type.name(), t.burst_len, t.burst_size,
(t.data.size() > 0) ? t.data[0] : 32'h0), UVM_MEDIUM)
endfunction
endclass
Connecting Decorators in the Environment
The environment creates the monitor and its decorators, then wires them together in connect_phase:
class axi_env extends uvm_env;
`uvm_component_utils(axi_env)
axi_monitor monitor;
axi_coverage_collector cov_collector;
axi_protocol_checker proto_checker;
axi_txn_logger logger;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
monitor = axi_monitor::type_id::create("monitor", this);
cov_collector = axi_coverage_collector::type_id::create("cov_collector", this);
proto_checker = axi_protocol_checker::type_id::create("proto_checker", this);
logger = axi_txn_logger::type_id::create("logger", this);
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
monitor.ap.connect(cov_collector.analysis_export);
monitor.ap.connect(proto_checker.analysis_export);
monitor.ap.connect(logger.analysis_export);
endfunction
endclass
Three connect() calls. Each one attaches a decorator to the monitor. The monitor still does one thing — collect and broadcast. Each subscriber does one thing — its specific concern. When the monitor calls ap.write(txn), all three subscribers receive the transaction and process it independently.
Common Pitfalls
- Forgetting
connect()— A subscriber that is created but not connected to the analysis port will never receive transactions. No error message, no warning — just silent absence. Always verify yourconnect_phasewiring. - Wrong parameterization —
uvm_subscriber#(axi_txn)anduvm_subscriber#(apb_txn)are completely different types. If you connect a subscriber parameterized with the wrong transaction type, you get a compilation error (if you are lucky) or a runtime type mismatch. - Blocking in
write()— Thewrite()function is a function, not a task. It cannot consume simulation time. If you need to do time-consuming work (like waiting for a response), use auvm_tlm_analysis_fifoto buffer the transaction and process it in a separate task inrun_phase. - Modifying the transaction in
write()— All subscribers receive the same transaction object. If one subscriber modifies a field, every subsequent subscriber sees the modified value. Treat the transaction as read-only insidewrite(), or clone it first.
Scaling Up: Stacking Decorators and Runtime Control
The real power of the Decorator pattern is not having three subscribers — it is controlling which subscribers exist at runtime. Different tests need different instrumentation. Regression tests want coverage and checking. Debug tests want logging. Smoke tests might want nothing at all. The decoration decision belongs in the test, not in the environment.
Conditional Decoration with uvm_config_db
Use configuration flags to control which decorators get created. The environment checks these flags in build_phase and only instantiates the requested subscribers:
class axi_env extends uvm_env;
`uvm_component_utils(axi_env)
axi_monitor monitor;
axi_coverage_collector cov_collector;
axi_protocol_checker proto_checker;
axi_txn_logger logger;
bit enable_coverage = 1;
bit enable_checking = 1;
bit enable_logging = 0;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
monitor = axi_monitor::type_id::create("monitor", this);
uvm_config_db#(bit)::get(this, "", "enable_coverage", enable_coverage);
uvm_config_db#(bit)::get(this, "", "enable_checking", enable_checking);
uvm_config_db#(bit)::get(this, "", "enable_logging", enable_logging);
if (enable_coverage)
cov_collector = axi_coverage_collector::type_id::create("cov_collector", this);
if (enable_checking)
proto_checker = axi_protocol_checker::type_id::create("proto_checker", this);
if (enable_logging)
logger = axi_txn_logger::type_id::create("logger", this);
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
if (cov_collector != null)
monitor.ap.connect(cov_collector.analysis_export);
if (proto_checker != null)
monitor.ap.connect(proto_checker.analysis_export);
if (logger != null)
monitor.ap.connect(logger.analysis_export);
endfunction
endclass
Now tests control the decoration:
class regression_test extends base_test;
function void build_phase(uvm_phase phase);
// Coverage + checking, no logging
uvm_config_db#(bit)::set(this, "env", "enable_coverage", 1);
uvm_config_db#(bit)::set(this, "env", "enable_checking", 1);
uvm_config_db#(bit)::set(this, "env", "enable_logging", 0);
super.build_phase(phase);
endfunction
endclass
class debug_test extends base_test;
function void build_phase(uvm_phase phase);
// Logging only — fast, minimal overhead
uvm_config_db#(bit)::set(this, "env", "enable_coverage", 0);
uvm_config_db#(bit)::set(this, "env", "enable_checking", 0);
uvm_config_db#(bit)::set(this, "env", "enable_logging", 1);
super.build_phase(phase);
endfunction
endclass
The environment is the same in both tests. Only the configuration flags differ, which determines which decorators are instantiated. Three flags, seven possible combinations, zero code duplication.
Execution Order
Analysis ports broadcast to all connected subscribers in the order they were connected. In practice, this order rarely matters — each subscriber is independent and processes the transaction without side effects (assuming you follow the "don't modify the transaction" rule). If execution order does matter for your use case, control it by ordering the connect() calls in connect_phase.
Decorator vs. Callbacks vs. Inheritance
| Aspect | Decorator (Subscribers) | Callbacks | Inheritance |
|---|---|---|---|
| Where behavior lives | External component | Hook method inside component | Subclass of component |
| Coupling to component | Loose — only needs analysis port | Moderate — needs callback hook | Tight — extends the class |
| Combinability | Stack any number independently | Multiple callbacks can register | Single inheritance chain only |
| Runtime control | Yes — create/skip in build_phase | Yes — register/unregister | No — fixed at compile time |
| Works with third-party VIP | Yes — if VIP has analysis port | Only if VIP has callback hooks | Only if VIP is not final |
| GoF pattern | Decorator | Observer / Hook | Template Method |
UVM callbacks (uvm_callback) are "Decorator-adjacent" — they add behavior to a component at runtime. The difference is where the behavior lives. Callbacks are hooks inside the component: the component's code calls `uvm_do_callbacks at specific points, and registered callback objects provide the behavior. Decorators (subscribers) are wrappers outside the component: they observe its output independently. Callbacks require the component author to have placed hooks in advance. Decorators work with any component that has an analysis port, no hooks needed.
Advanced: Decorator + Factory
The Factory pattern controls what gets created. The Decorator pattern controls what behavior gets added. When you combine them, the Factory can swap in enhanced versions of your decorators without changing the environment or the test infrastructure.
Factory Override on a Subscriber
Suppose your base axi_coverage_collector covers the basics — operation types, burst types, burst lengths. For signoff, you need deeper cross-coverage: operation crossed with cache type, QoS-aware burst analysis, and ID-based transaction tracking. Rather than modifying the base coverage class (which is shared across projects), you extend it:
class axi_detailed_coverage extends axi_coverage_collector;
`uvm_component_utils(axi_detailed_coverage)
covergroup axi_detailed_cg with function sample(axi_txn t);
cp_op: coverpoint t.op;
cp_cache: coverpoint t.cache {
bins non_cacheable = {4'b0000};
bins cacheable[] = {[4'b0001:4'b1111]};
}
cp_qos: coverpoint t.qos {
bins low = {[0:3]};
bins medium = {[4:11]};
bins high = {[12:15]};
}
cp_id: coverpoint t.id {
bins ids[] = {[0:15]};
}
cross_op_cache: cross cp_op, cp_cache;
cross_op_qos: cross cp_op, cp_qos;
cross_id_op: cross cp_id, cp_op;
endgroup
function new(string name, uvm_component parent);
super.new(name, parent);
axi_detailed_cg = new();
endfunction
function void write(axi_txn t);
super.write(t); // Base coverage — op, burst, length crosses
axi_detailed_cg.sample(t); // Extended coverage — cache, QoS, ID crosses
endfunction
endclass
The signoff test overrides the base subscriber via Factory:
class signoff_coverage_test extends base_test;
function void build_phase(uvm_phase phase);
axi_coverage_collector::type_id::set_type_override(
axi_detailed_coverage::get_type());
uvm_config_db#(bit)::set(this, "env", "enable_coverage", 1);
super.build_phase(phase);
endfunction
endclass
The environment still calls axi_coverage_collector::type_id::create(). The Factory returns axi_detailed_coverage instead. The monitor, the analysis port, the connect_phase wiring — all unchanged. One line in the test swaps basic coverage for detailed coverage.
The Override Flow
sequenceDiagram
participant Test
participant Factory
participant Env
participant Monitor
participant Subscriber as axi_detailed_coverage
Test->>Factory: set_type_override(axi_coverage_collector → axi_detailed_coverage)
Test->>Env: build_phase via super.build_phase()
Env->>Factory: create("cov_collector")
Factory-->>Env: returns axi_detailed_coverage instance
Env->>Env: connect_phase: monitor.ap.connect(cov_collector.analysis_export)
Monitor->>Subscriber: write(axi_txn) during run_phase
Subscriber->>Subscriber: sample base + detailed covergroups
The Factory and Decorator compose naturally because they operate at different levels. The Factory controls instantiation — which class gets created. The Decorator controls behavior — what each subscriber does. The Factory override picks the right decorator variant; the analysis port delivers transactions to it. Neither mechanism needs to know about the other.
Series Tie-Back
Three structural and creational patterns, three orthogonal concerns:
- Factory (Post 1) — controls what gets created. Swap implementations without changing the environment.
- Adapter (Post 5) — controls interface translation. Bridge incompatible types without modifying either side.
- Decorator (this post) — controls behavior addition. Layer concerns onto objects without touching their classes.
These three patterns compose. A test can use the Factory to override a decorator (swap basic coverage for detailed coverage), where the decorator observes a monitor whose output is adapted from a protocol-specific bus (Adapter). Each pattern handles one dimension of variation, and together they give you a testbench that is flexible in what it creates, how it translates, and what behavior it adds.
Quick Reference
UVM Decorator Components
| UVM Component | GoF Role | What It Does |
|---|---|---|
uvm_analysis_port#(T) | Component interface | Broadcasts transactions to all connected subscribers |
uvm_subscriber#(T) | Decorator base | Abstract subscriber — extend and implement write() |
write(T t) | operation() | The Decorator interface — called for each broadcast transaction |
analysis_export | Component port | The subscriber's connection point — pass to ap.connect() |
uvm_tlm_analysis_fifo#(T) | Buffered decorator | Queues transactions for processing in run_phase tasks |
uvm_config_db#(bit) | Runtime control | Toggle which decorators get created per test |
Common Mistakes
| Mistake | Fix |
|---|---|
| Subscriber created but never connected | Always call monitor.ap.connect(subscriber.analysis_export) in connect_phase. No connection = no transactions. |
| Wrong subscriber parameterization | Ensure uvm_subscriber#(axi_txn) matches the analysis port's parameter type uvm_analysis_port#(axi_txn). |
Blocking inside write() | write() is a function, not a task. Use uvm_tlm_analysis_fifo to buffer and process in a run_phase task. |
Modifying transaction in write() | All subscribers share the same object. Clone before modifying, or treat it as read-only. |
Creating subscriber with new() | Use type_id::create() so Factory overrides apply to your decorators. |
Null check missing in conditional connect_phase | If decorators are conditionally created, check != null before calling connect(). |
Decorator vs. Adapter vs. Proxy
| Pattern | Intent | Interface Change? | Behavior Change? | UVM Example |
|---|---|---|---|---|
| Adapter | Make incompatible interfaces work together | Yes | No | uvm_reg_adapter — reg2bus() / bus2reg() |
| Decorator | Add responsibilities dynamically | No | Yes | uvm_subscriber — coverage, checking, logging |
| Proxy | Control access to an object | No | Maybe | uvm_reg_field access policies — RW, RO, WO |
Structural Patterns Progress
| Pattern | Status | UVM Focus |
|---|---|---|
| Adapter | Done — Post 5 | uvm_reg_adapter, reg2bus() / bus2reg() |
| Decorator | This post | Analysis subscribers, transaction wrappers |
| Facade | Next | UVM agent encapsulating driver + monitor + sequencer |
| Proxy | Coming soon | uvm_reg_field access policies |
Previous: Adapter Pattern — Bridging the register model to your bus with uvm_reg_adapter
Next: Facade Pattern — Simplifying complex subsystems behind a single interface
Comments (0)
Leave a Comment