¿Qué tan grande está bien para una clase?

29

Soy un desarrollador de mucho tiempo (tengo 49 años) pero soy bastante nuevo en el desarrollo orientado a objetos. He estado leyendo sobre OO desde Eiffel de Bertrand Meyer, pero he hecho muy poca programación de OO.

El punto es que cada libro sobre diseño OO comienza con un ejemplo de un bote, automóvil o cualquier objeto común que usemos muy a menudo, y comienzan a agregar atributos y métodos, y explican cómo modelan el estado del objeto y qué se puede hacer con eso.

Por lo tanto, suelen decir algo así como "cuanto mejor sea el modelo, mejor representa el objeto en la aplicación y mejor sale todo".

Hasta ahora todo bien, pero, por otro lado, he encontrado varios autores que dan recetas como "una clase debe caber en una sola página" (agregaría "¿en qué tamaño de monitor?" Ahora que intentamos no para imprimir el código!).

Tomemos, por ejemplo, una PurchaseOrderclase, que tiene una máquina de estados finitos que controla su comportamiento y una colección de PurchaseOrderItem, uno de los argumentos aquí en el trabajo es que deberíamos usar una PurchaseOrderclase simple, con algunos métodos (poco más que una clase de datos), y tener una PurchaseOrderFSM"clase experta" que maneja la máquina de estados finitos para el PurchaseOrder.

Diría que eso cae en la clasificación de "Envidia de características" o "Intimidad inapropiada" de la publicación de los olores de código de Jeff Atwood en Coding Horror. Simplemente lo llamaría sentido común. Si puedo emitir, aprobar o cancelar mi orden de compra real, entonces la PurchaseOrderclase debe tener issuePO, approvePOy cancelPOmétodos.

¿No va eso con los principios antiguos de "maximizar la cohesión" y "minimizar el acoplamiento" que entiendo como pilares de OO?

Además, ¿eso no ayuda a la mantenibilidad de la clase?

Miguel Veloso
fuente
17
No hay razón para codificar el nombre de tipo en el nombre del método. Es decir, si tiene PurchaseOrder, sólo podía nombrar sus métodos issue, approvey cancel. El POsufijo agrega solo valor negativo.
dash-tom-bang
2
Mientras se mantenga alejado de los métodos de agujero negro , debería estar bien. :)
Mark C
1
Con mi resolución de pantalla actual, diría 145 caracteres de gran tamaño.
DavRob60
Gracias a todos por sus comentarios, realmente me ayudaron con este problema. Si este sitio hubiera permitido, también habría elegido la respuesta de TMN y Lázaro, pero solo permite una, así que fue a Craig. Atentamente.
Miguel Veloso

Respuestas:

27

Una clase debe usar el Principio de responsabilidad única. La mayoría de las clases muy grandes que he visto hacen muchas cosas, por eso son demasiado grandes. Mire cada método y el código decida si debe estar en esta clase o por separado, el código duplicado es una pista. Es posible que tenga un método de problema PO, pero ¿contiene 10 líneas de código de acceso a datos, por ejemplo? Ese código probablemente no debería estar allí.

Craig
fuente
1
Craig, no conocía el Principio de Responsabilidad Única, y solo leí sobre él. Lo primero que se me ocurrió fue, ¿cuál es el alcance de una responsabilidad? para un PO, diría que está administrando su estado, y esa es la razón de los métodos problemPO, approvePO y cancelPO, pero también incluye la interacción con la clase PurchaseOrderItem que maneja el estado del artículo PO. Entonces no estoy claro si este enfoque es consistente con el SRP. ¿qué dirías?
Miguel Veloso
1
+1 para una respuesta muy agradable y sucinta. Siendo realistas, no puede haber una medida definitiva de cuán grande debe ser una clase. La aplicación del SRP asegura que la clase sea tan grande como sea necesario .
S.Robins
1
@MiguelVeloso: la aprobación de una orden de compra probablemente tiene una lógica y reglas diferentes asociadas a la emisión de una orden de compra. Si es así, tiene sentido separar las responsabilidades en un POApprover y un POIssuer. La idea es que si las reglas cambian para una, ¿por qué querría meterse con la clase que contiene la otra?
Matthew Flynn
12

En mi opinión, el tamaño de la clase no importa siempre que las variables, funciones y métodos sean relevantes para la clase en cuestión.

Mike42
fuente
1
Por otro lado, el gran tamaño indica que esos métodos probablemente no son relevantes para la clase en cuestión.
Billy ONeal
55
@ Billy: Diría que las clases grandes son un olor a código, pero no son malas automáticamente.
David Thornley,
@David: Es por eso que hay la palabra "probablemente" allí - es importante :)
Billy ONeal
Tendría que estar en desacuerdo ... Trabajo en un proyecto que tiene una convención de nomenclatura bastante consistente y las cosas están bien establecidas ... pero todavía tengo una clase que tiene 50 mil líneas de código. Es demasiado grande :)
Jay
2
Esta respuesta muestra exactamente cómo va el "camino al infierno": si uno no restringe las responsabilidades de una clase, habrá muchas variables, funciones y métodos que serán relevantes para la clase, y esto lleva fácilmente a tener demasiados de ellos.
Doc Brown
7

El mayor problema con las clases grandes es cuando se trata de probar esas clases o su interacción con otras clases. El tamaño de clase grande por sí solo no es un problema, pero como todos los olores, indica un problema potencial . En términos generales, cuando la clase es grande, es una indicación de que la clase está haciendo más de lo que una clase debería hacer.

Para la analogía de su automóvil, si la clase cares demasiado grande, probablemente sea una indicación de que debe dividirse en clase engine, clase doory clase windshield, etc.

Billy ONeal
fuente
8
Y no olvidemos que un automóvil debe implementar la interfaz del vehículo. :-)
Chris
7

No creo que haya una definición específica de una clase que sea demasiado grande. Sin embargo, usaría las siguientes pautas para determinar si debo refactorizar mi clase o redefinir el alcance de la clase:

  1. ¿Hay muchas variables que son independientes entre sí? (Mi tolerancia personal es de alrededor de 10 ~ 20 en la mayoría de los casos)
  2. ¿Hay muchos métodos diseñados para casos de esquina? (Mi tolerancia personal es ... bueno, depende en gran medida de mi estado de ánimo, supongo)

Con respecto a 1, imagine que define el tamaño de su parabrisas, el tamaño de la rueda, el modelo de la bombilla de la luz trasera y otros 100 detalles en su clase car. Aunque todos son "relevantes" para el automóvil, es muy difícil rastrear el comportamiento de dicha clase car. Y cuando algún día su colega cambie accidentalmente (o, en algún caso, intencionalmente) el tamaño del parabrisas según el modelo de la bombilla de la luz trasera, le llevará una eternidad descubrir por qué el parabrisas ya no cabe en su automóvil.

Con respecto al 2, imagine que intenta definir todos los comportamientos que un automóvil puede tener en la clase car, pero car::open_roof()que solo estará disponible para los convertibles, y car::open_rear_door()no se aplica a esos automóviles de 2 puertas. Aunque los convertibles y los autos de 2 puertas son "autos" por definición, implementarlos en la clase carcomplica la clase. La clase se vuelve más difícil de usar y más difícil de mantener.

Además de estas 2 pautas, sugeriría una definición clara del alcance de la clase. Una vez que defina el alcance, implemente la clase estrictamente basada en la definición. La mayoría de las veces, las personas obtienen una clase "demasiado grande" debido a la mala definición del alcance, o debido a las características arbitrarias agregadas a la clase

YYC
fuente
7

Di lo que necesitas en 300 líneas.

Nota: esta regla general supone que está siguiendo el enfoque de 'una clase por archivo', que es en sí mismo una buena idea de mantenimiento

