Метод цепочки против инкапсуляции

17

Существует классическая проблема ООП цепочки методов по сравнению с методами с одной точкой доступа:

main.getA().getB().getC().transmogrify(x, y)

против

main.getA().transmogrifyMyC(x, y)

Первый, кажется, имеет то преимущество, что каждый класс отвечает только за меньший набор операций и делает все намного более модульным - добавление метода к C не требует никаких усилий в A, B или C для его раскрытия.

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

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

дуб
источник

Ответы:

25

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

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

Недостатком закона Деметры является то, что иногда требуется написать большое количество небольших «упаковочных» методов для распространения вызовов методов на компоненты. Кроме того, интерфейс класса может стать громоздким, так как он содержит методы для содержащихся классов, что приводит к классу без связного интерфейса. Но это также может быть признаком плохого ОО дизайна.

Петер Тёрёк
источник
Я забыл об этом законе, спасибо, что напомнили мне. Но то , что я спрашиваю о здесь в основном то , что преимущества и недостатки, точнее , как я должен решить , использовать один стиль другой.
Дуб
@ Ладно, я добавил цитаты, описывающие преимущества и недостатки.
Петер Тёрёк
10

Я вообще стараюсь, чтобы цепочка методов была как можно более ограниченной (на основе закона Деметры )

Единственное исключение, которое я делаю, - это свободное владение интерфейсами / программирование в стиле DSL.

Мартин Фаулер проводит такое же различие в предметно-ориентированных языках, но по причинам нарушения разделения командных запросов, которое гласит:

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

Фаулер в своей книге на странице 70 говорит:

Разделение команд и запросов является чрезвычайно ценным принципом в программировании, и я настоятельно рекомендую командам использовать его. Одним из последствий использования метода Method Chaining во внутренних DSL является то, что он обычно нарушает этот принцип - каждый метод изменяет состояние, но возвращает объект для продолжения цепочки. Я использовал много децибел, унижающих людей, которые не следуют разделению команд и запросов, и сделаю это снова. Но беглые интерфейсы следуют другому набору правил, поэтому я рад разрешить это здесь.

KeesDijk
источник
3

Я думаю, вопрос в том, используете ли вы подходящую абстракцию.

В первом случае мы имеем

interface IHasGetA {
    IHasGetB getA();
}

interface IHasGetB {
    IHasGetC getB();
}

interface IHasGetC {
    ITransmogrifyable getC();
}

interface ITransmogrifyable {
    void transmogrify(x,y);
}

Где основной тип IHasGetA. Вопрос в том, подходит ли эта абстракция. Ответ не тривиален. И в этом случае это выглядит немного не так, но в любом случае это теоретический пример. Но для построения другого примера:

main.getA(v).getB(w).getC(x).transmogrify(y, z);

Часто лучше чем

main.superTransmogrify(v, w, x, y, z);

Потому что в последнем примере , как thisи в mainзависимости от типов v, w, x, yи z. Кроме того, код не выглядит намного лучше, если каждое объявление метода имеет полдюжины аргументов.

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

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

Например, вы могли бы иметь:

class Main implements IHasGetA, IHasGetA, IHasGetA, ITransmogrifyable {
    IHasGetB getA() { return this; }
    IHasGetC getB() { return this; }
    ITransmogrifyable getC() { return this; }
    void transmogrify(x,y) {
        return x + y;//yeah!
    }
}

Где mainэто экземпляр Main. Если знание класса mainуменьшает зависимость IHasGetAвместо Main, вы обнаружите, что связь на самом деле довольно низкая. Вызывающий код даже не знает, что он на самом деле вызывает последний метод исходного объекта, что фактически иллюстрирует степень развязки.
Вы идете по пути кратких и ортогональных абстракций, а не глубоко во внутреннюю часть реализаций.

back2dos
источник
Очень интересный момент о большом увеличении количества параметров.
Дуб
2

Закон Деметры , а @ Петера Török указывает, предлагает «компактную» форму.

Кроме того, чем больше методов вы явно упоминаете в своем коде, тем больше классов зависит от вашего класса, что увеличивает проблемы обслуживания. В вашем примере компактная форма зависит от двух классов, тогда как более длинная форма зависит от четырех классов. Более длинная форма не только противоречит Закону Деметры; это также заставит вас изменять ваш код каждый раз, когда вы меняете любой из четырех упомянутых методов (в отличие от двух в компактной форме).

CesarGon
источник
С другой стороны, слепое следование этому закону означает, что число методов Aвзорвется, и многие методы, Aвозможно, захотят все равно отменить. Тем не менее, я согласен с зависимостями - это значительно уменьшает количество зависимостей, требуемых от клиентского кода.
Дуб
1
@ Oak: делать что-либо вслепую никогда не бывает хорошо. Нужно смотреть на плюсы и минусы и принимать решения на основе доказательств. Это включает в себя и закон Деметры.
CesarGon
2

Я боролся с этой проблемой сам. Недостатком «глубокого проникновения» в разные объекты является то, что при рефакторинге вам в конечном итоге придется менять очень много кода, поскольку существует так много зависимостей. Кроме того, ваш код становится немного раздутым и трудным для чтения.

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

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

Homde
источник
1

Я часто нахожу, что логику программы легче понять цепными методами. Для меня это customer.getLastInvoice().itemCount()вписывается в мой мозг лучше, чем customer.countLastInvoiceItems().

Стоит ли это головной боли от обслуживания дополнительного соединения, решать только вам. (Мне также нравятся маленькие функции в маленьких классах, поэтому я склонен цепляться. Я не говорю, что это правильно - это именно то, что я делаю.)

JT Grimes
источник
это должно быть IMO customer.NrLastInvoices или customer.LastInvoice.NrItems. Эта цепочка не слишком длинная, поэтому, вероятно, не стоит сплющивать ее, если количество комбинаций несколько велико
Homde