SOLID: Principios de Programación

SOLID: Principios de Programación

Francisco

-
Sígueme en:
linkedin icongithub icon
Publicado el: Jan 29, 2026

Predecir el futuro siempre ha sido casi imposible, más aun en la programación. Actualmente, muchas de las personas que programan o escriben código usan asistente para su trabajo, los cuales pueden incrementar la productividad de manera exponencial en manos expertas, pero en manos inexpertas puede ser lo contrario. Sin embargo, los humanos seguimos siendo los responsables de nuestro trabajo, por lo que debemos validar, cuestionar y testear lo construidos por lo asistentes en base a nuestra experiencia y conocimiento. Hoy en día, más que nunca, tópicos tales como: fundamentos de programación, patrones de diseño, conceptos y la experiencia son excelentes herramientas para cuestionar lo que hacen las maquinas, guiándolas para construir soluciones reales.

Debido a mi formación más practica que teórica en la programación, yo nunca he visto mucho sobre patrones o formas correctas de escribir código, pero en este tiempo que estado estudiando afuera del país, he visto cosas que creo que son interesantes (si eres programador, seguramente te lo pasaron en la universidad), y que pueden ayudar a otros al momento de construir soluciones más robustas y solidas. La programación es una materia bastante estudiada desde los años 1930s, con Alan Tunning como uno de los pioneros de la materia, y como cualquier industria, se han construido marcos teóricos, patrones, frameworks y diferentes lenguajes a través del tiempo.

Python es uno de los lenguajes más popular de los programadores, debido a su alta orientación a objetos y su interpretabilidad, permitiendo que personas de otras áreas del conocimiento (no informáticos) aprendan y trabajen con el. Un lenguaje “object-oriented” es un tipo de lenguaje que se basa en el concepto de objetos, donde las clases son representadas por objetos (sustantivos) y sus acciones son métodos (las cosas que pueden hacer esos objetos). En base a este tipo de lenguaje, se han diseñado cinco tipos de principios, que son:

  • Single-Responsibility Principle (SRP)
  • Open-Closed Principle (OCP)
  • Liskov Substitution Principle (LSP)
  • Interface Segregation Principle (ISP)
  • Dependency Inversion Principle (DIP)

Estos cinco principios representan un letra, y en conjunto forma la palabra SOLID.

En este articulo hablaré un poco sobre estos principios, y la importancia de ellos para construir soluciones de calidad.

Single-Responsibility Principle (SRP)

El primer principio es el  single-responsibility principle (SRP) que fue creado por Robert C. Martin . Su declaración es la siguiente:

A class should have only one reason to change. (Una clase debería tener una sola razón para cambiar.)

Esto significa que una clase debería tener una sola responsabilidad, expresadas en sus métodos. Si una clase tiene más de una tarea, deberíamos separarla en dos dedicadas clases con nombres distintos. Esto suena un poco abstracto, pero imaginemos la siguiente idea, expresada en el código de abajo:

1class MLOService:
2 def __init__(self):
3 self.model = None
4 self.data = None
5
6 def load_data(self):
7 # Code to load data from a file
8 pass
9
10 def train(self):
11 # Code to train the model
12 pass
13
14 def predict(self):
15 # Code to make predictions using the model
16 pass
17

Pensemos que estamos construyendo un pipeline de machine learning, y tenemos una clase que hace todos el proceso, desde la extracción de los datos, entrenamiento y sus predicciones. Esta clase se ve extraña, tiene muchas cosas, y todo esta muy condensado en solo un objeto. Una buena practica seria separar el problema en partes más pequeñas, construyendo distintas clases para cada tarea. Si re factorizamos este código, quedaría de la siguiente manera:

1class DataLoader:
2 def load_data(self):
3 # Code to load data from a file
4 pass
5
6class ModelTrainer:
7 def __init__(self, data):
8 self.data = data
9 self.model = None
10
11 def train(self):
12 # Code to train the model
13 pass
14
15class Predictor:
16 def __init__(self, model):
17 self.model = model
18
19 def predict(self):
20 # Code to make predictions using the model
21 pass

La refactorizacion ha hecho que el código sea mucho más modular, separando tareas por cada clase, y permitiendo que otros cambios se amolde a la estructura de las clases. Estos es una buena practica, ya que divide el problema, y permite dejar cada clase como un objeto separado, preocupándose cada una de ella a lo que mejor saben hacer, más que tener una clase global que unifique todo.

Open-Closed Principle (OCP)

El segundo principio se llama open-closed principle, y fue introducido por Bertrand Meyer  en 1998. Este principio dice lo siguiente:

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification. (Las entidades de software (clases, módulos, funciones, etc.) deberían estar abiertas a la extensión, pero cerradas a la modificación)

