2009-06-25 12 views
17

Esto es cosas muy básicas, pero aquí va. Encuentro que nunca puedo estar de acuerdo conmigo mismo si la manera en que yo dividí las clases grandes en más pequeñas hace que las cosas sean más fáciles de mantener o menos mantenibles. Estoy familiarizado con patrones de diseño, aunque no en detalle, y también con los conceptos de diseño orientado a objetos. Dejando a un lado todas las reglas y pautas de lujo, estoy buscando elegir tu cerebro con respecto a lo que me falta con un escenario de muestra muy simple. Esencialmente en la línea de: "... este diseño lo hará más difícil", etc ... Todas las cosas que no anticipo debido a la falta de experiencia en esencia.Cómo dividir el código en componentes ... ¿grandes clases? clases pequeñas?

Supongamos que necesita escribir una clase de estilo básica de "lector de archivos/grabadora de archivos" para procesar un determinado tipo de archivo. Llamemos al archivo un archivo YadaKungFoo. El contenido de los archivos de YadaKungFoo es esencialmente similar a los archivos de INI, pero con diferencias sutiles.

hay secciones y valores:

[Sections] 
Kung,Foo 
Panda, Kongo 

[AnotherSection] 
Yada,Yada,Yada 
Subtle,Difference,Here,Yes 

[Dependencies] 
PreProcess,SomeStuffToPreDo 
PreProcess,MoreStuff 
PostProcess,AfterEight 
PostProcess,TheEndIsNear 
PostProcess,TheEnd 

OK, por lo que este puede producir 3 clases básicas:

public class YadaKungFooFile 
public class YadaKungFooFileSection 
public class YadaKungFooFileSectionValue 

Las dos últimas clases son esencialmente sólo las estructuras de datos con un override ToString() para escupir una lista de cadenas de valores almacenados usando un par de listas genéricas. Esto es suficiente para implementar la funcionalidad de guardado YadaKungFooFile.

Con el tiempo, el YadaYadaFile comienza a crecer. Varias sobrecargas para guardar en diferentes formatos, incluido XML, etc., y el archivo comienza a avanzar hacia 800 líneas más o menos. Ahora la pregunta real: Queremos agregar una función para validar el contenido de un archivo YadaKungFoo. La primera cosa que viene a la mente es, obviamente, para agregar:

var yada = new YadaKungFooFile("C:\Path"); 
var res = yada .Validate() 

Y hemos terminado (incluso podemos llamar a ese método desde el constructor). El problema es que la validación es bastante complicado, y hace que la clase muy grande, por lo que deciden crear una nueva clase como esta:

var yada = new YadaKungFooFile("C:\Path"); 
var validator = new YadaKungFooFileValidator(yada); 
var result = validator.Validate(); 

Ahora bien, esta muestra es, obviamente, terriblemente simple, trivial e insignificante. Cualquiera de las dos formas anteriores, probablemente no hará mucha diferencia, pero lo que no me gusta es:

  1. El YadaKungFooFileValidator clase y la claseYadaKungFooFile parecen estar muy fuertemente acoplados por este diseño . Parece que un cambio en una clase, probablemente provocaría cambios en la otra.
  2. Soy consciente de que frases como "Validador", "Controlador", "Administrador", etc. indican una clase que se preocupa por el estado de otros objetos en lugar de "su propio negocio", y por lo tanto viola la separación de principios de preocupación y envío de mensajes.

En general, creo que no tengo la experiencia para entender todos los aspectos de por qué un diseño es malo, en qué situaciones realmente no importa y qué preocupación tiene más peso: más pequeño clases o más clases cohesivas. Parecen ser requisitos contradictorios, pero tal vez estoy equivocado.Tal vez la clase de validador debe ser un objeto compuesto?

Básicamente, estoy solicitando comentarios sobre posibles beneficios/problemas que podrían surgir del diseño anterior. ¿Qué se puede hacer de manera diferente? (clase FileValidator base, interfaz FileValidator, etc. u nombre). Piense en la funcionalidad de YadaKungFooFile como siempre crece con el tiempo.

Respuesta

15

Bob Martin escribió una serie de artículos sobre el diseño de la clase, a la que se refiere como los principios sólidos:

http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod

Los principios son:

  1. La Responsabilidad Individual Principio: Un la clase debe tener una, y solo una, razón para cambiar.
  2. Principio de Open-Closed: Debería poder extender el comportamiento de una clase, sin modificarla.
  3. Principio de sustitución de Liskov: Las clases derivadas deben ser sustituibles por sus clases base.
  4. Principio de segregación de interfaz: Cree interfaces de grano fino que son específicas del cliente.
  5. Principio de inversión de dependencia: Depende de las abstracciones, no de las concreciones.

Así que a la luz de esos, vamos a ver algunas de las declaraciones:

Así que con el tiempo el YadaYadaFile empieza a crecer. Varias sobrecargas para guardar en diferentes formatos, incluyendo XML, etc., y el archivo empieza a empujar hacia 800 líneas más o menos

Ésta es una primera bandera roja grande: YadaYadaFile comienza con dos responsabilidades: 1) el mantenimiento de una clave/sección/estructura de datos de valor, y 2) saber cómo leer y escribir un archivo de tipo INI. Entonces, está el primer problema. Agregar más formatos de archivo complica el problema: YadaYadaFile cambia cuando 1) la estructura de datos cambia, o 2) cambia el formato de un archivo, o 3) se agrega un nuevo formato de archivo.

