February 2005
Inversion of Control (IOC) through injection also known as Injection IOC has
not been a designer or programmer friendly pattern. Many question its
validity and the validity of IOC in general. IOC seems to be a
contradiction to the fundamental concept of object encapsulation. Context
IOC is a new approach that attempts to capture Inversion of Control as a
pure design pattern to demonstrate that IOC is indeed a very powerful
concept.
Overview of IOC
Inversion of Control (IOC) is a new pattern that has been gaining popularity
recently. The pattern is based on a few simple concepts that deliver a
highly decoupled, lightweight, mobile, and unit-testable code base. The
core concept behind IOC is that an object exposes its dependencies via some
contract. Dependencies include such things as object implementations,
system resources etc.., essentially anything that an object needs to perform
its designated function but is not concerned with its implementation. In a
nested object graph, each object in the call chain exposes its dependencies
to the outer caller that uses it, who in turn exposes those dependencies
including any of its own to its caller and so on -- until all dependencies
manifest itself at the top. The top-object then assembles the dependency
graph before activating the objects. The top-object is generally an entry
point into your system such as an application main, Servlet, or even an EJB.
Validity of IOC
IOC seems to be at odds with the fundamental paradigm of object
encapsulation. The concept of upstream propagation of dependencies is
indeed a contradiction to encapsulation. Encapsulation would dictate that a
caller should know nothing of its helper objects and how it functions. What
the helper objects do and need is their business as long as they adhere to
their interface - they can grab what they need directly whether it be
looking up EJBs, database connections, queue connections or opening sockets,
files etc.. Of course, we are now discovering that this strict
interpretation of encapsulation is in itself invalid. It forces us to come
up with contrived solutions for managing functionality and resources across
object boundaries, jumping through all sorts of hoops - such as managing
thread locals for transactions, security, user info, caching etc.. Whether
these hoops are jumped for us by an EJB container or contrived within the
application - we have to come to terms with the fact that the paradigm of
encapsulation has to be eased up a bit. Encapsulation has to be eased up to
the point where any dependency that is managed or has the potential to be
managed across object boundaries must be exposed for higher management. Not
much different from a corporation where an employee's purchase request for a
PC propagates up though management. What should be exposed is not just
limited to usages of managed resources but include usages of managed
functionality as well. Therefore, the upstream propagation of dependencies
to a top-object via IOC is indeed valid. The top-object knows its
environment - how to obtain and manage shared resources, configurations and
functionality for a usecase. In essence, we need a paradigm shift from
strict encapsulation to exposing of managed objects via IOC. Encapsulation
is still an important concept and easing it a little by no way means
throwing it out the window. Besides cross boundary managed objects there is
an additional driving force toward IOC, namely isolation of concerns. This
is an old object-oriented mantra that is delivered to its full potential
with IOC. Objects can focus on intended functionality and leave
implementations of unintended concerns to higher authorities via IOC.
Dimensions of IOC
There are 2 dimensions along which IOC can be applied within your
application. The first dimension is a horizontal or breadth-wise dimension
where at the extreme you would have all objects decoupled from each other
and the top-object assembles a highly complex object graph and manages the
life-cycle of all objects. The second dimension is a vertical or depth-wise
dimension where at the extreme you would have objects highly coupled except
for the most heavyweight of resource usages which will be assembled by the
top-object. As usual the best design is somewhere in between where most
objects are decoupled, but then there are also intermediary objects that
assemble a part of the object graph to provide concrete and encapsulated
units - much like a corporation with multiple layers of management leading
up to the CEO.
Benefits of IOC
The benefits of IOC are that objects become highly focused in its
functionality, highly reusable, and most of all highly unit testable. Cross
boundary management of resources and functionality becomes straightforward.
Objects are not coupled directly to environment resources or other
unintended implementations. A unit test for an object can easily mock up
its dependencies and then unit test that object's logic explicitly in
isolation. These tests are easier to write and lightening fast. Most unit
tests would NOT require you to mock up database connections and JNDI trees
which can result in slow and cumbersome tests. Mind you, mocking up
database connections have their place in integration tests - but not for the
90% of logic there is to unit test.
Injection IOC
Injection IOC is a popular approach to IOC that advocates that an object
expose its dependencies via its constructor or Java bean properties. This
popular use of Injection IOC also includes a generic container that manages
the overall object graph and the injection of dependencies when an object's
life-cycle begins. I refer to the container as generic because it can be
used with any application as it reads your object graph from a descriptor
file and then proceeds to manage it.
Problems with Injection IOC
The problem with exposing dependencies via constructors or Java bean
properties in general is that it clutters an object and overwhelms the real
functional purposes of the constructor and Java bean properties. It also
forces the caller to perform a lot of assembly i.e glue work which cannot be
easily captured, reused and extended. Generic containers attempt to capture
the glue work in descriptor files such as XML that are loaded and assembled
by the container. They gear applications toward an extreme horizontal
dimension of IOC with superfluous decoupling and complex object graphs
exposed as untyped descriptor files which are not easily extensible and do
not provide any compile time verification. Of course externalizing
environmental dependencies such as resource bindings etc.. into XML
deployment descriptors as is done with J2EE containers is another matter,
this type of externalizing is necessary, but externalizing an entire object
graph seems over ambitious and counter productive. This type of glue work
is considered core functionality and may be subject to business requirements
and testing procedures and exposing it as an untyped descriptor file can
make the application very brittle. Finally, this core functionality type
glue work should be reusable and extensible just like any other part of the
application that represents the business domain.
Context IOC
Injection IOC, even though a step forward in design, does not do IOC
complete justice for the reasons I have already mentioned. The concept of a
generic container managing your core usecase flow diminishes IOC as a design
pattern decreasing its accessibility for general purpose use - after all
design patterns should only need language expression. Additionally, people
are thrown off by the injection clutter and most of all by the "perception"
that IOC implies ZERO encapsulation.
Context IOC on the other hand is Inversion of Control at its purest - just a
pattern for design without any baggage. It eliminates clutter and promotes
encapsulation. It provides a clean way to organize an object's contextual
dependencies in the form of a Context interface declared within its class.
Inversion of Control is then further expressed through Context Extension.
Example-1 : Pool Game
The PoolGame class expresses its dependencies via an inner interface called
a Context and then takes an instance of the Context in its constructor. The
class performs its encapsulated logic to play a pool game but delegates to
its Context functionality such as draw() whose implementations do not
concern the PoolGame. Note that later refactoring of the Context interface
may move some of the Context's methods to other interfaces - one of the
benefits of using Context IOC as seen later.
public class PoolGame {
public interface Context {
public String getApplicationProperty(String key);
public void draw(Point point, Drawable drawable);
public void savePoolGame(String name, PoolGameState poolGameState);
public PoolGameState findPoolGame(String name);
}
public PoolGame(Context cxt) {
this.context = cxt;
}
public void play() {
//start play
}
public void endPlay() {
//end play
}
public void savePlay(String name) {
PoolGameState saveGameState = new PoolGameState(this.gameState);
context.savePoolGame(name, saveGameState);
}
public void play(String savedPoolGameName) {
PoolGameState savedGameState = context.findPoolGame(savedPoolGameName);
gameState = new PoolGameState(savedGameState);
play();
}
private final Context context;
private PoolGameState gameState;
}
The Context interface removes the clutter of exposing dependencies via
constructors or Java bean properties (except Of course for the one context
argument). It provides better clarity by organizing an object's
dependencies into a single personalized interface unique to that class. The
PoolGame.Context interface is unique to the PoolGame class. Now constructor
arguments and properties can be reserved for what they are meant for i.e.
varying an object's usable functionality, such as a gameType property that
could control the type of game that is played such as 9-ball or Straight
Pool.
Context Extension
Inversion of Control is expressed further through Context Extension.
Contexts can extend other Contexts. A caller must implement its helper's
Context or pass the contract further upstream by providing its own Context
interface that extends its helper's Context. The caller then adds
contextual dependencies of its own to its Context. In essence the caller is
stating that its contextual needs include everything from its helper as well
as some of its own. This approach of extending Contexts to propagate
dependencies is perfectly valid between objects that collaborate with each
other e.g. services and their helpers. The alternative approach would be
for the caller to declare a dependency to the callee's interface within its
Context. This decouples the caller from callee's implementation. The top
assembler then implements both the caller and callee's Contexts and chooses
the correct implementation of the callee for the caller. This approach
would be used to decouple large grained objects and resource consumers from
each other such as services from other services. In essence Context
Extension is used by intermediary assemblers that represent a concrete
encapsulated large grained functional unit. They glue together concrete
implementations of helper objects to achieve this large grained
functionality and then propagate upstream all dependencies of its helpers
via Context Extension. These large grained units are just as unit testable
as its helpers and should have their own detailed unit tests that show their
assembled functionality is in proper order. Its also important to note that
in addition to assembling concrete helpers, these large grained units will
often add more business logic in and around the assembly work.
Propagating dependencies with Injection IOC via constructors or Java bean
properties forces modification to every class in the call-chain(s) to
accommodate new dependencies unless you implement an extreme horizontal
dimension of IOC which in turn would increase the clutter within each
object. Introducing or modifying new dependencies with Context Extension
impacts only the top-assembler's class(s) that assemble the large grained
objects - since it is only they that implement the Contexts. Modifications
to an object's Context will propagate upstream to the top via Context
Extension without affecting intermediary classes in the call-chain(s).
Context interfaces provide a great way to abstract out dependencies into its
own object hierarchy and design allowing for reuse and extensibility.
Methods in a Context that represent similar issues can also be refactored
into its own interface - the Context can then either extend this new
interface or provide a getter to it. Even more importantly, implementations
of Contexts i.e. glue code can be generalized and abstracted out for reuse
and extension. Implementations of Contexts are generally provided by
top-level classes i.e. usecase-entry points. Context implementations in
whole or in parts can be abstracted out and shared across multiple usecases
that have similar environments or requirements. Additionally, refactoring
of Context implementations will identify configurables that should truly
reside externally as application or deployment properties. Application
properties typically being stored in a database for administrative
applications while deployment properties residing in XML or properties files
for the deployers.
In the end, all of this application glue is captured in extensible Contexts
that are compile-time verified. Introduce or modify a dependency in an
object's Context and a compile will verify all usecases that use that object
and consequently implement some version of its contextual dependencies. On
the other hand, generic IOC containers that read in object graphs from a
descriptor and claim maximum configurability do not provide true ROI - they
have captured essential application glue in an untyped descriptor file that
cannot be understood or modified by application administrators or deployers.
Furthermore, changes in the glue descriptors can modify core usecase flows
and should go through proper testing before being released, therefore, it
should not be as easily modifiable external to the application.
Example-2 : Matching jobs
The usecase of matching jobs to a candidate demonstrates the use of Context
IOC, its integration with existing patterns to fulfill functionality, and
finally how it is applied in enterprise deployment.
Service pattern with Context IOC
The Service pattern is used to represent and group usecases. Each method
defined in a Service interface represents a usecase activated by the user or
the system. It generally also represents a transaction of one or more
resources. The Service's business functionality is implemented as a plain
Java object and represents a large grained functional unit. It propagates
upstream contextual needs of its helpers via Context Extension. Note that
this unit adds business functionality in and around its assembly of its
helpers and and so declares some contextual needs of its own.
/**
* The Service interface for matching Jobs.
*/
public interface JobMatchService {
/**
* Represents the usecase for matching jobs to a candidate.
* @return Set : The set of Job objects.
*/
public Set match(CandidateKey candidateKey);
//...other usecase methods
}
/**
* The JobMatchService's business logic in a plain Java object.
*/
public class JobMatchServiceImpl
implements JobMatchService
{
/**
* Declares what this impl needs in its Context interface.
* In addition to defining its own needs in its Context, it extends
* Contexts of helper classes that it uses, so as to propagate all
* contextual needs upstream.
*/
public interface Context
extends MatchStrategyFactory.Context
{
public CandidateStore getCandidateStore();
... //other methods
}
/**
* Note that the provided Context argument can be passed along as is to
* all helper objects since the Context implements all their Contexts as
well.
*/
public JobMatchServiceImpl(Context cxt) {
this.context = cxt;
this.matchStrategyFactory = new MatchStrategyFactory(cxt);
}
/**
* Contains all business logic for the job matching usecase.
*/
public Set match(CandidateKey candidateKey)
throws CandidateNotValidException
{
Candidate candidate =
context.getCandidateStore().findCandidate(candidateKey);
if (candidate == null || candidate.isRegistrationExpired()) {
throw new CandidateNotValidException(candidateKey);
}
MatchStrategy strategy = matchStrategyFactory.createStrategy(candidate);
Set jobs = strategy.match(candidate);
candidate.setLastMatchedDate(new Date());
candidate.setMatchStrategyUsed(strategy.getName());
context.getCandidateStore().update(candidate);
return jobs;
}
//...other usecase method implemenations
private final Context context;
private final MatchStrategyFactory matchStrategyFactory;
}
Strategy pattern with Context IOC (Example-2 continued)
The strategy pattern is used to determine the appropriate matching
algorithm to apply under different circumstances. A factory creates the
appropriate strategy and uses its Context and Context Extension to propagate
requirements of the individual strategies. In this example we have a
predetermined number of strategies and therefore each strategy can provide
their own Contexts which are combined and propagated upstream via the
factory's Context. However, in the cases where strategies need to be added
dynamically - a single Context can be defined in a base strategy class that
all sub-classes will need.
/**
* This factory creates the appropriate matching strategy for a given
candidate.
*/
public class MatchStrategyFactory {
/**
* Being a factory for all the strategies - it propagates all the
* contextual needs of the underlying strategies it creates.
*/
public interface Context
extends JobMatchConfig.Context, QuickMatchStrategy.Context,
SinglePhaseMatchStrategy.Context, TwoPhaseMatchStrategy.Context,
MaximumPhaseMatchStrategy.Context
{
//...no methods since no contextual needs of its own.
}
public MatchStrategyFactory(Context cxt) {
this.context = cxt;
config = new JobMatchConfig(cxt);
}
/**
* Create the strategy appropriate for the given candidate.
* Note that the provided Context argument can be passed along as is to
* all strategy objects since the Context implements all their Contexts
* as well.
*/
public MatchStrategy createStrategy(Candidate candidate) {
//note: config was initialized in the constructor above...
if (candidate.daysOnSearch() < config.getDaysOnSearchLow()) {
strategy = new QuickMatchStrategy(context);
}
else if (candidate.daysOnSearch() < config.getDaysOnSearchMedium()) {
strategy = new SinglePhaseMatchStrategy(context);
}
else if (candidate.daysOnSearch() < config.getDaysOnSearchHigh()) {
strategy = new TwoPhaseMatchStrategy(context);
}
else{
strategy = new MaximumPhaseMatchStrategy(context);
}
return strategy;
}
private final Context context;
private final JobMatchConfig config;
}
/**
* Represents a strategy to match Jobs for a candidate.
*/
public interface MatchStrategy {
public String getName();
public Set match(Candidate candidate);
}
/**
* A Quick and dirty implementation of a MatchStrategy.
*/
public class QuickMatchStrategy
implements MatchStrategy
{
/**
* Context declaring that it needs the Read Only version
* of the Job Persistent Store.
*
* Context vs. Constructor
* The advantage to placing things in the Context vs. Constructor
* are that this object's users need not be affected by what the object
* needs in order to implement itself.
*/
public interface Context {
public JobStore getJobStoreReadable();
}
/**
* Resides in same package as MatchStrategyFactory and so only provides
* package visibility for constructor.
*/
QuickMatchStrategy(Context cxt) {
this.context = cxt;
}
/**
* Quick and dirty algorithm...
* Finds jobs based on top skill only.
*/
public Set match(Candidate candidate) {
Set jobs =
context.getJobStoreReadable().findJobsBySkill(candidate.getTopSkill());
Iterator jobsIter = jobs.iterator();
while(jobsIter.hasNext()) {
Job job = (Job)jobsIter.next();
if (job.yearsOfExperience() > candidate.yearsOfExperience()) {
if (job.salary() < candidate.salary()) {
jobsIter.remove();
}
}
}
return jobs;
}
private final Context context;
}
Config pattern with Context IOC (Example-2 continued)
A simple pattern to access configuration within an application.
Configuration is grouped by concern into classes which then provide an
encapsulated well typed interface to individual properties. The Config
pattern uses Context IOC to access application properties.
/**
* A helper to read and translate properties used for JobMatch.
*/
public class JobMatchConfig {
/**
* Context declaring that it needs an application property.
* Which may be stored in a Database or URL or local file-system
* as determined by the top usecase assembler.
*
* Context vs. Constructor
* The advantage to placing things in the Context vs. Constructor
* are that its callers can easily pass on the implementation
* requirements through Context Extension.
*/
public interface Context {
public String getApplicationProperty(String key);
}
public JobMatchConfig(Context cxt) {
this.context = cxt;
}
public int getDaysOnSearchLow() {
String key = "job.match.days.on.search.low";
String value = context.getApplicationProperty(key);
try {
return Integer.parseInt(value)
}catch(NumberFormatException x){
throw new IllegalConfigException(key, value, x);
}
}
public int getDaysOnSearchMedium() {
//...similar to others
}
public int getDaysOnSearchHigh() {
//...similar to others
}
... //other methods
private final Context context;
}
Store pattern with Context IOC (Example-2 continued)
The Store pattern is used for accessing persistent data. A common misuse of
IOC is when business objects access DataSources for their specific data
needs. They may have used IOC to access the DataSources which is partly
good but misses the point. These business objects are not as reusable and
not as easily unit testable - since unit testing them means mocking up
DataSources for them. These objects should instead declare and expose their
dependency to the data they need - allowing the top-level usecase assembler
to decipher where it comes from. A good design would be to have these
business objects declare a dependency to a Store interface that gives them a
specific type of persistent data entity they need. This allows easy unit
testing of these business objects since mocking up a Store and its mock data
becomes easy and more importantly lightening fast. In general, unit
testability of an object is a good measuring stick for good IOC designs.
The CandidateStore follows a similar pattern as shown.
/**
* Interface that identifies complete read / write to the
* persistent Store for Job data.
*/
public interface JobStore
extends JobStoreReadable
{
public Job add(Job job);
public Job update(Job job);
public void addSkills(JobKey jobKey, Set skills);
... //other methods
}
/**
* Interface that identifies only the Readable interface to the
* Job Persistent Store.
*
* Allows top-objects to optimize transactions if a usecase
* flow requires only Readable Stores.
*/
public interface JobStoreReadable {
public Job findJob(JobKey key);
public Set findJobsBySkills(Set skills);
... //other methods
}
/**
* The implementation of reading and writing to the Job Persistent Store.
*/
public class JobStoreImpl
implements JobStore
{
/**
* Context declares that it needs a JDBC Connection to the Database.
* This approach removes the burden/errors of Connection closing from all the objects.
* The burden is solely on the top-level usecase assembler to provideand then close the
* connection for a given usecase. Allows top-level usecase assembler
to reuse
* Connections etc.. (See alternatives next).
*
* Alternative: Can declare DataSource instead of Connection.
* To allow for a more fine grained connection usage - but burden/errors
of Connection
* closing is left to each StoreImpl to handle themselves.
*
* Alternative: Can declare a HibernateSession instead of Connection.
* This approach removes the burden/errors of Connection and
HibernateSession closing from
* all the objects. The burden is left solely on the top-level assembler
to provide the
* HibernateSession and then close the Session and Connection for a
given usecase.
* Allows top-level usecase assembler to cache/reuse Sessions, reuse
Connections etc..
*
* Alternative: Can accept DataSource/Connection/HibernateSession via
Constructor.
* This is Not as extendable. With a Context, callers can easily extend
and pass on the
* responsibility higher without really being affected by what the
StoreImpl needs.
* Also, the top-object need only implement this method once for all
StoreImpls used in
* the usecase and lazily create the connection.
*
* Optional: If multiple DataSources exist in your application or you'd
like to accommodate
* such a possibility. You can clarify getConnection() to specify a
logical datasource name:
* as getConnection(String datasource)
* allowing the top-level usecase assembler to assemble the correct
Connection from the
* correct DataSource.
*/
public interface Context {
public java.sql.Connection getConnection();
}
public JobStoreImpl(Context cxt) {
this.context = cxt;
}
public Job findJob(JobKey key) {
... //use Connection from context to find
}
public Set findJobsBySkills(Set skillset) {
... //use Connection from context to find
}
public Job add(Job job) {
... //use Connection from context to add
}
public Job update(Job job) {
... //use Connection from context to update
}
public void addSkills(JobKey jobKey, Set skills) {
... //use Connection from context to add skills.
}
... //other methods
private final Context context;
}
Enterprise deployment with Context IOC (Example-2 continued)
Once a Service and its ServiceImpl(s) with its usecase methods and its
business logic have been built using Context IOC, it can be plugged-into any
environment. If you were to deploy on a J2EE server - you may choose to
wrap your Service within an EJB or a Servlet depending on your
auto-management needs for security, transaction, resources etc. If you
choose to deploy it as part of a standard main() or batch type application
then you may wrap your Service in an Application class. I like to think of
EJBs, Servlets etc.. as merely entry points into a system that act as
top-level usecase assemblers. They essentially take a Service and adapt its
contextual needs to the environment that the entry point represents. The
assemblers merely fulfilll the Service's Context and then delegate to the
Service's usecase method to perform all the functionality associated with
the usecase.
/**
* A Stateless SessionBean deployment of the JobMatchService.
* This EJB uses container-managed security and transactions.
*/
public class JobMatchServiceEJB
implements SessionBean, JobMatchService
{
/**
* Assembles the JobMatchService's Context to perform the job match
usecase.
* Treated as just an entry point for a type of environment - it knows how
to provide
* security, manage transactions, and access the resources needed by the
useacase service.
*/
public Set match(CandidateKey candidateKey)
throws CandidateNotValidException
{
ServiceContext context = new ServiceContext(); //see private class
later.
try {
JobMatchService service = new JobMatchServiceImpl(context);
return service.match(candidateKey);
}finally{
context.destroy();
}
}
/**
* A private implementation of the Service's Context used only by this
EJB.
* If several EJB's need to share this Context or parts of it - it can be
* extracted out and refactored for reuse.
*
* Thread safety:
* The ServiceContext holds state (for lazy initialization etc..) but
thread
* synchronization is not needed since its created and executed inside
* the EJB's method.
*/
private class ServiceContext
implements JobMatchServiceImpl.Context, CandidateStoreImpl.Context,
JobStoreImpl.Context, AppConfigStoreImpl.Context
{
public CandidateStore getCandidateStore() {
return new CandidateStoreImpl(this); //can be optimized as one
instance.
}
public JobStoreReadable getJobStoreReadable() {
return new JobStoreImpl(this); //can be optimized as one instance.
}
/**
* Obtains application properties from the database.
* Can be modified to say read from a URL.
*/
public String getApplicationProperty(String key) {
//can be refactored for longer term caching.
if (appConfig == null) {
AppConfigStore configStore = new AppConfigStoreImpl(this);
appConfig = configStore.findAppConfig("JobMatch");
}
return appConfig.getProperty(key);
}
/**
* Used by all the StoreImpl classes that are part of a usecase.
*
* Only one instance of the Connection created and used for the single
* execution of a usecase. This approach provides a one place easy
management
* of a Connection for an entire usecase and is sufficient for most
scenarios.
* The individual StoreImpls don't need to worry about closing
Connections.
* Resource leaks are avoided with this approach.
*
* Alternative: If usecase has a lot of processing between Connection
usage its
* Context should request a DataSource instead and manage its own
Connection
* life-cycle.
*/
public Connection getConnection() {
try {
if (conn == null) {
DataSource ds = (DataSource)(new
InitialConext()).lookup("jdbc/job");
conn = ds.getConnection();
}
return conn;
}catch(Exception x){
throw new InvalidStateException(x);
}
}
/**
* Clean up resources used by this Context.
*/
public void destroy() {
try {
if (conn != null) {
conn.close();
}
}catch(Exception x){
log.warn(x);
}
}
private Connection conn = null;
private AppConfig appConfig = null;
}
}
Unit tests with Context IOC (Example-2 continued)
Unit tests are merely another entry point into a system - more specifically
a test system, and as such adapts a Service's contextual needs to the test
system by mocking up implementations. JUnits can not only mock up the
Service's Context but also the Contexts of any of its helpers or utilities.
I prefer to create concrete mocks that implement the various Contexts or
Stores directly rather than use the generic mock libraries that are freely
available - but either will work. The concrete mocks look like actual
classes that can themselves be generalized, analyzed and refactored for
maximum reuse of your test data within your test system.
public class JobMatchServiceImplTest
extends TestCase
{
/**
* A simple test for the job match service.
*/
public void testMatch() {
TestContext testContext = new TestContext(); //see private class later
JobMatchServiceImpl jobMatchService = new
JobMatchServiceImpl(testContext);
CandidateKey candidateKey = new TestCandidateKey("Sony");
Set jobs = jobMatchService.match(candidateKey);
assertNotNull(jobs);
assertEquals(5, jobs.size());
assertEquals(1, testContext.candidateStore.updateCount());
//...other asserts.
}
/**
* A private test context implementation used by this Test only.
* But can be refactored for reuse by many tests.
*/
private class TestContext
implements JobMatchServiceImpl.Context
{
final TestCandidateStore candidateStore = new TestCandidateStore();
final TestJobStore jobStore = new TestJobStore();
public CandidateStore getCandidateStore() {
return candidateStore;
}
public JobStoreReadable getJobStoreReadable() {
return jobStore;
}
public String getApplicationProperty(String key) {
if (key.equals("job.match.days.on.search.low") {
return "7";
}else if (key.equals("job.match.days.on.search.medium") {
return "14";
}else if (key.equals("job.match.days.on.search.high") {
return "25";
}else{
throw new IllegalStateException("Bad application property
requested " + key);
}
}
public Connection getConnection() {
throw new IllegalStateException("Connection request invalid in a
test case");
}
}
}
/**
* An e.g. of a mock Store used by many tests.
*
* A test case can choose to create its own test Store
* and assert directly in its methods OR use a shared test
* Store which can later be inspected and asserted on.
*/
public class TestCandidateStore implements CandidateStore {
private final Collection candidates = new ArrayList(100);
public TestCandidateStore() {
add(new Candidate("Sony"));
add(new Candidate("Roney"));
add(new Candidate("James"));
//... more data.
}
public void add(Candidate candidate) {
candidates.add(candidate);
//... add to various indexed maps
//... for the find methods.
}
public Job findCandidate(CanidateKey key) {
//lookup an indexed map to find.
}
public void update(Candidate candidate) {
//lookup an indexed map to find.
//then update the object or throw exception if not found.
//track updates for later inspection
}
/**
* E.g. method that allows inspection of Store to assert on.
*/
public int updateCount() {
//return count from update list.
}
//... other methods
}
Finally
Inversion of Control(IOC) is indeed a valid concept and the old fogies of
OO design must come to terms with this natural evolution towards IOC. The concept
of strict encapsulation must be eased to allow for cross boundary managed resources
and functionality via IOC. Unit testability of isolated concerns as delivered
by IOC is also a major leap forward in providing quality software. Injection
IOC though a step forward in design does not deliver on IOC's full potential
and Context IOC attempts to deliver where Injection IOC falls short - as a general
purpose design pattern.
Author Bio
Sony Mathew
Lead Architect, Prime Therapeutics, MN 55121
smathew@primetherapeutics.com
Sony Mathew is the Lead Architect at Prime Therapeutics located in Minnesota,
formulating the direction of its enterprise applications comprised primarily
of delivering pharmacy products and services through web-sites and through direct
B2B integration with web-services. He has about 6 years of experience developing
and designing J2EE applications.
PRINTER FRIENDLY VERSION
|