The test automation code should be as professional as the application code.
It should be built using the same principles.
Such as “program to an interface” and “extend, dont change”.
How do you build it so that it follows such principles?
Let’s imagine that we have a class that formats a phone number with spaces between the area code, prefix and the line number.
For a phone number like 6143567891, the PhoneNumber class displays it as 614 356 7891.
import java.util.ArrayList; import java.util.List; public class PhoneNumber { private List<Integer> digits = new ArrayList<>(); public PhoneNumber(List<Integer> digits) { if (digits.size() != 10) throw new RuntimeException("there are less than 10 digits!"); this.digits = digits; } public List<Integer> getDigits() { return this.digits; } public String getValue() { return String.format("%s %s %s", areaCode(), prefix(), lineNumber()); } public String areaCode() { return String.format("%d%d%d", digits.get(0), digits.get(1), digits.get(2)); } public String prefix() { return String.format("%d%d%d", digits.get(3), digits.get(4), digits.get(5)); } public String lineNumber() { return String.format("%d%d%d%d", digits.get(6), digits.get(7), digits.get(8), digits.get(9)); } @Override public String toString() { return this.digits.toString(); } }
The following test displays a phone number formatted properly:
import java.util.Arrays; import java.util.List; import org.testng.annotations.Test; public class TestClass { private final static List<Integer> TEN_DIGITS = Arrays.asList(4, 6, 1, 2, 3, 2, 6, 7, 4, 5); @Test public void testNormalPhone() { PhoneNumber phone = new PhoneNumber(TEN_DIGITS); System.out.println(phone.getValue()); } }
For a while, this phone template is the only template needed for the project.
Multiple classes use objects of the PhoneNumber class.
But things change and various other formats become useful such as:
(614) 356 7891 : the area code is in ()
614-356-7891 : the area code, prefix and line number are separated by –
604 356 7891 : the 604 prefix is used
778 356 7891 : the 778 prefix is used
How can the additional templates be implemented?
You may choose to make changes to the PhoneNumber class for the additional templates.
Since there is already a getValue() method used in many other classes, you leave it as is and create additional methods, one for each additional template:
import java.util.ArrayList; import java.util.List;
public class PhoneNumber { private List<Integer> digits = new ArrayList<>(); public PhoneNumber(List<Integer> digits) { if (digits.size() != 10) throw new RuntimeException("there are less than 10 digits!"); this.digits = digits; } public List<Integer> getDigits() { return this.digits; } public String getValue() { return String.format("%s %s %s", areaCode(), prefix(), lineNumber()); }
public String getValueWithParanthesis() { return String.format("(%s) %s %s", areaCode(), prefix(), lineNumber()); } public String getValueWithHyphens() { return String.format("%s-%s-%s", areaCode(), prefix(), lineNumber()); } public String getValueWith604Prefix() { return String.format("604 %s %s", prefix(), lineNumber()); }
public String getValueWith778Prefix() { return String.format("778 %s %s", prefix(), lineNumber()); } public String areaCode() { return String.format("%d%d%d", digits.get(0), digits.get(1), digits.get(2)); } public String prefix() { return String.format("%d%d%d", digits.get(3), digits.get(4), digits.get(5)); } public String lineNumber() { return String.format("%d%d%d%d", digits.get(6), digits.get(7), digits.get(8), digits.get(9)); } @Override public String toString() { return this.digits.toString(); } }
The test displays all possible formats:
import java.util.Arrays; import java.util.List;
import org.testng.annotations.Test;
public class TestClass {
private final static List<Integer> TEN_DIGITS = Arrays.asList(4, 6, 1, 2, 3, 2, 6, 7, 4, 5); @Test public void testPhoneTemplates() { PhoneNumber phone1 = new PhoneNumber(TEN_DIGITS); System.out.println(phone1.getValue()); PhoneNumber phone2 = new PhoneNumber(TEN_DIGITS); System.out.println(phone2.getValueWithParanthesis()); PhoneNumber phone3 = new PhoneNumber(TEN_DIGITS); System.out.println(phone3.getValueWithHyphens()); PhoneNumber phone4 = new PhoneNumber(TEN_DIGITS); System.out.println(phone4.getValueWith604Prefix()); PhoneNumber phone5 = new PhoneNumber(TEN_DIGITS); System.out.println(phone5.getValueWith778Prefix()); } }
This works.
But we just violated many principles that should be followed when writing code:
-
program to an interface
When creating a phone number object, it should be saved in a variable that uses an interface as type:
IPhoneNumber phoneNumber = new PhoneNumber();
Instead, we save the phone number object in a variable with the PhoneNumber type:
PhoneNumber phoneNumber = new PhoneNumber();
-
extend, not change
We changed the PhoneNumber class by adding 4 additional methods.
A class should not be changed since this may create issues in all other classes that use its objects.
-
single responsibility
The PhoneNumber class started with having one responsibility, to format a phone number.
Now, the class has 5 responsibilities by providing 5 types of formatting.
What do we do?
How do we change the code so all these 3 principles are followed?
-
Create a IPhoneNumber interface
import java.util.List;
public interface IPhoneNumber { public String getValue(); public List<Integer> getDigits(); public String areaCode(); public String prefix();
public String lineNumber(); }
2. Create a class for each phone number template and implement the IPhoneNumber interface
PhoneNumber class
import java.util.ArrayList; import java.util.List;
public class PhoneNumber implements IPhoneNumber { private final String phoneTemplate = "%s %s %s"; private List<Integer> digits = new ArrayList<>(); public PhoneNumber(List<Integer> digits) { if (digits.size() != 10) throw new RuntimeException("there are less than 10 digits!"); this.digits = digits; }
@Override public String getValue() { return String.format(phoneTemplate, areaCode(), prefix(), lineNumber() ); } @Override public List<Integer> getDigits() { return this.digits; } @Override public String areaCode() { return String.format("%d%d%d", digits.get(0), digits.get(1), digits.get(2)); } @Override public String prefix() { return String.format("%d%d%d", digits.get(3), digits.get(4), digits.get(5)); } @Override public String lineNumber() { return String.format("%d%d%d%d", digits.get(6), digits.get(7), digits.get(8), digits.get(9)); } @Override public String toString() { return this.digits.toString(); } }
PhoneNumberWithParanthesis class
import java.util.ArrayList; import java.util.List;
public class PhoneNumberWithParanthesis implements IPhoneNumber { private final String phoneTemplate = "(%s) %s %s"; private List<Integer> digits = new ArrayList<>(); public PhoneNumberWithParanthesis(List<Integer> digits) { if (digits.size() != 10) throw new RuntimeException("there are less than 10 digits!"); this.digits = digits; }
public String getValue() { return String.format(phoneTemplate, areaCode(), prefix(), lineNumber()); } public List<Integer> getDigits() { return this.digits; } public String areaCode() { return String.format("%d%d%d", digits.get(0), digits.get(1), digits.get(2)); } public String prefix() { return String.format("%d%d%d", digits.get(3), digits.get(4), digits.get(5)); } public String lineNumber() { return String.format("%d%d%d%d", digits.get(6), digits.get(7), digits.get(8), digits.get(9)); } @Override public String toString() { return this.digits.toString(); } }
PhoneNumberWithHyphens class
import java.util.ArrayList; import java.util.List;
public class PhoneNumberWithHyphens implements IPhoneNumber { private final String phoneTemplate = "%s-%s-%s"; private List<Integer> digits = new ArrayList<>(); public PhoneNumberWithHyphens(List<Integer> digits) { if (digits.size() != 10) throw new RuntimeException("there are less than 10 digits!"); this.digits = digits; }
@Override public String getValue() { return String.format(phoneTemplate, areaCode(), prefix(), lineNumber()); } @Override public List<Integer> getDigits() { return this.digits; } @Override public String areaCode() { return String.format("%d%d%d", digits.get(0), digits.get(1), digits.get(2)); } @Override public String prefix() { return String.format("%d%d%d", digits.get(3), digits.get(4), digits.get(5)); } @Override public String lineNumber() { return String.format("%d%d%d%d", digits.get(6), digits.get(7), digits.get(8), digits.get(9)); } @Override public String toString() { return this.digits.toString(); }
}
PhoneNumberStartingWith604 class
import java.util.ArrayList; import java.util.List;
public class PhoneNumberStartingWith604 implements IPhoneNumber { private final String phoneTemplate = "%s %s %s"; private List<Integer> digits = new ArrayList<>(); public PhoneNumberStartingWith604(List<Integer> digits) { if (digits.size() != 10) throw new RuntimeException("there are less than 10 digits!"); this.digits = digits; } @Override
public String getValue() { return String.format(phoneTemplate, areaCode(), prefix(), lineNumber()); } @Override public List<Integer> getDigits() { return this.digits; } @Override public String areaCode() { return "604"; } @Override public String prefix() { return String.format("%d%d%d", digits.get(3), digits.get(4), digits.get(5)); } @Override public String lineNumber() { return String.format("%d%d%d%d", digits.get(6), digits.get(7), digits.get(8), digits.get(9)); } @Override public String toString() { return this.digits.toString(); } }
PhoneNumberStartingWith778 class
import java.util.ArrayList; import java.util.List;
public class PhoneNumberStartingWith778 implements IPhoneNumber { private final String phoneTemplate = "%s %s %s"; private List<Integer> digits = new ArrayList<>(); public PhoneNumberStartingWith778(List<Integer> digits) { if (digits.size() != 10) throw new RuntimeException("there are less than 10 digits!"); this.digits = digits; }
@Override public String getValue() { return String.format(phoneTemplate, areaCode(), prefix(), lineNumber()); } @Override public List<Integer> getDigits() { return this.digits; } @Override public String areaCode() { return "778"; } @Override public String prefix() { return String.format("%d%d%d", digits.get(3), digits.get(4), digits.get(5)); } @Override public String lineNumber() { return String.format("%d%d%d%d", digits.get(6), digits.get(7), digits.get(8), digits.get(9)); } @Override public String toString() { return this.digits.toString(); } }
The test displays all possible templates for a phone number:
import java.util.Arrays; import java.util.List;
import org.testng.annotations.Test;
public class TestClass {
private final static List<Integer> TEN_DIGITS = Arrays.asList(4, 6, 1, 2, 3, 2, 6, 7, 4, 5); @Test public void testPhoneTemplates() { IPhoneNumber phone1 = new PhoneNumber(TEN_DIGITS); System.out.println(phone1.getValue()); IPhoneNumber phone2 = new PhoneNumberWithParanthesis(TEN_DIGITS); System.out.println(phone2.getValue()); IPhoneNumber phone3 = new PhoneNumberWithHyphens(TEN_DIGITS); System.out.println(phone3.getValue()); IPhoneNumber phone4 = new PhoneNumberStartingWith604(TEN_DIGITS); System.out.println(phone4.getValue()); IPhoneNumber phone5 = new PhoneNumberStartingWith778(TEN_DIGITS); System.out.println(phone5.getValue()); } }
The code is better because
- each class has one responsibility only (for a single phone template)
- each class implements an interface; each phone object, even if created with a class, is saved in a variable with the IPhoneNumber interface
But, looking at all classes, it is easy to see that have so much duplicated code.
What do we do next?
A different approach is needed.
We started with a PhoneNumber class that formats a phone number using a template.
We would like not to modify this class but extend it so that we can format the phone number with other templates.
Decorators provide this behaviour.
A decorator allows extending the functionality of a class without making any changes to the class.
We need a few steps for the new implementation:
-
We keep the same IPhoneNumber interface
import java.util.List;
public interface IPhoneNumber { public String getValue(); public List<Integer> getDigits(); public String areaCode(); public String prefix(); public String lineNumber();
}
2. We keep the PhoneNumber class in its original shape.
The PhoneNumber class implements the IPhoneNumber interface:
import java.util.ArrayList; import java.util.List;
public class PhoneNumber implements IPhoneNumber { private final String phoneTemplate = "%s %s %s"; private List<Integer> digits = new ArrayList<>(); public PhoneNumber(List<Integer> digits) { if (digits.size() != 10) throw new RuntimeException("there are less than 10 digits!"); this.digits = digits; } @Override public List<Integer> getDigits() { return this.digits; } @Override public String getValue() { return String.format(phoneTemplate, areaCode(), prefix(), lineNumber()); }
@Override public String areaCode() { return String.format("%d%d%d", digits.get(0), digits.get(1), digits.get(2)); } @Override public String prefix() { return String.format("%d%d%d", digits.get(3), digits.get(4), digits.get(5)); } @Override public String lineNumber() { return String.format("%d%d%d%d", digits.get(6), digits.get(7), digits.get(8), digits.get(9)); } }
3. Create an abstract class named PhoneDecorator:
import java.util.List;
public abstract class PhoneDecorator implements IPhoneNumber {
private IPhoneNumber decoratedPhone; public PhoneDecorator(IPhoneNumber phone) { decoratedPhone = phone; } public String getValue() { return decoratedPhone.getValue(); } public List<Integer> getDigits() { return decoratedPhone.getDigits(); } public String areaCode() { return decoratedPhone.areaCode(); } public String prefix() { return decoratedPhone.prefix(); } public String lineNumber() { return decoratedPhone.lineNumber(); } }
The abstract class PhoneDecorator will be used as parent for all specific decorator classes.
We will have one decorator class for each new template:
- Phone With Paranthesis
- Phone With Hyphens
- Phone With 604 Prefix
- Phone With 778 Prefix
4. Create the specific decorators
PhoneWithParanthesisDecorator class
public class PhoneWithParanthesisDecorator extends PhoneDecorator {
public PhoneWithParanthesisDecorator(IPhoneNumber phone) {
super(phone);
}
public String getValue() {
String phoneTemplate = "(%s) %s %s";
return String.format(phoneTemplate,
super.areaCode(),
super.prefix(),
super.lineNumber());
}
}
PhoneWithHyphensDecorator class
public class PhoneWithHyphensDecorator extends PhoneDecorator {
public PhoneWithHyphensDecorator(IPhoneNumber phone) {
super(phone);
}
public String getValue() {
String phoneTemplate = "%s-%s-%s";
return String.format(phoneTemplate,
super.areaCode(),
super.prefix(),
super.lineNumber());
}
}
PhoneWith604PrefixDecorator class
public class PhoneWith604PrefixDecorator extends PhoneDecorator {
public PhoneWith604PrefixDecorator(IPhoneNumber phone) {
super(phone);
}
public String getValue() {
String phoneTemplate = "%s %s %s";
return String.format(phoneTemplate,
"604",
super.prefix(),
super.lineNumber());
}
}
PhoneWith778PrefixDecorator class
public class PhoneWith778PrefixDecorator extends PhoneDecorator {
public PhoneWith778PrefixDecorator(IPhoneNumber phone) {
super(phone);
}
public String getValue() {
String phoneTemplate = "%s %s %s";
return String.format(phoneTemplate,
"778",
super.prefix(),
super.lineNumber());
}
}
Each decorator only implements the change about a specific template.
It does this by encapsulating an IPhoneNumber object and then overriding the getValue() method of the PhoneDecorator class.
There is no code duplication between the phone decorator classes.
They do not have code for anything else than the getValue() method.
5. Wrap the PhoneNumber objects with specific decorators to use other phone templates
The test method puts everything together:
import java.util.Arrays;import java.util.List;
import org.testng.annotations.Test;
public class TestClass {
private final static List<Integer> TEN_DIGITS = Arrays.asList(4, 6, 1, 2, 3, 2, 6, 7, 4, 5); @Test public void testPhoneTemplates() { IPhoneNumber phone1 = new PhoneNumber(TEN_DIGITS); System.out.println(phone1.getValue()); IPhoneNumber phone2 = new PhoneWithParanthesisDecorator( new PhoneNumber(TEN_DIGITS)); System.out.println(phone2.getValue()); IPhoneNumber phone3 = new PhoneWithHyphensDecorator( new PhoneNumber(TEN_DIGITS)); System.out.println(phone3.getValue()); IPhoneNumber phone4 = new PhoneWith604PrefixDecorator( new PhoneNumber(TEN_DIGITS)); System.out.println(phone4.getValue()); IPhoneNumber phone5 = new PhoneWith778PrefixDecorator( new PhoneNumber(TEN_DIGITS)); System.out.println(phone5.getValue()); } }
What did we get when using decorators?
- No changes are made to the original class
- We program to an interface; all phone number objects are saved in IPhoneNumber variables
- Every time we need a different phone template than the one implemented in the PhoneNumber class, we create a decorator for it.
- To use the new template, we “decorate” the PhoneNumber object by wrapping it with a decorator object
You must be logged in to post a comment.