DEV Community

Kauê Gatto
Kauê Gatto

Posted on

4 2 2 2 2

Encapsulamento: O Básico que todo jr. precisa saber!

Leitura no Notion: Boas práticas
Leitura no Notion: Mais sobre encapsulamento


Mudanças em Cascata, Menos pontos de contato

Encapsulamento é um princípio fundamental da programação orientada a objetos (POO) que ajuda a controlar o acesso e a modificação de dados dentro de uma classe. Ele se refere à prática de esconder os detalhes de implementação de uma classe de códigos externos e expor apenas uma interface pública para interagir com a classe. Isso pode ajudar a evitar mudanças em cascata em um sistema de software limitando o número de pontos de contato entre diferentes partes do código.

Uma maneira de alcançar o encapsulamento (pelo menos em uma linguagem OO) é usando modificadores de acesso como "private" ou "protected" em campos e métodos de classe. Isso pode evitar que códigos externos acessem ou modifiquem diretamente o estado interno da classe e forçar o uso de métodos ou propriedades públicas que fornecem uma forma controlada de interagir com a classe.

Essa não é a única maneira e nem todos os atributos de uma classe devem ter cegamente getters e setters, faz sentido uma pessoa depois de sua criação ter seu id alterado? Esse setter tem que existir, realmente? Outra ocasião importante que indica falta de encapsulamento é comentada no ponto 1 do capítulo de 3. Refatoração → Casos Usuais.

class MyClass {
    private int data;

    public int getData() {
        return data;
    }

    public void setData(int newData) {
        data = newData;
    }
}
Enter fullscreen mode Exit fullscreen mode

O encapsulamento também pode ser usado para evitar mudanças em cascata mantendo o número de pontos de contato entre diferentes partes do código o mínimo possível. Ou seja, ao invés de termos em 5 lugares diferentes o código Float.parseFloat(getData()) poderíamos encapsular isso dentro de um método na classe Data se isso for uma regra de Data ou um comportamento relacionado à ela (mesmo que não seja, provavelmente é um comportamento de alguma outra classe). Depois dessa refatoração, ao mexermos nesse método, cuidaremos dos 5 pontos que o utilizam, ao invés de ficar buscando por ai a linha de Float.parseFloat(getData()) e quebrarmos o código pois não encontramos uma ocorrência.

Devo abstrair um determinado parâmetro/retorno?

Já dissemos muito sobre abstrações e encapsulamento, mas quando você deve abstrair um certo parâmetro ou retorno? Aquele atributo String CPF na sua classe é realmente prejudicial? Se sim, vale a pena criar uma classe para defini-lo?

Suponha que você precise criar um objeto Usuário, que tem como parâmetro do construtor um nome e senha.

class Usuario{
    ...
    public Usuario (String nome, String senha){}
}
Usuario user1 = new Usuario("kaue", "123456");
Enter fullscreen mode Exit fullscreen mode

Nesse caso, como você pode garantir que usuário deve receber sua senha como plain text, e não depois de passar por um hash? Ou que a senha deve ter ao menos 6 caracteres, isso foge da lógica e semântica estabelecida pela tipo String, apesar disso, não é uma validação que dá muito trabalho, um simples length já resolveria o problema do tamanho, nesse caso, não acho que valeria criar uma classe somente para isso, mas quanto ao problema do hash, como validaríamos que o cliente usou corretamente o construtor e passou uma senha como plain text?

É simples, somente ver a implementação da classe Usuário, mas se isso não está claro, temos ai um code smell, uma espécie de acoplamento mental. Se você não tivesse acesso à Usuário, não teria como adivinhar. Nesse caso em específico, mudar o nome da variável para plainText seria uma solução caso.

Contudo, se a solução não for tão simples, acredito que normalmente passa a valer a pena criar uma classe de domínio para o parâmetro (ou retorno). Uma classe Senha com um construtor privado e métodos factory seria uma alternativa.

Untitled

Trazendo isso para um exemplo real, durante o handling de metadatas as guardávamos como Map<String,Object> , repetidos em diversos lugares, apesar disso, usávamos o Map em seu exato contexto, sem a necessidade de métodos a mais além dos próprios do Map e o seu contexto e significado era exatamente o que um map representava, nesse caso, não sentimos necessidade de abstrair o tipo.

Protegendo Fronteiras

