La herencia es un mecanismo que tiene por objetivo principal el compartir lógica/código similar. Esto lleva a evitar la duplicación de lógica/código. Cuando un objeto recibe un mensaje, mediante Method lookup buscará el comportamiento requerido en la clase de la cual es instancia y, en caso de no tener un método para el mismo, en sus superclases.
Una clase tiene siempre una superclase pero solo una.
Lenguajes que implementan este tipo de herencia: Smalltalk, Java, C#, entre muchos otros
Una clase puede tener más de una superclase.
Lenguajes que implementan este tipo de herencia: C++, Eiffel
Empezamos con dos clases, Golondrina (con una variable de instancia energia) y Picaflor (con una variable de instancia energia) , definimos métodos para ambos
#Golondrina
>> energia
^energia
>> come: gramos
"Una golondrina aumenta su energia en cuatro veces los gramos ingeridos"
energia := energia + (gramos * 4)
>> vola: kms
"Una golondrina disminuye su energia en 1 Joule por cada kilometro recorrido + 10 Joules que utiliza para el despegue "
energia := energia - (kms + 10)
#Picaflor
>> energia
^energia
>> come: gramos
"Un picaflor aumenta su energia en cuatro veces los gramos ingeridos"
energia := energia + (gramos * 4)
>> vola: kms
"Un picaflor disminuye su energia en 1 Joule por cada kilometro recorrido + 20 Joules que utiliza para el despegue "
energia := energia - (kms + 20)
class Golondrina {
var energia
method energia() {
return energia
}
method come(gramos){
//Una golondrina aumenta su energia en cuatro veces los gramos ingeridos
energia = energia + (gramos * 4)
}
method vola(kms) {
// Una golondrina disminuye su energia en 1 Joule por cada kilometro recorrido + 10 Joules que utiliza para el despegue
energia = energia - (kms + 10)
}
}
class Picaflor {
var energia
method energia() {
return energia
}
method come(gramos){
//Un picaflor aumenta su energia en cuatro veces los gramos ingeridos
energia = energia + (gramos * 4)
}
method vola(kms) {
// Un picaflor disminuye su energia en 1 Joule por cada kilometro recorrido + 20 Joules que utiliza para el despegue
energia = energia - (kms + 20)
}
}
Lo que nos permite la idea de generalización utilizando herencia es crear nuevas abtracciones.
En el código de arriba nos podemos dar cuenta que tanto las golondrinas como los picaflores saben decirnos su energia, comer y volar.
Ahora bien, las golondrinas y los picaflores (por ejemplo) saben comer pero además comen de la misma forma. Estaría bueno poder generalizar eso, si las únicos pajaritos con los que estoy trabajando son golondrinas y picaflores puedo decir que todas las aves comen de la misma forma. Entonces generalizo el concepto de Golondrina y Picaflor utilizando una nueva abstracción, como necesito poner en esa abstracción métodos y definir atributos nada mejor que esa nueva abstracción sea una nueva clase
#Ave >> come: gramos
"Un ave aumenta su energia en cuatro veces los gramos ingeridos"
energia := energia + (gramos * 4)
class Ave {
method come(gramos){
//Un ave aumenta su energia en cuatro veces los gramos ingeridos
energia = energia + (gramos * 4)
}
Pero no puedo poner ese código en la clase Ave porque esa clase no tiene una variable de instancia energia.
Si todas las aves tienen que tener una variable de instancia es algo que me gustaría dejar escrito solo en Ave.
Ave tiene definida una variable de instancia energia
class Ave {
var energia
....
¿Cómo sigue esto?
Tengo que explicitar que las golondrinas tienen todo el comportamiento que esta en la clase Golondrina y también tienen el comportamiento que está en la clase Ave. Además tengo que explicitar que los picaflores tienen todo el comportamiento que esta en la clase Picaflor y también tienen el comportamiento que está en la clase Ave.
Esto se hace diciendo que Ave es superclase de Golondrina y Ave es superclase de Picaflor; además tenemos que eliminar el código repetido de las clases Golondrina y Picaflor.
En Smalltalk la forma de crear una clase es enviándole el mensaje #subclass:instanceVariableNames:classVariableNames: (o uno similar, depende del dialecto de Smalltalk utilizado) a la superclase de la clase que queremos crear.
Superclass
subclass: #NameOfSubclass
instanceVariableNames: 'variableInstancia1 variableInstancia2 variableInstanciaN'
classVariableNames: 'variableClase1 variableClase2 variableClaseN'
En nuestro ejemplo
Object
subclass: #Ave
instanceVariableNames: 'energia'
classVariableNames: ''.
Ave
subclass: #Golondrina
instanceVariableNames: ''
classVariableNames: ''.
Ave
subclass: #Picaflor
instanceVariableNames: ''
classVariableNames: ''.
En Wollok, lo ponemos directamente en la definición de las clases, usando la palabra clave inherits
:
class Ave {
var energia = 0
method come(gramos){
//Un ave aumenta su energia en cuatro veces los gramos ingeridos
energia = energia + (gramos * 4)
}
class Picaflor inherits Ave {
method vola(kms) {
// Un picaflor disminuye su energia en 1 Joule por cada kilometro recorrido + 20 Joules que utiliza para el despegue
energia = energia - (kms + 20)
}
}
class Golondrina inherits Ave {
method vola(kms) {
// Una golondrina disminuye su energia en 1 Joule por cada kilometro recorrido + 10 Joules que utiliza para el despegue
energia = energia - (kms + 10)
}
}
En el ejemplo anterior la clase Ave se está usando como superclase de Golondrina y Picaflor. Si nosotros queremos instanciar un ave deberíamos elegir si será una golondrina o un picaflor para mandarle el mensaje new a alguna de esas clases. Lo que no sería correcto es:
unAve := Ave new.
var unAve = new Ave()
Este código va a funcionar en principio, pero si yo considero que un ave tiene que poder volar y no hay una implementación en Ave para esta operación ya que está definido de formas diferentes en sus subclases, unAve no va a entender el mensaje.
Entonces, una clase abstracta es aquella que no tiene sentido instanciar porque es demasiado genérica y no tiene una implementación concreta para algunos mensajes que debería entender porque está definido en sus subclases.
Una buena práctica para formalizar el contrato de lo que es ser un ave es definir los métodos faltantes en la clase Ave de la siguiente forma:
#Ave >> vola: unosKilometros
self subclassResponsibility
Cuando un objeto recibe el mensaje #subclassResponsibility se produce un error indicando que el método debería redefinirse en alguna subclase.
Se indica con la palabra clave abstract
antes del método
class Ave {
abstract method vola(unosKilometros)
....
}
De esa forma si agregamos otra subclase de Ave, como ser Gaviota, y olvidamos redefinir vola
, cuando una instancia de Gaviota reciba ese mensaje el error será más descriptivo que el error de no entender el mensaje, y el desarrollador sabrá que las gaviotas deberían definir su forma de volar.