Search code examples
javafilterdesign-patternsstrategy-pattern

Strategy pattern to filter objects, how to compose them?


Say I have come class that looks like this

class Product {
    double Price;
    String name;
    String category;
}

I have a client facing API that allows users to filter my list of objects based on the name or rating or price. These are implemented through Strategy Pattern like this

interface IFilterStrategy {
    boolean apply(Product product);
}

class NameMatchStrategy implements IFilterStrategy {
    String name;
    public NameMatchStrategy(String name) {
        this.name = name;
    }

    boolean apply(Product product) {
        return product.getName().equals(name);
    }
}

class PriceMatchStrategy implements IFilterStrategy {
    double minVal;
    double maxVal;
    public StringMatchStrategy(double minVal, double maxVal) {
        this.minVal = minVal;
        this.maxVal = maxVal;
    }

    boolean apply(Product product) {
        return product.getPrice() >= minVal && product.getPrice() <= maxVal;
    }
}

My question is what if I wanted to add a new Strategy, say that matches just the Prefix of a name. So perhaps a user passes "iph" and I should be able to find a product name that's called "iphone" and match it with that. I know I can create another NamePrefixMatchStrategy, but what if in the future I want to extend this Prefix strategy to say, Category as well. Like a user could pass in "ph" and I should find the category "phone".

The brute force solution would be to just create a CategoryPrefixMatchStrategy, but the number of my strategies would quickly become hard to maintain.

What's the best design pattern to get around this? Should I create a generic StringMatchStrategy or something, I'm just not sure how to incorporate this into my current design.


Solution

  • You could create interfaces with default methods. So you can reuse the code, but you still have the problem with the number of strategies.

    abstract class ExactMatchStrategy implements IFilterStrategy {
    
        String value;
    
        public ExactMatchStrategy(String value) {
            this.value = value;
        }
    
        public boolean apply(Product product) {
            return getFieldValue(product).equals(value);
        }
    
        public abstract String getFieldValue(Product product);
    
    }
    
    class ExactNameMatchStrategy extends ExactMatchStrategy {
        
        public ExactNameMatchStrategy(String name) {
            super(name);
        }
    
        public String getFieldValue(Product product) {
             return product.getName();
        }
    }
    
    class ExactCategoryMatchStrategy extends ExactMatchStrategy {
        
        public ExactCategoryMatchStrategy(String category) {
            super(category);
        }
    
        public String getFieldValue(Product product) {
             return product.getCategory();
        }
    }
    

    Or with the new Java function package as follows:

    class ExactMatchStrategy implements IFilterStrategy {
    
        String value;
        Function<Product, String> getField;
    
        public ExactMatchStrategy(String value, Function<Product, String> getField) {
            this.value = value;
            this.getField = getField;
        }
    
        public boolean apply(Product product) {
            return getField.apply(product).equals(value);
        }
    
    }
    
    class ExactNameMatchStrategy extends ExactMatchStrategy {
        public ExactNameMatchStrategy(String name) {
            super(name, (p) -> p.getName());
        }
    }
    
    class ExactCategoryMatchStrategy extends ExactMatchStrategy {
    
        public ExactCategoryMatchStrategy(String category) {
            super(category, (p) -> p.getCategory());
        }
    }
    

    Then you could also instantiate inline

    ExactMatchStrategy exactNameMatcher = new ExactMatchStrategy(name, (p) -> p.getName());
    ExactMatchStrategy exactCategoryMatcher = new ExactMatchStrategy(category, (p) -> p.getCategory());