Día 14: Cambiaste una clase y rompiste 10 más. Efecto dominó.
Alejandro La
·
2026-04-24
·
via DEV Community
<p>Cambiaste un campo en un DTO. Un rename inocente. Y de repente tienes 10 clases con errores de compilación, 3 tests rotos y un compañero preguntándote "¿qué tocaste?".</p> <p>No tocaste nada raro. Tocaste <strong>una clase</strong>. El problema es que esa clase está conectada a todo como si fuera el centro del universo. Y en tu codebase, probablemente lo es.</p> <h2> Esto no es solo de Java </h2> <p>El anti-pattern es el mismo en cualquier stack. Solo cambia la sintaxis:</p> <ul> <li> <strong>Java / Spring Boot</strong>: un <code>UserDTO</code> compartido entre controller, service y repository</li> <li> <strong>TypeScript / NestJS</strong>: un <code>user.dto.ts</code> que se usa en controllers, guards, pipes y servicios</li> <li> <strong>Python / Django</strong>: un serializer gigante que sirve para crear, leer, actualizar y exportar</li> <li> <strong>C# / .NET</strong>: un <code>UserModel</code> compartido entre API, business layer y data access</li> <li> <strong>Ruby on Rails</strong>: un modelo <code>User</code> con <code>as_json</code> sobreescrito 4 veces para diferentes contextos</li> <li> <strong>PHP / Laravel</strong>: un <code>UserResource</code> que hace transform distinto según quién lo llame</li> </ul> <p>El patrón es siempre el mismo: un objeto compartido entre capas con necesidades distintas. Cuando lo tocas, se rompe todo.</p> <h2> Alto acoplamiento, baja cohesión </h2> <p>Dos conceptos que suenan a examen de facultad pero que definen si tu código es mantenible o una bomba de tiempo:</p> <p><strong>Acoplamiento</strong> = cuánto depende una clase de otra. Si cambiar A rompe B, C y D, están acopladas.</p> <p><strong>Cohesión</strong> = cuánto se relacionan las cosas dentro de una clase. Si una clase hace cosas que no tienen nada que ver entre sí, tiene baja cohesión.</p> <p>Lo ideal: bajo acoplamiento, alta cohesión. La realidad de la mayoría de los proyectos: exactamente lo opuesto.</p> <h2> El ejemplo: un DTO que gobierna todo </h2> <p>Un solo <code>UserDTO</code> compartido entre controller, service y hasta la capa de persistencia:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight java"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">UserDTO</span> <span class="o">{</span> <span class="kd">private</span> <span class="nc">Long</span> <span class="n">id</span><span class="o">;</span> <span class="kd">private</span> <span class="nc">String</span> <span class="n">name</span><span class="o">;</span> <span class="kd">private</span> <span class="nc">String</span> <span class="n">email</span><span class="o">;</span> <span class="kd">private</span> <span class="nc">String</span> <span class="n">password</span><span class="o">;</span> <span class="kd">private</span> <span class="nc">String</span> <span class="n">role</span><span class="o">;</span> <span class="kd">private</span> <span class="nc">LocalDateTime</span> <span class="n">createdAt</span><span class="o">;</span> <span class="kd">private</span> <span class="nc">List</span><span class="o"><</span><span class="nc">OrderDTO</span><span class="o">></span> <span class="n">orders</span><span class="o">;</span> <span class="kd">private</span> <span class="nc">AddressDTO</span> <span class="n">address</span><span class="o">;</span> <span class="kd">private</span> <span class="nc">String</span> <span class="n">creditCardLast4</span><span class="o">;</span> <span class="o">}</span> </code></pre> </div> <p>Y se usa en todos lados:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight java"><code><span class="nd">@RestController</span> <span class="kd">public</span> <span class="kd">class</span> <span class="nc">UserController</span> <span class="o">{</span> <span class="nd">@PostMapping</span><span class="o">(</span><span class="s">"/users"</span><span class="o">)</span> <span class="kd">public</span> <span class="nc">UserDTO</span> <span class="nf">createUser</span><span class="o">(</span><span class="nd">@RequestBody</span> <span class="nc">UserDTO</span> <span class="n">dto</span><span class="o">)</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span> <span class="nd">@GetMapping</span><span class="o">(</span><span class="s">"/users/{id}"</span><span class="o">)</span> <span class="kd">public</span> <span class="nc">UserDTO</span> <span class="nf">getUser</span><span class="o">(</span><span class="nd">@PathVariable</span> <span class="nc">Long</span> <span class="n">id</span><span class="o">)</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span> <span class="o">}</span> <span class="nd">@Service</span> <span class="kd">public</span> <span class="kd">class</span> <span class="nc">UserService</span> <span class="o">{</span> <span class="kd">public</span> <span class="nc">UserDTO</span> <span class="nf">createUser</span><span class="o">(</span><span class="nc">UserDTO</span> <span class="n">dto</span><span class="o">)</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span> <span class="kd">public</span> <span class="nc">UserDTO</span> <span class="nf">findById</span><span class="o">(</span><span class="nc">Long</span> <span class="n">id</span><span class="o">)</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span> <span class="o">}</span> <span class="nd">@Service</span> <span class="kd">public</span> <span class="kd">class</span> <span class="nc">OrderService</span> <span class="o">{</span> <span class="kd">public</span> <span class="nc">List</span><span class="o"><</span><span class="nc">OrderDTO</span><span class="o">></span> <span class="nf">getOrders</span><span class="o">(</span><span class="nc">UserDTO</span> <span class="n">user</span><span class="o">)</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span> <span class="o">}</span> <span class="nd">@Service</span> <span class="kd">public</span> <span class="kd">class</span> <span class="nc">NotificationService</span> <span class="o">{</span> <span class="kd">public</span> <span class="kt">void</span> <span class="nf">sendWelcome</span><span class="o">(</span><span class="nc">UserDTO</span> <span class="n">user</span><span class="o">)</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span> <span class="o">}</span> <span class="nd">@Service</span> <span class="kd">public</span> <span class="kd">class</span> <span class="nc">ReportService</span> <span class="o">{</span> <span class="kd">public</span> <span class="nc">Report</span> <span class="nf">generateUserReport</span><span class="o">(</span><span class="nc">UserDTO</span> <span class="n">user</span><span class="o">)</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span> <span class="o">}</span> </code></pre> </div> <p>Ahora renombras <code>name</code> a <code>fullName</code>. O eliminas <code>creditCardLast4</code> porque ya no lo necesitas en la respuesta. O agregas un campo.</p> <p>Todos esos servicios se rompen. El controller se rompe. Los tests se rompen. El frontend se rompe. Todo porque compartes <strong>un objeto entre 5 capas con necesidades distintas</strong>.</p> <p>El <code>OrderService</code> no necesita el password. El <code>NotificationService</code> no necesita las órdenes. El <code>ReportService</code> no necesita la tarjeta de crédito. Pero todos dependen del mismo DTO gigante.</p> <p>En TypeScript sería el mismo problema con una interfaz <code>User</code> compartida entre routes, middleware y services. En Python, un serializer con 15 campos que se usa para crear, listar y exportar. La tecnología cambia, el error no.</p> <h2> La solución: cada capa recibe lo que necesita </h2> <p>import InArticleCTA from '../../components/InArticleCTA.astro';</p> <div class="highlight js-code-highlight"> <pre class="highlight java"><code><span class="c1">// Lo que el frontend envía para crear un usuario</span> <span class="kd">public</span> <span class="n">record</span> <span class="nf">CreateUserRequest</span><span class="o">(</span> <span class="nc">String</span> <span class="n">name</span><span class="o">,</span> <span class="nc">String</span> <span class="n">email</span><span class="o">,</span> <span class="nc">String</span> <span class="n">password</span> <span class="o">)</span> <span class="o">{}</span> <span class="c1">// Lo que el endpoint devuelve</span> <span class="kd">public</span> <span class="n">record</span> <span class="nf">UserResponse</span><span class="o">(</span> <span class="nc">Long</span> <span class="n">id</span><span class="o">,</span> <span class="nc">String</span> <span class="n">name</span><span class="o">,</span> <span class="nc">String</span> <span class="n">email</span><span class="o">,</span> <span class="nc">String</span> <span class="n">role</span><span class="o">,</span> <span class="nc">LocalDateTime</span> <span class="n">createdAt</span> <span class="o">)</span> <span class="o">{}</span> <span class="c1">// Lo que el servicio de notificaciones necesita</span> <span class="kd">public</span> <span class="n">record</span> <span class="nf">UserNotificationInfo</span><span class="o">(</span> <span class="nc">String</span> <span class="n">name</span><span class="o">,</span> <span class="nc">String</span> <span class="n">email</span> <span class="o">)</span> <span class="o">{}</span> <span class="c1">// Lo que el servicio de reportes necesita</span> <span class="kd">public</span> <span class="n">record</span> <span class="nf">UserReportData</span><span class="o">(</span> <span class="nc">Long</span> <span class="n">id</span><span class="o">,</span> <span class="nc">String</span> <span class="n">name</span><span class="o">,</span> <span class="nc">String</span> <span class="n">role</span><span class="o">,</span> <span class="nc">LocalDateTime</span> <span class="n">createdAt</span><span class="o">,</span> <span class="kt">int</span> <span class="n">totalOrders</span> <span class="o">)</span> <span class="o">{}</span> </code></pre> </div> <p>Cada clase recibe exactamente lo que necesita. Ni más, ni menos.<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight java"><code><span class="nd">@RestController</span> <span class="kd">public</span> <span class="kd">class</span> <span class="nc">UserController</span> <span class="o">{</span> <span class="nd">@PostMapping</span><span class="o">(</span><span class="s">"/users"</span><span class="o">)</span> <span class="kd">public</span> <span class="nc">UserResponse</span> <span class="nf">createUser</span><span class="o">(</span><span class="nd">@RequestBody</span> <span class="nc">CreateUserRequest</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span> <span class="nd">@GetMapping</span><span class="o">(</span><span class="s">"/users/{id}"</span><span class="o">)</span> <span class="kd">public</span> <span class="nc">UserResponse</span> <span class="nf">getUser</span><span class="o">(</span><span class="nd">@PathVariable</span> <span class="nc">Long</span> <span class="n">id</span><span class="o">)</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span> <span class="o">}</span> <span class="nd">@Service</span> <span class="kd">public</span> <span class="kd">class</span> <span class="nc">NotificationService</span> <span class="o">{</span> <span class="kd">public</span> <span class="kt">void</span> <span class="nf">sendWelcome</span><span class="o">(</span><span class="nc">UserNotificationInfo</span> <span class="n">user</span><span class="o">)</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span> <span class="o">}</span> </code></pre> </div> <p>Ahora renombras un campo en <code>CreateUserRequest</code>. ¿Qué se rompe? Solo el controller y su test. Nada más. El servicio de notificaciones ni se entera. El de reportes sigue funcionando. Cero efecto dominó.</p> <h2> Los números </h2> <div class="table-wrapper-paragraph"><table> <thead> <tr> <th></th> <th>Antes</th> <th>Después</th> </tr> </thead> <tbody> <tr> <td>Clases afectadas por un cambio de DTO</td> <td>10+</td> <td>1-2</td> </tr> <tr> <td>Campos innecesarios viajando por la red</td> <td>5-6</td> <td>0</td> </tr> <tr> <td>Riesgo de exponer password en respuesta</td> <td>Alto</td> <td>Nulo</td> </tr> </tbody> </table></div> <h2> La otra cara: cohesión </h2> <p>Acoplamiento bajo sin cohesión alta no sirve de mucho. Mira esta clase:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight java"><code><span class="nd">@Service</span> <span class="kd">public</span> <span class="kd">class</span> <span class="nc">UserService</span> <span class="o">{</span> <span class="kd">public</span> <span class="nc">User</span> <span class="nf">createUser</span><span class="o">(</span><span class="nc">CreateUserRequest</span> <span class="n">req</span><span class="o">)</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span> <span class="kd">public</span> <span class="kt">void</span> <span class="nf">sendWelcomeEmail</span><span class="o">(</span><span class="nc">User</span> <span class="n">user</span><span class="o">)</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span> <span class="kd">public</span> <span class="nc">Report</span> <span class="nf">generateMonthlyReport</span><span class="o">()</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span> <span class="kd">public</span> <span class="kt">void</span> <span class="nf">syncWithExternalCRM</span><span class="o">()</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span> <span class="kd">public</span> <span class="kt">byte</span><span class="o">[]</span> <span class="nf">exportToCsv</span><span class="o">()</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span> <span class="o">}</span> </code></pre> </div> <p>Crear usuarios, mandar mails, generar reportes, sincronizar con un CRM y exportar a CSV. ¿Qué tienen en común? Que involucran usuarios. Y eso no es suficiente para estar en la misma clase.</p> <p>Alta cohesión significa que todo lo que hace una clase está estrechamente relacionado. Los métodos trabajan sobre los mismos datos para el mismo propósito.<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight java"><code><span class="nd">@Service</span> <span class="kd">public</span> <span class="kd">class</span> <span class="nc">UserService</span> <span class="o">{</span> <span class="kd">public</span> <span class="nc">User</span> <span class="nf">create</span><span class="o">(</span><span class="nc">CreateUserRequest</span> <span class="n">req</span><span class="o">)</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span> <span class="kd">public</span> <span class="nc">User</span> <span class="nf">findById</span><span class="o">(</span><span class="nc">Long</span> <span class="n">id</span><span class="o">)</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span> <span class="kd">public</span> <span class="nc">User</span> <span class="nf">update</span><span class="o">(</span><span class="nc">Long</span> <span class="n">id</span><span class="o">,</span> <span class="nc">UpdateUserRequest</span> <span class="n">req</span><span class="o">)</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span> <span class="kd">public</span> <span class="kt">void</span> <span class="nf">deactivate</span><span class="o">(</span><span class="nc">Long</span> <span class="n">id</span><span class="o">)</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span> <span class="o">}</span> <span class="nd">@Service</span> <span class="kd">public</span> <span class="kd">class</span> <span class="nc">UserNotificationService</span> <span class="o">{</span> <span class="kd">public</span> <span class="kt">void</span> <span class="nf">sendWelcomeEmail</span><span class="o">(</span><span class="nc">User</span> <span class="n">user</span><span class="o">)</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span> <span class="kd">public</span> <span class="kt">void</span> <span class="nf">sendDeactivationEmail</span><span class="o">(</span><span class="nc">User</span> <span class="n">user</span><span class="o">)</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span> <span class="o">}</span> <span class="nd">@Service</span> <span class="kd">public</span> <span class="kd">class</span> <span class="nc">UserExportService</span> <span class="o">{</span> <span class="kd">public</span> <span class="kt">byte</span><span class="o">[]</span> <span class="nf">exportToCsv</span><span class="o">(</span><span class="nc">UserExportCriteria</span> <span class="n">criteria</span><span class="o">)</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span> <span class="kd">public</span> <span class="nc">Report</span> <span class="nf">generateMonthlyReport</span><span class="o">()</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span> <span class="o">}</span> </code></pre> </div> <p>Cada clase tiene una razón para existir. Cada método trabaja con los mismos conceptos. Eso es cohesión alta.</p> <p>Si ayer leíste el artículo del God Object, esto te va a sonar familiar. SRP, acoplamiento y cohesión son tres formas de mirar el mismo problema: clases que hacen demasiado y están conectadas a todo.</p> <h2> La regla práctica </h2> <p>Cuando necesitas hacer un cambio, hazte estas tres preguntas:</p> <ol> <li> <strong>¿Cuántas clases tengo que tocar?</strong> Si son más de 2 o 3, tienes acoplamiento alto.</li> <li> <strong>¿Los métodos de esta clase cambian juntos?</strong> Si no, tienes cohesión baja.</li> <li> <strong>¿Estoy compartiendo un objeto entre capas que tienen necesidades diferentes?</strong> Si sí, necesitas modelos separados.</li> </ol> <h2> El error de fondo </h2> <p>El efecto dominó no es un problema de Spring, ni de Java, ni de ningún framework. Es un problema de diseño. Compartir objetos entre capas porque es "más rápido" y "menos código" funciona al principio. Después pagas el precio cada vez que tocas algo.</p> <p>Un poco de duplicación explícita (modelos separados por capa) es infinitamente mejor que un acoplamiento implícito que te explota en la cara cuando menos lo esperas.</p> <p>La próxima vez que vayas a compartir un DTO entre capas, hazte una pregunta: ¿realmente necesitan exactamente los mismos campos? Casi seguro que no.</p> <p>Día 14 de <strong>#100ArchitectureDays</strong>.</p>
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。