Platzi
Platzi

Suscríbete a Expert y aprende de tecnología al mejor precio anual.

Antes: $249
$209
Currency
Antes: $249
Ahorras: $40
COMIENZA AHORA
Termina en: 5D : 19H : 6M : 45S

Principios SOLID3/32

Lectura

Anteriormente en Platzi hemos hablado de los principios SOLID (te invito a revisar más a detalle estos conceptos en el Curso de Buenas Prácticas para Escritura de Código y el Curso de Arquitectura de Android). En esta clase daré un repaso general utilizando ejemplos más prácticos para Android y Kotlin.

Principio de Responsabilidad Única (Single Responsability Principle)

Este principio dice que, básicamente, una clase, módulo o componente debe tener sólo una razón para cambiar. En este ejemplo, el objetivo de esta clase es obtener el caso de uso de una aplicación que necesita obtener todas las canciones de un artista a partir de su ID, dándole sólo un propósito que manejar, y esto lo hace solamente invocando al repositorio correspondiente para obtener la información (más adelante en el curso te explicaré qué es la capa de casos de uso y cómo sacarle provecho en Clean Architecture).

class GetAllSongsByArtistIDUseCase(private val songRepository: SongRepository) {
    fun invoke(artistId: Int): List<Song> = songRepository.getAllSongsByArtistID(artistId)
}

Principio de Abierto/Cerrado (Open/Closed Principle)

Este principio indica que una entidad de software debe quedar abierta para su extensión, pero cerrada para su modificación. Para este ejemplo, imagina que hay diferentes roles para un ingeniero de software y al inicializar un proyecto, cada uno de ellos empieza a trabajar en su plataforma. Usando este principio, podemos definir una entidad tal como SoftwareEngineerContract, que será con la que trabajaremos en Project, y si se llegaran a crear más puestos como, por ejemplo, un PythonDeveloper, esta clase, en teoría, no se ve afectada.

interface SoftwareEngineerContract {
    fun develop()
}

class AndroidDeveloper : SoftwareEngineerContract {
    override fun develop() {
       println("Developing for Android...")
    }
}

class IOsDeveloper : SoftwareEngineerContract {
    override fun develop() {
       println("Developing for iOs...")
    }
}

class Project(private val softwareEngineerList: List<SoftwareEngineerContract>) {
    fun startProject(){
        softwareEngineerList.forEach { softwareEngineer -> softwareEngineer.develop() }
    }
}

fun main() {
    val project = Project(listOf(AndroidDeveloper(), IOsDeveloper()))
    project.startProject()
}

Principio de Sustitución de Liskov (Liskov Substitution Principle)

Este principio establece que cada clase que hereda de otra puede usarse como su padre sin necesidad de conocer las diferencias entre ellas. En este ejemplo la clase Bird define dos métodos, y su clase hija Eagle trabaja con estas sin problema aparente, pero tenemos otra clase hija, Penguin, que sobreescribe la función fly() para lanzar una excepción, por lo que te imaginarás, cuando se ejecute la función main() el programa fallará.

Implementation incorrecta

open class Bird {
    open fun eat() { println("Eating...") }
    open fun fly() { println("Flying...") }
}

class Eagle : Bird() { }

class Penguin : Bird() {
    override fun fly() {
        throw Exception("Penguins can't fly")
    }
}

fun main() {
    val birdList = listOf<Bird>(Eagle(), Penguin())
    birdList.forEach { bird -> 
        bird.eat() 
        bird.fly()
    }
}

Para evitarlo, debemos mejorar la abstracción de Bird, generando dos clases más FlyingBird y NonFlyingBird que se encargarán de darle más sentido a la implementación como se muestra en el siguiente ejemplo.

Implementation correcta

abstract class Bird {
    fun eat() { println("Eating...") }
}

open class FlyingBird: Bird() {
    fun fly() { println("Flying...") }
}

open class NonFlyingBird: Bird() { }

class Eagle : FlyingBird() { }

class Penguin : NonFlyingBird() { }

fun main() {
    val flyingBirdList = listOf<FlyingBird>(Eagle(), Eagle())
    flyingBirdList.forEach { bird -> 
        bird.eat() 
        bird.fly()
    }
    
    val nonFlyingBirdList = listOf<NonFlyingBird>(Penguin(), Penguin())
    nonFlyingBirdList.forEach { bird -> 
        bird.eat() 
    }
}

