пятница, 21 апреля 2017 г.

Функциональное программирование в Java 8 (Часть 4): Splitter

Сегодня, мы напишем несколько полезных примеров, используя предыдущие главы. Вы потренируюте парадигмы из прошлых частей и узнаете, как оптимизировать процесс выполнения вашей программы.

Пример: Разделение stream, используя специальный фильтр

Вы могли сталкиваться с такой проблемой раньше: у вас есть коллекция объектов, и вы хотите разделить ее с помощью фильтра. После вы хотите выполнить действия с элементами, которые подошли условию фильтра и другое действие с элементами, которые не подошли.

Обычныи (и медленный) подход

public <T> void splitAndPerform(Collection<T> items, Predicate<T> splitBy, Consumer<T> passed, Consumer<T> notPassed) {
    items.stream()
        .filter(splitBy)
        .forEach(passed);
    items.stream()
        .filter(splitBy.negate())
        .forEach(notPassed);
}
Решение работает, но достаточно медленно. Сложность  выполнения O(2n), т.к. мы проходим по коллекции два раза:  один проход, для тех элементов, которые удовлетворяют условиям, второй, для тех, которые не удовлетворяют.
Но что, если мы сами созданим разделитель. Он сортирует элементы коллекции, в зависимости от того, проходят ли они условия или нет, разделит их на 2 списка. Это позволит снизить сложность выполнения до О(n), т.к. проход по списку происходит один раз. Давайте сделаем это.

Более быстрый подход.

1. Разделение коллекции.
Как вы уже догадались, сначала нужно создать разделение.
В нашей функции splitBy(), мы хотим получить Predicate<T> в качестве парамета и вернуть новый объект Splitter, каоторый представляет из себя объект из двух списков. Один список содержит объекты, которые подошли фильру, второй - которые не подошли.
public class Splitter<T> {
    private List<T> passed;
    private List<T> notPassed;
    private Splitter(List<T> passed, List<T> notPassed) {
        this.passed = passed;
        this.notPassed = notPassed;
    }
    public static <T> Splitter<T> splitBy(Collection<T> items,Predicate<T> test) {
        List<T> passed = new LinkedList<T>();
        List<T> notPassed = new LinkedList<T>();
        items.stream()
                .forEach(item -> {
                    if(test.test(item)){
                        passed.add(item);
                        return;
                    }
                    notPassed.add(item);
                });
        return new Splitter<T>(passed, notPassed);
    }
}
Как вы заметили, мы использовали паттер Фабрика для создания Splitter. Теперь надо создать сам объект и наделить его функционалом.
2. Работа с разделенными списками.
Мы хотим работать со списками таким же способом, как со Stream. Но мы не хотим пересоздавать весь функционал, котоый есть у Stream, для наших списков. Здесь нам пригодится паттерн, я узнал о нем из этого видео и это очень клевый способ использовать лямбды. Фактически, мы создаем две функции, workOnPassedItems и workOnNotPassedItems. Они принимают Consumer<Stream<T>>. Следовательно, мы можем создавать лямды  и работать внутри нее с нормальным потоком. Этот метод будет применен к обоим спискам.

public class Splitter<T> {
    //...
    public Splitter<T> workWithPassed(Consumer<Stream<T>> func) {
        func.accept(passed.stream());
        return this;
    }
    public Splitter<T> workWithNotPassed(Consumer<Stream<T>> func) {
        func.accept(notPassed.stream());
        return this;
    }
}
Мы использовали шаблон Каскад, чтобы сделать использование различных методов более приятным. Вы увидите это в примере ниже.
В общем-то этои есть наш разделитель (Splitter). Рассмотрим несколько примеров использования.

Пример 1. Вывод числе и возведение в квадрат всех нечетных

В нашем первом примере мы хотим оперировать со списком чисел. Мы хотим печатать простые числа и возвести в квадрат все нечетные перед тем как их напечатать. Сначала мы раделяем числа не четные и нечетным. Далее работаем с каждым списком как описывалось.

public void workOnNumbers() {
    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    Splitter.splitBy(numbers, num -> num%2 == 0)
            .workWithPassed(passed ->
                    passed.forEach(even -> System.out.println("" + even + " -> " + even)))
            .workWithNotPassed(notPassed ->
                        notPassed.forEach(odd -> System.out.println(odd + " -> " + (odd * odd))
                    ));
}

Пример 2. Отправка всем победителям уведомление и проигравшим другое

У нас есть список кандидатов. У все есть метода hasWon(), который возвращает boolean. Мы хотим разделить список на победителей и проигравших. Далее мы хотим разослать всем уведомления о победе или поражении. 

public void sendEmails(List<Candidates> candidates) {
    Splitter.splitBy(candidates, Candidates::hasWon)
            .workWithPassed(winners ->
                    winners.forEach(winner -> Email.send(winner.getEmail(), "You won!"))
    )
    .workWithNotPassed(losers ->
            losers.forEach(loser -> Email.send(loser.getEmail(), "You lost, sorry!"))
    );
}

об использовании partitioningBy

Получая обратную связь на данную статью, выявилось, что  Stream.collect(Collectors.partitioningBy(Predicate<T> test)) подходит нашему случаю и я с этим полностью согласен.
Происходит разделение Stream, в зависимости от test. Т.о. для нас map будет выглядеть как то так {true: passed, false: notPassed}. Далее мы получаем два списка из map и продолжаем. Новый метод будет выглядеть так:

public static <T> Splitter<T> splitBy(Collection<T> items,Predicate<T> test) {
        Map<Boolean, List<T>> map = items.stream()
                .collect(Collectors.partitioningBy(test));
        return new Splitter<T>(map.get(true), map.get(false));
}
И я должен признать, что данный метод выглядит лучше.

Какие права на существования у Splitter

Цель Splitter - продемонстировать как вы можете работать в функцией, как с объектом. Его цель не в том, чтобы заменить методы в JDK. Это класс для изучения и экспериментов. Если вы хотите эксперементировать - делайте это. Пожалуйста, оставьте комментарий к тому, что вы узнали или где вы оптимизировали класс, чтобы другие могли учиться на этом. Так же мы рассмотрели несколько паттернов. Они помогают сделать синтекс более приятным.

Вывод

На сегодня это все.
Мы научились создавать наш первый полезный класс с помощью Stream. Мы также оптимизировали среду выполнения нашей программы. Так же мы закрепили наши знания о шаблонах проектирования, таких как фабрика и каскадный шаблон. Наконец, мы опробовали наш сплиттер с некоторыми примерами.

PS это мой перевод данной статьи

Комментариев нет :

Отправить комментарий