5 PRINCIPIOS PARA ESCRIBIR CODIGO SOLID

5 principios para escribir código SOLID

Principios de diseño SOLID

SOLID es el acrónimo de una colección de 5 principios de diseño orientado a objetos, conceptualizados por primera vez por Robert C. Martin hace unos 20 años, y han dado forma a la forma en que escribimos software hoy.

  1. Principio de responsabilidad única
  2. Principio abierto / cerrado
  3. Principio de sustitución de Liskov
  4. Principio de segregación de interfaz
  5. Principio de inversión de dependencia

Todos son conceptos simples que son fáciles de comprender, pero realmente valiosos al escribir código estándar de la industria.

1. Principio de responsabilidad única

Una clase debe tener una, y solo una, razón para cambiar.

Este es probablemente el principio más intuitivo, también válido para componentes de software o microservicios. Tener “solo una razón para cambiar” podría reformularse como tener “solo una responsabilidad”. Esto hace que el código sea más robusto y flexible, más fácil de entender para otra persona y evitará algunos efectos secundarios inesperados al cambiar el código existente. También necesitará hacer menos cambios: cuantas más razones independientes tenga una clase para cambiar, más a menudo tendrá que cambiar. Si tiene muchas clases que dependen unas de otras, la cantidad de cambios que necesita hacer podría crecer exponencialmente. Cuanto más complicadas sean tus clases, más difícil será cambiarlas sin consecuencias inesperadas.

clase Álbum: 
def __init __ (yo, nombre, artista, canciones) -> Ninguno:
self.name = nombre
self.artist = artista
self.songs = canciones
def add_song (self, song):
self.songs.append (canción)
def remove_song (yo, canción):
self.songs.remove (canción)
def __str __ (self) -> str:
return f "Álbum {self.name} de {self.artist} \ nTracklist: \ n {self.songs}"
# rompe el SRP
def search_album_by_artist (self):
"" "Buscando en la base de datos otros álbumes del mismo artista" ""
pass

En el ejemplo anterior, he creado una clase AlbumEsto almacena el nombre del álbum, el artista y la lista de pistas, y puede manipular el contenido del álbum, como agregar canciones o eliminar. Ahora, si agrego una función para buscar álbumes del mismo artista, rompo el Principio de Responsabilidad Única. Mi clase tendría que cambiar si decido almacenar álbumes de una manera diferente (por ejemplo, agregando la etiqueta de grabación o almacenando la lista de pistas como un diccionario del nombre y la duración de la pista), y mi clase también debería cambiar si cambio la base de datos donde guardo estos álbumes (por ejemplo, me muevo a una base de datos en línea desde una hoja de Excel). Está claro que se trata de dos responsabilidades distintas.
En su lugar, debería crear una clase para interactuar con la base de datos de Álbumes. Esto podría ampliarse con la búsqueda de álbumes por letra inicial, número de pistas, etc. (consulte el siguiente principio sobre cómo exactamente)

# en su lugar: 
class AlbumBrowser:
"" "Clase para navegar en la base de datos de álbumes" ""
def search_album_by_artist (self, albums, artist):
pass

def search_album_starting_with_letter (self, albums, letter):
pass

Una advertencia: hacer que las clases sean demasiado simples es hacer que el código sea tan difícil de leer, ya que uno tendría que seguir una larga cadena de objetos pasados entre sí y podría conducir a una base de código fragmentada con clases de un solo método. Este principio no significa que cada clase deba hacer una sola cosa como en un método, sino un concepto .

2.Principio de abierto-cerrado

Las entidades de software (clases, módulos, funciones, etc.) deben estar abiertas para su extensión, pero cerradas para modificaciones.

Esto significa que debería poder agregar una nueva funcionalidad sin cambiar mi estructura de código existente, sino agregando un nuevo código en su lugar. El objetivo es cambiar el código probado existente lo menos posible para evitar errores y tener que probar todo de nuevo. Si no se sigue este principio, el resultado podría ser una larga lista de cambios en las clases dependientes, regresión de las características existentes y horas innecesarias de pruebas.

Esto se demuestra con el siguiente ejemplo:

