CodeGym /Blogue Java /Random-PT /Regras de codificação: da criação de um sistema ao trabal...
John Squirrels
Nível 41
San Francisco

Regras de codificação: da criação de um sistema ao trabalho com objetos

Publicado no grupo Random-PT
Bom Dia todo mundo! Hoje gostaríamos de falar com você sobre como escrever um bom código. Claro, nem todo mundo quer mastigar livros como Clean Code imediatamente, uma vez que eles contêm grandes quantidades de informações, mas não muito claro a princípio. E quando terminar de ler, você pode matar todo o seu desejo de codificar. Considerando tudo isso, hoje quero fornecer a você um pequeno guia (um pequeno conjunto de recomendações) para escrever um código melhor. Neste artigo, vamos examinar as regras e conceitos básicos relacionados à criação de um sistema e ao trabalho com interfaces, classes e objetos. A leitura deste artigo não levará muito tempo e, espero, não o aborrecerá. Vou trabalhar de cima para baixo, ou seja, da estrutura geral de um aplicativo para seus detalhes mais restritos. Regras de codificação: da criação de um sistema ao trabalho com objetos - 1

Sistemas

As seguintes são características geralmente desejáveis ​​de um sistema:
  • Complexidade mínima. Projetos excessivamente complicados devem ser evitados. O mais importante é a simplicidade e clareza (mais simples = melhor).
  • Facilidade de manutenção. Ao criar um aplicativo, lembre-se de que ele precisará de manutenção (mesmo que você não seja responsável por sua manutenção). Isso significa que o código deve ser claro e óbvio.
  • Acoplamento solto. Isso significa que minimizamos o número de dependências entre diferentes partes do programa (maximizando nossa conformidade com os princípios OOP).
  • Reutilização. Projetamos nosso sistema com a capacidade de reutilizar componentes em outras aplicações.
  • Portabilidade. Deve ser fácil adaptar um sistema a outro ambiente.
  • Estilo uniforme. Projetamos nosso sistema usando um estilo uniforme em seus vários componentes.
  • Extensibilidade (escalabilidade). Podemos aprimorar o sistema sem violar sua estrutura básica (adicionar ou alterar um componente não deve afetar todos os outros).
É praticamente impossível construir um aplicativo que não exija modificações ou novas funcionalidades. Precisamos constantemente adicionar novas peças para ajudar nossa ideia a acompanhar os tempos. É aqui que a escalabilidade entra em jogo. Escalabilidade é essencialmente estender o aplicativo, adicionar novas funcionalidades e trabalhar com mais recursos (ou seja, com uma carga maior). Ou seja, para facilitar a inclusão de novas lógicas, seguimos algumas regras, como reduzir o acoplamento do sistema aumentando a modularidade.Regras de codificação: da criação de um sistema ao trabalho com objetos - 2

Fonte da imagem

Etapas do projeto de um sistema

  1. Sistema de software. Desenhe o aplicativo como um todo.
  2. Divisão em subsistemas/pacotes. Defina partes logicamente distintas e defina as regras de interação entre elas.
  3. Divisão dos subsistemas em classes. Divida as partes do sistema em classes e interfaces específicas e defina a interação entre elas.
  4. Divisão de classes em métodos. Crie uma definição completa dos métodos necessários para uma classe, com base em sua responsabilidade atribuída.
  5. Projeto de método. Crie uma definição detalhada da funcionalidade de métodos individuais.
Normalmente, desenvolvedores comuns cuidam desse projeto, enquanto o arquiteto do aplicativo cuida dos pontos descritos acima.

Princípios e conceitos gerais de projeto de sistema

Inicialização preguiçosa. Nesse idioma de programação, o aplicativo não perde tempo criando um objeto até que ele seja realmente usado. Isso acelera o processo de inicialização e reduz a carga no coletor de lixo. Dito isso, você não deve ir longe demais, porque isso pode violar o princípio da modularidade. Talvez valha a pena mover todas as instâncias de construção para alguma parte específica, por exemplo, o método principal ou para uma classe de fábrica . Uma característica de um bom código é a ausência de código clichê repetitivo. Como regra, esse código é colocado em uma classe separada para que possa ser chamado quando necessário.

