User Tools

Site Tools


wiki:psp_1

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revisionPrevious revision
Next revision
Previous revision
wiki:psp_1 [2024/08/22 15:49] – [Ocultamiento de la Información] adminwiki:psp_1 [2024/08/28 23:52] (current) admin
Line 26: Line 26:
  
 Esta propiedad, una traducción de la expresión information hiding (ocultación de información), fue discutida por primera vez en 1972 por David Parnas, en uno de los artículos más importantes e influyentes en el área de Ingeniería de Software de todos los tiempos, cuyo título es "//[[https://dl.acm.org/doi/10.1145/361598.361623|On the criteria to be used in decomposing systems into modules]]//". Esta propiedad, una traducción de la expresión information hiding (ocultación de información), fue discutida por primera vez en 1972 por David Parnas, en uno de los artículos más importantes e influyentes en el área de Ingeniería de Software de todos los tiempos, cuyo título es "//[[https://dl.acm.org/doi/10.1145/361598.361623|On the criteria to be used in decomposing systems into modules]]//".
 +
 +<blockquote>
 +<color black>Este artículo discute la modularización como un mecanismo capaz de hacer que los sistemas de software sean más flexibles y fáciles de entender y, al mismo tiempo, reducir su tiempo de desarrollo. La efectividad de una determinada modularización depende del criterio utilizado para dividir un sistema en módulos </color> 
 +<cite><color black>David Parnas, "On the criteria to be used in decomposing systems into modules"</color></cite>
 +</blockquote>
  
 **Aviso:** Parnas usa el término módulo en su artículo, pero esto fue en una época en que la orientación a objetos aún no había surgido, al menos como la conocemos hoy. En este capítulo, escrito casi 50 años después del trabajo de Parnas, optamos por el término clase, en lugar de módulo. El motivo es que las clases son la principal unidad de modularización en lenguajes de programación modernos como Java, C++ y Ruby. Sin embargo, el contenido del capítulo se aplica a otras unidades de modularización, incluidas aquellas más pequeñas que las clases, como métodos y funciones, y también a unidades más grandes, como paquetes y componentes. **Aviso:** Parnas usa el término módulo en su artículo, pero esto fue en una época en que la orientación a objetos aún no había surgido, al menos como la conocemos hoy. En este capítulo, escrito casi 50 años después del trabajo de Parnas, optamos por el término clase, en lugar de módulo. El motivo es que las clases son la principal unidad de modularización en lenguajes de programación modernos como Java, C++ y Ruby. Sin embargo, el contenido del capítulo se aplica a otras unidades de modularización, incluidas aquellas más pequeñas que las clases, como métodos y funciones, y también a unidades más grandes, como paquetes y componentes.
Line 45: Line 50:
 **Ejemplo: ** **Ejemplo: **
  
-A continuación un trecho de código para un sistema de estacionamiento en su primera versión. identifique el problema en relación a la propiedad de diseño de ocultamiento de la información. +A continuación un trecho de código para un sistema de estacionamiento en su primera versión. Identifique el problema en relación a la propiedad de diseño de ocultamiento de la información. 
  
 <code java> <code java>
 import java.util.Hashtable; import java.util.Hashtable;
  
