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

в 11:21, , рубрики: java, Совершенный код

Введение

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

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

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

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

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

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

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

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

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

    public Money sell(Set<Product> 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<Product> 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<Product> products, Wallet wallet) VS sell(Set<Product> 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<Product> 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() — формально тоже противоречит закону, но не противоречит идее коллекции.

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

Автор: NikchukIvan

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js