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

160

У меня есть эта функция 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 # избежать проблемы «слишком много параметров» без введения изменчивости? Возможно ли использовать структуру readonly для этой цели и при этом не иметь плохой дизайн API?

ПОЯСНЕНИЯ:

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

ОБНОВИТЬ

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

ссг
источник
Чистый код: Справочник по мастерству гибкого программного обеспечения от Роберта Мартина и книги Рефакторинга Мартина Фаулера об этом немного рассказывает
Ян Рингроз
1
Разве это решение Builder не является избыточным, если вы используете C # 4, где у вас есть дополнительные параметры ?
khellang
1
Я мог бы быть глупым, но я не вижу, как это проблема, учитывая, что DoSomeActionParametersэто одноразовый объект, который будет отброшен после вызова метода.
Vilx-
6
Обратите внимание, что я не говорю, что вы должны избегать полей readonly в структуре; неизменяемые структуры - это лучшая практика, а поля, доступные только для чтения, помогают создавать самодокументируемые неизменяемые структуры. Я хочу сказать, что вы не должны полагаться на то, что поля, доступные только для чтения, никогда не изменятся , потому что это не является гарантией того, что поле только для чтения дает вам структуру. Это частный случай более общего совета, что вы не должны рассматривать типы значений, как будто они являются ссылочными типами; они очень разные животные.
Эрик Липперт
7
@ssg: Мне бы тоже это понравилось. Мы добавили в C # функции, которые поддерживают неизменяемость (например, LINQ), и добавили функции, которые способствуют изменчивости (например, инициализаторы объектов). Было бы неплохо иметь лучший синтаксис для продвижения неизменяемых типов. Мы много думаем об этом и у нас есть интересные идеи, но я бы не ожидал ничего подобного в следующей версии.
Эрик Липперт

Ответы:

22

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

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
Теоман Сойгул
источник
Конденсация всех строк в массив строк имеет смысл, только если они все связаны. Судя по порядку аргументов, они не полностью связаны между собой.
icktoofay
Согласитесь, кроме того, что это сработает, только если у вас много одинаковых типов в качестве параметра.
Тимо Виллемсен
Чем это отличается от моего первого примера в вопросе?
Седат Капаноглу
Что ж, это обычный принятый стиль даже в .NET Framework, указывающий, что ваш первый пример совершенно прав в том, что он делает, даже если он вносит незначительные проблемы с изменяемостью (хотя этот пример, очевидно, из другой библиотеки). Хороший вопрос, кстати.
Теоман Сойгул
2
Я со временем сходился к этому стилю. По-видимому, значительное количество проблем «слишком много параметров» можно решить с помощью хороших логических групп и абстракций. В конце концов, это делает код более читабельным и более модульным.
Седат Капаноглу
87

Используйте комбинацию компоновщика и 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());
Сэмюэл Нефф
источник
+1 - такой подход (который я обычно видел как «свободный интерфейс») - это именно то, что я имел в виду.
Даниэль Приден
+1 - это то, что я попал в ту же ситуацию. За исключением того, что был только один класс, и DoSomeActionбыл метод этого.
Vilx-
+1 Я тоже думал об этом, но так как я новичок в беглых интерфейсах, я не знал, идеально ли это здесь подходило. Спасибо за подтверждение моей собственной интуиции.
Мэтт
Я не слышал о паттерне Builder до того, как задал этот вопрос, так что это был для меня проницательный опыт. Интересно, как часто это происходит? Потому что любой разработчик, который не слышал об этом шаблоне, может иметь проблемы с пониманием использования без документации.
Седат Капаноглу
1
@ssg, это часто встречается в этих сценариях. Например, BCL имеет классы компоновщика строки соединения.
Сэмюэль Нефф
10

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

