If you heard about the term separation of concerns, you could agree this concept is very important for making a system "clean". One of the most important characteristics of a clean system is testable.
Let me tell you my story about how I've just come acrosss the design pattern Proxy.
Note: to get started, you can find a very simple example of the pattern Proxy here.
Let's start!
I have an interface as below:public interface DocumentGenerator { File generate(Document document) throws BusinessException; }
The following is my first implementation for a concrete class of DocumentGenerator.
public class DocumentGeneratorImpl implements DocumentGenerator { private Dossier dossier; private Locale locale; public DocumentGeneratorImpl(Dossier dossier, Locale locale) { validateNotNullParams(dossier, locale); this.dossier = dossier; this.locale = locale; } private void validateNotNullParams(LibertyDossier dossier, Locale locale) { if(Objects.isNull(dossier)) { throw new IllegalArgumentException("Dossier must not be null!"); } if(Objects.isNull(dossier.getDossierType())){ throw new IllegalArgumentException("Dossier type must not be null!"); } if(Objects.isNull(locale)) { throw new IllegalArgumentException("Locale must be defined!"); } } @Override public File generate(Document document, boolean temporary) throws BusinessException { setCobIdForDossierIfNotExist(); switch (dossier.getDossierType()) { case TYPE_A: // Do some logic here in case TYPE_A case TYPE_BA: // Do some logic here in case TYPE_B default: throw new BusinessException("Not supported dossier type"); } } private void setCobIdForDossierIfNotExist() { if(StringUtils.isEmpty(dossier.getCobId())) { String generatedCobId = DossierService.getInstance().generateCobId(); dossier.setCobId(generatedCobId); } } }
The client code constructs the concrete class DocumentGeneratorImpl.
DocumentGenerator generator = new DocumentGeneratorImpl(dossier, locale);
I saw that some private methods "validateNotNullParams(dossier, locale)" and "setCobIdForDossierIfNotExist()" are just a second responsibility of class DocumentGeneratorImpl.
Follow "S" of SOLID - Single Responsibility
Firstly, I was thinking about how to separate these methods into another class. Then, I create a class called DocumentGeneratorHelper which contains to these methods. It was just improved a bit.public class DocumentGeneratorImpl implements DocumentGenerator { private Dossier dossier; private Locale locale; public DocumentGeneratorImpl(Dossier dossier, Locale locale) { DocumentGeneratorHelper.validateNotNullParams(dossier, locale); this.dossier = dossier; this.locale = locale; } @Override public File generate(Document document, boolean temporary) throws BusinessException { DocumentGeneratorHelper.setCobIdForDossierIfNotExist(); switch (dossier.getDossierType()) { case TYPE_A: // Do some logic here in case TYPE_A case TYPE_BA: // Do some logic here in case TYPE_B default: throw new BusinessException("Not supported dossier type"); } } }
However, if I create tests for DocumentGeneratorImpl, I need to mock DocumentGeneratorHelper. Huh....! Any better approach?
I was thinking about that it is somehow I need to apply an Aspect-Oriented Programming (AOP) approach. As my google searching result, there are some approaches but they are quite complicated and heavy. They use Java Proxy or AOP Frameworks such as AspectJ.
A Simple AOP Approach
Fortunately, the keyword "proxy" leads me to the pattern Proxy. The idea of Proxy is really simple and cool!Okay, now I create a Proxy instead of a Helper as before.
public class DocumentGeneratorProxy implements DocumentGenerator{ private DocumentGenerator generator; private Dossier dossier; public DocumentGeneratorProxy(Dossier dossier, Locale locale) { validateNotNullParams(dossier, locale); this.dossier = dossier; this.generator = new DocumentGenerator(this.dossier, locale); } private void validateNotNullParams(Dossier dossier, Locale locale) { if(Objects.isNull(dossier)) { throw new IllegalArgumentException("Dossier must not be null!"); } if(Objects.isNull(dossier.getDossierType())){ throw new IllegalArgumentException("Dossier type must not be null!"); } if(Objects.isNull(locale)) { throw new IllegalArgumentException("Locale must be defined!"); } } @Override public File generate(Document document, boolean temporary) throws BusinessException { setCobIdForDossierIfNotExist(); return generator.generate(document, temporary); } private void setCobIdForDossierIfNotExist() { if(StringUtils.isEmpty(dossier.getCobId())) { String generatedCobId = DossierService.getInstance().generateCobId(); dossier.setCobId(generatedCobId); } } }
Done! DocumentGeneratorImpl doesn't contain its second responsibilities anymore. These methods are tested by the Proxy and we don't need to mock in DocumentGeneratorImpl.
the public class DocumentGeneratorImpl implements DocumentGenerator { private Dossier dossier; private Locale locale; public LibertyDocumentGenerator(Dossier dossier, Locale locale) { this.dossier = dossier; this.locale = locale; } @Override public File generate(Document document, boolean temporary) throws BusinessException { switch (dossier.getDossierType()) { case TYPE_A: // Do some logic here in case TYPE_A case TYPE_BA: // Do some logic here in case TYPE_B default: throw new BusinessException("Not supported dossier type"); } } }
So, instead of calling directly DocumentGeneratorImpl, we call DocumentGeneratorProxy in the client code as below:
DocumentGenerator generator = new DocumentGeneratorProxy(dossier, locale);
Happy codings!
Comments
Post a Comment