Metodos y clases abstractas

En general entendemos por clase totalmente abstracta cualquier clase en la que todos sus metodos son abstractos.

La abstracción de metodos es una técnica muy util para definir patrones de comportamiento de aquellas clases que hereden de la clase que estamos definiendo.

Un metodo abstracto es un metodo de una clase para el cual no se va a proporcionar implementación sino que se espera que que las clases que heredan de ella implementen dicho metodo. En delphi, si se intenta ejecutar un metodo abstracto obtendremos una excepción puesto que realmente no hay un código definido para ese metodo. El ejemplo más clasico de metodo abstracto es algo así:

type TPoligono = class

 public
   function ObtenerPerimetro : integer; virtual; abstract;
   function ObtenerArea : integer; virtual; abstract;

end;

type TRectangulo = class(TPoligono)

 private
   base, altura : integer;
 public
   function ObtenerPerimetro : integer; override;
   function ObtenerArea : integer; override;

end;

type TTriangulo = class(TPoligono)

 private
   base, lado1, lado2, altura : integer;
 public
   function ObtenerPerimetro : integer; override;
   function ObtenerArea : integer; override;

end;

type TPrueba = class

 public 
   procedure LeeInfo;

end;

function TRectangulo.ObtenerPerimetro; begin

 result := base*2 + altura*2;

end;

function TRectangulo.ObtenerArea; begin

 result := base * altura;

end;

function TTriangulo.ObtenerPerimetro; begin

 result := base * altura / 2;

end;

function TTriangulo.ObtenerArea; begin

 result := base + lado1 + lado2;

end;

procedure TPrueba.LeeInfo(poligono : TPoligono); begin

 WriteLn('Area: ' + IntToStr(poligono.ObtenerArea));
 WriteLn('Perimetro: ' + IntToStr(poligono.ObtenerPerimetro));

end;

En el código anterior, el metodo LeeInfo acepta como parametro un TPoligono o cualquier clase hija de TPoligono, sin embargo el metodo ejecutado será distinto si entra un rectangulo o si entra un triangulo. Es más, no tendrá sentido pasar un objeto de la clase TPoligono (puesto que la clase es abstracta de forma que si lo hacemos dará un error de abstracto). De esta forma hemos conseguido definir un concepto, el póligono. Triangulos, cuadrados, rectangulos son tipos de poligonos y por tanto de todos ellos se puede calcular el area y el perimetro pero sin embargo no existe algo como un poligono, nunca se va a instanciar, sirve sencillamente de modelo para otras clases.

Interfaces

Un interface es otra forma de definir un modelo de comportamiento. Cuando hablamos de clases abstractas definimos las características básicas de un tipo de objeto, sin embargo cuando definimos interfaces estamos hablando de capacidades o habilidades de un objeto.

Veamos un ejemplo concreto que me surgió en un pequeño proyecto de DirectX. El principal objeto del proyecto es la escena. Una escena define todo aquello que existe ahora mismo en el escenario que se va a dibujar. Esto incluye distintos tipos de objetos (cubos, esferas, objetos importados) visible y otros que no lo son (puntos de viento, mágnéticos, etc).

La tarea que se encarga de renderizar la escena tiene que recorrer los objetos y dibujar cada uno de forma correcta, ahora bien, cada objeto se dibuja de distinta forma (y algunos incluso no se dibujan).

Una de las opciones que tenía era haber creado una clase padre CRenderObject de la que heredaran todos los objetos que se pueden renderizar … pero el problema de esta solución (a parte del hecho de que la herencia se utiliza con el significado semántico A es un tipo de B) es que solo nos sirve una vez (la mayor parte de los lenguajes de programación no soporta herencia multiple y aunque así fuera no es una técnica que personalmente me guste lo más minimo) de forma que si quiero distinguir también entre objetos que puedan verse afectados por fuerzas físicas (por ejemplo) ya no me sirve.

Para suplir esto (entre otras cosas) están los interfaces. Un interfaces no expresa un modelo de comportamiento, no es un tipo de herencia de forma que no implica “un doberman es un perro y todos los perros andan” sino, como ya he dicho, expresan una capacidad.

Otra forma de enfocarlo, más genérica pero quizá más correcta, es que un interfaz identifica un contrato y, si una clase dice que implementa dicho interfaz, es porque se compromete a cumplir dicho contrato.

De esta forma definí un interfaz llamado IRenderable que en lenguaje coloquial quiere decir, un objeto que puede Renderizarse, y otro llamado IWeighted (del ingles “que tiene peso”) de forma que, los objetos que pueden dibujarse implementan el interfaz IRenderable (y definen ellos mismos como se dibujan) y aquellos que se ven afectados por las leyes físicas implementan el interfaz IWeighted (que provee una serie de propiedades físicas inherentes como el indice de rozamiento, peso, volumen …). Por último obviamente si un objeto es dibujable y se ve afectado por la física implementa los dos interfaces, o lo que es lo mismo, cumple los dos contratos y por tanto se compromete a implementar todo lo que dichos interfaces especifican.

Lo anterior en código:

interface IRenderable =

 { Renderiza el objeto dados los engines DX }
 procedure Render(DXEngineCollection engines);

end;

interface IWeighted =

 function GetWeight : Double;
 function GetFrictionCoeficient : Double;
 function GetVolume : Double;

end;

type TCube = class(TInterfacedObject, IRenderable, IWeighted)

 procedure Render(DXEngineCollection engines);
 function GetWeight : Double;
 function GetFrictionCoeficient : Double;
 function GetVolume : Double;

end;

type TBlackHole = class(TInterfacedObject, IWeighted);

 { Peso del agujero negro --> fuerza con la
   que atrae al resto de objetos de la escena }
 function GetWeight;
 { Rozamiento será infinito }
 function GetFrictionCoeficient;
 { Sin efecto --> no se mueve }
 function GetVolume : Double;

end;

De esta forma, nuestros dos interfaces especifican que, en caso de que declaremos ser renderizables estamos obligados a implementar un método Render, con los parametros especificados. De esta forma mi procedimiento de renderizado, por ejemplo, puede obtener una lista de objetos global y, para cada uno de los objetos, en caso de que sean renderizables, realizar el render sin preocuparse de los detalles concretos del objeto, por ejemplo:

procedure DoRender; var

 i : integer;
 iRenderIface : IRenderable;

begin

 for i := 0 to FListaObjetos.Count - 1 do
 begin
   // Comprobar si el objeto soporta el interfaz
   if Supports(FListaObjectos[i], IRenderable, iRenderIface) then
      IRenderIface.Render(FEngines);
 end;

end;

¿Cuando usar unos y otros?

La decisión de cuando deben usarse clases abstractas y cuando deben usarse interfaces ha surgido muchas veces y no es una de esas discusiones complejas con muchos detractores de cada una de las opciones. En general hay una regla básica que suele dar buen resultado una y otra vez: Utiliza clases abstractas para definir relaciones que puedan responder a la frase “es un”, utiliza interfaces para definir relaciones que puedan responder a la frase “es capaz de”, o dicho de otra forma, una clase abstracta generalmente representa lo que un objeto es, mientras que un interfaz representa algo que el objeto puede hacer.

Por ejemplo, para los casos anteriores podemos decir que un triangulo es un poligono, pero resulta bastante chocante decir que es capaz de ser un poligono.

Dicho todo lo anterior, antes que cualquier otra cosa se aplica la regla de oro, usa tu sentido común. A veces un interfaz cuadra mucho mejor por razones de herencia múltiple que una clase abstracta, incluso cuando la regla anterior te diga lo contrario, en esos casos usa el interfaz.