Марк Симанн
источник
1
Я бы по-прежнему проводил рефакторинг, вводя один или несколько объектов параметров, но, очевидно, если вы переместите все аргументы в один неизменный тип, вы ничего не добьетесь. Хитрость заключается в том, чтобы искать кластеры аргументов, которые более тесно связаны, а затем реорганизовать каждый из этих кластеров в отдельный объект параметров.
Марк Симанн
2
Вы можете превратить каждую группу в неизменный объект сам по себе. Каждый объект должен принимать только несколько параметров, поэтому, хотя фактическое количество аргументов остается неизменным, количество аргументов, используемых в любом отдельном конструкторе, будет уменьшено.
Марк Симанн
2
+1 @ssg: Каждый раз, когда мне удавалось убедить себя в чем-то подобном, я со временем доказывал, что ошибаюсь, изгоняя полезную абстракцию из больших методов, требующих такого уровня параметров. Книга DDD Эванса может дать вам некоторые идеи о том, как думать об этом (хотя ваша система звучит так, как будто она далеко не уместна для применения таких шаблонов) - (и в любом случае это просто отличная книга).
Рубен Бартелинк
3
@Ruben: Ни одна здравомыслящая книга не сказала бы, что «в хорошем дизайне ОО класс не должен иметь более 5 свойств». Классы логически сгруппированы, и такая контекстуализация не может основываться на количествах. Однако поддержка неизменяемости в C # начинает создавать проблемы с определенным количеством свойств, прежде чем мы начнем нарушать хорошие принципы проектирования ОО.
Седат Капаноглу
3
@Ruben: Я не судил твои знания, отношение или характер. Я ожидаю, что вы сделаете то же самое. Я не говорю, что ваши рекомендации не хороши. Я говорю, что моя проблема может появиться даже на самом совершенном дизайне, и это кажется здесь упущенным моментом. Несмотря на то, что я понимаю, почему опытные люди задают фундаментальные вопросы о наиболее распространенных ошибках, выяснить их становится менее приятно после нескольких раундов. Я должен еще раз сказать, что очень возможно иметь эту проблему с идеальным дизайном. И спасибо за отзыв!
Седат Капаноглу
10

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

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

Плюсы:

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

Минусы:

  • Неизменность не очевидна, требует внимания разработчиков.
  • Ненужное копирование для поддержания неизменности
  • Занимает пространство стека
Джеффри Л Уитледж
источник
+1 Это правда, но это не решает часть о "разоблачении полей - это зло". Я не уверен, как все может ухудшиться, если я выберу поля вместо свойств этого API.
Седат Капаноглу
@ssg - так сделайте их открытыми свойствами вместо полей. Если вы рассматриваете это как структуру, в которой никогда не будет кода, то не имеет большого значения, используете ли вы свойства или поля. Если вы решите когда-нибудь дать ему код (например, валидацию или что-то в этом роде), то вам непременно захочется придать им свойства. По крайней мере, в публичных областях ни у кого никогда не будет иллюзий относительно существующих в этой структуре инвариантов. Он должен быть проверен методом точно так же, как параметры.
Джеффри Л Уитледж
О, это правда. Потрясающие! Спасибо.
Седат Капаноглу
8
Я думаю, что правило разоблачения полей зла применимо к объектам в объектно-ориентированном дизайне. Структура, которую я предлагаю, - это просто контейнер параметров с чистым металлом. Поскольку ваши инстинкты должны были идти с чем-то неизменным, я думаю, что такой базовый контейнер мог бы подойти для этого случая.
Джеффри Л Уитледж
1
@JeffreyLWhitledge: Мне действительно не нравится идея, что структуры с открытыми полями являются злом. Такое утверждение кажется мне равносильным утверждению, что отвертки - это зло, и люди должны использовать молотки, потому что острие отверток вдавливает гвозди. Если нужно вбить гвоздь, нужно использовать молоток, но если нужно вбить винт, нужно использовать отвертку. Есть много ситуаций, когда структура с открытым полем - это как раз то, что нужно для работы. Между прочим, гораздо меньше случаев, когда структура со свойствами get / set является действительно правильным инструментом (в большинстве случаев, где ...
суперкат
6

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

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();
        }
    }
