¿Qué significa cuando uno dice "Encapsular lo que varía"?

25

Uno de los principios de OOP que encontré es: -Encapsular lo que varía.

Entiendo cuál es el significado literal de la frase, es decir, ocultar lo que varía. Sin embargo, no sé cómo contribuiría exactamente a un mejor diseño. ¿Alguien puede explicarlo con un buen ejemplo?

Haris Ghauri
fuente
Ver en.wikipedia.org/wiki/Encapsulation_(computer_programming) que lo explica bien. Creo que "lo que varía" no es correcto ya que a veces también debería encapsular constantes.
qwerty_so
I don't know how exactly would it contribute to a better designLos detalles de encapsulación tratan sobre el acoplamiento flojo entre el "modelo" y los detalles de implementación. Cuanto menos ligado esté el "modelo" a los detalles de implementación, más flexible será la solución. Y hace que sea más fácil evolucionarlo. "Resumen de los detalles".
Laiv
@Laiv ¿Entonces "varía" se refiere a lo que evoluciona durante el ciclo de vida de su software o qué cambios durante la ejecución de su programa o ambos?
Haris Ghauri
2
@HarisGhauri ambos. Agrupe lo que varía. Aislar lo que varía de forma independiente. Sospeche de lo que supone que nunca cambiará.
candied_orange
1
@laiv pensando "abstracto" es un buen punto. Puede ser abrumador hacer eso. En cualquier objeto se supone que tienes una responsabilidad. Lo bueno de eso es que solo tienes que pensar cuidadosamente sobre esa única cosa aquí. Cuando los detalles del resto del problema son el problema de alguien más, esto facilita las cosas.
candied_orange

Respuestas:

30

Puede escribir código que se vea así:

if (pet.type() == dog) {
  pet.bark();
} else if (pet.type() == cat) {
  pet.meow();
} else if (pet.type() == duck) {
  pet.quack()
}

o puedes escribir código que se vea así:

pet.speak();

Si lo que varía está encapsulado, entonces no tiene que preocuparse por eso. Solo te preocupas por lo que necesitas y lo que sea que estés usando descubre cómo hacer lo que realmente necesitas según lo variado.

Encapsula lo que varía y no tienes que distribuir código que se preocupe por lo que varía. Simplemente configura la mascota como un cierto tipo que sabe hablar como ese tipo y luego puede olvidar qué tipo y tratarlo como una mascota. No tienes que preguntar qué tipo.

Puede pensar que el tipo está encapsulado porque se requiere un captador para acceder a él. Yo no. Getter realmente no encapsula. Simplemente chirrían cuando alguien rompe tu encapsulación. Son un buen decorador como un gancho orientado a aspectos que se usa con mayor frecuencia como código de depuración. No importa cómo lo corte, todavía está exponiendo el tipo.

Podrías mirar este ejemplo y pensar que estoy combinando polimorfismo y encapsulación. No soy. Estoy combinando "lo que varía" y "detalles".

El hecho de que tu mascota sea un perro es un detalle. Uno que puede variar para ti. Uno que tal vez no. Pero ciertamente uno que puede variar de persona a persona. A menos que creamos que este software solo será utilizado por los amantes de los perros, es inteligente tratar al perro como un detalle y encapsularlo. De esa forma, algunas partes del sistema desconocen felizmente al perro y no se verán afectadas cuando nos unamos con "los loros somos nosotros".

Desacoplar, separar y ocultar detalles del resto del código. No permita que el conocimiento de los detalles se extienda a través de su sistema y seguirá "encapsular lo que varía" perfectamente.

naranja confitada
fuente
3
Eso es realmente extraño. "Encapsular lo que varía" para mí significa ocultar los cambios de estado, por ejemplo, nunca tener variables globales. Pero su respuesta también tiene sentido, incluso si se siente más una respuesta al polimorfismo que la encapsulación :)
David Arno
2
@DavidArno Polymorphism es una forma de hacer que esto funcione. Podría haber convertido la estructura if en una mascota y las cosas se verían bien aquí gracias a la encapsulación de la mascota. Pero eso solo sería mover el desastre en lugar de limpiarlo.
candied_orange
1
"Encapsular lo que varía" para mí significa ocultar los cambios de estado . No, no. Me gusta el comentario de CO. La respuesta de Derick Elkin es más profunda, léala más de una vez. Como dijo @JacquesB "Este principio es realmente bastante profundo"
radarbob
16

"Varía" aquí significa "puede cambiar con el tiempo debido a los requisitos cambiantes". Este es un principio de diseño central: separar y aislar piezas de código o datos que pueden tener que cambiar por separado en el futuro. Si cambia un solo requisito, idealmente debería exigirnos que cambiemos el código relacionado en un solo lugar. Pero si la base del código está mal diseñada, es decir, altamente interconectada y la lógica del requisito se extiende en muchos lugares, entonces el cambio será difícil y tendrá un alto riesgo de causar efectos inesperados.