No es tan claro al leerlo, pero su esencia es que cualquier clase debe ser escrita en tal manera que sea usada como es. La clase puede ser “extendida” (mejorada), pero no debe ser modificada. En términos simples, tu puedes modificar la clase, pero su modificaciones debe ser en base a agregar código adicional, no cambiar el actual. Un ejemplo de este principio es el siguiente:

1class DataLoader:
2 def __init__(self, loaded_type):
3 if loaded_type == 'SQL':
4 self.loaded_type = loaded_type
5 elif loaded_type == 'API':
6 self.loaded_type = loaded_type
7 elif loaded_type == 'CSV':
8 self.loaded_type = loaded_type
9
10 def load_data(self):
11 if self.loaded_type == 'SQL':
12 return self._load_from_sql()
13 elif self.loaded_type == 'API':
14 return self._load_from_api()
15 elif self.loaded_type == 'CSV':
16 return self._load_from_csv()
17
18
19 def _load_from_sql(self):
20 # Code to load data from SQL database
21 pass
22 def _load_from_api(self):
23 # Code to load data from an API
24 pass
25 def _load_from_csv(self):
26 # Code to load data from a CSV file
27 pass
28
29

El problema del código es que funciona perfectamente para los tipos de datos descritos, pero para cualquier otro tipo de dato, se genera un problema. Si necesitas añadir otra fuente, se deberá ajustar la clase previa, y no solo añadir algo nuevo. Una forma de solucionarlo en la siguiente:

1from abc import ABC, abstractmethod
2
3class DataLoader(ABC):
4 @abstractmethod
5 def load_data(self):
6 pass
7
8
9class SQLDataLoader(DataLoader):
10 def load_data(self):
11 # Code to load data from SQL database
12 pass
13
14class APIDataLoader(DataLoader):
15 def load_data(self):
16 # Code to load data from an API
17 pass
18
19class CSVDataLoader(DataLoader):
20 def load_data(self):
21 # Code to load data from a CSV file
22 pass
23
24# Usage
25def get_data(loader: DataLoader):
26 return loader.load_data()
27
28## SQL
29sql_loader = SQLDataLoader()
30data = get_data(sql_loader)

En este caso el código se modularizo con una clase DataLoader, la cual tiene un método load_data, que no esta explicitamente definido, pero que sirve para que subclases lo usen de la manera que quieran, tal como: SQLDataLoader, APIDataLoader, etc. Si es necesario añadir otra forma de extracción de datos, se agrega otra subclase y no se modifica el código anterior.

Liskov Substitution Principle (LSP)

Este principio fue introducido por Barbara Liskov en la  OOPSLA conferencia de 1987. El principio reclama lo siguiente:

Subtypes must be substitutable for their base types. (Los subtipos deben poder sustituir a sus tipos base.)

Este principio en términos simples demuestra a que si tienes una pieza de código que funciona con una clase, entonces cualquier subclase debería ser capaz de usar los métodos de la clase madre sin romper el código. Esta idea se plantea en el siguiente código:

1
2class DataLoader:
3 def load_data(self):
4 pass
5
6 def transform_data(self, data):
7 pass
8
9class SQLDataLoader(DataLoader):
10 def load_data(self):
11 return [{"id": 1, "raw_value": "100"}]
12
13 def transform_data(self, data):
14 return [{"id": d["id"], "value": int(d["raw_value"])} for d in data]
15
16class PreprocessedLoader(DataLoader):
17 def load_data(self):
18 return [{"id": 1, "value": 100}]
19
20 def transform_data(self, data):
21 raise NotImplementedError("Data ya viene transformada")
22

Si pensamos que PreprocessedLoader es una clase que ya tiene los datos pre-procesados, no tiene sentido que tenga un método que transforme sus datos (transform_data), ya que ellos ya vienen directamente procesados de la fuente. Debido a esto, es mejor construir dos clases de la siguiente manera:

1class DataLoader(ABC):
2 @abstractmethod
3 def load_data(self):
4 pass
5
6 class Transformable(ABC):
7 @abstractmethod
8 def transform_data(self, data):
9 pass
10
11 class SQLDataLoader(DataLoader, Transformable):
12 # carga y transforma
13
14 class PreprocessedLoader(DataLoader):
15 # solo carga, no promete transformar

De esta forma se soluciona el problema, y se logra poder tener una estructura más robusta.

Interface Segregation Principle (ISP)

El principio ISP viene de la misma idea que el single-responsibility principle. Su principal objetivo es:

Clients should not be forced to depend upon methods that they do not use. Interfaces belong to clients, not to hierarchies. (Los clientes no deberían verse obligados a depender de métodos que no utilizan. Las interfaces pertenecen a los clientes, no a las jerarquías)

