Principios SOLID
¿Qué es SOLID y por qué estos principios son importantes?
Son muy importante en la Programación Orientada a Objetos, ya que estos principios están destinados a que el código que generemos sea entendible, flexible y mantenible con el tiempo y conforme el proyecto siga creciendo, contribuyendo al desarrollo ágil y adaptativo.
Evita code smells, deuda técnica y código spaghetti.
S (Single Responsibility)
Este principio indica que una clase solo debe y puede tener un único motivo para cambiar. Indicando que debe encargarse de una única responsabilidad / proceso / trabajo.
❌ Este código no cumple con el principio de Responsabilidad Única
- Si necesitamos cambiar la forma de calcular una amortización debes modificar una clase que no debería estar encargada de ese cálculo.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Service
@AllArgsConstructor
public class QuotaServiceImpl implements QuotaService {
private final QuotaRepository quotaRepository;
@Override
public void create(final QuotaFilterRequest request) {
final QuotaEntity quotaEntity = new QuotaEntity();
quotaEntity.setTotal(this.calculateAmortization(request.getMonths(), request.getTotal()));
this.quotaRepository.save(quotaEntity);
}
private BigDecimal calculateAmortization(final int months, final BigDecimal total) {
return (total.multiply(new BigDecimal("0.15")))
.divide(new BigDecimal(months), 2, RoundingMode.HALF_UP);
}
}
✅ Si indicamos en una clase aparte el cómo se calcula una amortización, solo debemos modificar esa clase en específico
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Service
@AllArgsConstructor
public class QuotaCalculatorServiceImpl implements QuotaCalculatorService {
public BigDecimal calculateAmortization(final int months, final BigDecimal total) {
return (total.multiply(new BigDecimal("0.15")))
.divide(new BigDecimal(months), 2, RoundingMode.HALF_UP);
}
}
@Service
@AllArgsConstructor
public class QuotaServiceImpl implements QuotaService {
private final QuotaRepository quotaRepository;
private final QuotaCalculatorService quotaCalculatorService;
@Override
public void create(final QuotaFilterRequest request) {
final QuotaEntity quotaEntity = new QuotaEntity();
quotaEntity.setTotal(this.quotaCalculatorService.calculateAmortization(request.getMonths(), request.getTotal()));
this.quotaRepository.save(quotaEntity);
}
}
Si vemos que una clase / método se está encargando de varios procesos / responsabilidades que no cumplen con el propósito inicial, es momento de separar esa lógica.
O (Open/Closed Principle)
Este principio indica que una clase debe estar disponible para extenderse y cerrada para modificarse
❌ Este código no cumple con el principio de Abierto/Cerrado
- Si necesitamos agregar un nuevo tipo de cuota necesitaríamos modificar código legacy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Service
@AllArgsConstructor
public class QuotaServiceImpl implements QuotaService {
private final QuotaRepository quotaRepository;
private final QuotaCalculatorService quotaCalculatorService;
@Override
public void create(final QuotaFilterRequest request) {
final QuotaEntity quotaEntity = new QuotaEntity();
quotaEntity.setTotal(this.quotaCalculatorService.calculateAmortization(request.getMonths(), request.getTotal()));
this.quotaRepository.save(quotaEntity);
}
}
@Service
@AllArgsConstructor
public class QuotaCalculatorServiceImpl implements QuotaCalculatorService {
public BigDecimal calculateAmortization(final QuotaType quotaType, final int months, final BigDecimal total) {
return switch (quotaType) {
case TECHNOLOGY -> (total.multiply(new BigDecimal("0.15"))).divide(new BigDecimal(months), 2, RoundingMode.HALF_UP);
case HOME -> (total.multiply(new BigDecimal("0.8"))).divide(new BigDecimal(months - 1), 2, RoundingMode.HALF_UP);
case COUPLE -> (total).divide(new BigDecimal(months), 2, RoundingMode.HALF_UP);
};
}
}
✅ En su lugar, podemos crear una interface del que cada tipo de cuota puede extender, con esto, evitando modificar código legacy.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface QuotaCalculator {
BigDecimal calcAmortization(BigDecimal total, int months);
}
public class TechnologyQuotaCalculator implements QuotaCalculator {
@Override
public BigDecimal calcAmortization(BigDecimal total, int months) {
return (total.multiply(new BigDecimal("0.15"))).divide(new BigDecimal(months), 2, RoundingMode.HALF_UP);
}
}
public class HomeQuotaCalculator implements QuotaCalculator {
@Override
public BigDecimal calcAmortization(BigDecimal total, int months) {
return (total.multiply(new BigDecimal("0.8"))).divide(new BigDecimal(months - 1), 2, RoundingMode.HALF_UP);
}
}
Este ejemplo lo podemos combinar con el Patrón de Diseño de Comportamiento Strategy, el cual nos permitiría detectar de “forma automática” el tipo de cuota y aplicándolo conforme cada caso.
L (Liskov Substitution)
Este principio indica que las clases superiores pueden ser sustituidas por objetos de subclases sin afectar integridad y funcionamiento software. Es decir si S es un subtipo de T, entonces los objetos de T pueden ser sustituidos por objetos de S sin alterar el comportamiento esperado.
❌ Este código no cumple con el principio de Sustitución de Liskov
- Si sustituimos Rectangulo por un objeto Cuadrado obtendremos un resultado no esperado.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class Rectangulo {
protected int ancho;
protected int alto;
public void setAncho(int ancho) {
this.ancho = ancho;
}
public void setAlto(int alto) {
this.alto = alto;
}
public int calcularArea() {
return ancho * alto;
}
}
public class Cuadrado extends Rectangulo {
protected int ancho;
protected int alto;
@Override
public void setAncho(int ancho) {
this.ancho = ancho;
this.alto = ancho;
}
@Override
public void setAlto(int alto) {
this.alto = alto;
this.ancho = alto;
}
@Override
public int calcularArea() {
return ancho * alto;
}
}
final Rectangulo rectangulo = new Cuadrado();
rectangulo.setAlto(10);
rectangulo.setAncho(2);
log.info("Area {}", rectangulo.calcularArea());
El resultado esperado sería 20, sin embargo, el resultado obtenido es 4
✅ Podemos abstraer el método para calcular el área y las dos clases Rectangulo y Cuadrado extiendan
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public abstract class Forma {
abstract int calcularArea();
}
public class Rectangulo extends Forma {
protected int ancho;
protected int alto;
public void setAncho(int ancho) {
this.ancho = ancho;
}
public void setAlto(int alto) {
this.alto = alto;
}
@Override
public int calcularArea() {
return ancho * alto;
}
}
public class Cuadrado extends Forma {
protected int lado;
public void setLado(int lado) {
this.lado = lado;
}
@Override
public int calcularArea() {
return lado * lado;
}
}
final Rectangulo rectangulo = new Rectangulo();
rectangulo.setAlto(10);
rectangulo.setAncho(2);
final Cuadrado cuadrado = new Cuadrado();
cuadrado.setLado(10);
log.info("Area Rectangulo {}", rectangulo.calcularArea());
log.info("Area Cuadrado {}", cuadrado.calcularArea());
Si notamos que una subclase necesita lanzar una excepción
UnsupportedOperationException, ignorar un parámetro o forzar efectos secundarios para “acoplarse” a lo lógica del padre, el principio de Sustitución de Liskov no se cumpliría.
I (Interface Segregation)
Tener varias interfaces específicas es mejor que tener una interfaz general.
❌ Este código no cumple con el principio de Segregación de Interfaces
- Con esta única interfaz obligamos que toda clase que lo implemente fuerce el acoplamiento, sea o no que lo necesite
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
public interface QuotaCalculator {
BigDecimal calcAmortization(BigDecimal total, int months);
BigDecimal calcTaxes(BigDecimal total, int months);
BigDecimal calcInsurance(BigDecimal total, int months);
BigDecimal calcMaintenanceFee(BigDecimal total);
void printAmortizationSchedule(BigDecimal total, int months);
}
@Slf4j
public class HomeQuotaCalculator implements QuotaCalculator {
@Override
public BigDecimal calcAmortization(BigDecimal total, int months) {
return (total.multiply(new BigDecimal("0.8")))
.divide(new BigDecimal(months - 1), 2, RoundingMode.HALF_UP);
}
@Override
public BigDecimal calcTaxes(BigDecimal total, int months) {
throw new UnsupportedOperationException("Home loans don't have taxes.");
}
@Override
public BigDecimal calcInsurance(BigDecimal total, int months) {
return (total.multiply(new BigDecimal("0.02")))
.divide(new BigDecimal(months), 2, RoundingMode.HALF_UP);
}
@Override
public BigDecimal calcMaintenanceFee(BigDecimal total) {
throw new UnsupportedOperationException("Home loans don't have maintenance fee.");
}
@Override
public void printAmortizationSchedule(BigDecimal total, int months) {
log.info("Amortization schedule for home loan...");
}
}
public class TechnologyQuotaCalculator implements QuotaCalculator {
private final int TECH_TAX = 5;
@Override
public BigDecimal calcAmortization(BigDecimal total, int months) {
return (total.multiply(new BigDecimal("0.15")))
.divide(new BigDecimal(months), 2, RoundingMode.HALF_UP);
}
@Override
public BigDecimal calcTaxes(BigDecimal total, int months) {
return total.multiply(new BigDecimal(TECH_TAX));
}
@Override
public BigDecimal calcInsurance(BigDecimal total, int months) {
throw new UnsupportedOperationException("Tech loans don't have insurance.");
}
@Override
public BigDecimal calcMaintenanceFee(BigDecimal total) {
return total.multiply(new BigDecimal("0.01"));
}
@Override
public void printAmortizationSchedule(BigDecimal total, int months) {
throw new UnsupportedOperationException("Tech loans don't print schedules.");
}
}
- En cada implementación de tipo de cuota se lanzan varios
UnsupportedOperationExceptionlo cual indica que no todos necesitan todos los métodos.
✅ La mejor opción es crear varias interfaces específicas, evitando el problema de que todas las clases “implementen forzosamente” métodos que no les corresponden
Si notamos que cada clase implementa métodos que no les competen, este principio no se cumple.
D (Dependency Injection)
Los módulos de alto nivel no deben depender de módulo de bajo nivel, si no, los dos deben depender de abstracciones.
❌ Este código no cumple con el principio de Inyección de Dependencias
- Si en un punto necesitamos cambiar el tipo de cuota debemos modificar el código base.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class LoanService {
private TechnologyQuotaCalculator technologyQuotaCalculator;
LoanService() {
technologyQuotaCalculator = new TechnologyQuotaCalculator();
}
public BigDecimal getMonthlyQuota(BigDecimal total, int months) {
BigDecimal amortization = technologyQuotaCalculator.calcAmortization(total, months);
BigDecimal taxes = technologyQuotaCalculator.calcTaxes(total, months);
return amortization.add(taxes);
}
}
// Si en lugar de "Technology" queremos usar "Home" debemos modificar el servicio base
public class LoanService {
private HomeQuotaCalculator HomeQuotaCalculator;
LoanService() {
HomeQuotaCalculator = new HomeQuotaCalculator();
}
public BigDecimal getMonthlyQuota(BigDecimal total, int months) {
BigDecimal amortization = HomeQuotaCalculator.calcAmortization(total, months);
BigDecimal taxes = HomeQuotaCalculator.calcTaxes(total, months);
return amortization.add(taxes);
}
}
LoanService service = new LoanService();
✅ En lugar de una clase en específico, indicaremos una abstracción
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class LoanService {
private QuotaCalculator quotaCalculator;
public LoanService(QuotaCalculator quotaCalculator) {
this.quotaCalculator = quotaCalculator;
}
public BigDecimal getMonthlyQuota(BigDecimal total, int months) {
BigDecimal amortization = quotaCalculator.calcAmortization(total, months);
BigDecimal taxes = quotaCalculator.calcTaxes(total, months);
return amortization.add(taxes);
}
}
final LoanService loanHomeService = new LoanService(new HomeQuotaCalculator());
final LoanService loanTechService = new LoanService(new TechnologyQuotaCalculator());
- Con esto, cualquier clase/objeto que implemente QuotaCalculator se puede pasar a LoanService sin necesidad de modificar su lógica interna.
