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

kauegatto

Kauê Gatto

Posted on October 25, 2023

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

💖 💪 🙅 🚩
kauegatto
Kauê Gatto

Posted on October 25, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related