Длинные цепочки делегирования в С++
Это определенно субъективно, но я хотел бы попытаться избежать этого становясь аргументированным. Я думаю, это может быть интересный вопрос, если люди относятся к нему надлежащим образом.
В моих нескольких недавних проектах я использовал для реализации архитектуры, где длинные цепочки делегирования - обычная вещь.
Две цепи делегирования встречаются очень часто:
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 строки кода, где объект появился из
- длинные цепочки делегирования выглядят хорошо
Минусы:
- выполняется интерактивная отладка, так как трудно проверить более одного временного объекта в цепочке делегирования
Я хотел бы узнать другие плюсы и минусы длинных цепочек делегирования. Пожалуйста, представьте свои аргументы и голос, основываясь на том, насколько хорошо аргументировано мнение, а не насколько хорошо вы с ним согласны.
Ответы
Ответ 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 < > , поэтому используйте его!