[ISP] Principio de segregación de interfaces

Descripción general

Principios del diseño orientado a objetos; Principios SOLID; [ISP] Principio de segregación de interfaces
Básicamente lo que nos quiere decir este principio es que las clases que implementen una interfaz o una clase abstracta no deberían estar obligadas a utilizar partes que no van a utilizar.

[TOC] Tabla de Contenidos


↑↑↑

[ISP] Principio de segregación de interfaces


↑↑↑

Los cinco principios SOLID


↑↑↑

ISP – Principio de Segregación de Interfaces

Principio de Segregación de Interfaces (Interface Segregation Principle, ISP), trata sobre las desventajas de las interfaces "pesadas", y guarda una estrecha relación con el nivel de cohesión de las aplicaciones.

Básicamente lo que nos quiere decir este principio es que "las clases que implementen una interfaz o una clase abstracta no deberían estar obligadas a utilizar partes que no van a utilizar".

Otras definiciones de este principio son:

La forma de solucionar el problema consiste en segregar las operaciones en pequeñas interfaces. Una interfaz es un contrato que debe cumplir una clase, y tales contratos deben ser específicos, no genéricos; esto nos proporcionará una forma más ágil de definir una única responsabilidad por interfaz - de otra forma, violaríamos además el Principio de Responsabilidad Única (Single Responsibility Principle, SRP).

Sin embargocuando hablamos de interfaces específicos para cada cliente significa que cada clase que use a otra clase debe hacerlo a través de un interface específico, si no que deberíamos clasificar y organizar los clientes por tipos, creando interfaces para cada uno de estos tipos. En esta aproximación por tipos de clientes e interfaces debemos tener en cuenta que si varios tipos de cliente necesitan el mismo método, este método debería añadirse a los interfaces correspondientes en vez de unir los diferentes tipos en uno solo.


↑↑↑

Ejemplo

Veamos un ejemplo que esté violando este principio (aclaro que el ejemplo no tiene demasiado sentido, pero es útil para comprender el tema).

Supongamos que tenemos la clase abstracta Proceso: (refiriéndonos con proceso a un tipo de tarea a llevar a cabo). En este definimos cuatro métodos, el primero y el último para iniciar y terminar el proceso, y el segundo y tercero para suspenderlo y reiniciarlo (estos dos últimos suponiendo que quien lo lleva a cabo la tarea es una persona y que, evidentemente, debe parar cada cierto tiempo a descansar).

Supongamos que tenemos dos tipos de procesos, unos van a ser manuales (ejecutado por personas) y otros automatizados (ejecutados por máquinas):

Una primera aproximación al problema sería la siguiente


↑↑↑

Solución errónea

Listado 1

public abstract class Proceso { public abstract void Iniciar(); public abstract void Suspender(); public abstract void Reanudar(); public abstract void Finalizar(); } public class ProcesoManual : Proceso { public override void Iniciar() { //... } public override void Suspender() { //... } public override void Reanudar() { //... } public override void Finalizar() { //... } } public class ProcesoAutomaizado : Proceso { public override void Iniciar() { //... } public override void Suspender() { throw new NotImplementedException(); } public override void Reanudar() { throw new NotImplementedException(); } public override void Finalizar() { //... } }

En la clase ProcesoManual se implementan todos los métodos, pero en la clase ProcesoAutomatizado no se implementan los métodos suspender y reiniciar, porque las máquinas no necesitan tomarse un descanso y por lo tanto no es necesario ni suspender el proceso ni volver a reiniciarlo.

Además cualquier llamada es uno de esos métodos generaría una excepción del tipo NotImplementedException. Podríamos quitar esa excepción... por ejemplo dejando el método vacío; pero entonces el programador que utilice la clase se encontrará con un método que sencillamente no hace nada. Podríamos intentar solucionarlo, mediante documentación, indicando que el método no es funcional, mediante comentarios en el código (pero es posible que un programador que utilice la clase no tenga acceso al código), etc. En cualquier caso, el problema seguiría existiendo, y solo estaríamos ocultándolo.

Como podemos ver en este caso estamos violando el principio ISP, ya que estamos obligando a una clase a implementar métodos que no va a utilizar.


↑↑↑

Primera solución buena

Una solución a este problema es dividir un poco las cosas y crear, por ejemplo, una interfaz que tenga definida las operaciones propias de los procesos manuales, Y en segundo lugar modificar la clase Proceso que ahora tiene únicamente, los métodos que son comunes a todos los subtipos de procesos. De esta forma evitaremos que los procesos automatizados implementen métodos que no les son útiles:

Listado 2

// Interfaz que deben cumplir los trabajos que // se realicen de forma manual public interface IManual { void Suspender(); void Reiniciar(); } // Clase Base que contiene la plantilla, los metodos // que deben implementar todas las clases // que representen a un proceso public abstract class Proceso { public abstract void Iniciar(); public abstract void Finalizar(); } // Clase que hereda de la clase base [Proceso] // Observa el modificador [override] // y que implementa la interfaz [IManual] public class ProcesoManual : Proceso, IManual { public override void Iniciar() { Console.WriteLine("Proceso Manual 1 -> Iniciar"); } public void Suspender() { Console.WriteLine("Proceso Manual 1 -> Suspender"); } public void Reiniciar() { Console.WriteLine("Proceso Manual 1 -> Reiniciar"); } public override void Finalizar() { Console.WriteLine("Proceso Manual 1 -> Finalizar"); } } // Clase que hereda de la clase base [Proceso] // Observa el modificador [override] public class ProcesoAutomaizado : Proceso { public override void Iniciar() { Console.WriteLine("Proceso Automatizado 1 -> Iniciar"); } public override void Finalizar() { Console.WriteLine("Proceso Automatizado 1 -> Finalizar"); } }

↑↑↑

Segunda solución mejor que la primera

El proceso puede mejorarse un poco mas usando interfaces. En el código que se muestra a continuación, se han diseñado dos interfaces, una para los trabajos manuales y otra para los trabajos automáticos. Cada una de ellas contiene los procesos que necesitan.

A continuación las clases implementan las interfaces que necesitan para definir sus comportamientos

Listado 3

//---------------------------------- // Interfaz que deben cumplir los trabajos que // se realicen de forma manual (por personas) public interface IManual { void Suspender(); void Reiniciar(); } //---------------------------------- // Interfaz que debe cumplir CUALQUIER TIPO de trabajo public interface IProceso { void Iniciar(); void Finalizar(); } //---------------------------------- // Clase que representa a un proceso realizado por personas // Implementa la interfaz [IProceso] correspondiente a cualquier proceso // e implementa la interfaz [IManual] procesos realizados por personas public class ProcesoManual : IProceso, IManual { public void Iniciar() { Console.WriteLine("Proceso Manual 2 -> Iniciar"); } public void Suspender() { Console.WriteLine("Proceso Manual 2 -> Suspender"); } public void Reiniciar() { Console.WriteLine("Proceso Manual 2 -> Reiniciar"); } public void Finalizar() { Console.WriteLine("Proceso Manual 2 -> Finalizar"); } } //---------------------------------- // Clase que representa a un proceso realizado por maquinas // Implementa la interfaz [IProceso] correspondiente a cualquier proceso public class ProcesoAutomatizado : IProceso { public void Iniciar() { Console.WriteLine("Proceso Automatizado 2 -> Iniciar"); } public void Finalizar() { Console.WriteLine("Proceso Automatizado 2 -> Finalizar"); } } //---------------------------------- // Una clase para manejar procesos public class GerenteProcesos { // Campo de la clase que guarda la referencia a la interfaz [IProceso] // Este valor se tiene que proporcionar a través del constructor // La palabra clave readonly corresponde a un modificador que // se puede utilizar en campos. // Cuando una declaración de campo incluye un modificador readonly, // las asignaciones a los campos que aparecen en la declaración // sólo pueden tener lugar en la propia declaración o // en un constructor de la misma clase. private readonly IProceso unProceso; // Constructor public GerenteProcesos(IProceso w) { unProceso = w; } // propiedad para leer el proceso que está almacenado en la clase public IProceso Proceso { get { return unProceso; } } // Poner en ejecución el proceso public void Gestionar() { unProceso.Iniciar(); //unProceso.Suspender(); // provoca un error //unProceso.Reiniciar(); // provoca un error unProceso.Finalizar(); } } //---------------------------------- // Clase para probar las clases anteriores public class Test { public void TestGerente() { ProcesoManual PM1 = new ProcesoManual(); ProcesoAutomatizado PA1 = new ProcesoAutomatizado(); GerenteProcesos GP1 = new GerenteProcesos(PM1); GP1.Gestionar(); GerenteProcesos GP2 = new GerenteProcesos(PA1); GP2.Gestionar(); } }
Figura 1

Figura 1 - Resultado del Test

Figura 2

Figura 2


↑↑↑

El ejemplo de Xerox

La historia de fondo no sé si es cierta o no, pero prescindiendo de ese "pequeño detalle", sirve como ejemplo de aplicación del principio ISP.

Cuenta la historia que... el Principio de Segregación de Interfaces fue utilizado por primera vez por Robert C. Martin durante unas sesiones de consultoría en Xerox. Por aquella época, Xerox estaba diseñando una impresora multifuncional. El software diseñado para la impresora funcionaba y se adaptaba perfectamente a las necesidades iniciales de la impresora; sin embargo, conforme fue evolucionando, y por lo tanto cambiando, se hizo cada vez más difícil de mantener. Cualquier modificación tenía un gran impacto global sobre el sistema. La utilización de ISP permitió reducir los riesgos de las modificaciones y otorgó una mayor facilidad al mantenimiento. El ISP declara que:

"Los clientes no deben ser forzosamente dependientes de las interfaces que no utilizan."


↑↑↑

Interfaces "pesadas"

Observemos el diagrama de clases de la figura siguiente. Básicamente, consta de dos modelos de impresoras representadas por las clases Modelo1998 y Modelo2000, ambas herederas de la clase abstracta ImpresoraMultifuncional.

Figura 3

Figura 3

Inicialmente, la clase abstracta ImpresoraMultifuncional declara los métodos correspondientes a las funciones típicas que realiza una impresora multifuncional, como son la impresión, el escaneado, el envío de fax y la cancelación de cualquier operación.

La impresora Modelo1998 fue el primer modelo en basarse en esta interfaz; poco después se añadió un nuevo modelo, Modelo2000, que además de las funciones anteriores añadía la posibilidad de hacer fotocopias.

Posteriormente, surgió un nuevo modelo (Modelo2002) que se basaba en la misma clase abstracta ImpresoraMultifuncional e incorporaba el soporte para comunicaciones TCP/IP en lugar del servicio de fax; este modelo permitía enviar un documento directamente por correo electrónico, evitando así los altos costes de telefonía. El problema se presenta al implementar en Modelo2002 el método heredado EnviarFax, ya que dicho modelo no dispone de dicha funcionalidad. En el listado siguiente podemos ver un ejemplo de ese código

Listado 4

class Modelo2002Inicial : ImpresoraMultifuncional { public override void Imprimir() { Impresion.EnviarImpresion(); } public override void Escanear() { Escaner.DigitalizarAFormatoPng(); } public override void Cancelar() { Impresion.CancelarImpresion(); } public override void EnviarFax() { throw new System.NotImplementedException(); } public void EnviarEMail() { // Enviamos por correo electrónico } }

El método EnviarFax no se implementa, y por consiguiente una llamada al método generaría una excepción del tipo NotImplementedException. Sí, es cierto que podríamos quitar dicha excepción y podríamos dejar el método vacío; pero entonces el programador que utilice la clase se encontrará con un método que sencillamente no hace nada. Esto podríamos intentar solucionarlo de varias formas: mediante documentación, indicando que el método no es funcional; mediante comentarios en el código (pero es posible que un programador que utilice la clase no tenga acceso al código), etc. En cualquier caso, el problema seguiría existiendo, y solo estaríamos ocultándolo.


↑↑↑

De "pesada" a confusa

Es importante que nos concienciemos de este problema. En este ejemplo, se trata de un único método, y eludir el problema puede ser bastante obvio; pero si la clase abstracta implementara una docena de métodos y únicamente utilizáramos tres o cuatro de ellos en un contexto no tan claro como el de las impresoras multifuncionales, el problema se haría más complejo.

Un ejemplo de esto lo tenemos en el propio .NET Framework. La clase abstracta System.Web.Security.MembershipProvider contiene todos los métodos necesarios para la autenticación ASP.NET: valida credenciales accediendo a algún mecanismo de almacenamiento (SQL Server, sistema de archivos, etc.), bloquea usuarios, gestiona contraseñas, etc. Si implementamos nuestro propio proveedor de autenticación e implementamos la clase MembershipProvider, seguramente solo utilizaremos algunos de los métodos heredados (es decir, implementaremos únicamente ciertas funcionalidades disponibles en la clase base), y en este caso no es tan evidente cuáles de esos métodos deberemos redefinir. ¿Cómo sabría el programador que utilice nuestra clase qué métodos implementa? ¿Qué comportamiento debería tener nuestra clase ante la llamada a un método no implementado? En definitiva, ¿qué valor real tienen los métodos que están disponibles pero no implementados? La respuesta es: ninguno, aparte de crear confusión.

En nuestro ejemplo, la clase Modelo2000 implementa, (al contrario que Modelo1998), la característica de Fotocopiar. Dicho método se implementa en la propia clase Modelo2000, y quizás nos hayamos preguntado por qué no hemos añadido el método a la clase abstracta ImpresoraMultifuncional. El motivo puede ser bien dispar. Quizás quién diseñó el sistema decidió no tocar la clase abstracta y extender la clase Modelo2000; sin embargo, resulta que a partir de Modelo2000 todas las impresoras tienen soporte de fotocopia y por lo tanto todos los modelos deberán implementar el método Fotocopiar. Utilizando esta estrategia, tenemos que vulnerar el principio DRY (Don't Repeat Yourself); y lo que es más preocupante, otro programador puede tener la ocurrencia o la necesidad de modificar el contrato o el nombre del método. En definitiva, ello complica el mantenimiento del sistema; cualquier modificación sobre del método Fotocopiar implicará buscarlo por todo el código, aumentando por tanto el riesgo de error. Es contradictorio que tengamos encapsulados en una clase abstracta miembros que no usamos, por ejemplo, en Modelo2002, mientras que otros que sí serían firmes candidatos a serlos, como el caso del método Fotocopiar, no lo son.


↑↑↑

Segregación de interfaces

Una interfaz es un contrato que debe cumplir una clase, y tales contratos deben ser específicos, no genéricos; esto nos proporcionará una forma más ágil de definir una única responsabilidad por interfaz - de otra forma, violaríamos además el Principio de Responsabilidad Única (Single Responsibility Principle, SRP).

Retomando de nuevo el ejemplo práctico, volvamos a replantear el sistema y separemos las responsabilidades por interfaces. En el siguiente listado podemos ver el resultado de dicha segregación.

Listado 5

public interface IImprimible { void Imprimir(); } public interface IFotocopiable { void Fotocopiar(); } public interface IEscaneable { void Escanear(); } public interface IFaxCompatible { void EnviarFax(); void RecibirFax(); } public interface ITcpIpCompatible { void EnviarEMail(); } class Modelo1998 : IImprimible, IEscaneable, IFaxCompatible { // ... } class Modelo2000 : IImprimible, IEscaneable, IFaxCompatible, IFotocopiable { // ... } class Modelo2002 : IImprimible, IEscaneable, IFotocopiable, ITcpIpCompatible { // ... }

↑↑↑

Conclusión

Mediante la segregación de interfaces, el planteamiento del diseño otorga una mayor cohesión al sistema, lo que se traduce, por una parte, en un menor coste de mantenimiento, y por otra, en un menor riesgo de errores y una mejor localización de los mismos.


↑↑↑

Referencia Bibliográfica


↑↑↑

A.2.Enlaces

[Para saber mas]
[Grupo de documentos]
[Documento Index]
[Apuntes sobre principios de diseño]
[Documento Start]
[Imprimir el Documento]