AOP

Também gostaria de observar a programação orientada a aspectos. Esse paradigma de programação trata da introdução de lógica transparente. Ou seja, o código repetitivo é colocado em classes (aspectos) e é chamado quando certas condições são satisfeitas. Por exemplo, ao chamar um método com um nome específico ou acessar uma variável de um tipo específico. Às vezes, os aspectos podem ser confusos, pois não está claro de onde o código está sendo chamado, mas ainda assim é uma funcionalidade muito útil. Especialmente ao armazenar em cache ou registrar. Adicionamos essa funcionalidade sem adicionar lógica adicional às classes comuns. As quatro regras de Kent Beck para uma arquitetura simples:
  1. Expressividade — A intenção de uma aula deve ser claramente expressa. Isso é alcançado por meio de nomenclatura adequada, tamanho pequeno e adesão ao princípio da responsabilidade única (que consideraremos com mais detalhes abaixo).
  2. Número mínimo de classes e métodos — Em seu desejo de tornar as classes tão pequenas e focadas quanto possível, você pode ir longe demais (resultando no antipadrão de cirurgia de espingarda). Este princípio exige manter o sistema compacto e não ir longe demais, criando uma classe separada para cada ação possível.
  3. Sem duplicação — O código duplicado, que cria confusão e é uma indicação de design de sistema abaixo do ideal, é extraído e movido para um local separado.
  4. Executa todos os testes — Um sistema que passa em todos os testes é gerenciável. Qualquer mudança pode fazer com que um teste falhe, revelando-nos que nossa mudança na lógica interna de um método também mudou o comportamento do sistema de maneiras inesperadas.

SÓLIDO

Ao projetar um sistema, vale a pena considerar os conhecidos princípios SOLID:

S (responsabilidade única), O (aberto-fechado), L (substituição de Liskov), I (segregação de interface), D (inversão de dependência).

Não vamos nos deter em cada princípio individual. Isso estaria um pouco além do escopo deste artigo, mas você pode ler mais aqui .

Interface

Talvez uma das etapas mais importantes na criação de uma classe bem projetada seja criar uma interface bem projetada que represente uma boa abstração, ocultando os detalhes de implementação da classe e simultaneamente apresentando um grupo de métodos claramente consistentes entre si. Vamos dar uma olhada mais de perto em um dos princípios do SOLID — segregação de interface: clientes (classes) não devem implementar métodos desnecessários que não usarão. Em outras palavras, se estamos falando em criar uma interface com o menor número de métodos destinados a realizar o único trabalho da interface (o que eu acho muito semelhante ao princípio da responsabilidade única), é melhor criar alguns menores em vez disso de uma interface inchada. Felizmente, uma classe pode implementar mais de uma interface. Lembre-se de nomear suas interfaces corretamente: o nome deve refletir a tarefa atribuída com a maior precisão possível. E, claro, quanto mais curto for, menos confusão causará. Comentários de documentação geralmente são escritos no nível da interface. Esses comentários fornecem detalhes sobre o que cada método deve fazer, quais argumentos ele usa e o que retornará.

Aula

Regras de codificação: da criação de um sistema ao trabalho com objetos - 3

Fonte da imagem

Vamos dar uma olhada em como as classes são organizadas internamente. Ou melhor, algumas perspectivas e regras que devem ser seguidas ao escrever aulas. Como regra, uma classe deve começar com uma lista de variáveis ​​em uma ordem específica:
  1. constantes estáticas públicas;
  2. constantes estáticas privadas;
  3. variáveis ​​de instância privadas.
Em seguida, vêm os vários construtores, desde aqueles com menos argumentos até aqueles com mais. Eles são seguidos por métodos do mais público ao mais privado. De um modo geral, os métodos privados que ocultam a implementação de alguma funcionalidade que queremos restringir estão na parte inferior.

Tamanho da turma