clase Álbum: 
def __init __ (yo, nombre, artista, canciones, género):
self.name = nombre
self.artist = artista
self.songs = canciones
self.genre = genre
# antes de la
clase AlbumBrowser:
def search_album_by_artist (yo, álbumes, artista):
devolver [álbum por álbum en álbumes si album.artist == artista]
def search_album_by_genre (yo, álbumes, género):
devolver [álbum por álbum en álbumes si album.genre == género]

Ahora, ¿qué pasa si quiero buscar por artista y género ? ¿Qué pasa si agrego un año de lanzamiento ? Tendré que escribir una nueva función cada vez (en total (2 ^ n) -1 para ser precisos), y el número crece exponencialmente.

En su lugar, debería definir una clase base con una interfaz común para mi especificación y luego definir subclases para cada tipo de especificación que hereda esta interfaz de la clase base:

#después de la 
clase SearchBy:
def is_matched (self, album):
pass

class SearchByGenre (SearchBy):
def __init __ (self, genre):
self.genre = genre
def is_matched (self, album):
return album.genre == self.genre

class SearchByArtist (SearchBy):
def __init __ (self, artist):
self.artist = artist
def is_matched (self, album):
volver album.artist == self.artist

class AlbumBrowser:
def navegar (self, albums, searchby):
return [album para album en álbumes si searchby.is_matched (album)]

Esto nos permite extender las búsquedas con otra clase cuando queramos (por ejemplo, por fecha de lanzamiento). Cualquier nueva clase de búsqueda deberá satisfacer la interfaz definida por Searchby, por lo que no tendremos sorpresas al interactuar con nuestro código existente. Para navegar por un criterio, ahora necesitamos crear un objeto SearchBy primero y pasarlo a AlbumBrowser.

Pero, ¿qué pasa con los criterios múltiples? Realmente me gusta esta solución que vi en este curso de Design Patterns Udemy . Esto permite que el uso para unir criterios de navegación se unan mediante &:

#add __and__: 
class SearchBy:
def is_matched (self, album):
pass

def __and __ (self, other):
return AndSearchBy (self, other)
clase AndSearchBy (SearchBy):
def __init __ (self, searchby1, searchby2):
self.searchby1 = searchby1
self.searchby2 = searchby2
def is_matched (self, album):
devuelve self.searchby1.is_matched (album) y self.searchby2.is_matched (album)

Este &método puede ser un poco confuso, por lo que el siguiente ejemplo demuestra el uso:

LAWoman = Álbum ( 
name = "LA Woman",
artist = "The Doors",
songs = ["Riders on the Storm"],
genre = "Rock",
)
Trash = Álbum (
name = "Trash",
artist = "Alice Cooper",
songs = ["Poison"],
genre = "Rock",
)
álbumes = [LAWoman, Trash]
# esto crea el objeto
AndSearchBy my_search_criteria = SearchByGenre (genre = "Rock") & SearchByArtist (
artist = "The Doors"
)
browser = AlbumBrowser ()
afirmar browser.browse (álbumes = álbumes, searchby = my_search_criteria) == [LAWoman]
# yay encontramos nuestro álbum

3. Principio de sustitución de Liskov

Este principio es de Barbara Liskov, quien formuló su principio de manera muy formal:

“Sea φ (x) una propiedad demostrable sobre objetos x de tipo T. Entonces φ (y) debería ser verdadera para objetos y de tipo S donde S es un subtipo de T.”

Esto significa que si tenemos una clase base T y una subclase S, debería poder sustituir la clase principal T con la subclase S sin romper el código. La interfaz de una subclase debe ser la misma que la interfaz de la clase base y la subclase debe comportarse de la misma manera que la clase base.

Si tiene un método en T que se anula en S, ambos métodos deben tomar las mismas entradas y devolver el mismo tipo de salida. La subclase puede devolver solo un subconjunto de los valores de retorno de la clase base, pero debe aceptar todas las entradas que hace la clase base.

