Strategy Design Pattern and Open Closed Principle in Java - Example

Strategy design pattern is based upon open closed design principle, the 'O' of famous SOLID design principles. It's one of the popular pattern in the field of object-oriented analysis and design along with Decorator, Observer and Factory patterns. Strategy pattern allows you to encapsulate possible changes in a process and encapsulate that in a Strategy class. By doing that your process (mainly a method) depends upon a strategy, higher level of abstraction than implementation. This makes your process open for extension by providing new Strategy implementation, but closed for modification, because the introduction of a new strategy doesn't require a change in a tested method. That's how it confirms open closed design principle.

Though like any other design pattern, it also introduces few more classes in your codebase, but that's worth doing because it better organize your code and provides much-needed flexibility. It also makes it easy to change attributes.

Strategy pattern is very useful, especially for implementing algorithmic strategy e.g. encryption, compression, comparison etc. A couple of examples of Strategy pattern from JDK is Comparator and LayoutManager. You can view Comparator as Strategy interface, define compare() method, now it's up to the classes how they compare themselves.

This method is utilized for sorting inside Collections.sort(), which confirms OCP because it doesn't require any change when comparison logic changes. Similarly LayoutManager e.g. GridLayout, BorderLayout helps you to organize components differently.




Strategy design Pattern Example - Open Closed Design principle

This example of Strategy pattern can also be seen as an example of open closed design principle, which states that a design, code (class or method) should remain open for extension but closed for modification.

When we say closed for modification, it means new changes should be implemented by the new code, rather than altering existing code. This reduces the possibility of breaking existing tried and tested code.

The strategy  pattern is also is one of the behavioral pattern in GOF list, first introduced in classic GOF design pattern book.

How to implement Strategy Pattern in Java


In this example, we need to filter the Incoming message by certain criterion e.g. filter message if it's of a particular type. But you know that this criterion can change, and you may need to filter the message by their size or a specific keyword in their content. In the center of this operation, we have a filter() method in a class say MessageUtils, this class is responsible for filtering messages.

In the simplest form, just removing them from incoming List of Messages. In order to keep this code intact even when filtering strategy changes due to new requirements, we will use Strategy design pattern.

In order to Strategy pattern, we will define a Strategy interface say FilteringStrategy which contains isFilterable(Message msg) method, this method returns true if Message passed to it is filterable by criteria, implemented by subclasses.

This design makes it very easy to introduce a new strategy. We have three implementations of this Strategy interface FilterByType, FilterBySize, and FilteryByKeyword.

Our data object Message contains a type, represented by Java Enum, an integer size, and String content. This design is also open for extension, as you can introduce any filtering strategy.

Here is UML diagram of Strategy pattern, this time, we are using sorting strategy to implement different sorting algorithms e.g. bubble sort, quick sort, and insertion sort.

Strategy design pattern example in Java



Strategy design Pattern Implementation in Java

Here is complete code example of Strategy design pattern in Java.

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Java program to implement Strategy design pattern 
 * and Open Closed design principle.
 * filter() method uses Strategy pattern to filter messages.
 *
 * @author Javin Paul
 */
public class StrategyPattern {

    private static final Logger logger = LoggerFactory.getLogger(StrategyPattern.class);

    public static void main(String args[]) {
        List<Message> messages = new ArrayList<>();
        messages.add(new Message(MessageType.TEXT, 100, "This is test message"));
        messages.add(new Message(MessageType.XML, 200, "How are you "));
        messages.add(new Message(MessageType.TEXT, 300, "Does Strategy pattern follows OCP design principle?"));
        messages.add(new Message(MessageType.TEXT, 400, "Wrong Message, should be filtered"));
       
        messages = filter(messages, new FilterByType(MessageType.XML));
        messages = filter(messages, new FilterByKeyword("Wrong"));
        messages = filter(messages, new FilterBySize(200));
       
    }

    /*
     * This method confirms Open Closed design principle, 
     * It's open for modification, because
     * you can provide any filtering criterion by providing 
     * implementation of FilteringStrategy, but
     * no need to change any code here. 
     * New functionality will be provided by new code.
     */
    public static final List<Message> filter(List<Message> messageList, FilteringStrategy strategy){
       
        Iterator<Message> itr = messageList.iterator();
       
        while(itr.hasNext()){
            Message msg = itr.next();           
            if(strategy.isFilterable(msg)){
                logger.info(strategy.toString() + msg);
                itr.remove();
            }
        }
       
        return messageList;
    }
   
}

Output:
- Filtering By type: XML Message{type=XML, size=200, content=<data>How are you </data>}
- Filtering By keyword: Wrong Message{type=TEXT, size=400, content=Wrong Message, should be filtered}
- Filtering By maxSize: 200 Message{type=TEXT, size=300, content=Does Strategy pattern follows OCP design principle?}

You can see that our List contains four messages one of type XML and three of type TEXT. So when we first filter messages by type  XML, you can see that only the message with XML type is filtered. Next, when we filter messages based upon keyword "Wrong", only the message which contains this keyword are filtered.


Lastly, when I filtered messages on the size of 200, only the message whose size is greater than 200 are filtered. So we can do different things from same code by just providing a new implementation of Strategy interface, this is the power of Strategy design pattern.

You can also use lambda expressions to implement Strategy pattern in Java, as you can use lambdas in place of anonymous class in Java. This will make your code even more readable and concise.


Important classes of this Example
Here are other important classes to execute this example :

Message.java
/* * A class to represent a Message with type, size and content */ class Message{ private MessageType type; private int size; private String content; public Message(MessageType type, int size, String content) { this.type = type; this.size = size; this.content = content; } public String getContent() { return content; } public int getSize() { return size; } public MessageType getType() { return type; } @Override public String toString() { return " Message{" + "type=" + type + ", size=" + size + ", content=" + content + '}'; } }

This class is used to define different message types

MessageType.java
/* * Enum to denote different Message type */ public enum MessageType { TEXT, BYTE, XML; }


This is the core interface to define Strategy

FilteringStrategy.java

/* * interface which defines Strategy for this pattern. */ public interface FilteringStrategy{ public boolean isFilterable(Message msg); }


This is one implementation of Strategy interface, which filters messages by their type.


FilterByType.java
/* * An implementation of Strategy interface, which decides to filter * message by type. */ public class FilterByType implements FilteringStrategy{ private MessageType type; public FilterByType(MessageType type) { this.type = type; } @Override public boolean isFilterable(Message msg) { return type == msg.getType(); } @Override public String toString() { return "Filtering By type: " + type; } }

Here is another implementation of Strategy interface which filters messages by size :

FilterBySize.java
/* * Another Strategy implementation for filtering message by size */ public class FilterBySize implements FilteringStrategy{ private int maxSize; public FilterBySize(int maxSize) { this.maxSize = maxSize; } @Override public boolean isFilterable(Message msg) { return msg.getSize() > maxSize; } @Override public String toString() { return "Filtering By maxSize: " + maxSize; } }


Here is one more implementation of Strategy interface which will filter messages by keywords

FilterByKeyword.java
/* * Another Strategy implementation for filtering message by keyword in content. */ public class FilterByKeyword implements FilteringStrategy{ private String keyword; public FilterByKeyword(String keyword) { this.keyword = keyword; } public String getKeyword() { return keyword; } public void setKeyword(String keyword) { this.keyword = keyword; } @Override public boolean isFilterable(Message msg) { return msg.getContent()==null || msg.getContent().contains(keyword); } @Override public String toString() { return "Filtering By keyword: " + keyword; } }

That's all in this real life example of Strategy design pattern in Java. As I said, since Strategy pattern confirms open closed design principle, you can also view this as an example of Open Closed design principle in Java. Design patterns are tried and tested way of solving problem in a particular context and knowledge of core patterns really helps you to write better code. I strongly recommend Head First Object-Oriented Analysis and Design and Head First Design Pattern book to every Java developer, both senior and junior, who wants to learn more about design pattern and their effective use in Java.



No comments :

Post a Comment