Trarei um artigo resumindo os capítulos do nível intermediário do Código limpo. Focando no Capítulo 6: Objetos e Estruturas de Dados e Capítulo 10: Classes. Claro que citarei os demais capítulos, mas focarei nesses dois a princípio.
Capítulo 6 - Objetos e Estruturas de Dados
Se você já programou em uma linguagem tipada ou até uma linguagem que possuísse modularização de um agrupamento de valores determinados em classe, você já se deparou com o primeiro caso de estudo que o nosso tio Beto expõe. A separação do conceito do que é Objeto e Estrutura de dados.
Objetos são classes com valores agrupados e privados que possuem certa lógica relacional e os seus valores são protegidos e/ou privados para demais acessos. É recomendado possuirem uma assinatura de classe, uma interface. Por exemplo:
public interface class RubberDuckObject{
String quackQuack();
Future<void> floats();
Future<Think> helpThink();
}
public class RubberDuckImpl implements RubberDuckObject{
//numa folha qualquer eu desenho um código amarelo...
}
Qual é a implementação desse objeto? não importa no momento. Importou ao desenvolvedor que a desenvolveu. Entende o porquê dela existir? Esse nível de abstração é que te dá a segurança que o problema dos outros nesse objeto não vão ser os meus ao utilizá-la (só acaso o método não esteja correto, o que ocorre quase sempre).
Se os objetos não expõe os seus valores privados, as estruturas de dados por outro lado são uma entidade que tem a função de aglomerar dados relacionados e expor para a utilização. As funções que a estrutura tem estão relacionadas a própria estrutura.
public class Person{
final int age;
final String name;
final Datetime dateOfBirth;
final bool isAlive;
final Datetime? dateOfDeath;
Person({
required age,
required name
required dateOfBirth,
required isAlive,
dateOfDeath,
});
factory Person.fromMap(Map map) => Person(
age: map['age'],
name: map['name']
dateOfBirth: map['dateOfBirth'],
isAlive:map['isAlive'],
dateOfDeath:Datetime.format(map['dateOfDeath']) ?? null,
);
String toMap () =>{
'age': this.age,
'name': this.name,
'dateOfBirth': this.dateOfBirth.toIso8601String(),
'isAlive': this.isAlive,
'dateOfDeath': this.dateOfDeath!.toIso8601String() ?? null,
}
}
Ainda dentro de estruturas de dados, podemos criar uma interface para correlacionar estruturas que tem características semelhantes, porém alguns atributos diferentes. Essa correlação é chamada de polimofismo.
abstract interface class Pokemon{
Type type;
double height;
double weight;
List<Ability> Abilities;
}
class Charizard implements Pokemon{
final bool isShine;
final Damage damage;
Pokemon({super, required isShine, required damage});
}
class Jigglypuff implements Pokemon{
final bool isArtist;
final bool isToxic;
final Music jigglyMusic;
Pokemon({super.this,
required isArtist,
required isToxic,
required jigglyMusic,
});
}
Sempre que necessário, você pode fazer uma fusão entre os dois tipos, gerando um tipo híbrido. Um tipo híbrido é utilizado quando você precisa das funções relacionadas a uma classe com variáveis (ou outros objetos) privados, mas possui a injeção de de informações sendo necessárias. Olha, é uma confusão a parte, faça por sua conta e risco, eu não vou reproduzir por aqui. Só saiba que existe e não é errado... eu só não recomendo. Ou recomendo. Vai depender muito do contexto. Em geral não. Separe frei Damião de frei de caminhão.
Capítulo 7 - Tratamento de erros
Como o tio Beto é javista, não posso deixar de ressaltar que muita coisa do capítulo é desnecessária se você nunca tocou em Java na sua vida. Tentarei sintetizar o que realmente importa sobre o tratamento de erros. Faça.
Ok, mas como fazer o tratamento de erro certinho? Primeiro, devemos verificar a fonte do erro. É a execução de uma função assíncrona? é uma operação que pode me retornar um valor nulo? é um acesso que foi fora da curva? Whatever. Utilizaremos a estrutura try catch para filtrar a existência daquele erro, ou seja, evitar que o erro se torne uma exception de sistema e encerre a execução do fluxo normal do seu programa. Vamos ao exemplo:
Future<void> callMeByYourName(String yourName){
try{
await you.call(person: me, call: yourName);
}catch(Exception e){
log('Oh, no! you call me Lorem!');
}
}
No código, A estrutura try tem como escopo o método call que é um método assíncrono e que pode ser ou não executado, como todo método assíncrono corre o risco. Caso execute sem erro algum, o código prossegue sem a participação do método catch. Se houver um erro, o método catch irá receber uma exceção e decidir como comunicá-la ao resto do sistema, o que chamamos de tratamento de erro. O código ali em cima é apenas um exemplo seboso, não reproduza em produção! log não é tratamento de erro... é ferramenta de debug.
Agora você me pergunta, o que podemos fazer com esta exceção além de encarar o nosso iminente fracasso e se lembrar de todo aquele bullying sofrido na escola, na universidade, no trabalho? Podemos transformar ele, com terapia? nah, com Orientação a objetos e estrutura de dados. Ainda estamos falando de código.
Para o tratamento de erro ser eficaz precisamos ter o mínimo controle de onde a exceção vem, mas como podemos fazer isso se uma Exception é uma Exception? Utilizando o conceito visto anteriormente em estruturas. Polimorfismo.
Vamos criar uma exception apenas para a função callMeByYourName.
class CallMeByYourNameFailure implements Exception{
//Aqui virá todos os métodos override que a class Exception tiver na linguagem
}
Agora com a nova exceção, podemos voltar ao código ruim, digo, código em construção lá de cima e consertá-lo.
Future<void> callMeByYourName(String yourName){
try{
await you.call(person: me, call: yourName);
}catch(Exception e){
throw new CallMeByYourNameFailure(message:'Oh, no! you call me Lorem!');
}
}
Como você vai modificar seus erros, como vai alertar ao usuário, se vai fazer alguma coisa com eles afinal, fica a seu critério. Porém é importante saber que é boa prática dar um fim digno as suas falhas (momento terapia).
Capítulo 8 - Limites
Este capítulo é voltado para pessoas desenvolvedoras que trabalham com códigos de terceiros e muitas vezes tem acesso restrito, muito pouco acesso ao seu código fonte, ou código que ainda não existe. A regra geral que o tio Beto dá é abstrações. Utilize interfaces, repositories e usecases para evitar que a presença do código do terceiro se enlace com o seu código.
class FairyBaseRepository {
Future<Either<bool, FairyBaseLoginFailure>> login(User user);
}
class FairyBaseRepositoryImpl implements FairyBaseRepository{
final FairyBase fairyBase = new FairyBase().open();
Future<Either<bool, FairyBaseLoginFailure>> login(User user){
try{
final result = await fairyBase.login(user.json);
}catch(Exception e){
throw new FairyBaseLoginFailure(message: 'erro no login');
}
}
}
O repositório vai tratar com o objeto fictício de conexão, não sabemos o que é o fairyBase só que ele faz o login. Enfim, tratamos o erro e chegamos ao Usecase:
class Login extends Usecase{
Future<bool> call({required name, required password}){\
final user = User(name, password);
return await fairyBaseRepository.login(user);
}
}
Limites são apenas uma garantia de que você não precisa ter dor de cabeça se precisar arrancar o código de terceiros do seu, ser fácil e indolor.
Capítulo 9 - Testes
Para quê, não é mesmo? brincadeira, brincadeira. Vamos pensar que testes são "necessários" para um sistema. Não trataremos testes como um código a parte. Testes são parte do seu código, ou seja, podem seguir as dicas vistas até o presente momento.
Primeiro, simplique seus testes. Se as funções devem ser simples, como vimos no Capítulo 3, os testes também. Vamos ao exemplo:
test('Should return the name when create a Person', () {
final Person person = Person(name:'Tio Beto', year:71, isProgrammer: true);
expect('Tio Beto', person.name);
});
Um teste curto, com apenas uma funcionalidade testada. Simples e limpo. Todos os testes vão ser assim? claro que não, mas é desejável ser unitário.
Além da característica de ter todas as outras dicas do Tio Beto citadas anteriormente para código em produção, os testes para serem limpos seguem cinco regrinhas definidas pelos chatos... digo, senhores, do Agility: F.I.R.S.T.
Fast (rapidez): Os testes devem ser executados rapidamente, se estão demorando, estão errados;
Independent (independência): nenhum teste depende de outro teste ou de estrutura para execução;
Repeatable (repetitividade): os testes devem ser repetidos em qualquer ambiente, em qualquer momento, em qualquer lugar;
Self-validating (auto validação): Ou é ou não é! testes são booleanos. São feitos para dar certo ou para dar errado;
Timely (pontualidade): Devem ser escritos no tempo de desenvolvimento. Pode ser antes, se o seu time utiliza TDD. Nunca depois.
O assunto teste é imenso, um dia talvez eu volte a tratar disso por aqui, por enquanto, eu vou fingir que eles não existem... só por enquanto.
Capítulo 10 - Classes
Já falamos de Estruturas de dados, falamos de objetos, agora vamos falar da sua generalização. Classes. Você me pergunta, porque carvalhos verdes e eretos este assunto é abordado ao final de todo esse rolê? E eu te respondo: não faço a mínima ideia. Eu chuto mau gosto do Tio Beto (se você é engenheiro ou arquiteto de software e tem a resposta, por favor, deixe nos comentários. Eu realmente quero saber).
O primeiro tópico abordado é a organização. O Tio Beto, como já citei é da igreja do Java do sétimo dia útil e segue recomendando a organização que os primeiros homens utilizavam.
class Classe{
// primeiro declaramos as variáveis públicas, variáveis estáticas e variáveis constantes
final String variavelPublica;
static Int variavelEstatica = 66;
const bool variavelConstante = true;
//após, seguindo a mesma ordem, as variáveis privadas
final String _variavelPrivada = '';
static Int _variavelEstatica = -66;
const bool _variavelConstante = false;
//Em seguida o construtor da classe
Classe({required variavelPublica, super.variavelEstatica});
//Funções da classe, sendo pública primeiro em seguida das
//auxiliares privadas, caso exista
void funcaoPublica(){
_funcaoPrivada();
}
void _funcaoPrivada(){
//use a imaginação;
}
}
Lembrando daquela bobagem que foi citado no Capítulo 3, as funções dentro de uma classe devem seguir uma ordem de storytelling... falta task a este jovem senhor, relevem.
Seguindo a máxima de quanto mais simplificado, melhor (olha que ironia) o conselho primordial é que as classes devem ser pequenas. Diferente das funções, que contávamos quantas operações cada função faria, com classes, uma classe é considerada pequena se ela tiver poucas responsabilidades, ou melhor, uma única responsabilidade.
O conceito de single-responsibility ou responsabilidade única é o famoso cada macaco no seu galho. Você vai dar a sua classe apenas as atribuições que ela tem.
Vamos estudar o exemplo abaixo
abstract class SoundFileEdit{
Future<File> resizeSound(File file);
Future<File> treatSound(File file);
Future<File> cutSound(File file);
Future<File> removeNoise(File file);
Future<File> publishSound(File file);
}
Temos uma super classe para SoundFile, vamos simplificar em classes menores, vamos criar os famosos Usecases.
abstract class ResizeSound{
Future<File> call(File file);
}
abstract class TreatSound{
Future<File> call(File file);
}
abstract class CutSound{
Future<File> call(File file);
}
// usem a imaginação pra ver onde vai terminar ...
Tá, tenho classes minúsculas que fazem uma única coisa, mas e como faço pra essas coisas trabalharem juntas? Daí fazemos uma classe que faz apenas uma coisa também, uma classe controladora. A classe controladora tem que ter apenas uma responsabilidade, controlar. E esse tipo de controle tem que ser restrito ao seu tema. Vamos seguir o exemplo anterior e criar uma controladora.
class SoundFileController{
final ResizeSound _resizeSound = ResizeSound();
final TreatSound _treatSound = TreatSound();
final CutSound _cutSound = CutSound();
Future<file> resize(File file) async{
return await _resizeSound(file);
}
Future<file> treat(File file) async{
return await _treatSound(file);
}
Future<File> cutSound(File file) async{
return await _cutSound(file);
}
}
A controller não tem uma lógica no escopo, não tem uma operação, a função dela é apenas de união e controle das demais classes citadas.
Agora fazendo uma análise de todo esse passo a passo, parece verboso e muito cansativo tudo. Porém imagine o caso de compartilhamento de usecases. Se fosse possível utilizar o mesmo usecase de tratamento de som em uma classe para a adição de áudio em um filme, não teríamos que importar toda a classe de edição de audio, apenas um arquivo. E se precisar remover esse arquivo? além de um delete, 3 linhas de código somem na sua controladora. Isso nos diz que o nosso código está isolado, fácil de remover ou modificar, baixo-acoplado. E quanto mais fácil de alteração um sistema é, mais fácil é a sua manutenção.
Isto é um adeus?
Como expliquei no textinho de abertura do projeto o Clube do Livro Dev: Código Limpo, o livro Código Limpo possui níveis de entendimento. A partir do capítulo 11, a dificuldade do livro para mim cresce. Infelizmente ainda não cheguei ao nível de desenvolvimento e expertise para falar com propriedade e até com uma pitada de ironia, de certos temas como Sistemas, Concorrência e Emergências. Outros temas abordados no livro, como temas próprios para Java não me brilham os olhos. Então deixo aqui uma promessa de que um dia voltarei a visitar o livro do Tio Beto, como uma nova experiência, outros tantos comentários sarcásticos e sem noção para fazer a este senhor que tanto fez pela comunidade e hoje nos enche de um misto de sentimentos.
Nunca é um adeus
Top comments (2)
Joia, pura joia! Espero que se seniorize logo para terminar essa série 😁
Eu também conto com isso -