Resistiendo el hedor de las clases grandes

Entre las novedades que trajo la versión 2.0 de .NET, allá por el año 2005, estaban las clases y los métodos parciales.

En aquella época Microsoft todavía estaba muy enfocado al wizard driven development y las clases y métodos parciales surgieron, fundamentalmente, para facilitar la integración de código generado por herramientas con código escrito por usuarios. Jamás en mi vida he usado un método parcial, tal vez porque nunca llegué a utilizar los infames DataSets tipados de la época, pero las clases parciales estaban a la orden del día si desarrollabas con Windows Forms, por lo que estaba más que acostumbrado a tratar con ellas.

En el fondo, ese uso de las clases parciales era equivalente al uso que se le había dado antes a las regiones: esconder la basura. Habíamos pasado de tener una región con el texto Windows Forms Designer generated code, a llevarnos ese código a un fichero MyForm.Designer.cs, pero no había mucho más cambio.

Lo único que permiten las clases parciales es separar la implementación de una clase en varios ficheros. En general, para que esto tenga sentido, o bien estamos mezclando código autogenerado con código nuestro, cosa que ya de por si es discutible, o bien tenemos una clase demasiado grande, cosa que directamente huele mal.

Si os cuento esto no es por repetiros cosas que seguro que ya sabéis, sino por, como de costumbre, hacer de abogado del diablo y explicar por qué (a veces) me gusta usar clases parciales pese a cómo huelan.

Lo malo es mucho, pero eso ya lo sabéis

No me voy a extender mucho en este punto porque ya lo he mencionado antes y no creo que pueda aportar mucho nuevo.

Si te ves en la necesidad de separar una clase que has escrito tú en varios ficheros, seguramente estés violando unos cuantos principios SOLID, empezando por el de principio de responsabilidad única. Si la clase tuviese menos responsabilidades, sería más pequeña y no tendrías que andar partiéndola en ficheros.

La forma kosher de tratar con una clase grande es partirla en varias clases más pequeñas, no en más ficheros. Esto tiene mucho sentido y presenta claras ventajas, como poder reutilizar potencialmente esas clases más pequeñas en otros contextos.

Qué me aporta una clase grande

Voy a asimilar el usar una clase parcial con tener una clase grande porque, sinceramente, si tengo una clase con 3 métodos, no le veo mucho sentido a partirla en varios ficheros.

Si quiero tener una clase grande es porque considero fundamental garantizar que se cumplen los invariantes de esa clase.

Para mi existe una tensión entre tener flexibilidad, con clases más pequeñas que puedan ser utilizadas (y reutilizadas) de distintas formas y en distintos contextos, y tener un sistema rígido en el que sólo sea posible hacer aquello que es correcto hacer y de la forma en que es correcta hacerlo.

No todas las partes de una misma aplicación son iguales y, en general, tiene sentido que la mayoría de la aplicación esté compuesta por partes pequeñas, manejables y flexibles, pero hay otras en las que garantizar que no se pueden usar de forma incorrecta es más importante.

En el típico sistema de facturación, la parte que es encarga de gestionar productos tal vez no sea tan crítica, pero la que calcula el importe final de una factura en base a sus impuestos sí lo es, y puede que merezcla la pena encapsular toda esa lógica en la factura, aunque pueda llevar a incrementar el tamaño de esa clase y nos acerque a otro antipatrón como es el god object.

Puedes extraer esa lógica a clases de apoyo, pero eso suele implicar exponer hacia el exterior más estado interno de la clase cuyos invariantes quieres mantener. Eso plantea dos problemas. Por una parte, te arriesgas a que alguien toque ese estado interno violando los invariantes sin utilizar la clase de apoyo que con tanto amor has diseñado, y por otra, alguien podría duplicar el código de la clase de apoyo realizando la misma tarea de forma ligeramente diferente, cosa que no es muy deseable. Podrías acabar con dos APIs para hacer lo mismo y sería complicado saber cuál es la correcta.

Al final se reduce un tema de visibilidad que, como bien dice Modesto, está sobrevalorada, pero que no deja de ser útil para determinadas cosas. Se trata de aprovechar las herramientas que te ofrece cada lenguaje, y si un lenguaje te ofrece mecanismos para controlarla, no está de más aprovecharlos.

Lo que acabo haciendo en muchas ocasiones es implementar esas clases de apoyo que encapsulan parte de la lógica de la clase “grande”, pero implementarlas como clases privadas dentro de la clase grande. De esa forma, puedo controlar que accedan a información privada de la clase “principal” sin necesidad de exponer esa información a todo el mundo.

La clase “principal” se convierte en una especie de fachada que almacena estado sobre el que trabajan un conjunto de clases internas más pequeñas. Esta clase expone un API hacia el exterior que, básicamente, se limita a redirigir las operaciones a sus clases internas.

