¿Cómo evitar el problema de "demasiados parámetros" en el diseño de API?

160

Tengo esta función API:

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

No me gusta Porque el orden de los parámetros se vuelve innecesariamente significativo. Se hace más difícil agregar nuevos campos. Es más difícil ver lo que se pasa. Es más difícil refactorizar el método en partes más pequeñas porque crea otra sobrecarga al pasar todos los parámetros en las subfunciones. El código es más difícil de leer.

Se me ocurrió la idea más obvia: tener un objeto encapsulando los datos y pasarlos en lugar de pasar cada parámetro uno por uno. Esto es lo que se me ocurrió:

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

Eso redujo mi declaración de API a:

public ResultEnum DoSomeAction(DoSomeActionParameters parameters, out Guid code)

Agradable. Parece muy inocente, pero en realidad introdujimos un gran cambio: introdujimos la mutabilidad. Porque lo que habíamos estado haciendo anteriormente era pasar un objeto inmutable anónimo: parámetros de función en la pila. Ahora creamos una nueva clase que es muy mutable. Creamos la capacidad de manipular el estado de la persona que llama . Eso apesta. Ahora quiero que mi objeto sea inmutable, ¿qué hago?

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

Como puede ver, en realidad volví a crear mi problema original: demasiados parámetros. Es obvio que ese no es el camino a seguir. ¿Que voy a hacer? La última opción para lograr tal inmutabilidad es usar una estructura "de solo lectura" como esta:

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

Eso nos permite evitar constructores con demasiados parámetros y lograr la inmutabilidad. En realidad soluciona todos los problemas (ordenamiento de parámetros, etc.). Todavía:

Fue entonces cuando me confundí y decidí escribir esta pregunta: ¿Cuál es la forma más directa en C # para evitar el problema de "demasiados parámetros" sin introducir mutabilidad? ¿Es posible usar una estructura de solo lectura para ese propósito y, sin embargo, no tener un mal diseño de API?

Aclaraciones

  • Suponga que no hay violación del principio de responsabilidad única. En mi caso original, la función simplemente escribe los parámetros dados en un solo registro DB.
  • No estoy buscando una solución específica para la función dada. Estoy buscando un enfoque generalizado para tales problemas. Estoy específicamente interesado en resolver el problema de "demasiados parámetros" sin introducir mutabilidad o un diseño terrible.

ACTUALIZAR

Las respuestas proporcionadas aquí tienen diferentes ventajas / desventajas. Por lo tanto, me gustaría convertir esto a una wiki comunitaria. Creo que cada respuesta con código de muestra y Pros / Contras sería una buena guía para problemas similares en el futuro. Ahora estoy tratando de averiguar cómo hacerlo.

ssg
fuente
Clean Code: A Handbook of Agile Software Craftsmanship por Robert C. Martin y el libro de Refactorización de Martin Fowler cubre esto un poco
Ian Ringrose
1
¿No es esa solución Builder redundante si usa C # 4 donde tiene parámetros opcionales ?
khellang
1
Puede que sea estúpido, pero no veo cómo esto es un problema, teniendo en cuenta que DoSomeActionParameterses un objeto desechable, que se descartará después de la llamada al método.
Vilx-
66
Tenga en cuenta que no estoy diciendo que deba evitar los campos de solo lectura en una estructura; Las estructuras inmutables son una práctica recomendada y los campos de solo lectura le ayudan a crear estructuras inmutables autodocumentadas. Mi punto es que no debe confiar en que los campos de solo lectura sean observados para que nunca cambien , porque eso no es una garantía de que un campo de solo lectura le dé en una estructura. Este es un caso específico de los consejos más generales de que no debe tratar los tipos de valor como si fueran tipos de referencia; Son un animal muy diferente.
Eric Lippert
77
@ssg: Me encantaría eso también. Hemos agregado características a C # que promueven la inmutabilidad (como LINQ) al mismo tiempo que agregamos características que promueven la mutabilidad (como los inicializadores de objetos). Sería bueno tener una mejor sintaxis para promover tipos inmutables. Estamos pensando mucho y tenemos algunas ideas interesantes, pero no esperaría nada de eso para la próxima versión.
Eric Lippert

Respuestas:

22

Un estilo adoptado en los marcos suele ser como agrupar parámetros relacionados en clases relacionadas (pero una vez más problemático con la mutabilidad):

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
Teoman Soygul
fuente
La condensación de todas las cadenas en una matriz de cadenas solo tiene sentido si todas están relacionadas. Por el orden de los argumentos, parece que no todos están completamente relacionados.
icktoofay
Además, acordó que eso solo funcionaría si tiene muchos de los mismos tipos como parámetro.
Timo Willemsen
¿Cómo es eso diferente a mi primer ejemplo en la pregunta?
Sedat Kapanoglu
Bueno, este es el estilo habitual, incluso en .NET Framework, lo que indica que su primer ejemplo es correcto en lo que está haciendo, incluso presenta problemas menores de mutabilidad (aunque este ejemplo es obviamente de otra biblioteca). Buena pregunta por cierto.
Teoman Soygul
2
He estado convergiendo con este estilo más con el tiempo. Aparentemente, una cantidad significativa de los problemas de "demasiados parámetros" se puede resolver con buenos grupos lógicos y abstracciones. Al final, hace que el código sea más legible y más modularizado.
Sedat Kapanoglu
87

Utilice una combinación de constructor y API de estilo de lenguaje específico de dominio - Interfaz fluida. El API es un poco más detallado, pero con intellisense es muy rápido de escribir y fácil de entender.

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());
Samuel Neff
fuente
+1: este tipo de enfoque (que comúnmente he visto llamado una "interfaz fluida") es exactamente lo que tenía en mente también.
Daniel Pryden
+1: esto es lo que terminé en la misma situación. Excepto que solo había una clase, y DoSomeActionera un método para ello.
Vilx-
+1 Yo también pensé en esto, pero como soy nuevo en interfaces fluidas, no sabía si encajaba perfectamente aquí. Gracias por validar mi propia intuición.
Matt
No había oído hablar del patrón Builder antes de hacer esta pregunta, así que esta ha sido una experiencia perspicaz para mí. Me pregunto qué tan común es? Porque cualquier desarrollador que no haya oído hablar del patrón podría tener problemas para comprender el uso sin una documentación.
Sedat Kapanoglu
1
@ssg, es común en estos escenarios. El BCL tiene clases de generador de cadenas de conexión, por ejemplo.
Samuel Neff
10

Lo que tienes allí es una indicación bastante segura de que la clase en cuestión está violando el Principio de Responsabilidad Única porque tiene demasiadas dependencias. Busque formas de refactorizar esas dependencias en grupos de dependencias de fachada .

Mark Seemann
fuente
1
Todavía refactorizaría introduciendo uno o más objetos de parámetros, pero obviamente, si mueve todos los argumentos a un solo tipo inmutable, no ha logrado nada. El truco consiste en buscar grupos de argumentos que estén más estrechamente relacionados, y luego refactorizar cada uno de esos grupos a un objeto de parámetro separado.
Mark Seemann
2
Puede convertir cada grupo en un objeto inmutable en sí mismo. Cada objeto solo necesitaría tomar algunos parámetros, por lo tanto, si bien el número real de argumentos sigue siendo el mismo, el número de argumentos utilizados en cualquier constructor individual se reducirá.
Mark Seemann
2
+1 @ssg: Cada vez que he logrado convencerme de algo como esto, he demostrado que me equivoco con el tiempo al extraer abstracciones útiles de grandes métodos que requieren este nivel de parámetros. El libro DDD de Evans puede darle algunas ideas sobre cómo pensar sobre esto (aunque su sistema parece estar lejos de ser un lugar relevante para la aplicación de tales patrones) - (y es un gran libro de cualquier manera).
Ruben Bartelink
3
@Ruben: Ningún libro sensato diría "en un buen diseño OO, una clase no debe tener más de 5 propiedades". Las clases se agrupan lógicamente y ese tipo de contextualización no puede basarse en cantidades. Sin embargo, el soporte de inmutabilidad de C # comienza a crear problemas en un cierto número de propiedades antes de que comencemos a violar los buenos principios de diseño de OO.
Sedat Kapanoglu
3
@ Rubén: No juzgué tu conocimiento, actitud o temperamento. Espero que hagan lo mismo. No digo que tus recomendaciones no sean buenas. Estoy diciendo que mi problema puede aparecer incluso en el diseño más perfecto y que parece ser un punto pasado por alto aquí. Aunque entiendo por qué las personas experimentadas hacen preguntas fundamentales sobre los errores más comunes, resulta menos agradable aclararlo después de un par de rondas. Debo decir nuevamente que es muy posible tener ese problema con un diseño perfecto. Y gracias por el voto!
Sedat Kapanoglu
10