Supongamos que tiene una aplicación que utiliza el cálculo del impuesto sobre las ventas en muchos lugares. Si la tasa del impuesto sobre las ventas cambia, ¿qué preferiría?

  • la tasa del impuesto sobre las ventas es un literal codificado en todas partes de la aplicación donde se calcula el impuesto sobre las ventas.

  • la tasa del impuesto sobre las ventas es una constante global, que se utiliza en todas partes en la aplicación donde se calcula el impuesto sobre las ventas.

  • Hay un único método llamado, calculateSalesTax(product)que es el único lugar donde se utiliza la tasa de impuesto a las ventas.

  • la tasa de impuesto a las ventas se especifica en un archivo de configuración o campo de base de datos.

Dado que la tasa del impuesto sobre las ventas puede cambiar debido a una decisión política independiente de otros requisitos, preferimos tenerla aislada en una configuración, para que pueda cambiarse sin afectar ningún código. Pero también es concebible que la lógica para calcular el impuesto sobre las ventas pueda cambiar, por ejemplo, diferentes tasas para diferentes productos, por lo que también nos gusta encapsular la lógica de cálculo. La constante global puede parecer una buena idea, pero en realidad es mala, ya que puede alentar el uso del impuesto a las ventas en diferentes lugares del programa en lugar de en un solo lugar.

Ahora considere otra constante, Pi, que también se usa en muchos lugares del código. ¿Se cumple el mismo principio de diseño? No, porque Pi no va a cambiar. Extraerlo a un archivo de configuración o campo de base de datos simplemente introduce una complejidad innecesaria (y todo lo demás es igual, preferimos el código más simple). Tiene sentido convertirlo en una constante global en lugar de codificarlo en varios lugares para evitar inconsistencias y mejorar la legibilidad.

El punto es que si solo miramos cómo funciona el programa ahora , la tasa de impuesto a las ventas y Pi son equivalentes, ambos son constantes. Solo cuando consideramos lo que puede variar en el futuro , nos damos cuenta de que debemos tratarlos de manera diferente en el diseño.

Este principio es en realidad bastante profundo, porque significa que debe mirar más allá de lo que se supone que debe hacer la base de código hoy en día , y también considerar las fuerzas externas que pueden hacer que cambie, e incluso comprender a las diferentes partes interesadas detrás de los requisitos.

JacquesB
fuente
2
Los impuestos son un buen ejemplo. Los cálculos de leyes e impuestos pueden cambiar de un día para otro. Si está implementando un sistema de declaración de impuestos, está fuertemente vinculado a este tipo de cambios. También cambia de un lugar a otro (países, provincias, ...)
Laiv
"Pi no va a cambiar" me hizo reír. Es cierto, es poco probable que cambie Pi, pero ¿y si ya no pudieras usarlo? Si algunas personas se salen con la suya, Pi será desaprobado. ¿Y si se convierte en un requisito? Espero que tengas un feliz día de Tau . Buena respuesta por cierto. Profundo de hecho.
candied_orange
14

Ambas respuestas actuales parecen dar solo en el blanco, y se centran en ejemplos que nublan la idea central. Esto tampoco es (únicamente) un principio de OOP sino un principio de diseño de software en general.

Lo que "varía" en esta frase es el código. Christophe está a punto de decir que generalmente es algo que puede variar, es decir, a menudo lo anticipa . El objetivo es protegerse de futuros cambios en el código. Esto está estrechamente relacionado con la programación en una interfaz . Sin embargo, Christophe es incorrecto al limitar esto a "detalles de implementación". De hecho, el valor de este consejo a menudo se debe a cambios en los requisitos .

Esto solo está indirectamente relacionado con el estado de encapsulación, que es lo que creo que está pensando David Arno. Este consejo no siempre (pero a menudo lo hace) sugiere un estado encapsulado, y este consejo se aplica también a los objetos inmutables. De hecho, simplemente nombrar constantes es una forma (muy básica) de encapsular lo que varía.

CandiedOrange combina explícitamente "lo que varía" con "detalles". Esto es solo parcialmente correcto. Estoy de acuerdo en que cualquier código que varía es "detalles" en algún sentido, pero un "detalle" puede no variar (a menos que defina "detalles" para hacer esto tautológico). Puede haber razones para encapsular detalles que no varían, pero este dictamen no es uno. Hablando en términos generales, si estaba muy seguro de que "perro", "gato" y "pato" serían los únicos tipos con los que tendría que tratar, entonces este dictamen no sugiere la refactorización de CandiedOrange.

