Шаблон стратегии для фильтрации объектов, как их составить?

Скажем, я пришел на урок, который выглядит вот так

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

У меня есть клиентский API, который позволяет пользователям фильтровать мой список объектов по имени, рейтингу или цене. Они реализованы с помощью паттерна стратегии, подобного этому.

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;
    }
}

Мой вопрос заключается в том, что, если я захочу добавить новую стратегию, скажем, которая соответствует только префиксу имени. Итак, возможно, пользователь передает «iph», и я смогу найти название продукта с названием «iphone» и сопоставить его с ним. Я знаю, что могу создать еще одну NamePrefixMatchStrategy, но что, если в будущем я захочу расширить эту Prefix стратегию, скажем также Category. Например, пользователь может ввести «ph», а я должен найти категорию «phone».

Решением грубой силы было бы просто создать CategoryPrefixMatchStrategy, но количество моих стратегий быстро стало бы трудно поддерживать.

Какой шаблон проектирования лучше всего обойти? Должен ли я создать универсальный StringMatchStrategy или что-то в этом роде, я просто не знаю, как включить это в мой текущий дизайн.

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
0
83
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Это вопрос мнения, но эти стратегии не очень обширны, поэтому вы можете просто создать их фабричными методами.

class NameMatchingStrategies {
 public static IFilterStrategy exactMatch(String toMatch) {
  return product -> product.getName().equals(toMatch);
 }
 public static IFilterStrategy containsMatch(String toContain) {
  return product -> product.getName().contains(toContain);
 }
}

Вы можете сделать это общим с помощью

 public static IFilterStrategy operatorMatch(String toMatch, BiPredicate<String> operator) {
   return product -> operator.test(product.getName(), toMatch);
 }

но на самом деле кажется сложным делать это, например, для точного совпадения (return operatorMatch(name, String::equals)) и даже для совпадения префикса (return operatorMatch(prefix, String::startsWith)).

Вы, конечно, можете сделать это подходящим классом

class ProductNameMatchingStrategy {
 private final BiPredicate<String> matcher;
 private final String toMatch;

 public boolean apply(Product product) {
  return matcher.test(product.getName(), toMatch);
 }
}

но это просто еще один код. Опять же дело вкуса.

Чтобы проверить другие поля, вы можете использовать тот же шаблон

 public static IFilterStrategy extractingOperatorMatch(String toMatch,
                  Function<Product, String> extractor,
                  BiPredicate<String> matcher) {
  return product -> matcher.test(extractor.apply(product), toMatch);
 }

Это пример композиции вместо наследования. Затем вы можете легко создать любую комбинацию этих двух методов (извлечение из продукта и метод сопоставления). Тогда сопоставителем категории может быть, например, extractingMatch("cat", Product::getCategory, String::contains) («товары, которые содержат слово «кошка» в поле категории»).

Вы можете и, вероятно, должны сделать это null-безопасным на случай, если поле не установлено, но это выходит за рамки этого вопроса, и вам придется решить, как обрабатывать нулевой случай (всегда соответствует? никогда не соответствует? оценивать другое совпадение?).

Да, и вы, конечно, можете обобщить его, чтобы также предоставить основу для других стратегий,

public static <T> IFilterStrategy extractingMatcher(T toMatch,
                  Function<Product, T> extractor,
                  BiPredicate<T> matcher) {
  return product -> matcher.test(extractor.apply(product), toMatch);
}

с использованием типа

public static IFilterStrategy minPriceFilter(Integer min) {
 return product -> extractingMatch(min,
                   Product::getPrice,
                   p -> p >= min);
 }

public static IFilterStrategy maxPriceFilter(Integer max) {
 return product -> extractingMatch(max,
                   Product::getPrice,
                   p -> p <= max);
 }

И поскольку вы можете создавать такие комбинированные фильтры

public static IFilterStrategy and(IFilterStrategy s1,
                                  IFilterStrategy s2) {
 return p -> s1.apply(p) && s2.apply(p);
}

вы можете создать свой minMaxFilter с помощью

IFilterStrategy minMaxPrice = and(minPriceFilter(min),
                                  maxPriceFilter(max));

