Reply to comment
Mastering Your Domain
Submitted by bingomanatee on 24 March, 2009 - 12:42I've been sniffing around domain driven developmen (DDD) and the Domain model for a while and frankly did not "get it"; where it came into focus for me was during my attempt to establish a non SQL based model for unit testing. previously I violated a lot of good advice by extending the hell out of active record table and record classes. What this gave me was a lower class count; what that took from me was the ability to create transport-independant functionality. Having the opportunity to start a project from stratch is relatively rare; it gave me the opportunity to establish best practices from day one, and therefore the responsibility of considering what those practices would be.
When I decided to start from day one with unit testing and create a tool that was guaranteed to have unit testing capacity from day one, I realized that altering the Database Abstraction object tree was not the way to go; instead I created a basic set of mock data using INI files. A lot more went into creating a mockable object heirarchy; when its better flushed out I'll dump the source here. But I wanted to sketch out the decision tree that went into the Domain abstraction. While this was a Zend Framework-centric model, the basic principles should be fairly universal.
Reinventing SQL
... on the face of it is of course a tremendous waste of time. So I attempted to break down the deliverables of SQL into a very limited set of functionality. The mock records were stored as stdClass (generic) objects based on field/value data from the test database.
- Retrieve a record by ID. Well, thats pretty easy no matter what mock system you use, right? Just keep a queue of the table indexed by ID, or create a looping filter on your table mock model and return the first ID match. Mission Accomplished! Let's start unit testing! oh wait....
- Retrieve a recordset by field criteria. You really can't ignore the need to retrieve records by an arbitrary property set. In the beginning you can get away with passing a hash of field/value parameters to your table model and finding all the matching records where foo = bar (and) alpha = beta [...]. You hare however limiting your system by two assumptions -- exact matches (=) and conjunctions (and).
As the system developed I allowed for array values that break down into value/comparator tuplets for other comparator (<, >) options, and modeled each comparator as a seperate function. In either case you need to allow for sorting the result by at least one field -- I used PHP usort() functions with hard coded field names in the function name (function compare_name($a, $b){..}).
- Add, Update and Delete Records. As I wanted the entire application -- not just the unit tests -- to be runnable using my mock data, I store updates to records in the session; using Zend's Session handlers I created a namespace for each table and entered mock records into the session by identity. I created a special purpose field __DELETED__ to signify erased record, and implemented a replacement switch to ensure that the latest version of a mock record was retrieved by first loading the default (ini file-based) table data, then overlaying it with session data, as part of the record retrieval process.
Building this functionality into a table abstract class (itself an extension of the Zend_DB_Table_Abstract class) , I added a seperate set of methods (find($pID), find_all($pParams), etc.) that had branching functionality. If the system configuration was set to test mode, they retrieved data from the mock sources. If it was not, the retrieval used standard Zend Framework SQL passthrough methodology.
I then built domain objects which both represented a record (by owning a table object based on my mockable extended Table class) and could serve as a "stub" that itself retrieved records (by id or search criteria) from its table, based on whether the constructor was passed an ID, or a special static flag 'STUB'.*
What We Gave Up
At this point we have tacitly "bought into" the Active Record paradigm. That is, you can't combine five records in a SQL construct and directly pull record information from this construct. You certainly couldn't unit test this process.
You can, of course, always run a query, harvest the identities, and use the identities to create Domain objects from the result. And you can recreate the effect of a complex query with a multi-step active record pattern. But you are still blowing past the problem: the actual complex SQL query itself is outside the scope of what you can unit test and is itself beyond the reach of yur test suite.
In smaller scale applications slaving yourself to pure Active Record constructs is not a huge compromise. In commercial grade applications, forsaking complex queries is simply not an option. Optimization is a real and primary requirement if you are, say, Live Nation, Digg or StumbleUpon and your database is immense and dynamic.
The only way to get a check in all boxes is to do one of two things.
- Write a flexible SQL parser
- Dump data from true SQL queries into a storeable format (JSON, INI, XML) and write unit tests on domain objects that harvest data from the storable format.
My money's on plan 2.
Fast Cheap and Easy
The point of all this is that the odds of painting yourself into a corner when establishing a data modeling and testing strategy is nearly 100%. Because of this its important to know which corner you want to end up in.
- If you are writing a small to mid-sized tool, you might not be too concerned with mainting the ability to write complex queries; buy into ActiveRecord and get your unit tests up and running.
- If you are writing a large scale commercial app (or already have one), your ability to use Active Record patterns will be limited by your need for complex SQL; build in a plan to capture complex SQL data for testing and do an assessment of how much Active Record theology you will be able to adapt as part of your test strategy.
Just keep in mind -- neither scenario gives you an excuse to skip testing. Smaller houses have smaller testing pipelines and therefore need unit tests all that much more. Larger houses have slimmer margins of error and complex codebases and therefore need unit tests all that much more.
*to be perfectly honest, you could retrieve other records from a domain object that has been initialized with record data -- its just a little more confusing to do so.