Agora eu gostaria de falar sobre o tamanho das turmas. Vamos relembrar um dos princípios SOLID — o princípio da responsabilidade única. Afirma que cada objeto tem apenas um propósito (responsabilidade), e a lógica de todos os seus métodos visa realizá-lo. Isso nos diz para evitar classes grandes e inchadas (que são, na verdade, o antipadrão do objeto Deus) e, se tivermos muitos métodos com todos os tipos de lógica diferente amontoados em uma classe, precisamos pensar em separá-los em um par de partes lógicas (classes). Isso, por sua vez, aumentará a legibilidade do código, pois não demorará muito para entender o propósito de cada método se soubermos o propósito aproximado de qualquer classe dada. Além disso, fique de olho no nome da classe, que deve refletir a lógica que ela contém. Por exemplo, se tivermos uma classe com mais de 20 palavras em seu nome, precisamos pensar em refatoração. Qualquer classe que se preze não deve ter tantas variáveis ​​internas. Na verdade, cada método trabalha com um ou alguns deles, causando muita coesão dentro da classe (que é exatamente como deveria ser, já que a classe deve ser um todo unificado). Como resultado, aumentar a coesão de uma turma leva a uma redução no tamanho da turma e, claro, o número de turmas aumenta. Isso é irritante para algumas pessoas, já que você precisa examinar mais os arquivos de classe para ver como uma tarefa grande específica funciona. Acima de tudo, cada classe é um pequeno módulo que deve estar minimamente relacionado com os demais. Esse isolamento reduz o número de alterações que precisamos fazer ao adicionar lógica adicional a uma classe. cada método trabalha com um ou alguns deles, causando muita coesão dentro da classe (que é exatamente como deveria ser, já que a classe deve ser um todo unificado). Como resultado, aumentar a coesão de uma turma leva a uma redução no tamanho da turma e, claro, o número de turmas aumenta. Isso é irritante para algumas pessoas, já que você precisa examinar mais os arquivos de classe para ver como uma tarefa grande específica funciona. Acima de tudo, cada classe é um pequeno módulo que deve estar minimamente relacionado com os demais. Esse isolamento reduz o número de alterações que precisamos fazer ao adicionar lógica adicional a uma classe. cada método trabalha com um ou alguns deles, causando muita coesão dentro da classe (que é exatamente como deveria ser, já que a classe deve ser um todo unificado). Como resultado, aumentar a coesão de uma turma leva a uma redução no tamanho da turma e, claro, o número de turmas aumenta. Isso é irritante para algumas pessoas, já que você precisa examinar mais os arquivos de classe para ver como uma tarefa grande específica funciona. Acima de tudo, cada classe é um pequeno módulo que deve estar minimamente relacionado com os demais. Esse isolamento reduz o número de alterações que precisamos fazer ao adicionar lógica adicional a uma classe. A coesão da classe leva a uma redução no tamanho das turmas e, claro, o número de turmas aumenta. Isso é irritante para algumas pessoas, já que você precisa examinar mais os arquivos de classe para ver como uma tarefa grande específica funciona. Acima de tudo, cada classe é um pequeno módulo que deve estar minimamente relacionado com os demais. Esse isolamento reduz o número de alterações que precisamos fazer ao adicionar lógica adicional a uma classe. A coesão da classe leva a uma redução no tamanho das turmas e, claro, o número de turmas aumenta. Isso é irritante para algumas pessoas, já que você precisa examinar mais os arquivos de classe para ver como uma tarefa grande específica funciona. Acima de tudo, cada classe é um pequeno módulo que deve estar minimamente relacionado com os demais. Esse isolamento reduz o número de alterações que precisamos fazer ao adicionar lógica adicional a uma classe.

Objetos

Encapsulamento

Aqui falaremos primeiro sobre um princípio OOP: encapsulamento. Ocultar a implementação não equivale a criar um método para isolar variáveis ​​(restringir o acesso por meio de métodos individuais, getters e setters, o que não é bom, pois todo o ponto de encapsulamento é perdido). O acesso oculto visa formar abstrações, ou seja, a classe fornece métodos concretos compartilhados que usamos para trabalhar com nossos dados. E o usuário não precisa saber exatamente como estamos trabalhando com esses dados — funciona e basta.

Lei de Deméter

