Skip to content

Principio de Responsabilidad Única – SOLID

28 abril, 2020

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 un UIViewController. Cuando esto ocurre, se suele añadir lógica de negocio, lógica de persistencia y lógica de presentación a los UIViewController. 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 o toString), o métodos save() 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 Posts que se han escrito o se van a escribir próximamente: PostListViewController.

Lista de posts en un UITableView

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 mediante URLSession, Alomofire, u otro framework de red.
  • // 2: parsear el JSON y convertirlo en un array de Post, mediante el nativo JSONDecoder() o utilizando otro framework como ObjectMapper.
  • // 3: Persistir la lista de post para poder leerlos offline. Podría usarse CoreData, 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 nativa URLSession, tendríamos que cambiar el controlador.
  • Si estamos usando ObjectMapper para convertir el JSON en un Swift object, y decidimos utilizar el JSONDecoder 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 nativo CoreData, ¿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 tipo Date en un String. 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 un UINavigationController.

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 imports en el fichero: en el ejemplo del método viewDidLoad(), si usásemos Alomofire, ObjectMapper y Realm tendríamos sendos imports al principio del fichero. Cuando un fichero tiene varios imports, 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?

Por último, si te ha gustado el post, ¿me echarías una mano compartiéndolo en tus redes sociales?

👇👇👇


¿Me ayudas a compartir en redes sociales?