Extensibilidad y Flexibilidad de componentes de Software

Mantenibilidad


Mantenibilidad es uno de los tipos de atributo de calidad (Quality Attributes) en el diseño de software y hace referencia a la capacidad que una pieza de software tiene para ser sometida a cambios y mantener su valor sobre el tiempo.

Para que una pieza de software cumpla con dicho atributo de calidad, debe ser capaz de ser extensible y flexible, es decir tener la capacidad de ser extendido en sus responsabilidades, capacidades funcionales y permitir que sea modificada sin mayores traumatismos.

Probablemente se han encontrado en situaciones en donde las reglas de negocio cambian o necesitan ser agregadas nuevas funcionalidades sobre el actual diseño, son escenarios bastantes comunes que difícilmente no estarán presentes.

Un buen diseño de software tendrá en consideración esos futuros cambios, sin embargo no siempre es así y los equipos de implementación toman la iniciativa de proponer soluciones para cumplir con los nuevos requerimientos solicitados.



¿Y cómo implementamos componentes que cumpla con la mantenibilidad y las capacidades de Extensibilidad y Flexibilidad?


La respuesta es, con la ayuda de patrones de diseño tales como Decorator, Adapter y el principio Open-Closed de SOLID.

Patrón Decorator y Adapter


Estos dos patrones ayudan a diseñar componentes de software que cuenten con esas capacidades de extensibilidad y flexibilidad asociadas a la mantenibilidad.

Una diferencia a tener presente entre ambos tipos de patrones es que el Patrón Adapter no pretende cambiar la estructura del componente original si no al contrario ofrecer una tercera estructura que sea capaz de adaptarla para que un cliente la pueda acceder. El Patrón Decorator tampoco pretende que la estructura original sea modificada pero sí en cambio busca que sea reemplazada por una nueva estructura que tenga mayores capacidades funcionales o de comportamiento. En esos términos, en la mayoría de los casos el Patrón Decorator tiene preferencia para cumplir con este atributo de calidad y dichas capacidades.





Open-Closed Principle


El principio Open-Closed dicta que un componente sea diseñado para ser abierto para extensión pero cerrado para modificación.

¿A cuántos nos ha ocurrido caer en la tentación de modificar una entidad u objeto ya existente para soportar nuevas operaciones o propiedades?


En soluciones que llevan bastante tiempo de haber sido implementadas y que exige constante mantenimiento se debe tener cuidado con modificar entidades o componentes base ya que se serán afectadas todas las entidades dependientes y ni que decir cuando se trata de una librería que es usada por múltiples clientes externos.

Parece bastante obvio la razón por la cual no debemos pensar como primera opción en modificar objetos existentes, pero cuando tienes al cliente respirando en el cuello y presionando por el tiempo muchos la toman como opción (el dilema de calidad vs tiempo).

¿A cuántos se nos ocurre como primera opción la herencia de clases para agregar nuevas propiedades u operaciones?¿O tal vez por qué no pensar en agregación?


Cuando agregamos responsabilidades a través de subclases, es decir usando la herencia, se corre el riesgo de arrastrar responsabilidades heredadas innecesarias. El Patrón Decorator es diseñado para permitir que las responsabilidades sean agregadas sin usar la herencia, en lugar de ello propone hacerlo por agregación. Una ventaja adicional de usar agregación en lugar de herencia en el diseño es contar con un sistema que a manera de plugins adicione o retire capacidades o responsabilidades en los objetos.

Implementación


En Swift, la extensibilidad se puede implementar a través de varias técnicas como:

  • Extensiones
  • Delegación
  • Extensiones de protocolo
  • Patrón Decorator

Extensiones
: Esta capacidad es propia del lenguaje Swift y permite agregarle comportamientos a una entidad a través de una estructura separada llamada extensión.

 
    struct OriginalEntity {
    
        var name: String
    }

    extension OriginalEntity {
     
        func operation() {
            print(name + " + new operation")
        }
    }

    let entity = OriginalEntity(name: "My Entity")
    entity.operation()

    Output:
    My Entity + new operation

Delegación: Consiste en asociarle a una entidad una extensión que conforme un protocolo con los comportamientos que se desean agregar, delegando la implementación sólo a las extensiones. En el mundo Java esto es similar a la aplicación de una clase abstracta.

    
    struct OriginalEntity {
    
        var name: String
    }

    protocol PlusOperations {
     
        func operation()
    }

    extension OriginalEntity: PlusOperations {
        func operation() {
            print(name + " + new operation")
        }
    }

    let entity = OriginalEntity(name: "My Entity")
    entity.operation()

    Output:
    My Entity + new operation


