SOLID en Android: Clean Architecture para Apps Modernas
Índice de contenidos
🏗️ Introducción a SOLID en Android
Los principios SOLID son la base de la programación orientada a objetos moderna y son cruciales para el desarrollo de apps Android escalables como PuzzleQuest.
- S - Single Responsibility Principle (SRP)
- O - Open/Closed Principle (OCP)
- L - Liskov Substitution Principle (LSP)
- I - Interface Segregation Principle (ISP)
- D - Dependency Inversion Principle (DIP)
Aplicar estos principios en Android nos ayuda a evitar el temido “Spaghetti Code” y nos facilita el mantenimiento y testing de nuestra app.
🎯 Single Responsibility Principle (SRP)
“Una clase debe tener una, y solo una, razón para cambiar.”
En nuestra app de puzzles, evitamos las “God Activities” que hacen todo.
❌ Violación de SRP
class GameActivity : AppCompatActivity() {
private val puzzleData = mutableListOf<Piece>()
// ❌ Lógica de UI mezclada con lógica de datos y red
fun loadPuzzle() {
// Llamada a API
// Parseo de JSON
// Lógica de juego
// Actualización de UI
}
}
✅ Aplicación de SRP
Separamos responsabilidades en clases dedicadas:
// Responsabilidad: Orquestar UI y ViewModel
class GameActivity : AppCompatActivity() {
private val viewModel: GameViewModel by viewModels()
// Solo código de UI
}
// Responsabilidad: Gestionar estado y lógica de presentación
class GameViewModel(
private val getPuzzleUseCase: GetPuzzleUseCase
) : ViewModel()
// Responsabilidad: Lógica de negocio pura del juego
class PuzzleGameEngine {
fun calculateMove(currentBoard: Board, move: Move): BoardResult
}
// Responsabilidad: Obtención de datos
class PuzzleRepository(
private val api: PuzzleApi,
private val db: PuzzleDao
)
🔓 Open/Closed Principle (OCP)
“Las entidades de software deben estar abiertas para extensión, pero cerradas para modificación.”
Queremos añadir nuevos tipos de puzzles sin modificar el código existente.
❌ Violación de OCP
class PuzzleRenderer {
fun render(puzzle: Puzzle) {
if (puzzle.type == "SUDOKU") {
// Renderizar Sudoku
} else if (puzzle.type == "CROSSWORD") {
// Renderizar Crucigrama
}
// Cada nuevo tipo requiere modificar esta clase
}
}
✅ Aplicación de OCP
Usamos polimorfismo para extender funcionalidad:
interface PuzzleRenderer {
fun render(puzzle: Puzzle)
}
class SudokuRenderer : PuzzleRenderer {
override fun render(puzzle: Puzzle) { /* Lógica Sudoku */ }
}
class CrosswordRenderer : PuzzleRenderer {
override fun render(puzzle: Puzzle) { /* Lógica Crucigrama */ }
}
// Factoría o Inyección de Dependencias provee el renderer adecuado
class GameRenderer(private val renderer: PuzzleRenderer) {
fun draw(puzzle: Puzzle) {
renderer.render(puzzle) // No sabe ni le importa qué tipo es
}
}
🔄 Liskov Substitution Principle (LSP)
“Las clases derivadas deben poder sustituirse por sus clases base.”
Si tenemos una jerarquía de clases de Puzzle, cualquier subclase debe comportarse correctamente como un Puzzle.
❌ Violación de LSP
open class Puzzle {
open fun shufflePieces() { /* ... */ }
}
class StaticPuzzle : Puzzle() {
override fun shufflePieces() {
throw UnsupportedOperationException("Static puzzles cannot be shuffled!")
}
}
Esto rompe LSP porque StaticPuzzle no se comporta como un Puzzle esperado.
✅ Aplicación de LSP
Refactorizamos la jerarquía para reflejar capacidades reales:
interface Puzzle {
val id: String
val name: String
}
interface ShufflablePuzzle : Puzzle {
fun shufflePieces()
}
class JigsawPuzzle : ShufflablePuzzle {
override fun shufflePieces() { /* ... */ }
}
class StaticPuzzle : Puzzle {
// No implementa ShufflablePuzzle, no tiene método shufflePieces()
}
✂️ Interface Segregation Principle (ISP)
“Los clientes no deben depender de interfaces que no usan.”
Evitamos interfaces gigantes (“Fat Interfaces”) en nuestros Listeners o Callbacks.
❌ Violación de ISP
interface GameEventListener {
fun onGameStarted()
fun onPieceMoved()
fun onScoreUpdated()
fun onTimerTick()
fun onGameOver()
fun onPause()
}
// Una vista de solo puntuación no necesita saber sobre movimiento de piezas
class ScoreView : GameEventListener {
override fun onScoreUpdated() { updateScore() }
override fun onPieceMoved() { /* Vacío - No me importa */ }
// ... muchos métodos vacíos
}
✅ Aplicación de ISP
Dividimos en interfaces más específicas:
interface GameStateListener {
fun onGameStarted()
fun onGameOver()
}
interface ScoreListener {
fun onScoreUpdated(newScore: Int)
}
interface MoveListener {
fun onPieceMoved(move: Move)
}
class ScoreView : ScoreListener {
override fun onScoreUpdated(newScore: Int) { updateScore() }
}
🔌 Dependency Inversion Principle (DIP)
“Depende de abstracciones, no de concreciones.”
Este es el corazón de la Clean Architecture y la Inyección de Dependencias.
❌ Violación de DIP
class PuzzleRepository {
// Dependencia directa de una implementación concreta (SQLite)
private val database = SQLiteDatabase()
fun getPuzzle(id: String) {
database.query(...)
}
}
✅ Aplicación de DIP
Usamos interfaces para invertir la dependencia:
// Abstracción (Domain Layer)
interface PuzzleDataSource {
fun getPuzzle(id: String): Puzzle
}
// Implementación Concreta (Data Layer)
class RoomPuzzleDataSource : PuzzleDataSource {
override fun getPuzzle(id: String): Puzzle { /* Room implementation */ }
}
class FirebasePuzzleDataSource : PuzzleDataSource {
override fun getPuzzle(id: String): Puzzle { /* Firebase implementation */ }
}
// Consumidor (Domain/Data Layer)
class PuzzleRepository(
private val dataSource: PuzzleDataSource // Depende de la abstracción
) {
fun getPuzzle(id: String) = dataSource.getPuzzle(id)
}
Ahora podemos cambiar Room por Firebase o un Mock para testing sin tocar el PuzzleRepository.
🚀 Conclusión
Aplicar SOLID en el desarrollo Android requiere disciplina, pero los beneficios son inmensos:
- Testabilidad: Código desacoplado es fácil de testear.
- Mantenibilidad: Cambios en un módulo no rompen otros.
- Escalabilidad: Fácil añadir nuevas features (como nuevos tipos de puzzles).
- Legibilidad: Clases pequeñas y enfocadas son más fáciles de entender.
En PuzzleQuest, estos principios nos permiten construir una base sólida sobre la cual podemos iterar y mejorar nuestro juego continuamente.
Artículos relacionados
Repository Pattern: La Verdadera Abstracción de Datos
Por qué el Repository es el patrón más importante en Clean Architecture. Estrategias de caché, manejo de errores y orquestación de fuentes de datos.
MVVM Model: La Capa de Datos Invisible pero Vital
El 'Model' en MVVM es mucho más que clases de datos. Aprende a diseñar una capa de modelo robusta que sobreviva a cambios de UI y backend.
Refactoring con IA: De Legacy Code a Clean Code
Aprende estrategias seguras para modernizar bases de código antiguas usando asistentes de IA. Refactoriza clases masivas, elimina código muerto y migra a Kotlin.