Pull to refresh

Comments 35

Мне кажется в реальной жизни Джо бы так просто не сдался, придумал бы контраргументы и таки заставил бы освоить Мартина паттерн Стратегия )

Мартин применил паттерн, даже не зная о нем.
Минус предложенного решения: слабо сочетается с состоянием.
Как изменить поведение утки в зависимости от сытости? Если у нас сто уровней сытости и пять вариантов поведения?

Ну вообще этой теме уже много лет. Мне кажется, Mario Fusco ее полностью рассмотрел уже года три назад. Даже странно что тут так мало было написано.

Автор, такая гибкая архитектура существовала со времён указателей на функции в С а написанное выше можно в Java 8 написать намного короче и чище:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class Duck extends ArrayList<Runnable> implements Runnable{
	
	
	@Override
	public void run() {
		this.stream().forEach(r-> r.run());		
	}
	
	
	public static void main(String[] args) {
		
		final Duck mallardDuck = new Duck();
		mallardDuck.add(() -> System.out.println("Я кряква"));
		mallardDuck.add(() -> System.out.println("Я летаю"));
		mallardDuck.add(() -> System.out.println("Кря-кря-кря"));
		
		final Duck rubberDuck = new Duck();
		rubberDuck.add(() -> System.out.println("Я резиновая утка"));
		rubberDuck.add(() -> System.out.println("Кря-кря-кря"));
		
		 final Duck exclusiveDuck = new Duck();
		 exclusiveDuck.add(() -> System.out.println("Я эксклюзивная утка"));
		 exclusiveDuck.add(() -> System.out.println("Я изрыгаю пламя"));
		 
		 
		 List<Duck> ducks = Arrays.asList(mallardDuck,rubberDuck,exclusiveDuck);
		 
		 ducks.forEach(duck -> {System.out.println("####"); duck.run();} );
		 
	}	
	
	
}


####
Я кряква
Я летаю
Кря-кря-кря
####
Я резиновая утка
Кря-кря-кря
####
Я эксклюзивная утка
Я изрыгаю пламя
Данная публикация относится только к языку Java. Ваш код прост и чист, но он примитивен. Например, заставьте mallardDuck крякнуть и не более того. Магические числа не используйте.
а, так вы мне полностью скажите задачу и я напишу простой и чистый код полностью покрывая её а приведённое в статье считаю избыточным и хуже читабельным.
ладно, самый примитивный способ заставить только крякнуть это
Map<String, Integer> actMap,
public void act(String action){
this.get(actMap.get(action)).run();
}

