Auto-dependency Generation
Verification Unleashed

Switching from AOP to OOP - Strategy and Factory Patterns

I started my verification career using Specman.  As such, I have a pretty good understanding of how to design a verification environment using e and Aspect Oriented Programming (AOP) techniques.  Over the last couple of years I've been building my expertise in strictly OOP based environments such as C++, SystemC, and Vera.  It's been a bit of a struggle, as AOP makes rapid prototyping of a test environment a piece of cake (relatively speaking).  The reason is that AOP allows the creation of cross-cutting concerns that can be applied after the fact to allow the user to modify very specific portions of a data structure.

As an example, suppose I wanted to create a monitor to watch three similar interfaces.  In e, I'd start with something like this:

type monitor_t : [A, B, C];
unit monitor {
  // What kind of monitor is this?
  kind: monitor_t;
  has_write_bus : bool;

  // Signal map
  sigs: monitor_sigs_u is instance;

  // Thread to look for data on bus
  daemon() @sigs.clk is {
    while (1) {
      // Look for a transaction
      process_transaction();
    };
  };
  // Extend this method to watch for a transaction
  process_transaction() @sigs.clk;
};

Then, somewhere later I could extend monitor to add functionality as needed:

extend A monitor {
  // Special handling for status for monitor A
  process_transaction() @sigs.clk is also { ... };
};

I could even add functionality that cuts across two different kinds of monitors that have something in common, like having a write bus:

extend TRUE'has_write_bus monitor {
  // Since we have a write bus, do something different for the transaction processing
  process_transaction() @sigs.clk is also { ... };
}

Cool stuff, eh?  But what happens when you're using a language such as SystemC or SystemVerilog where AOP is not available (or Vera where it's not used as frequently)?  The good news is that there is a solution.  The bad news is that it requires a little more planning and a bit more code to get everything working correctly.  The trick is understanding the tools of the OOP trade, otherwise known as design patterns.  The Prentice Hall web site has an interesting description of some of the most common design patterns.  The patterns are also described in the book "Design Patterns" by Gamma et al.  The two patterns that come in handy here are the Strategy and Factory patterns.  The description that follows uses both of the above sources as reference material.

The strategy pattern is used when you need to have several different ways to implement an operation depending on the specifics of a given scenario.  For example, the monitor in the example above may require different strategies for the "process_transaction()" and "process_status()" depending on whether we're looking at interface A, B, or C (or perhaps whether the interface has a write bus or not).  If we encapsulate each strategy in an object with a common base class, we can write the monitor using references to the base class but at run time generate an instance of the derived class that knows which strategy should be used.  The next logical question is - how do you generate a concrete instance of the correct derived class?  The factory pattern provides the solution.

Given input, the factory pattern determines which type of strategy is needed and generates the appropriate concrete class.  The following example demonstrates the factory and strategy patterns using Vera.

// Virtual base class... we'll never actually instantiate this.
virtual class busStrategyBase {
  // The strategy will have access to all the goodies in the Monitor
  // since we've passed it a pointer.
  virtual task execute(monitor m);
}

class txnStrategyFactory {
  virtual function busStrategyBase createTxnStrategy(string s) {
    if (s.compare("A")) {
      busAStrategy busA = new;
      getTxnStrategy = busA;
    } else if (s.compare("B")) {
      busBStrategy busB = new;
      getTxnStrategy = busB;
    } else {
      // Just for kicks, let's make the default C.
      busCStrategy busC = new;
      getTxnStrategy = busC;
    }
  }
}

class busAStrategy extends busStrategyBase {
   task execute(monitor m) {
      // Special stuff for bus A goes here.
   }
}

class monitor {

  string name;

  txnStrategyFactory tFactory;
  busStrategyBase BusStrategy;

  task daemon() {
    while (1) {
      // The strategy used will be dependent on the value of the "name" field.
      BusStrategy.execute();
    }
  }

  task new(string MonitorName) {
    name = MonitorName;
    tFactory = new(name);
    BusStrategy = tFactory.createTxnStrategy();
  }
}

The important thing to note is that when using OOP, you really need to consider where you're likely to want to use different strategies.  It can be difficult (though certainly not impossible) to hack strategies and factories back into existing code.  It's also important to realize that if you don't understand design patterns such as the factory and strategy patterns, you'll have trouble reaping the benefits of an object oriented design strategy.

Comments