Марто
источник
1
+1 Это совершенно правильное решение, но я думаю, что это слишком много лесов, чтобы поддерживать такое простое дизайнерское решение. Особенно, когда решение «только для чтения» «очень» близко к идеальному.
Седат Капаноглу
2
Как это решает проблему «слишком много параметров»? Синтаксис может быть другим, но проблема выглядит так же. Это не критика, мне просто любопытно, потому что я не знаком с этой моделью.
Alexd
1
@alexD это решает проблему с наличием слишком большого количества параметров функции и сохранением неизменности объекта. Только класс строителя может устанавливать приватные свойства, и как только вы получите объект параметров, вы не сможете его изменить. Проблема в том, что для этого требуется много кода.
Марто
5
Параметр объекта в вашем решении не является неизменным. Кто-то, кто сохраняет конструктор, может редактировать параметры даже после сборки
astef
1
Кстати, в момент написания @ astef DoSomeActionParameters.Builderэкземпляр может быть использован для создания и настройки ровно одного DoSomeActionParametersэкземпляра. После вызова Build()последующих изменений Builderсвойств s будет продолжено изменение свойств исходного DoSomeActionParameters экземпляра, а последующие вызовы Build()продолжат возвращать тот же DoSomeActionParametersэкземпляр. Так и должно быть public DoSomeActionParameters Build() { var oldObj = obj; obj = new DoSomeActionParameters(); return oldObj; }.
Бекон
6

Я не программист на 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")

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

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

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

Минусы:

  • Некоторая повторяющаяся работа (одни и те же объявления в двух разных сущностях)
  • Разработчик должен угадать, что DoSomeActionParametersэто класс, который может быть сопоставлен сIDoSomeActionParameters
trutheality
источник
+1 Я не знаю, почему я не подумал об этом? :) Думаю, я думал, что объект все еще будет страдать от конструктора со слишком большим количеством параметров, но это не так. Да, это тоже очень правильное решение. Единственная проблема, о которой я могу подумать, это то, что пользователю API не очень просто найти правильное имя класса, которое поддерживает данный интерфейс. Решение, которое требует наименьшего количества документации, лучше.
Седат Капаноглу
Мне нравится этот, дублирование и знание карты обрабатываются с помощью resharper, и я могу предоставить значения по умолчанию, используя конструктор по умолчанию для конкретного класса
Энтони Джонстон
2
Мне не нравится такой подход. Кто-то, кто имеет ссылку на произвольную реализацию IDoSomeActionParametersи хочет захватить в ней значения, не может знать, будет ли достаточно удерживать ссылку, или он должен скопировать значения в какой-то другой объект. Читаемые интерфейсы полезны в некоторых контекстах, но не как средство сделать вещи неизменными.
суперкат
«Параметры IDoSomeActionParameters» могут быть преобразованы в DoSomeActionParameters и изменены. Разработчик может даже не осознавать, что они обходят попытку сделать параметры неизменяемыми
Джозеф Симпсон
3

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

    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

Вы можете использовать подход в стиле 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);
Крис Уайт
источник
Ах, Марто обошел меня с предложением строителя!
Крис Уайт
2

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

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

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

Это немного отличается от 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 является неизменным, поскольку он может быть и не может быть создан напрямую, так как его конструктор по умолчанию является частным

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

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

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

Еще немного куки я знаю, но все равно попробую

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

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";
            })
        );
Энтони Джонстон
источник
1

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

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

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

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

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

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

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

jmoreno
источник
+1 Может быть, частные поля только для чтения и открытые свойства только для получения могут быть объединены в структуру, чтобы получить менее подробное решение.
Седат Капаноглу
Открытые свойства чтения-записи в структурах, которые мутируют, thisявляются гораздо более злыми, чем открытые поля. Я не уверен, почему вы думаете, что только установка значения однажды делает вещи "хорошо"; если thisначать с экземпляра структуры по умолчанию, проблемы, связанные со свойствами -mutating, все равно будут существовать.
суперкат
0

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

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

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

Vilx-
источник
Это изменчивый класс независимо.
Седат Капаноглу
@ssg - Ну да, это так. DoMagicИмеет в своем контракте , хотя , что она не будет изменять объект. Но я думаю, это не защищает от случайных изменений.
Vilx-