Wednesday, January 19, 2011

Applying IOC/DI to Method Design

Eventhough IOC is a generic design pattern, with Spring framework IOC/DI pattern became more popular.
We can find lot of definitions for IOC/DI over internet, but the underlying concept is same.
"Instead of component is responsible for getting the required dependencies to perform a task, a container/factory should build the dependencies and inject the dependencies into the component. Then the component can perform the sole activity for which it is responsible. The component need not care about from where it got its dependencies. Then the components code will be much more cleaner and testable".

Normally we use this principle to build and wire the services. We can also follow the same principle for method design which makes the methods testable.

Lets take a simple example of a DownloadServlet.
Suppose there is a DownloadServlet which get the message as a request parameter and write it to a file and throw it to the user as an attachment.
public class FileDownloadServlet extends HttpServlet 
{
    public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException 
    {
        String message = request.getParameter("message");
        String filename = "message.txt";
        FileWriter fw = new FileWriter();
        
        fw.write(response, message, filename );        
    }
}
public class FileWriter
{
    public static void write(HttpServletResponse response, String message, String filename) throws IOException
    {
        response.setContentType("text/plain");
        response.setHeader( "Content-Disposition", "attachment; filename=\"" + filename + "\"" );
        ServletOutputStream outputStream = response.getOutputStream();
        outputStream.write(message.getBytes());
    }
}
The above code works but it has some issues.
a) The FileWriter is tied to ServletAPI and hence can't be unit testable.
b) FileWriter.write() method is doing additional tasks in addition to writing the content to output stream.
   If you look at FileWriter.write() method, it is supposed to write the text to the given output stream only.
   But here it is preparing the output stream, which is a dependency to the method, to perform its task.
   It seems same like creating DAO instance in Service class. But IOC says DAO instance should be injected into Service by some container/factory.
  
Now let me refactor the FileDownloadServlet and FileWriter following IOC at method level.
public class FileDownloadServlet extends HttpServlet 
{
    public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException 
    {
        String message = request.getParameter("message");
        String filename = "message.txt";
        FileWriter fw = new FileWriter();
        
        response.setContentType("text/plain");
        response.setHeader( "Content-Disposition", "attachment; filename=\"" + filename + "\"" );
        OutputStream os = response.getOutputStream();
        fw.write(os, message);        
    }

}
public class FileWriter
{
    public static void write(OutputStream outputStream, String message) throws IOException
    {
        outputStream.write(message.getBytes());
    }
}
Here the OutputStream is injected to FileWriter.write() method by its caller(FileDownloadServlet). Now FileWriter.write() method need not bother about what type of file it is, what type of OutputStream it has to create. It will do its task only : writing content to output stream.

Now the FileWriter.write() method can be unit testable also.
public class FileWriterTest
{
    public static void main(String[] args) throws Exception
    {
        OutputStream outputStream = new FileOutputStream(new File("c:/message.txt"));
        String message = "Hello World!!!!!!!!!!";        
        FileWriter.write(outputStream, message);
    }
}
So in addition to wire the services of a system, we can apply IOC/DI principle to design better unit testable API also.

4 comments:

  1. Little fix
    1. fw.write(os, message, filename );
    2. fw.write(os, message);

    ReplyDelete
  2. changed to fw.write(os, message);
    :-)

    ReplyDelete
  3. Actually speaking first version of the code with Filewriter setting the response headers is totally against the single responsibility principle & makes the class FileWriter less reusable.

    ReplyDelete
  4. Telling the logic as simple as possible will be the great.

    ReplyDelete