القائمة الرئيسية

الصفحات

Make Our Code More Testable with Proxy Design Pattern


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