Também podemos considerar a Lei de Deméter: é um pequeno conjunto de regras que auxilia no gerenciamento da complexidade no nível de classe e método. Suponha que temos um objeto Car e ele tem um método move(Object arg1, Object arg2) . De acordo com a Lei de Demeter, este método é limitado a chamar:
  • métodos do próprio objeto Car (em outras palavras, o objeto "this");
  • métodos de objetos criados dentro do método move ;
  • métodos de objetos passados ​​como argumentos ( arg1 , arg2 );
  • métodos de objetos Car internos (novamente, "this").
Em outras palavras, a Lei de Deméter é algo como o que os pais podem dizer a uma criança: "você pode conversar com seus amigos, mas não com estranhos".

Estrutura de dados

Uma estrutura de dados é uma coleção de elementos relacionados. Ao considerar um objeto como uma estrutura de dados, existe um conjunto de elementos de dados sobre os quais os métodos operam. A existência desses métodos é implicitamente assumida. Ou seja, uma estrutura de dados é um objeto cuja finalidade é armazenar e trabalhar com (processar) os dados armazenados. Sua principal diferença em relação a um objeto regular é que um objeto comum é uma coleção de métodos que operam em elementos de dados cuja existência é implicitamente assumida. Você entende? O principal aspecto de um objeto comum são os métodos. Variáveis ​​internas facilitam seu correto funcionamento. Mas em uma estrutura de dados, os métodos existem para dar suporte ao seu trabalho com os elementos de dados armazenados, que são fundamentais aqui. Um tipo de estrutura de dados é um objeto de transferência de dados (DTO). Esta é uma classe com variáveis ​​públicas e sem métodos (ou apenas métodos para leitura/escrita) que é usada para transferir dados ao trabalhar com bancos de dados, analisar mensagens de soquetes, etc. Os dados geralmente não são armazenados nesses objetos por um longo período. É quase imediatamente convertido para o tipo de entidade que nosso aplicativo trabalha. Uma entidade, por sua vez, também é uma estrutura de dados, mas sua finalidade é participar da lógica de negócios em vários níveis da aplicação. O objetivo de um DTO é transportar dados de/para o aplicativo. Exemplo de DTO: também é uma estrutura de dados, mas sua finalidade é participar da lógica de negócios em vários níveis do aplicativo. O objetivo de um DTO é transportar dados de/para o aplicativo. Exemplo de DTO: também é uma estrutura de dados, mas sua finalidade é participar da lógica de negócios em vários níveis do aplicativo. O objetivo de um DTO é transportar dados de/para o aplicativo. Exemplo de DTO:

@Setter
@Getter
@NoArgsConstructor
public class UserDto {
    private long id;
    private String firstName;
    private String lastName;
    private String email;
    private String password;
}
Tudo parece bastante claro, mas aqui aprendemos sobre a existência de híbridos. Híbridos são objetos que possuem métodos para lidar com lógica importante, armazenar elementos internos e também incluir métodos de acesso (get/set). Esses objetos são confusos e dificultam a adição de novos métodos. Você deve evitá-los, porque não está claro para que servem — armazenar elementos ou executar lógica?

Princípios de criação de variáveis

Vamos refletir um pouco sobre variáveis. Mais especificamente, vamos pensar sobre quais princípios se aplicam ao criá-los:
  1. Idealmente, você deve declarar e inicializar uma variável antes de usá-la (não crie uma e esqueça dela).
  2. Sempre que possível, declare as variáveis ​​como final para evitar que seu valor mude após a inicialização.
  3. Não se esqueça das variáveis ​​de contador, que geralmente usamos em algum tipo de loop for . Ou seja, não se esqueça de zerá-los. Caso contrário, toda a nossa lógica pode quebrar.
  4. Você deve tentar inicializar variáveis ​​no construtor.
  5. Se houver a opção de usar um objeto com referência ou sem ( new SomeObject() ), opte por sem, pois após o uso do objeto ele será deletado no próximo ciclo de coleta de lixo e seus recursos não serão desperdiçados.
  6. Mantenha o tempo de vida de uma variável (a distância entre a criação da variável e a última vez que ela é referenciada) o mais curto possível.
  7. Inicialize as variáveis ​​usadas em um loop logo antes do loop, não no início do método que contém o loop.
  8. Sempre comece com o escopo mais limitado e expanda apenas quando necessário (você deve tentar tornar uma variável o mais local possível).
  9. Use cada variável para um único propósito.
  10. Evite variáveis ​​com um propósito oculto, por exemplo, uma variável dividida entre duas tarefas — isso significa que seu tipo não é adequado para resolver uma delas.

