
El poder de la herencia
La herencia es un concepto fundamental en la programación orientada a objetos. En el contexto de los smart contracts, la herencia permite construir contratos más complejos, organizados y extendiendo la funcionalidad de contratos existentes.
En esencia, la herencia permite crear un nuevo contrato que hereda las propiedades y comportamientos de uno o más contratos existentes, llamados contratos base o padres.
¿Por qué usar la herencia?
La herencia ofrece varios beneficios importantes en el desarrollo:
- Reutilización de código: Evitas la duplicación de código. Si tienes una lógica común que quieres usar en varios contratos, puedes definirla en un contrato base y luego heredarla en otros contratos. Esto reduce la cantidad de código que tienes que escribir y mantener.
- Organización y modularidad: La herencia te ayuda a estructurar tu código de manera más lógica y modular.
- Extensibilidad: Puedes extender la funcionalidad de un contrato base añadiendo nuevas funciones o modificando las existentes en el contrato hijo. Esto facilita la evolución de tus contratos a lo largo del tiempo.
- Abstracción: La herencia permite crear abstracciones y jerarquías de contratos. Puedes definir interfaces o contratos abstractos que definen un comportamiento general, y luego implementar diferentes versiones concretas de ese comportamiento en contratos hijos.
Sintaxis básica de la herencia
Para indicar que un contrato hereda de otro, se utiliza la palabra clave is
después del nombre del contrato hijo, seguido del nombre del contrato padre.
contract ContratoPadre{
uint public valorPadre= 10;
function obtenerValorPadre() public view returns (uint) {
return valorPadre;
}
function funcionPadre() public pure returns (string memory) {
return "Soy una función del contrato base";
}
}
// Contrato Hijo que hereda de ContratoPadre
contract ContratoHijo is ContratoPadre{
uint public valorHijo = 20;
function obtenerValorHijo() public view returns (uint) {
return valorHijo;
}
function funcionHijo() public pure returns (string memory) {
return "Soy una función del contrato hijo";
}
}
En este ejemplo:
ContratoPadre
es el contrato base o padre. Define variables y funciones que serán heredadas.ContratoHijo is ContratoPadre
declara queContratoHijo
hereda deContratoPadre
.
Un contrato hijo hereda:
- Variables de estado públicas y internas:
valorPadre
en este caso es accesible desdeContratoHijo
. - Funciones públicas y internas:
obtenerValorPadre()
yfuncionPadre()
están disponibles enContratoHijo
.
Desde un contrato hijo, puedes acceder a las variables y funciones heredadas como si fueran parte del propio contrato hijo.
Constructores
Cuando un contrato hereda de otro, el constructor del contrato padre no se ejecuta automáticamente cuando se despliega el contrato hijo. Es responsabilidad del contrato hijo llamar explícitamente al constructor del contrato padre, si es necesario.
Para llamar al constructor del contrato padre, se utiliza la sintaxis ContratoPadre(argumentos)
en la lista de herencia del contrato hijo,
o en el constructor del contrato hijo.
Ejemplo llamando al constructor en la lista de herencia:
contract ContratoPadreConConstructor {
uint public valorInicial;
constructor(uint _valorInicial) {
valorInicial = _valorInicial;
}
}
contract ContratoHijoConConstructor is ContratoPadreConConstructor(5) {
uint public valorHijo;
constructor(uint _valorHijo) {
valorHijo = _valorHijo;
}
}
En este caso, al desplegar ContratoHijoConConstructor
, el constructor de ContratoPadreConConstructor
se ejecutará
primero con el argumento 5
, y luego se ejecutará el constructor de ContratoHijoConConstructor
.
Ejemplo llamando al constructor dentro del constructor del contrato hijo:
contract ContratoPadreConConstructor {
uint public valorInicial;
constructor(uint _valorInicial) {
valorInicial = _valorInicial;
}
}
contract ContratoHijoConConstructor is ContratoPadreConConstructor {
uint public valorHijo;
constructor(
uint _valorInicialPadre,
uint _valorHijo
) ContratoPadreConConstructor(_valorInicialPadre) {
valorHijo = _valorHijo;
}
}
Aquí, ContratoPadreConConstructor(_valorInicialPadre)
en el constructor de ContratoHijoConConstructor
llama al
constructor de ContratoPadreConConstructor
pasándole el argumento _valorInicialPadre
.
Es importante recordar que si el contrato base tiene un constructor y no lo llamas explícitamente desde el contrato hijo, el compilador de Solidity te avisará con una advertencia.
En Solidity no se puede usar super
dentro de un constructor para llamar al constructor del contrato base. A diferencia
de otros lenguajes como JavaScript o Python, en Solidity, los constructores de los contratos base deben ser llamados
explícitamente en la lista de herencia del constructor del contrato hijo.
Sobreescritura de funciones (override)
La herencia no solo permite reutilizar código, sino también modificar o extender el comportamiento de las funciones heredadas. Esto se logra mediante la sobreescritura de funciones.
Para permitir que una función sea sobreescrita en un contrato hijo, la función en el contrato padre debe ser declarada
como virtual
. En el contrato hijo, la función que sobrescribe debe ser declarada con la palabra clave override
.
contract ContratoPadre {
function mensaje() public virtual pure returns (string memory) {
return "Mensaje desde el contrato base";
}
}
contract ContratoHijo is ContratoPadre {
function mensaje() public override pure returns (string memory) {
return "Mensaje sobreescrito desde el contrato hijo";
}
}
En este ejemplo:
mensaje()
enContratoPadre
esvirtual
, lo que indica que puede ser sobreescrita.mensaje()
enContratoHijo
esoverride
, indicando que está sobrescribiendo la funciónmensaje()
del contrato padre.
Si se llama a mensaje()
en una instancia de ContratoHijo
, se ejecutará la versión sobreescrita en el contrato hijo, no la versión del contrato padre.
Funciones Abstractas:
Además de virtual
, Solidity también soporta funciones abstractas. Una función abstracta se declara sin implementación en el contrato padre y
debe ser implementada (sobreescrita) en un contrato hijo que no sea abstracto. Un contrato que contiene al menos una función abstracta también debe ser declarado como abstract
.
abstract contract ContratoAbstracto {
// Función abstracta (sin implementación)
function funcionAbstracta() public virtual pure;
}
contract ContratoConcreto is ContratoAbstracto {
// Implementación de la función abstracta
function funcionAbstracta() public override pure {
// ... implementación ...
}
}
Los contratos abstractos no pueden ser desplegados directamente; deben ser heredados e implementados por contratos concretos. Son útiles para definir interfaces o comportamientos base que luego serán implementados de diferentes maneras por los contratos hijos.
Modificadores
Cuando un contrato hijo hereda una función de un contrato base, esta mantiene los modificadores que fueron definidos en el contrato padre solo si no se sobrescribe la función. Sin embargo, si la función es sobrescrita en el contrato hijo, los modificadores no se heredan automáticamente, por lo que es necesario volver a declararlos explícitamente.
contract ContratoPadre{
modifier soloAdmin {
require(msg.sender == owner,
"Solo el administrador puede llamar a esta función"
);
_;
}
address public owner;
constructor() {
owner = msg.sender;
}
function funcionProtegida() public virtual soloAdmin
returns (string memory) {
return "Función protegida del contrato base";
}
}
contract ContratoHijo is ContratoPadre{
modifier soloUsuarioEspecial {
require(msg.sender == usuarioEspecial,
"Solo el usuario especial puede llamar a esta función"
);
_;
}
address public usuarioEspecial;
constructor(address _usuarioEspecial) {
usuarioEspecial = _usuarioEspecial;
}
function funcionProtegida() public override soloAdmin soloUsuarioEspecial
returns (string memory) {
return "Función protegida y modificada en el contrato hijo";
}
}
Explicación del Código
ContratoPadre
: Define el modificadorsoloAdmin
y una funciónfuncionProtegida()
que lo usa.ContratoHijo
:- Hereda
ContratoPadre
. - Declara un nuevo modificador
soloUsuarioEspecial
. - Sobrescribe
funcionProtegida()
y especifica explícitamente ambos modificadores (soloAdmin
ysoloUsuarioEspecial
).
- Hereda
Herencia múltiple
Solidity soporta la herencia múltiple, lo que significa que un contrato puede heredar de múltiples contratos padre.
Se declara listando los contratos padre separados por comas después de la palabra clave is
.
contract ContratoPadreA {
// ...
}
contract ContratoPadreB {
// ...
}
contract ContratoHijoMultiple is ContratoPadreA, ContratoPadreB {
// ...
}
Sin embargo, la herencia múltiple puede introducir complejidad, especialmente con el famoso “problema del diamante” (diamond problem). Este problema surge cuando una clase común es heredada por múltiples contratos intermedios, lo que genera ambigüedades sobre qué implementación debe usarse en un contrato descendiente. Solidity resuelve esto mediante la linealización C3, un algoritmo que define un orden determinista de herencia.
Imagina esta estructura:
ContratoAbuelo
/ \
ContratoPadreA ContratoPadreB
\ /
ContratoNieto
Si ContratoAbuelo
define una función o variable, y ni ContratoPadreA
ni ContratoPadreB
la sobreescriben, pero ContratoNieto
hereda de ambos,
¿qué versión de la función o variable hereda ContratoNieto
? ¿La de ContratoAbuelo
a través de ContratoPadreA
o a través de ContratoPadreB
?
Si ambos padres sobreescriben la función de manera diferente, la ambigüedad se intensifica.
Linealización C3
La linealización C3 aplana la jerarquía de herencia en un orden lineal, asegurando que cada contrato padre aparezca una única vez en la cadena de herencia. Este proceso sigue tres principios fundamentales:
- Orden consistente: Se mantiene un orden determinista para la resolución de funciones.
- Preservación del orden local: Si ContratoA precede a ContratoB en la declaración de herencia, este orden se respeta en la linealización.
- Monotonicidad: La estructura de herencia de los ancestros se conserva en los contratos descendientes.
contract ContratoAbuelo {
function mensaje() public pure returns (string memory) {
return "Mensaje del abuelo";
}
}
contract ContratoPadreA is ContratoAbuelo {}
contract ContratoPadreB is ContratoAbuelo {}
contract ContratoNieto is ContratoPadreA, ContratoPadreB {}
Visibilidad
La visibilidad de las variables y funciones en la herencia es importante:
public
: Accesible desde cualquier parte, incluyendo contratos hijos y el mundo exterior.private
: Solo accesible desde el contrato donde se define. No se hereda ni es accesible desde contratos hijos.internal
: Accesible desde el contrato donde se define y desde contratos hijos. Es la visibilidad más común para miembros que se espera que sean utilizados en la herencia.external
: Solo accesible desde fuera del contrato. No puede ser llamado internamente ni desde contratos hijos directamente. Si necesitas llamarlo internamente, se debe usarthis.<funcion>()
lo que implica una llamada externa y consume más gas.
En general, para miembros que deseas que sean accesibles y reutilizables en la herencia, usa internal
o public
.
Evita private
si quieres que los contratos hijos puedan acceder a ellos.