Herança é interfaces
A herança e as interfaces são conceitos fundamentais na programação de smart contracts em Solidity, permitindo a organização modular e reutilizável do código, a abstração de funcionalidades e a criação de contratos compatíveis. Vamos guiá-lo através dos conceitos básicos e da implementação prática de herança e interfaces em Solidity.
Orientação a Objetos em Solidity
Conceito Básico de OO:
Objetos: Pense nos objetos como entidades do mundo real. Por exemplo, um carro. Cada carro tem atributos (como cor, modelo, marca) e comportamentos (como acelerar, frear).
Classes: Uma classe é como uma planta baixa (blueprint) para criar objetos. No caso dos carros, a classe definiria o que todo carro deve ter (atributos) e o que pode fazer (métodos).
Definindo uma Classe (Contrato) em Solidity:
Em Solidity, um contrato pode ser visto como uma classe. Ele define um conjunto de dados (estado) e funções (comportamentos).
pragma solidity ^0.8.0;
contract Car {
// Atributos (estado)
string public color;
string public model;
uint public mileage;
// Construtor - inicializa o objeto
constructor(string memory _color, string memory _model, uint _mileage) {
color = _color;
model = _model;
mileage = _mileage;
}
// Método (função)
function drive(uint distance) public {
mileage += distance;
}
}
Instanciando Objetos:
Quando você cria um novo contrato Car, está criando uma instância dessa classe. Cada instância tem seu próprio estado.
Car myCar = new Car("Red", "Tesla Model S", 100);
Herança:
Assim como em outras linguagens orientadas a objetos, Solidity suporta herança, permitindo que um contrato herde as propriedades e métodos de outro contrato.
contract ElectricCar is Car {
uint public batteryLife;
constructor(string memory _color, string memory _model, uint _mileage, uint _batteryLife)
Car(_color, _model, _mileage) {
batteryLife = _batteryLife;
}
function chargeBattery(uint amount) public {
batteryLife += amount;
}
}
Encapsulamento:
Encapsulamento é a prática de esconder os detalhes internos e expor apenas o necessário. Em Solidity, podemos usar modificadores de visibilidade como
public
,private
, einternal
para controlar o acesso.
contract Car {
string public color;
string private model;
uint internal mileage;
constructor(string memory _color, string memory _model, uint _mileage) {
color = _color;
model = _model;
mileage = _mileage;
}
function drive(uint distance) public {
mileage += distance;
}
function getModel() public view returns (string memory) {
return model;
}
}
Desafio prático : Contas de Banco Imagine um banco com diferentes tipos de contas: conta corrente, conta poupança e conta investimento. Cada conta possui características e funcionalidades próprias, como taxas de juros, limites de saque e opções de investimento.
Contas como contratos: Cada tipo de conta pode ser visto como um contrato inteligente, com suas próprias regras e funções.
OO para organização: A OO permite criar classes abstratas para as características básicas (conta) e classes derivadas para cada tipo de conta (corrente, poupança, investimento), com herança e redefinição de métodos.
Modularização: Maior organização, reutilização de código, segurança e confiabilidade nas transações bancárias.
Herança
Herança é um princípio da programação orientada a objetos que permite que um contrato (a classe base) transfira suas propriedades e métodos para outro contrato (a classe derivada). Isso promove a reutilização de código e facilita a manutenção.
Exemplo de Herança:
// Contrato Pai: Veiculo
contract Veiculo {
string marca;
string modelo;
constructor(string memory _marca, string memory _modelo) {
marca = _marca;
modelo = _modelo;
}
function getMarca() public view returns (string memory) {
return marca;
}
function getModelo() public view returns (string memory) {
return modelo;
}
}
// Contrato Filho: Carro
contract Carro is Veiculo {
uint256 numeroPortas;
constructor(string memory _marca, string memory _modelo, uint256 _numeroPortas) Veiculo(_marca, _modelo) {
numeroPortas = _numeroPortas;
}
function getNumeroPortas() public view returns (uint256) {
return numeroPortas;
}
}
Neste exemplo, o contrato Carro
herda do contrato Veiculo
. O contrato Carro
possui as propriedades e métodos do Veiculo
, como marca
, modelo
, getMarca
e getModelo
, além de adicionar sua própria propriedade numeroPortas
e método getNumeroPortas
.
Herança Múltipla
Solidity também suporta herança múltipla, permitindo que um contrato herde de vários contratos. Aqui está o código Solidity comentado, explicando o que cada parte faz:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// Definição do contrato A
contract A {
// Declaração de uma variável de estado privada que armazena o endereço do dono do contrato
address private dono;
// Modificador de função que restringe a execução apenas ao dono do contrato
modifier apenasDono() {
require(msg.sender == dono, "Apenas o dono pode chamar essa funcao");
_;
}
// Construtor do contrato que define o endereço do dono como o endereço que implanta o contrato
constructor() {
dono = msg.sender;
}
// Função pública que retorna uma string "Funcao A"
function funcA() public pure returns (string memory) {
return "Funcao A";
}
// Função pública restrita ao dono que retorna uma string "Funcao AX"
function funcX() public apenasDono virtual view returns (string memory) {
return "Funcao AX";
}
}
// Definição do contrato B
contract B {
// Função pública que retorna uma string "Funcao B"
function funcB() public virtual pure returns (string memory) {
return "Funcao B";
}
// Função pública que retorna uma string "Funcao BX"
function funcX() public virtual view returns (string memory) {
return "Funcao BX";
}
}
// Definição do contrato C que herda de A e B
contract C is A, B {
// Este contrato agora tem acesso a funcA e funcB
// Sobrescrita da função funcX que é restrita ao dono, combinando a lógica dos contratos A e B
function funcX() public apenasDono override(A, B) view returns (string memory) {
// lógica adicional pode ser adicionada aqui
return "Funcao CX";
}
// Sobrescrita da função funcB do contrato B
function funcB() public override(B) pure returns (string memory) {
return "Funcao CB";
}
}
Contrato A
address private dono;
: Armazena o endereço do dono do contrato.modifier apenasDono()
: Modificador que garante que apenas o dono pode executar a função.constructor()
: Define o dono do contrato como o endereço que o implantou.funcA()
: Função pública que retorna "Funcao A".funcX()
: Função pública restrita ao dono que retorna "Funcao AX".
Contrato B
funcB()
: Função pública que retorna "Funcao B".funcX()
: Função pública que retorna "Funcao BX".
Contrato C
Herda de
A
eB
.funcX()
: Sobrescreve a funçãofuncX
dos contratosA
eB
e é restrita ao dono.funcB()
: Sobrescreve a funçãofuncB
do contratoB
.
Benefícios da Herança:
Reutilização de código: Reduz a duplicação de código, promovendo a organização e a legibilidade do código.
Extensão de funcionalidades: Permite a criação de contratos mais complexos e especializados, construindo sobre a base de contratos existentes.
Organização modular: Facilita a estruturação do código em módulos distintos e interligados, promovendo a manutenção e a atualização do código.
Interfaces
As interfaces em Solidity definem um conjunto de funções que um contrato deve implementar, sem fornecer a implementação das funções. As interfaces permitem a abstração de funcionalidades e a criação de contratos compatíveis.
Exemplo de Interface:
// Interface: VeiculoInterface
interface VeiculoInterface {
function getMarca() external view returns (string memory);
function getModelo() external view returns (string memory);
}
// Contrato: Carro
contract Carro is VeiculoInterface {
string marca;
string modelo;
constructor(string memory _marca, string memory _modelo) {
marca = _marca;
modelo = _modelo;
}
function getMarca() public view override returns (string memory) {
return marca;
}
function getModelo() public view override returns (string memory) {
return modelo;
}
}
Neste exemplo, a interface VeiculoInterface
define as funções getMarca
e getModelo
. O contrato Carro
implementa a interface VeiculoInterface
, garantindo que ele possui as funções definidas na interface.
Benefícios das Interfaces:
Abstração de funcionalidades: Separa a definição da implementação, permitindo que diferentes contratos implementem as mesmas funções de maneira distinta.
Compatibilidade de contratos: Facilita a interação entre contratos, garantindo que todos os contratos que implementam a mesma interface possuem as mesmas funções disponíveis.
Desacoplamento de contratos: Reduz a dependência entre contratos, permitindo a fácil substituição de um contrato por outro que implementa a mesma interface.
Usando Herança e Interfaces Juntas
A herança e as interfaces podem ser usadas juntas para criar uma estrutura modular e flexível para seus smart contracts. Você pode usar interfaces para definir funcionalidades abstratas que diferentes contratos podem implementar, e usar herança para criar contratos que herdam funcionalidades de outros contratos e implementam interfaces.
Exemplo:
// Interface: VeiculoInterface
interface VeiculoInterface {
function getMarca() external view returns (string memory);
function getModelo() external view returns (string memory);
}
// Contrato Pai: Veiculo
contract Veiculo is VeiculoInterface {
string marca;
string modelo;
constructor(string memory _marca, string memory _modelo) {
marca
Especificadores de Visibilidade em Herança e Interfaces em Solidity
Os especificadores de visibilidade em Solidity controlam o acesso a atributos e funções de um contrato, definindo quem pode acessá-los e de onde. No contexto de herança e interfaces, os especificadores de visibilidade assumem importância crucial na organização modular e no controle de acesso entre diferentes contratos.
Em Solidity, existem quatro especificadores de visibilidade principais: external
, public
, internal
e private
. Vamos explorar cada um deles com exemplos práticos.
public
Atributos: Quando um atributo é declarado como
public
, Solidity automaticamente cria uma função getter para ele. Isso significa que o atributo pode ser acessado por qualquer pessoa dentro e fora do contrato.Funções: Funções
public
podem ser chamadas de dentro do contrato, por outros contratos e externamente através de transações.
Exemplo
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract PublicExample {
uint public valorPublico;
function setValorPublico(uint _valor) public {
valorPublico = _valor;
}
}
Neste exemplo, valorPublico
pode ser lido externamente, e setValorPublico
pode ser chamado tanto internamente quanto externamente.
external
Funções: Funções
external
só podem ser chamadas de fora do contrato. Elas não podem ser chamadas internamente sem o uso da palavra-chavethis
.
Exemplo
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract ExternalExample {
uint private valorExterno;
function setValorExterno(uint _valor) external {
valorExterno = _valor;
}
function chamarSetValorExterno(uint _valor) public {
// Chamando uma função external de dentro do contrato
this.setValorExterno(_valor);
}
}
Neste exemplo, setValorExterno
só pode ser chamada externamente ou internamente usando this
.
internal
Atributos e Funções: Atributos e funções
internal
podem ser acessados apenas dentro do contrato em que são definidos e em contratos que herdam desse contrato.
Exemplo
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract InternalExample {
uint internal valorInterno;
function setValorInterno(uint _valor) internal {
valorInterno = _valor;
}
}
contract DerivedInternalExample is InternalExample {
function atualizarValorInterno(uint _valor) public {
setValorInterno(_valor); // Chamando a função internal do contrato pai
}
}
Neste exemplo, valorInterno
e setValorInterno
são acessíveis apenas dentro de InternalExample
e DerivedInternalExample
.
private
Atributos e Funções: Atributos e funções
private
só podem ser acessados dentro do contrato em que são definidos. Eles não podem ser acessados por contratos derivados.
Exemplo
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract PrivateExample {
uint private valorPrivado;
function setValorPrivado(uint _valor) private {
valorPrivado = _valor;
}
function atualizarValorPrivado(uint _valor) public {
setValorPrivado(_valor); // Chamando a função private dentro do próprio contrato
}
}
contract DerivedPrivateExample is PrivateExample {
function tentarAcessarPrivado(uint _valor) public {
// setValorPrivado(_valor); // Isto gerará um erro
}
}
Neste exemplo, valorPrivado
e setValorPrivado
só podem ser acessados dentro de PrivateExample
. Qualquer tentativa de acessá-los em DerivedPrivateExample
resultará em um erro.
Visibilidade em Herança:
Atributos: A visibilidade de um atributo herdado é preservada no contrato filho. Por exemplo, se um atributo for
private
no contrato pai, ele também seráprivate
no contrato filho.Funções: A visibilidade de uma função herdada pode ser modificada no contrato filho. Por exemplo, uma função
public
no contrato pai pode ser alterada paraprivate
no contrato filho.
Visibilidade em Interfaces:
As interfaces definem apenas a assinatura das funções, não sua visibilidade.
Ao implementar uma interface, o contrato deve escolher a visibilidade adequada para cada função, considerando a lógica do contrato e os princípios de encapsulamento.
Exemplo de Visibilidade em Herança:
// Contrato Pai: Veiculo
contract Veiculo {
string marca; // private
string modelo; // public
function getMarca() public view returns (string memory) {
return marca;
}
function getModelo() public view returns (string memory) {
return modelo;
}
}
// Contrato Filho: Carro
contract Carro is Veiculo {
uint256 numeroPortas;
constructor(string memory _marca, string memory _modelo, uint256 _numeroPortas) Veiculo(_marca, _modelo) {
numeroPortas = _numeroPortas;
}
function getNumeroPortas() public view returns (uint256) {
return numeroPortas;
}
}
Neste exemplo, o contrato Veiculo
possui um atributo marca
privado e um atributo modelo
público. No contrato Carro
, o atributo marca
permanece privado, enquanto o atributo modelo
pode ser acessado por contratos externos.
Exemplo de Visibilidade em Interface:
// Interface: VeiculoInterface
interface VeiculoInterface {
function getMarca() external view returns (string memory);
function getModelo() external view returns (string memory);
}
// Contrato: Carro
contract Carro is VeiculoInterface {
string marca;
string modelo;
constructor(string memory _marca, string memory _modelo) {
marca = _marca;
modelo = _modelo;
}
function getMarca() public view override returns (string memory) {
return marca;
}
function getModelo() public view override returns (string memory) {
return modelo;
}
}
Neste exemplo, a interface VeiculoInterface
define as funções getMarca
e getModelo
. O contrato Carro
implementa a interface e escolhe a visibilidade public
para ambas as funções.
Considerações Importantes:
Utilize os especificadores de visibilidade com cuidado para garantir o encapsulamento e a segurança dos seus smart contracts.
Evite expor atributos e funções desnecessariamente, limitando o acesso a apenas quem precisa deles.
Avalie cuidadosamente a visibilidade de funções herdadas antes de modificá-las no contrato filho.
Considere a utilização de interfaces para definir funcionalidades abstratas e permitir a implementação com diferentes visibilidades em diferentes contratos.
Diferenças entre internal
e external
:
Acesso:
internal
é interno, enquantoexternal
é externo.Chamadas:
internal
pode ser chamada internamente e por derivados, enquantoexternal
só pode ser chamada externamente.Encapsulamento:
internal
promove encapsulamento, enquantoexternal
expõe a interface da função.
Os especificadores de visibilidade são essenciais para controlar o acesso a atributos e funções em contratos inteligentes Solidity. Utilizar esses especificadores de maneira adequada ajuda a proteger dados sensíveis e a manter uma boa organização do código. Aqui está um resumo rápido:
public
: Acessível por qualquer pessoa dentro e fora do contrato.external
: Acessível apenas de fora do contrato.internal
: Acessível dentro do contrato e em contratos derivados.private
: Acessível apenas dentro do contrato onde é definido.
Herança de atributos , funções, construtor, modifier, events
A herança de contratos é uma técnica que permite a reutilização e extensão de código. Através da herança, contratos filhos podem herdar atributos (variáveis de estado), funções, construtores, modifiers e eventos dos contratos pais. Vamos detalhar como a herança funciona para cada um desses componentes:
Atributos (Variáveis de Estado)
Quando um contrato herda de outro, ele automaticamente herda todas as variáveis de estado do contrato pai. Estas variáveis podem ser acessadas e modificadas diretamente no contrato filho, a menos que sejam declaradas como private
.
Exemplo:
pragma solidity ^0.8.0;
contract Parent {
uint public value;
}
contract Child is Parent {
function setValue(uint _value) public {
value = _value; // Acessa a variável de estado herdada
}
}
Funções
As funções de um contrato pai são herdadas pelo contrato filho e podem ser chamadas diretamente ou sobrescritas (override) no contrato filho. Para sobrescrever uma função, use a palavra-chave override
no contrato filho e virtual
no contrato pai.
Exemplo:
pragma solidity ^0.8.0;
contract Parent {
function sayHello() public virtual returns (string memory) {
return "Hello from Parent";
}
}
contract Child is Parent {
function sayHello() public override returns (string memory) {
return "Hello from Child";
}
}
Construtores
Construtores não são herdados diretamente, mas o contrato filho pode chamar o construtor do contrato pai utilizando a sintaxe de construtor de contrato pai. Isso é feito passando os argumentos necessários ao construtor do contrato pai na definição do construtor do contrato filho.
Exemplo:
pragma solidity ^0.8.0;
contract Parent {
uint public value;
constructor(uint _value) {
value = _value;
}
}
contract Child is Parent {
constructor(uint _value) Parent(_value) {
// Pode adicionar lógica adicional aqui
}
}
Modifiers
Modifiers também são herdados pelos contratos filhos. Assim como funções, modifiers podem ser sobrescritos usando as palavras-chave virtual
no contrato pai e override
no contrato filho.
Exemplo:
pragma solidity ^0.8.0;
contract Parent {
modifier onlyOwner() virtual {
require(msg.sender == owner, "Not the owner");
_;
}
address public owner;
constructor() {
owner = msg.sender;
}
}
contract Child is Parent {
modifier onlyOwner() override {
require(msg.sender == owner, "Not the owner");
_;
}
function restrictedFunction() public onlyOwner {
// Lógica da função restrita
}
}
Events
Eventos definidos em um contrato pai são herdados pelo contrato filho e podem ser emitidos tanto no contrato pai quanto no contrato filho.
Exemplo:
pragma solidity ^0.8.0;
contract Parent {
event ValueChanged(uint oldValue, uint newValue);
uint public value;
function setValue(uint _value) public {
emit ValueChanged(value, _value);
value = _value;
}
}
contract Child is Parent {
function updateValue(uint _value) public {
emit ValueChanged(value, _value); // Emitindo evento herdado
value = _value;
}
}
Resumo
Atributos (Variáveis de Estado): Herdados automaticamente, acessíveis a menos que
private
.Funções: Herdadas automaticamente, podem ser sobrescritas usando
virtual
eoverride
.Construtores: Não são herdados diretamente, mas podem ser chamados no construtor do contrato filho.
Modifiers: Herdados automaticamente, podem ser sobrescritos.
Events: Herdados automaticamente, podem ser emitidos tanto pelo contrato pai quanto pelo contrato filho.
Herança x struct x interface
A escolha entre structs, herança de contratos e interfaces depende do contexto específico e dos objetivos do seu contrato inteligente. Cada uma dessas ferramentas serve a diferentes propósitos. Aqui estão algumas diretrizes para ajudar a decidir quando usar structs, herança de contratos ou interfaces:
Structs
Quando usar structs:
Agrupamento de Dados: Utilize structs para agrupar múltiplos tipos de dados relacionados em uma única unidade lógica. Isso é útil quando você tem várias propriedades que descrevem uma entidade (por exemplo, um usuário, uma transação, um produto).
Organização de Dados Complexos: Quando você precisa armazenar dados complexos no estado do contrato, structs ajudam a manter a organização e clareza.
Simplificação de Funções: Structs podem ser usadas como parâmetros ou valores de retorno de funções para simplificar a passagem e manipulação de dados.
Exemplo de uso de struct:
pragma solidity ^0.8.0;
contract Marketplace {
struct Product {
uint id;
string name;
uint price;
address seller;
}
mapping(uint => Product) public products;
function addProduct(uint id, string memory name, uint price) public {
products[id] = Product(id, name, price, msg.sender);
}
function getProduct(uint id) public view returns (Product memory) {
return products[id];
}
}
Herança de Contratos
Quando usar herança:
Reutilização de Código: Utilize herança para reutilizar funcionalidades comuns entre diferentes contratos. Isso reduz a duplicação de código e facilita a manutenção.
Modularidade: Quando você tem funcionalidades que podem ser divididas em componentes menores e reutilizáveis. Contratos base podem fornecer funcionalidades comuns compartilhadas por contratos derivados.
Extensão e Personalização: Quando você precisa estender ou personalizar funcionalidades de um contrato existente sem modificar seu código original.
Uso de Contratos Base: Herança é útil para incorporar contratos base que fornecem funcionalidades padrão, como contratos de gerenciamento de propriedade (Ownable), pausabilidade (Pausable) ou controle de acesso (AccessControl).
Exemplo de uso de herança:
pragma solidity ^0.8.0;
contract Ownable {
address private _owner;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
constructor() {
_owner = msg.sender;
emit OwnershipTransferred(address(0), _owner);
}
modifier onlyOwner() {
require(_owner == msg.sender, "Ownable: caller is not the owner");
_;
}
function transferOwnership(address newOwner) public onlyOwner {
require(newOwner != address(0), "Ownable: new owner is the zero address");
emit OwnershipTransferred(_owner, newOwner);
_owner = newOwner;
}
}
contract MyContract is Ownable {
// MyContract herda as funcionalidades de Ownable
}
Interfaces
Quando usar interfaces:
Desacoplamento e Abstração: Use interfaces para definir um conjunto de funções que devem ser implementadas por outros contratos, sem impor uma implementação específica. Isso promove a modularidade e permite que diferentes contratos interajam de maneira padronizada.
Interoperabilidade: Quando você precisa interagir com contratos externos (por exemplo, padrões como ERC20, ERC721), interfaces permitem chamar funções desses contratos sem precisar da implementação completa.
Múltiplas Implementações: Se você espera ter várias implementações diferentes para um conjunto de funções, usar interfaces define uma estrutura comum que todas as implementações devem seguir.
Exemplo de uso de interface:
pragma solidity ^0.8.0;
interface IERC20 {
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address recipient, uint256 amount) external returns (bool);
// Outras funções ERC20...
}
contract MyToken is IERC20 {
mapping(address => uint256) private _balances;
uint256 private _totalSupply;
function totalSupply() external view override returns (uint256) {
return _totalSupply;
}
function balanceOf(address account) external view override returns (uint256) {
return _balances[account];
}
function transfer(address recipient, uint256 amount) external override returns (bool) {
require(_balances[msg.sender] >= amount, "Insufficient balance");
_balances[msg.sender] -= amount;
_balances[recipient] += amount;
return true;
}
// Implementação das outras funções ERC20...
}
Considerações
Structs são ideais para agrupar e organizar dados relacionados em uma unidade lógica, facilitando o gerenciamento e a manipulação de dados complexos.
Herança é útil para compartilhar lógica comum, modularizar funcionalidades e estender contratos existentes, promovendo reutilização e manutenção de código.
Interfaces são essenciais para definir contratos padrão e garantir interoperabilidade entre diferentes contratos, promovendo uma arquitetura desacoplada e padronizada.