El Principio de Responsabilidad Única, conocido como Single Responsibility Principle o por sus siglas SRP, es el primero de los 5 principios SOLID de la programación orientada a objetos.
Significa que un módulo de software debería de tener sólo una única responsabilidad. Debería ser responsable de únicamente una tarea, y sólo debería ser modificado por una sola razón.
Es el más sencillo de entender y también es el más fácil de detectar, .
En el mundo de desarrollo de iOS, también es uno de los principios más violados por los desarrolladores. Incluso por la propia Apple. Algunos ejemplos pueden ser:
- Storyboards: son responsables de la capa de presentación de las aplicaciones. Sin embargo, también se pueden utilizar para configurar la navegación entre las diferentes pantallas.
- Los
UIViewController
: es muy habitual, sobre todo en desarrolladores más novatos, mal entender el patrón de diseño Modelo-Vista-Controlador y pensar que el único controlador que puede haber es unUIViewController
. Cuando esto ocurre, se suele añadir lógica de negocio, lógica de persistencia y lógica de presentación a losUIViewController
. Y tal y como su propio nombre indica, sólo debería ser un controlador de la vista. - API Clients: también es habitual añadir lógica de persistencia o de analítica en los API Clients, cuando éstos sólo deberían encargarse de obtener los datos a través de un API. Nada más.
- Modelos: he visto infinidad de modelos de dominio que tienen lógica de presentación (el famoso
description
otoString
), o métodossave()
para persistirse a sí mismos. Un modelo debería encargarse únicamente de ser un contenedor de datos. - Y muchos más…
Ejemplo de violación del Principio de Responsabilidad Única
Imagina que estamos creando una aplicación para este blog, donde tenemos una pantalla con la lista de Post
s que se han escrito o se van a escribir próximamente: PostListViewController
.
Dentro de este controlador, tenemos el siguiente código en el viewDidLoad()
:
override func viewDidLoad() {
super.viewDidLoad()
fetchPostList { [unowned self] result in // 1
switch result {
case .success(let data):
let postList: [Post] = self.parse(data: data) // 2
self.save(posts: postList) // 3
self.postList = postList
DispatchQueue.main.async {
self.tableView.reloadData()
}
case .failure:
// Manejar el error
break
}
}
}
También tenemos el siguiente método de UITableViewDataSource
:
extension PostListViewController: UITableViewDataSource {
...
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CellId") ?? UITableViewCell(style: .subtitle, reuseIdentifier: "CellId")
let post = postList[indexPath.row]
// 4
let formatter = DateFormatter()
formatter.dateFormat = "dd/MM/yyyy"
let date = formatter.string(from: post.date)
cell.textLabel?.text = post.title
if post.date < Date() {
cell.detailTextLabel?.text = "Publicado el \(date)"
}
else {
cell.detailTextLabel?.text = "Próximamente..."
}
return cell
}
}
Y por último, un método del UITableViewDelegate
para navegar a la pantalla del detalle del Post
, PostDetailViewController
:
extension PostListViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let post = postList[indexPath.row]
// 5
let postDetailViewController = PostDetailViewController(post: post)
navigationController?.pushViewController(postDetailViewController, animated: true)
}
}
A priori, puede parecer que está todo bien. Pero si te paras a pensar un momento, puedes darte cuenta de que el PostListViewController
hace demasiadas cosas, tiene varias responsabilidades. Pero vamos por partes:
En el método viewDidLoad()
:
class PostListViewController: UIViewController {
...
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
fetchPostList { [unowned self] result in // 1
switch result {
case .success(let data):
let postList: [Post] = self.parse(data: data) // 2
self.save(posts: postList) // 3
DispatchQueue.main.async {
self.tableView.reloadData()
}
case .failure:
// Manejar el error
break
}
}
}
}
// MARK: Network
extension PostListViewController {
func fetchPostList(completion: @escaping (Result<Data, HTTPError>) -> Void) {
// Descargar la lista de posts de un API
}
func parse(data: Data) -> [Post] {
// Decodificar el JSON data y transformarlo en un array de Post
}
func save(posts: [Post]) {
// Persistir la lista de posts
}
}
El PostListViewController
es responsable de:
// 1
: descargar de red un JSON de un API medianteURLSession
,Alomofire
, u otro framework de red.// 2
: parsear el JSON y convertirlo en un array dePost
, mediante el nativoJSONDecoder()
o utilizando otro framework comoObjectMapper
.// 3
: Persistir la lista de post para poder leerlos offline. Podría usarseCoreData
,Realm
, o cualquier otro mecanismo de persistencia.
Volvamos a la definición del SRP:
«Un objeto sólo debería hacer una cosa, y tener una única razón para cambiar»
Está claro que este controlador hace más de una cosa con el código anterior, pero además:
- Si estamos utilizando
Alomofire
, pero decidimos borrar esta librería y utilizar la nativaURLSession
, tendríamos que cambiar el controlador. - Si estamos usando
ObjectMapper
para convertir el JSON en un Swift object, y decidimos utilizar elJSONDecoder
nativo, también tendríamos que modificar el código en el controlador. - Por último, si utilizamos
Realm
como motor de persistencia, pero decidimos utilizar el nativoCoreData
, ¿sabes qué tendríamos que hacer?… Creo que sí.
Como puedes observar, el PostListViewController
no tiene únicamente una razón por la que cambiar. Idealmente, éste sólo debería cambiar si la UI de la pantalla cambia para reflejar y controlar esas modificaciones.
Una posible solución sería crear un HTTPService
encargado exclusivamente de descargar el JSON de red, un Transformer
que sepa cómo parsear los JSON y convertirlos en DomainModels y, por último, un PersistenceService
que encapsule la lógica de persistencia de la aplicación.
Método del UITableViewDataSource
:
extension PostListViewController: UITableViewDataSource {
...
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CellId") ?? UITableViewCell(style: .subtitle, reuseIdentifier: "CellId")
let post = postList[indexPath.row]
// 4
let formatter = DateFormatter()
formatter.dateFormat = "dd/MM/yyyy"
let date = formatter.string(from: post.date)
cell.textLabel?.text = post.title
if post.date < Date() {
cell.detailTextLabel?.text = "Publicado el \(date)"
}
else {
cell.detailTextLabel?.text = "Próximamente..."
}
return cell
}
}
// 4
: aquí el controlador se encarga de transformar un objeto tipoDate
en unString
. También se encarga de parte de lógica de negocio, ya que si el post todavía no ha sido publicado, el subtítulo es diferente.
Como ya mencioné un poco más arriba, un UIViewController
debería de encargarse solamente de controlar sus subvistas y de nada más. Tampoco de transformar un tipo fecha en un String
legible por un ser humano.
Una posible solución sería proveer a este controlador de un ViewModel
con una propiedad date
, ya de tipo String
.
Método de UITableViewDelegate
:
extension PostListViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let post = postList[indexPath.row]
// 5
let postDetailViewController = PostDetailViewController(post: post)
navigationController?.pushViewController(postDetailViewController, animated: true)
}
}
// 5
: por último, este controlador también es responsable de la navegación hacia la vista de detalle, mediante un push en unUINavigationController
.
Además de violar el principio de responsabilidad única estás añadiendo acoplamiento entre el PostListViewController
y el PostDetailViewController
.
Y no sólo eso, si no que también estás añadiendo acoplamiento a la forma en la que se presenta esta vista de detalle, mediante un push a un UINavigationController
. ¿Qué ocurre si en un iPad se presenta modalmente, o mediante un UISplitViewController
? Efectivamente, este ViewController
tendría que ser modificado.
La solución aquí es clara. Respetar el SRP. Hacer que este controlador sea únicamente responsable de controlar sus subvistas, informando de cuándo un elemento es pulsado, como en este caso.
Podríamos informar de este evento a un objeto Coordinator
o Presenter
. El nombre no importa. los diferentes patrones de diseño (VIPER, MVVM, Coordinator, etc…) lo único que hacen es respetar los principios SOLID y darle un nombre diferente a cada componente. Éstos no son más que patrones MVC con muchos Cs. El Coordinator
, Presenter
o ViewModel
no son más que un controlador de controladores.
¿Cómo detectar si estamos violando el Principio de Responsabilidad Única?
Con el paso del tiempo, lo irás detectándo con mayor facilidad, aunque ahí van unos trucos:
- Pregúntate siempre cuál es la responsabilidad del objeto que vas a modificar: es muy tentador añadir un pequeño método a algún objeto preexistente. Pregúntate, siempre, si ese método que estás añadiendo tiene que ver con su responsabilidad.
- Observa el número de
import
s en el fichero: en el ejemplo del métodoviewDidLoad()
, si usásemos Alomofire, ObjectMapper y Realm tendríamos sendosimport
s al principio del fichero. Cuando un fichero tiene variosimport
s, es una señal de que podemos estar violando el SRP. - Asegúrate de que no se mezclan varias capas de arquitectura: todo el código de una aplicación puede dividirse en varios grupos o capas: capa de persistencia, capa de servicios, lógica de negocio y lógica de presentación. Si un objeto tiene lógica de más de una capa, estás violando el principio de responsabilidad única.
- ¿Tu objeto es fácil de testar?: Si tienes dificultades para añadir test unitarios, es posible que el objeto haga demasiadas cosas y no cumpla con el principio de responsabilidad única.
Únete a la newsletter para no perderte ningún nuevo tutorial: recibe un email cuando se publiquen nuevos artículos o vídeos y no pierdas la oportunidad de seguir aprendiendo.
Conclusión
El Single Responsibility Principle es el primero de los cinco principios SOLID para programación orientada a objetos.
Éste intenta aislar los cambios de un objeto de manera que sólo exista una única razón para cambiar, y eso termina provocando que el objeto sea responsable de una sola tarea, haciendo que sea mucho más fácil de leer, mantener y modificar en el futuro.
En definitiva, ayuda a que el software que escribas sea más flexible y de mayor calidad.
¿A dónde puedes ir ahora?
- En el siguiente post hablaré del segundo SOLID principle, el principio Open/Closed o Abierto/Cerrado.
- También volver a la página principal sobre los Principios SOLID en programación.
Por último, si te ha gustado el post, ¿me echarías una mano compartiéndolo en tus redes sociales?
👇👇👇