Альтернативы передаче флага в метод?
Иногда при исправлении дефекта в существующей кодовой базе я могу (часто из лени) принимать решение об изменении метода из:
void
MyClass::foo(uint32_t aBar)
{
// Do something with aBar...
}
в
void
MyClass::foo(uint32_t aBar, bool aSomeCondition)
{
if (aSomeCondition)
{
// Do something with aBar...
}
}
Во время обзора кода коллега упомянул, что лучший подход будет заключаться в подклассе MyClass
для предоставления этой специализированной функциональности.
Однако я бы сказал, что до тех пор, пока aSomeCondition
не нарушает цель или сплоченность MyClass
, это приемлемый шаблон. Только в том случае, если код был внедрен с флагами, а операторы if
- наследование было бы лучшим вариантом, иначе мы потенциально могли бы войти в архитектуру территории астронавта.
Что здесь может быть здесь?
Примечание: Я только что увидел этот связанный ответ, который предполагает, что enum
может быть лучше
чем a bool
, но я думаю, что мой вопрос по-прежнему применяется в этом случае.
Ответы
Ответ 1
Существует не только одно решение этой проблемы.
Boolean имеет очень низкую семантику. Если вы хотите добавить в будущем новое условие, вам придется добавить новый параметр...
После четырех лет обслуживания ваш метод может иметь полдюжины параметров, если эти параметры являются логическими, это очень хорошая ловушка для сопровождающих.
Enum - хороший выбор, если дела исключительны.
Перечисления могут быть легко перенесены в битовую маску или объект контекста.
Бит-маска: С++ включает язык C, вы можете использовать некоторые простые старые практики. Иногда небольшая маска на unsigned int является хорошим выбором (но вы теряете проверку типов), и вы можете ошибочно ошибочно принять маску. Это удобный способ плавного перехода от логического или аргумента перечисления к этому типу шаблона.
Битовая маска может быть перенесена с некоторым усилием на объект-контекст. Возможно, вам придется реализовать какую-то поразрядную арифметику, такую как operator |
и operator &
, если вам нужно поддерживать совместимость времени сборки.
Наследование - это хороший выбор, если разделение поведения велико, и это поведение связано с жизненным циклом экземпляра. Обратите внимание, что вам также необходимо использовать полиморфизм, и это может замедлить метод, если этот метод сильно используется.
И, наконец, наследование вызывает изменение во всем вашем коде factory... И что вы будете делать, если у вас есть несколько методов для изменения в эксклюзивном режиме? Вы будете загромождать свой код определенных классов...
На самом деле, я думаю, что это вообще не очень хорошая идея.
Разбиение метода. Еще одно решение - разделить метод на несколько частных и предоставить два или более общедоступных метода.
Контекстный объект: С++ и C отсутствие именованного параметра можно обойти, добавив параметр контекста. Я использую этот шаблон очень часто, особенно когда мне приходится передавать много данных на уровне сложной структуры.
class Context{
public:
// usually not a good idea to add public data member but to my opinion this is an exception
bool setup:1;
bool foo:1;
bool bar:1;
...
Context() : setup(0), foo(0), bar(0) ... {}
};
...
Context ctx;
ctx.setup = true; ...
MyObj.foo(ctx);
Примечание:
Это также полезно для минимизации доступа (или использования) статических данных или запроса к объекту singleton, TLS...
Контекстный объект может содержать намного больше данных кэширования, связанных с алгоритмом.
...
Я позволяю вашему воображению бежать бесплатно...
Анти-шаблоны
Я добавляю здесь несколько анти-шаблонов (чтобы предотвратить некоторую смену подписи):
* НИКОГДА НЕ ДЕЛАЙТЕ ЭТО *
- * НИКОГДА НЕ ДЕЛАЙТЕ ЭТОГО * используйте статический int/bool для передачи аргументов (некоторые люди делают это, и это кошмар, чтобы удалить этот материал). Перерыв по крайней мере многопоточности...
- * НИКОГДА НЕ ДЕЛАЙТЕ ЭТО * добавить член данных для передачи параметра в метод.
Ответ 2
К сожалению, я не думаю, что есть четкий ответ на проблему (и это я часто встречаю в своем собственном коде). С булевым:
foo( x, true );
вызов трудно понять.
С перечислением:
foo( x, UseHigherAccuracy );
это легко понять, но вы, как правило, получаете такой код:
foo( x, something == someval ? UseHigherAccuracy : UseLowerAccuracy );
что вряд ли является улучшением. И с несколькими функциями:
if ( something == someval ) {
AccurateFoo( x );
}
else {
InaccurateFoo( x );
}
у вас получилось намного больше кода. Но я думаю, это проще всего читать, и я бы привык, но мне все еще не нравится: - (
Одна вещь, которую я определенно НЕ делал, однако, является подклассом. Наследование должно быть последним инструментом, который вы когда-либо достигали.
Ответ 3
Основной вопрос заключается в том, влияет ли флаг на поведение класса или одной функции. Локальные изменения функции должны быть параметрами, а не подклассами. Наследование времени выполнения должно быть одним из последних достижимых инструментов.
Ответ 4
Общее правило, которое я использую: если aSomeCondition
существенно изменяет характер функции, тогда я рассматриваю подклассу.
Подклассификация - относительно большое усилие по сравнению с добавлением флага, который имеет лишь незначительный эффект.
Некоторые примеры:
- если это флаг, который изменяет направление, в котором сортированная коллекция возвращается вызывающему, что незначительное изменение в природе (флаг).
- если это флаг одного выстрела (что влияет на текущий вызов, а не на постоянное изменение объекта), он, вероятно, не должен быть подклассом (поскольку спуск по этой дорожке, вероятно, приведет к массивному количеству классов).
- если это перечисление, которое изменяет базовую структуру данных вашего класса из массива в связанный список или сбалансированное дерево, это сложное изменение (подкласс).
Конечно, последнее может быть лучше обработано путем полного скрытия базовой структуры данных, но я предполагаю, что вы хотите иметь возможность выбрать один из многих по причинам, таким как производительность.
Ответ 5
IMHO, флаг aSomeCondition
изменяется или зависит от состояния текущего экземпляра, поэтому при определенных условиях этот класс должен изменить свое состояние и обрабатывать указанную операцию по-разному. В этом случае я могу предложить использование State Pattern. Надеюсь, что это поможет.
Ответ 6
Я бы просто изменил код:
void MyClass::foo(uint32_t aBar, bool aSomeCondition)
{
if (aSomeCondition)
{
// Do something with aBar...
}
}
чтобы:
void MyClass::foo(uint32_t aBar)
{
if (this->aSomeCondition)
{
// Do something with aBar...
}
}
Я всегда опускаю bool в качестве параметра функции и предпочитаю помещать в struct, даже если бы мне пришлось вызывать
myClass->enableCondition();