No es una regla absoluta, pero si veo más de 200 líneas de código en una clase, sospecho. Se encienden 300 líneas y campanas de advertencia. Cuando abro el código de otra persona y encuentro 2000 líneas, sé que es tiempo de refactorización sin siquiera leer la primera página.

Principalmente esto se reduce a la mantenibilidad. Si el objeto es tan complejo que no puede expresar su comportamiento en 300 líneas, será muy difícil que alguien más lo entienda.

Me doy cuenta de que estoy doblando esta regla en el Modelo -> ViewModel -> Ver mundo donde estoy creando un 'objeto de comportamiento' para una página de IU porque a menudo se necesitan más de 300 líneas para describir la serie completa de comportamientos en una página. Sin embargo, todavía soy nuevo en este modelo de programación y encuentro buenas formas de refactorizar / reutilizar comportamientos entre páginas / modelos de vista, lo que reduce gradualmente el tamaño de mis modelos de vista.

Jay Beavers
fuente
Mi pauta personal también ronda las 300 líneas. +1
Marcie
Creo que el OP está buscando una heurística, y esta es buena.
neontapir
Gracias, es valioso usar números precisos como guía.
Will Sheppard
6

Vayamos al revés:

¿No va eso con los principios antiguos de "maximizar la cohesión" y "minimizar el acoplamiento" que entiendo como pilares de OO?

En realidad, es una piedra angular de la programación, de la cual OO es solo un paradigma.

Dejaré que Antoine de Saint-Exupéry responda tu pregunta (porque soy vago;)):

La perfección se logra, no cuando no hay nada más que agregar, sino cuando no hay nada más que quitar.

Cuanto más simple sea la clase, más seguro estará de que es correcto, más fácil será probarlo por completo .

Matthieu M.
fuente
3

Estoy con el OP en la clasificación Feature Envy. Nada me irrita más que tener un montón de clases con un montón de métodos que funcionan en las partes internas de otras clases. OO sugiere que modele datos y las operaciones en esos datos. ¡Eso no significa que coloque las operaciones en un lugar diferente de los datos! Está tratando de llegar a poner los datos y los métodos juntos .

Con los modelos cary persontan frecuentes, sería como poner el código para hacer la conexión eléctrica, o incluso hacer girar el motor de arranque, en la personclase. Espero que esto obviamente sea incorrecto para cualquier programador experimentado.

Realmente no tengo un tamaño ideal de clase o método, pero prefiero mantener mis métodos y funciones ajustados en una pantalla para poder verlos todos en un solo lugar. (Tengo Vim configurado para abrir una ventana de 60 líneas, que está justo alrededor del número de líneas en una página impresa). Hay lugares donde esto no tiene mucho sentido, pero funciona para mí. Me gusta seguir la misma directriz para las definiciones de clase, y tratar de mantener mis archivos en unas pocas "páginas" de largo. Cualquier cosa por encima de 300, 400 líneas comienza a sentirse difícil de manejar (principalmente porque la clase ha comenzado a asumir demasiadas responsabilidades directas).

dash-tom-bang
fuente
+1: buenas reglas generales, pero no ser autoritario al respecto. Sin embargo, diría que el hecho de que una clase esté dividida no significa que las diferentes clases deberían estar tocando a los miembros privados de los demás.
Billy ONeal
No creo que diferentes clases deban tocar las partes privadas de cada uno. Si bien el acceso 'amigo' es conveniente para poner en marcha algo (para mí) es un trampolín para un mejor diseño. En general, también evito los accesores, ya que depender de los elementos internos de otra clase es otro olor a código .
dash-tom-bang
1

Cuando una definición de clase tiene un par de cientos de métodos, es demasiado grande.

C Johnson
fuente
1

