Parece que Java ha tenido el poder de declarar clases no derivables durante siglos, y ahora C ++ también lo tiene. Sin embargo, a la luz del principio de Abrir / Cerrar en SOLID, ¿por qué sería útil? Para mí, la final
palabra clave suena igual friend
que: es legal, pero si la está usando, lo más probable es que el diseño sea incorrecto. Proporcione algunos ejemplos en los que una clase no derivable sea parte de una gran arquitectura o patrón de diseño.
54
final
? Mucha gente (incluyéndome a mí) considera que es un buen diseño hacer que cada clase no abstractafinal
.final
.Respuestas:
final
expresa la intención . Le dice al usuario de una clase, método o variable "Este elemento no debe cambiar, y si desea cambiarlo, no ha entendido el diseño existente".Esto es importante porque la arquitectura del programa sería muy, muy difícil si tuvieras que anticipar que cada clase y cada método que escribas podría cambiar para hacer algo completamente diferente por una subclase. Es mucho mejor decidir por adelantado qué elementos se supone que son cambiables y cuáles no, y hacer cumplir la inmutabilidad a través de
final
.También puede hacerlo a través de comentarios y documentos de arquitectura, pero siempre es mejor dejar que el compilador haga cumplir las cosas que puede que esperar que los futuros usuarios lean y obedezcan la documentación.
fuente
Evita el problema de la clase base frágil . Cada clase viene con un conjunto de garantías implícitas o explícitas e invariantes. El Principio de sustitución de Liskov exige que todos los subtipos de esa clase también deben proporcionar todas estas garantías. Sin embargo, es realmente fácil violar esto si no lo usamos
final
. Por ejemplo, tengamos un verificador de contraseña:Si permitimos que se anule esa clase, una implementación podría bloquear a todos, otra podría dar acceso a todos:
Esto generalmente no está bien, ya que las subclases ahora tienen un comportamiento que es muy incompatible con el original. Si realmente pretendemos que la clase se extienda con otro comportamiento, una Cadena de Responsabilidad sería mejor:
El problema se hace más evidente cuando una clase más complicada llama a sus propios métodos, y esos métodos pueden ser anulados. A veces me encuentro con esto cuando imprimo una estructura de datos o escribo HTML. Cada método es responsable de algún widget.
Ahora creo una subclase que agrega un poco más de estilo:
Ahora ignorando por un momento que esta no es una muy buena manera de generar páginas HTML, ¿qué sucede si quiero cambiar el diseño una vez más? Tendría que crear una
SpiffyPage
subclase que de alguna manera envuelva ese contenido. Lo que podemos ver aquí es una aplicación accidental del patrón del método de plantilla. Los métodos de plantilla son puntos de extensión bien definidos en una clase base que están destinados a ser anulados.¿Y qué pasa si la clase base cambia? Si el contenido HTML cambia demasiado, esto podría romper el diseño proporcionado por las subclases. Por lo tanto, no es realmente seguro cambiar la clase base después. Esto no es evidente si todas sus clases están en el mismo proyecto, pero es muy notable si la clase base es parte de algún software publicado sobre el que otras personas se basan.
Si se pretendía esta estrategia de extensión, podríamos haber permitido al usuario cambiar la forma en que se genera cada parte. O bien, podría haber una estrategia para cada bloque que se pueda proporcionar externamente. O podríamos anidar decoradores. Esto sería equivalente al código anterior, pero mucho más explícito y mucho más flexible:
(El
top
parámetro adicional es necesario para asegurarse de que las llamadaswriteMainContent
pasen por la parte superior de la cadena del decorador. Esto emula una característica de subclasificación llamada recursión abierta ).Si tenemos múltiples decoradores, ahora podemos mezclarlos más libremente.
Mucho más a menudo que el deseo de adaptar ligeramente la funcionalidad existente es el deseo de reutilizar alguna parte de una clase existente. He visto un caso en el que alguien quería una clase en la que pudieras agregar elementos e iterar sobre todos ellos. La solución correcta habría sido:
En cambio, crearon una subclase:
Esto de repente significa que toda la interfaz se
ArrayList
ha convertido en parte de nuestra interfaz. Los usuarios puedenremove()
cosas, oget()
cosas en índices específicos. ¿Esto fue pensado de esa manera? OKAY. Pero a menudo, no pensamos cuidadosamente en todas las consecuencias.Por lo tanto, es aconsejable
extend
una clase sin pensarlo detenidamente.final
excepto si tiene la intención de anular cualquier método.Hay muchos ejemplos en los que esta "regla" tiene que romperse, pero generalmente lo guía a un diseño bueno y flexible, y evita errores debido a cambios no intencionados en las clases base (o usos no intencionados de la subclase como una instancia de la clase base )
Algunos idiomas tienen mecanismos de aplicación más estrictos:
virtual
fuente
Me sorprende que nadie haya mencionado aún Effective Java, 2nd Edition de Joshua Bloch (que al menos debería requerirse la lectura para todos los desarrolladores de Java). El ítem 17 en el libro discute esto en detalle, y se titula: " Diseñe y documente la herencia o prohíbala ".
No repetiré todos los buenos consejos del libro, pero estos párrafos en particular parecen relevantes:
fuente
Una de las razones por las que
final
es útil es que se asegura de que no pueda subclasificar una clase de una manera que viole el contrato de la clase principal. Tal subclasificación sería una violación de SOLID (sobre todo "L") y hacer que una clase lofinal
impida.Un ejemplo típico es hacer que sea imposible subclasificar una clase inmutable de una manera que haga que la subclase sea mutable. En ciertos casos, tal cambio de comportamiento podría conducir a efectos muy sorprendentes, por ejemplo, cuando usa algo como claves en un mapa pensando que la clave es inmutable, mientras que en realidad está utilizando una subclase que es mutable.
En Java, podrían introducirse muchos problemas de seguridad interesantes si pudieras subclasificar
String
y hacerlo mutable (o hacer que volviera a casa cuando alguien llama a sus métodos, por lo que posiblemente extraiga datos confidenciales del sistema) a medida que se pasan estos objetos alrededor de algún código interno relacionado con la carga de clases y la seguridad.En ocasiones, Final también es útil para evitar errores simples, como reutilizar la misma variable para dos cosas dentro de un método, etc. En Scala, se recomienda usar solo
val
lo que corresponde aproximadamente a las variables finales en Java, y en realidad cualquier uso de unvar
o variable no final se mira con sospecha.Finalmente, los compiladores pueden, al menos en teoría, realizar algunas optimizaciones adicionales cuando saben que una clase o método es final, ya que cuando llamas a un método en una clase final sabes exactamente qué método se llamará y no tienes que ir a través de la tabla de métodos virtuales para verificar la herencia.
fuente
SecurityManager
. La mayoría de los programas no lo usan, pero tampoco ejecutan ningún código no confiable. +++ Debe suponer que las cadenas son inmutables ya que de lo contrario obtendrá cero seguridad y un montón de errores arbitrarios como bonificación. Cualquier programador que asuma que las cadenas de Java pueden cambiar en el código productivo seguramente es una locura.La segunda razón es el rendimiento . La primera razón es porque algunas clases tienen comportamientos o estados importantes que se supone que no deben modificarse para permitir que el sistema funcione. Por ejemplo, si tengo una clase "PasswordCheck" y para construir esa clase, he contratado a un equipo de expertos en seguridad y esta clase se comunica con cientos de cajeros automáticos con protocolos bien estudiados y definidos. Permitir que un nuevo empleado recién salido de la universidad haga una clase "TrustMePasswordCheck" que amplíe la clase anterior podría ser muy perjudicial para mi sistema; se supone que esos métodos no deben ser anulados, eso es todo.
fuente
Cuando necesito una clase, escribiré una clase. Si no necesito subclases, no me importan las subclases. Me aseguro de que mi clase se comporte según lo previsto, y los lugares donde uso la clase asumen que la clase se comporta según lo previsto.
Si alguien quiere subclasificar mi clase, quiero negar completamente cualquier responsabilidad por lo que sucede. Lo logro haciendo que la clase sea "final". Si desea subclasificarlo, recuerde que no tomé en cuenta la subclase mientras escribía la clase. Por lo tanto, debe tomar el código fuente de la clase, eliminar el "final" y, a partir de ese momento, todo lo que ocurra es totalmente su responsabilidad .
¿Crees que "no está orientado a objetos"? Me pagaron para hacer una clase que hace lo que se supone que debe hacer. Nadie me pagó por hacer una clase que podría subclasificarse. Si le pagan para que mi clase sea reutilizable, puede hacerlo. Comience por eliminar la palabra clave "final".
(Aparte de eso, "final" a menudo permite optimizaciones sustanciales. Por ejemplo, en Swift "final" en una clase pública, o en un método de una clase pública, significa que el compilador puede saber completamente qué código ejecutará una llamada a método, y puede reemplazar el despacho dinámico con despacho estático (beneficio minúsculo) y, a menudo, reemplazar el despacho estático con alineación (posiblemente un gran beneficio)).
adelphus: ¿Qué es tan difícil de entender acerca de "si quieres subclasificarlo, toma el código fuente, elimina el 'final' y es tu responsabilidad"? "final" es igual a "advertencia justa".
Y estoy no pagan para hacer que el código reutilizable. Me pagan para escribir código que haga lo que se supone que debe hacer. Si me pagan para hacer dos bits de código similares, extraigo las partes comunes porque es más barato y no me pagan para perder mi tiempo. Hacer que el código sea reutilizable y que no se reutilice es una pérdida de tiempo.
M4ks: Siempre haces que todo sea privado al que no se debe acceder desde el exterior. Nuevamente, si desea subclasificar, toma el código fuente, cambia las cosas a "protegido" si lo necesita, y asume la responsabilidad de lo que hace. Si cree que necesita acceder a cosas que marqué como privadas, es mejor que sepa lo que está haciendo.
Ambas: la subclasificación es una parte muy pequeña del código de reutilización. Crear bloques de construcción que se puedan adaptar sin subclasificar es mucho más poderoso y se beneficia enormemente de "final" porque los usuarios de los bloques pueden confiar en lo que obtienen.
fuente
Imaginemos que el SDK para una plataforma incluye la siguiente clase:
Una aplicación subclasifica esta clase:
Todo está bien y bien, pero alguien que trabaja en el SDK decide que pasar un método
get
es una tontería, y hace que la interfaz sea mejor asegurándose de aplicar la compatibilidad con versiones anteriores.Todo parece estar bien, hasta que la aplicación de arriba se recompila con el nuevo SDK. De repente, ya no se llama al método anulado get y no se cuenta la solicitud.
Esto se llama el problema de la clase base frágil, porque un cambio aparentemente inocente da como resultado una ruptura de la subclase. En cualquier momento, el cambio a los métodos que se llaman dentro de la clase puede provocar que se rompa una subclase. Eso tiende a significar que casi cualquier cambio puede causar que se rompa una subclase.
Final evita que alguien subclasifique su clase. De esa manera, qué métodos dentro de la clase se pueden cambiar sin preocuparse de que en algún lugar alguien dependa exactamente de qué método se realizan las llamadas.
fuente
Final efectivamente significa que su clase es segura para cambiar en el futuro sin afectar ninguna clase basada en herencia descendente (porque no hay ninguna), o cualquier problema relacionado con la seguridad de la clase de subprocesos (creo que hay casos en los que la palabra clave final en un campo impide algunos subprocesos basados en High-Jinx).
Final significa que usted es libre de cambiar la forma en que funciona su clase sin ningún cambio involuntario en el comportamiento que se arrastra en el código de otras personas que se basa en el suyo como base.
Como ejemplo, escribo una clase llamada HobbitKiller, que es genial, porque todos los hobbits son engañosos y probablemente deberían morir. Rasca eso, todos definitivamente necesitan morir.
Usas esto como una clase base y agregas un nuevo método increíble para usar un lanzallamas, pero usas mi clase como base porque tengo un excelente método para apuntar a los hobbits (además de ser engañosos, son rápidos), que lo usas para ayudar a apuntar tu lanzallamas.
Tres meses después, cambio la implementación de mi método de orientación. Ahora, en algún momento futuro cuando actualice su biblioteca, sin que usted lo sepa, la implementación de tiempo de ejecución real de su clase ha cambiado fundamentalmente debido a un cambio en el método de superclase del que depende (y generalmente no controla).
Por lo tanto, para ser un desarrollador concienzudo y asegurar la muerte sin problemas del hobbit en el futuro usando mi clase, tengo que tener mucho cuidado con los cambios que realice en las clases que se puedan extender.
Al eliminar la capacidad de extender, excepto en los casos en que tengo la intención específica de extender la clase, me ahorro (y espero que a otros) muchos dolores de cabeza.
fuente
final
El uso de
final
no es de ninguna manera una violación de los principios SÓLIDOS. Desafortunadamente, es extremadamente común interpretar el Principio Abierto / Cerrado ("las entidades de software deben estar abiertas para la extensión pero cerradas para la modificación") como "en lugar de modificar una clase, subclasificarla y agregar nuevas características". Esto no es lo que originalmente quería decir con él, y generalmente se considera que no es el mejor enfoque para lograr sus objetivos.La mejor manera de cumplir con OCP es diseñar puntos de extensión en una clase, proporcionando específicamente comportamientos abstractos que se parametrizan inyectando una dependencia al objeto (por ejemplo, utilizando el patrón de diseño de la Estrategia). Estos comportamientos deben diseñarse para usar una interfaz para que las nuevas implementaciones no dependan de la herencia.
Otro enfoque es implementar su clase con su API pública como una clase abstracta (o interfaz). Luego puede producir una implementación completamente nueva que puede conectarse a los mismos clientes. Si su nueva interfaz requiere un comportamiento ampliamente similar al original, puede:
fuente
Para mí es una cuestión de diseño.
Supongamos que tengo un programa que calcula los salarios de los empleados. Si tengo una clase que devuelve el número de días hábiles entre 2 fechas según el país (una clase para cada país), pondré esa final y proporcionaré un método para que cada empresa brinde un día libre solo para sus calendarios.
¿Por qué? Sencillo. Supongamos que un desarrollador quiere heredar la clase base WorkingDaysUSA en una clase WorkingDaysUSAmyCompany y modificarla para reflejar que su empresa estará cerrada por huelga / mantenimiento / cualquier motivo el 2 de marzo.
Los cálculos para los pedidos y la entrega de los clientes reflejarán el retraso y funcionarán en consecuencia cuando llamen a WorkingDaysUSAmyCompany.getWorkingDays () en tiempo de ejecución, pero ¿qué sucede cuando calculo el tiempo de vacaciones? ¿Debo agregar el 2do de Marte como día festivo para todos? No. Pero como el programador usó la herencia y no protegí a la clase, esto puede generar confusión.
O digamos que heredan y modifican la clase para reflejar que esta empresa no trabaja los sábados, en el país donde trabajan los sábados a medio tiempo. Luego, un terremoto, una crisis eléctrica o alguna circunstancia hace que el presidente declare 3 días no laborables como sucedió recientemente en Venezuela. Si el método de la clase heredada ya se resta cada sábado, mis modificaciones en la clase original podrían llevar a restar el mismo día dos veces. Tendría que ir a cada subclase en cada cliente y verificar que todos los cambios sean compatibles.
¿Solución? Haga que la clase sea final y proporcione un método addFreeDay (companyID mycompany, Date freeDay). De esa manera, está seguro de que cuando llama a una clase WorkingDaysCountry es su clase principal y no una subclase
fuente