tutorials tutorial syllabus guide docs certification book java design ocp

tutorial - Principio abierto-cerrado y modificador "final" de Java



syllabus java certification (7)

El principio de apertura-cierre establece que "las entidades de software (clases, módulos, funciones, etc.) deben estar abiertas para extensión, pero cerradas para modificación".

Sin embargo, Joshua Bloch en su famoso libro "Java efectiva" da el siguiente consejo: "Diseñe y documente para herencia, o bien prohíba", y alienta a los programadores a usar el modificador "final" para prohibir las subclases.

Creo que estos dos principios claramente se contradicen entre sí (¿Me equivoco?). ¿Qué principio sigues al escribir tu código y por qué? ¿Dejas tus clases abiertas, no permites la herencia de algunas de ellas (¿cuáles?), O usas el modificador final siempre que sea posible?


Francamente creo que el principio abierto / cerrado es más un anacronismo que no. Semis desde los años 80 y 90 cuando los marcos OO se construyeron sobre el principio de que todo debe heredar de otra cosa y que todo debe ser subclasificable.

Esto fue más tipificado en los marcos de UI de la era como MFC y Java Swing. En Swing, tiene una herencia ridícula donde el botón (iirc) extiende la casilla de verificación (o al revés) dando a uno de ellos un comportamiento que no se usa (creo que es la llamada setDisabled () en la casilla de verificación). ¿Por qué comparten una ascendencia? No hay otra razón más que, bueno, tenían algunos métodos en común.

La composición de estos días se favorece sobre la herencia. Mientras que Java permitía la herencia por defecto, .Net adoptó el enfoque (más moderno) de no permitirlo por defecto, lo que creo es más correcto (y más consistente con los principios de Josh Bloch).

DI / IoC también han hecho el caso de la composición.

Josh Bloch también señala que la herencia rompe la encapsulación y da algunos buenos ejemplos de por qué. También se ha demostrado que cambiar el comportamiento de las colecciones de Java es más consistente si se hace por delegación en lugar de ampliar las clases.

En lo personal, en gran medida veo la herencia como poco más que un detalle implícito en estos días.


Hoy en día utilizo el modificador final de forma predeterminada, casi por reflejo como parte de la plantilla. Hace las cosas más fáciles de razonar, cuando sabes que un método dado siempre funcionará como se ve en el código que estás viendo en este momento.

Por supuesto, a veces hay situaciones en las que una jerarquía de clases es exactamente lo que quieres, y sería tonto no usar una en ese momento. Pero tenga miedo de las jerarquías de más de dos niveles, o de aquellas en las que las clases no abstractas están subclasificadas. Una clase debe ser abstracta o final.

La mayoría de las veces, usar la composición es el camino a seguir. Ponga toda la maquinaria común en una clase, ponga los casos diferentes en clases diferentes, luego componga las instancias para que funcionen completas.

Puede llamarlo "inyección de dependencia", "patrón de estrategia" o "patrón de visitante" o lo que sea, pero a lo que se reduce es a usar la composición en lugar de la herencia para evitar la repetición.


Las dos afirmaciones

Las entidades de software (clases, módulos, funciones, etc.) deben estar abiertas para extensión, pero cerradas para modificación.

y

Diseñar y documentar para herencia, o bien prohibirlo.

no están en directa contradicción entre sí. Puede seguir el principio de abrir y cerrar siempre que lo diseñe y documente (según el consejo de Bloch).

No creo que Bloch declare que debería preferir prohibir la herencia mediante el modificador final, solo que debe elegir explícitamente permitir o rechazar la herencia en cada clase que cree. Su consejo es que debes pensarlo y decidir por ti mismo, en lugar de simplemente aceptar el comportamiento predeterminado del compilador.


No creo que el principio Abierto / cerrado, tal como se presentó originalmente, permita interpretar que las clases finales se pueden extender mediante la inyección de dependencias.

A mi entender, el principio consiste en no permitir cambios directos en el código que se ha puesto en producción, y la forma de lograrlo mientras aún se permiten modificaciones en la funcionalidad es usar la herencia de implementación.

Como se señaló en la primera respuesta, esto tiene raíces históricas. Hace décadas, la herencia estaba a favor, las pruebas del desarrollador eran desconocidas y la compilación de la base de código a menudo tomaba demasiado tiempo.

Además, tenga en cuenta que en C ++ los detalles de implementación de una clase (en particular, los campos privados) se expusieron comúnmente en el archivo de encabezado ".h", por lo que si un programador tuviera que cambiarlo, todos los clientes necesitarían una recompilación. Tenga en cuenta que este no es el caso con lenguajes modernos como Java o C #. Además, no creo que los desarrolladores en ese entonces pudieran contar con IDE sofisticados capaces de realizar análisis de dependencia sobre la marcha, evitando la necesidad de reconstrucciones completas frecuentes.

En mi propia experiencia, prefiero hacer exactamente lo contrario: "las clases deben estar cerradas por extensión ( final ) por defecto, pero abiertas para modificación". Piénselo: hoy favorecemos prácticas como el control de versiones (hace que sea más fácil recuperar / comparar versiones anteriores de una clase), refactorizar (lo que nos alienta a modificar el código para mejorar el diseño o como preludio a la introducción de nuevas características), y desarrollador Pruebas , que proporciona una red de seguridad al modificar el código existente.


No creo que las dos afirmaciones se contradigan. Un tipo puede estar abierto para extensión y aún estar cerrado para herencia.

Una forma de hacer esto es emplear la inyección de dependencia. En lugar de crear instancias de sus propios tipos de ayuda, un tipo puede tener estos suministrados en la creación. Esto le permite cambiar las partes (es decir, abrir para extensión) del tipo sin cambiar el tipo en sí (es decir, cerrar para modificar).


Respeto mucho a Joshua Bloch, y considero que Eficaz Java es prácticamente la biblia de Java . Pero creo que el hecho de omitir automáticamente private acceso private suele ser un error. Tiendo a hacer que las cosas estén protected por defecto para que al menos se pueda acceder a ellas extendiendo la clase. Esto se debió principalmente a la necesidad de componentes de prueba unitaria, pero también me parece útil para anular el comportamiento predeterminado de las clases. Me resulta muy molesto cuando trabajo en el código base de mi propia empresa y tengo que copiar y modificar la fuente porque el autor eligió "ocultar" todo. Si es que está en mi poder, hago cabildeo para que se cambie el acceso a protected para evitar la duplicación, que es mucho peor en mi humilde opinión.

También tenga en cuenta que los antecedentes de Bloch están en el diseño de bibliotecas API públicas de roca de fondo; la barra para obtener dicho código "correcto" debe ser muy alta, por lo que es probable que no sea la misma situación que la mayoría de los códigos que escribirá. Las bibliotecas importantes, como el JRE en sí, tienden a ser más restrictivas para garantizar que no se abusa del idioma. ¿Ver todas las API en desuso en el JRE? Es casi imposible cambiarlos o eliminarlos. Su código base probablemente no está escrito en piedra, por lo que tiene la oportunidad de arreglar las cosas si resulta que inicialmente cometió un error.


En principio abierto-cerrado (abierto por extensión, cerrado por modificación) todavía puede usar el modificador final. Aquí hay un ejemplo:

public final class ClosedClass { private IMyExtension myExtension; public ClosedClass(IMyExtension myExtension) { this.myExtension = myExtension; } // methods that use the IMyExtension object } public interface IMyExtension { public void doStuff(); }

ClosedClass está cerrado por modificación dentro de la clase, pero está abierto para extensión a través de otra. En este caso, puede ser de cualquier cosa que implemente la interfaz IMyExtension . Este truco es una variación de la inyección de dependencia, ya que estamos alimentando a la clase cerrada con otra, en este caso a través del constructor. Dado que la extensión es una interface no puede ser final pero sí su clase de implementación.

Usar el final en las clases para cerrarlas en java es similar al uso sealed en C #. Hay discusiones similares al respecto en el lado de .NET.