Técnicas de refatoração: Connascence

Adriano Croco
7 min readMar 8, 2023

--

Olá!

Hoje eu gostaria de comentar um pouco sobre uma técnica de refatoração que me pareceu muito interessante e que não vejo muitas pessoas falarem disso na comunidade de tecnologia que fala português.

Para que as coisas façam sentido, vamos começar pelo termo em si. A palavra é Connascence. Caso você pesquise pela tradução em português, não existe no dicionário. Eu perguntei para a ChatGPT o que significa o termo e ela me respondeu isso:

Resposta da ChatGPT

Dado que não tem muito como traduzir, vamos seguir com termo original mesmo.

O termo em si não é novo (é de um artigo de 1992) e já foi discutido de várias formas em algumas talks por aí. Confesso que já tinha visto o termo na literatura sobre arquitetura, porém, nunca tinha dado muita atenção. Foi após ver essa palestra aqui do Jim Weirich que resolvi me aprofundar.

O termo é uma métrica que mede o acoplamento entre dois elementos de software. Além disso, há uma classificação entre estática e dinâmica, gerando uma escala que vai do tipo mais simples (Name) até o mais complexo (Identity).

Isso pode ser representado na seguinte imagem:

Espectro que contempla todos os tipos de Connascence

Vou comentar sobre cada um deles separadamente. Começando pelos tipos estáticos.

1-) Connascence of Name (CoN): Múltiplos componentes devem concordar com o nome de uma entidade.

// nesse exemplo, ambas as funções devem concordar com o que 
// os termos length e width significam
// caso haja mudança nesses termos em uma das funções, podem gerar erros
// de entendimento e consequentemente, bugs e/ou code smell

function calculateAreaRectangle(length, width) {
return length * width;
}

function printRectangleInfo(l, w) {
wconsole.log("Length: ", l);
console.log("Width: ", w);
console.log("Area: ", calculateAreaRectangle(l, w));
}

2-) Connascence of Type (CoT): Múltiplos componentes devem concordar com o tipo de uma entidade. Aqui está um exemplo em Typescript, dado que estamos falando de tipos:

function addTwoNumbers(a: number, b: number): number {
return a + b;
}

function calculateAverage(numbers: number[]): number {
const total = numbers.reduce(addTwoNumbers);
return total / numbers.length;
}

O que aconteceria caso mudássemos os tipos de number para string na função addTwoNumbers?

Se você ainda não fixou bem o conceito de acoplamento, eu diria o seguinte para te ajudar: Se mexer em um lugar te obrigar a mexer em outro, é um sinal de acomplamento. Até agora, vimos tipos de Connascence que são relativamente inofensivos. Mas calma que piora.

3-) Connascence of Convention (CoC): Múltiplos componentes devem concordar com o significado de valores específicos.

O exemplo mais simples nesse caso é o formato de datas. Um componente usando o formato YYYY-MM-DD e outro usando DD-MM-YYYY gerará necessidade de adaptação. Caso ambos usam DD-MM-YYYY, problema resolvido.

4-) Connascence of Algorithm (CoA): Múltiplos componentes devem concordar com o algoritmo usado. Vamos a um exemplo de geração de arquivos:

// Data extractor, que depende do formato CSV para funcionar
function extractData() {
const data = [
{ name: "John", age: 25, email: "john@example.com" },
{ name: "Mary", age: 30, email: "mary@example.com" },
{ name: "Bob", age: 35, email: "bob@example.com" },
];
return data;
}

// CSV generator
function generateCsv(data) {
let csv = "Name,Age,Email\n"; // CSV header

data.forEach((item) => {
csv += `${item.name},${item.age},${item.email}\n`;
});

return csv;
}

function main()
{
const data = extractData();
const csv = generateCsv(data);
}

Caso a função generateCsv mude algo internamente e deixe de gerar os dados no formato separado por vírgula e troque para um outro separador qualquer (como um Pipe, por exemplo), a função extractData deixa de funcionar e precisará ser adaptada.

5-) Connascence of Position (CoP): Múltiplos componentes devem concordar com a posição de seus valores.

O exemplo mais simples que eu consigo pensar é simplesmente o contrato de uma API, como a seguinte API de pedidos:

POST /orders

Request body:
{
"user_id": 123,
"items": [
{
"item_id": 456,
"quantity": 2,
"unit_price": 9.99
},
{
"item_id": 789,
"quantity": 1,
"unit_price": 15.99
}
]
}

Nesse caso, todos os clientes dessa API precisam concordar em passar item_id, quantity, unit_price, nessa ordem, para preencher um objeto do array Items. Caso isso não seja feito, ocorrerá problemas de integridade nos dados.

Isso cobre os tipos estáticos. Vamos agora passar pelos tipos dinâmicos.

6-) Connascence of Execution Order (CoE): Quando a ordem de execuções dos componentes importa. Vamos a um exemplo:

function initialize() {
// initialize component A
}

function render() {
// render component B
}

function fetchData() {
// fetch data for component C
}

// ordem de execução é importante,
// initialize() precisa ser chamado anter de render()
// caso contrário, o componente dá erro
initialize();
render();

// fetchData() precisa rodar antes de render()
// caso o componente de render não tenha dados, dá erro
fetchData();
render();

