Метод цепочки - почему это хорошая практика, или нет?

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

participant.addSchedule(events[1]).addSchedule(events[2]).setStatus('attending').save()

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

participant.getSchedule('monday').saveTo('monnday.file')

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

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

participant.attend(event).setNotifications('silent').getSocialStream('twitter').postStatus('Joining '+event.name).follow(event.getSocialId('twitter'))

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

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

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

15 ответов


Я согласен, что это субъективно. По большей части я избегаю цепочек методов, но недавно я также нашел случай, когда это было правильно - у меня был метод, который принимал что-то вроде 10 параметров и нуждался в большем, но в большинстве случаев вам нужно было указать только несколько. С overrides это стало очень громоздким очень быстро. Вместо этого я выбрал цепной подход:

MyObject.Start()
    .SpecifySomeParameter(asdasd)
    .SpecifySomeOtherParameter(asdasd)
    .Execute();

Это что-то вроде шаблона "фабрика". Метод сцепления является факультативным, но это облегчило написание кода (особенно с помощью IntelliSense). Имейте в виду, что это один изолированный случай, хотя и не является общей практикой в моем коде.

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


только мои 2 цента;

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

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

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


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

foo.setHeight(100).setWidth(50).setColor('#ffffff');
foo.moveTo(100,100).highlight();

Я не использую его, когда один или несколько цепных методов вернут любой объект, кроме foo в моем примере. Хотя синтаксически вы можете связывать что угодно, пока вы используете правильный API для этого объекта в цепочке, изменение объектов IMHO делает вещи менее читаемыми и может быть действительно запутанным, если API для различные объекты имеют какое-либо сходство. Если вы делаете какой-то действительно распространенный вызов метода в конце (.toString(), .print(), что) на какой объект вы в конечном итоге действуете? Кто-то случайно читающий код может не уловить, что это будет неявно возвращаемый объект в цепочке, а не исходная ссылка.

цепочка различных объектов также может привести к неожиданным ошибкам null. В моих примерах, предполагая, что фу допустимо, все вызовы метода "безопасный" (например, действителен для foo). В Примере OP:

participant.getSchedule('monday').saveTo('monnday.file')

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


Мартин Фаулер имеет хорошее обсуждение здесь:

Способ Сцепления

когда

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

способ подключения особенно эффективно с грамматиками, такими как parent::= (это | то)*. Использование различных методы обеспечивает читаемый путь посмотрим, какой аргумент будет следующим. Аналогично необязательные аргументы могут быть легко пропускается с помощью метода Сцепление. Перечень обязательных положений, например, parent::= первая секунда не работа с основной формой, хотя он может быть поддержан также использование прогрессивных интерфейсов. Большинство время я предпочитаю вложенную функцию для этого случай.

самая большая проблема для способ Цеплять проблема отделкой. Пока обходные пути, обычно если ты столкнешься с этим, тебе будет лучше. usng вложенная функция. Вложенный Функция также лучший выбор если ты ввязываешься в неприятности с Переменные Контекста.


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

как:

someList.addObject("str1").addObject("str2").addObject("str3")

лучше:

someList.addObject("str1")
someList.addObject("str2")
someList.addObject("str3")

исключение может быть, когда addObject () возвращает новый объект, в этом случае освобожденный код может быть немного более громоздким, как:

someList = someList.addObject("str1")
someList = someList.addObject("str2")
someList = someList.addObject("str3")

Это опасно, потому что вы можете зависеть от большего количества объектов, чем ожидалось, например, ваш вызов возвращает экземпляр другого класса:

приведу пример:

foodStore-это объект, который состоит из многих продовольственных магазинов, которыми вы владеете. гастроном.getLocalStore () возвращает объект, содержащий информацию о ближайшем к параметру хранилище. getPriceforProduct (anything) - это метод этого объекта.

поэтому, когда вы называете гастроном.getLocalStore (параметры).getPriceforProduct (что угодно)

вы зависите не только от FoodStore, как вы, но и от LocalStore.

Если getPriceforProduct (anything) когда-либо изменится, вам нужно изменить не только FoodStore, но и класс, который вызвал цепной метод.

вы всегда должны стремиться к слабой связи между классами.

Это, как говорится, мне лично нравится их при программировании на Ruby.


преимущества цепочки
то есть, где мне нравится его использовать

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

Я знаю, что это надуманный пример, но скажем, у вас есть следующие классы

Public Class Location
   Private _x As Integer = 15
   Private _y As Integer = 421513

   Public Function X() As Integer
      Return _x
   End Function
   Public Function X(ByVal value As Integer) As Location
      _x = value
      Return Me
   End Function

   Public Function Y() As Integer
      Return _y
   End Function
   Public Function Y(ByVal value As Integer) As Location
      _y = value
      Return Me
   End Function

   Public Overrides Function toString() As String
      Return String.Format("{0},{1}", _x, _y)
   End Function
End Class