As fronteiras são a parte do seu software que agem como portas ao mundo externo. No caso de uma API, os seus clientes e pessoas que chamam a API também são externos, nenhum dado externo deve ser confiado.

Proteger as fronteiras de um software refere-se a garantir que as interfaces externas e entradas do software sejam validadas e sanitizadas adequadamente para evitar ataques maliciosos ou entradas inesperadas. Isso é uma parte importante da segurança de software e pode ajudar a prevenir problemas como SQL Injection, cross-site scripting (XSS) e outros tipos de ataques de injeção.

Uma das principais partes de proteger as fronteiras de software é a validação de entrada (input validation). Isso envolve verificar todos os dados de entrada para garantir que eles atendam a certos critérios antes de serem processados. Por exemplo, uma entrada de formulário pode ser necessária para ter um certo comprimento ou estar em um formato específico. Isso pode ajudar a prevenir erros e comportamentos inesperados, além de proteger contra entradas maliciosas.

Outra parte importante de proteger as fronteiras de software é a sanitização. Isso envolve remover ou modificar qualquer elemento potencialmente prejudicial de dados de entrada. Por exemplo, uma aplicação web pode sanitizar a entrada de usuários para remover qualquer código JavaScript ou HTML que possa ser usado para realizar um ataque cibernético.

O DDD (Domain Driven Design) também desempenha um papel importante na proteção das fronteiras de software. O DDD é uma abordagem de design de software que se concentra no domínio empresarial e enfatiza a importância de criar uma separação clara entre a lógica do domínio e os detalhes técnicos da implementação. Isso pode ajudar a garantir que as interfaces externas do software sejam bem definidas e fáceis de entender, o que pode tornar mais fácil validar e sanitizar entradas.

Serializar classes de domínio → não fazer

Ao expor os objetos do domínio através de uma API, serializá-los e enviá-los diretamente para o cliente como uma resposta, pode levar a uma série de problemas:

  • O cliente pode potencialmente modificar o estado do objeto do domínio e violar as regras do domínio.
  • O objeto do domínio pode conter informações sensíveis que não devem ser expostas ao cliente.
  • O objeto do domínio pode conter informações que não são relevantes para o cliente e podem levar à coleta excessiva de dados.

Para evitar esses problemas, é recomendado criar um DTO (Objeto de Transferência de Dados) separado que é especificamente projetado para a API, ele deve conter apenas as informações que o cliente precisa e não deve conter nenhum comportamento ou métodos que possam mudar o estado do objeto do domínio. Isso ajudará a proteger a integridade do modelo de domínio e manter o cliente desacoplado dos detalhes de implementação interna dos objetos do domínio.


Princípio: Favorecemos coesão através do encapsulamento

O Básico

O Encapsulamento é basicamente o ato de juntar comportamentos e estados que fazem sentido no mesmo lugar, garantindo maior coesão ao código, é um conceito básico que precisa ser dominado. Veja esse código em um repositório de um framework da Apache