Estoy totalmente de acuerdo con el uso del principio de responsabilidad única para las clases, pero si se sigue y da como resultado clases grandes, que así sea. El enfoque real debe ser que los métodos y las propiedades individuales no sean demasiado grandes, la complejidad ciclomática debe ser la guía allí y el seguimiento que puede dar lugar a muchos métodos privados que aumentarán el tamaño general de la clase pero lo dejarán legible y comprobable a un nivel granular. Si el código sigue siendo legible, el tamaño es irrelevante.

Lázaro
fuente
1

Emitir una orden de compra es a menudo un proceso complicado que involucra más que solo la orden de compra. En este caso, mantener la lógica de negocios en una clase separada y simplificar la orden de compra es probablemente lo correcto. Sin embargo, en general, tienes razón: no quieres clases de "estructura de datos". Por ejemplo, el método de su clase de negocios "POManager" issue()probablemente debería llamar al issue()método del PO . El PO puede establecer su estado en "Emitido" y anotar la fecha de emisión, mientras que el POManager puede enviar una notificación a las cuentas por pagar para informarles que esperan una factura y otra notificación al inventario para que sepan que hay algo entrando y La fecha esperada.

Cualquiera que empuje las "reglas" para el tamaño del método / clase obviamente no ha pasado suficiente tiempo en el mundo real. Como otros han mencionado, a menudo es un indicador de refactorización, pero a veces (especialmente en el trabajo de tipo "procesamiento de datos") realmente necesita escribir una clase con un método único que abarque más de 500 líneas. No te obsesiones con eso.

TMN
fuente
Supongo que POManager sería el "controlador" y PurchaseOrder sería el "modelo" en el patrón MVC, ¿verdad?
Miguel Veloso
0

He encontrado varios autores que dan recetas como "una clase debe caber en una sola página"

En términos del tamaño físico de una clase, ver una clase grande es una indicación de un olor a código, pero no necesariamente significa que siempre haya olores. (Por lo general, lo son) Como lo dirían los piratas de Piratas del Caribe, esto es más lo que llamarías "pautas" que reglas reales. Intenté seguir estrictamente el "no más de cien LOC en una clase" una vez y el resultado fue MUCHA ingeniería innecesaria.

Por lo tanto, suelen decir algo así como "cuanto mejor sea el modelo, mejor representa el objeto en la aplicación y mejor sale todo".

Normalmente comienzo mis diseños con esto. ¿Qué hace esta clase en el mundo real? ¿Cómo debería mi clase reflejar este objeto como se indica en sus requisitos? Agregamos un poco de estado aquí, un campo allí, un método para trabajar en esos campos, agregamos algunos más y ¡listo! Tenemos una clase trabajadora. Ahora complete las firmas con las implementaciones adecuadas y estamos listos, o eso cree. Ahora tenemos una gran clase con toda la funcionalidad que necesitamos. Pero el problema con esto es que no toma en cuenta la forma en que funciona el mundo real. Y con eso quiero decir que no tiene en cuenta, "CAMBIO".

La razón por la que las clases se dividen preferiblemente en partes más pequeñas es que es difícil cambiar las clases grandes más adelante sin afectar la lógica existente o introducir un nuevo error o dos sin querer o simplemente hacer que todo sea difícil de reutilizar. Aquí es donde entran en juego la refactorización, los patrones de diseño, SOLID, etc. y el resultado final suele ser una clase pequeña que trabaja en otras subclases más pequeñas y más granulares.

Además, en su ejemplo, agregar IssuePO, ApprovePO y Cancel PO a la clase PurchaseOrder parece ilógico. Las órdenes de compra no se emiten, aprueban ni cancelan. Si PO debe tener métodos, entonces los métodos deberían funcionar en su estado, no en la clase en su conjunto. Realmente son solo clases de datos / registros en las que trabajan otras clases.

Jonn
fuente
0

Lo suficientemente grande como para contener toda la lógica necesaria para realizar su trabajo.

Suficientemente pequeño para ser mantenible y seguir el Principio de responsabilidad única .

RMalke
fuente