No tiene mucho sentido poner un ejemplo completo porque hablamos de escenarios complejos, pero para que os hagáis una idea estos ficheros están sacado de un proyecto real:

// Clase "principal" que representa una tarea cuyo 
// estado se computa a partir de eventos externos 
// que se van recibiendo. Dependiendo de varios 
// factores (estado de la tarea, eventos recibidos, 
// hora, etc.) los eventos se aceptan o no, provocan 
// unos cambios u otros, etc.
Task.cs

// Clase interna que construye eventos a partir
// de información externa
Task.EventBuilder.cs

// Clase interna que construye la política de 
// aceptación de eventos en base a la información 
// encapsulada actualmente en la tarea
Task.EventPolicyFactory.cs


// Clases internas (hay unas cuantas) para cada 
// política de aceptación concreta de eventos.
// Forman un patrón Strategy de libro ;-)
Task.OpenTaskPolicy.cs
Task.AssignedTaskPolicy.cs
Task.AcceptedTaskPolicy.cs
//....

Ésta era la miga real de un sistema que controlaba una parte importante del funcionamiento de un aeropuerto.

Era poco código con respecto al total (al final siempre pesa más el interfaz de usuario, acceso a datos, comunicaciones, etc.), pero que el sistema funcionara o no, dependía de que los cálculos que se realizaban aquí dentro estuvieran bien. Para nosotros era crítico que se respetasen correctamente los invariantes del objeto Task porque errores en esta parte tenían repercusiones económicas importantes, así que blindar esta parte del sistema tenía sentido en ese contexto determinado.

Supongo que queda claro, pero cuando hablo aquí de proteger invariantes, me refiero sobre todo a protegerlos de nosotros mismos. No se trata de protegerlo de “gente idiota que no sabe programar”, o al menos no de otros idiotas que no seamos nosotros. Por muy bueno que te creas y muy cuidadoso que seas, llega un momento en que te equivocas, y cuanto más te ayude tu código a defenderte de tu yo del futuro, mejor.

Si entrecierras un poco los ojos y lo miras con cariño, es equivalente a crear un closure sobre unas variables (el estado de la clase principal) que son accedidas desde otras funciones (las clases internas). O dicho de otra forma, el patrón revealing module tan frecuente en Javascript.

Se puede conseguir algo parecido jugando con la visibilidad a nivel de assembly y los internal, pero eso obliga a tener que partir el código en assemblies, lo que implica mayor complejidad de despliegue y, sobre todo, mayor lentitud en desarrollo por la forma en que Visual Studio gestiona los proyectos. Hace mucho que no utilizo assemblies para controlar visibilidad y preferiría no tener que volver a pasar por ello.

Esta técnica de tener clases grandes encapsulando de forma opaca mucho comportamiento presenta un problema indudable para los que queremos testearlas, porque nos obligan a escribir tests más complejos.

Llegado el caso, lo primero que me planteo es si necesito testear las partes por separado. A veces realmente prefiero invertir el tiempo en los tests más complicados porque son los que me aportan más valor, pero hay otras que quiero testear una parte concreta de un algoritmo. En esos escenarios, intento llevarme el comportamiento a testear a una función pura y testearlo independientemente del estado de las clases afectadas.

Si no es posible y realmente creo que el test merece la pena, acabo haciendo pública la clase interna. Tampoco sufro demasiado por ello, porque por la forma en que trabajamos “sabemos” que una clase así es raro que sea pública y sospechamos si vemos un uso suyo fuera de un proyecto de test.

Otros usos de clases parciales

A título anecdótico, existió otro uso interesante para las clases parciales: crear fachadas estáticas extensibles.

Si tenías una fachada estática, por ejemplo para generar constraints en los tests, del tipo de Is.EqualTo, Is.SameAs, etc., podías hacer que la clase stática Is de la que cuelgan todos los métodos de factoría para crear constraints fuese parcial, y así otros proyectos podían colgar su propias constraints de ella y mantener un único punto de entrada facilitando el uso para los clientes.

Cuando aparecieron los métodos extensores esta técnica dejó de tener mucho sentido (había formas mejores de conseguir cosas similares) y cayó en desuso, pero hubo una época (alrededor de 2006-2008) en la que se hacían cosas curiosas con ella.

Conclusión

Utilizar clases lo bastante grandes como para que merezca la pena separarlas en varios ficheros es sospechoso. Huele mal. Pero que algo sea sospechoso no quiere decir que necesariamente sea culpable.

A veces compensa encapsular más información y más lógica en una única clase para facilitar el mantenimiento de invariantes que sean importantes en la aplicación.

En ese caso, podemos utilizar clases privadas internas a la clase principal para encapsular parte de ese comportamiento, haciendo que sea más legible y que sea más fácil razonar sobre él.

