[Из песочницы] Закон Деметры

Введение


На данный момент существует множество доказанных временем практик, помогающих разработчикам писать хорошо поддерживаемый, гибкий и удобно читаемый код. Закон Деметры — одна из таких практик.

Поскольку мы говорим об этом законе в контексте написания хорошего кода, перед его рассмотрением я хотел бы выразить свое понимание того, каким должен быть хороший код. Прочитав «Совершенный код» Макконнелла, я твёрдо уверовал в то, что главный технический императив разработки ПО — управление сложностью. Управление сложностью — довольно обширная тема, так как понятие сложности применимо на любом уровне проекта. Можно говорить о сложности в контексте общей архитектуры проекта, в контексте взаимосвязей модулей проекта, в контексте отдельного модуля, отдельного класса, отдельного метода. Но, пожалуй, большую часть времени разработчики сталкиваются со сложностью на уровне отдельных классов и методов, поэтому общее, упрощенное правило управления сложностью я бы сформулировал следующим образом: «Чем меньше вещей нужно держать в голове, глядя на отдельный участок кода, тем меньше сложность этого кода». То есть программный код нужно организовывать так, чтобы можно было безопасно работать с отдельными фрагментами по очереди (по возможности не думая об остальных фрагментах).

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

Закон Деметры — один из рецептов, помогающих в борьбе со сложностью (а следовательно и с увеличением гибкости вашего кода).

Закон Деметры


Закон Деметры говорит нам о том же, о чем в детстве говорили родители: «Не разговаривай с незнакомцами». А разговаривать можно вот с кем:

— С методами самого объекта.
 — С методами объектов, от которых объект зависит напрямую.
 — С созданными объектами.
 — С объектами, которые приходят в метод в качестве параметра.
 — С глобальными переменными (что лично мне не кажется верным, так как глобальные переменные во многом увеличивают общую сложность)

Мы рассмотрим конкретный пример, а после сделаем выводы о том, каким образом закон Деметры помогает нам в написании хорошего кода.

public class Seller {
    private PriceCalculator priceCalculator = new PriceCalculator();

    public Money sell(Set products, Wallet wallet) throws NotEnoughMoneyException {
        Money actualSum = wallet.getMoney(); // закон не нарушается, взаимодействие с объектом параметром метода (п. 4)
        Money requiredSum = priceCalculator.calculate(products);  // не нарушается, взаимодействие с методом объекта, от которых объект зависит напрямую (п. 2)

        if (actualSum.isLessThan(requiredSum)) { // нарушение закона.
            throw new NotEnoughMoneyException(actualSum, requiredSum);
        } else {
            return actualSum.subtract(requiredSum); // нарушение закона.
        }
    }
}

Закон нарушается потому, что из объекта, который приходит в метод параметром (Wallet), мы берём другой объект (actualSum) и позже вызываем на нем метод (isLessThan). То есть в конечном итоге получается цепочка: wallet.getMoney ().isLessThan (otherMoney).

Возможно у вас, как и у меня, возник определенный дискомфорт, глядя на метод, который принимает в качестве аргумента кошелёк и вытаскивает из него деньги.

Более корректная версия, удовлетворяющая закону выглядела бы так:

public class Seller {
    private PriceCalculator priceCalculator = new PriceCalculator();

    public Money sell(Set products, Money moneyForProducts) throws NotEnoughMoneyException {
        Money requiredSum = priceCalculator.calculate(products);

        if (moneyForProducts.isLessThan(requiredSum)) {
            throw new NotEnoughMoneyException(moneyForProducts, requiredSum);
        } else {
            return moneyForProducts.subtract(requiredSum);
        }
    }
}

Теперь мы передаём в метод sell список продуктов на покупку и деньги за эти продукты. Этот код кажется более естественным и понятным, он улучшает уровень абстракции метода:
sell(Set products, Wallet wallet) VS sell(Set products, Money moneyForProducts ).

