Buscar

Revisão de JavaScript POO

Prévia do material em texto

Revisão de JavaScript P.O.O
Terminamos o vídeo anterior com a ideia de criarmos um molde com o qual conseguiríamos obter uma réplica parecida ou idêntica de um conceito, e agora queremos aplicá-la ao nosso projeto em JavaScript. Em programação, esse tipo de molde é chamado de "classe". É possível declarar classes em JavaScript usando a palavra reservada class seguida do nome desejado, no caso Cliente.
É importante escolhermos um nome adequado, pois estamos tratando do contexto do nosso negócio. Por exemplo, o banco Bytebank precisa de uma representação de um cliente, portanto nada mais adequado do que utilizarmos esse nome para a classe. Em seguida, para começarmos a definição da classe, abriremos e fecharemos chaves ({}).
class Cliente {
}COPIAR CÓDIGO
É entre essas chaves que definiremos os atributos (também chamados de "campos" ou "propriedades") que a nossa classe irá conter, que serão nome, cpf, agencia e saldo.
Em algumas linguagens, as palavras "atributos", "campos" e "propriedades" podem não ser sinônimas como são no JavaScript.
class Cliente {
 nome;
 cpf;
 agencia;
 saldo;
}COPIAR CÓDIGO
Com esses atributos, estamos definindo que todo cliente possuirá um nome, um CPF, uma agência e um saldo, uniformizando esse contexto no sistema. Agora que temos o molde, precisamos aprender a utilizá-lo. Se queremos criar um novo cliente a partir da classe que foi definida, usamos a instrução new seguida do nome da classe e da abertura e fechamento de parênteses - ou seja, new Cliente().
class Cliente {
 nome;
 cpf;
 agencia;
 saldo;
}
new Cliente();COPIAR CÓDIGO
Para que consigamos acessar o cliente criado, teremos que atribuí-lo a uma variável, por exemplo cliente1.
class Cliente {
 nome;
 cpf;
 agencia;
 saldo;
}
const cliente1 = new Cliente();COPIAR CÓDIGO
Com isso, conseguimos um cliente1 que, em si, contém as informações necessárias para cada cliente, podendo ser referenciado ou alterado diretamente, e evitamos a fragmentação desses dados em contextos diferentes (como era o caso anterior).
Agora, se quisermos acessar alguma propriedade da classe, podemos fazê-lo usando a sintaxe objeto.atributo, como cliente1.nome ou cliente1.cpf. Faremos isso para atribuir informações a cada um dos atributos de cliente1, usando os mesmos dados que já tínhamos disponíveis. Em seguida, faremos um console.log(cliente1) para imprimirmos todas as informações desse objeto.
class Cliente {
 nome;
 cpf;
 agencia;
 saldo;
}
const cliente1 = new Cliente();
cliente1.nome = "Ricardo";
cliente1.cpf = 11122233309;
cliente1.agencia = 1001;
cliente1.saldo = 0;
console.log(cliente1);COPIAR CÓDIGO
Ao executarmos node ./index.js no terminal, teremos:
Cliente { nome: 'Ricardo', cpf: 11122233309, agencia: 1001, saldo: 0 }
Assim, conseguimos obter o nosso cliente como um todo, sem termos que imprimir cada campo individualmente. Passaremos para a criação de cliente2 repetindo os passos anteriores.
Como temos uma classe criada, o Intellisense do Visual Studio Code (uma espécie de autocompletar) nos sugerirá os atributos dela quando estivermos fazendo o preenchimento, por exemplo de saldo ao digitarmos cliente2.s.
Para imprimirmos as informações do novo cliente, não é necessário fazermos um novo console.log(), bastando passarmos cliente2 após uma vírgula.
class Cliente {
 nome;
 cpf;
 agencia;
 saldo;
}
const cliente1 = new Cliente();
const cliente2 = new Cliente();
cliente1.nome = "Ricardo";
cliente1.cpf = 11122233309;
cliente1.agencia = 1001;
cliente1.saldo = 0;
cliente2.nome = "Alice";
cliente2.cpf = 88822233309;
cliente2.agencia = 1001;
cliente2.saldo = 0;
console.log(cliente1, cliente2);COPIAR CÓDIGO
Ao executarmos, conseguiremos exibir ambos os clientes, "Ricardo" e "Alice".
Cliente { nome: 'Ricardo', cpf: 11122233309, agencia: 1001, saldo: 0 } Cliente { nome: 'Alice', cpf: 88822233309, agencia: 1001, saldo: 0 }
Até o momento as vantagens de estruturarmos o código dessa forma podem não parecer óbvias, já que o processo continua ligeiramente semelhante ao anterior. Entretanto, conforme avançarmos, você perceberá que a criação de classes nos traz diversas facilidades.
Por exemplo, se quisermos adicionar um número de RG aos nossos clientes, podemos simplesmente criar um atributo rg em Cliente.
class Cliente {
 nome;
 cpf;
 agencia;
 saldo;
 rg;
}COPIAR CÓDIGO
Em seguida, poderemos atribuir um valor a esse atributo da mesma forma que fizemos com as demais informações.
cliente1.nome = "Ricardo";
cliente1.cpf = 11122233309;
cliente1.agencia = 1001;
cliente1.saldo = 0;
cliente1.rg = 123456789;COPIAR CÓDIGO
Ao executarmos, o RG passará a ser exibido no objeto:
Cliente { nome: 'Ricardo', cpf: 11122233309, agencia: 1001, saldo: 0, rg: 123456789 } Cliente { nome: 'Alice', cpf: 88822233309, agencia: 1001, saldo: 0, rg: undefined }
Como não fizemos essa definição em cliente2, ele é mostrado como undefined ("indefinido"), ainda que a propriedade exista. Cada dos clientes que criamos a partir da instrução new Cliente() (no caso, cliente1 e cliente2) são chamados de "objetos", e daí vem o nome "programação orientada a objetos".
Em resumo, uma classe é o molde que define o resultado que queremos obter, e os objetos são criados a partir desse molde.
Quando usaremos a chamada new Cliente(), podemos afirmar que estamos criando uma "instância" dessa classe.
Removendo o atributo rg e sua definição em cliente1, nosso console.log() deixará de exibir tal informação.
class Cliente {
 nome;
 cpf;
 agencia;
 saldo;
}
const cliente1 = new Cliente();
const cliente2 = new Cliente();
cliente1.nome = "Ricardo";
cliente1.cpf = 11122233309;
cliente1.agencia = 1001;
cliente1.saldo = 0;
//...código omitido
console.log(cliente1, cliente2);COPIAR CÓDIGO
Cliente { nome: 'Ricardo', cpf: 11122233309, agencia: 1001, saldo: 0 } Cliente { nome: 'Alice', cpf: 88822233309, agencia: 1001, saldo: 0 }
Ou seja, uma alteração realizada somente na classe será repercutida por todo o nosso projeto!
Vimos que a criação de uma classe pode ser feita através da palavra-chave class e com isso definimos quais atributos queremos que essa classe possua.
		Classes definem uma forma de organizarmos uma série de informações repetidas no nosso código e por isso devemos usar elas quando temos código que se repete e que faz parte de um contexto coeso.
