Какой лучший способ реорганизовать метод, который содержит слишком много (6+) параметров?

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

return new Shniz(foo, bar, baz, quux, fred, wilma, barney, dino, donkey)

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

ShnizArgs args = new ShnizArgs(foo, bar, baz, quux, fred, wilma, barney, dino, donkey)
return new Shniz(args);

Так что это не похоже на улучшение. Итак, каков наилучший подход?

Ответ 1

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

Например, если вы передаете спецификацию для прямоугольника, вы можете передать x, y, width и height или просто передать объект прямоугольника, который содержит x, y, width и height.

Ищите такие вещи, когда рефакторинг должен немного почистить. Если аргументы действительно не могут быть объединены, начните смотреть, есть ли у вас нарушение принципа единой ответственности.

Ответ 2

Я предполагаю, что вы имеете в виду С#. Некоторые из этих вещей относятся и к другим языкам.

У вас есть несколько вариантов:

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

class C
{
    public string S { get; set; }
    public int I { get; set; }
}

new C { S = "hi", I = 3 };

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

Шаблон Builder.

Подумайте о связи между string и StringBuilder. Вы можете получить это для своих классов. Мне нравится реализовать его как вложенный класс, поэтому class C имеет связанный класс C.Builder. Мне также нравится свободный интерфейс для строителя. Сделано правильно, вы можете получить синтаксис следующим образом:

C c = new C.Builder()
    .SetX(4)    // SetX is the fluent equivalent to a property setter
    .SetY("hello")
    .ToC();     // ToC is the builder pattern analog to ToString()

// Modify without breaking immutability
c = c.ToBuilder().SetX(2).ToC();

// Still useful to have a traditional ctor:
c = new C(1, "...");

// And object initializer syntax is still available:
c = new C.Builder { X = 4, Y = "boing" }.ToC();

У меня есть PowerShell script, который позволяет мне генерировать код построителя, чтобы сделать все это, где ввод выглядит следующим образом:

class C {
    field I X
    field string Y
}

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

"Ввести объект параметров" рефакторинг. См. Каталог рефакторинга. Идея состоит в том, что вы берете некоторые из параметров, которые вы передаете, и помещаете их в новый тип, а затем передаете экземпляр этого типа. Если вы это сделаете без размышлений, вы вернетесь туда, где вы начали:

new C(a, b, c, d);

становится

new C(new D(a, b, c, d));

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

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

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

  • Ищите функциональность, которая находится в существующем коде, но принадлежит к новому типу.

Например, возможно, вы видите код, который выглядит так:

bool SpeedIsAcceptable(int minSpeed, int maxSpeed, int currentSpeed)
{
    return currentSpeed >= minSpeed & currentSpeed < maxSpeed;
}

Вы можете взять параметры minSpeed и maxSpeed и поместить их в новый тип:

class SpeedRange
{
   public int Min;
   public int Max;
}

bool SpeedIsAcceptable(SpeedRange sr, int currentSpeed)
{
    return currentSpeed >= sr.Min & currentSpeed < sr.Max;
}

Это лучше, но чтобы действительно воспользоваться новым типом, переместите сравнения в новый тип:

class SpeedRange
{
   public int Min;
   public int Max;

   bool Contains(int speed)
   {
       return speed >= min & speed < Max;
   }
}

bool SpeedIsAcceptable(SpeedRange sr, int currentSpeed)
{
    return sr.Contains(currentSpeed);
}

И теперь мы где-то получаем: реализация SpeedIsAcceptable() теперь говорит о том, что вы имеете в виду, и у вас есть полезный класс многократного использования. (Следующий очевидный шаг - сделать SpeedRange in до Range<Speed>.)

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

Ответ 3

Если это конструктор, особенно если есть несколько перегруженных вариантов, вы должны посмотреть на шаблон Builder:

Foo foo = new Foo()
          .configBar(anything)
          .configBaz(something, somethingElse)
          // and so on

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

Ответ 4

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

например. вместо:

driver.connect(host, user, pass)

Вы можете использовать

config = new Configuration()
config.setHost(host)
config.setUser(user)
config.setPass(pass)
driver.connect(config)

YMMV

Ответ 5

Это цитируется из книги Фаулера и Бек: "Рефакторинг"

Длинный список параметров

В наши ранние дни программирования нас учили проходить в качестве параметров, необходимых всем рутина. Это было понятно, потому что альтернативой были глобальные данные, а глобальные данные злой и обычно болезненный. Объекты меняют эту ситуацию, потому что, если у вас нет чего-то вам нужно, вы всегда можете попросить другой объект получить его для вас. Таким образом, с объектами, которые вы не пройти во всем, что нужно методу; вместо этого вы проходите достаточно, чтобы метод мог все, что ему нужно. Многое из того, что нужно для метода, доступно в классе хоста метода. В объектно-ориентированные списки параметров программ, как правило, намного меньше, чем в традиционных программы. Это хорошо, потому что длинный список параметров трудно понять, потому что они становятся непоследовательны и сложны в использовании, а также потому, что вы навсегда меняете их по мере необходимости больше данных. Большинство изменений удаляются путем передачи объектов, потому что вы гораздо более вероятны нужно сделать всего пару запросов, чтобы получить новую часть данных. Используйте параметр "Заменить параметр" с помощью метода, когда вы можете получить данные в одном параметре, сделав запрос объекта, о котором вы уже знаете. Этот объект может быть полем или может быть другой параметр. Используйте "Сохранять весь объект", чтобы получить кучу данных, полученных из объект и заменить его самим объектом. Если у вас несколько элементов данных без логических объект, использование Введите объект параметра. Существует одно важное исключение для внесения этих изменений. Это когда вы явно делаете не хотите создавать зависимость от вызываемого объекта к более крупному объекту. В этих случаях распаковка данных и отправка их в качестве параметров являются разумными, но обратите внимание на боль участвует. Если список параметров слишком длинный или слишком часто изменяется, вам нужно переосмыслить свои структура зависимостей.

Ответ 6

Я не хочу звучать как мудрый треск, но вы также должны проверить, чтобы данные, которые вы проходили, действительно должны были передаваться: Передача материала конструктору (или методу в этом случае) пахнет бит, как мало внимания на поведение объекта.

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

Этот вид запаха (поскольку мы говорим о рефакторинге, это ужасное слово кажется подходящим...) также может быть обнаружено для объектов, которые имеют много свойств (читаемых: any) или getters/seters.

Ответ 7

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

return new Shniz.Builder(foo, bar).baz(baz).quux(quux).build();

Подробности этого описаны в "Эффективной Java", 2-е изд., с. 11. Для аргументов метода одна и та же книга (стр. 189) описывает три подхода для сокращения списков параметров:

  • Разбить метод на несколько методов, которые принимают меньше аргументов
  • Создайте статические классы-помощники для представления групп параметров, т.е. передайте DinoDonkey вместо dino и donkey
  • Если параметры не являются обязательными, построитель выше может быть принят для методов, определения объекта для всех параметров, установки необходимых и последующего вызова некоторого метода выполнения на нем

Ответ 8

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

EverythingInTheWorld earth=new EverythingInTheWorld(firstCustomerId,
  lastCustomerId,
  orderNumber, productCode, lastFileUpdateDate,
  employeeOfTheMonthWinnerForLastMarch,
  yearMyHometownWasIncorporated, greatGrandmothersBloodType,
  planetName, planetSize, percentWater, ... etc ...);

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

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

 Order order=new Order(customerName, customerAddress, customerCity,
   customerState, customerZip,
   orderNumber, orderType, orderDate, deliveryDate);

Мы могли бы:

Customer customer=new Customer(customerName, customerAddress,
  customerCity, customerState, customerZip);
Order order=new Order(customer, orderNumber, orderType, orderDate, deliveryDate);

Хотя я, конечно, предпочитаю функции, которые принимают только 1 или 2 или 3 параметра, иногда мы должны признать, что, реалистично, эта функция берет кучу и что число само по себе не создает сложности. Например:

Employee employee=new Employee(employeeId, firstName, lastName,
  socialSecurityNumber,
  address, city, state, zip);

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

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

void updateCustomer(String type, String status,
  int lastOrderNumber, int pastDue, int deliveryCode, int birthYear,
  int addressCode,
  boolean newCustomer, boolean taxExempt, boolean creditWatch,
  boolean foo, boolean bar);

И затем я вижу, что он вызван с помощью:

updateCustomer("A", "M", 42, 3, 1492, 1969, -7, true, false, false, true, false);

Меня беспокоит. Глядя на вызов, он совершенно не понимает, что означают все эти загадочные числа, коды и флаги. Это просто требует ошибок. Программист может легко запутаться в отношении порядка параметров и случайно переключить два, и если они будут одного и того же типа данных, компилятор просто примет его. Я бы предпочел иметь подпись, где все эти вещи перечислены, поэтому вызов проходит в таких вещах, как Type.ACTIVE, вместо "A" и CreditWatch.NO вместо "false" и т.д.

Ответ 9

Я бы использовал конструктор по умолчанию и свойства. С# 3.0 имеет хороший синтаксис, чтобы сделать это автоматически.

return new Shniz { Foo = foo,
                   Bar = bar,
                   Baz = baz,
                   Quuz = quux,
                   Fred = fred,
                   Wilma = wilma,
                   Barney = barney,
                   Dino = dino,
                   Donkey = donkey
                 };

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

Ответ 10

Вы можете попытаться сгруппировать свой параметр в многозначные значения struct/class (если возможно).

Ответ 11

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