Теперь этот код стало легче тестировать. Для тестирования достаточно создать объект Money, тогда как до этого необходимо было создать объект Wallet, затем объект Money, а затем положить Money в Wallet (возможно объект wallet был бы достаточно сложным и на него пришлось бы писать mock’и, что ещё больше увеличило бы общую сложность теста).

Пожалуй самое важное — это то, что сопряжение уменьшилось и метод стало куда легче использовать. Теперь опция продажи никак не зависит от наличия кошелька — она зависит только от наличия денег. Неважно откуда приходят деньги — из кошелка, кармана или кредитной карточки, этот метод не будет завязан на «хранилище» денег.

Когда я читал про закон Деметры, мне часто казалось, что соблюдение других принципов/инструментов написания хорошего кода (SOLID, DRY, KISS, паттерны и т.д.) просто не могут привести к ситуации, когда этот закон нарушается.

Лично для меня закон Деметры не является «правилом №1». Скорее так: если этот закон нарушается, для меня это повод задуматься о том, всё ли я делаю правильно.

Важно, чтобы соблюдение закона не являлось самоцелью, потому что сам закон не имеет семантики, и слепое соблюдение закона может только усложнить код. Например:

public class Seller {
    private PriceCalculator priceCalculator = new PriceCalculator();

    public Money sell(Set products, Wallet wallet ) throws NotEnoughMoneyException {
        Money requiredSum = priceCalculator.calculate(products);  // закон не нарушается, п. 2
        Money actualSum = wallet.getMoney();  // закон не нарушается, п. 4

        return subtract( actualSum, requiredSum);
    }

    private Money subtract(Money actualSum, Money requiredSum) throws NotEnoughMoneyException {
        if ( actualSum .isLessThan(requiredSum)) {  // закон не нарушается, п. 4
            throw new NotEnoughMoneyException( actualSum , requiredSum);
        } else {
            return moneyForProducts.subtract( actualSum );  // закон не нарушается, п. 4
        }
    }
}

Формально каждый метод удовлетворяет закону Деметры. Фактически — он обладает всеми теми же недостатками, какими обладал самый первый вариант (в публичном методе. Частый совет, который я встречал в подобных случаях — следить не только за соблюдением закона, но и за количеством зависимостей класса, количеством методов в классе и за числом аргументов в методах.

Выводы


Теперь давайте обобщим, какие именно преимущества мы получаем при соблюдении закона.

— Уменьшение связанности кода. Достигается за счёт того, что классы общаются только со своими близкими родственниками (с собой, аргументами метода и прямыми зависимостями).

— Сокрытие (структурной) информации. Благодаря закону Деметры мы избегаем ситуаций, когда мы вытаскиваем из объекта его части (деньги из кошелька в нашем примере) и манипулируем ими. После применения закона мы манипулируем только этими частями (только деньгами). Это позволяет избежать лишних знаний, используя лишь то, что нам нужно (и как правило улучшает общий уровень абстракции).

— Локализация информации. При соблюдении закона мы исключаем цепочки в виде someClass.getOther ().getAnother ().callMethod (), ограничивая круг возможных участников общения. Это помогает гораздо легче ориентироваться в написанном коде, уменьшая интеллектуальное напряжение при чтении кода.

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

Когда можно не использовать закон Деметры?


 — При взаимодействии с core jdk классами. Например, seller.toString ().toLowerCase () формально нарушает закон Деметры, но если он используется, например, в контексте логирования, в этом нет ничего страшного.

— В DTO-классах. Причина создания DTO классов — это трансфер объектов, и цепочки вызова методов DTO-классов противоречат закону Деметры, но вписываются в идею самих DTO-объектов.

— В коллекциях. warehouses.get (i).getName () — формально тоже противоречит закону, но не противоречит идее коллекции.

— Руководствуйтесь здравым смыслом ;)

Комментарии (1)

  • 16 января 2017 в 14:23

    0

    Дополню альтернативной и спорной точкой зрения.

    http://www.yegor256.com/2016/07/18/law-of-demeter.html

© Habrahabr.ru