Extraer código repetido suele parecer una mejora evidente. Dos pantallas muestran algo parecido, creamos un componente y ambas lo utilizan. Después llega una necesidad exclusiva de una pantalla. Añadimos un booleano. Más adelante aparece otra excepción y añadimos otro.
El resultado puede seguir llamándose «compartido», aunque cada consumidor utilice una parte diferente. Podemos describir este problema como una convivencia entre responsabilidades que han dejado de encajar. La reutilización pierde valor cuando obliga a coordinar decisiones que deberían evolucionar por separado.
Los booleanos pueden revelar una separación pendiente
Imaginemos una app de lectura. La estantería permite marcar libros como favoritos y el catálogo identifica los que necesitan suscripción. Una primera implementación concentra ambas opciones:
struct BookTile: View {
let title: String
let showFavorite: Bool
let showSubscription: Bool
var body: some View {
Text(title)
.font(.headline)
.padding()
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12))
.overlay(alignment: .topTrailing) {
if showFavorite {
Image(systemName: "heart.fill")
}
}
.overlay(alignment: .bottomTrailing) {
if showSubscription {
Image(systemName: "lock.fill")
}
}
}
}
Dos booleanos permiten cuatro combinaciones. Quizá el producto solo necesita dos. Con tres booleanos serían ocho; con cuatro, dieciséis. El número de combinaciones posibles crece aunque las pantallas reales sigan siendo pocas.
Un booleano no demuestra por sí mismo un mal diseño. La señal aparece cuando cada pantalla activa sus propias funciones y desactiva sistemáticamente las demás. El componente empieza a conocer demasiados detalles sobre quienes lo utilizan.
Conservar la base y componer las diferencias
En este caso podemos mantener la presentación común del título y trasladar las decoraciones a las pantallas correspondientes:
struct BookTile: View {
let title: String
var body: some View {
Text(title)
.font(.headline)
.padding()
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12))
}
}
struct ShelfBook: View {
let title: String
let isFavorite: Bool
var body: some View {
BookTile(title: title)
.overlay(alignment: .topTrailing) {
if isFavorite {
Image(systemName: "heart.fill")
}
}
}
}
struct CatalogBook: View {
let title: String
let requiresSubscription: Bool
var body: some View {
BookTile(title: title)
.overlay(alignment: .bottomTrailing) {
if requiresSubscription {
Image(systemName: "lock.fill")
}
}
}
}
El modificador overlay(alignment:content:) permite añadir contenido sobre una vista. Aquí nos sirve para colocar cada decoración fuera del componente común.
Las condiciones siguen existiendo, pero ahora expresan el estado de cada libro dentro de su contexto. BookTile ya no decide qué funcionalidades pertenecen a la estantería o al catálogo. Si cambia la identificación de los libros de suscripción, esa modificación queda localizada en CatalogBook.
Cuando las diferencias afectan al cálculo
La composición visual funciona bien cuando las variaciones pueden añadirse alrededor de una base. En la lógica de negocio, las reglas pueden depender unas de otras.
Supongamos que compartimos un cálculo de páginas con opciones como excludeFinished, useRemainingPages y applyDailyLimit. El informe histórico necesita todas las páginas leídas; el plan de hoy necesita páginas pendientes y un límite diario. Activar nuevas combinaciones puede mezclar criterios incompatibles.
Podemos dar a cada operación un nombre y una implementación propios:
struct ReadingProgress {
let pagesRead: Int
let totalPages: Int
}
func historicalPages(_ books: [ReadingProgress]) -> Int {
books.reduce(0) { $0 + $1.pagesRead }
}
func pagesForToday(
_ books: [ReadingProgress],
dailyLimit: Int
) -> Int {
let remaining = books.reduce(0) {
$0 + max(0, $1.totalPages - $1.pagesRead)
}
return min(remaining, max(0, dailyLimit))
}
Ambas funciones recorren una colección, pero responden a preguntas distintas. El límite diario pertenece al plan de lectura y no forma parte del informe histórico. Compartir ese recorrido aportaría poco si exige introducir parámetros para distinguir ambos significados.
Repetir unas líneas puede aclarar el diseño
Una posible solución consiste recuperar la duplicación cuando una abstracción se ha vuelto inadecuada: devolver el código a sus consumidores, eliminar lo que cada uno no necesita y observar qué merece seguir compartido.
Para aplicar esta revisión, conviene empezar por los usos reales del componente. Identifica qué reglas necesita cada consumidor, conserva las pruebas del comportamiento esperado y separa las decisiones exclusivas. Después, comprueba especialmente los casos donde interactúan filtros, límites u otras condiciones.
Cambiar los booleanos por un enum puede restringir combinaciones válidas, pero no resuelve automáticamente la mezcla de responsabilidades. Tampoco hace falta crear un protocolo por cada diferencia.
El objetivo es que cada regla tenga un lugar claro. Si queda una base útil, podemos conservarla y componer alrededor de ella. Si apenas queda una operación trivial, eliminar el componente compartido puede hacer que el código resulte más fácil de entender y modificar.