Este portafolio tendra todo lo que se realizara en la clase de fundamentos de diseño

Overview

Principios SOLID 🚀

Todos sabemos que la POO (Programación Orientada a Objetos) nos permite agrupar entidades con funcionalidades parecidas o relacionadas entre sí, pero esto no implica que los programas no se vuelvan confusos o difíciles de mantener.


Lenguaje de programacion manejado para los ejemplos es Dart

De hecho, muchos programas acaban volviéndose un monstruo al que se va alimentando según se añaden nuevas funcionalidades, se realiza mantenimiento, etc…

Viendo este problema, Robert C. Martin estableció cinco directrices o principios para facilitarnos a los desarrolladores la labor de crear programas legibles y mantenibles.

Estos principios se llamaron S.O.L.I.D. por sus siglas en inglés:
S: Single responsibility principle o Principio de responsabilidad única

O: Open/closed principle o Principio de abierto/cerrado

L: Liskov substitution principle o Principio de sustitución de Liskov

I: Interface segregation principle o Principio de segregación de la interfaz

D: Dependency inversion principle o Principio de inversión de dependencia

Aplicar estos principios facilitará mucho el trabajo, tanto propio como ajeno (es muy probable que tu código lo acabe leyendo muchos otros desarrolladores a lo largo de su ciclo de vida). Algunas de las ventajas de aplicarlo son:

-Mantenimiento del código más fácil y rápido

-Permite añadir nuevas funcionalidades de forma más sencilla

-Favorece una mayor reusabilidad y calidad del código, así como la encapsulación

Vamos a ver en detalle cada uno de estos principios, junto a ejemplos básicos, que, a pesar de no ser aplicables en el mundo real, espero que aporten la suficiente claridad para que seas capaz de entender y aplicar estos principios en tus desarrollos.

S: Principio de responsabilidad única



Como su propio nombre indica, establece que una clase, componente o microservicio debe ser responsable de una sola cosa (el tan aclamado término “decoupled” en inglés). Si por el contrario, una clase tiene varias responsabilidades, esto implica que el cambio en una responsabilidad provocará la modificación en otra responsabilidad.

Considera este ejemplo:

class Coche {
 String marca = "";

 Coche(String marca) {
   this.marca;
 }

 String getMarcaCoche() {
   return marca;
 }

 void guardarCocheDB(Coche coche) {}
}

 

¿Por qué este código viola el principio de responsabilidad única? Para un minuto y piensa un poco ;)



Como podemos observar, la clase Coche permite tanto el acceso a las propiedades de la clase como a realizar operaciones sobre la BBDD, por lo que la clase ya tiene más de una responsabilidad.

Supongamos que debemos realizar cambios en los métodos que realizan las operaciones a la BBDD. En este caso, además de estos cambios, probablemente tendríamos que tocar los nombres o tipos de las propiedades, métodos, etc, cosa que no parece muy eficiente porque solo estamos modificando cosas que tienen que ver con la BBDD, ¿verdad?

Para evitar esto, debemos separar las responsabilidades de la clase, por lo que podemos crear otra clase que se encargue de las operaciones a la BBDD:

class Coche {
 String marca = "";

 Coche(String marca) {
   this.marca;
 }

 String getMarcaCoche() {
   return marca;
 }
}

class CocheDB {
 void guardarCocheDB(Coche coche) {...}
 void eliminarCoccheDB(Coche coche) {...}
}


 

Nuestro programa será mucho más cohesivo y estará más encapsulado aplicando este principio.

O: Principio abierto/cerrado

Establece que las entidades software (clases, módulos y funciones) deberían estar abiertos para su extensión, pero cerrados para su modificación.

Si seguimos con la clase Coche:

class Coche {
  String marca = "";

  Coche(String marca) {
    this.marca;
  }

  String getMarcaCoche() {
    return marca;
  }
}



Si quisiéramos iterar a través de una lista de coches e imprimir sus marcas por pantalla:

Coche coche1 = Coche("");
 Coche coche2 = Coche("");
void main(){

  
  List<String> listaDeCoches = [ coche1.marca = "Audi", coche2.marca = "Renault"];
  
  imprimirPrecioCoche(listaDeCoches);
}


void imprimirPrecioCoche(List coches){
  
  coches.forEach((element){
    if(element == coche1.marca){
      print(180000);
    }
    if(element == coche2.marca){
      print(25000);
    }
  });

}

image



Esto no cumpliría el principio abierto/cerrado, ya que si decidimos añadir un nuevo coche de otra marca:

 List<String> listaDeCoches = [ coche1.marca = "Audi", coche2.marca = "Renault"];



También tendríamos que modificar el método que hemos creado anteriormente:

void imprimirPrecioCoche(List coches){

coches.forEach((element){
  if(element == coche1.marca){
    print(180000);
  }
  if(element == coche2.marca){
    print(25000);
  }
});



Como podemos ver, para cada nuevo coche habría que añadir nueva lógica al método precioMedioCoche(). Esto es un ejemplo sencillo, pero imagina que tu aplicación crece y crece… ¿cuántas modificaciones tendríamos que hacer? Mejor evitarnos esta pérdida de tiempo y dolor de cabeza, ¿verdad?

Para que cumpla con este principio podríamos hacer lo siguiente:

abstract class Coche {
int precioMedioCoche();
}

class Renault extends Coche {
@override
int precioMedioCoche() {
  return 18000;
}
}

class Audi extends Coche {
@override
int precioMedioCoche() {
  return 25000;
}
}

class Mercedes extends Coche {
@override
int precioMedioCoche() {
  return 27000;
}
}

main() {
Renault renault = Renault();
Audi audi = Audi();
Mercedes mercedes = Mercedes();
List<int> listaDeCoche = [
  renault.precioMedioCoche(),
  audi.precioMedioCoche(),
  mercedes.precioMedioCoche()
];
imprimirPrecioCoche(listaDeCoche);
}

void imprimirPrecioCoche(List coches) {
for (var element in coches) {
  print(element);
}
}

image



Cada coche extiende la clase abstracta Coche e implementa el método abstracto precioMedioCoche().

Así, cada coche tiene su propia implementación del método precioMedioCoche(), por lo que el método imprimirPrecioMedioCoche() itera el array de coches y solo llama al método precioMedioCoche().

Ahora, si añadimos un nuevo coche, precioMedioCoche() no tendrá que ser modificado. Solo tendremos que añadir el nuevo coche al array, cumpliendo así el principio abierto/cerrado.

Principio de Substitución de Liskov

Declara que una subclase debe ser sustituible por su superclase, y si al hacer esto, el programa falla, estaremos violando este principio.
Cumpliendo con este principio se confirmará que nuestro programa tiene una jerarquía de clases fácil de entender y un código reutilizable.
Veamos un ejemplo:

class Coche {
String marca ;

Coche(String marca ) {
  this.marca;
  
}

String getMarcaCoche() {
  return marca;
}
}


Coche coche1 = Coche("");
Coche coche2 = Coche("");
void main(){
List<String>  listaDeCoches = [coche1.marca="Audi", coche2.marca="Renault"];

imprimirNumeroAsientos(listaDeCoches);
}

void imprimirNumeroAsientos(List coches) {

  if(coche1.marca == "Audi"){
    print("el auto ${coche1.marca} tiene 2 asientos"); 
}
 if(coche2.marca == "Renault"){
    print("el auto ${coche2.marca} tiene 4 asientos");
  
}
}

image

Esto viola tanto el principio de substitución de Liskov como el de abierto/cerrado. El programa debe conocer cada tipo de Coche y llamar a su método numAsientos() asociado.

Así, si añadimos un nuevo coche, el método debe modificarse para aceptarlo.

class Coche {
  String marca;

  Coche(String marca) {
    this.marca;
  }

  String getMarcaCoche() {
    return marca;
  }
}

Coche coche1 = Coche("");
Coche coche2 = Coche("");
Coche coche3 = Coche("");

void main() {
  List<String> listaDeCoches = [
    coche1.marca = "Audi",
    coche2.marca = "Renault",
    coche3.marca = "Toyota TXL"
  ];

  imprimirNumeroAsientos(listaDeCoches);
}

void imprimirNumeroAsientos(List coches) {
  if (coche1.marca == "Audi") {
    print("el auto ${coche1.marca} tiene 2 asientos");
  }
  if (coche2.marca == "Renault") {
    print("el auto ${coche2.marca} tiene 4 asientos");
  }
  if (coche3.marca == "Toyota TXL") {
    print("el auto ${coche3.marca} tiene 6 asientos");
  }
}

image

Para que este método cumpla con el principio, seguiremos estos principios:

-Si la superclase (Coche) tiene un método que acepta un parámetro del tipo de la superclase (Coche), entonces su subclase (Renault) debería aceptar como argumento un tipo de la superclase (Coche) o un tipo de la subclase (Renault).

-Si la superclase devuelve un tipo de ella misma (Coche), entonces su subclase (Renault) debería devolver un tipo de la superclase (Coche) o un tipo de la subclase (Renault).

Si volvemos a implementar el método anterior:

main() {
  
  Renault renault = Renault();

  List<int> arrayCoches = [renault.numAsientos()];

  imprimirNumeroAsientos(arrayCoches);
}

void imprimirNumeroAsientos(List coches) {
  for (var element in coches) {
    print("El carro  tiene $element asientos");
  }
}

Ahora al método no le importa el tipo de la clase, simplemente llama al método numAsientos() de la superclase. Solo sabe que el parámetro es de tipo coche, ya sea Coche o alguna de las subclases.

Para esto, ahora la clase Coche debe definir el nuevo método:

abstract class Coche {
  int numAsientos();
}

Y las subclases deben implementar dicho método:

class Renault extends Coche {
  @override
  int numAsientos() {
    return 4;
  }
}

Como podemos ver, ahora el método imprimirNumAsientos() no necesita saber con qué tipo de coche va a realizar su lógica, simplemente llama al método numAsientos() del tipo Coche, ya que por contrato, una subclase de Coche debe implementar dicho método.



Principio de segregación de interfaz



Este principio establece que los clientes no deberían verse forzados a depender de interfaces que no usan.

Dicho de otra manera, cuando un cliente depende de una clase que implementa una interfaz cuya funcionalidad este cliente no usa, pero que otros clientes sí usan, este cliente estará siendo afectado por los cambios que fuercen otros clientes en dicha interfaz.

Imaginemos que queremos definir las clases necesarias para albergar algunos tipos de aves. Por ejemplo, tendríamos loros, tucanes y halcones:

abstract class IAve {
  void volar();
  void comer();
}

class Loro implements IAve {
  @override
  void volar() {
    return print("EL loro puede volar");
  }

  @override
  void comer() {
    return print("EL loro puede comer");
  }
}

class Tucan implements IAve {
  @override
  void volar() {
    return print("EL Tucan puede Volar");
  }

  @override
  void comer() {
    return print("EL Tucan puede comer");
  }
}

void main() {
  Loro loro = Loro();
  loro.volar();

  Tucan tucan = Tucan();
  tucan.comer();
}

image

Hasta aquí todo bien. Pero ahora imaginemos que queremos añadir a los pingüinos. Estos son aves, pero además tienen la habilidad de nadar. Podríamos hacer esto:



abstract class IAve {
void volar();
void comer();
void nadar();
}

class Loro implements IAve {
@override
void volar() {
  return print("EL loro puede volar");
}

@override
void comer() {
  return print("EL loro puede comer");
}

@override
void nadar() {
  return print("");
}
}

class Tucan implements IAve {
@override
void volar() {
  return print("EL Tucan puede Volar");
}

@override
void comer() {
  return print("EL Tucan puede comer");
}

void nadar() {
  return print("");
}
}

void main() {
Loro loro = Loro();
loro.volar();

Tucan tucan = Tucan();
tucan.comer();
}

image


El problema es que el loro no nada, y el pingüino no vuela, por lo que tendríamos que añadir una excepción o aviso si se intenta llamar a estos métodos. Además, si quisiéramos añadir otro método a la interfaz IAve, tendríamos que recorrer cada una de las clases que la implementa e ir añadiendo la implementación de dicho método en todas ellas. Esto viola el principio de segregación de interfaz, ya que estas clases (los clientes) no tienen por qué depender de métodos que no usan.

Lo más correcto sería segregar más las interfaces, tanto como sea necesario. En este caso podríamos hacer lo siguiente:



  abstract class IAve {  
  void comer();
}
abstract class IAveVoladora {  
  void volar();
}

abstract class IAveNadadora {  
  void nadar();
}

class Loro implements IAve, IAveVoladora{

  @override
   void volar() {
      return print("el Loro puede volar");
  }

  @override
   void comer() {
      return print("el Loro puede Comer");
  }
}

class Pinguino implements IAve, IAveNadadora{

  @override
   void nadar() {
       return print("el Pinguino puede Nadar");
  }

  @override
   void comer() {
       return print("el Pinguino puede comer");
  }
}

main(){
Loro loro = Loro();
loro.volar();

Pinguino pinguino = Pinguino(); 
pinguino.nadar();
}

image

Así, cada clase implementa las interfaces de la que realmente necesita implementar sus métodos. A la hora de añadir nuevas funcionalidades, esto nos ahorrará bastante tiempo, y además, cumplimos con el primer principio (Responsabilidad Única).

D: Principio de inversión de dependencias

Establece que las dependencias deben estar en las abstracciones, no en las concreciones. Es decir:

-Los módulos de alto nivel no deberían depender de módulos de bajo nivel. Ambos deberían depender de abstracciones.

-Las abstracciones no deberían depender de detalles. Los detalles deberían depender de abstracciones.

En algún momento nuestro programa o aplicación llegará a estar formado por muchos módulos. Cuando esto pase, es cuando debemos usar inyección de dependencias, lo que nos permitirá controlar las funcionalidades desde un sitio concreto en vez de tenerlas esparcidas por todo el programa. Además, este aislamiento nos permitirá realizar testing mucho más fácilmente.

Supongamos que tenemos una clase para realizar el acceso a datos, y lo hacemos a través de una BBDD:

class DatabaseService{  
    //...
    void getDatos() {}
}

class AccesoADatos {

     DatabaseService  databaseService = DatabaseService();

     AccesoADatos(DatabaseService databaseService){
        this.databaseService = databaseService;
    }

    Dato getDatos(){
        databaseService.getDatos();
        //...
    }
}


class Dato{
}

Imaginemos que en el futuro queremos cambiar el servicio de BBDD por un servicio que conecta con una API. Para un minuto a pensar qué habría que hacer... ¿Ves el problema? Tendríamos que ir modificando todas las instancias de la clase AccesoADatos, una por una.

Esto es debido a que nuestro módulo de alto nivel (AccesoADatos) depende de un módulo de más bajo nivel (DatabaseService), violando así el principio de inversión de dependencias. El módulo de alto nivel debería depender de abstracciones.

Para arreglar esto, podemos hacer que el módulo AccesoADatos dependa de una abstracción más genérica:

abstract class Conexion {  
    Dato getDatos();
    void setDatos();
}

class AccesoADatos {

     Conexion conexion;

     AccesoADatos(Conexion conexion){
        this.conexion = conexion;
    }

     getDatos(){
        conexion.getDatos();
    }
}

class Dato{}

Así, sin importar el tipo de conexión que se le pase al módulo AccesoADatos, ni este ni sus instancias tendrán que cambiar, por lo que nos ahorraremos mucho trabajo.

Ahora, cada servicio que queramos pasar a AccesoADatos deberá implementar la interfaz Conexion:

      
abstract class Conexion {  
    Dato getDatos();
    void setDatos();
}

class DatabaseService implements Conexion {

    @override
     Dato getDatos() { }

    @override
     void setDatos() { }
}

class APIService implements Conexion{

    @override
     Dato getDatos() { }

    @override
     void setDatos() { }
}
      
      


class AccesoADatos {

     Conexion conexion;

     AccesoADatos(Conexion conexion){
        this.conexion = conexion;
    }

     getDatos(){
        conexion.getDatos();
    }
}

class Dato{}



Así, tanto el módulo de alto nivel como el de bajo nivel dependen de abstracciones, por lo que cumplimos el principio de inversión de dependencias. Además, esto nos forzará a cumplir el principio de Liskov, ya que los tipos derivados de Conexion (DatabaseService y APIService) son sustituibles por su abstracción (interfaz Conexion).


Al tener nuestro software con los principios SOLID, nos ayudara a manejar un código limpio y mucho más dinámico de mantener, al tener estos principios nunca dará miedo en cuanto al crecimiento del software porque siempre será fácil de mantener y entender, entonces los principios SOLID siempre será la mejor opción para que el desarrollador tengo su código limpio y asi otros desarrolladores les sea más fácil comprenderlo. Sin embargo, al no usar estos principios, todo será mas duro, porque si no los utilizamos será complejo poder resolver un error, ya que todo está unido, será código spaghetti, al ver un crecimiento del software será muy probable, de que siempre habrá errores, entonces esto retardaría el tiempo de salida del programa, será mucho mas complejo, para un programador nuevo, llevar y ver ese código asi sin tener ningún principio.

Los principios SOLID contribuirá a tener un código limpio y mucho más fácil de mantener. Los principios SOLID aportan:

  • Flexibilidad en el desarrollo: el desacoplamiento y cohesión entre clases les da independencia a las mismas lo que nos permite trabajar de maneras más cómoda.
  • Software mantenible y escalable: gracias a que la aplicación de estos principios el código es más entendible tanto para el desarrollador como para quienes trabajaran posteriormente sobre el código, sea para su mantenimiento o nuevas implementaciones.
  • Claridad en la arquitectura: al tener una estructura muy bien diseñada y entendible la arquitectura será más clara.
  • Aplicación más sencilla de test: al tener un código desacoplado y una arquitectura muy clara, será fácil la realización de los test

Carlos Macías Martín . (03 April 2019). Principios SOLID. 2021, noviembre 12, de enmilocalfunciona Recuperado de https://enmilocalfunciona.io/principios-solid/

You might also like...

Todo Flutter application with sqflite as a local database and bloc state management.

Todo Flutter application with sqflite as a local database and bloc state management.

Todo App A Flutter application developed to add todo tasks and handles it I used Sqflite as a local database to store all the tasks I used flutter_sli

Oct 17, 2022

Flutter ToDo application using Clean Code architecture

Flutter ToDo application using Clean Code architecture

DoneIt 📝 DoneIt is a sample note app 📝 Flutter application 📱 built to demonstrate use of Clean Architecture tools. Dedicated to all Flutter Develop

Dec 27, 2022

This is a todo app for managing your tasks and life. Built with Flutter

todo_app A new Flutter project. Getting Started This project is a starting point for a Flutter application. A few resources to get you started if this

Oct 30, 2021

An elegant todo app with some advanced features.

An elegant todo app with some advanced features.

💙 An Elegant "Todopad" Made With Flutter 💙 🌟 Star 🌟 the repo if you like it This is a todo app with local backup and restore functionality made wi

Jan 1, 2023

An open source task manager (todo list) app, developed using Dart language and Flutter framework.

An open source task manager (todo list) app, developed using Dart language and Flutter framework.

Tasker An open source task manager (todo list) app, developed using Dart language and Flutter framework. Screenrecords     Screenshots                

Dec 29, 2022

Todo App with Flutter + CleanArchitecture + sqflite + riverpod + state_norifier + freezed!

Todo App with Flutter + CleanArchitecture + sqflite + riverpod + state_norifier + freezed!

CleanArchitectureTodoAppTrainingWithFlutter Flutter + CleanArchitecture + sqflite + riverpod + state_notifier + freezed! Motivation I wanted to practi

Dec 16, 2022

A simple todo app for keeping track of complete and incomplete tasks

bloc_todo_list A simple todo app built using bloc architecture and state management Getting Started This project is a starting point for a Flutter app

Nov 29, 2021

Todo app created by Flutter.

todo_app A new Flutter project. Getting Started This project is a starting point for a Flutter application. A few resources to get you started if this

Dec 3, 2021

Sytôdy, a Flutter "speech to text" todo app POC

 Sytôdy, a Flutter

Sytôdy, a Flutter "speech to text" todo app POC ⚠️ iOS10(Swift) & Android Usage Install flutter cd sytody flutter run 📺 Video demo How it works TL;DR

Jan 19, 2022
Owner
Luis Eduardo Salcedo Aya
I am a developer, with knowledge in HTML, CSS, JS, Dart and Flutter
Luis Eduardo Salcedo Aya
Todo-App - Flutter Todo App using Bloc, Sqflite, and shared preferences

Todo App A new Flutter application. Getting Started Flutter application using Bl

Haitham Ahmed 11 Nov 8, 2022
Todo app - an application maintaining user's todo list. Made to learn state management

todo_app A new Flutter project. Getting Started This project is a starting point for a Flutter application. A few resources to get you started if this

Lokesh Ghule 1 Mar 15, 2022
Neo ToDo - Beautiful neumorphism style todo list

Neo ToDo - Beautiful neumorphism style todo list • Neomorphism style. • Categories of tasks. • Beautiful animations. • Dark theme. • Font Awesome icon

Alexey Z 87 Apr 30, 2022
App que faz perguntas e devolve uma pontuação

This is a simple app that asks questions and keeps track of points It was made in flutter and dart, it has a pretty simple UI and it's porpose was to

Murilo Benassi 1 Oct 16, 2021
A Simple Todo app design in Flutter to keep track of your task on daily basis. Its build on BLoC Pattern. You can add a project, labels, and due-date to your task also you can sort your task on the basis of project, label, and dates

WhatTodo Life can feel overwhelming. But it doesn’t have to. A Simple To-do app design in flutter to keep track of your task on daily basis. You can a

Burhanuddin Rashid 1k Jan 6, 2023
Flutter ToDo App with Firebase

Taskist Taskist is a ToDo List app for Task Management inspired by the design below The app is using Firebase, you have to configure it from your side

Hugo EXTRAT 817 Jan 6, 2023
Minimalist Flutter Todo App, built using BLoC pattern

Deer Minimalist Todo Planner app built around the idea of efficiency and clean aesthetic. Showcase Development Deer uses BLoC (Business Logic Componen

Aleksander Woźniak 355 Dec 24, 2022
Flutter basic desktop project. Desktop todo app.

Glory Todo Desktop Basic and Primitive Flutter Desktop Project! Goal My goal is to accept my inexperience without worrying about the plugin shortcomin

Özgür 52 Dec 3, 2022
Create TODO LIST with Get Storage !

todo-list-get-storage Create TODO LIST with Get Storage ! dependencies: flutter: sdk: flutter # The following adds the Cupertino Icons font

Kauê Murakami 11 Aug 24, 2022
My notes, todo list, and memories.

My notes, todo list, and memories.

Minh Tran 18 Dec 14, 2022