En el ejemplo clásico con rectángulos y cuadrados, creamos una clase Rectangle, con valores de ancho y alto. Si tiene un cuadrado, el definidor de ancho también necesita cambiar el tamaño de la altura y viceversa para mantener la propiedad del cuadrado. Esto nos obliga a tomar una decisión: mantenemos la implementación de la clase Rectangle, pero luego Square deja de ser un cuadrado cuando usas el setter en él, o cambias los setters para hacer que la altura y el ancho sean iguales para los cuadrados. Esto podría llevar a un comportamiento inesperado si tiene una función que cambia el tamaño de la altura de su forma.

class Rectangle: 
def __init __ (self, height, width):
self._height = height
self._width = width
@property
def width (self):
return self._width
@ width.setter
def width (self, value):
self._width = valor
@property
def height (self):
return self._height
@ height.setter
def altura (self, value):
self._height = valor
def get_area (self):
return self._width * self._height
class Cuadrado (Rectángulo):
def __init __ (self, size):
Rectangle .__ init __ (self, size, size)
@ Rectangle.width.setter
def width (self, value):
self._width = valor
self._height = valor
@ Rectangle.height.setter
def height (self, value):
self._width = valor
self._height = valor
def get_squashed_height_area (Rectangle):
Rectangle.height = 1
area = Rectangle.get_area ()
return area
rectángulo = Rectángulo (5, 5)
cuadrado = Cuadrado (5)
afirmar get_squashed_height_area (rectángulo) == 5 # esperado 5
afirmar get_squashed_height_area (cuadrado) == 1 # esperado 5

Si bien esto puede no parecer un gran problema (¡¿seguramente puede recordar que sqaure también cambia el ancho?!), Esto se convierte en un problema mayor cuando las funciones son más complicadas o cuando está utilizando el código de otra persona, y simplemente asume que la subclase se comporta de la misma manera. mismo.

Un ejemplo breve pero intuitivo que realmente me gusta del artículo Wiki del problema del círculo-elipse :

class Person (): 
def walkNorth (metros):
pass
def walkSouth (metros):
pass
clase Prisoner (Person):
def walkNorth (metros):
pass
def walkSouth (metros):
pass

Obviamente, no podemos implementar los métodos de caminata en los prisioneros, ya que no son libres de caminar distancias arbitrarias en direcciones arbitrarias. No se nos debería permitir llamar a métodos walk en la clase, la interfaz es incorrecta. Lo que nos lleva a nuestro siguiente principio …

4. Principio de segregación de interfaces

“Los clientes no deben verse obligados a depender de interfaces que no utilizan”.

Si tiene una clase base con muchos métodos, posiblemente no todas sus subclases los necesitarán, tal vez solo algunos. Pero debido a la herencia, podrá llamar a estos métodos en todas las subclases, incluso en aquellas que no lo necesiten. Esto significa una gran cantidad de interfaces que no se utilizan, son innecesarias y darán lugar a errores cuando se llamen accidentalmente.

Este principio está destinado a evitar que esto suceda. Deberíamos hacer las interfaces lo más pequeñas posible, de modo que no necesitemos implementar funciones que no necesitamos. En lugar de una clase base grande, deberíamos dividirlas en varias. Solo deben tener métodos que tengan sentido para cada uno, y luego hacer que nuestras subclases hereden de ellos.

En el siguiente ejemplo, usaremos métodos abstractos. Los métodos abstractos crean una interfaz en una clase base que no tiene implementación, pero están obligados a implementarse en cada subclase que hereda de la clase base. Los métodos abstractos están esencialmente imponiendo una interfaz.

class PlaySongs: 
def __init __ (self, title):
self.title = título
def play_drums (self):
print ("Ba-dum ts")
def play_guitar (self):
print ("* Solo de guitarra conmovedor *")
def sing_lyrics (self):
print ("NaNaNaNa")
# Esta clase está bien, solo cambiando la
clase de guitarra y letra PlayRockSongs (PlaySongs):
def play_guitar (self):
print ("* Solo de guitarra muy metal *")
def sing_lyrics (self):
print ("Quiero rockear toda la noche")
# Esto rompe el ISP, no tenemos la
clase de letras PlayInstrumentalSongs (PlaySongs):
def sing_lyrics (self):
raise Exception ("No hay letras para canciones instrumentales")

