paint-brush
Principios SOLID en Java: una guía para principiantespor@ps06756
14,604 lecturas
14,604 lecturas

Principios SOLID en Java: una guía para principiantes

por Pratik Singhal14m2024/03/22
Read on Terminal Reader

Demasiado Largo; Para Leer

Los Principios SOLID son los principios de la programación orientada a objetos esenciales para desarrollar software escalable. Los principios son: S: Principio de responsabilidad única O: Principio abierto/cerrado L: Principio de sustitución de Liskov I: Principio de segregación de interfaz D: Principio de inversión de dependencia
featured image - Principios SOLID en Java: una guía para principiantes
Pratik Singhal HackerNoon profile picture
0-item

Los principios de diseño SÓLIDOS son los principios de diseño más importantes que necesita conocer para escribir código limpio. Tener un dominio sólido de los principios SOLID es una habilidad indispensable para cualquier programador. Son la base sobre la que se desarrollan otros patrones de diseño.


En este artículo, abordaremos los principios de diseño SOLID utilizando algunos ejemplos de la vida real y comprenderemos su importancia.


Junto con el polimorfismo, la abstracción y la herencia, los principios SOLID son realmente importantes para ser bueno en la programación orientada a objetivos.

¿Por qué son importantes los principios SOLID?

Los principios SÓLIDOS son importantes por múltiples razones:


  • Los principios SOLID nos permiten escribir código limpio y fácil de mantener: cuando comenzamos un nuevo proyecto, inicialmente la calidad del código es buena porque tenemos un conjunto limitado de características que implementamos. Sin embargo, a medida que incorporamos más funciones, el código comienza a volverse confuso.


  • Los principios SOLID se basan en los fundamentos de la abstracción, el polimorfismo y la herencia y conducen a patrones de diseño para casos de uso comunes. Comprender estos patrones de diseño ayuda a implementar casos de uso comunes en la programación.


  • Los principios SOLID nos ayudan a escribir código limpio que mejora la capacidad de prueba del código. Esto se debe a que el código es modular y está débilmente acoplado. Cada módulo se puede desarrollar y probar de forma independiente.


Exploremos ahora cada uno de los principios SOLID en detalle con ejemplos del mundo real.

1. Principio de responsabilidad única

S en principios SOLID significa Principio de Responsabilidad Única. El principio de responsabilidad única establece que una clase debe tener una sola razón para cambiar. Esto limita la cantidad de lugares que necesitamos para realizar cambios al incorporar requisitos adicionales en nuestro proyecto.

Cada clase debe tener exactamente una razón para cambiar.