можно в Java 8 написать намного короче и чище:
public class Duck extends ArrayList<Runnable> implements Runnable{

Можно ли утку, рождённую от ArrayList, назвать чистой?
У меня сомнения на этот счёт.

www.martinfowler.com/ieeeSoftware/explicit.pdf
в архитектуре с кучей лямбд потом будет очень сложно разобраться, хотя я не против лямбд, но для статического поведения и улучшения читаемости лучше определять явные интерфейсы, именно с явным именем IFlying, IMoving, и давать их подклассам… а репозиторий если уж динамически модифицируемый нужен, он не существует сам по себе, гдето юзер в UI / или api если это серв, — будет вызывать ваши методы, ему надо показать ваши объекты, что там у каждого объекта за список behaviour-ов, и что он не может делать, и это в коде также не составит труда определить явно через тот же список behaviours в каждом объекте и реверсного списка объектов которые имплементируют конкретный интерфейс (возможно по типу интерфейса или enum)
Допустим, подкласс MallardDuck наследует явный интерфейс IFlying и может летать, если задать конкретную реализацию. Далее подкласс BroilerDuck расширяет класс MallardDuck. BroilerDuck — это домашняя утка, которая слишком тяжела, чтобы летать. У BroilerDuck будут свои подклассы и все они, вместе с BroilerDuck, летать не будут никогда. Однако интерфейс IFlying они все наследуют. Это будет Legacy-интерфейс, мертвый груз, от которого хотелось бы избавиться перманентно. Как избавляться от принудительно унаследованных ненужных интерфейсов?
никак, значит неверно то что Duck имеет интерфейс IFlying поскольку не все они летают.
разделяем уток на интерфейсы FlyingDuck и NonFlyingDuck, и т.п.
В яве будет несколько неудобно потому что нет двойного наследования.
Это в статической типизации.
Можно просто вообще иметь один(!) класс Duck менять методы в рантайме имея Map<Key,Function> dict наподобии более высоких языков ака JS или Python.
Можно просто вообще иметь один(!) класс Duck менять методы в рантайме имея Map<Key,Function> dict наподобии более высоких языков ака JS или Python.

Так в статье и сделано
в языках типа JS, вы потом запутаетесь отслеживать где тут какой Function передается и куда, и отслеживать баги и самое главное вникать как же оно все работает, где вход где выход и т.п
не думайте что это автоматом удобно всегда. написать 1 раз и только автору понимать — да удобней. но поддерживать даже автору, а тем более новому человеку такой код — ад.

к тому же у вас разные поведения с разным числом аргументов могут быть. это уже Func<T,T2,T3> а гдето Func<T,T2> и т.п
если точно также все объединить под максимальное число аргументов (типо Func<T,T2,T3>) тогда у вас будут неиспользуемые аргументы в реализациях которым они не нужны — что тоже запутывает (типо читаешь код и думаешь — а зачем это суда передается), а если более явно определять где-то будет список Func, гдето список Func<T,T2> гдето 3х аргументов и т.п. то это уже не 1 Map а 3 штуки! И поиск надо будет делать динамически во всех Map, и добавлять только в конкретную. И т.п.
Еще раз. Explicit принцип придумал не я, а Фаулер. Вы будете спорить с ним? О том что он не нужен? Чтобы всегда делать кучу неявного кода через указатели на методы? Серьезно?

Далее, конкретно по вашему заявлению об «BroilerDuck расширяет класс MallardDuck… У BroilerDuck будут свои подклассы и все они, вместе с BroilerDuck, летать не будут никогда.» это неправильный подход, не надо их наследовать.
Вы объявляете класс BroilerDuck implements IFlying, и никого от нее не наследуете. Если вы хотите иметь общую реализацию в каких-то классах, делайте через компонентное наследование.

«Как избавляться от принудительно унаследованных ненужных интерфейсов?» ну не наследоваться просто так.
UFO landed and left these words here
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;

public class Duck {
    private final String name;

    public Duck(String name) {
        this.name = name;
    }

    private Map<Language, Consumer<Duck>> greetings = new HashMap<>();

    public void addGreeting(Language language, Consumer<Duck> greeting) {
        greetings.put(language, greeting);
    }

    public void greet(Language language) {
        greetings.get(language).accept(this);
    }

    public String getName() {
        return name;
    }

    public static void main(String[] args) {
        Duck donald = new Duck("Donald");
        donald.addGreeting(Language.RUS, duck -> System.out.println("Привет, я " + duck.getName()));
        donald.addGreeting(Language.ENG, duck -> System.out.println("Hello, I'm " + duck.getName()));

        donald.greet(Language.RUS);
        donald.greet(Language.ENG);
    }

    public enum Language {
        RUS,
        ENG
    }
}
UFO landed and left these words here
Мне как-то ни разу не приходилось использовать Стратегию всместе с наследованием, но если очень хочется:
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;

public abstract class Duck<T extends Duck> {
    private final String name;

    public Duck(String name) {
        this.name = name;
    }

    abstract public T getDuck();

    private Map<Language, Consumer<T>> greetings = new HashMap<>();

    public void addGreeting(Language language, Consumer<T> greeting) {
        greetings.put(language, greeting);
    }

    public void greet(Language language) {
        greetings.get(language).accept(getDuck());
    }

    public String getName() {
        return name;
    }

    public static void main(String[] args) {
        DuckWithAge donald = new DuckWithAge("Donald", 18);
        donald.addGreeting(Language.RUS, duck -> System.out.println("Привет, я " + duck.getName() + " мне " + duck.getAge()));
        donald.addGreeting(Language.ENG, duck -> System.out.println("Hello, I'm " + duck.getName() + " I'm " + duck.getAge()));

        donald.greet(Language.RUS);
        donald.greet(Language.ENG);
    }

    public enum Language {
        RUS,
        ENG
    }

    public static class DuckWithAge extends Duck<DuckWithAge> {
        private final int age;

        public DuckWithAge(String name, int age) {
            super(name);
            this.age = age;
        }

        public int getAge() {
            return age;
        }

