Sunday, February 07, 2010

The right collaboration

Classes collaborate with each other to deliver their responsibility. In doing so, classes must make the least assumptions about the other classes. Too few assumptions, the collaboration will not be useful. Too many assumptions, the collaboration becomes a pack of cards (changes on one class will break the other class). More collaboration assumptions made is more coupling.

Interfaces help a given class to explicitly specify the assumptions that the clients can safely make when collaborating with that class. This set of assumptions must be balanced to be useful enough but not too detailed.

For example consider a StudentController class (a application UI class) that collaborates with JDBCStudentDAO class.

Class StudentController
{
JDBCStudentDAO dao = new JDBCStudentDAO();
public Student saveStudentUIAction(Student student)
{
return dao.jdbcSave(student);
}
}

In this case, StudentController makes assumptions about the implementation details of JDBCStudentDAO and public methods exposed by the implementation. This has the following disadvantages:
1. Any change in JDBCStundetDAO that violates any assumption made by StudentController will break StudentController.
2. Consider a HibernateStudentDAO which does *not* have the same public methods as JDBCStudentDAO (say HibernateStudentDAO only has a hibernateSave(Student) method and not a jdbcSave(Student)). Then replacing JDBCStudentDAO with HibernateStudentDAO will
a. Take more time to change StundentController
b. Cause more regression (every implementation assumption made by StudentController on JDBCStudentController that is violated by HibernateStundentController will lead to regression)

The solution: let StudentController reference a StudentDAO (an interface that explicitly states the assumptions that clients like StudentController can make). So we have

class StudentController
{
StudentDAO dao = new JDBCStudentDAO();
public Student saveStudentUIAction(Student student)
{
return dao.save(student); // Implemented by JDBCStudentDAO & HibernateStudentDAO
}
}

This solution solves ton of issues but not all of them. StudentController is still statically bound to JDBCStudentDAO.

To unit test StudentController let us suppose we write the following StudentControllerTest

class StudentControllerTest
{
public testSaveStudentUIAction()
{
Student student = new Student("Johnson");
Student savedStudent = studentController.saveStudentUIAction(student);
assertNotNull(savedStudent.getKey());
}
}

Let us assume that saveStudentUIAction can return null & throw a DuplicateStudentException. To simulate this scenario, JDBCStudentDAO must be exercised. But that is a test appropriate to JDBCStudentDAOTest, *not* StudentControllerTest. In short, StudentControllerTest must exercise and test only StudentController and not the classes that StudentController is dependent on. To achieve this, StudentController should be made to interface with a stub implementation of StudentDAO in the test cases and an actual implementation (like JDBCStudentDAO or HibernateStudentDAO) in the application. So StudentController should must not instantiate JDBCStudentController. But a implementation of StudentDAO must be handed over to it. This handing over of StudentDAO implementation can be achieved through Spring IOC (Inversion of control) or dependency injection.

So we now have

class StudentController
{
StudentDAO dao;
public Student saveStudentUIAction(Student student)
{
return dao.save(student); // Implemented by JDBCStudentDAO & HibernateStudentDAO
}
// This setter is invoked by spring IOC with stub implementation (that
// does what the test case wants to exercise) in test cases and
// actual implementation in the application
public void setDao(StudentDAO studentDAO)
{
dao = studentDAO;
}
public void getDao()
{
return dao;
}
}

No comments: