Interfaces são como contratos que nossos objetos ou classes devem atender quando definimos este combinado. Por exemplo, suponha que você irá desenvolver uma aplicação que é um jogo de RPG.
Você irá criar classes que são Raças e personagens de RPG. Todas as raças e todos personagens deverão possuir um inventário de itens (objeto
), que nada mais é do que um array de objetos
. Portanto, podemos criar um combinado de como um Item deve ser:
// interfaces.ts
export interface Item {
name: string,
weight: number,
}
// Acima temos a Interface de um Item, ou seja, como o Objeto "Item" deve ser
Agora, para utilizarmos esta interface em uma classe podemos criar uma classe que utilizaremos como base para a criação de raças de RPG
/* Suponha que você está criando uma aplicação que é um jogo de RPG, e você
deve criar criou uma classe que representa uma raça de RPG */
// race.ts
import { Item } from "./interfaces";
export default class Race {
private _height: number;
protected _languages: string[];
// Aqui, definimos um atributo que deverá utilizar a interfacec criada
private _inventory: Item[];
constructor(
private _name: string,
minHeight: number = 0.6,
maxHeight: number = 2.1,
) {
this._height = this.getRandomHeight(min, max);
this._languages = ['westron'];
this._inventory = [{ name: 'rock', weight: 0.1 }];
}
get name() { return this._name; }
get height() { return this._height; }
get languages() { return this._languages; }
get inventory() { return this._inventory; }
// Aqui, fazemos a utilização do atributo criado utilizando a interface definida
public pickUpItem(item: Item): void {
this._inventory.push(item);
console.log(`Adicionado o item `, item.name);
}
private getRandomHeight(min: number, max: number) {
return parseFloat(
(Math.random() * (maxHeight - minHeight) + minHeight).toFixed(2)
);
}
public toString(): string {
return `${this._name} speaks ${this._languages} and has height ${this._height}`;
}
}
Em orientação a objeto, quando criamos uma classe, podemos herdar parâmetros e atributos dessa classe na criação de outra classe.
Utilizando como base a classe Race
criada anteriormente, podemos criar classes que herdam suas propriedades. Por exemplo, podemos criar a classe Hobbit
que é um tipo de “Raça” de RPG:
// hobbit.ts
import { Item } from "./interfaces";
import Race from "./race";
/* aqui utilizamoss a palavra mágica 'extends', e temos acesso a todos os
atributos e parâmetros que são públicos ou protected */
export default class Hobbit extends Race {
constructor(
/* Posso já criar os atributos direto no constructor sem a necessidade
de criá-los antes e chamá-los aqui dentro utilizando o 'this'*/
name: string,
private _stealth: number,
private _maxLoad: number,
) {
/* Eu defino o 'super' após o 'constructor', sendo que no constructor
eu possuo os atributos específicos de Hobbit, e no supoer possuo os
atributos e parãmetros da classe 'Race', neste caso, passando para a
classe 'Race' o parâmetro name recebido no constructor e as alturas
máximas e mínimas que um Hobbit pode ter */
super(name, 0.6, 1.2);
this._languages.push('hobbitês')
}
/* Aqui definimos um método público 'pickUpItem' para utilizar quando um
personagem da raça Hobbit pegar um item no jogo */
public pickUpItem(item: Item): void {
const currentLoad = this.inventory.reduce(
(acc, curr) => acc + curr.weight,
0,
);
if (currentLoad + item.weight <= this._maxLoad) {
/* aqui nós utilizamos o método pickUpItem da classe Race
passando o item adicionado como parâmetro */
super.pickUpItem(item);
} else {
console.log(`Inventory is full.`);
}
}
}
IMPORTANTE
se um atributo ou parâmetro na classe “pai” estiver com a restriçãoprotected
, na classe “filho” não poderá ter uma restriçãoprivate
, apenasprotected
oupublic
, as restrições sempre caminham no sentido de igual ou menos restrito.
No arquivo abaixo iremos chamar a classe Hobbit, criando uma nova instância dela:
// indedx.ts
import Hobbit from './hobbit';
// Criei o frodo, como sendo uma nova instância de Hobbit
const frodo = new Hobbit('Frodo', 1, 10);
Agora, suponha que tenhamos várias raças, como Hobbits, Elfos, Magos, Humanos, e para cada raça temos uma série de personagens, como ‘Frodo’, ‘Gandalf’, ‘Aragorn’, ‘Saurom’, etc, e queremos criar uma classe ‘Player’ que deverá utilizar algum personagem, e portanto, pertencer a alguma raça.
Como faremos isso?
A classe ‘Player’ deverá herdar algo de algum personagem que por sua vez irá herdar algo da classe Race?
💡 Isso parece funcionar, porém isso acaba gerando muita dependência no código pois teremos que duplicar várias classes, o que tornará muito difícil a sua manutenção. Por isso, devemos utilizar `Composição`Composição é uma forma, na modelagem de software, que reutiliza objetos sem a necessidade de duplicar as classes. Um ótimo artigo que detalhe melhor isto pode ser encontrado no link abaixo:
Herança ou Composição, eis a questão?
// interfaces.ts
/* Pegando o modelo de um personagem vamos montar um player e como regra, vamos
definir que o player deve respeitar/utilizar as interfaces IFinder e IFigther */
/* export interface Item {
name: string,
weight: number,
} */
// Interface que define que deve existir uma função de ataque
export interface IFigther {
attack(): void;
}
// Interface que define que deve existir uma função de pegar um item
export interface IFinder {
pickUpItem(item: Item): void;
}
Agora que criamos as interfaces, vamos utilizá-ls em uma composição para a criação de um ‘player’:
// player.ts
import { IFigther, IFinder, Item } from "./interfaces";
import Race from "./race";
/* O que dizemos aqui é que a classe Player deverá ter um atributo _player
que respeite a tipagem da interface IFinder - isso chama-se 'Generics' pois
o atributo _player pode receber uma classe ou objeto ou qualquer estrutura que
respeite o que foi definido pela interface IFinder, que no caso, é ter uma função
pickupItem que receba um Item no parâmetro e retorne void
Também estamos dizendo que esta classe deverá ter em sua estrutura, algo que
respeite as interfaces IFinder e IFighter, ou seja, ter as funções attack e
pickUpItem */
export default class Player<Character extends IFinder> implements IFinder, IFigther {
constructor(
private _hp: number,
private _mp: number,
// Character pode ser
private _player: Character,
) { }
/* O que temos abaixo é uma estrutura que respeitE a composição definida pelo
uso das interfaces IFinder e IFighter, tendo um método attack e pickUpItem */
attack(): void {
throw new Error("Method not implemented.");
}
pickUpItem(item: Item): void {
this._player.pickUpItem(item);
}
}
// Agora vamos criar um player
// indedx.ts
import Hobbit from './hobbit';
import Human from './Human';
// Criei o frodo, como sendo uma nova instância de Hobbit
const frodo = new Hobbit('Frodo', 1, 10);
// Agora criamos o Player
const playerOne = new Player<Hobbit>(100, 10, frodo);
/* Aqui acima, onde tipamos <Hobbit> poderia estar em branco, ou poderiamos
utilizar outra estrutura que tenha uma função pickUpItem, da seguinte forma */
/* Implicitamente ele reconhece que estpa sendo passado uma instância que
respeita a tipagem definida para o atributo _player da classe Player */
const playerOne = new Player(100, 10, frodo);
// ou
const playerOne = new Player<Race>(100, 10, frodo);
/* Suponha que utilizamos agora a classe Human e criassemos uma instância de
um personagem, ao utilizarmos esta classe */
const aragorn = new Human('Aragorn', 5, 20);
//Agora criamos o Player
const playerTwo = new Player<Human>(100, 10, aragorn);
/* Se utilizassemos Player<Human>(100, 10, frodo) receberiamos um erro, pois o
TypeScript identifica que frodo não é uma instância de Human, e sim de Hobbit
porém Player<Race>(100, 10, aragorn) ou Player<Race>(100, 10, frodo) não apresenta
erro, pois Race é pai de Human e Hobbit */
-
IMPORTANTE
EmTypeScript
,interfaces
conseguem estenderclasses
. A ideia de extensão de interface é realmente você puxar tudo o que tem naquela interface. Você não herda a implementação, você herda a declaração dos métodos. Ou seja, ao utilizarmos -Character extends IFinder
- Estamos falando que a classePlayer
vai ter um tipo ‘Character
’ que deverá ter todos os métodos e atributos que tem emIFinder
-
IMPORTANTE
Composição de interfaces é mais recomendado do que herança de interfaces// Ou seja, é melhor você fazer isto: export default class Xablau implements Xabley, Xablusca { ... } // Do que fazer herança de interfaces: Xablusca extends Xabley { ... } export default class Xablau implements Xablusca { ... }