En cambio, podríamos tener una clase para el canto y la música por separado (asumiendo que la guitarra y la batería siempre ocurren juntas en nuestro caso, de lo contrario tenemos que dividirlos aún más, quizás por instrumento). De esta manera, solo tenemos las interfaces que necesidad, no podemos llamar a cantar letras en canciones instrumentales.

desde abc import ABCMeta
class PlaySongsLyrics:
@abstractmethod
def sing_lyrics (self, title):
pass
clase PlaySongsMusic:
@abstractmethod
def play_guitar (self, title):
pass
@abstractmethod
def play_drums (yo, título):
pasar
clase PlayInstrumentalSong (PlaySongsMusic):
def play_drums (self, title):
print ("Ba-dum ts")
def play_guitar (self, title):
print ("* Solo de guitarra que mueve el alma *")
class PlayRockSong (PlaySongsMusic, PlaySongsLyrics):
def play_guitar (self):
print ("* Solo de guitarra muy metal *")
def sing_lyrics (self):
print ("Quiero rockear toda la noche")
def play_drums (self, title):
print ("Ba-dum ts")

5. Principio de inversión de dependencia

El último principio dice

Los módulos de alto nivel no deben depender de módulos de bajo nivel. Ambos deberían depender de abstracciones (por ejemplo, interfaces).

Las abstracciones no deberían depender de los detalles. Los detalles (implementaciones concretas) deben depender de abstraccionesSi su código tiene interfaces abstractas bien definidas, cambiar la implementación interna de una clase no debería romper su código. Una clase con la que interactúa no debería tener conocimiento del funcionamiento interno de la otra clase y no debería verse afectada siempre que las interfaces sean las mismas. Un ejemplo sería cambiar el tipo de base de datos que usa (SQL o NoSQL) o cambiar la estructura de datos en la que almacena sus datos (diccionario o lista).

Esto se ilustra en el siguiente ejemplo, donde ViewRockAlbums depende explícitamente del hecho de que los álbumes se almacenan en una tupla en un orden determinado dentro de AlbumStore. No debe tener conocimiento de la estructura interna de Albumstore. Ahora bien, si cambiamos el orden en las tuplas del álbum, nuestro código se rompería.

clase AlbumStore: 
álbumes = []
def add_album (yo, nombre, artista, género):
self.albums.append ((nombre, artista, género))
class ViewRockAlbums:
def __init __ (self, album_store):
para el álbum en album_store.albums:
if album [2] == "Rock":
print (f "Tenemos {album [0]} en la tienda.")

En su lugar, necesitamos agregar una interfaz abstracta a AlbumStore para ocultar los detalles, que pueden ser invocados por otras clases. Esto debería hacerse como en el ejemplo del Principio Abierto-Cerrado, pero asumiendo que no nos importa filtrar por nada más, simplemente agregaré un método filter_by_genre. Ahora bien, si tuviéramos otro tipo de AlbumStore, que decide almacenar el álbum de manera diferente, necesitaría implementar la misma interfaz para filter_by_genre para que ViewRockAlbums funcione.

class GeneralAlbumStore: 
@abstractmethod
def filter_by_genre (self, genre)
pass
clase MyAlbumStore (GeneralAlbumStore):
álbumes = []
def add_album (yo, nombre, artista, género):
self.albums.append ((nombre, artista, género))
def filter_by_genre (self, genre):
if album [2] == "Rock":
producir album [0]
class ViewRockAlbums:
def __init __ (self, album_store):
para album_name en album_store.filter_by_genre ("Rock"):
print (f "Tenemos {album_name} en la tienda.")

Conclusión

Los principios de diseño SOLID están destinados a ser una guía para escribir código mantenible, expandible y fácil de entender. Vale la pena tenerlos en cuenta la próxima vez que piense en un diseño, para escribir código SÓLIDO. Simplemente repase las letras en su mente, recordando lo que significaban cada una:

  1. Principio de responsabilidad única
  2. Principio abierto / cerrado
  3. Principio de sustitución de Liskov
  4. Principio de segregación de interfaz
  5. Principio de inversión de dependencia

¡Ahora ve y haz del mundo un lugar mejor código base por código base

Fuente: https://n9.cl/m95pe

1 comentario en “5 PRINCIPIOS PARA ESCRIBIR CODIGO SOLID”

Los comentarios están cerrados.