Cuando llegamos a ese punto, separar cada clase en un fichero independiente haciendo uso de clases parciales es una forma razonable de mantener el código organizado.


3 comentarios en “Resistiendo el hedor de las clases grandes

  1. Una idea cojonuda. Personalmente siempre me había parecido bordear el “code smell” cuando se crean clases privadas dentro de otra clase. En mi caso me gusta que las clases estén en un fichero propio y que pueda identificar qué clases tengo con un vistazo en el explorador.

    Este uso ocurrente de “partial” me ha encantado, también la nomenclatura utilizada para las clases internas.

  2. Miriam García dijo:

    Realemente muy bueno. Me siento reflejada en muchos aspectos.

    Ahora mismo, me ha llegado el mantenimiento de un Legacy WebForms, con una clase CodeBehind enorme (aspx.cs, realmente ascx.cs) para mí. 3000 líneas de código y un montón de métodos. En la parte de interfaz suele pasar.

    Buena idea pensar en clases partial y clases privadas, y mantener los invariantes de la página o ascx.
    También pienso en Refactoring Large Class, y no sé cómo usar algún patrón-técnica-loquesea en WebForms legacy (no MVVM) para casos así: Condiciones -> Acciones

    if (TipoAccionPagina == TipoAccion.Modificacion || TipoAccionPagina == TipoAccion.MisDatosM)
    {
    	if (user.Habilitado && esEmpleado)
    	{
    		// 1a) Si (empleado) NO existe en AD ==> Error
    		if (!existeEnAD)
    		{
    			MostrarNotificacion(msg, TipoComunicacion.Error, true);
    			return;
    		}
    
    		// 1b) Si empleado) existe en AD ==> Habilitar en CRM (si false, Solicitud Alta CRM)
    		// Habilitar en CRM (si false, Solicitud Alta CRM)
    	}
    
    	if (user.Habilitado && !esEmpleado)
    	{
    		// 2a) Si NO empleado NO existe en AD ==> Alta en AD y Habilitar en CRM (si false, Solicitud Alta CRM)
    		if (!existeEnAD)
    		{
    			//  Alta en AD
    
    			// Habilitar en CRM (si false, Solicitud Alta CRM)
    		}
    
    		// 2b) Si NO empleado existe en AD ==> Warning (Correo) y Habilitar en CRM (si false, Solicitud Alta CRM)
    		if (existeEnAD)
    		{
    			// EnviarCorreo Warning
    			// Habilitar en CRM (si false, Solicitud Alta CRM)
    		}
    		
    	}
    
    
    	// DES-Habilitar
    	// 1a) Si (empleado) NO existe en AD ==> Error
    	// 1b) Si (empleado) existe en AD ==> DesHabilitar en CRM (Baja en CRM) - Eliminar de Licencias CRM 
    	if (!user.Habilitado && esEmpleado)
    	{
    		if (!existeEnAD)
    		{
    		   MostrarNotificacion(msg, TipoComunicacion.Error, true);
    			return;
    		}
    
    		// DesHabilitar en CRM (Baja en CRM) 
    		// Eliminar de Licencias CRM 
    
    	}
    
    	if (!user.Habilitado && !esEmpleado)
    	{
    		// 2a) Si NO empleado NO existe en AD ==> Warning (Correo) y DesHabilitar en CRM (Baja en CRM) - Eliminar de Licencias CRM 
    		if (!existeEnAD)
    		{
    			 // Warning (Correo)
    			// DesHabilitar en CRM (Baja en CRM) 
    			// Eliminar de Licencias CRM 
    		}
    
    		// 2b) Si NO empleado existe en AD ==> Eliminar de AD y DesHabilitar en CRM (Baja en CRM) - Eliminar de Licencias CRM 
    		if (!existeEnAD)
    		{
    			// Eliminar de AD
    			// DesHabilitar en CRM (Baja en CRM) 
    			// Eliminar de Licencias CRM 
    
    		}
    	}
    }
    
  3. Miriam, una idea loca que se me ocurre, aunque igual muy costosa de implementar.
    Pienso en State Machines, máquinas de estados, pero de alguna forma incluir un control de errores en las mismas.

    3 estados para las acciones (o comandos):
    A // Eliminar de AD
    B // DesHabilitar en CRM (Baja en CRM)
    C // Eliminar de Licencias CRM

    En flujo sin errores: A => B => C.

    Supongamos que hay un error, el flujo se interrumpe.

    Habría que recopilar los mensajes de error, notificar a UI, y en todo caso, ver si es posible transaccionalidad. Veo dificil pues llamas a Directorio Activo, WebServices, Envío de correo y acceso a datos.

    Juan comentaba algo de estos temas en este gran post
    http://blog.koalite.com/2012/08/patron-disyuntor-circuit-breaker/
    más creo no aplica a tu caso.

    Saludos.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *

*

Puedes usar las siguientes etiquetas y atributos HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>