Как избежать проблемы "слишком много параметров" в дизайне API?

У меня есть эта функция API:

public ResultEnum DoSomeAction(string a, string b, DateTime c, OtherEnum d, 
     string e, string f, out Guid code)

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

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

public class DoSomeActionParameters
{
    public string A;
    public string B;
    public DateTime C;
    public OtherEnum D;
    public string E;
    public string F;        
}

Это уменьшило мою декларацию API до:

public ResultEnum DoSomeAction(DoSomeActionParameters parameters, out Guid code)

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

public class DoSomeActionParameters
{
    public string A { get; private set; }
    public string B { get; private set; }
    public DateTime C { get; private set; }
    public OtherEnum D { get; private set; }
    public string E { get; private set; }
    public string F { get; private set; }        

    public DoSomeActionParameters(string a, string b, DateTime c, OtherEnum d, 
     string e, string f)
    {
        this.A = a;
        this.B = b;
        // ... tears erased the text here
    }
}

Как вы можете видеть, я действительно заново создал свою оригинальную проблему: слишком много параметров. Очевидно, что это не путь. Что я собираюсь делать? Последним вариантом достижения такой неизменности является использование структуры "readonly" следующим образом:

public struct DoSomeActionParameters
{
    public readonly string A;
    public readonly string B;
    public readonly DateTime C;
    public readonly OtherEnum D;
    public readonly string E;
    public readonly string F;        
}

Это позволяет нам избегать конструкторов со слишком большим количеством параметров и достигать неизменности. Фактически он исправляет все проблемы (порядок параметров и т.д.). Тем не менее:

Что, когда я запутался и решил написать этот вопрос: Какой самый простой способ в С# избежать проблемы с "слишком многими параметрами" без введения изменчивости? Можно ли использовать для этой цели структуру readonly и все же не иметь плохой дизайн API?

ПОЯСНЕНИЯ:

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

UPDATE

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

Ответ 1

Используйте комбинацию API-интерфейса для компоновщика и интерфейса для конкретного домена - Fluent Interface. API немного более подробный, но с intellisense он очень быстро печатает и легко понимает.

public class Param
{
        public string A { get; private set; }
        public string B { get; private set; }
        public string C { get; private set; }


  public class Builder
  {
        private string a;
        private string b;
        private string c;

        public Builder WithA(string value)
        {
              a = value;
              return this;
        }

        public Builder WithB(string value)
        {
              b = value;
              return this;
        }

        public Builder WithC(string value)
        {
              c = value;
              return this;
        }

        public Param Build()
        {
              return new Param { A = a, B = b, C = c };
        }
  }


  DoSomeAction(new Param.Builder()
        .WithA("a")
        .WithB("b")
        .WithC("c")
        .Build());

Ответ 2

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

var request = new HttpWebRequest(a, b);
var service = new RestService(request, c, d, e);
var client = new RestClient(service, f, g);
var resource = client.RequestRestResource(); // O params after 3 objects

Ответ 4

Просто измените структуру данных параметров с class на struct, и вы хорошо пойдете.

public struct DoSomeActionParameters 
{
   public string A;
   public string B;
   public DateTime C;
   public OtherEnum D;
   public string E;
   public string F;
}

public ResultEnum DoSomeAction(DoSomeActionParameters parameters, out Guid code) 

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

Плюсы:

  • Проще всего реализовать
  • Наименьшее изменение поведения в базовой механике.

Минусы:

  • Неизбежность не очевидна, требует внимания разработчика.
  • Ненужное копирование для поддержания неизменности
  • Занимает пространство стека.

Ответ 5

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

public class DoSomeActionParameters
    {
        public string A { get; private set; }
        public string B  { get; private set; }
        public DateTime C { get; private set; }
        public OtherEnum D  { get; private set; }
        public string E  { get; private set; }
        public string F  { get; private set; }

        public class Builder
        {
            DoSomeActionParameters obj = new DoSomeActionParameters();

            public string A
            {
                set { obj.A = value; }
            }
            public string B
            {
                set { obj.B = value; }
            }
            public DateTime C
            {
                set { obj.C = value; }
            }
            public OtherEnum D
            {
                set { obj.D = value; }
            }
            public string E
            {
                set { obj.E = value; }
            }
            public string F
            {
                set { obj.F = value; }
            }

            public DoSomeActionParameters Build()
            {
                return obj;
            }
        }
    }

    public class Example
    {

        private void DoSth()
        {
            var data = new DoSomeActionParameters.Builder()
            {
                A = "",
                B = "",
                C = DateTime.Now,
                D = testc,
                E = "",
                F = ""
            }.Build();
        }
    }

Ответ 6

Почему бы просто не сделать интерфейс, который обеспечивает неизменность (т.е. только геттеры)?

Это по сути ваше первое решение, но вы вынуждаете функцию использовать интерфейс для доступа к параметру.

public interface IDoSomeActionParameters
{
    string A { get; }
    string B { get; }
    DateTime C { get; }
    OtherEnum D { get; }
    string E { get; }
    string F { get; }              
}

public class DoSomeActionParameters: IDoSomeActionParameters
{
    public string A { get; set; }
    public string B { get; set; }
    public DateTime C { get; set; }
    public OtherEnum D { get; set; }
    public string E { get; set; }
    public string F { get; set; }        
}

и объявление функции становится:

public ResultEnum DoSomeAction(IDoSomeActionParameters parameters, out Guid code)

Плюсы:

  • Не имеет проблемы с пространством стека, например struct solution
  • Естественное решение с использованием языковой семантики
  • Неизбежность очевидна.
  • Гибкий (потребитель может использовать другой класс, если хочет)

Минусы:

  • Некоторая повторяющаяся работа (те же декларации в двух разных сущностях)
  • Разработчик должен угадать, что DoSomeActionParameters - это класс, который можно сопоставить с IDoSomeActionParameters

Ответ 7

Я не программист на С#, но я верю С# поддерживает именованные аргументы: (F # и С# в значительной степени совместимы с подобными вещами) Оно делает: http://msdn.microsoft.com/en-us/library/dd264739.aspx#Y342

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

public ResultEnum DoSomeAction( 
 e:"bar", 
 a: "foo", 
 c: today(), 
 b:"sad", 
 d: Red,
 f:"penguins")

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

Изменить: вот искусство, которое я нашел об этом. http://www.globalnerdy.com/2009/03/12/default-and-named-parameters-in-c-40-sith-lord-in-training/ Я должен упомянуть, что С# 4.0 поддерживает именованные аргументы, 3.0 не

Ответ 8

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

    public class ExampleClass
    {
        //create properties like this...
        private readonly int _exampleProperty;
        public int ExampleProperty { get { return _exampleProperty; } }

        //Private constructor, prohibiting construction outside of this class
        private ExampleClass(ExampleClassParams parameters)
        {                
            _exampleProperty = parameters.ExampleProperty;
            //and so on... 
        }

        //The object returned from here will be immutable
        public ExampleClass GetFromDatabase(DBConnection conn, int id)
        {
            //do database stuff here (ommitted from example)
            ExampleClassParams parameters = new ExampleClassParams()
            {
                ExampleProperty = 1,
                ExampleProperty2 = 2
            };

            //Danger here as parameters object is mutable

            return new ExampleClass(parameters);    

            //Danger is now over ;)
        }

        //Private struct representing the parameters, nested within class that uses it.
        //This is mutable, but the fact that it is private means that all potential 
        //"damage" is limited to this class only.
        private struct ExampleClassParams
        {
            public int ExampleProperty { get; set; }
            public int AnotherExampleProperty { get; set; }
            public int ExampleProperty2 { get; set; }
            public int AnotherExampleProperty2 { get; set; }
            public int ExampleProperty3 { get; set; }
            public int AnotherExampleProperty3 { get; set; }
            public int ExampleProperty4 { get; set; }
            public int AnotherExampleProperty4 { get; set; } 
        }
    }

Ответ 9

Вы можете использовать стиль в стиле Builder, хотя в зависимости от сложности вашего метода DoSomeAction это может быть прикосновение тяжелого веса. Что-то в этом роде:

public class DoSomeActionParametersBuilder
{
    public string A { get; set; }
    public string B { get; set; }
    public DateTime C { get; set; }
    public OtherEnum D { get; set; }
    public string E { get; set; }
    public string F { get; set; }

    public DoSomeActionParameters Build()
    {
        return new DoSomeActionParameters(A, B, C, D, E, F);
    }
}

public class DoSomeActionParameters
{
    public string A { get; private set; }
    public string B { get; private set; }
    public DateTime C { get; private set; }
    public OtherEnum D { get; private set; }
    public string E { get; private set; }
    public string F { get; private set; }

    public DoSomeActionParameters(string a, string b, DateTime c, OtherEnum d, string e, string f)
    {
        A = a;
        // etc.
    }
}

// usage
var actionParams = new DoSomeActionParametersBuilder
{
    A = "value for A",
    C = DateTime.Now,
    F = "I don't care for B, D and E"
}.Build();

result = foo.DoSomeAction(actionParams, out code);

Ответ 10

В дополнение к ответу манджи - вы также можете разделить одну операцию на несколько более мелких. Для сравнения:

 BOOL WINAPI CreateProcess(
   __in_opt     LPCTSTR lpApplicationName,
   __inout_opt  LPTSTR lpCommandLine,
   __in_opt     LPSECURITY_ATTRIBUTES lpProcessAttributes,
   __in_opt     LPSECURITY_ATTRIBUTES lpThreadAttributes,
   __in         BOOL bInheritHandles,
   __in         DWORD dwCreationFlags,
   __in_opt     LPVOID lpEnvironment,
   __in_opt     LPCTSTR lpCurrentDirectory,
   __in         LPSTARTUPINFO lpStartupInfo,
   __out        LPPROCESS_INFORMATION lpProcessInformation
 );

и

 pid_t fork()
 int execvpe(const char *file, char *const argv[], char *const envp[])
 ...

Для тех, кто не знает POSIX, создание ребенка может быть таким же простым, как:

pid_t child = fork();
if (child == 0) {
    execl("/bin/echo", "Hello world from child", NULL);
} else if (child != 0) {
    handle_error();
}

Каждый выбор дизайна представляет собой компромисс над тем, какие операции он может выполнять.

PS. Да - он похож на строителя - только наоборот (т.е. На стороне вызываемого абонента, а не на вызывающего абонента). Это может быть или не быть лучше, чем строитель в этом конкретном случае.

Ответ 11

здесь немного отличается от Mikeys но то, что я пытаюсь сделать, - сделать все, что нужно, написать как можно меньше

public class DoSomeActionParameters
{
    readonly string _a;
    readonly int _b;

    public string A { get { return _a; } }

    public int B{ get { return _b; } }

    DoSomeActionParameters(Initializer data)
    {
        _a = data.A;
        _b = data.B;
    }

    public class Initializer
    {
        public Initializer()
        {
            A = "(unknown)";
            B = 88;
        }

        public string A { get; set; }
        public int B { get; set; }

        public DoSomeActionParameters Create()
        {
            return new DoSomeActionParameters(this);
        }
    }
}

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

Инициализатор не является неизменным, но только транспорт

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

DoSomeAction(new DoSomeActionParameters.Initializer
            {
                A = "Hello",
                B = 42
            }
            .Create());

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

И проверка может идти в методе Create

public class Initializer
{
    public Initializer(int b)
    {
        A = "(unknown)";
        B = b;
    }

    public string A { get; set; }
    public int B { get; private set; }

    public DoSomeActionParameters Create()
    {
        if (B < 50) throw new ArgumentOutOfRangeException("B");

        return new DoSomeActionParameters(this);
    }
}

Итак, теперь он выглядит как

DoSomeAction(new DoSomeActionParameters.Initializer
            (b: 42)
            {
                A = "Hello"
            }
            .Create());

Еще немного kooki, я знаю, но все равно попытаюсь это сделать

Изменить: перемещение метода create в статику в объекте параметров и добавление делегата, который передает инициализатор, вытаскивает некоторые из kookieness из вызова

public class DoSomeActionParameters
{
    readonly string _a;
    readonly int _b;

    public string A { get { return _a; } }
    public int B{ get { return _b; } }

    DoSomeActionParameters(Initializer data)
    {
        _a = data.A;
        _b = data.B;
    }

    public class Initializer
    {
        public Initializer()
        {
            A = "(unknown)";
            B = 88;
        }

        public string A { get; set; }
        public int B { get; set; }
    }

    public static DoSomeActionParameters Create(Action<Initializer> assign)
    {
        var i = new Initializer();
        assign(i)

        return new DoSomeActionParameters(i);
    }
}

Итак, теперь вызов выглядит так:

DoSomeAction(
        DoSomeActionParameters.Create(
            i => {
                i.A = "Hello";
            })
        );

Ответ 12

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

• Все (включая FXCop и Jon Skeet) согласны с тем, что разоблачение публичных полей плохое.

Jon и FXCop будут укомплектованы, потому что вы подвергаете себя не полям.

• Эрик Липперт и другие, полагаясь на поля только для чтения для неизменяемости, являются ложью.

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

    private bool propC_set=false;
    private date pC;
    public date C {
        get{
            return pC;
        }
        set{
            if (!propC_set) {
               pC = value;
            }
            propC_set = true;
        }
    }

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

Ответ 13

Вариант Ответ Самуэля, который я использовал в своем проекте, когда у меня была та же проблема:

class MagicPerformer
{
    public int Param1 { get; set; }
    public string Param2 { get; set; }
    public DateTime Param3 { get; set; }

    public MagicPerformer SetParam1(int value) { this.Param1 = value; return this; }
    public MagicPerformer SetParam2(string value) { this.Param2 = value; return this; }
    public MagicPerformer SetParam4(DateTime value) { this.Param3 = value; return this; }

    public void DoMagic() // Uses all the parameters and does the magic
    {
    }
}

И использовать:

new MagicPerformer().SeParam1(10).SetParam2("Yo!").DoMagic();

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