Strategy Pattern and usage in Shiksha

Shiksha Engineering
8 min readMar 24, 2020

--

Author : Kushagra Varshney

What and Why?

Strategy pattern is a behavioural design pattern. Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it .

Directly putting all solutions in one class violates the open-closed principle. Also, the extension and scaling the code becomes a problem as the strategies grow.

Handwriting all the algorithms (or strategies) into one class isn’t desirable as different services (clients) may require a different strategy, if all the strategies were to be incorporated into one class, all the services would need to import all the strategy. While they may not require some. Adding removing strategies becomes a bottleneck as all the strategies are part of the client.

This can be prevented if we encapsulate these different strategies(Algorithms).

Example
Let’s solve a simple problem of expression tree which most of us would have addressed in our graduation years, but this time in a bit different way

Problem: Expression tree is a binary tree in which each internal node corresponds to operator and each leaf node corresponds to operand Expression tree for 3 + ((5+9)*2) would be :

Figure 1 : Expression Tree
public class Node{
public String data;
public Node left;
public Node right;
}
public class Solution{
public int evalauteExpression(Node node){
if(node == null)
return 0;
if(node.left == null && node.right == null)
return Integer.valueOf(node.data);
int leftOperand = evalauteExpression(node.left);
int rightOperand = evalauteExpression(node.right);

if (node.data == “+”)
return leftOperand + rightOperand;

if (node.data == “-”)
return leftOperand — rightOperand;

if (node.data == “*”)
return leftOperand * rightOperand;
if(rightOperand == 0){
throw new Exception(“The great Ramanujan coudnt solve it how can I”);
}
return leftOperand/rightOperand;
}
}

The above solution is a simple basic solution. The solution class needs to change on the addition of every new operation. This becomes a challenging situation for clients to extend this code. Though the interpreter design pattern can better solve this, I found this example as more catchy and easy to understand in this case too.

Lets see the Strategy Pattern way:

public class Node{
public IEvaluate expression(int a, int b);
public int data;
public Node left;
public Node right;
}
public interface IEvaluate{
int evaluate(int a, int b);
}
public class Add implements IEvaluate{
@Override
public int evaluate(int a, int b){
return a+b;
}
}
public class Subtract implements IEvaluate{
@Override
public int evaluate(int a, int b){
return a — b;
}
}
public class Divide implements IEvaluate{
@Override
public int evaluate(int a, int b){
if(b == 0)
throw new Exception(“The great Ramanujan couldn’t solve it how can I”);
return a / b;
}
}
public class Multiplication implements IEvaluate{
@Override
public int evaluate(int a, int b){
return a * b;
}
}
public class Solution{
public int evalauteExpression(Node node){
if(node == null)
return 0;
if(node.left == null && node.right == null)
return Integer.valueOf(node.data);
int leftOperand = evalauteExpression(node.left);
int rightOperand = evalauteExpression(node.right);
/*
* see how we have got rid of ugly and ever dreadful
* if statements, by just invoking the stored * strategy on runtime */
return node.expression(leftOperand, rightOperand);
}
}
Benefits and Pitfalls
Strategy pattern provides alternative to conditional statements and thus making code more readable and scalable. We have already seen this behaviour in our previous example.

Abstract classes, inheritance can help us factor out standard functionalities at one place

It provides a way for clients to define their new strategies.

Strategy pattern increases the number of objects.

Sometimes information passed to the interfaces may not be used in case of simple strategies. As in our example sometime only one operand might be required (As in decrement and increment operation).

Implementations in our code base
We have used the strategy pattern in assistant for lead generation. Its currently implemented for client assistant and will be expanded to shiksha assistant.

Validations

Saving the leads information

The motivation to use the pattern here stems from the fact that different clients can have various validations and also separate order of generation of Leads. E.g., A client may request to save name first and then the phone number, while this might be reversed for some other client. We should be able to do this on the fly without the need for code change or deployment. Also, each client can ask for a different set of validation. He can select from the family of validation we have in our system. Also, he can change this anytime. I am attaching some of the code snippets as to how we solved this in client assistant. Furthermore, you can visit the code base of the client assistant to understand the implementation better..

We will see the validation strategy implemented using enum strategy pattern:

