O que começou como um iconoclasmo alegre, cutucando o urso do SOLID, se transformou em algo mais concreto e tangível. Se eu não acho que os princípios SOLID são úteis hoje em dia, então com o que eu os substituiria? Qualquer conjunto de princípios pode valer para todos os softwares? O que queremos dizer com princípios?
Acredito que existem propriedades ou características do software que o tornam prazeroso de se trabalhar. Quanto mais seu código tiver essas qualidades, mais prazeroso será trabalhar com ele; mas tudo é uma troca, então você deve sempre considerar seu contexto.
Provavelmente há muitas dessas propriedades, sobrepostas e inter-relacionadas, e há muitas maneiras de descrevê-las. Escolhi cinco que sustentam muito do que me interessa no código. Há um retorno decrescente; cinco são suficientes para fazer um acrônimo útil e o suficiente para lembrar.
Vou expandir cada propriedade em artigos futuros para que este não fique mais longo, então, por favor, perdoe-me por não ser mais abrangente.
As cinco propriedades CUPID são:
- Componível: joga bem com os outros
- Filosofia Unix: faz uma coisa bem
- Previsível: faz o que você espera
- Idiomático: parece natural
- Baseado em domínio: o domínio da solução modela o domínio do problema em linguagem e estrutura.
Original
- Composable: plays well with others
- Unix philosophy: does one thing well
- Predictable: does what you expect
- Idiomatic: feels natural
- Domain-based: the solution domain models the problem domain in language and structure
Preâmbulo: há muito tempo… ¶
Você já abriu uma base de código desconhecida e apenas soube como se locomover? A estrutura, a nomeação, o fluxo são óbvios, familiares de alguma forma. Um sorriso aparece em seu rosto. "Eu sei isso!" você pensa.
Tive a sorte de experimentar isso um punhado de vezes ao longo de uma carreira de trinta anos, e cada vez isso me encheu de alegria. A primeira vez foi no início dos anos 1990 – lembro-me nitidamente – quando abri uma enorme base de código C que fazia manipulação complexa de imagens para impressão digital. Havia um bug no Someone Else's Code™, eu deveria rastreá-lo e corrigi-lo. Lembro-me da sensação, como programador novato: uma mistura de pavor e medo de me trair como amador que eu sabia que era.
Meu editor - vi com ctags - me permitiu navegar para as definições de função de sites de chamadas e, em poucos minutos, eu estava mergulhado em um ninho de chamadas, em uma base de código de muitas centenas de arquivos de origem e de cabeçalho, sentindo-me confiante de que sabia o que eu estava olhando. Rapidamente encontrei o culpado, que era um simples erro de lógica, fiz uma alteração, compilei o código e testei-o. Isso tudo sem testes automatizados, apenas usando Makefiles. TDD estava quase uma década no meu futuro e C não tinha esse tipo de ferramenta em nenhum caso.
Eu executei a transformação em várias imagens de amostra e elas saíram parecendo ok. Eu estava o mais confiante possível de que tinha a) encontrado e corrigido o bug, e
b) não introduzido nenhuma surpresa desagradável ao mesmo tempo.
Software alegre ¶
Alguns códigos são uma delícia de se trabalhar. Você sabe como encontrar o que precisa para trabalhar. Você sabe como fazer a mudança que precisa. O código é fácil de navegar, fácil de entender, fácil de raciocinar. Você se sente confiante de que sua mudança terá o efeito desejado, sem efeitos colaterais indevidos. O código te guia , te convida a olhar ao redor. Os programadores que vieram antes de você se preocuparam com a pessoa que viria mais tarde, talvez porque eles perceberam que o programador que veio depois pode ser eles!
Em seu livro “Refactoring”, Martin Fowler diz:
“Qualquer tolo pode escrever um código que um computador possa entender. Bons programadores escrevem código que os humanos podem entender.”
— Refatoração , Martin Fowler com Kent Beck, 1996
Eu li isso no início nos anos 2000 e suas palavras viraram meu mundo de programação de cabeça para baixo. E se uma boa programação for tornar o código compreensível para outros humanos? E se um desses humanos for o eu do futuro? Isso soou como algo para pensar.
Mas enquanto “compreensível” pode ser uma aspiração nobre, não é um nível tão alto! Na mesma época em que Martin estava escrevendo sobre refatoração, o pioneiro da computação
Richard P. Gabriel descreveu a
ideia de código ser habitável:
“Habitabilidade é a característica do código-fonte que permite que [as pessoas] compreendam sua construção e intenções e alterem-no com conforto e confiança.
“A habitabilidade torna um lugar habitável, como o lar.”
— Habitabilidade e Crescimento Parcial 1, adrões de Software P pp. 7-16, Richard P. Gabriel
Isso parece mais algo pelo qual lutar. Quão bom seria se sentir confortável e confiante mudando o código de outras pessoas? E se pudermos tornar o código habitável, que tal alegre? É possível que uma base de código o encha de alegria?
Se você passa seus dias de trabalho programando, navegar e manipular uma base de código define sua experiência de usuário. Você pode experimentar surpresa, frustração, pavor, antecipação, impotência, esperança, alegria, tudo por causa das escolhas que os programadores anteriores fizeram na base de código.
Se assumirmos que é possível que uma base de código seja alegre, cada base de código é seu próprio floco de neve especial, cujo impacto em sua psique é único? Ou podemos articular o que o torna alegre e oferecer um caminho para aumentar a alegria no código que tocamos?
Propriedades sobre princípios ¶
Quando comecei a formular uma resposta aos cinco princípios do SOLID, imaginei substituir cada um deles por algo que achasse mais útil ou relevante. Logo percebi que a própria ideia de princípios era problemática. Princípios são como regras: ou você está em conformidade ou não. Isso dá origem a “conjuntos limitados” de seguidores e aplicadores de regras, em vez de “conjuntos centrados” de pessoas com valores compartilhados.
Em vez disso, comecei a pensar em propriedades: qualidades ou características do código em vez de regras a serem seguidas. As propriedades definem um objetivo ou centro para o qual se mover. Seu código está apenas mais próximo ou mais distante do centro, e sempre há uma direção clara de deslocamento. Você pode usar propriedades como uma lente ou filtro para avaliar seu código e decidir quais abordar em seguida. Como as propriedades do CUPID estão todas inter-relacionadas, é provável que qualquer alteração feita para melhorar uma propriedade tenha um efeito positivo em algumas das outras.
Propriedades das propriedades ¶
Então, como escolhemos propriedades? O que torna uma propriedade mais ou menos útil? Eu decidi três “propriedades de propriedades” que eu quero que as propriedades CUPID tenham. Elas devem ser práticas , humanas e em camadas.
Para ser prático , as propriedades precisam ser:
- fácil de articular: assim você pode descrever cada uma delas em poucas frases e oferecer exemplos concretos e contra-exemplos.
- fácil de avaliar: para que você possa usá-las como uma lente para revisar e discutir o código e decidir facilmente o quanto o código exibe cada propriedade.
- fácil de adotar: para que você possa iniciar um código pequeno e evoluir incrementalmente em qualquer uma das dimensões do CUPID. Não há “all-in” e não há “fracasso”, assim como nunca há um “feito”. O código sempre pode melhorar.
Para o ser humano, as propriedades precisam ser lidas da perspectiva das pessoas, não do código. CUPID é sobre como é trabalhar com código, não uma descrição abstrata do código em si.
Por exemplo, enquanto a filosofia Unix de “faça uma coisa bem” pode soar como o Princípio da Responsabilidade Única, o primeiro é sobre como você usa o código, e o último é sobre as partes internas do próprio código.
Para serem em camadas, as propriedades devem oferecer orientação para iniciantes – o que é consequência de serem fáceis de articular – e nuances para pessoas mais experientes que desejam explorar a natureza do software mais profundamente. Cada uma das propriedades do CUPID é “óbvia” apenas o nome e uma breve descrição, mas cada uma incorpora muitas camadas, dimensões, abordagens. Podemos descrever o “centro” de cada propriedade, mas há muitos caminhos para chegar lá!
Combinável ¶
O software que é fácil de usar é usado, usado e usado novamente. Existem características que tornam o código mais ou menos componível, mas elas não são necessárias nem suficientes para dar garantias. Em cada caso, podemos encontrar contra-exemplos em ambos os lados, então você deve pensar nisso como heurísticas úteis. Mais não é necessariamente melhor; é tudo trocas.
Pequena área de superfície ¶
O código com uma API estreita e opinativa tem menos para você aprender, menos para dar errado e menos chance de conflito ou inconsistência com outro código que você está usando. Isso tem um retorno decrescente; se suas APIs forem muito restritas, você se verá usando grupos delas juntas e saber “a combinação certa” para casos de uso comuns se torna um conhecimento tácito que pode ser uma barreira à entrada. Acertar a granularidade de uma API é mais difícil do que parece. Há um ponto ideal de coesão “na medida certa” entre fragmentado e inchado.
Revelação de intenção ¶
Código revelador de intenção é fácil de descobrir e fácil de avaliar. Posso encontrar facilmente seu componente e decidir rapidamente se é o que preciso ou não. Um modelo que eu gosto - de projetos de código aberto como o venerável XStream - é ter um tutorial de 2 minutos, um tutorial de 10 minutos e um mergulho profundo. Isso me permite investir de forma incremental e mudar assim que descobrir que isso não é para mim.
Mais de uma vez comecei a escrever uma classe dando a ela um nome que revelasse a intenção, apenas para o IDE exibir uma importação sugerida com o mesmo nome. Geralmente acontecia que outra pessoa tinha a mesma ideia, e por acaso encontrei o código deles porque escolhemos nomes semelhantes. Isso não foi apenas coincidência; éramos fluentes no mesmo domínio, o que tornava mais provável que escolhêssemos nomes semelhantes. Isso é mais provável quando você tem código baseado em domínio.
Dependências mínimas ¶
O código com dependências mínimas oferece menos preocupações e reduz a probabilidade de incompatibilidades de versão ou biblioteca. Escrevi meu primeiro projeto de código aberto, XJB, em Java e usei log4j como estrutura de log, log4j quase onipresente. Um colega apontou que isso criava uma dependência, não apenas de log4j como biblioteca, mas em uma versão específica. Nem me ocorreu; por que alguém deveria se preocupar com algo tão inócuo quanto uma biblioteca de log? Então, removemos as dependências e até extraímos um outro projeto que fazia coisas divertidas com proxies dinâmicos Java, que por si só tinham dependências mínimas.
Filosofia Unix ¶
Unix e eu temos aproximadamente a mesma idade; nós dois começamos em 1969, e o Unix se tornou o sistema operacional mais prevalente no planeta. Durante a década de 1990, todo fabricante sério de hardware de computador tinha seu próprio Unix, até que as principais variantes de código aberto, Linux e FreeBSD, se tornaram onipresentes. Atualmente, ele executa quase todos os servidores de negócios, tanto na nuvem quanto em local, na forma de Linux; é executado em sistemas embarcados e dispositivos de rede; ele sustenta os sistemas operacionais macOS(hoje baseado em BSD) e Android; ele ainda vem como um subsistema opcional com o Microsoft Windows!
Um modelo simples e consistente ¶
Então, como um sistema operacional de nicho, que começou em um laboratório de pesquisa de telecomunicações, é copiado como um projeto de hobby por um estudante universitário e termina como o maior sistema operacional do mundo? Há, sem dúvida, razões comerciais e legais para seu sucesso em uma época em que os fornecedores de sistemas operacionais eram tão famosos por suas ações judiciais uns contra os outros quanto por sua tecnologia, mas seu apelo técnico duradouro está em sua filosofia de design simples e consistente .
A filosofia Unix diz para escrever [componentes] que funcionem bem juntos, descritos na propriedade Composability acima, e que façam uma coisa e façam bem. Por exemplo, o comando ls lista detalhes sobre arquivos e diretórios, mas não sabe nada sobre arquivos ou diretórios! Existe um comando de sistema chamado stat que fornece as informações; ls é apenas uma ferramenta para apresentar essas informações como texto.
Da mesma forma, comando cat imprime (concatena) o conteúdo de um ou mais arquivos, grep seleciona o texto que corresponde a um determinado padrão, sed substitui os padrões de texto e assim por diante. A linha de comando do Unix tem o poderoso conceito de “pipes” que anexam a saída de um comando como entrada para o próximo, criando um pipeline de seleção, transformação, filtragem, classificação e assim por diante. Você pode escrever programas sofisticados de processamento de texto e dados com base na composição de um punhado de comandos bem projetados, cada um fazendo uma coisa e bem.
Propósito único vs. responsabilidade única ¶
À primeira vista, isso parece o Princípio da Responsabilidade Única (SRP), e para certas interpretações do SRP há alguma sobreposição. Mas “fazer uma coisa bem” é uma perspectiva de fora para dentro; é a propriedade de ter um propósito específico, bem definido e abrangente. O SRP é uma perspectiva de dentro para fora: trata-se da organização do código.
O SRP, nas palavras de Robert C. Martin, que cunhou o termo, é que [o código]“ deve ter um, e apenas um, motivo para mudar”. O exemplo no artigo da Wikipedia é um módulo que produz um relatório, no qual você deve considerar o conteúdo e o formato do relatório como preocupações separadas que devem viver em classes separadas, ou mesmo módulos separados. Como eu disse em outro lugar, na minha experiência, isso cria costuras artificiais, e o caso mais comum é quando o conteúdo e o formato dos dados mudam juntos; um novo campo, por exemplo, ou uma alteração na origem de alguns dados que afete tanto seu conteúdo quanto a maneira como você deseja exibi-lo.
Outro cenário comum é um “componente de interface do usuário” em que o SRP exige que você separe a renderização e a lógica de negócios do componente. Como desenvolvedor, tê-los morando em lugares diferentes leva a uma tarefa administrativa de encadear campos idênticos. O maior risco é que isso possa ser uma otimização prematura que impeça uma separação mais natural de preocupações surgindo à medida que a base de código cresce e à medida que surgem componentes que “fazem uma coisa bem” e que são mais adequados ao modelo de domínio do espaço do problema. À medida que qualquer base de código cresce, chegará a hora de separá-la em subcomponentes sensíveis, mas as propriedades de Composability e estrutura baseada em domínio serão um indicador melhor de quando e como fazer essas alterações estruturais.
Previsível ¶
O código deve fazer o que parece, de forma consistente e confiável, sem surpresas desagradáveis. Deve ser não apenas possível, mas fácil de confirmar isso. Nesse sentido, a previsibilidade é uma generalização da testabilidade.
O código previsível deve se comportar como esperado e deve ser determinístico e observável.
Comporta-se como esperado ¶
A primeira das quatro regras de design simples de Kent Beck é que o código “passa em todos os testes”. Isso deve ser verdade mesmo quando não há testes! O comportamento pretendido do código previsível deve ser óbvio a partir de sua estrutura e nomeação. Se não houver testes automatizados para fazer isso, deve ser fácil escrever alguns. Michael Feathers chama esses testes de caracterização. Em suas palavras:
“Quando um sistema entra em produção, de certa forma, ele se torna sua própria especificação.” — Michael Feathers
Isso não é necessário, e acho que algumas pessoas pensam no desenvolvimento orientado a testes como uma religião e não como uma ferramenta. Certa vez, trabalhei em um aplicativo de negociação com algorítmico complexo que tinha cerca de 7% de “cobertura de teste”. Esses testes não foram distribuídos uniformemente! Grande parte do código não tinha testes automatizados, e alguns tinham quantidades loucas de testes sofisticados, verificando bugs sutis e casos extremos. Eu estava confiante em fazer alterações na maior parte da base de código, porque cada um dos componentes fazia uma coisa e seu comportamento era direto e previsível, então a mudança geralmente era óbvia.
Determinista ¶
O software deve fazer a mesma coisa todas as vezes. Mesmo o código projetado para ser não determinístico – digamos, um gerador de números aleatórios ou um cálculo dinâmico – terá limites operacionais ou funcionais que você pode definir. Você deve ser capaz de prever limites de memória, rede, armazenamento ou processamento, limites de tempo e expectativas de outras dependências.
O determinismo é um tema amplo. Para fins de previsibilidade, o código determinístico deve ser robusto, confiável e resiliente.
- Robustez é a amplitude ou completude das situações que cobrimos. Limitações e casos extremos devem ser óbvios.
- Confiabilidade é agir como esperado nas situações que cobrimos. Devemos obter sempre os mesmos resultados.
- Resiliência é o quão bem lidamos com situações que não cobrimos; perturbações inesperadas nas entradas ou no ambiente operacional.
Observável ¶
O código deve ser observável no sentido da teoria de controle: podemos inferir seu estado interno de suas saídas. Isso só é possível quando o projetamos. Assim que vários componentes estiverem interagindo, especialmente de forma assíncrona, haverá um comportamento emergente e consequências não lineares.
Instrumentar o código desde o início significa que podemos obter dados valiosos para entender suas características de tempo de execução. Eu descrevo um modelo de quatro estágios – com dois estágios de bônus! – assim:
1.Instrumentação é o seu software dizendo o que está fazendo.
2.A telemetria está disponibilizando essas informações, seja por pull – algo pedindo – ou push – enviando mensagens; “medição à distância”.
3.Monitorar é receber instrumentação e torná-la visível.
4.Alertar é reagir aos dados monitorados ou padrões nos dados.
Bônus:
6.Prever é usar esses dados para antecipar eventos antes que eles aconteçam.
7.Adaptar é mudar o sistema dinamicamente, seja para antecipar ou se recuperar de uma perturbação prevista.
A maioria dos softwares nem passa da etapa 1. Existem ferramentas que interceptam ou modificam sistemas em execução para adicionar um nível de percepção, mas elas nunca são tão boas quanto a instrumentação deliberada projetada em um aplicativo.
Idiomático ¶
Todo mundo tem seu próprio estilo de codificação. Seja espaços versus tabulações, tamanho de recuo, convenções de nomenclatura de variáveis, colocação de chaves ou parênteses, layout de código em um arquivo de origem ou inúmeras outras possibilidades. Sobre isso, podemos colocar as opções de bibliotecas, cadeia de ferramentas, caminho para o live, até mesmo estilo de comentário de controle de versão ou granularidade de commit. (Você usa controle de versão, não é?)
Isso pode adicionar uma carga cognitiva estranha significativa ao trabalho com código desconhecido. Além de entender o domínio do problema e o espaço da solução, você precisa interpretar o que outra pessoa quis dizer e se suas decisões foram deliberadas e contextuais ou arbitrárias e habituais.
O maior traço de programação é a empatia; empatia pelos seus usuários; empatia pelo pessoal de apoio; empatia pelos futuros desenvolvedores; qualquer um deles pode ser você no futuro. Escrever “código que humanos possam entender” significa escrever código para outra pessoa. Isso é o que significa código idiomático.
Nesse contexto, seu público-alvo é:
- familiar com a linguagem, suas bibliotecas, sua cadeia de ferramentas e seu ecossistema
- um programador experiente que entende de desenvolvimento de software
- alguém tentando fazer o trabalho!
Expressões idiomáticas ¶
O código deve estar de acordo com as expressões idiomáticas do idioma. Algumas linguagens têm opiniões fortes sobre como o código deve ser, o que facilita a avaliação de quão idiomático é seu código. Outros são menos opinativos, o que coloca o ônus de você “escolher um estilo” e depois cumpri-lo. Go e Python são dois exemplos de uma linguagem opinativa.
Os programadores Python usam o termo “pythonic” para descrever o código idiomático. Há um [ovo de Páscoa](https://en.wikipedia.org/wiki/Easter_egg_(media) maravilhoso que aparece se você for import this do Python REPL ou executar python -m this a partir de um shell. Ele imprime uma lista de aforismos de programação chamada “The Zen of Python”, que inclui esta linha, capturando o espírito do código idiomático: “Deveria haver uma – e de preferência apenas uma – maneira óbvia de fazer isso”.
A linguagem Go vem com um formatador de código chamado gofmt que faz com que todo o código-fonte pareça o mesmo. Isso elimina de uma só vez quaisquer divergências sobre recuo, colocação de chaves ou outras peculiaridades sintáticas. Isso significa que todos os exemplos de código que você vê nos documentos ou tutoriais da biblioteca parecem consistentes. Eles até têm um documento chamado Effective Go que mostra o Go idiomático, além da definição do idioma.
No outro extremo do espectro estão linguagens como Scala, Ruby 5, JavaScript e o venerável Perl. Essas linguagens são deliberadamente multiparadigmáticas; Perl cunhou a sigla TIMTOWTDI – “há mais de uma maneira de fazer isso” – pronunciada “Tim Toady”. Você pode escrever código funcional, procedural ou orientado a objetos na maioria deles, o que cria uma curva de aprendizado superficial de qualquer linguagem que você conheça.
Para algo tão simples quanto processar uma sequência de valores, a maioria dessas linguagens permite:
- usar um iterador
- usar um loop for indexado
- usar um loop condicional while
- usar um pipeline de função com um coletor (“map-reduce”)
- escrever uma função recursiva
Isso significa que em qualquer tamanho de código não trivial, você provavelmente encontrará exemplos de cada um deles, geralmente combinados entre si. Novamente, tudo isso adiciona carga cognitiva, impactando sua capacidade de pensar sobre o problema em questão, aumentando a incerteza e reduzindo a alegria.
Idiomas de código ocorrem em todos os níveis de granularidade: nomeação de funções, tipos, parâmetros, módulos; layout de código; estrutura de módulos; escolha de ferramentas; escolha de dependências; como você gerencia dependências; e assim por diante.
Onde quer que sua pilha de tecnologia esteja no espectro da opinião, o código que você escrever será mais empático e alegre se você dedicar um tempo para aprender as expressões idiomáticas da linguagem, seu ecossistema, sua comunidade e seu estilo preferido.
Sua curva de aprendizado para uma tecnologia provavelmente será mais curta do que qualquer código que você escrever nela, por isso é importante resistir ao desejo de escrever um código que esteja bem para você agora, porque essa pessoa não ficará por muito tempo! A única maneira de ter certeza de que você está escrevendo código idiomático é dedicar um tempo para aprender os idiomas.
Expressões idiomáticas locais ¶
Quando uma linguagem não tem consenso em torno do estilo idiomático ou de várias alternativas, cabe a você e sua equipe decidir o que é “bom” e introduzir restrições e diretrizes para incentivar a consistência. Essas restrições podem ser tão simples quanto regras de formatação de código compartilhado em seu IDE, ferramentas de “build cop” (linst) que deslindam e criticam o código e um acordo sobre uma cadeia de configurações padrão.
Os Registros de Decisão de Arquitetura, ou ADRs, são uma ótima maneira de documentar suas escolhas sobre estilo e idiomas. Estas não são menos “decisões técnicas significativas” do que qualquer outra discussão arquitetônica.
Baseado em domínio ¶
Nós escrevemos software para atender a uma necessidade. Isso pode ser específico e situacional, ou genérico e de longo alcance. Qualquer que seja seu propósito, o código deve transmitir o que está fazendo na linguagem do domínio do problema, para minimizar a distância cognitiva entre o que você escreve e o que ele faz. Isso é mais do que “usar as palavras certas”.
Linguagem baseada em domínio ¶
As linguagens de programação e suas bibliotecas estão cheias de construções da ciência da computação, como mapas de hash, listas vinculadas, conjuntos de árvores, conexões de banco de dados e assim por diante. Eles têm tipos básicos que incluem inteiros, caracteres, valores booleanos. Você pode declarar o sobrenome de alguém como um string[30], que pode ser como ele é armazenado, mas definir um tipo
Surnameserá mais revelador da intenção. Pode até ter operações, propriedades ou restrições relacionadas ao sobrenome. Muitos bugs sutis no software bancário são devidos à representação de quantias em dinheiro como valores de ponto flutuante; programadores de software financeiro experientes definirão um tipo Money com a Currencye an Amount, que por si só é um tipo composto.
Nomear bem os tipos e as operações não se trata apenas de capturar ou prevenir bugs, mas de facilitar a articulação e a navegação no espaço da solução no código. Fiz desta minha contribuição para “97 Coisas que Todo Programador Deve Saber”, como “Código na Linguagem do Domínio”.
Um critério para o sucesso com código orientado a domínio é que um observador casual não pode dizer se as pessoas estão discutindo o código ou o domínio. Eu experimentei isso uma vez em um sistema de negociação eletrônico, onde um analista financeiro estava discutindo uma lógica complexa de preços comerciais com dois programadores. Eu pensei que eles estavam discutindo as regras de precificação, mas eles estavam apontando para uma tela cheia de código e o analista estava falando com os programadores através do algoritmo de precificação, que era linha por linha como o código era lido! A única distância cognitiva entre o domínio do problema e o código da solução era alguma pontuação de sintaxe!
Estrutura baseada em domínio ¶
O uso de linguagem baseada em domínio é importante, mas a forma como você estrutura seu código pode ser igualmente significativa. Muitos frameworks oferecem um “projeto esqueleto” com um layout de diretório e arquivos stub projetados para você começar rapidamente. Isso impõe uma estrutura a priori em seu código que não tem nada a ver com o problema que você está resolvendo.
Em vez disso, o layout do código - os nomes dos diretórios, os relacionamentos das pastas filhas e irmãs, o agrupamento e a nomenclatura de arquivos relacionados - devem espelhar o domínio do problema o mais próximo possível.
O framework de aplicativos Ruby on Rails popularizou essa abordagem no início dos anos 2000, incorporando-a em suas ferramentas, e a ampla adoção do Rails fez com que muitos frameworks posteriores copiassem a ideia.
CUPID é agnóstico para linguagens e frameworks, mas Rails é um estudo de caso útil para entender a diferença entre estrutura baseada em domínio e estrutura baseada em framework.
Abaixo está parte do layout do diretório de um aplicativo Rails esqueleto gerado, focando no diretório (app) onde um desenvolvedor passará a maior parte de seu tempo. O esqueleto completo é executado em cerca de 50 diretórios contendo 60 arquivos, 7 no momento da escrita.
app
├── assets
│ ├── config
│ ├── images
│ └── stylesheets
├── channels
│ └── application_cable
├── controllers
│ └── concerns
├── helpers
├── javascript
│ └── controllers
├── jobs
├── mailers
├── models
│ └── concerns
└── views
└── layouts
Imagine que este será um aplicativo de gestão hospitalar, com uma seção para prontuários de pacientes. Este layout sugere que precisaremos de pelo menos:
- um model, que mapeia para um banco de dados em algum lugar
- uma view, que renderiza o registro do paciente em uma tela
- um controller, que faz a mediação entre visualizações e modelos
Depois, há espaço para helpers, assets e vários outros conceitos de estrutura, como preocupações de model ou preocupações de controller , mailers , jobs , channels e talvez um controller JavaScript para acompanhar seu controller Ruby. Cada um desses artefatos vive em um diretório separado, embora sejam semanticamente integrados.
A probabilidade é que qualquer mudança não trivial no gerenciamento de registros de pacientes envolva código espalhado por toda a base de código. O princípio SOLID de Single Responsibility diz que o código de visualização deve ser separado do código do controlador, e frameworks como Rails interpretam isso como significando tê-los em lugares completamente diferentes. Isso aumenta a carga cognitiva, reduz a coesão e aumenta o esforço de fazer mudanças no produto. Como discuti anteriormente, essa restrição ideológica pode tornar o trabalho mais difícil e a base de código menos prazerosa.
Ainda precisamos de artefatos como models, views e controllers, independentemente da forma como disponhamos o código, mas agrupá-los por tipo não deve formar a estrutura primária. Em vez disso, o nível superior da base de código deve mostrar os principais casos de uso do gerenciamento hospitalar; talvez patient_history, appointments, staffinge compliance.
Adotar uma abordagem baseada em domínio para a estrutura do código torna fácil entender para que serve o código e fácil de navegar para onde quer que você precise estar para algo mais complicado do que “tornar esse botão azul claro”.
Limites baseados em domínio ¶
Quando estruturamos o código da maneira que queremos e o nomeamos da maneira que queremos, os limites do módulo se tornam limites de domínio e a implantação se torna direta. Tudo o que precisamos para implantar um componente como um único artefato está junto, para que possamos alinhar os limites do domínio com os limites da implantação e implantar componentes e serviços de negócios coesos. Independentemente de você empacotar seus produtos ou serviços como um único monolito, muitos microsserviços pequenos ou em qualquer lugar entre eles, esse alinhamento reduz a complexidade do seu caminho para viver e torna menos provável que você esqueça algo ou inclua artefatos de um ambiente diferente ou um subsistema diferente.
Isso não nos limita a um nível único, plano e superior de estrutura de código. Os domínios podem conter subdomínios; componentes podem conter subcomponentes; as implantações podem ocorrer em qualquer nível de granularidade que faça sentido para sua mudança e perfil de risco. Alinhar os limites do código com os limites do domínio torna todas essas opções mais fáceis de raciocinar e gerenciar.
Considerações finais ¶
Acredito que o código que possui mais dessas propriedades — de composição, filosofia Unix, previsibilidade ou ser idiomático ou baseado em domínio — é mais agradável de se trabalhar do que o código que não possui. Embora eu valorize cada característica independentemente, acho que elas se reforçam mutuamente.
Código que pode ser composto e abrangente – fazendo uma coisa bem – é como um amigo confiável. O código idiomático parece familiar, mesmo que você nunca o tenha visto antes. O código previsível oferece ciclos sobressalentes para você se concentrar em surpresas em outros lugares. O código baseado em domínio minimiza a distância cognitiva da necessidade à solução. Mover o código para o “centro” de qualquer uma dessas propriedades o deixa melhor do que você o encontrou.
Como CUPID é um backronym, eu tinha vários candidatos para cada letra. Escolhi esses cinco porque eles parecem “fundamentais” de alguma forma; podemos derivar todas as outras propriedades candidatas a partir delas. Artigos futuros explorarão algumas das propriedades da lista restrita que não foram selecionadas e observarão como elas são consequências naturais da criação de software CUPID.
Estou ansioso para ouvir as aventuras das pessoas com o CUPID. Já estou ouvindo sobre equipes que usam essas propriedades para avaliar seu código e desenvolver estratégias para limpar bases de código legadas, e mal posso esperar para ouvir relatos de experiência e estudos de caso. Enquanto isso, quero me aprofundar no CUPID, explorando cada uma das propriedades, para ver o que mais está escondido à vista de todos.
Original > https://dannorth.net/2022/02/10/cupid-for-joyful-coding/
Top comments (0)