Un cliente se entiende como una clase o subclase, y un interfase como los métodos y atributos. En simple, si una clase no usa un particular atributo o método, entonces el método debe ser agregado dentro de una clase distinta. Un ejemplo ilustrativo es el siguiente:

1class ModelsHandler:
2 def __init__(self):
3 pass
4
5 def train_model(self, data):
6 # Code to train the model
7 pass
8
9 def get_feature_importance(self):
10 # Code to calculate feature importance
11 pass
12
13class XGBoostHandler(ModelsHandler):
14 def train_model(self, data):
15 # Code to train the XGBoost model
16 pass
17
18 def get_feature_importance(self):
19 # Code to calculate feature importance for XGBoost
20 pass
21
22class LinealRegression(ModelsHandler):
23 def train_model(self, data):
24 # Code to train the LightGBM model
25 pass
26
27 def get_feature_importance(self):
28 # Code to calculate feature importance for LightGBM
29 raise NotImplementedError("Fax functionality not supported")
30

Este código tiene el problema de que ModelHandler contiene un método que no puede ser replicado a todas las subclases, especialmente para el modelo de regresión lineal. La forma de solucionarlo es la siguiente:

1from abc import ABC, abstractmethod
2class ModelsHandler(ABC):
3 @abstractmethod
4 def train_model(self, data):
5 pass
6
7
8class XGBoostHandler(ModelsHandler):
9 def train_model(self, data):
10 # Code to train the XGBoost model
11 pass
12
13 def get_feature_importance(self):
14 # Code to calculate feature importance for XGBoost
15 pass
16
17class LinealRegression(ModelsHandler):
18 def train_model(self, data):
19 # Code to train the LightGBM model
20 pass
21

La nueva clase ModelsHandler solo contiene train_model metodo, dejando cualquier otro metodo para la subclase, no la clase madre. Esta es la forma de solucionar este problema. Si un modelo tiene una especifica función, esa función solo sera declarado a este modelo, no a todos los modelos.

Dependency Inversion Principle (DIP)

Este principio es el siguiente:

Abstractions should not depend upon details. Details should depend upon abstractions. (Las abstracciones no deberían depender de los detalles. Los detalles deberían depender de las abstracciones).

Este principio se puede expresar de mejor manera con el siguiente ejemplo:

1class DataSourceSQL:
2 def load_data(self, data):
3 # Code to load and preprocess data from SQL
4 pass
5
6
7class RouterOptimizer:
8 def __init__(self):
9 self.data_source = DataSourceSQL()
10
11 def optimize(self):
12 # Code to optimize routing based on data
13 pass
14
15 def run(self, data):
16 processed_data = self.data_source.load_data(data)
17 return self.optimize(processed_data)
18

Aqui tenemos un algoritmo de optimizacion de rutas de despacho, que depende de la base de datos para construir la ruta más optima. En este codigo, la clase RouterOptimizer depende de la clase DataSource, pero si agregamos otra fuente de datos, tendriamos que modificar tanto la clase del optimizador como el DataSource. Este comportamiento no es optimo, así que la forma de solucionarlo es la siguiente:

1from abc import ABC, abstractmethod
2
3
4class DataSourceInterface(ABC):
5 @abstractmethod
6 def load_data(self, data):
7 pass
8
9
10class DataSourceSQL(DataSourceInterface):
11 def load_data(self, data):
12 # Code to load and preprocess data
13 pass
14
15class DataSourceAPI(DataSourceInterface):
16 def load_data(self, data):
17 # Code to load and preprocess data
18 pass
19
20
21class RouterOptimizer:
22 def __init__(self, data_source: DataSourceInterface):
23 self.data_source = data_source
24
25 def optimize(self):
26 # Code to optimize routing based on data
27 pass
28
29 def run(self, data):
30 processed_data = self.data_source.load_data(data)
31 return self.optimize(processed_data)
32
33
34api_source = DataSourceAPI()
35optimizer = RouterOptimizer(api_source)
36optimizer.run(data)

En esta nueva forma, la clase DataSource fue construida como una interfase, por lo que, se puede agregar cualquier otra fuente de datos, sin afectar a la clase del algoritmo de optimización, removiendo la dependencia antes descrita.

Conclusión.

Buenas practicas o frameworks son marcos de referencia, aunque no siempre son seguidos al pie de la letra, es bueno saberlos ya que han sido iterados y probados a lo largo del tiempo, y siguen manteniéndose como excelentes estándares en la industria.

Referencias

1: https://www.youtube.com/playlist?list=PL4CE9F710017EA77A .

2: https://www.youtube.com/watch?v=agkWYPUcLpg&t=1s

3: https://realpython.com/solid-principles-python/


Ir arriba
SOLID: Principios de Programación