Функциональное мышление: Связывание и композиция, часть 1

Обзор ограничений, возникающих при использовании абстракций, основанных на связывании

При ежедневном использовании абстракций определенного типа (например, ООП) сложно понять, в каких ситуациях применение данного подхода приводит к появлению не самых оптимальных решений. Эта статья - первая в серии из двух статей, в которых исследуются некоторые ограничения объектно-ориентированного мышления с точки зрения повторного использования кода и сравнивается подход, используемый в ООП, с более функциональными подходами, например, с композицией.

Нил Форд, Архитектор приложений, ThoughtWorks

Нил Форд (Neal Ford) работает архитектором приложений в ThoughtWorks, революционной компании, предоставляющей профессиональные IT-услуги и помогающей талантливым людям по всему миру эффективнее использовать программное обеспечение. Он также является проектировщиком и разработчиком приложений, учебных материалов, журнальных статей, учебных курсов, видео/DVD-презентаций и автором книг "Разработка с Delphi: Объектно-ориентированный подход", "JBuilder 3 Unleashed" и "Искусство разработки Web-приложений на Java". Он специализируется на консультациях по построению широкомасштабных корпоративных приложений. Он также является общепризнанным докладчиком и выступал на многочисленных конференциях разработчиков по всему миру. С ним можно связаться по адресу: nford@thoughtworks.com.



12.05.2012

Об этой серии статей

Эта серия статей призвана переориентировать читателя в "функциональном" направлении и помочь ему взглянуть на проблемы под новым углом, чтобы улучшить повседневное написание кода. В ней изучаются принципы функционального программирования и инфраструктуры, позволяющие использовать функциональное программирование в языке Java. Так, в статьях рассматриваются функциональные языки программирования, работающие на виртуальной Java-машине, и некоторые направления будущего развития языков программирования. Эта серия статей предназначена для программистов, знающих язык Java и работу его абстракций, но не имеющих опыта в использовании функциональных языков.

Объектно-ориентированное программирование затрудняет понимание кода за счет инкапсуляции "движущихся частей". Функциональное программирование облегчает понимание кода за счет сокращения числа "движущихся частей" – цитата из твиттера Майкла Фиверса (Michael Feathers), автора книги Working with Legacy Code.

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

Одна из задач, стоящих перед объектно-ориентированным программированием, - это облегчить инкапсуляцию состояния и последующую работу с ним. Поэтому ОО-абстракции стараются решать стандартные проблемы с помощью состояния, навязывая использование множества взаимодействующих классов, т.е. того, что в приведенной выше цитате Майкла Фезерса было названо "движущимися частями". Функциональное программирование пытается минимизировать количество "движущихся частей" за счет композиции вместо того, чтобы заниматься связыванием структур между собой. Это достаточно тонкое различие, и у программистов, в основном использующих объектно-ориентированные языки, могут возникнуть трудности с его осознанием.

Повторное использование кода за счет структуры

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

Непреднамеренное дублирование кода

Чтобы проиллюстрировать повторное использование кода и его ограничения, необходимо вернутся к одной из версий классификатора чисел, которая использовалась в предыдущих статьях для иллюстрации структуры кода и стиля его написания. Классификатор чисел определяет, является ли положительное число избыточным, совершенным или неполным. Число является совершенным, если сумма всех его делителей (за исключением его самого) равна данному числу. Если сумма делителей числа (опять же за исключением самого числа) больше него, число является избыточным, а если меньше – то неполным.

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

В листинге 1 представлен классификатор чисел, написанный в императивном стиле:

Листинг 1. Императивный классификатор чисел
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import static java.lang.Math.sqrt;

public class ClassifierAlpha {
    private int number;

    public ClassifierAlpha(int number) {
        this.number = number;
    }

    public boolean isFactor(int potential_factor) {
        return number % potential_factor == 0;
    }

    public Set<Integer> factors() {
        HashSet<Integer> factors = new HashSet<Integer>();
        for (int i = 1; i <= sqrt(number); i++)
            if (isFactor(i)) {
                factors.add(i);
                factors.add(number / i);

            }
        return factors;
    }

    static public int sum(Set<Integer> factors) {
        Iterator it = factors.iterator();
        int sum = 0;
        while (it.hasNext())
            sum += (Integer) it.next();
        return sum;
    }

    public boolean isPerfect() {
        return sum(factors()) - number == number;
    }

    public boolean isAbundant() {
        return sum(factors()) - number > number;
    }

    public boolean isDeficient() {
        return sum(factors()) - number < number;
    }

}

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

Листинг 2. Императивная программа, проверяющая числа на простоту
import java.util.HashSet;
import java.util.Set;

import static java.lang.Math.sqrt;

public class PrimeAlpha {
    private int number;

    public PrimeAlpha(int number) {
        this.number = number;
    }

    public boolean isPrime() {
        Set<Integer> primeSet = new HashSet<Integer>() {{
            add(1); add(number);}};
        return number > 1 &&
                factors().equals(primeSet);
    }

    public boolean isFactor(int potential_factor) {
        return number % potential_factor == 0;
    }

    public Set<Integer> factors() {
        HashSet<Integer> factors = new HashSet<Integer>();
        for (int i = 1; i <= sqrt(number); i++)
            if (isFactor(i)) {
                factors.add(i);
                factors.add(number / i);
            }
        return factors;
    }
}

В листинге 2 стоит выделить несколько моментов. Во-первых, это немного необычная инициализация, выполняющаяся в методе isPrime(). В данном примере используется прием instance initializer (инициализатор объекта), доступный в Java и входящий в арсенал функционального программирования. Дополнительную информацию по этому вопросу можно найти в статье "Evolutionary architecture and emergent design: Leveraging reusable code, Part 2" (см. раздел "Ресурсы").

Также в листинге 2 интерес представляют два метода: isFactor() и factors(). Можно заметить, что они полностью совпадают с аналогичными методами в классе ClassifierAlpha (см. листинг 1). Это естественное последствие независимой реализации двух сходных решений, которое приводит к дублированию уже существующей функциональности.

Устранение дублирования с помощью рефакторинга

Для того чтобы избавиться от подобного дублирования, необходимо выполнить рефакторинг и перенести данный код в отдельный класс Factors, как показано в листинге 3.

Листинг 3. Общий код, подвергшийся рефакторингу
import java.util.Set;
import static java.lang.Math.sqrt;
import java.util.HashSet;

public class FactorsBeta {
    protected int number;

    public FactorsBeta(int number) {
        this.number = number;
    }

    public boolean isFactor(int potential_factor) {
        return number % potential_factor == 0;
    }

    public Set<Integer> getFactors() {
        HashSet<Integer> factors = new HashSet<Integer>();
        for (int i = 1; i <= sqrt(number); i++)
            if (isFactor(i)) {
                factors.add(i);
                factors.add(number / i);
            }
        return factors;
    }
}

Код в листинге 3 получен в результате применения приема рефакторинга Extract Superclass (извлечение суперкласса). Можно заметить, что, поскольку оба извлеченных метода используют объектную переменную number, она была перенесена в суперкласс. В ходе проведения этого рефакторинга среда разработки спросила у меня, как бы я хотел организовать доступ к этой переменной (через get/set методы, модификатор protected и т.д.). Я выбрал модификатор protected, что привело к добавлению в класс переменной number и созданию конструктора для установки её значения.

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

Листинг 4. Упрощенная версия классификатора чисел
import java.util.Iterator;
import java.util.Set;

public class ClassifierBeta extends FactorsBeta {

    public ClassifierBeta(int number) {
        super(number);
    }

    public int sum() {
        Iterator it = getFactors().iterator();
        int sum = 0;
        while (it.hasNext())
            sum += (Integer) it.next();
        return sum;
    }

    public boolean isPerfect() {
        return sum() - number == number;
    }

    public boolean isAbundant() {
        return sum() - number > number;
    }

    public boolean isDeficient() {
        return sum() - number < number;
    }

}

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

Листинг 5. Программа для проверки чисел на простоту после выполнения рефакторинга
import java.util.HashSet;
import java.util.Set;

public class PrimeBeta extends FactorsBeta {
    public PrimeBeta(int number) {
        super(number);
    }

    public boolean isPrime() {
        Set<Integer> primeSet = new HashSet<Integer>() {{
            add(1); add(number);}};
        return getFactors().equals(primeSet);
    }
}

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

Этот пример повторного использования кода основан на связывании (coupling), когда два элемента (в данном случае классы) соединяются через общее состояние: поле number и метод getFactors(), объявленные в суперклассе. Другими словами, такой подход работает за счет правил связывания, встроенных в сам язык. Принцип объектной ориентации определяет стили взаимодействия через связывание (например, как осуществляется доступ к полям через наследование), так что у вас уже имеются правила, определяющие, как объекты могут быть связаны между собой. Это хорошо, поскольку в результате вы можете проектировать функциональность в единообразном стиле. Постарайтесь понять меня, так как я не пытаюсь доказать, что использование наследования – это плохо. Нет, я только предполагаю, что наследование слишком часто используется в объектно-ориентированных языках и поэтому заслоняет другие более эффективные абстракции.


Повторное использование кода за счет композиции

Во второй статье данного цикла была представлена функциональная версия классификатора чисел на языке Java, приведенная в листинге 6.

Листинг 6. Функциональная версия классификатора чисел
public class FClassifier {

    static public boolean isFactor(int number, int potential_factor) {
        return number % potential_factor == 0;
    }

    static public Set<Integer> factors(int number) {
        HashSet<Integer> factors = new HashSet<Integer>();
        for (int i = 1; i <= sqrt(number); i++)
            if (isFactor(number, i)) {
                factors.add(i);
                factors.add(number / i);
            }
        return factors;
    }

    public static int sumOfFactors(int number) {
        Iterator<Integer> it = factors(number).iterator();
        int sum = 0;
        while (it.hasNext())
            sum += it.next();
        return sum;
    }