        @Override
        public DuckWithAge getDuck() {
            return this;
        }
    }
}

Наверное, есть и более элегантные варианты, в Kotlin я бы просто extension сделал бы:
fun <T: Duck> T.add(language: Language, greeting:(T) -> Unit) {}
UFO landed and left these words here
По-моему, прекрасный пример как динамическая типизация может упростить код. В примере можно легко зарегистрировать имплементацю кряканья в способность летать…

… а теперь давайте попробуем сделать так, чтобы в конкретный метод ("пруд") можно было передать только уток, у которых есть поведение "плавать", и это ограничение проверялось статически.

в классической имплементации Стратегии это тоже невозможно, так как Duck всё-равно будет один, без иерархии

… а у вас и есть "классическая имплементация паттерна Стратегия":


The strategy pattern
  • defines a family of algorithms,
  • encapsulates each algorithm, and
  • makes the algorithms interchangeable within that family.


Strategy lets the algorithm vary independently from clients that use it

Тот факт, что она сделана на делегатах вместо классов, всего лишь следствие того, что у вас в языке есть легко доступные делегаты (и, заметим, у такой реализации тоже есть недостатки).

плюсую. Надо явно определить интерфейс ISwimingDuck, и только утки реально уменющие плавать туда передадутся. К тому же я писал в комменте выше, что
а репозиторий если уж динамически модифицируемый нужен, он не существует сам по себе, гдето юзер в UI / или api если это серв, — будет вызывать ваши методы, ему надо показать ваши объекты, что там у каждого объекта за список behaviour-ов, и что он не может делать, и это в коде также не составит труда определить явно через тот же список behaviours в каждом объекте и реверсного списка объектов которые имплементируют конкретный интерфейс (возможно по типу интерфейса или enum)

т.е. вот есть юзер, у него есть UI, в нем вам надо показать список уток которые этот интерфейс имплементируют, через реверс-маппинг т.е List и туда заносить только могущить плавать.

Другое дело что иногда если есть редактор, скажем для игры, юзер может сам настраивать свойства уток, возможность их делать или не делать что-то, то тут лучше пойдет компонентный подход, как в том же unity3d, хотя и там не идеально сделано.
А зачем аргументам ставить final? Разве компилятор недостаточно умён, чтобы определить, что это effectively final, и сделать нужные оптимизации, если надо, без замусоривания кода?
UFO landed and left these words here
UFO landed and left these words here
… а теперь давайте попробуем сделать так что бы методы утки принимали различные аргументы
fly(City to, float speed);
sleep(int hours);
Я думал об этом. Планировал добавть в класс BehaviorRegistry возможность хранить и обрабатывать разные функциональные интерфейсы.
Скорее всего ваше новое решение либо не будет выдерживать никакой критики, либо будет очень далеко от изначального варианта.

Почему у BehaviorRegistry публичный внутренний map? Инкапсуляция не нужна?
Зачем вызывать add через sendTo? Чтобы выпендриться?


В целом получился Publisher-Subscriber, ну ок.


А ещё забыли реализовать пруд из условия и абсолютно всю часть взаимодействия с ним.

С вызовом add(arg1, arg2) через контейнер.sendTo(метод_вроде_add) я в данном случае выпендрился. Однако если методов вроде add(), одинаково принимающих два аргумента, будет штук пять и более, а у меня будут контейнеры EBehaviors, в полях которых присутствуют все нужные аргументы и один метод sendTo(), то я, желая передавать аргументом цельный контейнер в методы вроде add(), сэкономлю на их перегрузках.
Мартин не пытался реализовывать паттерны проектирования, так как не знал о них ничего. Ни Стратегию, ни Command, ни Publisher-Subscriber. Он сделал так, как подсказывали ему нововведенные лямбда-выражения, стандартные функциональные интерфейсы (из OpenJDK) и развитые стандартные динамические структуры данных (из OpenJDK), о которых можно узнать в обычном исчерпывающем современном учебнике по языку Java. Он не думал слишком долго об ахритектуре, не продумывал ее, не рефакторил, а сделал тяп-ляп. Если он случайно при этом сделал нечто похожее на какой-то шаблон проектирования, то это говорит о том, что этот шаблон стал тривиальным решением для Java 8 (OpenJDK).
UFO landed and left these words here
Sign up to leave a comment.

Articles