Digamos que tengo un procedimiento que hace cosas :
void doStuff(initalParams) {
...
}
Ahora descubro que "hacer cosas" es una operación bastante complicada. El procedimiento se hace más grande, lo divido en varios procedimientos más pequeños y pronto me doy cuenta de que tener algún tipo de estado sería útil al hacer cosas, por lo que necesito pasar menos parámetros entre los pequeños procedimientos. Entonces, lo factorizo en su propia clase:
class StuffDoer {
private someInternalState;
public Start(initalParams) {
...
}
// some private helper procedures here
...
}
Y luego lo llamo así:
new StuffDoer().Start(initialParams);
o así:
new StuffDoer(initialParams).Start();
Y esto es lo que se siente mal. Cuando uso la API .NET o Java, nunca llamo new SomeApiClass().Start(...);
, lo que me hace sospechar que lo estoy haciendo mal. Claro, podría hacer que el constructor de StuffDoer sea privado y agregar un método auxiliar estático:
public static DoStuff(initalParams) {
new StuffDoer().Start(initialParams);
}
Pero entonces tendría una clase cuya interfaz externa consta de un solo método estático, que también se siente raro.
De ahí mi pregunta: ¿Hay un patrón bien establecido para este tipo de clases que
- tener un solo punto de entrada y
- ¿no tiene un estado "reconocible externamente", es decir, el estado de instancia solo se requiere durante la ejecución de ese único punto de entrada?
bool arrayContainsSomestring = new List<string>(stringArray).Contains("somestring");
cuando todo lo que me importaba era esa información en particular y los métodos de extensión LINQ no están disponibles. Funciona bien y se ajusta dentro de unaif()
condición sin necesidad de saltar a través de aros. Por supuesto, desea un lenguaje recolectado de basura si está escribiendo código como ese.Respuestas:
Hay un patrón llamado Object Object donde factoriza un método único y grande con muchas variables / argumentos temporales en una clase separada. Hace esto en lugar de simplemente extraer partes del método en métodos separados porque necesitan acceso al estado local (los parámetros y las variables temporales) y no puede compartir el estado local utilizando variables de instancia porque serían locales para ese método (y los métodos extraídos de él) solo y no serían utilizados por el resto del objeto.
Entonces, en cambio, el método se convierte en su propia clase, los parámetros y las variables temporales se convierten en variables de instancia de esta nueva clase, y luego el método se divide en métodos más pequeños de la nueva clase. La clase resultante generalmente tiene un solo método de instancia pública que realiza la tarea que encapsula la clase.
fuente
Diría que la clase en cuestión (usando un único punto de entrada) está bien diseñada. Es fácil de usar y extender. Muy solido.
Lo mismo para
no "externally recognizable" state
. Es buena encapsulación.No veo ningún problema con código como:
fuente
doer
nuevamente, eso se reduce avar result = new StuffDoer(initialParams).Calculate(extraParams);
.StringComparer
clase que utilizasnew StringComparer().Compare( "bleh", "blah" )
bien diseñada? ¿Qué pasa con la creación de estado cuando no hay necesidad de hacerlo? De acuerdo, el comportamiento está bien encapsulado (bueno, al menos para el usuario de la clase, más sobre eso en mi respuesta ), pero eso no lo convierte en un "buen diseño" por defecto. En cuanto a su ejemplo de "resultado real". Esto solo tendría sentido si alguna vez usarasdoer
separado deresult
.one reason to change
. La diferencia puede parecer sutil. Tienes que entender lo que eso significa. Nunca creará clases de un métodoDesde una perspectiva de principios SÓLIDOS , la respuesta de jgauffin tiene sentido. Sin embargo, no debe olvidarse de los principios generales de diseño, por ejemplo, la ocultación de información .
Veo varios problemas con el enfoque dado:
Algunas referencias relacionadas
Posiblemente, un argumento principal radica en qué tan lejos se puede extender el Principio de Responsabilidad Única . "Si lo llevas a ese extremo y construyes clases que tienen una razón para existir, puedes terminar con un solo método por clase. Esto causaría una gran expansión de clases incluso para los procesos más simples, causando que el sistema sea difícil de entender y difícil de cambiar ".
Otra referencia relevante relacionada con este tema: "Divida sus programas en métodos que realicen una tarea identificable. Mantenga todas las operaciones en un método al mismo nivel de abstracción ". - Kent Beck Key aquí es "el mismo nivel de abstracción". Eso no significa "una cosa", ya que a menudo se interpreta. Este nivel de abstracción depende completamente del contexto para el que está diseñando.
Entonces, ¿cuál es el enfoque adecuado?
Sin conocer su caso de uso concreto, es difícil saberlo. Hay un escenario en el que a veces (no muy a menudo) uso un enfoque similar. Cuando quiero procesar un conjunto de datos, sin querer que esta funcionalidad esté disponible para todo el alcance de la clase. Escribí una publicación de blog sobre esto, cómo lambdas puede mejorar aún más la encapsulación . Yo también empecé a una pregunta sobre el tema aquí en Los programadores . El siguiente es un último ejemplo de dónde usé esta técnica.
Dado que el código muy 'local' dentro del
ForEach
no se reutiliza en ningún otro lugar, simplemente puedo mantenerlo en la ubicación exacta donde es relevante. Esbozar el código de tal manera que el código que se basa el uno en el otro está fuertemente agrupado lo hace más legible en mi opinión.Posibles alternativas
fuente
Diría que es bastante bueno, pero que su API aún debería ser un método estático en una clase estática, ya que eso es lo que el usuario espera. El hecho de que el método estático lo use
new
para crear su objeto auxiliar y hacer el trabajo es un detalle de implementación que debe ocultarse a quien lo llama.fuente
Hay un problema con los desarrolladores despistados que podrían usar una instancia compartida y usarla
Por lo tanto, esto necesita documentación explícita de que no es seguro para subprocesos y si es liviano (crearlo es rápido, no ata recursos externos ni mucha memoria) que está bien instanciar sobre la marcha.
Esconderlo en un método estático ayuda con esto, porque luego reutilizar la instancia nunca sucede.
Si tiene una inicialización costosa, podría (no siempre) ser beneficioso preparar la inicialización una vez y usarla varias veces creando otra clase que contenga solo estado y la haga clonable. La inicialización crearía un estado en el que se almacenaría
doer
.start()
lo clonaría y lo pasaría a métodos internos.Esto también permitiría otras cosas como el estado persistente de ejecución parcial. (Si lleva mucho tiempo y factores externos, por ejemplo, fallas en el suministro de electricidad, podrían interrumpir la ejecución). Pero estas cosas extravagantes adicionales generalmente no son necesarias, por lo que no valen la pena.
fuente
Además de mi otra respuesta más elaborada y personalizada , siento que la siguiente observación merece una respuesta por separado.
Hay indicios de que podrías estar siguiendo el antipatrón de Poltergeists .
fuente
Se pueden encontrar varios patrones en el catálogo P de EAA de Martin Fowler ;
fuente