    public static boolean isPerfect(int number) {
        return sumOfFactors(number) - number == number;
    }

    public static boolean isAbundant(int number) {
        return sumOfFactors(number) - number > number;
    }

    public static boolean isDeficient(int number) {
        return sumOfFactors(number) - number < number;
    }
}

Также у меня имеется и функциональная версия программы для проверки чисел на простоту, в которой используются только функции, и отсутствует общее состояние. В листинге 7 приведен метод isPrime() из данной реализации, а остальной её код совпадает с кодом одноименных методов, представленных в листинге 6.

Листинг 7. Функциональная версия программы для проверки чисел на простоту
public static boolean isPrime(int number) {
    Set<Integer> factors = factors(number);
    return number > 1 &&
            factors.size() == 2 &&
            factors.contains(1) &&
            factors.contains(number);
}

По аналогии с императивными версиями я извлеку повторяющийся код в отдельный класс Factors, изменив название метода factors() для большей понятности, как показано в листинге 8.

Листинг 8. Функциональный класс Factors, полученный в результате рефакторинга
import java.util.HashSet;
import java.util.Set;
import static java.lang.Math.sqrt;

public class Factors {
    static public boolean isFactor(int number, int potential_factor) {
        return number % potential_factor == 0;
    }

    static public Set<Integer> of(int number) {
        HashSet<Integer> factors = new HashSet<Integer>();
        for (int i = 1; i <= sqrt(number); i++)
            if (isFactor(number, i)) {
                factors.add(i);
                factors.add(number / i);
            }
        return factors;
    }
}

Так как в функциональной версии все состояние передается через параметры, то при "извлечении" нового класса не возникает никакого общего состояния. После извлечения этого класса я могу выполнить рефакторинг классификатора чисел и программы для проверки чисел на простоту, чтобы они перешли на использование созданного класса. В листинге 9 показана обновленная реализация классификатора чисел.

Листинг 9. Классификатор чисел после рефакторинга
public class FClassifier {

    public static int sumOfFactors(int number) {
        Iterator<Integer> it = Factors.of(number).iterator();
        int sum = 0;
        while (it.hasNext())
            sum += it.next();
        return sum;
    }

    public static boolean isPerfect(int number) {
        return sumOfFactors(number) - number == number;
    }

    public static boolean isAbundant(int number) {
        return sumOfFactors(number) - number > number;
    }

    public static boolean isDeficient(int number) {
        return sumOfFactors(number) - number < number;
    }
}

В листинге 10 приведена обновленная реализация программы для проверки чисел на простоту.

Листинг 10. Программа для проверки чисел на простоту после рефакторинга
import java.util.Set;

public class FPrime {

    public static boolean isPrime(int number) {
        Set<Integer> factors = Factors.of(number);
        return number > 1 &&
                factors.size() == 2 &&
                factors.contains(1) &&
                factors.contains(number);
    }
}

Заметьте, что я не пользовался специальными библиотеками или языками, чтобы сделать обновленные версии программ более функциональными. Я добился этого, обеспечив повторное использование кода за счет композиции, а не связывания. В коде, представленном в листингах 9 и 10, класс Factors используется исключительно в отдельных методах, без обращения к общему состоянию.

Хотя различие между связыванием и композицией и трудно уловить, его значение очень велико. В таких простых примерах, как были представлены выше, можно легко «выявить» структуру кода. Однако после того как вы выполните рефакторинг большого объема кода, связывание будет присутствовать в нем повсеместно, так как оно является одним из механизмов повторного использования, применяемых в ОО-языках. Трудности, связанные с пониманием чрезмерно сложных связанных структур, могут помешать повторному использованию кода в объектно-ориентированных языках, позволив эффективно применять повторное использование кода только в таких четко определенных областях, как объектно-реляционное преобразование данных и библиотеки GUI-компонентов. В случае же с менее структурированным Java-кодом (например, используемым в бизнес-приложениях) оказывается невозможно добиться подобного уровня повторного использования.

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


Заключение

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

Ресурсы

Научиться

  • Functional thinking: Coupling and composition, Part 1: оригинал статьи (EN).
  • The Productive Programmer (Neal Ford, O'Reilly Media, 2008): новая книга Нила Форда, в которой более подробно рассматриваются некоторые вопросы из данной серии статей.
  • Подкаст: Stuart Halloway о Clojure: узнайте больше о Clojure и двух основных причинах, способствующих его широкому распространению и росту его популярности.
  • The busy Java developer's guide to Scala: узнайте больше о Scala из этой серии статей Теда Ньюарда, также опубликованной на developerWorks.
  • Посетите магазин книг, посвященных ИТ-технологиям и различным аспектам программирования.

Получить продукты и технологии

  • Functional Java: Functional Java is a framework that adds many functional language constructs to Java.

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=815504
ArticleTitle=Функциональное мышление: Связывание и композиция, часть 1
publish-date=05122012