Casting del ejemplo de CandiedOrange en un contexto diferente, suponga que tenemos un lenguaje de procedimiento como C. Si tengo algún código que contenga:

if (pet.type() == dog) {
  pet.bark();
} else if (pet.type() == cat) {
  pet.meow();
} else if (pet.type() == duck) {
  pet.quack()
}

Puedo esperar razonablemente que este código cambie en el futuro. Puedo "encapsularlo" simplemente definiendo un nuevo procedimiento:

void speak(pet) {
  if (pet.type() == dog) {
    pet.bark();
  } else if (pet.type() == cat) {
    pet.meow();
  } else if (pet.type() == duck) {
    pet.quack()
  }
}

y usar este nuevo procedimiento en lugar del bloque de código (es decir, una refactorización de "método de extracción") En este punto, agregar un tipo de "vaca" o lo que sea, solo requiere actualizar el speakprocedimiento. Por supuesto, en un lenguaje OO, en su lugar, puede aprovechar el despacho dinámico como se alude a la respuesta de CandiedOrange. Esto sucederá naturalmente si accede a pettravés de una interfaz. La eliminación de la lógica condicional a través del despacho dinámico es una preocupación ortogonal que fue parte de por qué hice esta representación procesal. También quiero enfatizar que esto no requiere características particulares de OOP. Incluso en un lenguaje OO, encapsular lo que varía no significa necesariamente que deba crearse una nueva clase o interfaz.

Como un ejemplo más arquetípico (que está más cerca pero no del todo OO), digamos que queremos eliminar los duplicados de una lista. Digamos que lo implementamos iterando sobre la lista haciendo un seguimiento de los elementos que hemos visto hasta ahora en otra lista y eliminando cualquier elemento que hayamos visto. Es razonable suponer que es posible que queramos cambiar la forma en que hacemos un seguimiento de los elementos vistos por, al menos, razones de rendimiento. El dictamen para encapsular lo que varía sugiere que deberíamos construir un tipo de datos abstractos para representar el conjunto de elementos vistos. Nuestro algoritmo ahora se define en contra de este tipo de conjunto de datos abstracto, y si decidimos cambiar a un árbol de búsqueda binario, nuestro algoritmo no necesita cambiar ni importar. En un lenguaje OO, podemos usar una clase o interfaz para capturar este tipo de datos abstractos. En un lenguaje como SML / O '

Para un ejemplo basado en requisitos, supongamos que necesita validar algún campo con respecto a alguna lógica empresarial. Si bien es posible que ahora tenga requisitos específicos, sospecha que evolucionarán. Puede encapsular la lógica actual en su propio procedimiento / función / regla / clase.

Aunque esta es una preocupación ortogonal que no forma parte de "encapsular lo que varía", a menudo es natural abstraerse, es decir, parametrizarse por la lógica ahora encapsulada. Esto generalmente conduce a un código más flexible y permite cambiar la lógica al sustituir en una implementación alternativa en lugar de modificar la lógica encapsulada.

Derek Elkins
fuente
Oh amarga dulce ironía. Sí, esto no es únicamente un problema de OOP. Me sorprendiste dejando que un paradigma del lenguaje contaminase mi respuesta y me castigaste con razón al "variar" el paradigma.
candied_orange
"Incluso en un lenguaje OO, encapsular lo que varía no significa necesariamente que deba crearse una nueva clase o interfaz". Es difícil imaginar una situación en la que no crear una nueva clase o interfaz no violaría SRP
taurelas
11

"Encapsular lo que varía" se refiere a la ocultación de los detalles de implementación que pueden cambiar y evolucionar.

Ejemplo:

Por ejemplo, supongamos que la clase Courserealiza un seguimiento de Studentsque puede registrarse (). Puede implementarlo con a LinkedListy exponer el contenedor para permitir la iteración en él:

class Course { 
    public LinkedList<Student> Atendees; 
    public bool register (Student s);  
    ...
}

Pero esta no es una buena idea:

  • Primero, la gente podría carecer de buen comportamiento y usarlo como autoservicio, agregando directamente estudiantes a la lista, sin pasar por el método register ().
  • Pero aún más molesto: esto crea una dependencia del "código de uso" a los detalles de implementación internos de la clase utilizada. Esto podría evitar futuras evoluciones de la clase, por ejemplo, si prefiere utilizar una matriz, un vector, un mapa con el número de asiento o su propia estructura de datos persistentes.

Si encapsulas lo que varía (o mejor dicho, lo que puede variar), mantienes la libertad para que tanto el código que usa como la clase encapsulada evolucionen entre sí por sí mismos. Por eso es un principio importante en OOP.

Lectura adicional:

Christophe
fuente