Simplemente cambie su estructura de datos de parámetros de a classa structay estará listo.

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) 

El método ahora obtendrá su propia copia de la estructura. El método no puede observar los cambios realizados en la variable de argumento, y la persona que llama no puede observar los cambios que realiza el método en la variable. El aislamiento se logra sin inmutabilidad.

Pros:

  • Más fácil de implementar.
  • Menos cambio de comportamiento en la mecánica subyacente

Contras:

  • La inmutabilidad no es obvia, requiere la atención del desarrollador.
  • Copia innecesaria para mantener la inmutabilidad
  • Ocupa espacio de pila
Jeffrey L Whitledge
fuente
+1 Eso es cierto, pero no resuelve la parte de "exponer campos directamente es malo". No estoy seguro de qué tan mal podrían empeorar las cosas si opto por usar campos en lugar de propiedades en esa API.
Sedat Kapanoglu
@ssg: así que conviértalos en propiedades públicas en lugar de campos. Si trata esto como una estructura que nunca tendrá código, entonces no hace mucha diferencia si usa propiedades o campos. Si alguna vez decides darle código (como validación o algo así), definitivamente querrás convertirlas en propiedades. Al menos con los campos públicos, nadie se hará ilusiones acerca de los invariantes existentes en esta estructura. Tendrá que ser validado por el método exactamente como lo habrían sido los parámetros.
Jeffrey L Whitledge
Oh eso es verdad ¡Increíble! Gracias.
Sedat Kapanoglu
8
Creo que la regla exponer-campos-es-mal se aplica a los objetos en un diseño orientado a objetos. La estructura que estoy proponiendo es solo un contenedor de parámetros de metal desnudo. Como sus instintos iban a ir con algo inmutable, creo que un contenedor tan básico podría ser apropiado para esta ocasión.
Jeffrey L Whitledge
1
@JeffreyLWhitledge: Realmente no me gusta la idea de que las estructuras de campo expuesto sean malvadas. Tal afirmación me parece equivalente a decir que los destornilladores son malos y las personas deberían usar martillos porque las puntas de los destornilladores mellan las cabezas de los clavos. Si necesita clavar un clavo, debe usar un martillo, pero si necesita clavar un tornillo, debe usar un destornillador. Hay muchas situaciones en las que una estructura de campo expuesto es precisamente la herramienta adecuada para el trabajo. Por cierto, hay muchos menos en los que una estructura con propiedades get / set es realmente la herramienta correcta (en la mayoría de los casos donde ...
supercat
6

¿Qué tal crear una clase de generador dentro de su clase de datos? La clase de datos tendrá todos los establecedores como privados y solo el constructor podrá establecerlos.

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();
        }
    }
marto
fuente
1
+1 Esa es una solución perfectamente válida, pero creo que es demasiado andamiaje para mantener una decisión de diseño tan simple. Especialmente cuando la solución de "estructura de solo lectura" es "muy" cercana al ideal.
Sedat Kapanoglu
2
¿Cómo resuelve esto el problema de "demasiados parámetros"? La sintaxis puede ser diferente, pero el problema es el mismo. Esto no es una crítica, solo tengo curiosidad porque no estoy familiarizado con este patrón.
alexD
1
@alexD esto resuelve el problema de tener demasiados parámetros de función y mantener el objeto inmutable. Solo la clase de constructor puede establecer las propiedades privadas y una vez que obtiene el objeto de parámetros no puede cambiarlo. El problema es que requiere mucho código de andamios.
marto
55
El objeto de parámetro en su solución no es inmutable. Alguien que guarde el constructor puede editar los parámetros incluso después de la construcción
partir del
1
Para el punto de @ astef, como está escrito actualmente, DoSomeActionParameters.Builderse puede usar una instancia para crear y configurar exactamente una DoSomeActionParametersinstancia. Después de llamar Build()los cambios posteriores a las Builderpropiedades de 's, continuará modificando las propiedades de la instancia original DoSomeActionParameters , y las llamadas posteriores a Build()continuarán devolviendo la misma DoSomeActionParametersinstancia. Realmente debería serlo public DoSomeActionParameters Build() { var oldObj = obj; obj = new DoSomeActionParameters(); return oldObj; }.
BACON
6

