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!