Por ejemplo, digamos que estamos diseñando una aplicación bancaria en Java donde tenemos una clase SavingsAccount que permite operaciones básicas como débito, crédito y sendUpdates . El método sendUpdate toma una enumeración llamada NotificationMedium (como correo electrónico, SMS, etc.) y envía la actualización con el medio apropiado. Escribiremos el código para eso como se muestra a continuación.


 public class SavingsAccount { public int balance; public String name; public SavingsAccount(int initialBalance, String name) { this.balance = initialBalance; this.name = name; System.out.println("Created a savings account with balance = " + initialBalance); } public void debit(int amountToDebit) { // debit business logic } public void credit(int amountToCredit) { // credit business logic } public void sendNotification(NotificationMedium medium) { if (medium == NotificationMedium.SMS) { // Send SMS here } else if (medium == NotificationMedium.EMAIL) { // Send Email here } } }


 public enum NotificationMedium { SMS, EMAIL }


Ahora, si observa la clase SavingsAccount anterior, puede cambiar debido a múltiples razones:


  1. Si hay algún cambio en la lógica central de la clase SavingsAccount (como debit , credit , etc.).


  2. Si el banco decide introducir un nuevo medio de Notificación (digamos WhatsApp).


Esto es una violación del principio de responsabilidad única de los principios SOLID. Para solucionarlo, crearemos una clase separada que envíe la notificación.


Refactoricemos el código anterior de acuerdo con los principios SOLID.


 public class SavingsAccount { public int balance; public String name; public SavingsAccount(int initialBalance, String name) { this.balance = initialBalance; this.name = name; System.out.println("Created a savings account with balance = " + initialBalance); } public void debit(int amountToDebit) { // debit business logic } public void credit(int amountToCredit) { // credit business logic } public void printBalance() { System.out.println("Name: " + name+ " Account Balance: " + balance); } public void sendNotification(Medium medium) { Sender.sendNotification(medium, this); } }


 public enum NotificationMedium { SMS, EMAIL }


 public class Sender { public static void sendNotification(NotificationMedium medium, SavingsAccount account) { // extract account data from the account object if (medium == NotificationMedium.SMS) { //logic to send SMS here } else if (medium == NotificationMedium.EMAIL) { // logic to send Email here } } }


Ahora, dado que hemos refactorizado el código, si hay algún cambio en NotificationMedium o en el formato, cambiaremos la clase Sender . Sin embargo, si hay un cambio en la lógica central de SavingsAccount , habrá cambios en la clase SavingsAccount .


Esto soluciona la infracción que observamos en el primer ejemplo.

2. Principio de apertura/cierre

El principio Abrir Cerrar establece que debemos diseñar las clases de tal manera que estén abiertas a la extensión (en caso de agregar características adicionales) pero cerradas a la modificación. Estar cerrado por modificaciones nos ayuda de dos maneras:


  • Muchas veces, es posible que la fuente de la clase original ni siquiera esté disponible. Podría ser una dependencia consumida por su proyecto.


  • Mantener la clase original sin cambios reduce las posibilidades de que se produzcan errores. Ya que puede haber otras clases que dependan de la clase que queremos modificar.


Para ver un ejemplo del principio Abrir Cerrar, echemos un vistazo al ejemplo de un carrito de compras (como el implementado en sitios web de comercio electrónico).


Voy a crear una clase llamada Cart que contendrá una lista de Item que puede agregarle. Dependiendo del tipo de artículo y de los impuestos que le corresponden, queremos crear un método que calcule el valor total del carrito dentro de la clase Cart .


Las clases deben estar abiertas para extensión y cerradas para modificación.


 import java.util.ArrayList; import java.util.List; public class Cart { private List<Item> items; public Cart() { this.items = new ArrayList<>(); } public void addToCart(Item item) { items.add(item); } public double calculateCartValue() { double value = 0.0; for(Item item: items) { if (item.getItemType() == GIFT) { // 8% tax on gift + 2% gift wrap cost value += (item.getValue()*1.08) + item.getValue()*0.02 } else if (item.getItemType() == ItemType.ELECTRONIC_ITEM) { value += (item.getValue()*1.11); } else { value += item.getValue()*1.10; } } return value; } }


 @Getter @Setter public abstract class Item { protected double price; private ItemType itemType; public double getValue() { return price; } }
 public enum ItemType { ELECTRONIC, GIFT }


En el ejemplo anterior, el método calculateCartValue calcula el valor del carrito iterando sobre todos los artículos dentro del carrito e invocando la lógica basada en el tipo de artículo.


Aunque este código parece correcto, viola los principios SOLID.


Digamos que necesitamos agregar una nueva regla para un tipo diferente de artículo (por ejemplo, comestibles) mientras calculamos el valor del carrito. En ese caso, tendríamos que modificar la clase original Cart y escribir otra condición else if dentro de ella que verifique artículos del tipo Grocery .


Sin embargo, con poca refactorización, podemos hacer que el código se adhiera al principio de apertura/cierre. Veamos cómo.


Primero, haremos que la clase Item sea abstracta y crearemos clases concretas para diferentes tipos de Item (s) como se muestra a continuación.


 public abstract class Item { protected double price; public double getValue() { return price; } }


 public class ElectronicItem extends Item { public ElectronicItem(double price) { super.price = price; } @Override public double getValue() { return super.getValue()*1.11; } }


 public class GiftItem extends Item { public GiftItem(double price) { super.price = price; } @Override public double getValue() { return super.getValue()*1.08 + super.getValue()*0.02; } }


 public class GroceryItem extends Item { public GroceryItem(double price) { super.price = price; } @Override public double getValue() { return super.getValue()*1.03; } }


Dentro de cada clase de artículo concreta como GroceryItem , GiftItem y ElectronicItem implementan el método getValue() que contiene la lógica empresarial para el cálculo del valor y los impuestos.


Ahora, haremos que la clase Cart dependa de la clase abstracta Item e invocaremos el método getValue() para cada artículo como se muestra a continuación.


 import java.util.ArrayList; import java.util.List; public class Cart { private List<Item> items; public Cart(Payment paymentOption) { this.items = new ArrayList<>(); } public void addToCart(Item item) { items.add(item); } public double calculateCartValue() { double value = 0.0; for(Item item: items) { value += item.getValue(); } return value; } }


Ahora, en este código refactorizado, incluso si se introducen nuevos tipos de Item , la clase Cart permanece sin cambios. Debido al polimorfismo, cualquiera que sea el tipo real de Item dentro de los items ArrayList , se invocaría el método getValue() de esa clase.

3. Principio de sustitución de Liskov

El principio de sustitución de Liskov establece que en un código dado, incluso si reemplazamos el objeto de una superclase con un objeto de la clase hija, el código no debería romperse. En otras palabras, cuando una subclase hereda una superclase y anula sus métodos, debe mantener la coherencia con el comportamiento del método de la superclase.


Por ejemplo, si hacemos las siguientes clases Vehicle y dos clases Car y Bicycle . Ahora, digamos que creamos un método llamado startEngine() dentro de la clase Vehículo, se puede anular en la clase Car , pero no será compatible con la clase Bicycle ya que Bicycle no tiene motor (consulte el ejemplo de código a continuación)


La subclase debe mantener coherencia con el comportamiento de la superclase al anular métodos.

 class Vehicle { public void startEngine() { // start engine of the vehicle } } class Car extends Vehicle { @Override public void startEngine() { // Start Engine } } class Bicycle extends Vehicle { @Override public void startEngine(){ throw new UnsupportedOperationException("Bicycle doesn't have engine"); } }


Ahora, digamos que hay algún código que espera un objeto de tipo vehículo y se basa en el método startEngine() . Si, al llamar a ese fragmento de código en lugar de pasar un objeto de tipo Vehicle , pasamos un objeto Bicycle , se producirían problemas en el código. Dado que el método de la clase Bicycle (s) generará una excepción cuando se llame al método startEngine() . Esto sería una violación de los Principios SOLID (principio de sustitución de Liskov)


Para resolver este problema, podemos crear dos clases MotorizedVehicle y NonMotorizedVehicle y hacer que Car herede de la clase MotorizedVehicle y que Bicycle herede de NonMotorizedVehicle


 class Vehicle { } class MotorizedVehicle extends Vehicle { public void startEngine() { // start engine here } } class Car extends MotorizedVehicle { @Override public void startEngine() { // Start Engine } } class NonMotorizedVehicle extends Vehicle { public void startRiding() { // Start without engine } } class Bicycle extends NonMotorizedVehicle { @Override public void startRiding(){ // Start riding without the engine. } }


4. Principio de segregación de interfaz

La "I" en los Principios SOLID significa el Principio de Segregación de Interfaz.


El principio de segregación de interfaces establece que en lugar de tener interfaces más grandes que obliguen a las clases de implementación a implementar métodos no utilizados, deberíamos tener interfaces más pequeñas e implementar clases. De esta manera, las clases sólo implementan los métodos relevantes y permanecen limpias.

Divida sus interfaces en varias interfaces más pequeñas en lugar de una interfaz grande.

Por ejemplo, veamos el marco de Colecciones integrado en Java. Entre otras estructuras de datos, Java también proporciona estructuras de datos LinkedList y ArrayList .


La clase ArrayList implementa las siguientes interfaces: Serializable , Cloneable , Iterable , Collection , List y RandomAccess .

La clase LinkedList implementa Serializable , Cloneable , Iterable , Collection , Deque , List y Queue .


¡Son bastantes interfaces!


En lugar de tener tantas interfaces, los desarrolladores de Java podrían haber combinado Serializable , Cloneable , Iterable , Collecton , List y RandomAccess en una sola interfaz, digamos la interfaz IList . Ahora, tanto las clases ArrayList como LinkedList podrían haber implementado esta nueva interfaz IList .


Sin embargo, dado que LinkedList no admite el acceso aleatorio, podría haber implementado los métodos en la interfaz RandomAccess y podría haber generado UnsupportedOperationException cuando alguien intenta llamarlo.


Sin embargo, eso sería una violación del principio de segregación de interfaz en los principios SOLID, ya que "forzaría" a la clase LinkedList a implementar los métodos dentro de la interfaz RandomAccess aunque no sea necesario.


Por lo tanto, es mejor dividir la interfaz según el comportamiento común y dejar que cada clase implemente muchas interfaces en lugar de una interfaz grande.

Principio de inversión de dependencia

El principio de inversión de dependencia establece que las clases del nivel superior no deberían depender directamente de las clases del nivel inferior. Esto provoca un estrecho acoplamiento entre los dos niveles.


En lugar de eso, las clases inferiores deberían proporcionar una interfaz de la que deberían depender las clases de nivel superior.


Depender de interfaces en lugar de clases


Por ejemplo, continuemos con el ejemplo Cart que vimos arriba y mejorémoslo para agregar algunas opciones de pago. Supongamos que tenemos dos tipos de opciones de pago con nosotros DebitCard y Paypal . Ahora, en la clase Cart , queremos agregar un método para placeOrder que calcularía el valor del carrito e iniciaría el pago en función del pago proporcionado. método.


Para hacer esto, podríamos haber agregado dependencia en el ejemplo anterior Cart agregando las dos opciones de pago como campos dentro de la clase Cart . Sin embargo, eso uniría estrechamente la clase Cart con la clase DebitCard y Paypal .


En lugar de eso, crearíamos una interfaz Payment y haríamos que las clases DebitCard y Paypal implementaran las interfaces Payment . Ahora, la clase Cart dependerá de la interfaz Payment y no de los tipos de pago individuales. Esto mantiene las clases poco acopladas.


Vea el código a continuación.


 public interface Payment { void doPayment(double amount); } public class PaypalPayment implements Payment { @Override public void doPayment(double amount) { // logic to initiate paypal payment } } public class DebitCardPayment implements Payment { @Override public void doPayment(double amount) { // logic to initiate payment via debit card } } import java.util.ArrayList; import java.util.List; public class Cart { private List<Item> items; private Payment paymentOption; public Cart(Payment paymentOption) { this.items = new ArrayList<>(); this.paymentOption = paymentOption; } public void addToCart(Item item) { items.add(item); } public double calculateCartValue() { double value = 0.0; for(Item item: items) { value += item.getValue(); } return value; } public void placeOrder() { this.paymentOption.doPayment(calculateCartValue()); } }


Si está interesado en aprender más sobre programación orientada a objetos, patrones de diseño GoF y entrevistas de diseño de bajo nivel, consulte mi curso altamente calificado Programación orientada a objetos + Patrones de diseño Java.