Métodos

Regras de codificação: da criação de um sistema ao trabalho com objetos - 4

do filme "Star Wars: Episódio III - A Vingança dos Sith" (2005)

Vamos direto para a implementação da nossa lógica, ou seja, para os métodos.
  1. Regra #1 — Compacidade. Idealmente, um método não deve exceder 20 linhas. Isso significa que, se um método público "incha" significativamente, você precisa pensar em separar a lógica e movê-la para métodos privados separados.

  2. Regra nº 2 — if , else , while e outras instruções não devem ter blocos fortemente aninhados: muito aninhamento reduz significativamente a legibilidade do código. Idealmente, você não deve ter mais do que dois blocos {} aninhados .

    E também é desejável manter o código nesses blocos compacto e simples.

  3. Regra #3 — Um método deve realizar apenas uma operação. Ou seja, se um método realiza todo tipo de lógica complexa, nós o dividimos em submétodos. Como resultado, o próprio método será uma fachada cujo objetivo é chamar todas as outras operações na ordem correta.

    Mas e se a operação parecer simples demais para ser colocada em um método separado? É verdade que às vezes pode parecer disparar um canhão contra pardais, mas pequenos métodos oferecem várias vantagens:

    • Melhor compreensão do código;
    • Os métodos tendem a se tornar mais complexos à medida que o desenvolvimento avança. Se um método for simples para começar, será um pouco mais fácil complicar sua funcionalidade;
    • Os detalhes da implementação estão ocultos;
    • Reutilização de código mais fácil;
    • Código mais confiável.

  4. A regra stepdown — O código deve ser lido de cima para baixo: quanto mais baixo você lê, mais fundo você se aprofunda na lógica. E vice-versa, quanto mais alto você for, mais abstratos serão os métodos. Por exemplo, as instruções switch não são compactas e são indesejáveis, mas se você não puder evitar o uso de um switch, tente movê-lo o mais baixo possível, para os métodos de nível mais baixo.

  5. Argumentos do método — Qual é o número ideal? Idealmente, nenhum :) Mas isso realmente acontece? Dito isso, você deve tentar ter o mínimo de argumentos possível, porque quanto menos, mais fácil é usar um método e mais fácil é testá-lo. Na dúvida, tente antecipar todos os cenários para usar o método com um grande número de parâmetros de entrada.

  6. Além disso, seria bom separar os métodos que possuem um sinalizador booleano como parâmetro de entrada, pois isso por si só implica que o método executa mais de uma operação (se for verdadeiro, faça uma coisa; se for falso, faça outra). Como escrevi acima, isso não é bom e deve ser evitado, se possível.

  7. Se um método tiver um grande número de parâmetros de entrada (um extremo é 7, mas você deve realmente começar a pensar depois de 2-3), alguns dos argumentos devem ser agrupados em um objeto separado.

  8. Se houver vários métodos semelhantes (sobrecarregados), os parâmetros semelhantes devem ser passados ​​na mesma ordem: isso melhora a legibilidade e a usabilidade.

  9. Quando você passa parâmetros para um método, você deve ter certeza de que todos eles são usados, caso contrário, por que você precisa deles? Corte quaisquer parâmetros não utilizados da interface e termine com isso.

  10. try/catch não parece muito bom por natureza, então seria uma boa ideia movê-lo para um método intermediário separado (um método para lidar com exceções):

    
    public void exceptionHandling(SomeObject obj) {
        try {  
            someMethod(obj);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    

Falei sobre código duplicado acima, mas deixe-me repetir mais uma vez: Se tivermos alguns métodos com código repetido, precisamos movê-lo para um método separado. Isso tornará o método e a classe mais compactos. Não se esqueça das regras que regem os nomes: detalhes sobre como nomear corretamente classes, interfaces, métodos e variáveis ​​serão discutidos na próxima parte do artigo. Mas isso é tudo que tenho para você hoje.
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION