Как избежать проблемы «слишком много параметров» в дизайне API? - PullRequest
154 голосов
/ 05 июня 2011

У меня есть эта функция 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
    }
}

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

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;        
}

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

Вот тогда я запутался и решил написать этот вопрос: Какой самый простой способ в C # избежать проблемы «слишком большого количества параметров» без введения изменчивости?Можно ли использовать для этой цели только для чтения структуру и при этом не иметь плохой дизайн API?

УТОЧНЕНИЯ:

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

ОБНОВЛЕНИЕ

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

Ответы [ 13 ]

81 голосов
/ 05 июня 2011

Используйте комбинацию компоновщика и 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());
21 голосов
/ 05 июня 2011

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

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
10 голосов
/ 07 июня 2011

Просто измените структуру данных вашего параметра с 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) 

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

Плюсы:

  • Самый простой в реализации
  • Наименьшее изменение поведения в базовой механике

Минусы:

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

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

6 голосов
/ 05 июня 2011

Я не программист на C #, но я верю C # поддерживает именованные аргументы: (F # делает, а C # в значительной степени совместим для такого рода вещей) Оно делает: 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")

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

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

6 голосов
/ 05 июня 2011

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

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

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 решение
  • Естественное решение с использованиемсемантика языка
  • Неизменность очевидна
  • Гибкость (потребитель может использовать другой класс, если он хочет)

Минусы:

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

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

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();
        }
    }
3 голосов
/ 16 апреля 2012

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

    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; } 
        }
    }
2 голосов
/ 21 сентября 2012

здесь немного отличается от 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

И проверка может пойти вМетод создания

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());

Я все еще немного интересен, но все равно попробую

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

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";
            })
        );
2 голосов
/ 05 июня 2011

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

 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. Да - это похоже на конструктор - только наоборот (то есть на стороне вызываемого абонента вместо вызывающего абонента). В данном конкретном случае он может быть лучше, чем строитель, а может и не быть.

...