-public class Estacionamento {+public class Estacionamiento {
  
-  public Hashtable<String, String> veiculos;+  public Hashtable<String, String> vehiculos;
  
-  public Estacionamento() { +  public Estacionamiento() { 
-    veiculos = new Hashtable<String, String>();+    vehiculos = new Hashtable<String, String>();
   }   }
  
   public static void main(String[] args) {   public static void main(String[] args) {
-    Estacionamento e = new Estacionamento(); +    Estacionamento e = new Estacionamiento(); 
-    e.veiculos.put("TCP-7030", "Uno"); +    e.vehiculos.put("TCP-7030", "Uno"); 
-    e.veiculos.put("BNF-4501", "Gol"); +    e.vehiculos.put("BNF-4501", "Gol"); 
-    e.veiculos.put("JKL-3481", "Corsa");+    e.vehiculos.put("JKL-3481", "Corsa");
   }   }
 } }
 </code> </code>
  
 +Esta clase tiene un problema de exposición excesiva de información o, en otras palabras, no oculta estructuras que pueden cambiar en el futuro. Específicamente, la tabla hash que almacena los vehículos estacionados en el estacionamiento es pública. Como resultado, los clientes —como el método main— tienen acceso directo a ella para, por ejemplo, añadir vehículos al estacionamiento. Si en el futuro decidimos usar otra estructura de datos para almacenar los vehículos, todos los clientes deberán ser modificados.
 +
 +Supongamos que el sistema de estacionamiento fuera manual, con los nombres de los vehículos anotados en una hoja de papel. Haciendo una comparación, esta primera versión de la clase Estacionamiento correspondería —en el caso de este sistema manual— a que el cliente del estacionamiento, después de estacionar su vehículo, entrara en la cabina de control y escribiera él mismo la patente y el modelo de su vehículo en la hoja de control.
 +
 +La siguiente versión de la clase es mejor, ya que encapsula la estructura de datos responsable de almacenar los vehículos. Ahora existe el método estaciona para estacionar un vehículo. Con esto, los desarrolladores de la clase tienen la libertad de cambiar la estructura de datos sin causar impacto en sus clientes. La única restricción es que la firma de estaciona debe ser preservada.
 +
 +
 +<code java>
 +import java.util.Hashtable;
 +
 +public class Estacionamiento {
 +
 +  private Hashtable<String,String> vehiculos;
 +
 +  public Estacionamiento() {
 +    vehiculos = new Hashtable<String, String>();
 +  }
 +
 +  public void estaciona(String patente, String vehiculo) {
 +    vehiculos.put(patente, vehiculo);
 +  }
 +
 +  public static void main(String[] args) {
 +    Estacionamiento e = new Estacionamiento();
 +    e.estaciona("TCP-7030", "Uno");
 +    e.estaciona("BNF-4501", "Gol");
 +    e.estaciona("JKL-3481", "Corsa");
 +  }
 +}
 +</code>
 +
 +En resumen, esta nueva versión oculta una estructura de datos - sujeta a alteraciones durante la evolución del sistem - y proporciona una interfaz estable para los clientes da la clase - representada por el método //estaciona//.
 +
 +===== Cohesión =====
 +
 +La implementación de cualquier clase debe ser cohesiva, es decir, cada clase debe implementar una única funcionalidad o servicio. Específicamente, todos los métodos y atributos de una clase deben estar orientados a la implementación del mismo servicio. Otra forma de explicar la cohesión es afirmando que cada clase debe tener una única responsabilidad en el sistema. O, dicho de otra manera, debe existir un único motivo para modificar una clase.
 +
 +La cohesión tiene las siguientes ventajas:
 +
 +  * Facilita la implementación de una clase, así como su comprensión y mantenimiento.
 +  * Facilita la asignación de un único responsable para mantener una clase.
 +  * Facilita la reutilización y prueba de una clase, ya que es más sencillo reutilizar y probar una clase cohesiva que una clase con múltiples responsabilidades.
 +
 +La separación de intereses (separation of concerns) es otra propiedad deseable en los proyectos de software, la cual es similar al concepto de cohesión. Defiende que una clase debe implementar solo un interés (concern). En este contexto, el término interés se refiere a cualquier funcionalidad, requisito o responsabilidad de la clase. Por lo tanto, las siguientes recomendaciones son equivalentes: (1) una clase debe tener una única responsabilidad; (2) una clase debe implementar un único interés; (3) una clase debe ser cohesiva.
 +
 +==== Ejemplos ====
 +
 +   1. La discusión anterior nos dice que las clases deben ser cohesivas, sin embargo, el concepto se adapta también a métodos y funciones. Por ejemplo, suponga una función como la siguiente:
 +
 +
 +<code java>
 +
 +float sin_or_cos(double x, int op) {
 +  if (op == 1)
 +    "calcula y retorna seno de x"
 +  else
 +    "calcula y retorna coseno de x"
 +}
 +
 +</code>
 +
 +Esta función - que consiste en un ejemplo extremo y, que en la práctica seria poco común - presenta un problema serio de cohesión porque realiza dos cosas: calcula el seno o coseno de su argumento. Lo recomendable seria crear funciones separadas para cada una de estas tareas.
 +
 +   2. Suponga ahora la siguiente clase:
 +
 +<code java>
 +
 +class Stack<T> {
 +  boolean empty() { ... }
 +  T pop() { ... }
 +  push (T) { ... }
 +  int size() { ... }
 +}
 +
 +</code>
 +
 +Se trata de una clase cohesiva, porque todos lo métodos implementan operaciones importantes en una estructura de datos del tipo Pila.
 +
 +===== Acoplamiento  =====
 +
 +El acoplamiento es la fuerza de la conexión entre dos clases. Aunque pueda parecer simple, el concepto tiene algunas matices, las cuales derivan de la existencia de dos tipos de acoplamiento entre clases: acoplamiento aceptable y acoplamiento no aceptable.
 +
 +Decimos que existe un acoplamiento aceptable de una clase A a una clase B cuando:
 +
 +La clase A usa solo métodos públicos de la clase B.
 +
 +La interfaz proporcionada por B es estable desde el punto de vista sintáctico y semántico. Es decir, las firmas de los métodos públicos de B no cambian con frecuencia; y lo mismo ocurre con el comportamiento externo de dichos métodos. Por eso, son raros los cambios en B que tendrán un impacto en la clase A.
 +
 +Por otro lado, existe un acoplamiento no aceptable de una clase A a una clase B cuando los cambios en B pueden impactar fácilmente a A. Esto ocurre principalmente en las siguientes situaciones:
 +
 +  * Cuando la clase A realiza un acceso directo a un archivo o base de datos de la clase B.
 +
 +  * Cuando las clases A y B comparten una variable o estructura de datos global. Por ejemplo, la clase B altera el valor de una variable global que la clase A usa en su código.
 +
 +  * Cuando la interfaz de la clase B no es estable. Por ejemplo, los métodos públicos de B se renombran con frecuencia.
 +
 +En esencia, lo que caracteriza el acoplamiento no aceptable es el hecho de que la dependencia entre las clases no está mediada por una interfaz estable. Por ejemplo, cuando una clase altera el valor de una variable global, no es consciente del impacto de ese cambio en otras partes del sistema. Por otro lado, cuando una clase altera su interfaz, es consciente de que eso tendrá un impacto en los clientes, pues la función de una interfaz es precisamente anunciar los servicios que una clase ofrece al resto del sistema.
 +
 +En resumen: el acoplamiento puede ser de gran utilidad, especialmente cuando ocurre con la interfaz de una clase estable que presta un servicio relevante para la clase de origen. Sin embargo, el acoplamiento no aceptable debe evitarse, ya que es un acoplamiento no mediado por interfaces. Los cambios en la clase de destino del acoplamiento pueden propagarse fácilmente a la clase de origen.
 +
 +Frecuentemente, las recomendaciones sobre acoplamiento y cohesión se reúnen en una única recomendación:
 +
 +<blockquote>
 +<color black>Maximice la cohesión de las clases o minimice el acoplamiento entre ellas </color> 
 +</blockquote>
 +
 +De hecho, si una clase depende de muchas otras clases, por ejemplo, de decenas de clases, puede estar asumiendo demasiadas responsabilidades en forma de funcionalidades no cohesivas. Recuerda que una clase debe tener una única responsabilidad (o un único motivo para ser modificada). Por otro lado, debemos tener cuidado con el significado del verbo minimizar. El objetivo no debe ser eliminar completamente el acoplamiento de una clase con otras clases, ya que es natural que una clase necesite de otras clases, especialmente de aquellas que implementan servicios básicos, como estructuras de datos, entrada/salida, etc.
 +
 +  - Recuerde la clase ''Estacionamiento'', usada anteriormente, la cual posee un atributo que es una ''Hashtable''. Entonces, diremos que ''Estacionamiento'' está acoplada a ''Hashtable''. Sin embargo, de acuerdo a la clasificación dada, se trata de un acoplamiento aceptable, es decir, no hay motivo de preocupación por los siguientes motivos:
 +     * ''Estacionamiento'' solo es usa los métodos públicos de ''Hashtable''.
 +     * La interfaz de ''Hashtable'' es estable, ya que esta es parte del paquete oficial de estructuras de datos de Java. Así, un alteración en la firma de los métodos públicos de ''Hashtable'' quebraría no solo la clase  ''Estacionamiento'', sino que también millones de otras clases de diversos sistemas en Java al rededor del mundo.
 +  - Suponga el siguiente trecho de código, en el cual existen un archivo compartido por dos clases, ''A'' y ''B'', mantenidas por desarrolladores distintos. El método ''B.g()'' guarda un entero en el archivo, que es leído por ''A.f()''. Esta forma de comunicación origina un acoplamiento no aceptable entre las clases. Por ejemplo, el desarrollador que implementa ''B'' no puede saber que el archivo es leído por ''A''. Así, el puede decidir cambiar el formato del archivo por cuenta propia, sin comunicar al desarrollador de la clase ''A''.
 +
 +<code java>
 +class A {
 +  private void f() {
 +    int total; ...
 +    File arch = File.open("arch1.db");
 +    total = arch.readInt();
 +    ...
 +  }
 +}
 +</code>
 +
 +<code java>
 +class B {
 +  private void g() {
 +    int total;
 +    // calcula valor total
 +    File arch = File.open("arch1.db");
 +    arch.writeInt(total);
 +    ...
 +    arch.close();
 +  }
 +</code>
 +
 +En el ejemplo también existe un acoplamiento entre ''B'' y ''File''. Sin embargo, este es un acoplamiento aceptable, porque ''B'' realmente necesita persistir sus datos. Entonces, para conseguir esto, nada mejor que usar una clase de la biblioteca de entrada y salida del lenguaje de programación. Una solución mejor para el acomplamiento entre las clases ''A'' y ''B'' de este ejemplo se muestra a continuación:
 +
 +<code java>
 +class A {
 +
 +  private void f(B b) {
 +    int total;
 +    total = b.getTotal();
 +    ...
 +  }
 +</code>
 +
 +<code java>
 +class B {
 +
 +  int total;
 +
 +  public int getTotal() {
 +    return total;
 +  }
 +
 +  private void g() {
 +    // calcula valor total
 +    File arch = File.open("arch1");
 +    arch.writeInt(total);
 +    ...
 +  }
 +}
 +</code>
 +
 +En esta nueva versión, la dependencia entre ''A'' y ''B'' se hace explícita. Ahora, ''B'' tiene un método público que devuelve el valor total. Y la clase ''A'' tiene una dependencia de la clase ''B'' a través de un parámetro del método ''f''. Este parámetro se usa para solicitar explícitamente el valor total llamando al método ''getTotal()''. Como este método fue declarado público en ''B'', se espera que el desarrollador de esta clase se esfuerce por no alterar su firma. Por lo tanto, en esta nueva versión, decimos que, aunque existe una dependencia de ''A'' hacia ''B'', el acoplamiento creado por ella es aceptable. Es decir, no es un acoplamiento que genere preocupaciones.
 +
 +Aún en relación con el ejemplo anterior, es interesante mencionar que, en la primera versión, el código de ''A'' no declara ninguna variable o atributo del tipo ''B''. Y, aun así, tenemos un acoplamiento no aceptable entre las clases. En la segunda versión, ocurre lo contrario, ya que el método ''A.f()'' declara un parámetro del tipo ''B''. Aun así, el acoplamiento entre las clases es de mejor calidad, ya que es más fácil estudiar y mantener el código de ''A'' sin conocer los detalles de ''B''.
 +
 +Algunos autores utilizan también los términos acoplamiento estructural y acoplamiento evolutivo (o lógico) con el siguiente significado:
 +
 +  * **Acoplamiento estructural** entre ''A'' y ''B'' ocurre cuando una clase ''A'' tiene una referencia explícita en su código a una clase ''B''. Por ejemplo, el acoplamiento entre ''Estacionamiento'' y ''Hashtable'' es estructural.
 +
 +  * **Acoplamiento evolutivo (o lógico)** entre ''A'' y ''B'' ocurre cuando los cambios en la clase ''B'' tienden a propagarse a la clase ''A''. En el ejemplo mencionado, en el que la clase ''A'' depende de un entero almacenado en un archivo interno de ''B'', no existe acoplamiento estructural entre ''A'' y ''B'', ya que ''A'' no declara ninguna variable del tipo ''B'', pero existe acoplamiento evolutivo. Por ejemplo, los cambios en el formato del archivo creado por ''B'' tendrán un impacto en ''A''.
 +
 +El acoplamiento estructural puede ser aceptable o no aceptable, dependiendo de la estabilidad de la interfaz de la clase de destino. El acoplamiento evolutivo, especialmente cuando cualquier cambio en ''B'' se propaga a la clase de origen ''A'', representa un acoplamiento no aceptable.
 +
 +Kent Beck, cuando trabajaba en Facebook, creó un glosario de términos relacionados con el diseño de software. En este glosario, acoplamiento se define de la siguiente forma [[https://engineering.fb.com/2011/09/27/uncategorized/software-design-glossary/|(enlace)]]:
 +
 +
 +<blockquote>
 +<color black>Dos elementos están acoplados cuando los cambios en un elemento requieren cambios en otro elemento. El acoplamiento puede dar lugar a una relación muy sutil entre clases, como observamos frecuentemente en Facebook. Ciertos eventos que interrumpen el funcionamiento de una parte del sistema suelen ser causados por pequeños fragmentos de acoplamiento que no se esperaban; por ejemplo, cambios en la configuración del sistema ''A'' causan un tiempo de espera en el sistema ''B'', lo que provoca una sobrecarga en el sistema ''C''. </color> 
 +</blockquote>
 +
 +La definición de acoplamiento propuesta por Beck — cuando los cambios en un elemento requieren cambios en otro elemento — corresponde a la definición de acoplamiento evolutivo. Es decir, parece que Beck no se preocupa por el acoplamiento aceptable (es decir, estructural y estable) entre dos clases; ya que, de hecho, no debería ser motivo de preocupación.
 +
 +El comentario también deja claro que el acoplamiento puede ser indirecto. Es decir, los cambios en A pueden propagarse a ''B'', y luego alcanzar a ''C''. En ese caso, ''C'' está acoplado a ''A'', pero de forma indirecta.
 +
 +**Mundo Real:** Un ejemplo de problema real causado por acoplamiento indirecto se conoció como el episodio de left-pad. En 2016, una disputa de derechos de autor motivó a un desarrollador a eliminar una de sus bibliotecas del repositorio npm, muy utilizado para almacenar y distribuir bibliotecas de node.js/JavaScript. La biblioteca eliminada — llamada left-pad — tenía una única función de JavaScript, llamada leftPad, con solo 11 líneas de código. Esta función rellenaba una cadena de caracteres con espacios a la izquierda. Por ejemplo, ''leftPad('foo', 5)'' devolvería ''' foo''', es decir, '''foo''' con dos espacios a la izquierda.
 +
 +Miles de sistemas web dependían de esta función trivial, pero la dependencia ocurría de manera indirecta. Los sistemas usaban npm para descargar dinámicamente el código JavaScript de una biblioteca B1, que a su vez dependía de una biblioteca B2 cuyo código también estaba en npm, y así sucesivamente, hasta alcanzar una biblioteca Bn que dependía de left-pad. Como resultado, todos los sistemas que dependían de left-pad — de forma directa o indirecta — quedaron fuera de servicio durante algunas horas, hasta que la biblioteca se volvió a insertar en npm. En resumen, los sistemas se vieron afectados por un problema en una biblioteca trivial, y no tenían la menor idea de que estaban acoplados a ella.
 +
 +
 +====== Principios de Diseño SOLID y Otros ======
 + 
 +Los principios de diseño son recomendaciones más concretas que los desarrolladores de software deben seguir para cumplir con las propiedades de diseño. Así, las propiedades de diseño pueden verse como recomendaciones aún genéricas (o tácticas), mientras que los principios ahora están en un nivel operativo.
 +
 +Estudiaremos los siete principios de diseño enumerados en la siguiente tabla. La tabla también muestra las propiedades de diseño que se contemplan al seguir cada uno de estos principios.
 +
 +^ Principios de Diseño ^ Propiedades de Diseño ^ 
 +| [[wiki:pru|Responsabilidad Única]]    | Cohesión   
 +| [[wiki:psi|Segregación de Interfaces]]    | Cohesión 
 +| [[wiki:pid|Inversión de Dependencias]]    | Acoplamiento  |
 +| [[wiki:ppch|Preferir Composición sobre Herencia]]    | Acoplamiento  |
 +| [[wiki:pd|Demeter]]  | Ocultamiento de la Información  |
 +| [[wiki:pac|Abierto/Cerrado]]  | Extensibilidad  |
 +| [[wiki:psl|Sustitución de Liskov]]  | Extensibilidad  |
 +
 +Cinco de los principios que vamos a estudiar son conocidos como los Principios SOLID, un acrónimo acuñado por Robert Martin y Michael Feathers [[https://dl.acm.org/doi/book/10.5555/3175742|(enlace)]]. Este acrónimo deriva de la letra inicial de cada principio en inglés:
 +
 +  * **S**ingle Responsibility Principle (Principio de Responsabilidad Única)
 +  * **O**pen/Closed Principle (Principio Abierto/Cerrado)
 +  * **L**iskov Substitution Principle (Principio de Sustitución de Liskov)
 +  * **I**nterface Segregation Principle (Principio de Segregación de Interfaces)
 +  * **D**ependency Inversion Principle (Principio de Inversión de Dependencias)
  
 +Los principios de diseño que vamos a estudiar tienen un punto en común: no solo buscan resolver un problema, sino también asegurar que la solución encontrada pueda mantenerse y evolucionar con éxito en el futuro. Los mayores problemas con los proyectos de software suelen ocurrir después de la implementación, cuando el sistema necesita ser mantenido. Normalmente, existe una tendencia a que este mantenimiento se vuelva gradualmente más lento, costoso y arriesgado. Por lo tanto, los principios de diseño que estudiaremos intentan reducir o posponer esta continua degradación de la calidad interna de los sistemas de software. En resumen, el objetivo no es solo entregar un diseño capaz de resolver un problema, sino también facilitar los mantenimientos futuros. Recuerda que la principal regla sobre los requisitos de software es que cambian con frecuencia. Lo mismo ocurre con las tecnologías de implementación, como bibliotecas y frameworks.
wiki/psp_1.1724356174.txt.gz · Last modified: 2024/08/22 15:49 by admin