Além desse caso, é possível encontrar exemplos similares em qualquer framework que usa uma determinada ordem de execução de funções para funcionar (ex: React). Pense em qualquer máquina de estado, geralmente usar esse modelo mental na hora de produzir código pode gerar acomplamento na ordem de execução.

7-) Connascence of Timing (CoTm): Quando o tempo de execução dos componentes importa.

Exemplo real que eu já presenciei: Um sistema que processa transações financeiras em Batch e leva 3 horas para rodar, iniciando a meia noite em ponto (componente A). Após esse componente terminar, outro processo trabalha com essas transações e faz alguma outra coisa com essa massa de dados (componente B). O arranjo que gera Connascence de Timing é programar a execução do componente B para começar as 3 da manhã ao invés de esperar algum sinal de conclusão do componente A. Afinal, 3 horas é uma estimativa e pode atrasar, por vários motivos. Caso o horário do componente A não seja cumprido, o componente B falhará em seu processamento, necessariamente.

Portanto, a minha experiência e esse tipo dizem o seguinte: Não amarrem sistemas por horário! O uso de técnicas como eventos para passagem de sinais entre sistemas podem funcionar melhor.

8-) Connascence of Value (CoV): Quando existem restrições nos valores possíveis de alguns elementos compartilhados. É geralmente relacionado com invariantes.

A definição usa o termo invariante e acho que vale explicar um pouco do que se trata. Imagine invariante como uma restrição para algum valor específico. No exemplo abaixo acredito que tudo vai fazer sentido:

// Connascence of Value example
function calculateDiscount(price, discountPercentage) {
if (price <= 0) {
throw new Error('Price must be a positive value.');
}

if (discountPercentage < 0 || discountPercentage > 100) {
throw new Error('Discount percentage must be between 0 and 100.');
}

const discount = price * (discountPercentage / 100);
return price - discount;
}

const originalPrice = 50;
const discountPercentage = 25;
const discountedPrice = calculateDiscount(originalPrice, discountPercentage);

console.log(discountedPrice); // Output: 37.5

Nesse exemplo, a função calculateDiscount valida se preço é positivo e se o percentual de desconto é de fato um percentual. Isso acontece porque é impossível calcular o desconto de um valor negativo ou com um percentual inválido.

O grande ponto do acomplamento nesse tipo é que muitas vezes outros componentes que usam essa função não sabem quais são as restrições internas da função e ocorrem erros devido a esse uso incorreto. Apesar de parecer um erro de entendimento de regras de negócio, na verdade é um problema de acomplamento.

A solução para isso é o de sempre: Funções menores com responsabilidade única (uma função que valide explicitamente os valores de preço e desconto antes de calcular o desconto, por exemplo).

9-) Connascence of Identity (CoI): Quando múltiplos componentes referenciam uma mesma entidade.

Nesse exemplo, é um sistema que possui as Entidades Order e Payment, acopladas entre si fortemente:

// User module
class User {
constructor(username, password) {
this.username = username;
this.password = password;
}

authenticate() {
// Implementation of user authentication logic
}
}

// Order module
class Order {
constructor(user, items) {
this.user = user;
this.items = items;
}

create() {
// Implementation of order creation logic
}
}

// Payment module
class Payment {
constructor(order) {
this.order = order;
}

process() {
// Implementation of payment processing logic
}
}

Caso você precise alterar alguma Order, obrigatoriamente vai precisar alterar a entidade de Payment.

A solução para isso é a de sempre: Camadas de indireção, adicionando uma camada de OrderService, que irá orquestrar o pagamento sem amarrar os objetos envolvidos entre si. Ficará algo parecido com isso:

// User module
class User {
constructor(username, password) {
this.username = username;
this.password = password;
}

authenticate() {
// Implementation of user authentication logic
}
}

// OrderService module
class OrderService {
constructor() {
this.orders = [];
}

createOrder(user, items) {
const order = new Order(user, items);
this.orders.push(order);
return order;
}

processPayment(order) {
const payment = new Payment(order);
payment.process();
}
}

// Order module
class Order {
constructor(user, items) {
this.user = user;
this.items = items;
}

create() {
// Implementation of order creation logic
}
}

// Payment module
class Payment {
constructor(order) {
this.order = order;
}

process() {
// Implementation of payment processing logic
}
}

O módulo OrderService é responsável por gerenciar pedidos e processar pagamentos. O módulo Order agora é responsável apenas pela criação dos pedidos e não precisa saber sobre o módulo Payments. Da mesma forma, o módulo Payment só precisa saber sobre o objeto Order que recebe do módulo OrderService, não o próprio módulo Order. Isso reduz o acoplamento entre os módulos e torna o código mais fácil de manter e atualizar.

Como os conceitos podem ser bem subjetivos, existem dois princípios gerais para guiar o pensamento ao analisar o código com essa lente:

Regra de Força: Converta formas fortes de Connascence em formas mais fracas. Foi basicamente o que ocorreu em alguns exemplos.

Regra da Localidade: Conforme a distância entre os elementos de software aumenta, use formas mais fracas de Connascence. Ou seja, de uma maneira geral, quanto mais distante os componentes, menos acoplados ele devem ser.

Com essas duas regras, acredito que seja o suficiente para analisar código e refatorar código usando Connascence.

Espero que esse texto seja útil para você de alguma forma.

Até!

Você gostou do conteúdo e gostaria de fazer mentoria comigo? Clique aqui e descubra como.

--

--