Nesse artigo vamos ver a teoria por trás do Princípio da Segregação de Interface (Interface Segregation Principle), ou simplesmente ISP, e como representar esse conceito em aplicações JavaScript.

Sua definição formal diz:

Uma classe não deve ser forçada a implementar métodos que não irá usar.

Exemplo de violação

Abaixo temos um exemplo que viola o ISP:

Violação do ISP

A interface ISmartDevice define os métodos print e scan, e a classe Printer a implementa, dessa forma, toda classe que herdar de Printer deve implementar os dois métodos.

Simples, não? O problema é que a classe Economic **deve ter apenas o método print e dessa forma forçamos a implementação do método scan, violando o ISP**.

Resolvendo a violação do ISP

Aqui temos um exemplo onde aplicamos o ISP, dividindo ISmartDevice em duas interfaces menores: IPrinter, e IScanner. Desta forma, temos um código mais desacoplado e fácil de manter, podendo implementar classes que não precisam de todas as funcionalidades.

Resolvendo a violação do ISP

Até aí tudo bem, mas como vamos implementar isso em JavaScript se a linguagem não suporta interfaces? Vamos parar um pouco para pensar, o que é uma interface?

Interface é um contrato que força uma classe ou um objeto (no caso do JavaScript) a implementar uma funcionalidade especifica.

E na prática, como funciona?

Realmente não existe uma implementação concreta de interfaces em JavaScript, mas isso não nos impede de criar um “contrato” que se comporte dessa maneira. Vejamos o seguinte código:

class Printer {
    constructor(name) {
        this.name = name;
    }
}

class AllInOne extends Printer {
    constructor(name) {
        super(name);
    }

    scan() {
        return 'scanning...';
    }
}

function printerFunctionalities(printers) {
    console.log('Impressoras digitalizando: ');
    for (let printer of printers) {
        console.log(`${printer.name} is ${printer.scan()}`);
    }
}

let printers = [
    new AllInOne('HP 1'),
    new AllInOne('HP 2'),
];

printerFunctionalities(printers);

Aqui temos uma classe Printer e uma subclasse AllInOne que implementa o método scan. Temos também uma função chamada printerFunctionalities que percorre uma lista de instâncias de AllInOne ****chamando o método scan de cada um dos itens da lista.

Mas temos um problema, e se criarmos uma classe Economic que não deve ter o método scan?

class Economic extends Printer {
    constructor(name) {
        super(name);
    }
}

Se adicionarmos uma instância de Economic dentro do array o código retornará um erro, e é óbvio que é porque não implementamos o método scan dentro da classe Economic.

let printers = [
    new AllInOne('HP 1'),
    new AllInOne('HP 2'),
    new Economic('HP 3'),
];

De fato, JavaScript não tem uma implementação de interfaces, mas nesse código criamos um “contrato” que nos força a implementar o método scan.

Poderíamos acabar com esse erro implementando o método scan na classe Economic, mas não é isso que queremos, pois desta forma estaríamos violando o ISP. Então, para resolver esse problema de uma maneira simples que nos trará o resultado esperado. Vamos adicionar uma condição que verifica a existência do método em cada uma das instâncias.

function printerFunctionalities(printers) {
    console.log('Impressoras digitalizando: ');
    for (let printer of printers) {
        if (printer.scan) {
            console.log(`${printer.name} is ${printer.scan()}`);
        }
    }
}

Dessa forma tornamos o método scan opcional, não forçando classes concretas a implementarem métodos desnecessários, evitamos um aumento no acoplamento e deixamos nossas classes mais coesas.

Conclusão

O fato de JavaScript não possuir interfaces faz com que esse princípio não se aplique estritamente como os outros. Entretanto, é importante lembrar que interfaces são “contratos” e podemos implementá-los de forma implícita em JavaScript.

Espero que estejam gostando da série, se tiverem dúvidas ou sugestões não deixem de comentar. Abraço!