Skip to content

Principio Abierto Cerrado – SOLID

4 mayo, 2020

El Principio Abierto/Cerrado, también conocido como Open/Closed Principle o por sus siglas OCP, es el segundo de los 5 principios SOLID de la programación orientada a objetos.

En su definición, este principio dice que «un módulo de software debería estar abierto a extensión pero cerrado a modificación».

No sé tú, pero yo con esta definición no me enteré de nada la primera vez que la leí, así que te la voy a intentar explicar de manera sencilla y, como siempre, con ejemplos.

¿Qué quiere decir que la entidad de software tenga que estar abierta a extensión pero cerrada a modificación?

Imagina que estás construyendo una app en la que el usuario puede hacer el login con Facebook, Google y Twitter.

El módulo o entidad de software que vamos a poner como ejemplo es el servicio de autenticación de esta app: el AuthenticationService.

  • Abierto a extensión: quiere decir tienes que ser capaz de añadir nuevas funcionalidades, por ejemplo, el Sign in con Apple
  • Cerrado a modificación: quiere decir que para añadir la nueva funcionalidad no tienes que cambiar código que ya está escrito.

Es decir, que ante cambios en la app, tienes que ser capaz de añadir las nuevas funcionalidades sin modificar el código ya existente, siempre que esto sea posible.

Ejemplo del principio SOLID Open Closed en iOS Swift:

Actualmente, la app dispone de un objeto AuthenticationService encargado de la autenticación de los usuarios:

enum AuthenticationEngine {
    case facebook
    case google
    case twitter
}

final class AuthenticationService {
    
    func signIn(using engine: AuthenticationEngine, completion: (Result<User, LoginError> ) -> Void) {
        switch engine { 
        case .facebook:
            signInWithFacebook(completion: completion)
            
        case .google:
            signInWithGoogle(completion: completion)
            
        case .twitter:
            signInWithTwitter(completion: completion)
    }
}

extension AuthenticationService {
    func signInWithFacebook(completion: (Result<User, LoginError>) -> Void) {
        // Usar el SDK de Facebook para implementar el login
    }
    
    func signInWithGoogle(completion: (Result<User, LoginError>) -> Void) {
        // Usar el SDK de Google para implementar el login
    }
    
    func signInWithTwitter(completion: (Result<User, LoginError>) -> Void) {
        // Usar el SDK de Twitter para implementar el login
    }
}

A priori, puede parecer que el código anterior es totalmente correcto.

Pero, ¿qué sucede si ahora tienes que implementar el Sign in con Apple? Fácil, ¿no?

Empiezas añadiendo un nuevo case apple al enumerado AuthenticationEngine:

enum AuthenticationEngine {
    ...
    case apple
}

Ahora, el switch te lanza un error, ya que éste tiene que ser exhaustivo. Lo arreglas añadiendo el código para hacer el SignIn con Apple:

final class AuthenticationService {
    
    func signIn(using engine: AuthenticationEngine, completion: (Result<User, LoginError>) -> Void) {
        switch engine {
        ...
 
        case .apple:
            signInWithApple(completion: completion)
        }
    }
}

extension AuthenticationService {
    
    ...
    
    func signInWithApple(completion: (Result<User, LoginError>) -> Void) {
        // Usar el SDK de Apple para implementar el login
    }
}

Para añadir una funcionalidada tu app (a.k.a. «abierta a extensión») has tenido que modificar un objeto que ya tenías escrito. Si el día de mañana quieres añadir el SignIn con GitHub tendrías que cambiarlo de nuevo, así que tu código nunca estaría «cerrado a modificación», violando el Principio SOLID Abierto/Cerrado.

¿Cómo podemos solucionar el código para no violar el Principio Abierto/Cerrado?

El Principio SOLID Open Closed se suele resolver utilizando polimorfismo, clases abstractas, herencia o protocolos.

La clave es evitar que el objeto principal, el AuthenticationService, sepa cómo se realiza la autenticación. En su lugar, éste delegará la tarea en los objetos que utiliza, es decir, los AuthenticationEngines.

En Swift no tenemos clases abstractas en sí, y siendo un lenguaje tan moderno y potente yo suelo optar por utilizar composición mediante protocolos.

Es decir, los diferentes AuthenticationEngines tendrán una interfaz común y cada uno implementará su propia autenticación utilizando el SDK correspondiente.

Vamos a modificar la implementación anterior haciendo que AuthenticationEngine sea un protocolo en lugar de un enum:

protocol AuthenticationEngine: class {
    func signIn(completion: (Result<User, LoginError>) -> Void)
}

Después creamos las implementaciones para Facebook, Twitter, y Google, cada uno en su propio fichero:

final class FacebookAuthenticationEngine: AuthenticationEngine {
    
    func signIn(completion: (Result<User, LoginError>) -> Void) {
        // Usar el SDK de Facebook para implementar el sign in
    }
}
final class TwitterAuthenticationEngine: AuthenticationEngine {
    
    func signIn(completion: (Result<User, LoginError>) -> Void) {
        // Usar el SDK de Twitter para implementar el sign in
    }
}
final class GoogleAuthenticationEngine: AuthenticationEngine {
    
    func signIn(completion: (Result<User, LoginError>) -> Void) {
        // Usar el SDK de Google para implementar el sign in
    }
}

Y por último, el objeto AuthenticationService se quedaría así:

final class AuthenticationService {
        
    func signIn(using engine: AuthenticationEngine, completion: (Result<User, LoginError>) -> Void) {
        engine.signIn(completion: completion)
    }
}

Añadiendo una nueva funcionalidad: el SignIn con Apple

Con esta nueva arquitectura, para añadir SignIn con Apple bastaría con crear una nueva implementación del protocolo AuthenticationEngine:

final class AppleAuthenticationEngine: AuthenticationEngine {
    
    func signIn(completion: (Result<User, LoginError>) -> Void) {
        // Usar el SDK de Apple para implementar el sign in
    }
}

Fíjate que no tendrías que modificar el objeto principal AuthenticationService para soportar nuevos tipos de login con otras redes sociales.

Así, el AuthenticationService cumple el principio abierto cerrado: está abierto a extensión (podemos añadir nuevos tipos de login) pero está cerrado a modificación (no tenemos que modificarlo en absoluto para poder soportar nuevos tipos de login).

Bastaría con pasarle por parámetro una de las implementaciones de AuthenticationEngine.

final class SignInViewController: UIViewController {
    
    ...

    private let apple: AppleAuthenticationEngine
    
    ...

    @IBAction private func appleButtonTapped(_ button: UIButton) {
        authenticationService.signIn(using: apple) { result in
            // handle result
        }
    }
}

¿Cómo detectar que estás violando el Principio Abierto Cerrado?

Tienes que fijarte en los objetos que son modificados más a menudo cuando añades una funcionalidad a tu app. Si éstos siempre son los mismos, puede que estés violando el Principio Open/Closed.

El principio Abierto/Cerrado está muy relacionado con el acoplamiento en el software. Si varios objetos están acoplados entre sí, hacer un cambio en uno de ellos suele requerir hacer cambios en varios objetos y sitios diferentes de tu app.

Si has leído el artículo sobre el Principio de Responsabilidad Única, también te habrás dado cuenta que en el primer ejemplo, el AuthenticationService también violaba el Single Responsibility Principle ya que se encargaba de realizar el login con Facebook, con Twitter y con Google.

Ú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 principio Abierto/Cerrado es el segundo de los principios SOLID. Es importante aplicar este principio para tener un software protegido de los cambios en módulos donde las modificaciones son más frecuentes.

El patrón de diseño Strategy, utilizado en el ejemplo del `AuthenticationService`, o el patrón Decorator ayudan a seguir el principio Open/Closed.

¿A dónde puedes ir ahora?

¿Conocías este principio? Cuéntame qué te ha parecido en los comentarios, y si te ha resultado útil, ¿me ayudas a compartir?

👇👇👇


¿Me ayudas a compartir en redes sociales?