for (Address address : vcard.getAddresses()) {
                    boolean workAddress = false;
                    for (AddressType addressType : address.getTypes()) {
                        if (AddressType.PREF.equals(addressType) || AddressType.WORK.equals(addressType)) {
                            workAddress = true;
                            break;
                        }
                    }
                    if (!workAddress) continue;
Enter fullscreen mode Exit fullscreen mode

Sem entender muito do código e de seu contexto, já somos capaz de refatorar isso de uma maneira melhor, poderíamos simplesmente usar:

if(!adress.hasWorkAdress()) continue;
Enter fullscreen mode Exit fullscreen mode

Com isso, o código escrito na classe ficaria mais legível e a função de descobrir se há endereço de trabalho ou não, passa a ser da classe Adress e pode ser replicado sem problemas através de toda a aplicação. Se decidirmos mudar a regra de negócio no estado atual, teríamos que verificar por esse imenso código esparramado por todo o programa, o que não acontece no código refatorado.

Apenas com essa alteração:

  • O service fica mais legível
  • O service fica mais testável
  • O código é reaproveitado e muda junto com apenas um ponto de contato (dentro da classe Adres)
  • O service tem maior complexidade
    • A complexidade não surge, ela é distribuída, a regra já existe.

Como podemos detectar isso? Um forte indicativo que algo está estranho é estarmos usando um estado interno e aplicando lógica em cima desse estado interno fora de sua classe.

Métodos privados também podem indicar esse tipo de comportamento, talvez até mesmo a necessidade do nascimento de novas entidades.

private Person createUserAccount(String username, Collection<GrantedAuthority> authorities,PersonAttributesLookup personAttributesLookup) {
        Person person = null;

        if (hasAccountCreationPermission(authorities)) {
            person = new Person();
            person.setEnabled(true);
            person.setUsername(username);

            try {
                // Get the Person Attributes to create the person
                final PersonAttributesResult attr =
                        personAttributesLookup.lookupPersonAttributes(username);
                person.setSchoolId(attr.getSchoolId());
                person.setFirstName(attr.getFirstName());
                person.setLastName(attr.getLastName());
                person.setPrimaryEmailAddress(attr.getPrimaryEmailAddress());

                ensureRequiredFieldsForDirectoryPerson(person);
                person = create(person);
                externalPersonService.updatePersonFromExternalPerson(person, false);
                LOGGER.info("Successfully Created Account for {}", username);

            } catch (final ObjectNotFoundException onfe) {
                ...
            }
Enter fullscreen mode Exit fullscreen mode

Refatorando:

                person.setSchoolId(attr.getSchoolId());
                person.setFirstName(attr.getFirstName());
                person.setLastName(attr.getLastName());
                person.setPrimaryEmailAddress(attr.getPrimaryEmailAddress());
// passa a se tornar
public Class Person{
    ...
    Person setAttributesBasedOnAttributesResult(PersonAttributesResult attr){
                this.person.setSchoolId(attr.getSchoolId());
                this.person.setFirstName(attr.getFirstName());
                this.person.setLastName(attr.getLastName());
                this.person.setPrimaryEmailAddress(attr.getPrimaryEmailAddress());

    }
}
// seria usado:
person.setAttributesBasedOnAttributesResult(attr);
Enter fullscreen mode Exit fullscreen mode

Somente nessa alteração, já travamos setters que podem não ser interessantes para o negócio (como alterar o schoolId sem alterar o emailAdress, se isso for uma regra existente), ou seja, o código antigo estava desprotegido e acoplado mentalmente à alguma regra externa, precisamos sempre que atualizarmos o primeiro nome atualizar também o segundo? não sei. Se sim, podemos fazer com que o encapsulamento garanta isso.

Outro ponto de refatoramento em Person é essa parte:

        person = new Person();
        person.setEnabled(true);
        person.setUsername(username);
Enter fullscreen mode Exit fullscreen mode

Talvez os métodos de setEnabled e setUsername sejam necessários, ou seja, não faz sentido criarmos um usuário sem Username e desabilitado, por que isso não faz parte do construtor? Esses setters provavelmente nem deveriam existir, com os atributos setados no próprio construtor. Tratamos isso no capítulo de refatoração

O código final poderia ficar:

private Person createUserAccount(String username, Collection<GrantedAuthority> authorities,PersonAttributesLookup personAttributesLookup) {
        Person person = null;
        if (hasAccountCreationPermission(authorities)) {
            person = new Person();
            try {
                // Get the Person Attributes to create the person
                final PersonAttributesResult attr =
                        personAttributesLookup.lookupPersonAttributes(username);
                person.setAttributesBasedOnAttributesResult(attr);

                ensureRequiredFieldsForDirectoryPerson(person);
                person = create(person);
                externalPersonService.updatePersonFromExternalPerson(person, false);
                LOGGER.info("Successfully Created Account for {}", username);

            } catch (final ObjectNotFoundException onfe) {
                ...
            }     
Enter fullscreen mode Exit fullscreen mode

Disclaimer!

Não leve tudo que leu aqui como verdades absolutas ou regras a serem seguida em 100% dos casos, não existe martelo de ouro nem bala de prata. Se houver algum questionamento ou sugestão, pode deixar aqui!

Referências:

Encapsulamento para ganhar mais coesão: O feijão com arroz que precisa estar dominado

OOP E Solid para ninjas - Casa do código - Aniche

Desbravando SOLID - Casa do Código - Aquiles

Billboard image

Synthetic monitoring. Built for developers.

Join Vercel, Render, and thousands of other teams that trust Checkly to streamline monitor creation and configuration with Monitoring as Code.

Start Monitoring

Top comments (2)

Collapse
 
marlonhenq profile image
Marlon

Pô, muito bacana o artigo, parabéns!

Collapse
 
kauegatto profile image
Kauê Gatto

Valeuu!! espero que tenha sído útil

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more