Шниз (foo, bar, baz, quux, fred, wilma, barney, dino, donkey)

можно интерпретировать как:

void Shniz(int foo, int bar, int baz, int quux, int fred, 
           int wilma, int barney, int dino, int donkey) { ...

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

// old way
Shniz(1,2,3,2,3,2,1,2);
Shniz(1,2,2,3,3,2,1,2); 

//versus
ShnizParam p = new ShnizParam { Foo = 1, Bar = 2, Baz = 3 };
Shniz(p);

Альтернативно, если у вас есть:

void Shniz(Foo foo, Bar bar, Baz baz, Quux quux, Fred fred, 
           Wilma wilma, Barney barney, Dino dino, Donkey donkey) { ...

Это совсем другой случай, потому что все объекты разные (и вряд ли они будут запутаны). Согласился, что если все объекты необходимы, и все они разные, для создания класса параметров мало смысла.

Кроме того, некоторые параметры являются необязательными? Есть ли метод переопределения (такое же имя метода, но разные сигнатуры методов?). Все эти сведения касаются наилучшего ответа.

* Также может быть полезен пакет свойств, но не особенно лучше, если у вас нет фона.

Как вы можете видеть, есть более чем 1 правильный ответ на этот вопрос. Сделайте свой выбор.

Ответ 12

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

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

Ответ 13

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

Ответ 14

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

Ответ 15

Как не устанавливать его во всех сразу при конструкторах, но делать это через свойства/сеттеры? Я видел некоторые .NET-классы, которые используют этот подход, например Process class:

        Process p = new Process();

        p.StartInfo.UseShellExecute = false;
        p.StartInfo.CreateNoWindow = true;
        p.StartInfo.RedirectStandardOutput = true;
        p.StartInfo.RedirectStandardError = true;
        p.StartInfo.FileName = "cmd";
        p.StartInfo.Arguments = "/c dir";
        p.Start();

Ответ 16

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

Ответ 17

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

Ответ 18

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

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

Ответ 19

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

Другим вариантом, который я бы искал при создании такого рефакторинга, были бы группы связанных параметров, которые могли бы быть лучше обработаны как независимый объект. Используя класс Rectangle из более раннего ответа в качестве примера, конструктор, который принимает параметры для x, y, height и width, может выставить x и y в объект Point, позволяя вам передать три параметра в конструктор Rectangle. Или идите немного дальше и сделайте два параметра (UpperLeftPoint, LowerRightPoint), но это будет более радикальный рефакторинг.

Ответ 20

Это зависит от того, какие аргументы у вас есть, но если у них много логических значений/параметров, возможно, вы могли бы использовать Flag Enum?

Ответ 21

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

В некоторых случаях конструктор с 7 параметрами может указывать на неправильную иерархию классов: в этом случае вспомогательная структура/класс, предложенная выше, обычно является хорошим подходом, но тогда вы также склонны в конечном итоге загружать структуры, которые просто мешки с недвижимостью и не делайте ничего полезного. Конструктор с 8 аргументами может также указывать на то, что ваш класс слишком универсален/слишком универсален, поэтому ему нужно много вариантов, чтобы быть действительно полезным. В этом случае вы можете либо реорганизовать класс, либо реализовать статические конструкторы, которые скрывают реальные сложные конструкторы: например. Shniz.NewBaz(foo, bar) может фактически вызвать реальный конструктор, передающий правильные параметры.

Ответ 22

Одно из соображений, какое из значений будет доступно только для чтения после создания объекта?

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

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

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

Было бы странно иметь объект, для которого требуется 7 или более параметров, чтобы дать объекту полное состояние, и все это действительно является внешним по своей природе.

Ответ 23

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

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

См. ниже:

public class Toto {
    private final String state0;
    private final String state1;
    private final String state2;
    private final String state3;

    public Toto(String arg0, String arg1, String arg2, String arg3) {
        this.state0 = arg0;
        this.state1 = arg1;
        this.state2 = arg2;
        this.state3 = arg3;
    }

    public static class TotoBuilder {
        private String arg0;
        private String arg1;
        private String arg2;
        private String arg3;

        public TotoBuilder addArg0(String arg) {
            this.arg0 = arg;
            return this;
        }
        public TotoBuilder addArg1(String arg) {
            this.arg1 = arg;
            return this;
        }
        public TotoBuilder addArg2(String arg) {
            this.arg2 = arg;
            return this;
        }
        public TotoBuilder addArg3(String arg) {
            this.arg3 = arg;
            return this;
        }

        public Toto newInstance() {
            // maybe add some validation ...
            return new Toto(this.arg0, this.arg1, this.arg2, this.arg3);
        }
    }

    public static void main(String[] args) {
        Toto toto = new TotoBuilder()
            .addArg0("0")
            .addArg1("1")
            .addArg2("2")
            .addArg3("3")
            .newInstance();
    }

}