Exatamente. Se tivermos código que se repete porém não forma um contexto coeso, provavelmente teremos que criar duas classes, cada uma delimitando um contexto único que faça sentido.
O que foi estudado até aqui:
· Criando classes
· O que são atributos
· Instâcia/Objeto
· Operador new
Comportamentos de classes
Tendo criado a estrutura da classe Cliente, uma boa prática que nos permite, dentre outras facilidades, fazer alterações em um único ponto e tê-las refletidas em todas as instâncias daquela entidade, vamos praticar um pouco mais a criação de classes criando também a estrutura de uma conta-corrente, algo que também faz parte do sistema que o Bytebank nos contratou para desenvolver.
Termos como "atributo", "instância", "objeto" e assim por diante fazem parte do vocabulário da orientação a objetos, e é importante se familiarizar com eles. Caso tenha dúvidas, pode retornar às explicações nos vídeos anteriores ou buscar auxílio em nosso fórum!
Ainda no arquivo index.js, criaremos a classe ContaCorrente que usaremos para representar uma conta-corrente. É importante frisarmos o uso da palavra representa, já que a classe não é, em si, uma conta-corrente, mas sim a estrutura que desejamos que as contas-correntes tenham.
Alguns atributos que podem ser associados a uma conta-corrente são "agência" e "saldo". Entretanto, nós utilizamos esses campos no Cliente, o que agora parece ser bastante inadequado - afinal, um cliente possui uma conta-corrente, e esta, por sua vez, é quem possui uma agência e um saldo. Esse tipo de lógica é comum na estruturação de um sistema, sempre pensando em como podemos melhorar e readaptar o nosso código.
Feita essa reflexão,criaremos os atributos agencia e saldo na classe ContaCorrente e, como não queremos tê-los duplicados, os removeremos da classe Cliente.
class Cliente {
 nome;
 cpf;
}
class ContaCorrente {
 agencia;
 saldo;
}COPIAR CÓDIGO
À medida em que desenvolvemos o código, estamos abstraindo as informações e as estruturas de acordo com nosso raciocínio acerca do problema que estamos tentando resolver.
Antes de continuarmos, vamos reorganizar o nosso código removendo as atribuições de valores às propriedades agencia e saldo de objetos da classe Cliente, movendo a criação do objeto cliente2 para deixá-lo mais próximo das atribuições dos seus valores, removendo todo o código referente a cliente3 e separando a chamada de console.log() em duas, uma para cada cliente.
class Cliente {
 nome;
 cpf;
}
class ContaCorrente {
 agencia;
 saldo;
}
const cliente1 = new Cliente();
cliente1.nome = "Ricardo";
cliente1.cpf = 11122233309;
const cliente2 = new Cliente();
cliente2.nome = "Alice";
cliente2.cpf = 88822233309;
console.log(cliente1);
console.log(cliente2);COPIAR CÓDIGO
Passaremos então para a instanciação de uma nova ContaCorrente, que atribuiremos a uma variável contaCorrenteRicardo. Em seguida, usaremos contaCorrenteRicardo.saldo e contaCorrenteRicardo.agencia para definirmos os valores 0 e 1001 a esses atributos, respectivamente.
Lembre-se que o nome das variáveis é importante para identificar com o que estamos trabalhando. Por exemplo, contaCorrenteRicardo nos informa, especificamente, que essa é uma instância de ContaCorrente pertencente ao cliente1.
class Cliente {
 nome;
 cpf;
}
class ContaCorrente {
 agencia;
 saldo;
}
//...código omitido
const contaCorrenteRicardo = new ContaCorrente();
contaCorrenteRicardo.saldo = 0
contaCorrenteRicardo.agencia = 1001;
console.log(cliente1);
console.log(cliente2);COPIAR CÓDIGO
Sabemos que existem algumas operações comuns que são realizadas em contas-correntes, como saques, depósitos e transferências. Já temos uma conta-corrente com um saldo e agora queremos fazer um depósito. Começaremos esse processo fazendo um console.log() somente do saldo de contaCorrenteRicardo, de modo a verificarmos se ele realmente é 0.
const contaCorrenteRicardo = new ContaCorrente();
contaCorrenteRicardo.saldo = 0
contaCorrenteRicardo.agencia = 1001;
console.log(contaCorrenteRicardo.saldo);COPIAR CÓDIGO
Em seguida, faremos uma nova atribuição em contaCorrenteRicardo.saldo, dessa vez no valor 100, representando o depósito dessa quantia. Por fim, repetiremos o console.log(contaCorrenteRicardo.saldo).
const contaCorrenteRicardo = new ContaCorrente();
contaCorrenteRicardo.saldo = 0
contaCorrenteRicardo.agencia = 1001;
console.log(contaCorrenteRicardo.saldo);
contaCorrenteRicardo.saldo = 100;
console.log(contaCorrenteRicardo.saldo);COPIAR CÓDIGO
Ao executarmos nosso arquivo no terminal com node ./index.js, teremos:
0 100 Cliente { nome: 'Ricardo', cpf: 11122233309 } Cliente { nome: 'Alice', cpf: 88822233309 }
Como verificado, antes tínhamos um saldo zerado e, após o depósito, o valor na contaCorrenteRicardo passou a ser 100. Além disso, nossos clientes passaram a ter somente os atributos nome e cpf.
Entretanto, essa operação não parece correta. Imagine, por exemplo, que queremos fazer um saque. Para isso, criaremos uma variável valorSacado com o valor 200 e subtrairemos esse valor de contaCorrenteRicardo. Em seguida, faremos outro console.log() para obtermos o novo valor.
const contaCorrenteRicardo = new ContaCorrente();
contaCorrenteRicardo.saldo = 0
contaCorrenteRicardo.agencia = 1001;
console.log(contaCorrenteRicardo.saldo);
contaCorrenteRicardo.saldo = 100;
console.log(contaCorrenteRicardo.saldo);
let valorSacado = 200;
contaCorrenteRicardo.saldo -= valorSacado;
console.log(contaCorrenteRicardo.saldo);COPIAR CÓDIGO
Dessa vez nosso retorno será:
0 100 -100 Cliente { nome: 'Ricardo', cpf: 11122233309 } Cliente { nome: 'Alice', cpf: 88822233309 }
Note que agora temos um problema: o saldo resultante é negativo, algo que não faz sentido segundo a regra de negócios do Bytebank, que não quer fornecer crédito para um cliente que acabou de abrir uma conta no banco. Uma maneira de resolvermos tal situação é incluirmos uma condicional (if) verificando se contaCorrenteRicardo.saldo é maior do que valorSacado. Em caso positivo - ou seja, se existir saldo suficiente para o saque -, faremos a operação normalmente; do contrário, nada acontecerá.
let valorSacado = 200;
if (contaCorrenteRicardo.saldo >= valorSacado) {
 contaCorrenteRicardo.saldo -= valorSacado;
} 
console.log(contaCorrenteRicardo.saldo);COPIAR CÓDIGO
Ao executarmos, teremos:
0 100 100
O que significa que a operação de saque não foi realizada, assim como desejávamos. Entretanto, se fizermos todas as operações de saque dessa maneira, teremos que repetir código toda vez que uma nova operação for realizada.
Além disso, precisamos analisar todo um trecho de código para entendermos o que está acontecendo, sem que exista uma palavra específica que resuma a operação a ser realizada - nesse caso, um saque. Será possível tornar nosso código mais legível?
As classes em JavaScript não servem somente como pacotes que contém os dados de uma entidade, como uma conta-corrente ou cliente, e também podem possuir comportamentos. Para isso, podemos incluir nelas uma nova estrutura: as funções (ou métodos, nome comum em orientação a objetos).
Nesse caso, queremos criar um comportamento chamado "sacar". Como esse comportamento deverá ser executando passando alguns valores, será necessário que a palavra seja sucedida por parênteses, resultando em sacar(). Os parênteses denotam que estamos trabalhando com uma operação executável, e não com uma variável, e é dentro deles que podemos passar valores. O método sacar(), por exemplo, receberá um valor.
Queremos que o método sacar() tenha um comportamento semelhante à operação que estávamos fazendo anteriormente. Pensando nisso, moveremos aquele trecho de código para cima de modo a analisá-lo. Para definirmos os comportamentos de um método, usamos as chaves ({}) para delimitar o que chamamos de "escopo do método".
class ContaCorrente {
 agencia;
 saldo;
 sacar(valor) {
 }
}
let valorSacado = 200;
if (contaCorrenteRicardo.saldo >= valorSacado) {
 contaCorrenteRicardo.saldo -= valorSacado;
} 
console.log(contaCorrenteRicardo.saldo);COPIAR CÓDIGO
Podemos então criar uma condicional que verifica se contaCorrenteRicardo.saldo é maior ou igual ao valor recebido por parâmetro. Em caso positivo, a operação será feita normalmente.
class ContaCorrente {
 agencia;
 saldo;
 sacar(valor) {
 if (contaCorrenteRicardo.saldo >= valor) {
 contaCorrenteRicardo.saldo -= valor;
 }
 }
}COPIAR CÓDIGO
Entretanto, sabemos que a classe ContaCorrente é somente um molde utilizado para criação de qualquer conta, seja ela do Ricardo, da Alice e assim por diante. Ou seja, não faz sentido utilizarmos contaCorrenteRicardo, já que estamos acessando um objeto que, nesse ponto, nem mesmo existe no sistema.
Queremos, na verdade, acessar o saldo da própria conta-corrente a partir da qual chamamos o método, ou seja, "esta" conta. Para isso, usamos a palavra reservada this ("este(a)" em inglês).
class ContaCorrente {
 agencia;
 saldo;
 sacar(valor) {
 if (this.saldo >= valor) {
 this.saldo -= valor;
 }
 }
}COPIAR CÓDIGO
Assim, verificaremos e o saldo da conta-corrente é maior que o valor passado e, em caso positivo, subtrairemos esse valor do saldo desta mesma conta-corrente. A essa lógica atribuímos o nome de sacar().
Para testarmos, manteremos a operação de depósito no valor de 100 em contaCorrenteRicardo. Em seguida, chamaremos contaCorrenteRicardo.sacar() passando 50 como parâmetro e faremos um console.log() do novo saldo da conta.
const contaCorrenteRicardo = new ContaCorrente();
contaCorrenteRicardo.saldo = 0
contaCorrenteRicardo.agencia = 1001;
console.log(contaCorrenteRicardo.saldo);
contaCorrenteRicardo.saldo= 100;
console.log(contaCorrenteRicardo.saldo);
contaCorrenteRicardo.sacar(50);
console.log(contaCorrenteRicardo.saldo);COPIAR CÓDIGO
Ao executarmos, teremos:
0 100 50
Com isso temos uma regra de negócios da operação de saque encapsulada dentro de um método/função, ao qual atribuímos o nome sacar(), algo que melhora muito a legibilidade do nosso sistema.
Atributos Privados
Agora que já temos o comportamento do método sacar() para a classe ContaCorrente, vamos refletir a respeito do saldo.
Definimos anteriormente, de forma manual, que ao criarmos uma nova conta-corrente ela deve ter saldo igual a 0. Para a operação de depósito, definimos que contaCorrenteRicardo.saldo deve ser igual a 100, na verdade devemos usar o sinal de mais (+), pois o valor deve ser adicionado à conta e não substituir o valor anterior. Então, precisamos inserir o sinal de mais (+) acompanhado do sinal de igual (=) antes do valor que queremos depositar. Seguiremos esse padrão para todos os depósitos a serem feitos, vamos atribuir à contaCorrenteRicardo.saldo o valor de 100, depois 200 e, por fim, -1, para testar o que acontece se, por acaso, inserirmos um número negativo para depósito.
console.log(contaCorrenteRicardo,saldo);
contaCorrenteRicardo.saldo += 100;
contaCorrenteRicardo.saldo += 200;
contaCorrenteRicardo.saldo += -1;
console.log(contaCorrenteRicardo.saldo);
contaCorrenteRicardo.sacar(50);COPIAR CÓDIGO
Ao executarmos nosso arquivo no terminal com node ./index.js, teremos:
0 299 249 Cliente { nome: 'Ricardo', cpf: 11122233309 } Cliente { nome: 'Alice', cpf: 88822233309 }
Entretanto, houve uma subtração no saldo, o que não deveria acontecer na operação de depósito que deve somente adicionar dinheiro ao saldo da conta-corrente. Para não que isso não aconteça, criaremos um comportamento para melhorar e proteger o atributo saldo, pois ele é importante para a conta-corrente e o cliente não ficará feliz se o valor do saldo mudar para algum valor aleatório sem justificativa. Para isso vamos criar, logo abaixo do método sacar(), um comportamento chamado "depositar". O método depositar() receberá valor como parâmetro, que refere-se ao valor que queremos depositar.
Lembre-se que tudo o que está entre parênteses após o nome do método são os parâmetros que ele recebe. Pode ser um ou mais parâmetros, neste caso, separados por vírgula. Alguns profissionais referem-se a eles como "argumentos", mas aqui chamaremos de parâmetros.
Abriremos as chaves ({}) para delimitar o escopo do método depositar() e criaremos uma condicional para verificar se valor é maior do que 0. Em caso positivo, a atualização de this.saldo será permitida. Com essa verificação a soma acontecerá somente se o valor depositado for positivo.
 depositar(valor){
 if(valor > 0){
 this.saldo += valor; 
 }
}COPIAR CÓDIGO
Agora, em vez de fazer a operação de depósito manualmente com o atributo saldo, podemos chamar o método depositar(). Vamos seguir a mesma ordem de depósito dos valores que usamos anteriormente 100, 200 e -1 para ver se a regra que inserimos com a condicional no método depositar() está funcionando.
console.log(contaCorrenteRicardo.saldo);
contaCorrenteRicardo.depositar(100);
contaCorrenteRicardo.depositar(200);
contaCorrenteRicardo.depositar(-1);
console.log(contaCorrenteRicardo.saldo);
contaCorrenteRicardo.sacar(50);COPIAR CÓDIGO
Vamos salvar o arquivo index.js e ir para a janela do terminal. Ao executarmos nosso arquivo com node ./index.js, teremos:
0 300 250 Cliente { nome: 'Ricardo', cpf: 11122233309 } Cliente { nome: 'Alice', cpf: 88822233309 }
Agora nosso saldo está com 300, ou seja, o depósito do valor negativo não foi aceito. Então não será mais retirado dinheiro da conta se, por acaso, alguém tentar depositar um valor negativo.
Antes de continuarmos, vamos limpar o código para melhorar sua legibilidade. Podemos apagar as chamadas de console.log para contaCorrenteRicardo.saldo, pois já vimos que está funcionando. Apagaremos também os depósitos no valor de 200 e -1, e também as chamadas de console.log para cliente1 e cliente2 pois não as usaremos agora. Deixaremos apenas uma chamada console.log para contaCorrenteRicardo:
const contaCorrenteRicardo = new ContaCorrente();
contaCorrenteRicardo.saldo = 0;
contaCorrenteRicardo.agencia = 1001;
contaCorrenteRicardo.depositar(100);
contaCorrenteRicardo.sacar(50);
console.log(contaCorrenteRicardo);COPIAR CÓDIGO
Ao executarmos nosso arquivo no terminal com node ./index.js, teremos:
ContaCorrente { agencia 1001, saldo: 50 }
Agora nosso código do arquivo index.js está mais fácil de ler. Logo abaixo da declaração de cliente1 e cliente2 está a declaração da contaCorrenteRicardo, onde podemos depositar e sacar dinheiro.
Ao criarmos operações e métodos para definir o comportamento de uma classe conseguimos estabelecer barreiras e proteções dentro do código para que ele funcione sempre de acordo com as regras de negócio passadas pelo cliente.
Entretanto, agora que temos o método sacar() e depositar(), ainda faz sentido conseguirmos mexer diretamente no saldo, como por exemplo, para definirmos o valor de 10000 para contaCorrenteRicardo.saldo?
const contaCorrenteRicardo = new ContaCorrente();
contaCorrenteRicardo.saldo = 10000;
contaCorrenteRicardo.agencia = 1001;
contaCorrenteRicardo.depositar(100);
contaCorrenteRicardo.sacar(50);
console.log(contaCorrenteRicardo);COPIAR CÓDIGO
Ao executarmos nosso arquivo no terminal com node ./index.js, teremos:
ContaCorrente { agencia 1001, saldo: 10050 }
O problema é que, como o atributo saldo está exposto para qualquer pessoa com acesso ao código alterá-lo, podemos cair nos mesmos erros que existiam antes da criação de comportamentos do método depositar() e sacar(). Com a possibilidade de alterar o saldo sem nenhuma verificação, nosso sistema está sujeito a erros. Como fazer para proteger esse atributo e fazer com que ele seja alterado somente dentro da classe a que ele pertence?
Na verdade, ainda não existe um procedimento formal para isso na linguagem JavaScript. O que existe, hoje, é apenas uma proposta de como isso poderia funcionar. Essa proposta, escrita em inglês, foi feita no GitHub e está acessível neste link que também será disponibilizado para você na próxima atividade Proposta de campos Privados.
Essa proposta, que ainda não foi oficialmente implementada, tem como objetivo mudar a maneira como declaramos campos - os atributos - dentro das classes. Na proposta, há diversos exemplos explicando que é possível criar campos privados que só podem ser alterados dentro da classe a que pertencem, fazendo com que ninguém de fora da classe possa acessá-lo. A proposta de implementação apresentada é colocar uma cerquilha (#) à frente do atributo e assim defini-lo como campo privado.
Como essa proposta começou a ser discutida em 2016 e está no estágio três, prestes a ser aprovada, algumas bibliotecas, frameworks e ferramentas relacionadas ao JavaScript já implementaram essa proposta para a comunidade testá-la. Como por exemplo, o Node que a implementou em sua versão 12. Em nosso terminal, com o comando node -v podemos verificar qual versão do Node estamos utilizando. Como eu estou com a versão 13.5.0 é possível testar essa proposta em nosso código.
Lembrando que essa proposta ainda não foi oficializada, mas já conseguimos testar essa possibilidade. Talvez não valha a pena usá-la se colocarmos em produção, mas depois veremos como fazer isso nas convenções atuais aceitas pela comunidade como padrão da linguagem JavaScript. Colocaremos uma cerquilha (#) à frente do atributo saldo em todas as vez em que citamos esse atributo. Agora o nome da variável mudou para #saldo. Então this.#saldo significa que esse saldo é privado, mas como sacar() e depositar() estão dentro da classe ContaCorrente, é possível fazer alterações do saldo com esses métodos.
Como a máquina que eu estou usando tem o TypeScript instalado, o Visual Studio Code vai indicar que a cerquilha é um caractere inválido, mas podemos desconsiderar isso porque essa sintaxe não é de TypeScript.Vamos para a janela do terminal. Limparemos a tela com o comando clear e, em seguida, ao executarmos nosso arquivo com node ./index.js, teremos:
ContaCorrente { agencia 1001, saldo: 10000 }
Mas eu falei que ninguém poderia alterar o saldo, então, teoricamente, o que fizemos na linha de código contaCorrenteRicardo.saldo = 10000; não deveria ser válido, mas acontece que o JavaScript é dinamicamente tipado. Então, na verdade, o que não podemos é chamar a propriedade #saldo para contaCorrenteRicardo. Vamos fazer isso para ver o que acontece:
const contaCorrenteRicardo = new ContaCorrente();
contaCorrenteRicardo.#saldo = 10000;
contaCorrenteRicardo.agencia = 1001;
contaCorrenteRicardo.depositar(100);
contaCorrenteRicardo.sacar(50);
console.log(contaCorrenteRicardo);COPIAR CÓDIGO
No terminal, daremos um clear e, em seguida, executaremos nosso arquivo com node ./index.js. O sistema vai retornar um erro informando que a propriedade chamada é um campo privado e não podemos alterá-lo:
SyntaxError: Private Field '#saldo' must be declared in an enclosing class
Entretanto, ao chamarmos apenas saldo para contaCorrenteRicardo, o sistema adiciona um atributo chamado "saldo". Em vez de "saldo" poderíamos chamar, por exemplo, um atributo nomeado de "qualquerCoisa":
contaCorrenteRicardo.qualquerCoisa = 10000;COPIAR CÓDIGO
Ao executarmos nosso arquivo no terminal com node ./index.js, teremos:
ContaCorrente { agencia 1001, qualquerCoisa: 10000 }
Precisamos tomar muito cuidado com isso em JavaScript, pois a estrutura que estamos criando é o mínimo do que precisamos. Se durante a execução do programa alguém chamar um objeto e atribuir algo a esse objeto, esse atributo será criado dinamicamente ficando preso a essa instância única e não à classe como um todo. Por isso, precisamos ter cuidado para não cometer erros de digitação. Então, vamos corrigir e deixar contaCorrenteRicardo.#saldo.
Como não podemos acessar #saldo diretamente fora da classe, ao executarmos nosso arquivo no terminal com node ./index.js, ele retornará novamente um erro.
SyntaxError: Private Field '#saldo' must be declared in an enclosing class
Se não podemos acessar #saldo diretamente, vamos apagar a linha em que chamamos #saldo para contaCorrenteRicardo.
Apesar de estar declarado na classe ContaCorrente nosso saldo não está definido em nenhum lugar. Precisamos informar na classe ContaCorrente que no início o valor de #saldo deve ser 0.
class ContaCorrente{
 agencia;
 #saldo = 0;
//...
}COPIAR CÓDIGO
Ao executarmos nosso arquivo no terminal com node ./index.js, teremos:
ContaCorrente { agencia 1001 }
Ele continuará retornando que não temos saldo. Mas se inserirmos a chamada de console.log() para this.#saldo dentro da classe ContaCorrente, ao executarmos nosso arquivo no terminal com node ./index.js, teremos:
100 ContaCorrente { agencia 1001 }
Como #saldo é um campo privado, não aparece ao executarmos o console.log() da classe. Só é possível acessá-lo realmente dentro da classe. Ele está sendo alterado, mas só veremos isso dentro da classe. Se chamarmos o console.log() em qualquer outro lugar do código, o #saldo não aparecerá.
Para testar, podemos fazer a operação de depósito várias vezes, depositando três valores de 100:
const contaCorrenteRicardo = new ContaCorrente();
contaCorrenteRicardo.agencia = 1001;
contaCorrenteRicardo.depositar(100);
contaCorrenteRicardo.depositar(100);
contaCorrenteRicardo.depositar(100);
contaCorrenteRicardo.sacar(50);
console.log(contaCorrenteRicardo);COPIAR CÓDIGO
Ao executarmos nosso arquivo no terminal com node ./index.js, teremos:
100 200 300 ContaCorrente { agencia 1001 }
O saldo está sendo alterado, mas não fica exposto para outras pessoas mexerem.
Mas como explicamos, isso é uma proposta ainda não implementada na linguagem. Então não podemos fazer campos privados e não é recomendado usar esse tipo de sintaxe no seu programa em produção.
A convenção da comunidade é de que se colocamos um underline (_) à frente de um atributo, isso significa que ele é privado. Mas não é privado de verdade, podemos verificar que ainda podemos alterá-lo em outros lugares do nosso código. Se substituirmos #saldo por _saldo.
Ao executarmos nosso arquivo no terminal com node ./index.js, teremos:
ContaCorrente { agencia 100, _saldo:250 }
Então ele não fica realmente privado, mas é uma convenção da comunidade que não devemos mexer em atributos antecedidos pelo underline (_), ele não deve ser acessado de fora da classe. Apenas os métodos e comportamentos dentro do escopo da classe podem acessá-lo. Embora seja possível acessá-lo fora da classe, não é uma boa prática fazer isso.
Então, enquanto a proposta de campos privados não é oficialmente implementada, a convenção é usar o underline (_). Deixaremos o link de acesso para a proposta de campos privado no JavaScript como comentário no código, para você ler se tiver interesse.
Sobre métodos
 Função e método são termos sinônimos e que nós ajudam a criar um vocabulário mais rico dentro do nosso sistema
Correta! Usamos métodos para dar nomes aos comportamentos que nossa classe possui e isso facilita a comunicação dentro da equipe
 Alternativa correta
Um método pode receber qualquer quantidade de parâmetros.
Correto, um método pode ter nenhum, um ou mais parâmetros. Essa é a maneira de passarmos informações para podermos reutilizar métodos em diferentes cenários
Um método define o comportamento ou a maneira de fazer algo.
Correto, esse é o objetivo de métodos, definir o que um objeto saber fazer. O comportamento é implementado dentro do método.
Encapsulamento
Criamos métodos para proteger atributos e informações sensíveis de nossas classes. Porém se não fizermos nada essas informações ainda estão expostas e podem ser alteradas manualmente.
Por padrão no JS utilizamos o "_" para indicar que um atributo é privado e não deveria ser alterado.
Isso mesmo! Apesar de ainda ser possível alterar essa propriedade isso é considerado uma má prática e estamos quebrando o encapsulamento da classe.
Atualmente no Js nenhum atributo ou método é realmente privado
Sim, o JS é uma linguagem de escopo aberto e por isso é possível visualizar qualquer atributo ou propriedade de nossa classe.
Métodos com retorno
Agora temos a indicação de campos privados em nossa classe ContaCorrente, mostrando aos outros programadores que eles não devem ser acessados diretamente fora do escopo da própria classe, e imaginamos que a implementação definitiva desse tipo de atributo na linguagem será ainda mais benéfica para o desenvolvimento.
Passaremos então a refletir sobre o resto do nosso sistema. Já temos os métodos sacar() e depositar(), mas queremos expandir a lógica das nossas operações de modo que fiquem mais semelhantes às da vida real. Por exemplo, quando sacamos um valor, ele não simplesmente deixa de existir, mas sim passa a pertencer ao cliente que efetuou esse saque. No nosso código, simularemos isso atribuindo a chamada de contaCorrenteRicardo.sacar(50) a uma variável valorSacado e faremos o console.log() do seu valor.
const contaCorrenteRicardo = new ContaCorrente();
contaCorrenteRicardo.agencia = 1001;
contaCorrenteRicardo.depositar(100);
contaCorrenteRicardo.depositar(100);
contaCorrenteRicardo.depositar(100);
const valorSacado = contaCorrenteRicardo.sacar(50);
console.log(valorSacado);
console.log(contaCorrenteRicardo)COPIAR CÓDIGO
Nesse ponto, removeremos a linha console.log(this._saldo) na definição do método depositar().
Ao executarmos, teremos como retorno:
undefined ContaCorrente { agencia: 1001, _saldo: 250 }
Note que a operação de saque foi realizada, mas tivemos um undefined como retorno. Isso acontece pois estamos atribuindo o valor da operação sacar() a uma variável valorSacado, mas o método sacar() em si não devolve nada. Se queremos devolver um valor para quem chamou a função, precisamos fazer um retorno com a instrução return seguida daquilo que deve ser devolvido, nesse caso o próprio valor.
class ContaCorrente {
 agencia;
 _saldo = 0;
 sacar(valor) {
 if (this._saldo>= valor) {
 this._saldo -= valor;
 return valor;
 }
 return
 }
 depositar(valor) {
 if(valor > 0) {
 this._saldo += valor;
 }
 }
}COPIAR CÓDIGO
Feito isso, o valor 50 passará a ser exibido no console quando executarmos a aplicação.
50 ContaCorrente { agencia: 1001, _saldo: 250 }
No JavaScript, se um método não possui a palavra-chave return - ou seja, se ele não devolve um valor explicitamente -, ele sempre devolve um undefined. Outra característica interessante é que toda vez que um método encontra a palavra-chave return, a sua execução é encerrada. Sendo assim, se passarmos a instrução return valor para antes da nossa condicional e executarmos nosso código, o 50 será exibido no console, mas o saque não será efetuado com sucesso.
Note que a IDE inclusive tornará mais escuro o trecho de código abaixo da instrução return valor, ressaltando que ele não será executado (unreachable code detected).
sacar(valor) {
 return valor;
 if (this._saldo >= valor) {
 this._saldo -= valor;
 }
}COPIAR CÓDIGO
50 ContaCorrente { agencia: 1001, _saldo: 300 }
Portanto, devemos prestar atenção ao longo do desenvolvimento do nosso código, já que uma cláusula de retorno mal posicionada pode parar a execução do método em um momento indesejado. Feito esse teste, voltaremos a instrução return valorpara sua posição original.
sacar(valor) {
 if (this._saldo >= valor) {
 this._saldo -= valor;
 return valor;
 }
}COPIAR CÓDIGO
Note que nos métodos sacar() e depositar() estamos realizando verificações antes de executarmos a operação em si - no primeiro verificamos se o saldo é maior do que o valor a ser sacado, e no segundo se o valor a ser depositado é maior do que zero. Conforme nossos comportamentos forem se tornando mais complexos, com mais cláusulas de proteção, teremos também mais códigos indentados, como no exemplo ilustrativo abaixo:
depositar(valor) {
 if(valor > 0) {
 if() {
 if() {
 this._saldo += valor;
 }
 }
 }
}COPIAR CÓDIGO
Isso não é tão interessante, pois dificulta a leitura do código. Uma maneira de melhorarmos essa legibilidade é fazermos o chamado "early return" (ou "retorno antecipado", em tradução livre). Essa técnica consiste em verificarmos todas as situações indesejadas primeiro. Por exemplo, no método depositar() não queremos que o valor recebido seja menor ou igual a zero. Caso isso aconteça, simplesmente usaremos a instrução return para pararmos a execução do código.
depositar(valor) {
 if(valor <= 0) {
 return;
 }
 this._saldo += valor;
}COPIAR CÓDIGO
Dessa maneira, mantemos o código onde a operação realmente é realizada no mesmo nível de indentação da condicional, e conseguimos analisar facilmente que, caso a condição seja verdadeira, sairemos da função sem executá-la. Se todas as verificações passaremos corretamente, o código será executado ao final.
Se rodarmos o código no terminal, continuaremos recebendo o mesmo retorno:
50 ContaCorrente { agencia: 1001, _saldo: 300 }
Já se tentarmos realizar um depósito com o valor -100, somente duas das três chamadas de depositar(100) serão realizadas com sucesso, resultando em um saldo de 150.
50 ContaCorrente { agencia: 1001, _saldo: 150 }
A lógica do nosso método não foi alterada, apenas a maneira como ele é escrito. Inclusive, é bastante comum encontrar esse tipo de sintaxe:
depositar(valor) {
 if(valor <= 0) return;
 this._saldo += valor;
}COPIAR CÓDIGO
Perceba que aqui não estamos utilizando as chaves para delimitar o escopo da condicional, já que ele consiste somente na instrução return. Quando temos apenas uma linha de código a ser executada, o if permite que ela seja passada sem as chaves. Isso economiza algumas linhas de código, mas, claro, não é obrigatório. Quando estamos estudando, manter as chaves pode até mesmo facilitar a visualização do que está acontecendo naquele método, e é justamente isso que faremos.
depositar(valor) {
 if(valor <= 0) {
 return;
 }
 this._saldo += valor;
}COPIAR CÓDIGO
Recapitulando, o retorno de um método (return) pode ter duas funções: parar a execução antecipadamente ao nos depararmos com uma condição indesejada, algo que chamamos de early return; ou realmente retornar um valor para que o sistema continue trabalhando com ele de alguma forma.
O que aprendemos?
· Criação de métodos
· Palavra chave this
· Encapsulamento
· Proposta de atributos privados
· Return e early return
Já aplicamos diversos conceitos de orientação a objetos no nosso código. Por exemplo, criamos classes que podem ser instanciadas durante a execução do código usando a palavra new; entendemos o que são os objetos e métodos, que utilizamos para manipular os atributos da classe; expandimos nosso vocabulário aprendendo a definição de atributos, que podem ser públicos ou privados; e assim por diante.
Note que o arquivo index.js está ficando cada vez mais extenso, sendo necessário rolarmos a tela para acessarmos determinadas instruções. Na verdade, conforme nosso sistema cresce, não é uma boa prática mantermos todos os códigos em um único arquivo - afinal, trabalhando em cenários reais, teremos dezenas de entidades, classes e cenários possíveis para lidar.
O ideal é separarmos as classes em arquivos próprios, ainda que existam pessoas que prefiram manter duas ou três em um único arquivo, algo que vai da preferência de cada programador. No nosso caso, manteremos somente uma classe por arquivo. Criaremos então na raiz do projeto um arquivo Cliente.js.
Repare que o nome desse arquivo se inicia em caixa alta. É uma convenção da comunidade JavaScript que arquivos que representam classes sejam nomeados dessa forma.
Moveremos a definição da classe Cliente para nosso novo arquivo:
class Cliente {
 nome;
 cpf;
}COPIAR CÓDIGO
Repetiremos o processo para a classe ContaCorrente, movendo-a para um arquivo ContaCorrente.js.
class ContaCorrente {
 agencia;
 _saldo = 0;
 sacar(valor) {
 if (this._saldo >= valor) {
 this._saldo -= valor;
 return valor;
 }
 }
 depositar(valor) {
 if(valor <= 0) {
 return;
 }
 this._saldo += valor;
 }
}COPIAR CÓDIGO
Manteremos no index.js somente os códigos referentes a execução do nosso sistema:
const cliente1 = new Cliente();
cliente1.nome = "Ricardo";
cliente1.cpf = 11122233309;
const cliente2 = new Cliente();
cliente2.nome = "Alice";
cliente2.cpf = 88822233309;
const contaCorrenteRicardo = new ContaCorrente();
contaCorrenteRicardo.agencia = 1001;
contaCorrenteRicardo.depositar(100);
contaCorrenteRicardo.depositar(100);
contaCorrenteRicardo.depositar(-100);
const valorSacado = contaCorrenteRicardo.sacar(50);
console.log(valorSacado);
console.log(contaCorrenteRicardo);COPIAR CÓDIGO
Dessa forma nosso projeto fica bem mais organizado e fácil de trabalhar. Entretanto, se executarmos nosso projeto dessa forma, receberemos um erro:
const cliente1 = new Cliente(); ReferenceError: Cliente is not defined
Tal erro indica que a referência Cliente não foi definida, mas temos uma classe com esse nome no projeto. Na verdade, o problema é em geral, no JavaScript, cada arquivo atua separadamente. No início, o JavaScript era utilizado como uma linguagem de browser (e até hoje é muito associado com esse ambiente), e se limitava a poucas interações com scripts individuais.
Por isso, durante muito tempo não existia compartilhamento de código entre arquivos, sendo necessário definir todas as interações de código em escopo global. Mais recentemente, conforme a linguagem foi se desenvolvendo e sendo aplicada a sistemas maiores, passou a existir a necessidade de compartimentalizarmos nosso código, algo possível com a criação de módulos.
Módulos são um conceito relativamente novo no JavaScript, surgindo no Ecmascript 6. As versões mais recentes do Node, como a 13 utilizada nesse treinamento, aceitam a utilização dos módulos, portando é importante acompanharo curso usando uma versão compatível.
No JavaScript, cada arquivo define um módulo, e os módulos podem exportar informações para serem consumidas por outros módulos, algo que é feito com a instrução export. Em Cliente.js, como queremos exportar a definição da classe Cliente, adicionaremos essa palavra-chave antes da classe.
export class Cliente {
 nome;
 cpf;
}COPIAR CÓDIGO
Nosso Cliente está sendo utilizado no arquivo index.js, portanto teremos que importá-lo, algo que é feito usando a instrução import seguida de chaves circundando o nome da classe que estamos instanciando, e então o endereço do arquivo que contém a sua definição, como no exemplo a seguir:
import {Cliente} from "./Cliente.js"COPIAR CÓDIGO
A documentação dos módulos em JavaScript recomenda a utilização do caminho absoluto do arquivo que está sendo importado, desde a raiz do HD (algo que é exibido se deixarmos o mouse sobre o caminho relativo"./Cliente.js"). Porém, nesse momento, manteremos a sintaxe mais curta.
Como também estamos utilizando a classe ContaCorrente, precisaremos importá-la.
import {Cliente} from "./Cliente.js"
import {ContaCorrente} from "./ContaCorrente.js"COPIAR CÓDIGO
Lembre-se de adicionar a extensão do arquivo ao final do caminho, do contrário ele não será importado corretamente!
Feitas essas alterações, se executarmos nosso código no terminal receberemos o seguinte erro:
SyntaxError: Cannot use import statement outside a module
O Node está nos informando que não é possível utilizar a instrução import fora de um módulo. Entretanto, afirmamos anteriormente que cada arquivo define um módulo. Na verdade, o JavaScript pode ser utilizado como script padrão, sendo somente executado, ou compartimentalizado usando os módulos.
Por padrão, quando pedimos a execução de um arquivo JavaScript com o Node, isso é feito como um script, sem identificar que estamos trabalhando com módulos. Portanto, teremos que passar essa informação ao interpretador do JavaScript.
A própria tela de erro nos traz instruções sobre como fazer isso:
Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
*Para carregar um módulo EcmaScript, defina "type": "module" no arquivo package.json ou use a extensão .mjs
Entretanto, tais instruções não parecem familiares com o que estamos trabalhando até agora, e precisaremos explicá-las. Todo projeto Node possui um arquivo de configuração contendo uma série de informações sobre o pacote com que estamos trabalhando, e ele é chamado de package.json. Diversos frameworks, como o React, possuem esse arquivo nativamente.
É possível criarmos o arquivo package.json por meio de um comando, que pode ser executado no Prompt de Comando, no Powershell ou no próprio terminal do VSCode (que é aberto com "ctrl + J"). Para esse treinamento usaremos esta última opção.
Após abrirmos o terminal, executaremos o comando npm init, que inicializará um gerenciador de pacotes que nos auxiliará na criação do pacote. Esse gerenciador nos fará uma série de perguntas, começando pelo nome do pacote, que será "bytebank".
Pressionando "Enter" conseguimos avançar para as próximas definições, como version, que manteremos a sugestão 1.0.0; description, que definiremos como "Projeto do bytebank para seus clientes"; entry point, que define o arquivo inicial de execução do programa, no nosso caso index.js (o padrão do JavaScript); test command, um comando para testes que manteremos vazio; git repository, que permite adicionar um repositório do GitHub, etapa que pularemos; keywords, para definir palavras-chave para o projeto, e que também pulares; author, que mantivemos vazio na gravação do treinamento, mas que você pode preencher com seu nome se preferir (ou alterar futuramente); e license, que manteremos a sugestão padrão.
Finalizadas as configurações, será gerado um arquivo package.json na raiz do projeto.
{
 "name": "bytebank",
 "version": "1.0.0",
 "description": "Projeto do bytebank para seus clientes",
 "main": "index.js",
 "scripts": {
 "test": "echo \"Error: no test specified\" && exit 1"
 },
 "author": "",
 "license": "ISC"
}COPIAR CÓDIGO
Note que este é um arquivo de marcação contendo todas as informações que definimos. Para que o Node deixe de exibir o erro que tínhamos anteriormente, bastará adicionarmos uma nova marcação, "type", recebendo o valor "module".
{
 "name": "bytebank",
 "version": "1.0.0",
 "description": "Projeto do bytebank para seus clientes",
 "main": "index.js",
 "scripts": {
 "test": "echo \"Error: no test specified\" && exit 1"
 },
 "author": "Rodrigo",
 "license": "ISC",
 "type": "module"
}COPIAR CÓDIGO
Executando nosso código agora, continuaremos recebendo um erro:
import {ContaCorrente} from "./ContaCorrente.js" ^^^^^^^^^^^^^ SyntaxError: The requested module './ContaCorrente.js' does not provide an export named 'ContaCorrente'
Dessa vez o problema ocorre pois não exportamos a classe ContaCorrente, permitindo o acesso por meio de outro módulo. Faremos essa correção.
export class ContaCorrente {
 agencia;
 _saldo = 0;
 sacar(valor) {
 if (this._saldo >= valor) {
 this._saldo -= valor;
 return valor;
 }
 }
 depositar(valor) {
 if(valor <= 0) {
 return;
 }
 this._saldo += valor;
 }
}
COPIAR CÓDIGO
Com isso, nosso código passará a executar corretamente:
50 ContaCorrente { agencia: 1001, _saldo: 150 }
Repare que temos a mesma saída que recebíamos anteriormente. A única diferença é que agora estamos trabalhando com módulos.
Na versão 13 do Node também receberemos uma mensagem informando que os módulos que estamos utilizando são experimentais, já que sua implementação é recente. Ou seja, talvez não seja adequado utilizá-los em produção. Para esses casos, existem transpiladores (ou bundlers, como são comumente chamados) que traduzem esse código para uma versão retrocompatível do Node.js.
Organizando código
De qualquer forma, agora que passamos a utilizar os módulos, é possível trabalharmos em arquivos separados, organizando melhor o nosso código e facilitando a sua manutenção e desenvolvimento.
A organização de um projeto de programação é algo muito importante para que conforme o sistema crescer encontrarmos mais facilmente as classes e lugares que queremos alterar.
Para que serve a criação de módulos no JavaScript?
Criamos módulos para compartilhar código entre os diferentes arquivos do meu sistema, ajudando na organização dele.
Isso, dentro do JS cada arquivo é considerado um módulo e podemos escolher o que queremos exportar ou não a partir dele.
O package.json
Para melhorarmos a organização do nosso código e podermos separar as classes em diversos arquivos, precisamos usar módulos de Javascript e, para isso, criamos um arquivo chamado package.json. Mas o que é esse arquivo?
O package.json é um arquivo muito utilizado em aplicações JS modernas que guarda vários dados de nossa aplicação. O arquivo gerado para este curso tem apenas algumas informações.
{
 "name": "bytebank",
 "version": "1.0.0",
 "description": "Projeto do Bytebank para seus clientes",
 "main": "index.js",
 "scripts": {
 "test": "echo \"Error: no test specified\" && exit 1"
 },
 "author": "",
 "license": "ISC",
 "type": "module"
}COPIAR CÓDIGO
Algumas delas são simples de entender, como nome, versão, descrição e autor. Outras, como o campo script, já são mais difíceis de entender sem uma explicação mais detalhada.
Como desenvolvedores é normal queremos reaproveitar código de outras pessoas e bibliotecas que estão disponíveis para nosso time. Dessa forma, agilizamos o desenvolvimento de nossas aplicações. Mas onde podemos encontrar essas bibliotecas e código feitos pela comunidade?
Achamos essas bibliotecas dentro de gerenciadores de pacote – pense neles como um lugar centralizado onde toda a comunidade pode subir e compartilhar códigos para que outros desenvolvedores usem. E é justamente para organizar essa série de pacotes e bibliotecas que o package.json foi criado. Com ele éfácil de saber qual a versão do pacote, o nome dele, quem fez aquele código etc.
No caso do Javascript o gerenciador de pacotes mais utilizado é o NPM – Node package manager.
"Ok, mas e essa tag de script?"
Até este momento no curso não fizemos muitas operações complicadas, pois só estamos usando o terminal para chamar o interpretador do NodeJS e pedindo para ele executar o arquivo que queremos. Mas conforme nosso projeto cresce ou usamos outras bibliotecas para desenvolver a aplicação, é comum que o comando que precisamos rodar no terminal fique longo ou que ele precise de alguns parâmetros especiais. Como normalmente estamos trabalhando em equipe, não queremos que alguém na nossa equipe precise ficar perguntando o tempo todo qual o comando precisa escrever para o programa rodar. É nesse momento que usamos os scripts do package.json.
Podemos escrever um script com o comando que colocaríamos no terminal, por exemplo:
"scripts": {
 "start": "node index.js"
}COPIAR CÓDIGO
E ao invés de escrever esse comando podemos chamar o script com o comando npm start. Dessa forma não precisamos lembrar de todo o comando sempre que vamos executar o programa. Se você quiser saber mais, veja na documentação do NPM os tipos de scripts e os casos de uso.
"E por que eu preciso disso no meu programa agora?"
Como esse arquivo de configurações está presente em praticamente todo projeto de Javascript moderno, algumas ferramentas usam-no para colocar informações de configuração que elas precisam. No caso do NodeJS eles escolheram adicionar uma propriedade chamada "type" dentro desse arquivo e, sempre que vão executar algum código JS, eles leem essa propriedade e configuram o interpretador do JS de acordo com o valor lido.
Como os módulos JS são uma coisa nova e experimental, dentro desse interpretador do Node não é interessante deixá-lo configurado para tratar todo arquivo como um módulo, pois muitos projetos antigos teriam problemas ao atualizar a versão do Node que estão usando.
Então, esta é a opção que eles encontraram para deixar quem quisesse usar os módulos JS conseguir configurar a ferramenta para fazer os testes, e quem não quisesse não teria problemas e não precisaria mudar nenhuma configuração.
Se você quiser saber mais sobre as propriedades que esse arquivo suportam você pode encontrá-las nesta página da documentação.
Composição de classes
Agora que temos um código um pouco mais organizado, continuaremos trabalhando com a classe ContaCorrente.
export class ContaCorrente {
 agencia;
 _saldo = 0;
 sacar(valor) {
 if (this._saldo >= valor) {
 this._saldo -= valor;
 return valor;
 }
 }
 depositar(valor) {
 if(valor <= 0) {
 return;
 }
 this._saldo += valor;
 }
}COPIAR CÓDIGO
Até o momento, em ContaCorrente, temos os atributos agencia e _saldo, cujos valores consistem em números (um dos tipos primitivos que conhecemos no curso anterior). Em Cliente temos os atributos nome, que é uma string (outro tipo primitivo), e um CPF que atualmente também é representado por um número. Além disso, já temos as operações de saque e depósito, mas ainda falta um ponto muito importante que nos foi pedido pelo Bytebank: a associação entre uma conta e um cliente.
Para isso, criaremos um atributo cliente na classe ContaCorrente, logo após a agencia. Dessa forma, manteremos separados os atributos públicos e privados, demonstrando visualmente o que pode ou não ser alterado (ou quais informações são específicas ou não dessa classe).
export class ContaCorrente {
 agencia;
 cliente;
 _saldo = 0;
 sacar(valor) {
 if (this._saldo >= valor) {
 this._saldo -= valor;
 return valor;
 }
 }
 depositar(valor) {
 if(valor <= 0) {
 return;
 }
 this._saldo += valor;
 }
}COPIAR CÓDIGO
Em index.js já temos definido um cliente1 com o nome "Ricardo" e o CPF "11122233309". Agora que temos um atributo cliente na classe ContaCorrente, poderemos atribuir a ele o próprio cliente1 criado anteriormente.
const contaCorrenteRicardo = new ContaCorrente();
contaCorrenteRicardo.agencia = 1001;
contaCorrenteRicardo.cliente = cliente1;
contaCorrenteRicardo.depositar(100);
contaCorrenteRicardo.depositar(100);
contaCorrenteRicardo.depositar(-100);
const valorSacado = contaCorrenteRicardo.sacar(50);
console.log(contaCorrenteRicardo);COPIAR CÓDIGO
Ao executarmos nosso código, imprimindo na tela a contaCorrenteRicardo, teremos como retorno:
ContaCorrente { agencia: 1001, _saldo: 150, cliente: Cliente { nome: 'Ricardo', cpf: 11122233309 } }
Recebemos uma instância de ContaCorrente com todas as suas informações, incluindo cliente, que por sua vez é uma instância de Cliente. Ou seja, é possível utilizar classes como atributo de outras classes, compondo objetos mais complexos e relacionados.
Para continuarmos nossos testes, removeremos os depósitos e o saque que escrevemos anteriormente, já que sabemos como eles funcionam. Em seguida, criaremos uma nova instância de ContaCorrente, chamada conta2, e atribuiremos a ela uma agencia com o valor 102 e um cliente com o valor cliente2 (outra instância de Cliente que já havíamos definido anteriormente). Por fim, faremos o console.log() da nova conta.
cliente2.nome = "Alice";
cliente2.cpf = 88822233309;
const contaCorrenteRicardo = new ContaCorrente();
contaCorrenteRicardo.agencia = 1001;
contaCorrenteRicardo.cliente = cliente1;
const conta2 = new ContaCorrente();
conta2.cliente = cliente2;
conta2.agencia = 102;
console.log(conta2);COPIAR CÓDIGO
Ao executarmos, receberemos o objeto contendo os dados que definimos, dessa vez com o nome "Alice".
ContaCorrente { agencia: 102, _saldo: 0, cliente: Cliente { nome: 'Alice', cpf: 88822233309 } }
A composição de classes nos permite criar comportamentos mais interessantes, como a própria associação entre Cliente e ContaCorrente. O próximo passo será desenvolvermos uma funcionalidade de transferência entre contas. Para isso, em ContaCorrente, criaremos o método transferir().
Esse método será um pouco diferente dos demais, afinal ele não alterará somente as informações da própria conta que o chamou. Sendo assim, além de um valor, ele precisará receber por parâmetro uma conta que servirá como destino da transferência.
transferir (valor, conta) {
}COPIAR CÓDIGO
No corpo do método, criaremos uma variável valorSacado que receberá a chamada de this.sacar(valor) - ou seja, usaremos o valor recebido como parâmetro para realizaremos um saque na própria conta que chamou o método. Em seguida, faremos a chamada do método depositar() a partir da conta recebida, passando como parâmetro o valorSacado anteriormente.
transferir (valor, conta) {
 const valorSacado = this.sacar(valor);
 conta.depositar(valorSacado);
}COPIAR CÓDIGO
Em index.js, faremos um depósito com o valor 500 em contaCorrenteRicardo de modo a termos saldo disponível para a transferência. Em seguida, chamaremos o método transferir() a partir dessa conta usando os parâmetros 200 e conta2 e imprimiremos o objetoconta2 na tela.
const contaCorrenteRicardo = new ContaCorrente();
contaCorrenteRicardo.agencia = 1001;
contaCorrenteRicardo.cliente = cliente1;
contaCorrenteRicardo.depositar(500);
const conta2 = new ContaCorrente();
conta2.cliente = cliente2;
conta2.agencia = 102;
console.log(conta2);
contaCorrenteRicardo.transferir(200, conta2);
console.log(conta2)COPIAR CÓDIGO
Ao executarmos, receberemos:
ContaCorrente { agencia: 102, cliente: Cliente { nome: 'Alice', cpf: 88822233309 }, _saldo: 200 }
Como não havíamos feito nenhum depósito na conta2 anteriormente, ela passou a ter o valor 200. Podemos acompanhar isso mais de perto fazendo também um console.log() de contaCorrenteRicardo.
contaCorrenteRicardo.transferir(200, conta2);
console.log(conta2);
console.log(contaCorrenteRicardo);COPIAR CÓDIGO
ContaCorrente { agencia: 102, cliente: Cliente { nome: 'Alice', cpf: 88822233309 }, _saldo: 200 } ContaCorrente { agencia: 1001, cliente: Cliente { nome: 'Ricardo',cpf: 11122233309 }, _saldo: 300 }
Assim, conseguimos observar que os 200 depositados na conta da Alice foram sacados da conta do Ricardo, e fizemos tudo isso usando as propriedades da classe Cliente recebida por parâmetro. Note, também, que essa composição de classes/objetos nos permitiu criar um comportamento complexo utilizando um código bastante simples e legível.
transferir (valor, conta) {
 const valorSacado = this.sacar(valor);
 conta.depositar(valorSacado);
}COPIAR CÓDIGO
Além disso, esse código está em uma linguagem de abstração mais alta e mais próxima da linguagem do nosso negócio em si (saque, depósito, transferência), e mais distante da linguagem JavaScript, um vocabulário que também melhora a legibilidade.
Tipo de valor e tipo de referência
Criamos o método transferir() com o auxílio da composição de classes, que ainda tornou nosso código mais legível, já que estamos tratando de uma linguagem mais próxima do nosso negócio. Apesar de muito útil durante o desenvolvimento dos nossos projetos, é necessário tomar alguns cuidados.
Por exemplo, todas vez que passamos um parâmetro que representa um objeto para dentro de um método, como no caso da conta, estamos realmente passando a instância dessa entidade (no caso uma ContaCorrente). Sendo assim, as alterações que fizermos no objeto dentro do escopo do método realmente serão efetuadas.
Para testarmos isso, adicionaremos ao método transferir() uma instrução que cria, em conta, um novo atributo cidade e atribui a ele o valor "São Paulo". Lembrando que no JavaScript é possível criar novos atributos dinamicamente, algo que não é comum em outras linguagens (como Java e C#).
transferir (valor, conta) {
 conta.cidade = "São Paulo";
 const valorSacado = this.sacar(valor);
 conta.depositar(valorSacado);
}COPIAR CÓDIGO
Ao executarmos o código no terminal, veremos que nosso objeto conta2 foi alterado com o dado incluído no método transferencia().
ContaCorrente { agencia: 102, cliente: Cliente { nome: 'Alice', cpf: 88822233309 }, _saldo: 200, cidade: 'São Paulo' } ContaCorrente { agencia: 1001, cliente: Cliente { nome: 'Ricardo', cpf: 11122233309 }, _saldo: 300 }
Essa não é uma situação interessante, por isso devemos tomar cuidado. Para entendermos melhor o que acontece, testaremos outro cenário, no qual, ainda no método transferencia(), alteraremos o valor recebido por parâmetro para 20.
transferir (valor, conta) {
 conta.cidade = "São Paulo";
 const valorSacado = this.sacar(valor);
 conta.depositar(valorSacado);
 valor = 20
}COPIAR CÓDIGO
Em index.js, criaremos uma variável valor recebendo 200 e a passaremos como parâmetro para a chamada de contaCorrenteRicardo.transferir().
let valor = 200;
contaCorrenteRicardo.transferir(valor, conta2);
console.log(conta2);
console.log(contaCorrenteRicardo);COPIAR CÓDIGO
Incluiremos no código um console.log() com a string "valor: " seguida da própria variável valor, de modo a acompanharmos o que acontece com ela, e removeremos a impressão de contaCorrenteRicardo.
let valor = 200;
contaCorrenteRicardo.transferir(valor, conta2);
console.log("valor: ", valor);
console.log(conta2);COPIAR CÓDIGO
Ao executarmos, teremos:
valor: 200 ContaCorrente { agencia: 102, cliente: Cliente { nome: 'Alice', cpf: 88822233309 }, _saldo: 200, cidade: 'São Paulo' }
Perceba que, apesar do atributo cidade ter sido adicionado à conta2, o valor não foi alterado. Qual a diferença entre a variável valor e o objeto conta2? O valor consiste em um tipo primitivo (como strings, booleanos, inteiros e assim por diante). Sempre que uma variável desse tipo é passada como parâmetro para um método, na verdade estamos passando uma cópia do seu valor, mantendo a variável intacta.
Já quando passamos a conta2, estamos trabalhando diretamente com a referência ao objeto conta, e não uma cópia dele. Aqui entendemos a diferença entre tipos de referência e tipos de valor. Recapitulando: quando passamos como parâmetro de um método os tipos primitivos, também chamados de tipos de valor, estamos na verdade copiando aquela informação para utilizá-la; já com tipos de referência, como objetos de classes, estamos trabalhando com a informação direta, e todas as alterações serão refletidas no objeto original.
Existem outros detalhes sobre como isso funciona, relacionados, por exemplo, com a maneira como o JavaScript trabalha com a memória do computador, e nas atividades extras você encontrará um texto mais aprofundado sobre o tema.
Antes de prosseguirmos, não se esqueça de remover do método transferir() as linhas que utilizamos nesses testes, mais especificamente valor = 20 e conta.cidade = "São Paulo"!
transferir (valor, conta) {
 conta.cidade = "São Paulo";
 const valorSacado = this.sacar(valor);
}
Gerenciamento de memória
Ao criarmos um programa em qualquer linguagem de programação, precisamos utilizar variáveis para guardar valores durante a execução de nossos programas. Cada uma dessas variáveis é armazenada em um lugar na memória do computador.
Porém, vimos que, dependendo dos valores que uma variável guarda, seu comportamento dentro do código pode variar, passando como uma cópia de seu valor ou como uma referência ao espaço de memória onde ela está guardada. O JS faz isso baseado justamente nos valores que a variável guarda. Tipos primitivos são sempre passados como um valor, enquanto tipos não primitivos sempre são passados por referência.
"Mas o que são tipos primitivos?"
Tipos primitivos são os valores mais simples que a linguagem consegue trabalhar e geralmente são implementados no nível mais baixo da linguagem. Isso significa que eles não são representados como um objeto e por isso não possuem métodos ou atributos.
Um tipo primitivo é simplesmente um valor em sua forma mais simples de representação. Eles também são imutáveis, uma vez declarados seu valor, nunca muda.
Em Javascript, os tipos primitivos são:
· String (texto);
· Number (número);
· Boolean;
· Null;
· undefined;
· Symbol.
Qualquer outro tipo que encontramos em nossas aplicações, como Objetos, Arrays, etc... não são tipos primitivos e por isso seu comportamento é distinto. Variáveis não primitivas são mutáveis por natureza e tem potencial para armazenar estruturas muito mais complexas, o que torna as operações de cópia desses valores muito ineficiente.
"Mas se o JavaScript não o copia, o que ele faz?"
Quando criamos um objeto dentro do JavaScript ocorre a reserva de um espaço na memória local que chamamos de heap. Ao fazer isso, o que o JavaScript guarda na nova variável não é o valor do Objeto que criamos, mas sim o endereço de memória onde os valores estão salvos.
Dessa forma, ao declararmos uma variável que armazena informações dentro do heap, o que estamos fazendo é "alugar" um espaço da memória, o qual funções podem acessar. Isso torna as operações muito mais eficientes, mas tem um problema: quaisquer alterações no conteúdo armazenado pelos objetos não primitivos afetam todas as variáveis que o referenciam.
Como essas variáveis são links para espaços na memória, dizemos que elas são um "tipo de referência" enquanto que os tipos primitivos são "um tipo de valor".
Caso você queira ver em detalhes como essa operação de alocação de memória é feita, leia esse artigo sobe o modelo de gerenciamento de memória do JavaScript.
O que aprendemos?
· Package.json
· Modules
· import/export
· Quais as vantagens de ter um código onde usamos classes e composição ao invés de tipos primitivos
· Tipos de valor e tipos de referência
· Alterando dinamicamente um objeto
Null e undefined
Aprendemos que devemos tomar alguns cuidados quando passamos objetos como parâmetros dos métodos de nosso sistema, já que esse parâmetro atuará como referência ao próprio objeto.
Isso está relacionado à maneira como o computador e o JavaScript lidam com a memória. Toda vez que instanciamos uma nova ContaCorrente, estamos reservando um espaço na memória para as informações contidas nessa classe, que são agência, cliente e saldo. Da mesma forma, quando instanciamos um Cliente estamos reservandoum espaço para as informações "nome" e "cpf".
Com esse espaço reservado na memória, o que temos armazenado, por exemplo, em contaCorrenteRicardo, não é o objeto em si, mas sim uma referência ao objeto criado (ao espaço de memória onde as informações podem ser manipuladas). Costumamos chamar de objetos ou instâncias as variáveis que criamos, como cliente1, cliente1, contaCorrenteRicardo e conta2, mas, estritamente falando, se tratam de referências.
Isso tem algumas implicações. Por exemplo, se cliente2 = new Cliente() nos devolve uma referência para um endereço de memória, sabemos que em conta2.cliente = cliente2 estamos passando essa mesma referência de memória. Isso também significa que podemos substituir essa chamada pela instanciação direta de um novo Cliente.
const conta2 = new ContaCorrente();
conta2.cliente = new Cliente();
conta2.agencia = 102;COPIAR CÓDIGO
Com isso, teremos acesso a conta2.cliente.nome - ou seja, ao tributo nome do objeto atribuído ao atributo cliente do nosso objeto conta2. Atribuiremos a ele o mesmo valor que tínhamos em cliente2.nome, "Alice". Repetiremos esse processo para conta2.cliente.cpf e comentaremos todos os pontos do código em que trabalhávamos com a variável cliente2.
//código omitido 
//const cliente2 = new Cliente();
//cliente2.nome = "Alice";
//cliente2.cpf = 88822233309;
//código omitido 
const conta2 = new ContaCorrente();
conta2.cliente = new Cliente();
conta2.cliente.nome = "Alice";
conta2.cliente.cpf = 88822233309;
conta2.agencia = 102;
let valor = 200;
contaCorrenteRicardo.transferir(valor, conta2);
console.log("valor: ", valor);
console.log(conta2);COPIAR CÓDIGO
Ao executarmos, teremos o mesmo resultado obtido anteriormente.
valor: 200 ContaCorrente { agencia: 102, cliente: Cliente { nome: 'Alice', cpf: 88822233309 }, _saldo: 200 }
Assim aprendemos que, trabalhando com referências, é possível acessarmos diversos níveis de profundidade. Mas o que acontece se comentarmos a linha em que criamos a instância de Cliente?
const conta2 = new ContaCorrente();
//conta2.cliente = new Cliente();
conta2.cliente.nome = "Alice";
conta2.cliente.cpf = 88822233309;
conta2.agencia = 102;
let valor = 200;
contaCorrenteRicardo.transferir(valor, conta2);
console.log(conta2);COPIAR CÓDIGO
Ao executarmos, receberemos:
conta2.cliente.nome = "Alice"; ^ TypeError: Cannot set property 'nome' of undefined
Esse erro indica que é impossível definir a propriedade nome de algo indefinido. Inclusive, se pararmos de tentar acessá-lo comentando as linhas em que fazíamos as atribuições indevidas, veremos que o objeto conta2 realmente recebeu o valor padrão undefined no atributo cliente, que não foi inicializado.
const conta2 = new ContaCorrente();
//conta2.cliente = new Cliente();
//conta2.cliente.nome = "Alice";
//conta2.cliente.cpf = 88822233309;
conta2.agencia = 102;
let valor = 200;
contaCorrenteRicardo.transferir(valor, conta2);
console.log(conta2);COPIAR CÓDIGO
ContaCorrente { agencia: 102, cliente: undefined, _saldo: 0 }
Também obteremos o mesmo resultado se explicitamente atribuirmos o valor null à propriedade conta2.cliente.
const conta2 = new ContaCorrente();
conta2.cliente = null;
//conta2.cliente.nome = "Alice";
//conta2.cliente.cpf = 88822233309;
conta2.agencia = 102;
let valor = 200;
contaCorrenteRicardo.transferir(valor, conta2);
console.log(conta2);COPIAR CÓDIGO
ContaCorrente { agencia: 102, cliente: null, _saldo: 0 }
Ambos os casos significam a mesma coisa, com a diferença do null estar explícito no nosso código (ou seja, é intencional). Se tentarmos acessar uma propriedade de um valor nulo, também receberemos um erro.
const conta2 = new ContaCorrente();
conta2.cliente = null;
conta2.cliente.nome = "Alice";
//conta2.cliente.cpf = 88822233309;
conta2.agencia = 102;
let valor = 200;
contaCorrenteRicardo.transferir(valor, conta2);
console.log(conta2);COPIAR CÓDIGO
conta2.cliente.nome = "Alice"; ^ TypeError: Cannot set property 'nome' of null
Retornaremos nosso código ao estado original, com a instância de Cliente, eliminando o problema da referência indefinida ou nula.
const conta2 = new ContaCorrente();
conta2.cliente = new Cliente();
conta2.cliente.nome = "Alice";
conta2.cliente.cpf = 88822233309;
conta2.agencia = 102;
let valor = 200;
contaCorrenteRicardo.transferir(valor, conta2);
console.log(conta2);
Getters e Setters
No vídeo anterior aprendemos que é possível passar um objeto diretamente para uma propriedade de outro objeto, já que ele é uma referência a uma instância de uma classe. Também aprendemos que é possível passar qualquer valor para essa propriedade, como null ou mesmo 0. Por conta disso, é uma boa prática mantermos a inicialização do objeto em uma variável, já que iremos popular seus atributos - por exemplo de cliente2 - antes de utilizá-lo como atributo de outro objeto.
const cliente2 = new Cliente();
cliente2.nome = "Alice";
cliente2.cpf = 88822233309;
//...código omitido
const conta2 = new ContaCorrente();
conta2.cliente = cliente2;
conta2.agencia = 102;COPIAR CÓDIGO
Entretanto, nada garante que durante a execução do nosso sistema, ou conforme ele é desenvolvido em parceria com outras pessoas, um valor inadequado seja passado para nosso atributo cliente, como um valor 0.
const conta2 = new ContaCorrente();
conta2.cliente = 0;
conta2.agencia = 102;
let valor = 200;
contaCorrenteRicardo.transferir(valor, conta2);
console.log(conta2);COPIAR CÓDIGO
ContaCorrente { agencia: 102, cliente: 0, _saldo: 0 }
Anteriormente, conseguimos proteger o atributo _saldo da nossa ContaCorrente, e conhecemos a proposta para a implementação de campos privados no JavaScript. Da maneira que o definimos atualmente, o _saldo só deverá ser manipulado por meio das funções sacar(), depositar() e transferir(), que pertencem à própria classe.
Para fazermos o mesmo com cliente, vamos transformá-lo em um atributo privado com a adição do underline, resultando em _cliente. Porém, se agora temos outro atributo privado, será que precisaremos criar novos métodos para atribuí-lo, como atribuirCliente(), e outro para utilizá-lo, como pegarCliente()?
export class ContaCorrente {
 agencia;
 _cliente;
 _saldo = 0;
 atribuirCliente(){}
 pegarCliente(){}
//...código omitidoCOPIAR CÓDIGO
Isso parece estranho, pois precisaremos de novos métodos para cada campo privado que tivermos em nossas classes. Pensando nisso, o JavaScript possui uma sintaxe especial para casos em que temos um atributo privado e precisaremos dar acesso a ele de maneira controlada, os chamados métodos assessores.
Começaremos com o método que define _cliente. Sua construção se inicia utilizando a palavra reservada set seguida do nome que usaremos para nos referir a essa propriedade - nesse caso, cliente, sem underline e com letra minúscula. Algumas linguagens fazem diferenciação entre letras maiúsculas para propriedades e assessores desse tipo, e letras minúsculas para atributos, mas o JavaScript não. Prosseguindo, abriremos e fecharemos parênteses, definindo que esse assessor poderá receber parâmetros, e abriremos o seu escopo com chaves ({}).
export class ContaCorrente {
 agencia;
 _cliente;
 _saldo = 0;
 set cliente() {
 }
//código omitidoCOPIAR CÓDIGO
Note que nosso assessor se parece muito com os demais métodos da nossa classe, e ele funciona da mesma maneira. Se queremos atribuir um valor a _cliente, faremos this._cliente, dessa vez utilizando underline, já que estamos trabalhando diretamente com aquele atributo privado. Mas que valor exatamente gostaríamos de atribuir?
Como citado anteriormente, nosso assessor do tiop set pode receber um parâmetro, por exemplo novoValor, o qual poderemos atribuir normalmente.
export class ContaCorrente {
 agencia;
 _cliente;
 _saldo = 0;
 set cliente(novoValor) {
 this._cliente = novoValor;
 }
//...COPIAR CÓDIGO
O assessor set, diferente de um método, possui uma característica especial, permitindo que façamos a atribuição de valores normalmente com uma igualdade, sem utilizarmos a sintaxe dos outros métodos(nesse caso, algo como conta2.cliente()).
const conta2 = new ContaCorrente();
conta2.cliente = 0;
conta2.agencia = 102;
let valor = 200;
contaCorrenteRicardo.transferir(valor, conta2);
console.log(conta2);COPIAR CÓDIGO
ContaCorrente { agencia: 102, _cliente: 0, _saldo: 0 }
Como queremos proteger nosso atributo privado, podemos incluir uma condicional definindo que a atribuição de novoValor só será feita se ele for uma instância de cliente, algo que conseguimos verificar usando o operador instanceof.
Nesse ponto precisaremos fazer a importação da classe Cliente como aprendemos nas aulas anteriores.
import { Cliente } from "./Cliente.js";
export class ContaCorrente {
 agencia;
 _cliente;
 _saldo = 0;
 set cliente(novoValor) {
 if (novoValor instanceof Cliente) {
 this._cliente = novoValor;
 }
 }
//..COPIAR CÓDIGO
Ao executarmos nosso código dessa forma, o atributo _cliente será devolvido como undefined.
ContaCorrente { agencia: 102, _cliente: undefined, _saldo: 0 }
Ou seja, a verificação não passou e o valor 0 não foi atribuído, recebendo um valor padrão. Se passarmos uma instância de Cliente, como cliente1, tudo ocorrerá como deveria.
const conta2 = new ContaCorrente();
conta2.cliente = cliente1;
conta2.agencia = 102;
let valor = 200;
contaCorrenteRicardo.transferir(valor, conta2);
console.log(conta2);COPIAR CÓDIGO
ContaCorrente { agencia: 102, _cliente: Cliente { nome: 'Ricardo', cpf: 11122233309 }, _saldo: 0 }
Os assessores são muito poderosos, pois nos concedem acesso a propriedades privadas ao mesmo tempo em que nos permitem definir uma regra de proteção dentro deles. Já se for necessário pegar o valor de _cliente, precisaremos de um novo assessor, dessa vez do tipo get. Ele também será semelhante a um método, mas nesse caso simplesmente retornaremos o valor de this._cliente.
import { Cliente } from "./Cliente.js";
export class ContaCorrente {
 agencia;
 _cliente;
 _saldo = 0;
 set cliente(novoValor) {
 if (novoValor instanceof Cliente) {
 this._cliente = novoValor;
 }
 }
 get cliente() {
 return this._cliente;
 }
//...COPIAR CÓDIGO
Feito isdso, será possível acessarmos o valor de conta2.cliente normalmente.
const conta2 = new ContaCorrente();
conta2.cliente = cliente1;
conta2.agencia = 102;
let valor = 200;
contaCorrenteRicardo.transferir(valor, conta2);
console.log(conta2.cliente);COPIAR CÓDIGO
Cliente { nome: 'Ricardo', cpf: 11122233309 }
Nesse caso, nossa maior preocupação era a criação do assessor set de modo a protegermos nosso atributo, fazendo com que somente o tipo esperado (Cliente) fosse aceito. Já no caso do atributo _saldo, que também é privado, podemos manipulá-lo por meio dos métodos sacar() e depositar(), mas ainda não temos algo que nos retorne seu valor atual para o exibirmos em uma interface gráfica. Nessa situação, precisaremos somente do assessor do tipo get, que criaremos da mesma forma que o anterior, simplesmente retornando this._saldo.
import { Cliente } from "./Cliente.js";
export class ContaCorrente {
 agencia;
 _cliente;
 _saldo = 0;
//...código omitido
 get saldo() {
 return this._saldo;
 }
//...COPIAR CÓDIGO
Após essa alteração, conseguiremos receber o valor de conta2.saldo com um console.log().
const conta2 = new ContaCorrente();
conta2.cliente = cliente1;
conta2.agencia = 102;
let valor = 200;
contaCorrenteRicardo.transferir(valor, conta2);
console.log(conta2.saldo);COPIAR CÓDIGO
200
Entretanto, se tentarmos fazer uma atribuição direta, receberemos um erro informando que não é possível atribuir um valor à propriedade saldo, já que ela só possui um método assessor de leitura (tipo get).
conta2.saldo = 30000;
console.log(conta2.saldo);COPIAR CÓDIGO
conta2.saldo = 30000; ^ TypeError: Cannot set property saldo of # which has only a getter
Dessa forma temos um encapsulamento melhor da nossa classe, protegendo os atributos mais sensíveis e permitindo acesso somente quando assim o desejamos.
Encapsulando com assessores
Sobre os assessores do tipo get e set marque as alternativas corretas:
Usando assessores do tipo set podemos alterar a regra de como um atributo pode ou não ser modificado sem precisar alterar isso em diversos pontos do código
Excelente! Essa é a ideia mesmo, assim se qualquer regra de atribuição mudar só precisamos modificar um único lugar.
Usar assessores do tipo set é uma boa prática para garantirmos que a atribuição de propriedades está sempre segura
Sim! Chamamos essa ideia de proteger atributos de nossas classes de encapsulamento e devemos sempre manter o máximo de encapsulamento possível.
O que aprendemos?
O que aprendemos nessa aula:
· Diferenças entre null e undefined
· Aprofundando nos conceitos de referências a um espaço de memória
· Get e Set
· Melhor encapsulamento da classe
· Protegendo atributos sensíveis
Construtores
Agora que nossos atributos estão bem protegidos, podemos tomar um tempo para organizar o código da classe ContaCorrente, separando nossos atributos públicos e privados na parte superior, seguidos dos métodos assessores e por fim listando nossos métodos.
import { Cliente } from "./Cliente.js";
export class ContaCorrente {
 agencia;
 _cliente;
 _saldo = 0;
 set cliente(novoValor) {
 if (novoValor instanceof Cliente) {
 this._cliente = novoValor;
 }
 }
 get cliente() {
 return this._cliente;
 }
 get saldo() {
 return this._saldo;
 }
 sacar(valor) {
 if (this._saldo >= valor) {
 this._saldo -= valor;
 return valor;
 }
 }
 depositar(valor) {
 if(valor <= 0) {
 return;
 }
 this._saldo += valor;
 }
 transferir (valor, conta) {
 const valorSacado = this.sacar(valor);
 }
}COPIAR CÓDIGO
Ainda precisamos ajustar algumas coisas que não fazem sentido no sistema. Por exemplo, a classe Cliente possui um atributo cpf. Sabemos que cada pessoa possui um único cpf, que nunca (ou dificilmente) é alterado durante a sua vida. Sendo assim, seria interessante termos um cpf único para cada cliente e que ele não fosse alterável.
Uma maneira de protegermos esse atributo é tornando-o privado, o que traria a necessidade de um assessor getter para recuperá-lo.
export class Cliente {
 nome;
 _cpf;
 get cpf() {
 this._cpf;
 }
}COPIAR CÓDIGO
Entretanto, só queremos que essa atribuição seja feita no momento da criação de um novo Cliente, em nenhum outro momento do ciclo de vida do nosso objeto. Para essa e outras situações existem os construtores (constructor()), uma função especial que recebe, por exemplo, os atributos que a classe precisa para criar um objeto - nesse caso, nome e cpf.
export class Cliente {
 nome;
 _cpf;
 get cpf() {
 this._cpf;
 }
 constructor(nome, cpf) {
 }
}COPIAR CÓDIGO
Queremos, então, associar o nome e o cpf recebidos por parâmetros com os próprios atributos nome e _cpf da classe Cliente, algo que faremos com o auxílio da palavra reservada this.
export class Cliente {
 nome;
 _cpf;
 get cpf() {
 this._cpf;
 }
 constructor(nome, cpf) {
 this.nome = nome;
 this._cpf = cpf; 
 }
}COPIAR CÓDIGO
Dessa forma, no momento em que construirmos um objeto, passaremos a referência necessária de nome e cpf e dispensaremos a existência de um assessor do tipo set. Voltando ao index.js, observe o momento em que instanciamos a classe Cliente:
const cliente1 = new Cliente();COPIAR CÓDIGO
Sempre que utilizamos parênteses em JavaScript, estamos chamando um método. Nesse caso, ao instanciarmos uma classe, chamamos o seu método construtor. Como ele não existia, o próprio JavaScript se encarregava de criar um espaço na memória para um construtor implícito que simplesmente inicializa a classe, sem realizar a atribuição de nenhum parâmetro. Agora que temos um construtor explícito, poderemos passar o nome ("Ricardo") e o CPF (11122233309).
const cliente1 = new

Continue navegando