¿La “composición sobre la herencia” está violando el “principio seco”?

36

Por ejemplo, considere que tengo una clase para que otras clases extiendan:

public class LoginPage {
    public String userId;
    public String session;
    public boolean checkSessionValid() {
    }
}

y algunas subclases:

public class HomePage extends LoginPage {

}

public class EditInfoPage extends LoginPage {

}

De hecho, la subclase no tiene ningún método para anular, tampoco accedería a HomePage de manera genérica, es decir: no haría algo como:

for (int i = 0; i < loginPages.length; i++) {
    loginPages[i].doSomething();
}

Solo quiero reutilizar la página de inicio de sesión. Pero de acuerdo con https://stackoverflow.com/a/53354 , preferiría la composición aquí porque no necesito la interfaz LoginPage, por lo que no uso la herencia aquí:

public class HomePage {
    public LoginPage loginPage;
}

public class EditInfoPage {
    public LoginPage loginPage;
}

pero el problema viene aquí, en la nueva versión, el código:

public LoginPage loginPage;

se duplica cuando se agrega una nueva clase. Y si LoginPage necesita setter y getter, se deben copiar más códigos:

public LoginPage loginPage;

private LoginPage getLoginPage() {
    return this.loginPage;
}
private void setLoginPage(LoginPage loginPage) {
    this.loginPage = loginPage;
}

Entonces mi pregunta es, ¿la "composición sobre la herencia" está violando el "principio seco"?

ocomfd
fuente
13
A veces no necesita herencia ni composición, lo que significa que a veces una clase con algunos objetos hace el trabajo.
Erik Eidt
23
¿Por qué quieres que todas tus páginas sean la página de inicio de sesión? Tal vez solo tenga una página de inicio de sesión.
Sr. Cochese
29
Pero, con la herencia, tienes duplicados por extends LoginPagetodas partes. ¡COMPRUEBE MATE!
el.pescado
2
Si tiene mucho problema en el último fragmento, es posible que esté utilizando en exceso getters y setters. Tienden a violar la encapsulación.
Brian McCutchon
44
Por lo tanto, haga que su página se pueda decorar y que sea LoginPageun decorador. No más duplicaciones, solo una simple page = new LoginPage(new EditInfoPage())y ya está. O usa el principio de abrir-cerrar para hacer que el módulo de autenticación se pueda agregar a cualquier página dinámicamente. Hay muchas maneras de lidiar con la duplicación de código, principalmente involucrando encontrar una nueva abstracción. LoginPagees probable que sea un mal nombre, lo que quiere asegurarse es que el usuario esté autenticado mientras navega por esa página, mientras se lo redirige LoginPageo se le muestra un mensaje de error adecuado si no lo está.
Polygnome

Respuestas:

46

Er espera, te preocupa que repitas

public LoginPage loginPage;

en dos lugares viola SECO? Por esa lógica

int x;

ahora solo puede existir en un objeto en toda la base de código. Bleh

SECO es algo bueno a tener en cuenta, pero vamos. Además

... extends LoginPage

se está duplicando en su alternativa, por lo que incluso ser anal sobre DRY no tiene sentido.

Las preocupaciones válidas de DRY tienden a enfocarse en el comportamiento idéntico que se necesita en múltiples lugares que se definen en múltiples lugares, de modo que la necesidad de cambiar este comportamiento termina requiriendo un cambio en varios lugares. Tome decisiones en un solo lugar y solo tendrá que cambiarlas en un solo lugar. No significa que solo un objeto pueda contener una referencia a su página de inicio de sesión.

SECO no debe seguirse a ciegas. Si está duplicando porque copiar y pegar es más fácil que pensar en un buen método o nombre de clase, entonces probablemente esté equivocado.

Pero si desea poner el mismo código en un lugar diferente porque ese lugar diferente está sujeto a una responsabilidad diferente y es probable que deba cambiar de forma independiente, entonces probablemente sea conveniente relajar su aplicación de DRY y permitir que este comportamiento idéntico tenga una identidad diferente . Es el mismo tipo de pensamiento que entra en prohibir los números mágicos.

DRY no se trata solo de cómo se ve el código. Se trata de no difundir los detalles de una idea con una repetición sin sentido que obliga a los encargados del mantenimiento a arreglar las cosas utilizando la repetición sin sentido. Es cuando tratas de decirte a ti mismo que la repetición sin sentido es solo tu convención que las cosas van mal de verdad.

De lo que creo que realmente estás tratando de quejarte se llama código repetitivo. Sí, usar composición en lugar de herencia exige un código repetitivo. Nada queda expuesto de forma gratuita, debe escribir un código que lo exponga. Con ese estándar viene la flexibilidad de estado, la capacidad de estrechar la interfaz expuesta, dar nombres diferentes a las cosas que sean apropiadas para su nivel de abstracción, buena indirecta, y está utilizando lo que está compuesto desde el exterior, no el adentro, así que te enfrentas a su interfaz normal.

Pero sí, es una gran cantidad de teclado adicional. Siempre que pueda evitar el problema del yoyo de rebotar hacia arriba y hacia abajo en una pila de herencia mientras leo el código, vale la pena.

Ahora no es que me niegue a usar la herencia. Uno de mis usos favoritos es dar nuevos nombres a las excepciones:

public class MyLoginPageWasNull extends NullPointerException{}
naranja confitada
fuente
int x;no puede existir más de cero veces en una base de código
K. Alan Bates
@ K.AlanBates disculpe ?
candied_orange
... Sabía que ibas a replicar con la clase Point jajaja. A nadie le importa la clase Point. Si entro en su base de código y veo int a; int b; int x;que presionaré para que 'usted' sea eliminado.
K. Alan Bates
@ K.AlanBates Wow. es triste que pienses que ese tipo de comportamiento te haría un buen programador. El mejor codificador no es el que puede encontrar la mayoría de las cosas sobre las que quejarse. Es el que hace que el resto sea mejor.
candied_orange
El uso de nombres sin sentido no es un comienzo y probablemente convierte al desarrollador de ese código en una responsabilidad en lugar de un activo.
K. Alan Bates
127

Un malentendido común con el principio DRY es que de alguna manera está relacionado con no repetir líneas de código. El principio DRY es "Cada conocimiento debe tener una representación autoritaria, inequívoca y única dentro de un sistema" . Se trata de conocimiento, no de código.

LoginPagesabe cómo dibujar la página para iniciar sesión. Si EditInfoPagesupiera cómo hacerlo, sería una violación. La inclusión a LoginPagetravés de la composición no constituye en modo alguno una violación del principio DRY.

El principio DRY es quizás el principio más mal utilizado en ingeniería de software y siempre debe considerarse no como un principio para no duplicar el código, sino como un principio para no duplicar el conocimiento abstracto del dominio. En realidad, en muchos casos, si aplica DRY correctamente , duplicará el código , y esto no es necesariamente algo malo.

wasatz
fuente
27
El "principio de responsabilidad única" se usa mucho más mal. "No te repitas" podría entrar fácilmente como número dos :-)
gnasher729
28
"DRY" es un acrónimo útil para "Don't Repeat Yourself", que es una frase clave, pero no el nombre del principio, el nombre real del principio es "Once And Only Once", que a su vez es un nombre pegadizo para un principio que requiere un par de párrafos para describirlo. Lo importante es entender los dos párrafos, no memorizar las tres letras D, R e Y.
Jörg W Mittag
44
@ gnasher729 Sabes, de hecho estoy de acuerdo contigo en que SRP probablemente se usa mucho más mal. Aunque para ser justos, en muchos casos creo que a menudo se usan mal juntos. Mi teoría es que no se debe confiar en los programadores en general para usar siglas con "nombres fáciles".
wasatz
17
En mi humilde opinión, esta es una buena respuesta. Sin embargo, para ser justos: según mi experiencia, la forma más frecuente de violaciones de DRY es causada por la programación de copiar y pegar y simplemente duplicar el código. Alguien me dijo una vez "cada vez que vaya a copiar y pegar un código, piénselo dos veces si la parte duplicada no se puede extraer en una función común", lo cual fue un muy buen consejo en mi humilde opinión.
Doc Brown
99
El abuso de copiar y pegar es ciertamente una fuerza impulsora para violar DRY, pero simplemente prohibir copiar y pegar es una mala guía para seguir DRY. No sentiría ningún remordimiento por tener dos métodos con código idéntico cuando esos métodos representan diferentes conocimientos. Sirven dos responsabilidades diferentes. Simplemente tienen un código idéntico hoy. Deben ser libres de cambiar de forma independiente. Sí, conocimiento, no código. Así poner. Escribí una de las otras respuestas, pero me inclinaré ante esta. +1.
candied_orange
12

Respuesta corta: sí, en cierto grado menor y aceptable.

A primera vista, la herencia a veces puede ahorrarle algunas líneas de código, ya que tiene el efecto de decir "mi clase de reutilización contendrá todos los métodos y atributos públicos de una manera 1: 1". Entonces, si hay una lista de 10 métodos en un componente, uno no tiene que repetirlos en código en la clase heredada. Cuando en un escenario de composición, 9 de esos 10 métodos deben exponerse públicamente a través de un componente de reutilización, entonces uno debe anotar 9 llamadas delegadas y dejar salir el restante, no hay forma de evitarlo.

¿Por qué es esto tolerable? Mire los métodos que se replican en un escenario de composición: estos métodos delegan exclusivamente las llamadas a la interfaz del componente, por lo que no contienen lógica real.

El corazón del principio DRY es evitar tener dos lugares en el código donde se codifican las mismas reglas lógicas , porque cuando estas reglas lógicas cambian, en el código no DRY es fácil adaptar uno de esos lugares y olvidarse del otro, que introduce un error

Pero dado que delegar llamadas no contiene lógica, estas normalmente no están sujetas a tal cambio, por lo que no causan un problema real cuando se "prefiere la composición sobre la herencia". E incluso si la interfaz del componente cambia, lo que podría inducir cambios formales en todas las clases que usan el componente, en un lenguaje compilado el compilador nos dirá cuándo olvidamos cambiar una de las personas que llaman.

Una nota a su ejemplo: no sé cómo son su aspecto HomePagey el suyo EditInfoPage, pero si tienen la funcionalidad de inicio de sesión y un HomePage(o un EditInfoPage) es un LoginPage , entonces la herencia podría ser la herramienta correcta aquí. Un ejemplo menos discutible, donde la composición será la mejor herramienta de una manera más obvia, probablemente aclararía las cosas.

Suponiendo que HomePageni lo EditInfoPageson ni uno LoginPage, y uno quiere reutilizar este último, como usted escribió, es muy probable que uno requiera solo algunas partes del LoginPage, no todo. Si ese es el caso, un mejor enfoque que usar la composición de la manera mostrada podría ser

  • extraer la parte reutilizable de LoginPageen un componente propio

  • reutilice ese componente HomePagey de EditInfoPagela misma manera que se usa ahora dentroLoginPage

De esa manera, por lo general, se volverá mucho más claro por qué y cuándo la composición sobre la herencia es el enfoque correcto.

Doc Brown
fuente