Public Class HomeLocation
   Inherits Location

   Public Overrides Function toString() As String
      Return String.Format("Home Is at: {0},{1}", X(), Y())
   End Function
End Class

и скажите, что у вас нет доступа к базовому классу или что значения по умолчанию являются динамическими, основанный на времени, etc. Да, вы можете создать экземпляр, а затем изменить значения, но это может стать громоздким, особенно если вы просто передаете значения методу:

  Dim loc As New HomeLocation()
  loc.X(1337)
  PrintLocation(loc)

но разве это не намного легче читать:

  PrintLocation(New HomeLocation().X(1337))

или, как насчет члена класса?

Public Class Dummy
   Private _locA As New Location()
   Public Sub New()
      _locA.X(1337)
   End Sub
End Class

vs

Public Class Dummy
   Private _locC As Location = New Location().X(1337)
End Class

вот как я использую цепочку, и обычно мои методы предназначены только для конфигурации, поэтому они имеют длину всего 2 строки, устанавливают значение, затем Return Me. Для нас он очистил огромные строки, очень трудно читать и понимать код в одну строку, которая читается как предложение. что-то вроде

New Dealer.CarPicker().Subaru.WRX.SixSpeed.TurboCharged.BlueExterior.GrayInterior.Leather.HeatedSeats

Vs что-то вроде

New Dealer.CarPicker(Dealer.CarPicker.Makes.Subaru
                   , Dealer.CarPicker.Models.WRX
                   , Dealer.CarPicker.Transmissions.SixSpeed
                   , Dealer.CarPicker.Engine.Options.TurboCharged
                   , Dealer.CarPicker.Exterior.Color.Blue
                   , Dealer.CarPicker.Interior.Color.Gray
                   , Dealer.CarPicker.Interior.Options.Leather
                   , Dealer.CarPicker.Interior.Seats.Heated)

Ущерб Цепочки
то есть, где я не люблю его использовать

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

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

вывод

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

Я стараюсь следовать этим правилам.

  1. старайтесь не цепляться между классами
  2. сделать процедуры специально для цепь
  3. сделайте только одну вещь в цепочке рутина
  4. используйте его, когда он улучшает читаемость
  5. используйте его, когда он делает код проще

метод цепочки может позволить для проектирования advanced DSL-языки непосредственно на Java. По сути, вы можете моделировать по крайней мере эти типы правил DSL:

1. SINGLE-WORD
2. PARAMETERISED-WORD parameter
3. WORD1 [ OPTIONAL-WORD]
4. WORD2 { WORD-CHOICE-A | WORD-CHOICE-B }
5. WORD3 [ , WORD3 ... ]

эти правила могут быть реализованы с помощью этих интерфейсов

// Initial interface, entry point of the DSL
interface Start {
  End singleWord();
  End parameterisedWord(String parameter);
  Intermediate1 word1();
  Intermediate2 word2();
  Intermediate3 word3();
}

// Terminating interface, might also contain methods like execute();
interface End {}

// Intermediate DSL "step" extending the interface that is returned
// by optionalWord(), to make that method "optional"
interface Intermediate1 extends End {
  End optionalWord();
}

// Intermediate DSL "step" providing several choices (similar to Start)
interface Intermediate2 {
  End wordChoiceA();
  End wordChoiceB();
}

// Intermediate interface returning itself on word3(), in order to allow for
// repetitions. Repetitions can be ended any time because this interface
// extends End
interface Intermediate3 extends End {
  Intermediate3 word3();
}

С помощью этих простых правил вы можете реализовать сложные DSL, такие как SQL непосредственно в Java, как это делается jOOQ, библиотека, которую я создал. См. довольно сложный пример SQL, взятый из мой блог здесь:

create().select(
    r1.ROUTINE_NAME,
    r1.SPECIFIC_NAME,
    decode()
        .when(exists(create()
            .selectOne()
            .from(PARAMETERS)
            .where(PARAMETERS.SPECIFIC_SCHEMA.equal(r1.SPECIFIC_SCHEMA))
            .and(PARAMETERS.SPECIFIC_NAME.equal(r1.SPECIFIC_NAME))
            .and(upper(PARAMETERS.PARAMETER_MODE).notEqual("IN"))),
                val("void"))
        .otherwise(r1.DATA_TYPE).as("data_type"),
    r1.NUMERIC_PRECISION,
    r1.NUMERIC_SCALE,
    r1.TYPE_UDT_NAME,
    decode().when(
    exists(
        create().selectOne()
            .from(r2)
            .where(r2.ROUTINE_SCHEMA.equal(getSchemaName()))
            .and(r2.ROUTINE_NAME.equal(r1.ROUTINE_NAME))
            .and(r2.SPECIFIC_NAME.notEqual(r1.SPECIFIC_NAME))),
        create().select(count())
            .from(r2)
            .where(r2.ROUTINE_SCHEMA.equal(getSchemaName()))
            .and(r2.ROUTINE_NAME.equal(r1.ROUTINE_NAME))
            .and(r2.SPECIFIC_NAME.lessOrEqual(r1.SPECIFIC_NAME)).asField())
    .as("overload"))