Extensiones de protocolo: En esta técnica, se define un protocolo y se le asocia una extensión que implemente los comportamientos que se desean agregar a la entidad, después se hace que la entidad conforme el protocolo y por ende herede los comportamientos agregados. La principal diferencia entre ésta técnica y la Delegación consiste en que cualquier entidad que conforme el protocolo heredará los comportamientos agregados, en cambio en la delegación serán las extensiones las que conozca los comportamientos agregados.

 
 struct OriginalEntity {
     
     var name: String
 }

 protocol PlusOperations {}

 extension PlusOperations {
     func operation() {
         print(" + new operation")
     }
 }

 extension OriginalEntity: PlusOperations {}

 let entity = OriginalEntity(name: "My Entity")
 entity.operation()

 Output:
  + new operation
 

Patrón Decorator: Es la técnica recomendada para garantizar extensibilidad a un nivel superior, aprovecha las estrategias anteriormente mencionadas y puede construirse usando clases o estructuras, tengamos en cuenta que en el uso de estructuras se usan objetos por valor y no por referencia como en el caso de las clases dandole así una ventaja a las estructuras quizás a nivel de performance.

La ventaja del patrón Decorator con respecto a las técnicas anteriores consiste en que las entidades originales a nivel de abstracción no son modificadas en cambio se utilizan las entidades originales sin ser modificadas y se extienden sus capacidades en una nueva entidad envolvente (wrapper).

 
 protocol Component {
     func operation() -> String
 }

 protocol Decorator: Component {
     var component: Component { get }
     func operation() -> String
 }

 extension Decorator {
     func operation() -> String {
         return component.operation()
     }
 }

 struct ConcreteComponent: Component {
     func operation() -> String {
         return "Operation from concrete component"
     }
 }

 struct ConcreteDecoratorA: Decorator {
     let component: Component
     func operation() -> String {
         return component.operation() + addedBehavior()
     }
     func addedBehavior() -> String {
         return " + operator from decorator"
     }
 }

 let originalComponent = ConcreteComponent()
 print(originalComponent.operation())

 let decoratorOriginalComponent = ConcreteDecoratorA(component: originalComponent)
 print(decoratorOriginalComponent.operation())

 Output:
 Operation from concrete component
 Operation from concrete component + operator from decorator
 

Ahora veamos en nuestro ejemplo inicial cómo sería la implementación del Patrón Decorator. Se utilizará la entidad original y un par de decoradores así:

 
 protocol Entity {
     func operation() -> String
 }

 protocol PlusOperations: Entity {
     var entity: Entity { get }
     func operation() -> String
 }

 extension PlusOperations {
     func operation() -> String {
         return entity.operation()
     }
 }

 struct OriginalEntity: Entity {
     func operation() -> String {
         return "Operation from concrete component"
     }
 }

 struct Addition: PlusOperations {
     
     let entity: Entity
     func operation() -> String {
         return entity.operation() + addedBehavior()
     }
     func addedBehavior() -> String {
         return " + operator from decorator"
     }
 }

 struct Subtraction: PlusOperations {
     
     let entity: Entity
     func operation() -> String {
         return entity.operation() + addedBehavior()
     }
     func addedBehavior() -> String {
         return " - operator from decorator"
     }
 }

 let originalEntity = OriginalEntity()
 print(originalEntity.operation())

 let additionEntity = Addition(entity: originalEntity)
 print(additionEntity.operation())

 let subtractionEntity = Subtraction(entity: originalEntity)
 print(subtractionEntity.operation())

 Output:
 Operation from concrete component
 Operation from concrete component + operator from decorator
 Operation from concrete component - operator from decorator
 

En el ejemplo, PlusOperations es un Decorator y las estructuras Addition, Subtraction son sus implementaciones concretas. La entidad original en este caso llamada OriginalEntity se mantiene sin modificación alguna.

Conclusión


La aplicación del Patrón Decorator y el Principio Open-Closed es más común de lo que creemos. Son muchas las situaciones en donde se pueden usar. Aplican para cualquier nivel o capa de arquitectura en la que estemos trabajando. Por ejemplo en un DataManager que requiera consumir diferentes tipos de API's de diferentes formas hasta un InputText nativo al que se le quisiera agregar un capacidad de animación de texto particular. Lo más importante es saber que existen, conocerlos y tenerlos como opciones para ser aplicados en nuestro código y hacer de nuestro trabajo un producto de mejor calidad.

Previous
Next Post »
Thanks for your comment