public enum FieldType {
LIST(“list”){
@Override
public boolean validate(String value){
if(StringUtils.isEmpty(value))
return false;
return true;
}
},
EMAIL(“email”){
@Override
public boolean validate(String email){
if(StringUtils.isEmpty(value))
return false;
String regex = “^[\\w-_.+]*[\\w-_.]@([\\w]+\\.)+[\\w]+[\\w]$”;
return email.matches(regex);
}
},
PHONE(“phone”){
@Override
public boolean validate(String phoneNumber){
if(StringUtils.isEmpty(value))
return false;
if(phoneNumber.length() > 20)
return false;
String regex = “^(?:(?:\\+|0{0,2})91(\\s*[\\-]\\s*)?|[0]?)?[6789]\\d{9}$”;
return phoneNumber.matches(regex);
}
},
GENERIC(“generic”){
@Override
public boolean validate(String value) {
return !StringUtils.isEmpty(value);
}
};
private static final Logger logger = LoggerFactory.getLogger(FieldType.class);
public String value;
private static Map<String, FieldType> valueEnumMap = new HashMap<>();
FieldType(String value){
this.value = value;
}
@JsonValue
public String getValue() {
return value;
}
public abstract boolean validate(String value);
private static void setValueEnumMap() {
FieldType.valueEnumMap = Arrays.stream(FieldType.values()).collect(Collectors.toMap(FieldType::getValue, t -> t));
}
static{
FieldType.setValueEnumMap();
}
public static FieldType getByValue(String value) {
if(FieldType.valueEnumMap == null){
logger.error(“FieldType map null while getting object”);
FieldType.setValueEnumMap();
}
if(FieldType.valueEnumMap.get(value) != null){
return FieldType.valueEnumMap.get(value);
}
throw new EnumConstantNotPresentException(FieldType.class, value);
}
}
public class Test{
public boolean validationTest{
RegistrationResponse registrationResponse = new RegistrationResponse();
prepareRegistrationResponse(registrationResponse);
String value = userInput.getUserInputText();
/*
* field type is set in db with field name
*/
boolean correctValue = registrationResponse.getFieldType().validate(value);
}
public void prepareRegistrationResponse(RegistrationResponse registrationResponse){
ClientRegistrationOrder clientRegistrationOrder = clientRegistrationOrderRepository.findByClientIdAndDisplayOrderAndStatus(clientInfo.getClientId(), registrationResponse.getOrder(), Status.LIVE);
if(clientRegistrationOrder == null){
registrationResponse.setRegistrationComplete(true);
return;
}
registrationResponse.setRegistrationComplete(false)
.setField(LeadFields.getByValue(clientRegistrationOrder.getFieldName().getValue()))
.setFieldType(clientRegistrationOrder.getType())
.setQuestion(clientRegistrationOrder.getQuestion())
.setExtraData(clientRegistrationOrder.getExtraData())
.setFailureMessage(clientRegistrationOrder.getFailureMessage())
.setSuccessMessage(clientRegistrationOrder.getSuccessMessage());
}
}

P.S.: This implementation violates the open-closed principle but since any other service will not further implement this. There was no need for having an interface.This can be prevented if we encapsulate these different strategies(Algorithms).

Example

Let’s solve a simple problem of expression tree which most of us would have addressed in our graduation years, but this time in a bit different way Problem: Expression tree is a binary tree in which each internal node corresponds to operator and each leaf node corresponds to operand Expression tree for 3 + ((5+9)*2) would be :

Expression Tree

public class Node{
public String data;
public Node left;
public Node right;
}
public class Solution{
public int evalauteExpression(Node node){
if(node == null)
return 0;
if(node.left == null && node.right == null)
return Integer.valueOf(node.data);
int leftOperand = evalauteExpression(node.left);
int rightOperand = evalauteExpression(node.right);

if (node.data == "+")
return leftOperand + rightOperand;

if (node.data == "-")
return leftOperand - rightOperand;

if (node.data == "*")
return leftOperand * rightOperand;
if(rightOperand == 0){
throw new Exception("The great Ramanujan coudnt solve it how can I");
}
return leftOperand/rightOperand;
}
}

The above solution is a simple basic solution. The solution class needs to change on the addition of every new operation. This becomes a challenging situation for clients to extend this code. Though the interpreter design pattern can better solve this, I found this example as more catchy and easy to understand in this case too.

Lets see the Strategy Pattern way:

public class Node{
public IEvaluate expression(int a, int b);
public int data;
public Node left;
public Node right;
}
public interface IEvaluate{
int evaluate(int a, int b);
}
public class Add implements IEvaluate{
@Override
public int evaluate(int a, int b){
return a+b;
}
}
public class Subtract implements IEvaluate{
@Override
public int evaluate(int a, int b){
return a - b;
}
}
public class Divide implements IEvaluate{
@Override
public int evaluate(int a, int b){
if(b == 0)
throw new Exception("The great Ramanujan couldn't solve it how can I");
return a / b;
}
}
public class Multiplication implements IEvaluate{
@Override
public int evaluate(int a, int b){
return a * b;
}
}
public class Solution{
public int evalauteExpression(Node node){
if(node == null)
return 0;
if(node.left == null && node.right == null)
return Integer.valueOf(node.data);
int leftOperand = evalauteExpression(node.left);
int rightOperand = evalauteExpression(node.right);
/*
* see how we have got rid of ugly and ever dreadful
* if statements, by just invoking the stored * strategy on runtime */
return node.expression(leftOperand, rightOperand);
}
}

