c# - ¿Cómo evitar el problema de "demasiados parámetros" en el diseño de API?
data-structures immutability (13)
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 vuelve más difícil agregar nuevos campos. Es más difícil ver lo que se está pasando. 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 funciones secundarias. El código es más difícil de leer.
Se me ocurrió la idea más obvia: tener un objeto que encapsule los datos y pasarlos en lugar de pasar cada parámetro uno por uno. Aquí 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)
Bonito. Parece muy inocente, pero en realidad presentamos un gran cambio: presentamos mutabilidad. Porque lo que previamente habíamos estado haciendo era pasar un objeto anónimo inmutable: 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é debo hacer?
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 pueden ver, en realidad recreé 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 dicha 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:
- Todos (incluidos FXCop y Jon Skeet) coinciden en que exponer los campos públicos es malo .
- Eric Lippert y otros dicen que confiar en los campos de solo lectura para la inmutabilidad es una mentira .
Fue entonces cuando me confundí y decidí escribir esta pregunta: ¿Cuál es la forma más directa en C # de evitar el problema de "demasiados parámetros" sin introducir la mutabilidad? ¿Es posible utilizar una estructura de solo lectura para ese propósito y, sin embargo, no tener un diseño de API incorrecto?
ACLARACIONES
- Por favor, suponga que no hay violación del principio de responsabilidad individual. En mi caso original, la función solo escribe los parámetros dados en un único registro de base de datos.
- 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 de la comunidad. 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 descubrir cómo hacerlo.
¿Por qué no crear una interfaz que refuerce la inmutabilidad (es decir, solo getters)?
Básicamente es su primera solución, pero fuerza 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 la función se convierte en:
public ResultEnum DoSomeAction(IDoSomeActionParameters parameters, out Guid code)
Pros:
- No tiene problema de espacio en la pila como la solución de
struct
- Solución natural usando semántica de lenguaje
- La inmutabilidad es obvia
- Flexible (el consumidor puede usar una clase diferente si lo desea)
Contras:
- Algunos trabajos repetitivos (las mismas declaraciones en dos entidades diferentes)
- El desarrollador tiene que adivinar que
DoSomeActionParameters
es una clase que se puede asignar aIDoSomeActionParameters
¿Qué tal crear una clase de constructor dentro de su clase de datos? La clase de datos tendrá todos los setters como privados y solo el constructor podrá configurarlos.
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();
}
}
Además de la respuesta manji, también puede 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 de un 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 opción de diseño representa una compensación sobre las operaciones que puede realizar.
PD. Sí, es similar al generador, solo al revés (es decir, en el lado de llamada en lugar de llamar). Puede o no ser mejor que el constructor en este caso específico.
Aquí hay una ligeramente 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 obtienes mi deriva) y puedo tener los 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 quieres que se necesiten algunos, puedes ponerlos en el constructor predeterminado del Inicializador
Y la validación podría ir en el método 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);
}
}
Así que ahora parece
DoSomeAction(new DoSomeActionParameters.Initializer
(b: 42)
{
A = "Hello"
}
.Create());
Todavía un poco kooki lo sé, pero voy a intentarlo de todos modos
Editar: mover el método de creación a una estática en el objeto de parámetros y agregar un delegado que pase el inicializador quita algo de la curiosidad 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";
})
);
Lo que tienes allí es una indicación bastante segura de que la clase en cuestión está violando el Principio de Responsabilidad Individual porque tiene demasiadas dependencias. Busque formas de refactorizar esas dependencias en grupos de Dependencias de fachada .
No soy un programador de C # pero creo que C # admite argumentos con nombre: (F # lo hace y C # es en gran parte una característica compatible para ese tipo de cosas) Lo hace: http://msdn.microsoft.com/en-us/library/dd264739.aspx#Y342
Así que 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 en absoluto lo que está sucediendo en el sistema de la nada. Ni siquiera tiene que recodificar nada para indicar que los argumentos se nombran
Editar: aquí hay una artical 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 lo hizo
Puede usar un enfoque de estilo Constructor, aunque dependiendo de la complejidad de su método DoSomeAction
, este podría ser un peso pesado al tacto. Algo en esta línea:
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);
Sé que esta es una vieja pregunta, pero pensé que entraría con mi sugerencia ya que solo tuve que resolver el mismo problema. Ahora bien, admitidly mi problema era ligeramente diferente al suyo ya que tenía el requisito adicional de no querer que los usuarios pudieran construir este objeto ellos mismos (toda la hidratación de los datos provino de la base de datos, así que podría encarcelar todas las construcciones 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; }
}
}
Simplemente cambie la estructura de datos de parámetros de una class
a una struct
y struct
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 el llamador no puede observar los cambios realizados en la variable. El aislamiento se logra sin inmutabilidad.
Pros:
- Más fácil de implementar
- Mínimo 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 el espacio de la pila
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
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 intencionalmente modificables, porque los métodos setter no permitieron todas las combinaciones posibles, y solo expusieron combinaciones comunes de ellos. Eso es porque algunos de mis parámetros eran bastante complejos y los métodos de escritura para todos los casos posibles hubieran sido difíciles e innecesarios (rara vez se usan combinaciones locas).
Use la estructura, pero en lugar de campos públicos, tenga propiedades públicas:
• Todos (incluidos FXCop y Jon Skeet) coinciden en que exponer los campos públicos es malo.
Jon y FXCop se sentirán satisfechos porque están exponiendo propiedades, no campos.
• Eric Lippert y otros dicen que confiar en los 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 (se puede establecer el valor pero no se cambia). Funciona por valor y tipos de referencia.
Use una combinación de API de estilo de lenguaje específico y de compilador - Interfaz fluida. La API es un poco más detallada, pero con intellisense es muy rápida 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());