De manera similar, tener un único validador para estas tres responsabilidades impone demasiada responsabilidad en esa clase única.

Una clase grande es un "olor a código": no es malo en sí mismo, pero a menudo resulta de algo que realmente es malo, en este caso, una clase que intenta ser demasiadas cosas.

2

Voy a abordar este

La clase YadaKungFooFileValidator y la clase YadaKungFooFile parece ser muy fuertemente acoplados por este diseño. Parece un cambio en una clase, podría desencadenar cambios en la otra.

Sí, así que a la luz de eso, descubra cómo hacerlo de la manera más transparente posible. En el mejor de los casos, diseñe la clase YadaKungFooFile de tal forma que el validador pueda retomar eso por sí mismo.

Primero, la sintaxis en sí debería ser bastante fácil. No esperaría que la validación de la sintaxis cambie, por lo que probablemente solo sea seguro codificarla.

En cuanto a las propiedades aceptables, podría exponer los Enumeradores de la clase File que el Validator verificará. Si un valor analizado no está en ninguno de los enumeradores para una sección dada, arroje una excepción.

así sucesivamente y así sucesivamente ...

14

no creo que el tamaño de una clase es una preocupación. La preocupación es más cohesión y acoplamiento. Desea diseñar objetos que estén flojos y cohesivos. Es decir, deberían preocuparse por una cosa bien definida. Entonces, si la cosa resulta ser muy complicada, la clase crecerá.

Puede usar varios patrones de diseño para ayudar a administrar la complejidad. Una cosa que puede hacer, por ejemplo, es crear una interfaz Validator y hacer que la clase YadaKunfuFile dependa de la interfaz en lugar del Validator. De esta forma puede cambiar la clase Validator sin cambios en la clase YadaKungfuFile, siempre y cuando la interfaz no cambie.

+4

"No creo que el tamaño de una clase sea una preocupación". En teoría, estoy tentado de estar de acuerdo. En la práctica, nunca he visto una clase con más de 2000 líneas que me hayan gustado. – PeterAllenWebb

+1

@PeterAllenWebb ... Estoy de acuerdo, pero el número de LOC no debe ser el factor decisivo, más bien la semántica de la clase debería ayudar a decidir si la clase se preocupa por una cosa o más de una cosa. En realidad, la mayoría de las clases serán pequeñas. –

+1

@Vincent Ramdhanie LOC es un buen indicador de olor, mi regla general es que si hay más de 100 LOC en un método es sospechoso hasta que se pruebe Innocent y 500 LOC para las clases –

0

Mi manera personal de manejar las cosas en este caso sería tener una clase YadaKungFooFile que estaría formada por una lista de YadaKungFooFileSections, que sería una lista de YadaKungFooFileValues.

En el caso del validador, haría que cada clase llamara al método de validación de la clase que se encuentra debajo de ella, con la clase más baja en la jerarquía implementando la carne real del método de validación.

0

Si la validación no depende del archivo completo, sino que solo funciona en valores de sección individuales, puede descomponer la validación de la clase YadaKungFooFile especificando un conjunto de validadores que operan en valores de sección.

Si la validación depende del archivo completo, la validación está naturalmente acoplada a la clase de archivo y debería considerar ponerla en una clase.

0

El tamaño de clase no es un problema tanto como el tamaño del método. Las clases son un medio de organización y, aunque existen formas buenas y malas de organizar el código, no están relacionadas directamente con el tamaño de la clase.

El tamaño del método es diferente ya que un método está haciendo el trabajo real. Los métodos deben tener trabajos muy específicos que inevitablemente limitarán su tamaño.

Creo que la mejor manera de determinar el tamaño del método es mediante la escritura de pruebas unitarias. Si está escribiendo pruebas unitarias con suficiente cobertura de código, entonces se vuelve evidente cuando un método es demasiado grande.

Los métodos de prueba de la unidad deben ser simples o de lo contrario terminará necesitando pruebas para probar sus pruebas de la unidad para saber que están funcionando correctamente. Si los métodos de prueba de la unidad se vuelven complicados, es probable que se deba al hecho de que un método está tratando de hacer demasiado y se debe dividir en métodos más pequeños con responsabilidades claras.

4

YadaKungFooFile no debería saber leerse desde el disco. Solo debe saber cómo atravesar, revelar sus hijos, etc. También debe proporcionar formas de agregar/eliminar hijos, etc.

Debería haber una interfaz IYadaKungFooReader que YadaKungFooFile tomaría en su método de carga que usará para cargarse del disco Además de varias implementaciones como AbstractKungFooReader, PlainTextYadaKungFooReader, XMLYadaKungFooWriter que saben leer y los formatos correspondientes.

Lo mismo para escribir.

Finalmente, debe haber YadaKungFooValidatingReader que tomará un lector IYadaKungFooReader, úselo para leer y validar la entrada tal como se lee. Luego pasaría el lector de validación a YadaKungFooFile.Load siempre que lo quiera validar a medida que se lee desde el disco.

Como alternativa, puede activar el lector, en lugar de la clase pasiva. Luego, en lugar de pasarlo a YadaKungFooFile, lo usaría como una fábrica que crearía su YadaKungFooFile usando los últimos métodos de acceso normal. En este caso, su lector también debe implementar la interfaz YadaKungFooFile para que pueda encadenar un lector normal -> lector de validación -> YadaKungFooFile. ¿Tiene sentido?

Cuestiones relacionadas