Скажем, я пришел на урок, который выглядит вот так
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 или что-то в этом роде, я просто не знаю, как включить это в мой текущий дизайн.
Это вопрос мнения, но эти стратегии не очень обширны, поэтому вы можете просто создать их фабричными методами.
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.