No soy un programador de C # pero creo que C # admite argumentos con nombre: (F # sí y C # es en gran medida una característica compatible para ese tipo de cosas) Sí: http://msdn.microsoft.com/en-us/library/dd264739 .aspx # Y342

Entonces llamar a su código original se convierte en:

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

Esto no requiere más espacio / pensamiento que la creación de su objeto y tiene todos los beneficios, del hecho de que no ha cambiado lo que está sucediendo en el sistema infalible en absoluto. Ni siquiera tiene que recodificar nada para indicar que los argumentos están nombrados

Editar: aquí hay un artículo que encontré al respecto. http://www.globalnerdy.com/2009/03/12/default-and-named-parameters-in-c-40-sith-lord-in-training/ Debo mencionar que C # 4.0 admite argumentos con nombre, 3.0 no

Lyndon White
fuente
Una mejora de hecho. Pero esto solo resuelve la legibilidad del código y solo lo hace cuando el desarrollador opta por usar parámetros con nombre. Es fácil olvidar especificar nombres y regalar los beneficios. No ayuda al escribir el código en sí. por ejemplo, al refactorizar la función en otras más pequeñas y pasar los datos en un solo paquete contenido.
Sedat Kapanoglu 05 de
6

¿Por qué no simplemente hacer una interfaz que imponga la inmutabilidad (es decir, solo getters)?

Es esencialmente su primera solución, pero obliga a la función a usar la interfaz para acceder al parámetro.

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

y la declaración de función se convierte en:

public ResultEnum DoSomeAction(IDoSomeActionParameters parameters, out Guid code)

Pros:

  • No tiene problema de espacio de pila como structsolución
  • Solución natural utilizando semántica del lenguaje.
  • La inmutabilidad es obvia
  • Flexible (el consumidor puede usar una clase diferente si lo desea)

Contras:

  • Algunos trabajos repetitivos (mismas declaraciones en dos entidades diferentes)
  • El desarrollador tiene que adivinar que DoSomeActionParameterses una clase que podría asignarse aIDoSomeActionParameters
la verdad
fuente
+1 No sé por qué no pensé en eso. :) Creo que pensé que el objeto aún sufriría de un constructor con demasiados parámetros, pero ese no es el caso. Sí, esa también es una solución muy válida. El único problema que se me ocurre es que no es realmente sencillo para el usuario de la API encontrar el nombre de clase correcto que admita la interfaz dada. La solución que requiere la menor documentación es mejor.
Sedat Kapanoglu 05 de
Me gusta este, la duplicación y el conocimiento del mapa se manejan con resharper, y puedo proporcionar valores predeterminados utilizando el constructor predeterminado de la clase concreta
Anthony Johnston,
2
No me gusta ese enfoque. Alguien que tiene una referencia a una implementación arbitraria IDoSomeActionParametersy desea capturar los valores allí no tiene forma de saber si mantener la referencia será suficiente o si debe copiar los valores a algún otro objeto. Las interfaces legibles son útiles en algunos contextos, pero no como un medio para hacer que las cosas sean inmutables.
supercat
Los "parámetros IDoSomeActionParameters" se pueden convertir a DoSomeActionParameters y cambiar. El desarrollador puede incluso no darse cuenta de que están eludiendo un intento de hacer que los parámetros imutable
Joseph Simpson
3

Sé que esta es una vieja pregunta, pero pensé en meterme con mi sugerencia, ya que solo tuve que resolver el mismo problema. Ahora, sin duda, mi problema era ligeramente diferente al suyo, ya que tenía el requisito adicional de no querer que los usuarios pudieran construir este objeto por sí mismos (toda la hidratación de los datos provenía de la base de datos, por lo que podía encarcelar toda la construcción internamente). Esto me permitió usar un constructor privado y el siguiente patrón;

    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; } 
        }
    }
Mikey Hogarth
fuente
2

