Diseñar clases para ser heredadas…

Una de las ventajas de la programación orientada a objetos, es la herencia de clases y el polimorfismo: eso es la capacidad para crear clases derivadas a partir de otras clases y poder usar las clases derivadas en cualquier lugar donde se espere la clase base.

El comportamiento por defecto de C# (y VB.NET) es que cuando creamos una clase, esa se puede exteder, es decir puede crearse una clase derivada. Debemos declarar explicitamente la clase como sellada (sealed) para impedir que alguien cree una clase derivada a partir de la nuestra. Es una buena práctica declarar tantas clases sealed como sea posible (al menos las clases públicas, para las internas no son necesarias tantas precauciones ya que no serán visibles desde fuera de nuestro assembly). Si dejamos una clase sin sellar, debemos ser conscientes de que estamos dando la posibilidad a alguien de que derive de nuestra clase. Eso, obviamente, no tiene nada de malo… pero entonces debemos asegurarnos de que nuestra clase está preparada para que se derive de ella.

Métodos virtuales

Los métodos virtuales definen los puntos de extensión de nuestra clase: es decir la clase derivada sólo puede redefinir (override) los métodos que nosotros hayamos marcado como virtuales en nuestra clase. Ese es uno de los aspectos que más me gustan de C# respecto a Java: en Java no hay el concepto de método virtual (o dicho de otro modo, todos lo son). En C# (y en VB.NET) al tener que marcar explícitamente los métodos que vamos a permitir que las clases derivadas redefinan, nos obliga a tener que pensar en cómo puede extenderse nuestra clase. Si nuestra clase no tiene ningún método virtual… qué sentido tiene dejarla sin sellar? Si nuestra clase no tiene métodos virtuales es que o bien hemos pensado que no tiene sentido que se extienda de ella, o que ni hemos pensado en cómo puede extenderse, y en ambos casos, para evitar problemas, es mejor dejar la clase sellada.

Miembros privados

Los miembros privados sólo son accesibles dentro de la propia clase que los declara. Cuando creamos una clase pensada para que se pueda heredar de ella, debemos tener siempre presente el principio de sustitución de Liskov (LSP). Este principio es, digamos, la base teórica del polimorfismo, y viene a decir que si S es una clase derivada de T, entonces los objetos de tipo T pueden ser reemplazados por objetos de tipo S sin alterar el comportamiento de nuestro sistema.

Si estáis habituados con el polimorfismo, diréis que viene a ser lo mismo… pero de hecho es posible tener polimorfismo sin respetar LSP. El polimorfismo viene dado por el lenguaje: es el lenguaje quien nos deja usar objetos de tipo S donde se esperen objetos de tipo T, pero el lenguaje no nos garantiza que se respete el LSP… eso debemos hacerlo nosotros.

¿Y que tiene que ver ese rollo con los métodos privados? Pues bien… imaginad un método virtual (por lo tanto redefinible desde la clase base), que accede a un método privado, para comprobar por ejemplo una precondición:

public class T {
private int count;
public virtual void Remove() {
if (count <= 0) throw new InvalidOperationException();
}
}

Si alguien deriva de nuestra clase T, y redefine el método Remove no tiene mecanismo para poder comprobar esa precondición. Es decir, incluso aunque nosotros documentemos la precondición quien redefine el método Remove() no tiene manera de poder reproducirla, puesto que no puede acceder a la variable count.

Así, si quieres que quien herede de tus clases pueda respetar el LSP, recuerda de no acceder a miembros privados desde métodos virtuales.

Excepciones

El LSP implica que los métodos redefinidos en una clase derivada, no deben lanzar nuevos tipos de excepciones que los que lanza el mismo método en la clase base (a no ser que esos nuevos tipos de excepciones sean a la vez subtipos de las excepciones lanzadas en el método de la clase base). Es decir, si un método foo() de una clase base T, lanza la excepcion IOException y se deriva de dicha clase T, al redefinir el método foo puede lanzarse la excepción IOException o cualquier derivada de esta, pero no podría lanzar una excepción de tipo ArgumentException p.ej.

Java define la clausula throws que indica que tipo de excepciones puede lanzar un método y no permite que los métodos redefinidos lancen excepciones de cualquier otro tipo al declarado en throws. C# no tiene ningún mecanismo que pueda obligar al cumplimiento de esta norma del LSP, así que sólo nos queda, al menos, documentar las excepciones que lanza cada método. Otra opción es definir métodos protegidos para lanzar todas las excepciones. De esa manera si quien deriva de nuestra clase detecta que debe lanzar la excepción X, puede llamar al método ThrowX. Eso garantiza que todas las excepciones se lanzan de forma coherente.

Code Contracts

Los que seguís mi blog sabréis que he hablado de Code Contracts un par de veces. Si eres de los que piensa que Code Contracts es un nuevo Debug.Assert, cuando quieras quedamos para tomar unas cervecillas y discutimos el asunto 🙂

Para el tema que nos ocupa, Code Contracts es básicamente una bendición. LSP obliga a que una clase derivada:

  • Debe mantener todas las precondiciones de la clase base, sin poder añadir más precondiciones.
  • Debe garantizar todas las postcondiciones de la clase base, sin poder eliminar postcondiciones.
  • Debe preservar todos los invariantes de la clase base.

Si no usamos Code Contracts, el principal problema es que las precondiciones, postcondiciones y invariantes, son codigo normal. Si en mi método virtual Foo() tengo un código que comprueba una precondición determinada, cuando se redefina este método dicho código debe ser escrito otra vez, para volver a comprobar la precondición si queremos mantener el LSP. Code Contracts nos garantiza esto automáticamente:

class T
{
protected bool empty;
public T()
{
empty = true;
}
public virtual void Add()
{
Contract.Requires(empty);
Contract.Ensures(!empty);
empty = false;
}
}

class S : T
{
public override void Add()
{
}
}

En este código cuando llamamos al método Add() de la clase S, se evalúan las precondiciones del método… que están definidas en la clase base.

De esta manera el desarrollador de la clase S, no debe preocuparse de reimplementar todas las precondiciones y postcondiciones de nuevo y puede concentrarse en lo que interesa: la redefinición del método Add().

Nota Técnica: Usar Code Contracts no nos exime de declara la variable empty con la suficiente visibilidad. Es decir, aunque sólo accedamos a empty dentro de la precondición contenida en T.Add(), debemos tener presente que desde el punto de vista de Code Contracts, esta precondición también se ejecutará dentro del método S.Add(). Y eso en Code Contracts es literal: Code Contracts no funciona creando un método “oculto” en la clase T que compruebe las precondiciones, sinó que modifica el IL generado por el compilador, para “copiar y pegar” las precondiciones y postcondiciones en cada método donde sean requeridas. Así, pues es “literalmente” como si las llamadas a Contract estuviesen también en S.Add(). Si declaramos la variable empty como private, el código compila (puesto que para el compilador todo es correcto), pero al ejecutarse se lanzará una excepción indicandonos que desde S.Add() estamos intentando acceder a un miembro sobre el cual no tenemos visibilidad.

Code Contracts no obliga al cumplimiento estricto de LSP, dado que el desarrollador de la clase S puede añadir nuevas precondiciones al método Add:

public override void Add()
{
Contract.Requires(otherPostcondition);
}

De todos modos si el desarrollador de la clase derivada hace esto, ya lo hace bajo su conocimiento y responsabilidad y además Code Contracts emite un warning para que quede claro: Method ‘CC1.S.Add’ overrides ‘CC1.T.Add’, thus cannot add Requires.

Conclusión

Cuando creamos clases, espcialmente clases públicas que formen parte de una API que usen otras personas, debemos tener especial cuidado a la hora de diseñarlas. Debemos prestar especial atención al LSP y tener presente que aunqué cumplir el LSP (aunqué siempre es muy recomendable) no sea siempre obligatorio, sí que puede serlo en otros casos, y nosotros como creadores de la clase base, debemos asegurarnos de tener el cuidado necesario y facilitar la vida al máximo para que quien derive de nuestras clases pueda cumplir el LSP… Y a riesgo de hacerme pesado, insisto que Code Contracts es una bendición.

Un saludo a todos!