Но ваш IFilterStrategy в любом случае всего лишь Predicate<Product>, поэтому вы можете использовать его (он уже содержит методы композиции and() и or()); вы можете построить их точно так же, как описано выше.

Ответ принят как подходящий

Вы можете создавать интерфейсы с методами по умолчанию. Таким образом, вы можете повторно использовать код, но у вас все равно остается проблема с количеством стратегий.

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();
    }
}

Или с помощью нового пакета функций Java следующим образом:

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());
    }
}

Тогда вы также можете создать экземпляр встроенного

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

Во-первых, чтобы предоставить клиентам больше гибкости, я бы использовал Predicate<Product> вместо создания IFilterStrategy.

Во-вторых, похоже, что ваша проблема состоит из двух частей: какое поле тестировать и как его тестировать. У вас может быть что-то вроде FieldExtractingPredicate, который предоставляет интерфейс для составления предиката из двух частей:

public class FieldExtractingPredicate<T> implements Predicate<Product> {

    private final Function<Product, T> fieldExtractingFunction;
    private final Predicate<T> fieldPredicate;

    public FieldExtractingPredicate(Function<Product, T> fieldExtractingFunction, Predicate<T> fieldPredicate) {
        this.fieldExtractingFunction = fieldExtractingFunction;
        this.fieldPredicate = fieldPredicate;
    }

    @Override
    public boolean test(Product product) {
        return fieldPredicate.test(fieldExtractingFunction.apply(product));
    }

    public static <T> Predicate<Product> fieldEqualsPredicate(Function<Product, T> fieldExtractingFunction, T matchValue) {
        return new FieldExtractingPredicate<>(fieldExtractingFunction, value -> value.equals(matchValue));
    }

    public static Predicate<Product> fieldBetween(Function<Product, Double> fieldExtractingFunction, double minVal, double maxVal) {
        return new FieldExtractingPredicate<>(fieldExtractingFunction, value -> value >= minVal && value <= maxVal);
    }

    public static Predicate<Product> fieldContainsPredicate(Function<Product, String> fieldExtractingFunction, String substring) {
        return new FieldExtractingPredicate<>(fieldExtractingFunction, value -> value.contains(substring));
    }

    public static Predicate<Product> fieldStartsWithPredicate(Function<Product, String> fieldExtractingFunction, String prefix) {
        return new FieldExtractingPredicate<>(fieldExtractingFunction, value -> value.startsWith(prefix));
    }
}

Теперь добавить новые стратегии, скажем «заканчивается», довольно просто:

    public static Predicate<Product> fieldEndsWithPredicate(Function<Product, String> fieldExtractingFunction, String suffix) {
        return new FieldExtractingPredicate<>(fieldExtractingFunction, value -> value.endsWith(suffix));
    }

Если вы хотите скрыть fieldExtractingFunction от пользователя, вы можете предоставить фабричные методы для определенных полей:

public static Predicate<Product> nameEquals(String expectedName) {
    return fieldEqualsPredicate(Product::getName, expectedName);
}

public static Predicate<Product> priceBetween(double minPrice, double maxPrice) {
    return fieldBetween(Product::getPrice, minPrice, maxPrice);
}

Использование Predicate также позволяет вам достаточно легко составлять более сложные стратегии, используя Predicate.and(), Predicate.or() и Predicate.not(). Например:

    nameEquals("basketball").and(priceBetween(10.0, 20.0));

В качестве альтернативы вы можете полностью оставить функцию тестирования на усмотрение клиента и предоставить фабричные методы для конкретных полей:

public static Predicate<Product> nameMatches(Predicate<String> nameTest) {
    return new FieldExtractingPredicate<>(Product::getName, nameTest);
}

Если вы хотите реализовать гибкие стратегии.

Я думаю, вы можете использовать библиотеки отражений и динамических сценариев. Вы можете погуглить некоторую информацию о Java scriptEngine, чтобы воплотить в жизнь свои мысли.

Вы можете использовать отражение, чтобы получить поля в классе Product. А затем напишите правильные операторы сценария и выполните его с помощью scriptEngine.

Другие вопросы по теме