Подтвердить что ты не робот

Длинные цепочки делегирования в С++

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

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

Две цепи делегирования встречаются очень часто:

bool Exists = Env->FileSystem->FileExists( "foo.txt" );

И тройное делегирование вообще не редко:

Env->Renderer->GetCanvas()->TextStr( ... );

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

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

1) Я повторно использую объект, полученный через цепочку делегирования:

{ // make C invisible to the parent scope
   clCanvas* C = Env->Renderer->GetCanvas();
   C->TextStr( ... );
   C->TextStr( ... );
   C->TextStr( ... );
}

2) Промежуточный объект где-то в середине цепочки делегирования должен быть проверен для NULL перед использованием. Например.

clCanvas* C = Env->Renderer->GetCanvas();

if ( C ) C->TextStr( ... );

Я использовал для борьбы с случаем (2) путем предоставления прокси-объектов, чтобы метод мог быть вызван для объекта, отличного от NULL, что приводит к результату empty.

Мои вопросы:

  • Является ли один из случаев (1) или (2) шаблоном или антипаттерном?
  • Есть ли лучший способ справиться с длинными цепочками делегирования в С++?

Вот некоторые плюсы и минусы, которые я рассматривал при выборе:

Плюсы:

  • это очень описательно: это понятно из 1 строки кода, где объект появился из
  • длинные цепочки делегирования выглядят хорошо

Минусы:

  • выполняется интерактивная отладка, так как трудно проверить более одного временного объекта в цепочке делегирования

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

4b9b3361

Ответ 1

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

Вы можете обойти это, используя этот синтаксис:

if (clCanvas* C = Env->Renderer->GetCanvas()) {
  C->TextStr( ... );
  /* some more things with C */
}

Это разрешено в С++ (в то время как оно не в C) и позволяет вам поддерживать правильную область видимости (C имеет область видимости внутри условного блока) и проверяет наличие NULL.

Утверждение, что что-то не является NULL, во что бы то ни стало лучше, чем убийство SegFault. Поэтому я бы не рекомендовал просто пропустить эти проверки, если вы на 100% не уверены, что этот указатель никогда не может быть NULL.


Кроме того, вы можете инкапсулировать свои чеки в дополнительную бесплатную функцию, если вы чувствуете себя особенно денди:

template <typename T>
T notNULL(T value) {
  assert(value);
  return value;
}

// e.g.
notNULL(notNULL(Env)->Renderer->GetCanvas())->TextStr();

Ответ 2

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

Ответ 3

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

Ответ 4

Интересный вопрос, я думаю, что это открыто для интерпретации, но:

Мои два цента

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

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

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


Что делать:

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

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

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

Производительность

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

Ответ 5

Для bool Exists = Env->FileSystem->FileExists( "foo.txt" ); я предпочел бы более подробное разбиение вашей цепочки, поэтому в моем идеальном мире есть следующие строки кода:

Environment* env = GetEnv();
FileSystem* fs = env->FileSystem;
bool exists = fs->FileExists( "foo.txt" );

и почему? Некоторые причины:

  • читаемость: мое внимание теряется, пока я не прочитаю до конца строки в случае bool Exists = Env->FileSystem->FileExists( "foo.txt" ); Это слишком долго для меня.
  • validity: считает, что вы упомянули объекты, если ваша компания завтра нанимает нового программиста, и он начинает писать код, послезавтра объекты могут отсутствовать. Эти длинные строки довольно недружелюбны, новые люди могут бояться их и будут делать что-то интересное, например, оптимизировать их... что потребует более опытного программиста дополнительное время для исправления.
  • отладка: если это возможно (и после того, как вы наняли нового программиста), приложение выдает ошибку сегментации в длинном списке цепочки, довольно сложно выяснить, какой объект был виновным один. Чем более подробным является разбивка, тем легче найти местоположение ошибки.
  • скорость: если вам нужно много вызовов для получения одинаковых элементов цепи, возможно, быстрее "вытащить" локальную переменную из цепочки вместо вызова "правильного" получателя для него. Я не знаю, является ли ваш код производством или нет, но он, кажется, пропускает "правильную" функцию getter, вместо этого он, кажется, использует только атрибут.

Ответ 6

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

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

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

Лучшим способом было бы создание архитектуры, которая выполняла бы принципы SOLID и использовала бы такие методы, как Injection Dependency и Inversion Of Control, чтобы гарантировать, что ваши объекты всегда имеют доступ к тому, что им нужно для выполнения своих обязанностей. Такой подход также хорошо подходит для автоматизированного и модульного тестирования.

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

Ответ 7

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

clCanvas & C = Env.Renderer().GetCanvas();

Для объектов, которые не могут существовать, я предоставил дополнительные методы, такие как has, is и т.д.

if ( Env.HasRenderer() ) clCanvas* C = Env.Renderer().GetCanvas();

Ответ 8

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

Говоря это, я вижу, что вы везде используете голые указатели. Я бы предположил, что вместо этого вы начинаете использовать интеллектуальные указатели. Когда вы используете оператор → , интеллектуальный указатель обычно будет бросать, если указатель имеет значение NULL. Поэтому вы избегаете SegFault. Не только это, если вы используете интеллектуальные указатели, вы можете хранить копии, и объекты не просто исчезают под вашими ногами. Вы должны явно указывать reset каждый умный указатель до того, как указатель перейдет в NULL.

Это говорит о том, что это не помешало бы оператору- > отбрасывать время от времени.

В противном случае я предпочел бы использовать подход, предложенный AProgrammer. Если объекту A нужен указатель на объект C, выделенный объектом B, то работа, выполняемая объектом A, вероятно, является тем, что должен делать объект B. Таким образом, A может гарантировать, что у него есть указатель на B во все времена (поскольку он содержит общий указатель на B и, следовательно, он не может идти NULL), и поэтому он всегда может вызывать функцию на B для выполнения действия Z на объекте C. В функции Z, B знает, имеет ли он всегда указатель на C или нет. Эта часть реализации B.

Обратите внимание, что с С++ 11 у вас есть std:: smart_ptr < > , поэтому используйте его!