Podría usar un enfoque de estilo de constructor, aunque dependiendo de la complejidad de su DoSomeActionmétodo, esto podría ser un poco pesado. Algo en este sentido:

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);
Chris White
fuente
¡Ah, Marto me ganó la sugerencia del constructor!
Chris White
2

Además de la respuesta manji, es posible que desee dividir una operación en varias más pequeñas. Comparar:

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

y

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

Para aquellos que no conocen POSIX, la creación del niño puede ser tan fácil como:

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

Cada elección de diseño representa una compensación sobre las operaciones que puede realizar.

PD. Sí, es similar al constructor, solo al revés (es decir, en el lado de la persona que llama en lugar de la persona que llama). Puede o no ser mejor que el generador en este caso específico.

Maciej Piechotka
fuente
Es una operación pequeña, pero es una buena recomendación para cualquiera que esté violando el principio de responsabilidad única. Mi pregunta tiene lugar justo después de que se hayan realizado todas las demás mejoras de diseño.
Sedat Kapanoglu 05 de
UPS. Lo siento, preparé una pregunta antes de su edición y la publiqué más tarde, por lo tanto, no me di cuenta.
Maciej Piechotka
2

aquí es un poco diferente de Mikeys, pero lo que estoy tratando de hacer es hacer todo lo menos posible para escribir

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 es inmutable, ya que puede y no puede crearse directamente, ya que su constructor predeterminado es privado.

El inicializador no es inmutable, sino solo un medio de transporte.

El uso aprovecha el inicializador en el Inicializador (si obtiene mi deriva) Y puedo tener valores predeterminados en el constructor predeterminado del Inicializador

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

Los parámetros serán opcionales aquí, si desea que se requieran algunos, puede ponerlos en el constructor predeterminado de Initializer

Y la validación podría ir en el método Crear

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

Entonces ahora parece que

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

Todavía soy un poco kooki, lo sé, pero lo intentaré de todos modos

Editar: al mover el método de creación a una estática en el objeto de parámetros y agregar un delegado que pasa el inicializador, se elimina algo de lo extraño de la llamada

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

Entonces la llamada ahora se ve así

DoSomeAction(
        DoSomeActionParameters.Create(
            i => {
                i.A = "Hello";
            })
        );
Anthony Johnston
fuente
1

Use la estructura, pero en lugar de los campos públicos, tenga propiedades públicas:

• Todos (incluidos FXCop y Jon Skeet) están de acuerdo en que exponer los campos públicos es malo.

Jon y FXCop estarán satisfechos porque está exponiendo propiedades y no campos.

• Eric Lippert et al dicen que confiar en campos de solo lectura para la inmutabilidad es una mentira.

Eric estará satisfecho porque al usar propiedades, puede asegurarse de que el valor solo se establezca una vez.

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

Un objeto semiinmutable (el valor puede establecerse pero no modificarse). Funciona por valor y tipos de referencia.

jmoreno
fuente
+1 Quizás los campos privados de solo lectura y las propiedades públicas de solo getter podrían combinarse en una estructura para permitir una solución menos detallada.
Sedat Kapanoglu 05 de
Las propiedades públicas de lectura y escritura en estructuras que mutan thisson mucho más malas que los campos públicos. No estoy seguro de por qué piensas que solo establecer el valor una vez hace que las cosas estén "bien"; Si uno comienza con una instancia predeterminada de una estructura, los problemas asociados con thislas propiedades de mutación seguirán existiendo.
supercat
0

Una variante de la respuesta de Samuel que utilicé en mi proyecto cuando tuve el mismo problema:

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

Y para usar:

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

En mi caso, los parámetros fueron modificables intencionalmente, porque los métodos de establecimiento no permitieron todas las combinaciones posibles, y simplemente expusieron combinaciones comunes de ellos. Esto se debe a que algunos de mis parámetros eran bastante complejos y los métodos de escritura para todos los casos posibles habrían sido difíciles e innecesarios (rara vez se usan combinaciones locas).

Vilx-
fuente
Esta es una clase mutable independientemente.
Sedat Kapanoglu
@ssg - Bueno, sí, lo es. Sin DoMagicembargo, tiene en su contrato que no modificará el objeto. Pero supongo que no protege de modificaciones accidentales.
Vilx-