Skip to content

Instantly share code, notes, and snippets.

@Fraitz
Created April 25, 2022 05:17
Show Gist options
  • Save Fraitz/9800bb2407558249ed457865dc8461d3 to your computer and use it in GitHub Desktop.
Save Fraitz/9800bb2407558249ed457865dc8461d3 to your computer and use it in GitHub Desktop.
Gist que resume a abordagem de Herança, Composição e Interface, utilizando generics em JavaScript

Interface

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}`;
    }
} 

Herança

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ção protected, na classe “filho” não poderá ter uma restrição private, apenas protected ou public, 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);

Composição

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 */

Generics:

  • IMPORTANTE Em TypeScript, interfaces conseguem estender classes. 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 classe Player vai ter um tipo ‘Character’ que deverá ter todos os métodos e atributos que tem em IFinder

  • 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 { ... }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment