Design Patterns in Go
  • Portada
  • Introducción
  • Publicación
  • Parte I
    • Sobre Go
    • POO en Go
      • Objetos
      • Herencia / Composición
      • S.O.L.I.D
  • Parte II
    • Patrones de Diseño
      • GoF
      • Patrones de Comportamiento
        • Strategy
        • Chain of Responsibility
        • Command
        • Template Method
        • Memento
        • Interpreter
        • Iterator
        • Visitor
        • State
        • Mediator
        • Observer
      • Patrones Creacionales
        • Singleton
        • Builder
        • Factory Method
        • Abstract Factory
        • Prototype
      • Patrones Estructurales
        • Composite
        • Adapter
        • Bridge
        • Proxy
        • Decorator
        • Facade
        • Flyweight
  • Parte III
    • Conclusiones
    • Acerca De
  • Recursos de interés
  • Glosario
Con tecnología de GitBook
En esta página
  • Principio de responsabilidad única
  • Principio abierto cerrado
  • Principio de substitución de Liskov
  • Principio de segregación de la interfaz
  • Principio de inversión de la dependencia
  • Conclusión

¿Te fue útil?

  1. Parte I
  2. POO en Go

S.O.L.I.D

AnteriorHerencia / ComposiciónSiguienteParte II

Última actualización hace 5 años

¿Te fue útil?

SOLID es un acrónimo introducido por Robert C. Martin en su libro "Agile Software Development, Principles, Patterns and Practices" y hace referencia a los siguientes cinco principios:

  • S: (SRP - Single responsibility principle) Principio de responsabilidad única.

  • O: (OCP - Open closed principle) Principio abierto cerrado.

  • L: (LSP - Liskov substitution principle) Principio de substitución de Liskov.

  • I: (ISP - Interface segregation principle) Principio de segregación de la interfaz.

  • D: (DIP - Dependency inversion principle) Principio de inversión de la dependencia.

El objetivo de aplicar estos principios es obtener sistemas orientados a objetos con código de mayor calidad, facilidad de mantenimiento y mejores oportunidades de reuso de código.

Principio de responsabilidad única

Una clase debe tener una, y sólo una, razón para cambiar, lo que significa que una clase debe tener un solo trabajo.

La primera observación respecto de este principio es que en Go no existen clases. Sin embargo, como vimos mediante la incorporación de comportamientos a tipos de datos, podemos llegar a un concepto equivalente.

Este principio hace foco en que un objeto debe tener únicamente una responsabilidad encapsulada por la clase. Cuando se hace referencia a una responsabilidad es para referirse a una razón para cambiar. Mantener una clase que tiene múltiples objetivos o responsabilidades es mucho más complejo que una clase enfocada en una única responsabilidad.

El siguiente ejemplo no cumple con este principio porque otorga a una estructura dos responsabilidades bien diferenciadas: guardar en un archivo local y guardar en una base de datos.

package main

type Documento struct {
    Nombre  string
}

func (d *Documento) GuardarEnArchivo() {
   // ...
}

func (d *Documento) GuardarEnBaseDatos() {
   // ...
}

Un Documento debería saber cómo acceder al sistema de archivos local y a la vez como conectarse y operar contra una base de datos. Implementar ambas acciones en una misma estructura genera un alto acoplamiento.

Creando estructuras con responsabilidades bien definidas se puede mejorar el código de la siguiente manera:

package model

type Documento struct {
    Nombre  string
}
package store

import "model"

type GuardadorDocumento interface {
    Guardar(d model.Documento)
}

type GuardadorDocumentoArchivo struct {
    Path  string
}

func (gda GuardadorDocumentoArchivo) Guardar(d model.Documento) {
   // ...
}

type GuardadorDocumentoBaseDatos struct {
    StringConnection  string
}

func (gdbd GuardadorDocumentoBaseDatos) Guardar(d model.Documento) {
   // ...
}

¿Qué pasa en Go: Gracias a la organización en paquetes que permite Go es posible crear estructuras, tipos, funciones y métodos empaquetados con propósitos claros y bien definidos.

Principio abierto cerrado

Los objetos o entidades deberían estar abiertos para la extensión, pero cerrados para su modificación.

Este principio propone que una entidad esté cerrada, lista para ser usada y estable en su calidad e interfaces, y al mismo tiempo abierta, es decir, que permita extender su comportamiento (pero sin modificar su código fuente).

Imaginemos que se requiere cambiar el comportamiento de la siguiente estructura únicamente en uno de sus métodos:

type Animal struct {
    Nombre  string
}

func (a Animal) Caminar() {
    // ...
}

func (a Animal) Saltar() {
    // ...
}

Podemos hacerlo de la siguiente forma:

type AnimalModificado struct {
    Animal
}

func (am AnimalModificado) Caminar() {
    // ...
}

¿Qué pasa en Go: Gracias a la composición que permite Go es posible componer tipos simples en más complejos manteniendo la interfaz del tipo original.

Principio de substitución de Liskov

Cada clase que herede de otra debe poder utilizarse como su clase padre sin necesidad de conocer las diferencias que pudieran existir entre ellas.

Este principio propone que el contrato de una clase base debe ser honrado por sus clases derivadas para que instancias de las clases derivadas puedan reemplazar a instancias de la clase base.

Veamos el siguiente código:

type RespuestaJSON struct {}

func (r RespuestaJSON) Imprimir() {
    // ...
}

type RespuestaHTML struct {}

func (r RespuestaHTML) Imprimir() {
    // ...
}

type Emision struct {}

func (e Emision) EmitirJSON(r RespuestaJSON) {
    // ...
}

func (e Emision) EmitirHTML(r RespuestaHTML) {
    // ...
}

La estructura Emision debe implementar dos comportamientos, ya que debe poder gestionar impresiones en HTML y JSON. Si a futuro se requiriera de otro tipo de impresión - xml por ejemplo - se debería modificar su código fuente.

La siguiente modificación permite intercambiar cualquier tipo de respuesta para su impresión:

type Respuesta interface {
    Imprimir()
}

type RespuestaJSON struct {}

func (r RespuestaJSON) Imprimir() {
    // ...
}

type RespuestaHTML struct {}

func (r RespuestaHTML) Imprimir() {
    // ...
}

type Emision struct {}

func (e Emision) Emitir(r Respuesta) {
    // ...
}

¿Qué pasa en Go: Al definir firmas de métodos a través de interfaces, y no mediante tipos concretos, es posible utilizar cualquier tipo que respete implícitamente la interfaz.

Principio de segregación de la interfaz

Nunca se debe obligar a un cliente a implementar una interfaz que no utilice, o no se debe forzar a los clientes a depender de métodos que no usan.

Este principio hace foco en como deben definirse las interfaces. Las mismas deben ser pequeñas y específicas. Grandes y complejas interfaces obligan al cliente a implementar métodos que no necesita.

Veamos la siguiente interface:

type Boton interface {
    OnClick()
    OnDobleClick()
}

type BotonLogin struct {}

func (b BotonLogin) OnClick() {
    // ...
}

func (b BotonLogin) OnDobleClick() {
    // vacio
}

Como puede verse la interface Boton obliga a implementar ambos comportamientos en sus clientes cuando es muy factible que no todos ellos deban implementar ambas opciones.

Una solución podría ser la siguiente:

type OnClickListener interface {
    OnClick()
}

type OnDobleClickListener interface {
    OnDobleClick()
}

type BotonLogin struct {}

func (b BotonLogin) OnClick() {
    // ...
}

type BotonIcono struct {}

func (b BotonIcono) OnDobleClick() {
    // ...
}

¿Qué pasa en Go: En Go puede aplicarse este concepto aislando el comportamiento requerido utilizando interfaces más pequeñas.

Principio de inversión de la dependencia

Los módulos de alto nivel no deben depender de módulos de bajo nivel. Ambos deberían depender de abstracciones. Las abstracciones no deben depender de los detalles. Los detalles deben depender de las abstracciones.

Este principio esta basado en reducir las dependencias entre los módulos del código para atacar el alto acoplamiento.

Veamos la siguiente ejemplo:

type B struct {}

func (b B) Saludar() {
    // ...
}

type A struct {
    B
}

Como puede verse el tipo A depende del tipo B por lo que si el tipo B es modificado afectará al tipo A.

Una solución podría ser la siguiente:

type Saludador interface {
    Saludar()
}

type B struct {}

func (b B) Saludar() {
    // ...
}

type A struct {
    Saludador
}

¿Qué pasa en Go: Componer tipos mediante interfaces, y no mediante tipos concretos, permite evitar una fuerte dependencia entre tipos.

Conclusión

Como se vio en el apartado anterior, el poder de la composición y de las interfaces implícitas le permiten a Go implementar buenas prácticas y conceptos propios de la programación orientada a objetos.

Si bien el libro de Robert C. Martin tiene más de una década y media y hace referencia a lenguajes propiamente orientados a objetos, vimos como también pueden aplicarse esos principios en Go.

Atención: Esta publicación se encuentra abandonada. Puede acceder a la versión vigente en

[48]
Ejecutar código
Ejecutar código
Ejecutar código
Ejecutar código
Ejecutar código
Ejecutar código
Ejecutar código
Ejecutar código
Ejecutar código
Ejecutar código
[48]
https://leanpub.com/designpatternsingo