11 comentarios sobre “Diseñar clases para ser heredadas…”

  1. Muy bueno Edu!! Solo un apunte respecto a CodeContracts.

    El tema de la aplicación del diseño por contractos o design by contracts tiene algunas peculiridades. Para un subtipo S que hereda de una clase base B, la precondición en S no debe ser más fuerte que en B y la postcondición no debe ser más debil que en B para garantizar LSP. esto debe ser tenido en cuenta por si se quiere sobreescribir tanto la pre como la postcondición en S.

    Saludos!!!

  2. Buenas Edu, muy bueno el post.

    De todas formas he mantenido una conversación con César de la Torre sobre LSP, yo inicialmente tenia la idea que tu has expuesto, es decir la del polimorfismo de toda la vida y como tu bien has expuesto: «S es una clase derivada de T, entonces los objetos de tipo T pueden ser reemplazados por objetos de tipo S sin alterar el comportamiento de nuestro sistema», pero César me ha hecho ver otra opción y es garantizar que: «los subtipos o clases hijas deben ser sustituibles por sus propios tipos base relacionados», para ello me ha enviado un link que también te los remito, para ver que opinas:

    http://www.eventhelix.com/RealtimeMantra/Object_Oriented/liskov_substitution_principle.htm

    y añado éste:

    http://www.lostechies.com/cfs-filesystemfile.ashx/__key/CommunityServer.Blogs.Components.WeblogFiles/derickbailey/LiskovSubtitutionPrinciple_5F00_52BB5162.jpg

    que también me lo ha enviado César y es muy bueno.

    Saludos,

  3. Gracias a todos por vuestros comentarios! 😉

    @Jordi
    Ei!!! Que tal? 😀

    MMmmm… creo que ambas ideas son equivalentes. Lo importante de LSP es la coletilla final «sin alterar el comportamiento de nuestro sistema». Eso es MUCHO más estricto que el polimorfismo tradicional. Si yo se que cualquier clase S derivada de T no me altera el comportamiento, eso significa que puedo diseñar mi sistema desde el punto de vista de T: Si mi sistema trabaja bien con T, trabajará bien con cualquier S derivada que cumpla LSP (es lo que viene a decir la frase «All code operating with a pointer or reference to the base class should be completely transparent to the type of the inherited object.» que hay en el primer link que me envías).

    @José Miguel
    Efectivament tienes toda la razón 😀
    De todos modos creo que ya lo menciono en mi post, cuando digo p.ej. que una clase derivada debe mantener las precondiciones de la clase base SIN poder añadir ninguna más.
    Lo que es importante de Code Contracts, es que te permite soportar LSP automáticamente, por el mero hecho de que las pre/post de la clase base son heredadas por la clase derivada.
    Es cierto que luego p.ej. puedes añadir más precondiciones, lo que viola LSP, pero al menos te suelta un warning 🙂

    @Jorge
    Muchas gracias por tu comentario!!! 😀

  4. Gran post Eduard, ojala pudiera meterlo (con comentarios y todo) en una SD y meterselo a algunos developers en la cabeza (se me ocurre un orificio para meter la SD, pero está un poco far away de la cabeza!)

    Salu2 y gran post 😀

  5. @Bruno
    Que crack jejejeee… 🙂 Gracias por tu comentario 😉

    @Cach_Ondo
    Buen nick, jejejee 😛
    Si haces base.Remove() se ejecuta el método de la clase base, con lo que obtienes todas las pre/postcondiciones de éste ciertamente… el problema es que también obtienes «todo lo demás»: la implementación entera del método base.
    No tienes manera de decir «quiero ejecutar sólo las pre/postcondiciones del método base, pero el resto de código NO, porque yo lo reimplemento de otro modo»…
    … justamente esto es lo que te permite hacer Code Contracts, y ahí radica su enorme poder!

    Un saludo y gracias por vuestros comentarios!!! 🙂

  6. De todas formas he mantenido una conversación con César de la Torre sobre LSP, yo inicialmente tenia la idea que tu has expuesto, es decir la del polimorfismo de toda la vida y como tu bien has expuesto

Responder a anonymous Cancelar respuesta

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