11 December 2005

As mentioned in previous article, we have a problem while testing code with 'new' operator:

public Movie createMovie(String title, Media rawContent) {
    //we can't mock 'new' operator
    Movie movie = new Movie(title, rawContent) ;
    movieDAO.save(movie);
    return movie ;
}

Have an another class MovieFactory can overcome this problem but it is overkill! Yesterday I got a idea that building a small, cheap factory like this:

public class Movie {
   //a cheap factory:
   public static Movie a = new Movie() ;

   //make constructor private, or package private:
   private Movie() {} 

   //construction method
   public Movie newInstance(String title, Media rawContent) {
       Movie movie = new Movie() ;
       movie.title = title;
       movie.content = convert(rawContent) ;
       return movie ; 
   }
   private Media convert(Media rawContent) {
       return //do some heavy calculation
   }
   //omit field...
}

Let's use this little factory:

public class MovieServieImpl implements MovieService {
   public Movie createMovie(String title, Media rawContent) {

       //the method call sound like 'a new instance' of Movie
       Movie movie = Movie.a.newInstance(title, rawContent) ;

       movieDAO.save(movie);
       return movie ;
   }
   //omit movieDAO field and setter
}

The syntax looks a little odd... though personally I am ok with this. The cheap factory 'a' is non-final, so we can replace with mock while testing:

public void testCreateMovie() {
   IMocksControl control = EasyMock.createStrictControl() ;

   //create mock:
   MovieDAO mockMovieDAO = control.createMock(MovieDAO.class);
   //use EasyMock 2.0 extension, so we can create Mock for concrete class:
   Movie mockMovieFactory = control.createMock(Movie.class);

   //prepare fixture:
   Media rawContent = Helper.createRawContent() ;
   Movie expectCreatedMovie = Helper.createMovie();

   //object under test:
   MovieServieImpl service = new MovieServiceImpl() ;

   //inject mock
   service.setMovieDAO(mockMovieDAO);

   //swap cheap Movie factory with mock. we can write a utils to help
   Movie originalMovieFactory = Movie.a ;
   Movie.a = mockMovieFactory ;

   //start recording:
   EasyMock.expect(mockMovieFactory.newInstance("title", rawContent)
        .andReturn(expectCreatedMovie);
   mockMovieDAO.save(expectCreatedMovie);
   control.replay();

   //test production code:
   Movie created = service.createMovie("title", rawContent) ;

   //verify
   control.verify();
   assertSame(expectCreatedMovie, created);
}

//remember to swap back all cheap factories, or it may affect other tests
public void tearDown() {
   CheapFactoryUtils.swapAllBack() ;
}

That's it! We replace original cheap factory - 'Movie.a' with EasyMock constructed mock object, and are free to do anything we want in Unit Test. One thing to remember -- you must restore back to original cheap factory at tearDown() or other tests will failed because 'Movie.a' is still in testing state.

Cheap factory not only help solving 'new' operator problem in testing, but also is a good place holder for injecting dependency into Domain Object. For example, we want to inject MovieDAO into all Movie instances:

public class Movie {
   public static Movie a = new Movie() ;
   private Movie() {} 

   //write a getter and delegate call to cheap factory:
   public MovieDAO getMovieDAO() {
      return a.getMovieDAO();
   }

   // a demo method that requires collaborators to work properly:
   public void save(){
      getMovieDAO().save(this);
   }
   
   public Movie newInstance(String title, Media rawContent) {//...}
   //omit rest...
}

If we do not inject movieDAO into cheap factory 'a', calling getMovieDAO() will cause stack overflow. Thus we must inject movieDAO at runtime. Spring support method-injection since 1.1:

<bean id="cheapMovieFactory" class="com.foo.Movie">
   <lookup-method name="getMovieDAO" bean="hibernateMovieDao" />
</bean>

Remember adding cglib.jar into your classpath since <lookup-method> use cglib to intercept method call on getMovieDAO(). Now we have an dao-injected bean 'cheapMovieFactory'. Next, we need to swap orignal 'Movie.a' to cheapMovieFactory bean. A CheapFactoryInjector can help this:

//Implements InitializingBean so swap happened after spring create this bean
public class CheapFactoryInjector implements InitializingBean{

    //specify which domain beans need to swap cheap factory:
    private List beans;
    public void setBeans(List beans) {
        this.beans = beans;
    }

    //define field name of cheap factory, default to "a"
    private String cheapFactoryName = "a";
    public void setCheapFactoryName(String cheapFactoryName) {
        this.cheapFactoryName = cheapFactoryName;
    }

    public void afterPropertiesSet() throws Exception {
        //swap all cheapFactory by reflection:
        for (Object bean : beans) {
            Field cheapDomainFactory = bean.getClass().getField(
                cheapFactoryName);
            cheapDomainFactory.setAccessible(true);
            cheapDomainFactory.set(null, bean);
        }
    }
}

and configures in spring like this:

<bean class="com.foo.CheapFactoryInjector" lazy-init="false">
   <!-- specify which domain classes need to be swap:-->
   <property name="beans">
	<list>
	  <ref bean="cheapMovieFactory"/>
	  <ref bean="cheapFooFactory"/>
	  <ref bean="cheapBarFactory"/>
	</list>
   </property>
   <property name="cheapFactoryName" value="a" />
</bean>

That's all! No matter newly created domain object or loaded from O-R mapper. All domain objects can do things like movie.save() everywhere !

Summary:

pros

  • Cheap factory is small and use its own class as factory. There is no need to create a FooFactory class just for testing.
  • Building Cheap factory is simple, just making constructor private and declare a 'public static DomainObject a = new DomainObject();'. And constructing objects via instance method like 'public DomainObject newInstance()'
  • As Effective Java said, Factory Method is always better than Constructor. This also applies to cheap factory, we can do things like 'Movie.a.newAvi()' or 'Movie.a.newDivx()' ... etc.
  • Cheap factory is swappable, this makes testing much easier.
  • Cheap factory can be a injection point for domain objects: Just delegating method call to cheap factory like -- 'public DAO getDAO(){return a.getDAO(); }' and configure CheapFactoryInjector properly then all domain objects are injected. Domain Object is still a pure POJO, no interface/subclass/utils required.
  • For service facade, which is singleton in nature, setter injection is prefered. But for Domain Object, method injection is perfered because Domain Object ususally requires collaborators with different life cycle.
  • Since cheap factory use method-injection, there is no serialization issue when putting domain object into things like httpSession.

cons

  • Syntax such as 'Movie.a.newInstance()' looks odd, and it's not a coding standard, either.
  • Cheap factory is non-final, this causes domain object in corrupted state if someone try to alter it accidently. Hopefully, since the syntax is strange enough, altering cheap factory like 'Movie.a = null;' should rarely happen.
  • Injection affects all Domain Objects, not per object.

回響

可以用 Tag <I>、<B>,程式碼請用 <PRE>