Benefits and Pitfalls

  • Strategy pattern provides alternative to conditional statements and thus making code more readable and scalable. We have already seen this behaviour in our previous example.
  • Abstract classes, inheritance can help us factor out standard functionalities at one place
  • It provides a way for clients to define their new strategies.
  • Strategy pattern increases the number of objects.
  • Sometimes information passed to the interfaces may not be used in case of simple strategies. As in our example sometime only one operand might be required (As in decrement and increment operation).

Implementations in our code base

We have used the strategy pattern in assistant for lead generation. Its currently implemented for client assistant and will be expanded to shiksha assistant.

  • Validations
  • Saving the leads information

The motivation to use the pattern here stems from the fact that different clients can have various validations and also separate order of generation of Leads. E.g., A client may request to save name first and then the phone number, while this might be reversed for some other client. We should be able to do this on the fly without the need for code change or deployment. Also, each client can ask for a different set of validation. He can select from the family of validation we have in our system. Also, he can change this anytime. I am attaching some of the code snippets as to how we solved this in client assistant. Furthermore, you can visit the code base of the client assistant to understand the implementation better..

We will see the validation strategy implemented using enum strategy pattern:

public enum FieldType {    LIST("list"){
@Override
public boolean validate(String value){
if(StringUtils.isEmpty(value))
return false;
return true;
}
},
EMAIL("email"){
@Override
public boolean validate(String email){
if(StringUtils.isEmpty(value))
return false;
String regex = "^[\\w-_.+]*[\\w-_.]@([\\w]+\\.)+[\\w]+[\\w]$";
return email.matches(regex);
}
},
PHONE("phone"){
@Override
public boolean validate(String phoneNumber){
if(StringUtils.isEmpty(value))
return false;
if(phoneNumber.length() > 20)
return false;
String regex = "^(?:(?:\\+|0{0,2})91(\\s*[\\-]\\s*)?|[0]?)?[6789]\\d{9}$";
return phoneNumber.matches(regex);
}
},
GENERIC("generic"){
@Override
public boolean validate(String value) {
return !StringUtils.isEmpty(value);
}
};
private static final Logger logger = LoggerFactory.getLogger(FieldType.class);
public String value;
private static Map<String, FieldType> valueEnumMap = new HashMap<>();
FieldType(String value){
this.value = value;
}
@JsonValue
public String getValue() {
return value;
}
public abstract boolean validate(String value); private static void setValueEnumMap() {
FieldType.valueEnumMap = Arrays.stream(FieldType.values()).collect(Collectors.toMap(FieldType::getValue, t -> t));
}
static{
FieldType.setValueEnumMap();
}
public static FieldType getByValue(String value) {
if(FieldType.valueEnumMap == null){
logger.error("FieldType map null while getting object");
FieldType.setValueEnumMap();
}
if(FieldType.valueEnumMap.get(value) != null){
return FieldType.valueEnumMap.get(value);
}
throw new EnumConstantNotPresentException(FieldType.class, value);
}
}
public class Test{
public boolean validationTest{
RegistrationResponse registrationResponse = new RegistrationResponse();
prepareRegistrationResponse(registrationResponse);
String value = userInput.getUserInputText();
/*
* field type is set in db with field name
*/
boolean correctValue = registrationResponse.getFieldType().validate(value);
}
public void prepareRegistrationResponse(RegistrationResponse registrationResponse){
ClientRegistrationOrder clientRegistrationOrder = clientRegistrationOrderRepository.findByClientIdAndDisplayOrderAndStatus(clientInfo.getClientId(), registrationResponse.getOrder(), Status.LIVE);
if(clientRegistrationOrder == null){
registrationResponse.setRegistrationComplete(true);
return;
}
registrationResponse.setRegistrationComplete(false)
.setField(LeadFields.getByValue(clientRegistrationOrder.getFieldName().getValue()))
.setFieldType(clientRegistrationOrder.getType())
.setQuestion(clientRegistrationOrder.getQuestion())
.setExtraData(clientRegistrationOrder.getExtraData())
.setFailureMessage(clientRegistrationOrder.getFailureMessage())
.setSuccessMessage(clientRegistrationOrder.getSuccessMessage());
}
}

P.S.: This implementation violates the open-closed principle but since any other service will not further implement this. There was no need for having an interface.

References

[1] Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides. Design
Patterns: Elements of Reusable Object-oriented Software. Addison-Wesley
Longman Publishing Co., Inc., Boston, MA, USA, 1995.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

No responses yet

Write a response