Потенциально возможное продолжение книги Паттерны проектирования (Элизабет Фримен и другие).
На дворе 2017 год. В компанию, где работает старший разработчик Джо, пришел на стажировку молодой студент Мартин. Он целый год скрупулезно изучал Java по современному учебнику с акцентом на функциональные интерфейсы, лямбда-выражения и прочие новшества.
Джо поинтересовался, знает ли Мартин паттерны проектирования. Мартин не знал, совсем не знал. И Джо понял, как он будет стажировать Мартина. Он даст ему те задачи, которые, по его мнению, без паттернов проектирования будут иметь трудно поддерживаемые решения. Когда Мартин напишет приложение, Джо сначала помучает его с расширением архитектуры, а затем покажет ему, как легко можно ее расширять, если при проектировании изначально заложить в нее чудесные паттерны. Однако, он решил сразу предупредить Мартина о направлениях расширения: а вдруг он сам сможет нечаянно реализовать паттерны проектирования, не зная их? Молодые люди весьма изобретательны.
Джо: Слушай, Мартин, для тебя есть ответственная работа. Нужно написать прототип симулятора утиного пруда. Создай модели уток, они могут крякать, летать, плавать и делать прочие действия. Вместо действий пока используй заглушки, выводящие в консоль текстовые сообщения. Учти, что утки могут быть разных видов, в том числе резиновая утка, деревянная, газетная, ручная утки, утки с утятами. Значит, не все разновидности уток могут в принципе крякать или летать или плавать или совершать другие действия. Некоторые утки могут со временем приобретать новые возможности, или утрачивать их. Учти, что тебе это приложение придется поддерживать длительное время, а начальство будет придумывать всё новые виды уток.
Мартин молча принялся за работу.
Хм. Есть много разных видов уток. Утки разных видов обладают разными наборами умений. Эти наборы могут со временем динамически меняться, уменьшаться или увеличиваться в количестве, заменяться на другие однотипные. Было бы замечательно сохранять умения уток, то есть поведение, то есть функции, в каких-нибудь переменных. В этом могут помочь лямбда-выражения. Например, можно сохранить поведение так:
Runnable fly = () -> System.out.println("Я летаю");
И затем выполнить его так:
fly.run();
Раз мы сохранили поведение в переменной, то его всегда можно будет заменить на любое другое поведение, даже динамически во время жизни объекта, не говоря уже о наследовании. А раз количество поведений также может меняться, то можно не заводить под каждое действие свою переменную, а сохранять их в динамически изменяемой структуре данных. Например, в Set. Или, лучше, в Map<T,U>, указывая в качестве ключа текстовый идентификатор поведения, а то мы это поведение потом не сможем отличить от других. Наверное, стоит для хранения и манипуляции поведений создать свой собственный класс и его объект встроить в поле базового класса уток. С него и начнем:
package patterns.and.lambdas.ducks;
import java.util.concurrent.ConcurrentSkipListMap;
/**
* Динамически изменяемый реестр способностей.
*
* @param <T>
* Любой функциональный интерфейс: Runnable, Callable<V>, Supplier<T>, BooleanSupplier,
* Consumer<T>, BiConsumer<T,U>, Predicate<T>, BiPredicate<T,U>, Function<T,R>,
* BiFunction<T,U,R>, UnaryOperator<T>, BinaryOperator<T>, другой из пакета
* {@link java.util.function} или свой собственный или любой другой функциональный.
*/
public class BehaviorRegistry<T> {
public ConcurrentSkipListMap<String, T> map = new ConcurrentSkipListMap<>();
public void add(final String behaveName, final T behaveFunc) {
this.assertContainsNameNot(behaveName);
BehaviorRegistry.assertArgNotNull(behaveFunc);
this.map.put(behaveName, behaveFunc);
}
public boolean contains(final String behaveName) {
BehaviorRegistry.assertArgNotNull(behaveName);
this.assertMapNotNull();
return this.map.containsKey(behaveName) && (this.map.get(behaveName) != null);
}
public T get(final String behaveName) {
this.assertContainsName(behaveName);
return this.map.get(behaveName);
}
public void replace(final String behaveName, final T behaveFunc) {
this.assertContainsName(behaveName);
BehaviorRegistry.assertArgNotNull(behaveFunc);
this.map.put(behaveName, behaveFunc);
}
public void remove(final String behaveName) {
this.assertContainsName(behaveName);
this.map.remove(behaveName);
}
protected static void assertArgNotNull(final Object arg) {
if ((arg instanceof String) && !"".equals(arg)) return;
if (arg != null) return;
throw new RuntimeException("Пустой аргумент.");
}
protected void assertContainsName(final String behaveName) {
BehaviorRegistry.assertArgNotNull(behaveName);
this.assertMapNotNull();
if (!this.contains(behaveName)) {
throw new RuntimeException("Способность "" + behaveName + "" не зарегистрирована.");
}
}
protected void assertContainsNameNot(final String behaveName) {
BehaviorRegistry.assertArgNotNull(behaveName);
this.assertMapNotNull();
if (this.contains(behaveName)) {
throw new RuntimeException("Способность "" + behaveName + "" уже зарегистрирована.");
}
}
protected void assertMapNotNull() {
if (this.map == null) throw new RuntimeException("Отсутствует map.");
}
}
Метод запуска способности мы в классе BehaviorRegistry реализовывать не будем, так как этот класс обобщен, поэтому мы не знаем, какой функциональный интерфейс лежит в основе его конкретного экземпляра, а значит не знаем название исполняющей функции: run(), call(), accept(), test(), apply() и т.д., и не знаем количество и типы аргументов для этих функций, и что эти функции возвращают.
Теперь воспользуемся им в классе Duck:
package patterns.and.lambdas.ducks;
public class Duck {
protected BehaviorRegistry<Runnable> behaviors = new BehaviorRegistry<>();
public void perform(final String behaveName) {
this.behaviors.get(behaveName).run();
}
/**
* Исполняет все зарегистрированные способности утки в порядке их добавления в реестр.
*/
public void performAll() {
this.behaviors.map.descendingKeySet().forEach(this::perform);
System.out.println("----------------------------------------------");
}
}</nobr>
В принципе, это всё. Даже наследование и иерархия классов не понадобилась. Просто будем создавать объекты Duck и в поле behaviors сохранять столько разных вариантов поведения, сколько нужно, и затем исполнять их когда нужно. Вот такая получилась архитектура:
На диаграмме нет ни одного прямоугольника ни для какого поведения, так как в данной архитектуре поведение утки является таким же анонимным значением, как и число 6 является анонимным значением для переменной int number. Архитектуре не важно, что делает и как устроено поведение, лишь бы оно удовлетворяло функциональному интерфейсу Runnable, как и переменной int number не важно, какое число в него сохраняют, лишь бы оно было целочисленным.
Эксплуатировать архитектуру будет проще, если заранее заготовить справочник некоторых поведений в виде enum:
package patterns.and.lambdas.ducks;
import java.util.function.BiConsumer;
/**
* Справочник стандартных способностей уток.
*/
public enum EBehaviors {
Display("представиться", () -> System.out.println("Я утка")),
Fly("летать", () -> System.out.println("Я летаю")),
Sleep("спать", () -> System.out.println("Z-z-z-z")),
Quack("крякать", () -> System.out.println("Кря-кря-кря")),
Propagate("размножаться", () -> System.out.println("O_o"));
public String name;
public Runnable func;
private EBehaviors(final String m_name, final Runnable m_func) {
this.name = m_name;
this.func = m_func;
}
public void sendTo(final BiConsumer<String, Runnable> someApiFunction) {
someApiFunction.accept(this.name, this.func);
}
public void sendTo(final BiConsumer<String, Runnable> someApiFunction, final Runnable m_func) {
someApiFunction.accept(this.name, m_func);
}
}
Теперь можно смело начинать конкретную эксплуатацию:
package patterns.and.lambdas.ducks;
import java.util.stream.Stream;
import patterns.and.lambdas.ducks.Duck;
import patterns.and.lambdas.ducks.EBehaviors;
public class Test {
public static void main(final String[] args) {
Runnable behaviorFunc = null;
final Duck mallardDuck = new Duck();
behaviorFunc = () -> System.out.println("Я кряква");
EBehaviors.Display.sendTo(mallardDuck.behaviors::add, behaviorFunc);
EBehaviors.Fly.sendTo(mallardDuck.behaviors::add);
EBehaviors.Quack.sendTo(mallardDuck.behaviors::add);
final Duck redheadDuck = new Duck();
behaviorFunc = () -> System.out.println("Я красноголовая утка");
EBehaviors.Display.sendTo(redheadDuck.behaviors::add, behaviorFunc);
EBehaviors.Fly.sendTo(redheadDuck.behaviors::add);
EBehaviors.Quack.sendTo(redheadDuck.behaviors::add);
final Duck rubberDuck = new Duck();
behaviorFunc = () -> System.out.println("Я резиновая утка");
EBehaviors.Display.sendTo(rubberDuck.behaviors::add, behaviorFunc);
EBehaviors.Quack.sendTo(rubberDuck.behaviors::add);
final Duck decoyDuck = new Duck();
behaviorFunc = () -> System.out.println("Я деревянная утка");
EBehaviors.Display.sendTo(decoyDuck.behaviors::add, behaviorFunc);
final Duck exclusiveDuck = new Duck();
behaviorFunc = () -> System.out.println("Я эксклюзивная утка");
EBehaviors.Display.sendTo(exclusiveDuck.behaviors::add, behaviorFunc);
behaviorFunc = () -> System.out.println("Я изрыгаю пламя <== эксклюзивное поведение");
exclusiveDuck.behaviors.add("палить огнем", behaviorFunc);
final Duck[] ducks = { mallardDuck, redheadDuck, rubberDuck, decoyDuck, exclusiveDuck };
// Простой тест всех уток.
System.out.println("############################################## 1");
Stream.of(ducks).forEachOrdered(Duck::performAll);
// Подменяем стандартное поведение созданной красноголовой утки.
System.out.println("############################################## 2");
behaviorFunc = () -> System.out.println("Кряяааааааа! <== подменили в RunTime");
EBehaviors.Display.sendTo(redheadDuck.behaviors::replace, behaviorFunc);
redheadDuck.performAll();
// Добавляем последействие к каждой способности кряквы.
System.out.println("############################################## 3");
EBehaviors.Propagate.sendTo(mallardDuck.behaviors::add);
EBehaviors.Sleep.sendTo(mallardDuck.behaviors::add);
mallardDuck.behaviors.map.forEach((name, func) -> {
final Runnable newFunc = () -> {
func.run();
System.out.println(" ^^^^^ последействие");
};
mallardDuck.behaviors.replace(name, newFunc);
});
mallardDuck.performAll();
// Удаляем все стандартные способности у всех уток, если они у них есть.
System.out.println("############################################## 4");
for (final Duck duck : ducks) {
Stream.of(EBehaviors.values()).map(val -> val.name).filter(duck.behaviors::contains)
.forEach(duck.behaviors::remove);
}
Stream.of(ducks).forEachOrdered(Duck::performAll);
}
}
И вот результат:
############################################## 1 Я кряква Я летаю Кря-кря-кря ---------------------------------------------- Я красноголовая утка Я летаю Кря-кря-кря ---------------------------------------------- Я резиновая утка Кря-кря-кря ---------------------------------------------- Я деревянная утка ---------------------------------------------- Я эксклюзивная утка Я изрыгаю пламя <== эксклюзивное поведение ---------------------------------------------- ############################################## 2 Кряяааааааа! <== подменили в RunTime Я летаю Кря-кря-кря ---------------------------------------------- ############################################## 3 Z-z-z-z ^^^^^ последействие O_o ^^^^^ последействие Я кряква ^^^^^ последействие Я летаю ^^^^^ последействие Кря-кря-кря ^^^^^ последействие ---------------------------------------------- ############################################## 4 ---------------------------------------------- ---------------------------------------------- ---------------------------------------------- ---------------------------------------------- Я изрыгаю пламя <== эксклюзивное поведение ----------------------------------------------
Мартин сдает работу Джо…
Джо: Это что? Разве это архитектура приложения? Всего-лишь два прямоугольника с одной связью на диаграмме. Когда я сделал свою версию программы, на моей диаграмме было двенадцать прямоугольников в самом начале, когда утки могли только крякать и летать, и их стало более трехсот через полгода, когда начальство придумало пятьдесят различных аспектов поведения, и для каждого аспекта по несколько различных вариантов реализаций! Я отделил изменяющееся от постоянного, и для каждой группы в архитектуре создал по своей иерархии, чтобы добиться гибкости и устранить дублирование кода. А у тебя, я смотрю, изменяющееся вообще вынесено за пределы архитектуры, в область анонимного и неопределенного, ты оставил в ней только постоянное. Однако, твое приложение по крайней мере не менее гибкое и расширяемое, чем мое. И дублирования кода я шибко не вижу, разве что при создании кряквы и красноголовой утки одинаковое добавление способностей крякать и летать можно было вынести в отдельный метод, но это мелочи. Я думаю, ты будешь очень быстро расширять это приложение, поэтому у меня есть для тебя еще работенка для нагрузки. Как насчет создания мониторов погоды для метеостанции? Я только что получил техтребования.
Автор: Дмитрий Мальков