.from(r1)
.where(r1.ROUTINE_SCHEMA.equal(getSchemaName()))
.orderBy(r1.ROUTINE_NAME.asc())
.fetch()

еще один хороший пример -jrtf по, немного DSL, предназначенный для сертификации RTF-документов непосредственно на Java. Пример:

rtf()
  .header(
    color( 0xff, 0, 0 ).at( 0 ),
    color( 0, 0xff, 0 ).at( 1 ),
    color( 0, 0, 0xff ).at( 2 ),
    font( "Calibri" ).at( 0 ) )
  .section(
        p( font( 1, "Second paragraph" ) ),
        p( color( 1, "green" ) )
  )
).out( out );

Это кажется довольно субъективным.

метод цепочки не является soemthing, что по своей сути плохо или хорошо ИМО.

читаемость-это самое главное.

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


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

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


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

$this->db->select('something')->from('table')->where('id', $id);

это выглядит намного чище (и имеет больше смысла, на мой взгляд), чем:

$this->db->select('something');
$this->db->from('table');
$this->db->where('id', $id);

это действительно субъективно; у каждого есть свое мнение.


полностью пропущенная точка здесь - это то, что цепочка методов позволяет сухой. Это эффективный дублер для " С " (который плохо реализован на некоторых языках).

A.method1().method2().method3(); // one A

A.method1();
A.method2();
A.method3(); // repeating A 3 times

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

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


Я согласен, поэтому я изменил способ реализации интерфейса fluent в моей библиотеке.

перед:

collection.orderBy("column").limit(10);

после:

collection = collection.orderBy("column").limit(10);

в реализации" до " функции изменили объект и закончились на return this. Я изменил реализацию на возврат нового объекта того же типа.

мои рассуждения об этом изменении:

  1. возвращаемое значение не имело ничего сделайте с функцией, она была чисто там для того чтобы поддержать приковывая часть, она должна была быть пустой функцией согласно ООП.

  2. цепочка методов в системных библиотеках также реализует его таким образом (например, linq или string):

    myText = myText.trim().toUpperCase();
    
  3. исходный объект остается нетронутым, позволяя пользователю API решить, что с ним делать. Это позволяет:

    page1 = collection.limit(10);
    page2 = collection.offset(10).limit(10);
    
  4. A реализация копии также может быть используется для строительства объектов:

    painting = canvas.withBackground('white').withPenSize(10);
    

    здесь setBackground(color) функция изменяет экземпляр и ничего не возвращает (как положено).

  5. поведение функций более предсказуемо (см. пункт 1 и 2).

  6. использование короткого имени переменной также может уменьшить беспорядок кода, не заставляя api на модели.

    var p = participant; // create a reference
    p.addSchedule(events[1]);p.addSchedule(events[2]);p.setStatus('attending');p.save()
    

вывод:
В моем мнение свободный интерфейс, который использует return this реализация-это просто неправильно.


хорошо:

  1. это лаконично, но позволяет вам поместить больше в одну строку элегантно.
  2. иногда вы можете избежать использования переменной, которая иногда может быть полезна.
  3. Он может работать лучше.

плохое:

  1. вы реализуете возврат, по существу добавляя функциональность к методам на объектах, которые на самом деле не являются частью того, для чего предназначены эти методы. Это возвращает то, что ты уже получил. придется чисто сохранить несколько байтов.
  2. он скрывает контекстные переключатели, когда одна цепь ведет к другой. Вы можете получить это с геттерами, за исключением того, что это довольно ясно, когда контекст переключается.
  3. цепочка над несколькими строками выглядит уродливо, не играет хорошо с отступом и может вызвать некоторую путаницу оператора (особенно в языках с ASI).
  4. Если вы хотите начать возвращать что - то еще, что полезно для цепного метода, у вас потенциально будет труднее исправить это или ударить больше проблем с ним.
  5. вы разгружаете управление на объект, который вы обычно не разгружаете исключительно для удобства, даже в строго типизированных языках ошибки, вызванные этим, не всегда могут быть обнаружены.
  6. он может работать хуже.

общие:

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

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

на accasation его можно злоупотреблять, например, вместо другого подхода (например, передача массива) или смешивания методов причудливыми способами (родитель.setSomething().getChild().setSomething().getParent().setSomething()).


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

основные причины, по которым я использую его, - это читаемость и предотвращение затопления моего кода переменными.

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

кроме этого:

convertTextToVoice.LoadText ("source.формат txt.)"ConvertToVoice ("пункт назначения.в формате WAV");

Это то, как я обычно использую его. Использование его для цепочки x количество параметров не так, как я обычно его использую. Если бы я хотел ввести x количество параметров в вызове метода, я бы использовал params синтаксис:

public void foo (params object [] items)

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