Um dos princípios mais importantes da programação orientada a objetos – delimitar a interface interna da externa.
Essa é uma prática “obrigatória” no desenvolvimento de algo mais complexo do que um aplicativo “olá mundo”.
Para entender isso, vamos romper com o desenvolvimento e voltar nossos olhos para o mundo real.
Normalmente, os dispositivos que usamos são bastante complexos. Mas delimitar a interface interna da externa permite utilizá-las sem problemas.
Por exemplo, uma máquina de café. Simples por fora: um botão, um display, alguns furos…E, com certeza, o resultado – um ótimo café! :)
Mas por dentro… (uma foto do manual de reparo)
Muitos detalhes. Mas podemos usá-lo sem saber de nada.
As máquinas de café são bastante confiáveis, não são? Podemos usar um por anos, e somente se algo der errado – trazê-lo para conserto.
O segredo da confiabilidade e simplicidade de uma máquina de café – todos os detalhes estão bem ajustados e escondidos no interior.
Se retirarmos a tampa protetora da máquina de café, o seu uso será muito mais complexo (onde pressionar?) E perigoso (pode eletrocutar).
Como veremos, na programação os objetos são como máquinas de café.
Mas para ocultar detalhes internos, usaremos não uma capa protetora, mas sim uma sintaxe especial da linguagem e convenções.
Na programação orientada a objetos, propriedades e métodos são divididos em dois grupos:
Interface interna – métodos e propriedades, acessíveis a partir de outros métodos da classe, mas não externamente.
Interface externa – métodos e propriedades, acessíveis também de fora da classe.
Se continuarmos a analogia com a máquina de café – o que está escondido dentro: um tubo de caldeira, um elemento de aquecimento, e assim por diante – é a sua interface interna.
Uma interface interna é utilizada para o funcionamento do objeto, seus detalhes se utilizam. Por exemplo, um tubo de caldeira está ligado ao elemento de aquecimento.
Mas do lado de fora uma máquina de café é fechada pela capa protetora, para que ninguém possa alcançá-la. Os detalhes estão ocultos e inacessíveis. Podemos usar seus recursos através da interface externa.
Então, tudo que precisamos para usar um objeto é conhecer sua interface externa. Podemos não ter consciência de como isso funciona por dentro, e isso é ótimo.
Essa foi uma introdução geral.
Em JavaScript, existem dois tipos de campos de objeto (propriedades e métodos):
Público: acessível de qualquer lugar. Eles compõem a interface externa. Até agora estávamos usando apenas propriedades e métodos públicos.
Privado: acessível apenas de dentro da classe. Estes são para a interface interna.
Em muitas outras linguagens também existem campos “protegidos”: acessíveis apenas de dentro da classe e daqueles que a estendem (como private, mas com acesso adicional de classes herdadas). Eles também são úteis para a interface interna. De certa forma, eles são mais difundidos que os privados, porque geralmente queremos que classes herdadas tenham acesso a eles.
Os campos protegidos não são implementados em JavaScript no nível da linguagem, mas na prática são muito convenientes, por isso são emulados.
Agora faremos uma máquina de café em JavaScript com todos esses tipos de propriedades. Uma máquina de café tem muitos detalhes, não vamos modelá-los para permanecerem simples (embora pudéssemos).
Vamos primeiro fazer uma aula simples de máquina de café:
class Máquina de Café { quantidade de água = 0; // a quantidade de água dentro construtor(potência) { este.poder = poder; alert( `Criada uma máquina de café, power: ${power}` ); } } //cria a máquina de café deixe CoffeeMachine = new CoffeeMachine(100); //adiciona água caféMachine.waterAmount = 200;
Neste momento as propriedades waterAmount
e power
são públicas. Podemos facilmente obtê-los/defini-los externamente para qualquer valor.
Vamos mudar a propriedade waterAmount
para protected para ter mais controle sobre ela. Por exemplo, não queremos que ninguém o defina abaixo de zero.
As propriedades protegidas geralmente são prefixadas com um sublinhado _
.
Isso não é imposto no nível da linguagem, mas existe uma convenção bem conhecida entre os programadores de que tais propriedades e métodos não devem ser acessados externamente.
Portanto, nossa propriedade se chamará _waterAmount
:
class Máquina de Café { _waterAmount = 0; definir quantidade de água(valor) { se (valor <0) { valor = 0; } this._waterAmount = valor; } obterwaterAmount() { retorne isto._waterAmount; } construtor(potência) { this._power = poder; } } //cria a máquina de café deixe CoffeeMachine = new CoffeeMachine(100); //adiciona água caféMachine.waterAmount = -10; // _waterAmount se tornará 0, não -10
Agora que o acesso está sob controle, torna-se impossível definir a quantidade de água abaixo de zero.
Para propriedade power
, vamos torná-la somente leitura. Às vezes acontece que uma propriedade deve ser definida apenas no momento da criação e nunca modificada.
É exactamente o caso de uma máquina de café: a potência nunca muda.
Para fazer isso, precisamos apenas criar o getter, mas não o setter:
class Máquina de Café { // ... construtor(potência) { this._power = poder; } obter poder() { retorne isto._power; } } //cria a máquina de café deixe CoffeeMachine = new CoffeeMachine(100); alert(`A potência é: ${coffeeMachine.power}W`); //A potência é: 100W caféMachine.power = 25; // Erro (sem setter)
Funções getter/setter
Aqui usamos a sintaxe getter/setter.
Mas na maioria das vezes as funções get.../set...
são preferidas, assim:
class Máquina de Café { _waterAmount = 0; setQuantidadeÁgua(valor) { se (valor < 0) valor = 0; this._waterAmount = valor; } getWaterAmount() { retorne isto._waterAmount; } } new CoffeeMachine().setWaterAmount(100);
Parece um pouco mais longo, mas as funções são mais flexíveis. Eles podem aceitar vários argumentos (mesmo que não precisemos deles agora).
Por outro lado, a sintaxe get/set é mais curta, portanto, em última análise, não há uma regra estrita, cabe a você decidir.
Os campos protegidos são herdados
Se herdarmos class MegaMachine extends CoffeeMachine
, então nada nos impede de acessar this._waterAmount
ou this._power
a partir dos métodos da nova classe.
Portanto, os campos protegidos são naturalmente herdáveis. Ao contrário dos privados que veremos a seguir.
Uma adição recente
Esta é uma adição recente ao idioma. Não suportado em motores JavaScript, ou ainda parcialmente suportado, requer polyfilling.
Há uma proposta JavaScript finalizada, quase no padrão, que fornece suporte em nível de linguagem para propriedades e métodos privados.
Os particulares devem começar com #
. Eles só são acessíveis de dentro da classe.
Por exemplo, aqui está uma propriedade privada #waterLimit
e o método privado de verificação de água #fixWaterAmount
:
class Máquina de Café { #waterLimit = 200; #fixÁguaQuantidade(valor) { se (valor < 0) retornar 0; if (valor > this.#waterLimit) return this.#waterLimit; } setQuantidadeÁgua(valor) { this.#waterLimit = this.#fixWaterAmount(valor); } } deixe CoffeeMachine = new CoffeeMachine(); //não é possível acessar privates de fora da classe caféMáquina.#fixWaterAmount(123); // Erro caféMachine.#waterLimit = 1000; // Erro
No nível do idioma, #
é um sinal especial de que o campo é privado. Não podemos acessá-lo de fora ou herdando classes.
Os campos privados não entram em conflito com os públicos. Podemos ter campos privados #waterAmount
e públicos waterAmount
ao mesmo tempo.
Por exemplo, vamos tornar waterAmount
um acessador para #waterAmount
:
class Máquina de Café { #waterAmount = 0; obterwaterAmount() { retorne isto.#waterAmount; } definir quantidade de água(valor) { se (valor < 0) valor = 0; this.#waterAmount = valor; } } deixe máquina = new CoffeeMachine(); máquina.waterAmount = 100; alerta(máquina.#waterAmount); // Erro
Ao contrário dos protegidos, os campos privados são impostos pela própria linguagem. Isso é uma coisa boa.
Mas se herdarmos de CoffeeMachine
, não teremos acesso direto a #waterAmount
. Precisaremos contar com o getter/setter waterAmount
:
classe MegaCoffeeMachine estende CoffeeMachine { método() { alerta( this.#waterAmount ); // Erro: só é possível acessar do CoffeeMachine } }
Em muitos cenários, essa limitação é demasiado severa. Se estendermos uma CoffeeMachine
, poderemos ter motivos legítimos para acessar seus componentes internos. É por isso que os campos protegidos são usados com mais frequência, mesmo que não sejam suportados pela sintaxe da linguagem.
Os campos privados não estão disponíveis como este[nome]
Os campos privados são especiais.
Como sabemos, normalmente podemos acessar campos usando this[name]
:
classe Usuário { ... digaOi() { deixe fieldName = "nome"; alert(`Olá, ${este[nomedocampo]}`); } }
Com campos privados isso é impossível: this['#name']
não funciona. Essa é uma limitação de sintaxe para garantir a privacidade.
Em termos de OOP, a delimitação da interface interna da externa é chamada de encapsulamento.
Oferece os seguintes benefícios:
Proteção aos usuários, para que não dêem um tiro no pé
Imagine, há uma equipe de desenvolvedores usando uma máquina de café. Foi fabricado pela empresa “Best CoffeeMachine” e funciona bem, mas a capa protetora foi removida. Portanto, a interface interna fica exposta.
Todos os desenvolvedores são civilizados – eles usam a máquina de café conforme pretendido. Mas um deles, John, decidiu que ele era o mais inteligente e fez alguns ajustes na parte interna da máquina de café. Então a máquina de café falhou dois dias depois.
Certamente não é culpa de John, mas sim da pessoa que removeu a capa protetora e deixou John fazer suas manipulações.
O mesmo na programação. Se um usuário de uma classe mudar coisas que não deveriam ser alteradas externamente – as consequências são imprevisíveis.
Suportável
A situação na programação é mais complexa do que com uma máquina de café real, porque não a compramos apenas uma vez. O código passa constantemente por desenvolvimento e aprimoramento.
Se delimitarmos estritamente a interface interna, o desenvolvedor da classe poderá alterar livremente suas propriedades e métodos internos, mesmo sem informar os usuários.
Se você é desenvolvedor dessa classe, é ótimo saber que métodos privados podem ser renomeados com segurança, seus parâmetros podem ser alterados e até mesmo removidos, pois nenhum código externo depende deles.
Para os usuários, quando uma nova versão sai, pode ser uma reformulação total internamente, mas ainda assim simples de atualizar se a interface externa for a mesma.
Escondendo complexidade
As pessoas adoram usar coisas simples. Pelo menos de fora. O que está dentro é uma coisa diferente.
Os programadores não são uma exceção.
É sempre conveniente quando os detalhes da implementação estão ocultos e uma interface externa simples e bem documentada está disponível.
Para ocultar uma interface interna usamos propriedades protegidas ou privadas:
Os campos protegidos começam com _
. Essa é uma convenção bem conhecida, não aplicada no nível do idioma. Os programadores só devem acessar um campo começando com _
de sua classe e das classes herdadas dela.
Os campos privados começam com #
. JavaScript garante que só possamos acessá-los de dentro da classe.
No momento, os campos privados não são bem suportados pelos navegadores, mas podem ser preenchidos com polyfill.