Principio de Segregación de Interfaces (Interface Segregation Principle)

Este principio plantea que los clientes de un programa dado sólo deberían conocer los métodos que realmente usan. ¿Cuántas veces te ha tocado que una clase implementa interfaces que deja vacías las implementaciones de algunos métodos, o peor aún, que lanzan excepciones que realmente no son necesarias en el flujo de tu proyecto?

Imagina el siguiente ejemplo: se define una interfaz Musician, la cual tiene los métodos sing() (lo que hace un cantante), playWithTapping (tocar con una técnica muy popular entre algunos guitarristas y bajistas) y doDrumRoll() (hacer redobles en el tambor o tarola). La clase OzzyOrbourne implementa la interfaz y sabe usar sing() (“Iron Man 🎤”), pero los demás métodos no los usa, por lo que quedan vacíos.

Implementation incorrecta

interface Musician {
    fun sing()
    fun playWithTapping()
    fun doDrumRoll()
}

class OzzyOsbourne: Musician() {
    fun sing() { 
        println("Sing Iron Man |m|") 
    }
    
    fun platWithTapping() { 
        //Do nothing 
    }
    
    fun doDrumRoll() { 
        //Do nothing 
    }
}

La mejor forma de solucionar esto es generando interfaces con los métodos que sí ocupará el cliente tales como SingerMusician, GuitaristMusician y DrummerMusician. De esta forma, las clases OzzyOsbourne, EddieVanHalen y NickoMcbrain se enfocarán en lo que mejor saben hacer 😉

Implementation correcta

interface SingerMusician {
    fun sing()
}

interface GuitaristMusician {
    fun playWithTapping()
}

interface DrummerMusician {
    fun doDrumRoll()
}

class OzzyOsbourne: SingerMusician() {
    fun sing() { 
        println("Sing Iron Man |m|") 
    }
}

class EddieVanHalen: GuitaristMusician() {
    fun playWithTapping() { 
        println("Playing Eruption |m|") 
    }
}

class NickoMcbrain: DrummerMusician() {
    fun doDrumRoll() { 
        println("Playing The Trooper |m|") 
    }
}

Principio de Inversión de Dependencias (Dependency Inversion Principle)

Este principio es uno de los más importantes que trabajarás en el curso. Indica que los módulos de alto y bajo nivel deben depender de abstracciones, es decir, los detalles deben depender de abstracciones. Para el siguiente ejemplo se indica una interfaz LocalBookDataSource que será el contrato que definan las clases FileBookDataSource (que obtiene los libros a partir de un archivo), RoomBookDataSource (que obtiene los libros a partir de una base de datos manejada con Room) y MockLocalBookDataSource (que se puede utilizar para realizar pruebas unitarias). De esta forma, puedes utilizar alguna de estas implementaciones en la función main() sin afectar el código de tu aplicación. Más adelante te enseñaré con detalle este principio en acción con el proyecto final.

interface LocalBookDataSource {
    fun getAllBooks(): List<Book>
}

//Option 1
class FileBookDataSource(): LocalBookDataSource {
    override fun getAllBooks(): List<Book> {
        //Search book list data from file.
    }
}

//Option 2
class RoomBookDataSource(): LocalBookDataSource {
    override fun getAllBooks(): List<Book> {
        //Search book list data from database using Room.
    }
}

//Option 3
class MockLocalBookDataSource(): LocalBookDataSource {
    override fun getAllBooks(): List<Book> {
        //Create a mock for unit test purpose
    }
}

fun main(){
    val bookDataSource1: LocalBookDataSource = FileBookDataSource() //Use Option 1
    val bookDataSource2: LocalBookDataSource = RoomBookDataSource() //or use Option 2
    val bookDataSource3: LocalBookDataSource = MockLocalBookDataSource() //or use Option 3
    
    bookDataSource1.getAllBooks()
    bookDataSource2.getAllBooks()
    bookDataSource3.getAllBooks()
}

En conclusión, los principios SOLID son fundamentales para la creación de código de calidad, por lo que ahora que tienes mayor contexto de cómo aplicarlos en Android notarás que, al aplicarlos en Clean Architecture, tus proyectos tendrán una mejor estructura y calidad.

Aportes 5

Preguntas 0

Ordenar por: