Избегайте чрезмерных параметров функции: центрируемый классом или центрируемый функцией подход?

Как Вы исправили бы следующий плохой код, который раздает слишком много параметров?

void helper1(int p1, int p3, int p5, int p7, int p9, int p10) {
  // ...
}

void helper2(int p1, int p2, int p3, int p5, int p6, int p7, int p9, int p10) {
  // ...
}

void foo(int p1, int p2, int p3, int p4, int p5, int p6, int p7, int p8, 
         int p9, int p10) {
  helper1(p1, p3, p5, p7, p9, p10);
  helper2(p1, p2, p3, p5, p6, p7, p9, p10);
}

Я вижу два разных подхода:

Подход 1: Поместите все функции в класс

class Foo {
  private:
    int p1, p2, p3, p4, p5, p6, p7, p8, p9, p10;
    void helper1() {}
    void helper2() {}
  public:
    void foo() {
      helper1();
      helper2();
    }
    // add constructor
};

Подход 2: Просто передайте параметры как класс

struct FooOptions {
    int p1, p2, p3, p4, p5, p6, p7, p8, p9, p10;
};

void helper1(const FooOptions& opt) {
  // ...
}

void helper2(const FooOptions& opt) {
  // ...
}

void foo(const FooOptions& opt) {
  helper1(opt);
  helper2(opt);
}

Каковы преимущества и недостатки подходов?

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

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

(Этот вопрос связан, но не обсуждает эти две альтернативы.)

6
задан Community 23 May 2017 в 12:14
поделиться

7 ответов

Краткий ответ:

С Новым годом! Я бы избегал варианта №1 и выбрал вариант №2 только в том случае, если параметры можно разделить на четкие и логические группы, которые имеют смысл вне вашей функции.

Длинный ответ

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

calcTime(int p1, int p2, int p3, int p4, int p5, int p6) {
    dist = calcDistance( p1, p2, p3 );
    speed = calcSpeed( p4, p5, p6 );
    return speed == 0 : 0 ? dist/speed; }

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

С другой стороны, код, который мне передавали, часто выглядит так:

calcTime(int p1, int p2, int p3, int p4, int p5, int p6) {
    height = height( p1, p2, p3, p6 );
    type = getType( p1, p4, p5, p6 );
    if( type == 4 ) {
        return 2.345; //some magic number
    }
    value = calcValue( p2, p3, type ); //what a nice variable name...
    a = resetA( p3, height, value );
    return a * value; }

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

calcTime(Type type, int height, int value, int p2, int p3)

, а затем вызвать его

calcTime(
    getType( p1, p4, p5, p6 ),
    height( p1, p2, p3, p6 ),
    p3,
    p4 );

, что может вызвать дрожь по позвоночнику, когда этот маленький голос в вашей голове кричит: «СУХОЙ, СУХОЙ, СУХОЙ!» Какой из них более читабелен и, следовательно, удобнее в обслуживании?

Вариант №1 мне не подходит, так как очень велика вероятность, что кто-то забудет установить один из параметров. Это может очень легко привести к трудно обнаруживаемой ошибке, которая проходит простые модульные тесты. YMMV.

7
ответ дан 8 December 2019 в 17:22
поделиться

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

Более простым вариантом может быть обертывание всего в класс, где одни параметры передаются в конструкторе (те, которые обычно остаются одинаковыми при вызове целой партии), а другие, которые должны передаваться при каждом вызове, так как они постоянно меняются, передаются при реальном вызове функции (вероятно, перегруженный оператор()).

Не зная реального значения вашего кода, невозможно сказать, как его лучше всего рефакторизовать.

.
1
ответ дан 8 December 2019 в 17:22
поделиться

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

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

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

Здесь - смежный вопрос, говорящий об этом с большим количеством респондентов

.
1
ответ дан 8 December 2019 в 17:22
поделиться

Если p1,p2 и т.д... на самом деле являются всего лишь опциями (флаги и т.д...), и совершенно не связаны друг с другом, то я бы выбрал второй вариант.

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

class Something {
   int p1, p3, p5 ...;
   void helper1a();
};

class SomethingElse {
   int p7, p9, p10 ...;
   void helper1b();
};

class Options {
   int p2, p4, ...
};

void foo(Something &s, SomethingElse & else, Options &options) {
  helper2(options);
  s.helper1a();
  else.helper1b();
}
1
ответ дан 8 December 2019 в 17:22
поделиться

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

.
1
ответ дан 8 December 2019 в 17:22
поделиться

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

class Point {
      private:
        int x, y
        void moveUp() {}
        void moveRight() {}
      public:
        void moveUpAndRight() {
          moveUp();
          moveRight();
        }
        // add constructor
    };

struct Point {
  int x, y;
};

void moveUp {}
void moveRight {}
void moveUpAndRight {
  moveUp {}
  moveRight {}
}

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

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

struct Dwarves {
  int Doc, Grumpy, Happy, Sleepy, Bashful, Sneezy, Dopey;
}

void killDwarves(Dwarves& dwarves) {}
void buryDwarves(Dwarves& dwarves) {}

void disposeOfDwarves(Dwarves& dwarves) {
  killDwarves(dwarves);
  buryDwarves(dwarves);
}

Тогда имеет смысл использовать Подход 2.

Это проектный выбор ОК, так что есть несколько правильных решений, и из того, что нам дано, трудно дать окончательный ответ.

.
2
ответ дан 8 December 2019 в 17:22
поделиться

Опция № 2.

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

Затем вы можете создавать экземпляры этого объекта, передайте его в свою программу.

Обычно это может что-то вроде объекта приложения или объект сеанса и т. Д.

1
ответ дан 8 December 2019 в 17:22
поделиться
Другие вопросы по тегам:

Похожие вопросы: