¿Por qué sería útil la palabra clave 'final'?

54

Parece que Java ha tenido el poder de declarar clases como no derivables por mucho tiempo, y ahora C ++ también lo tiene. Sin embargo, a la luz del principio Open / Close en SOLID, ¿por qué sería útil? Para mí, la palabra clave final suena como friend : es legal, pero si la está utilizando, probablemente el diseño sea incorrecto. Proporcione algunos ejemplos en los que una clase no derivable formaría parte de una gran arquitectura o patrón de diseño.

    
pregunta Vorac 12.05.2016 - 10:33

10 respuestas

134

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 todas las clases y todos los métodos que hayas escrito podrían cambiarse para hacer algo completamente diferente por una subclase. Es mucho mejor decidir de antemano qué elementos se supone que deben cambiarse y cuáles no, y hacer cumplir la inmutabilidad mediante final .

También puede hacer esto a través de comentarios y documentos de arquitectura, pero siempre es mejor dejar que el compilador aplique cosas que pueda que esperar que los futuros usuarios lean y obedezcan la documentación.

    
respondido por el Kilian Foth 12.05.2016 - 10:48
58

Evita el Problema de clase base frágil . Cada clase viene con un conjunto de garantías e invariantes implícitas o explícitas. 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 usamos final . Por ejemplo, tengamos un verificador de contraseñas:

public class PasswordChecker {
  public boolean passwordIsOk(String password) {
    return password == "s3cret";
  }
}

Si permitimos que esa clase se anule, una implementación podría bloquear a todos, otra podría dar acceso a todos:

public class OpenDoor extends PasswordChecker {
  public boolean passwordIsOk(String password) {
    return true;
  }
}

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:

PasswordChecker passwordChecker =
  new DefaultPasswordChecker(null);
// or:
PasswordChecker passwordChecker =
  new OpenDoor(null);
// or:
PasswordChecker passwordChecker =
 new DefaultPasswordChecker(
   new OpenDoor(null)
 );

public interface PasswordChecker {
  boolean passwordIsOk(String password);
}

public final class DefaultPasswordChecker implements PasswordChecker {
  private PasswordChecker next;

  public DefaultPasswordChecker(PasswordChecker next) {
    this.next = next;
  }

  @Override
  public boolean passwordIsOk(String password) {
    if ("s3cret".equals(password)) return true;
    if (next != null) return next.passwordIsOk(password);
    return false;
  }
}

public final class OpenDoor implements PasswordChecker {
  private PasswordChecker next;

  public OpenDoor(PasswordChecker next) {
    this.next = next;
  }

  @Override
  public boolean passwordIsOk(String password) {
    return true;
  }
}

El problema se vuelve más evidente cuando una clase más complicada llama a sus propios métodos, y esos métodos pueden ser anulados. A veces encuentro esto cuando imprimo bastante una estructura de datos o escribe HTML. Cada método es responsable de algún widget.

public class Page {
  ...;

  @Override
  public String toString() {
    PrintWriter out = ...;
    out.print("<!DOCTYPE html>");
    out.print("<html>");

    out.print("<head>");
    out.print("</head>");

    out.print("<body>");
    writeHeader(out);
    writeMainContent(out);
    writeMainFooter(out);
    out.print("</body>");

    out.print("</html>");
    ...
  }

  void writeMainContent(PrintWriter out) {
    out.print("<div class='article'>");
    out.print(htmlEscapedContent);
    out.print("</div>");
  }

  ...
}

Ahora creo una subclase que agrega un poco más de estilo:

class SpiffyPage extends Page {
  ...;


  @Override
  void writeMainContent(PrintWriter out) {
    out.print("<div class='row'>");

    out.print("<div class='col-md-8'>");
    super.writeMainContent(out);
    out.print("</div>");

    out.print("<div class='col-md-4'>");
    out.print("<h4>About the Author</h4>");
    out.print(htmlEscapedAuthorInfo);
    out.print("</div>");

    out.print("</div>");
  }
}

Ahora, ignorando por un momento que esta no es una buena forma de generar páginas HTML, ¿qué sucede si deseo cambiar el diseño una vez más? Tendría que crear una subclase SpiffyPage que de alguna manera envuelva ese contenido. Lo que podemos ver aquí es una aplicación accidental del patrón de 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 los contenidos HTML cambian demasiado, esto podría romper el diseño provisto 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 que otras personas construyen.

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 a los decoradores. Esto sería equivalente al código anterior, pero mucho más explícito y mucho más flexible:

Page page = ...;
page.decorateLayout(current -> new SpiffyPageDecorator(current));
print(page.toString());

public interface PageLayout {
  void writePage(PrintWriter out, PageLayout top);
  void writeMainContent(PrintWriter out, PageLayout top);
  ...
}

public final class Page {
  private PageLayout layout = new DefaultPageLayout();

  public void decorateLayout(Function<PageLayout, PageLayout> wrapper) {
    layout = wrapper.apply(layout);
  }

  ...
  @Override public String toString() {
    PrintWriter out = ...;
    layout.writePage(out, layout);
    ...
  }
}

public final class DefaultPageLayout implements PageLayout {
  @Override public void writeLayout(PrintWriter out, PageLayout top) {
    out.print("<!DOCTYPE html>");
    out.print("<html>");

    out.print("<head>");
    out.print("</head>");

    out.print("<body>");
    top.writeHeader(out, top);
    top.writeMainContent(out, top);
    top.writeMainFooter(out, top);
    out.print("</body>");

    out.print("</html>");
  }

  @Override public void writeMainContent(PrintWriter out, PageLayout top) {
    ... /* as above*/
  }
}

public final class SpiffyPageDecorator implements PageLayout {
  private PageLayout inner;

  public SpiffyPageDecorator(PageLayout inner) {
    this.inner = inner;
  }

  @Override
  void writePage(PrintWriter out, PageLayout top) {
    inner.writePage(out, top);
  }

  @Override
  void writeMainContent(PrintWriter out, PageLayout top) {
    ...
    inner.writeMainContent(out, top);
    ...
  }
}

(El parámetro top adicional es necesario para asegurarse de que las llamadas a writeMainContent pasan por la parte superior de la cadena de decoradores. Esto emula una característica de subclasificación llamada recursión abierta .)

Si tenemos varios decoradores, ahora podemos mezclarlos más libremente.

Mucho más frecuente 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 pudiera agregar elementos e iterarlos todos. La solución correcta habría sido:

final class Thingies implements Iterable<Thing> {
  private ArrayList<Thing> thingList = new ArrayList<>();

  @Override public Iterator<Thing> iterator() {
    return thingList.iterator();
  }

  public void add(Thing thing) {
    thingList.add(thing);
  }

  ... // custom methods
}

En su lugar, crearon una subclase:

class Thingies extends ArrayList<Thing> {
  ... // custom methods
}

De repente, esto significa que toda la interfaz de ArrayList se ha convertido en parte de nuestra . Los usuarios pueden remove() things, o get() things en índices específicos. Esto fue pensado de esa manera? DE ACUERDO. Pero a menudo, no pensamos cuidadosamente en todas las consecuencias.

Por lo tanto, es aconsejable

  • nunca extend una clase sin pensarlo detenidamente.
  • siempre marque sus clases como final , excepto si tiene la intención de anular cualquier método.
  • cree interfaces donde desee intercambiar una implementación, por ejemplo, para pruebas unitarias.

Hay muchos ejemplos en los que se debe romper esta "regla", pero por lo general 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 deseados de la subclase como una instancia de la clase base).

Algunos idiomas tienen mecanismos de cumplimiento más estrictos:

  • Todos los métodos son finales de forma predeterminada y deben marcarse explícitamente como virtual
  • Proporcionan herencia privada que no hereda la interfaz, sino solo la implementación.
  • Requieren que los métodos de clase base estén marcados como virtuales, y requieren que todas las anulaciones también estén marcadas. Esto evita problemas cuando una subclase definió un nuevo método, pero luego se agregó un método con la misma firma a la clase base, pero no fue pensado como virtual.
respondido por el amon 12.05.2016 - 12:30
32

Me sorprende que nadie haya mencionado aún Effective Java, 2nd Edition de Joshua Bloch (que debería ser una lectura obligatoria para todos los desarrolladores de Java). El artículo 17 en el libro trata esto en detalle, y se titula: " Diseñe y documente para herencia o, de lo contrario, prohíba ".

No repetiré todos los buenos consejos del libro, pero estos párrafos en particular parecen relevantes:

  

Pero ¿qué pasa con las clases concretas ordinarias? Tradicionalmente, tampoco son definitivas.   ni diseñado ni documentado para subclasificar, pero este estado de cosas es peligroso.   Cada vez que se realiza un cambio en dicha clase, existe la posibilidad de que el cliente   Las clases que extienden la clase se romperán. Esto no es sólo un problema teórico. Es   no es infrecuente recibir informes de errores relacionados con la subclasificación después de modificar el   internos de una clase concreta no final que no fue diseñada y documentada para   herencia.

     

La mejor solución a este problema es prohibir las subclases en clases que   no están diseñados ni documentados para ser subclasificados de forma segura. Hay dos formas de   para prohibir la subclasificación. El más fácil de los dos es declarar la clase final. los   La alternativa es hacer que todos los constructores sean privados o de paquete privado y agregar   Fábricas públicas static en lugar de los constructores. Esta alternativa, que proporciona   la flexibilidad para usar subclases internamente, se discute en el ítem 15.   El enfoque es aceptable.

    
respondido por el Daniel Pryden 12.05.2016 - 20:57
21

Una de las razones por las que final es útil es que se asegura de que no se pueda crear una subclase de clase de una manera que infrinja el contrato de la clase principal. Tal subclasificación sería una violación de SOLID (la mayoría de todas las "L") y hacer una clase final lo evita.

Un ejemplo típico es hacer que sea imposible subclasificar una clase inmutable de manera que la subclase sea mutable. En ciertos casos, tal cambio de comportamiento podría llevar a efectos muy sorprendentes, por ejemplo, cuando utiliza algo como claves en un mapa pensando que la clave es inmutable, mientras que en realidad está usando una subclase que es mutable.

En Java, podrían introducirse muchos problemas de seguridad interesantes si pudiera subclase String y lo convirtiera en mutable (o hizo que volviera a llamar a casa cuando alguien llama a sus métodos, por lo que posiblemente extraiga datos confidenciales del sistema). ) ya que estos objetos se pasan alrededor de algún código interno relacionado con la carga de clases y la seguridad.

La final a veces también es útil para prevenir errores simples, como reutilizar la misma variable para dos cosas dentro de un método, etc. En Scala, se recomienda utilizar solo val , que corresponde aproximadamente a las variables finales en Java. y en realidad, cualquier uso de una variable var o no final se considera con sospecha.

Finalmente, los compiladores pueden, al menos en teoría, realizar algunas optimizaciones adicionales cuando saben que una clase o método es definitivo, ya que cuando llama a un método en una clase final, sabe exactamente qué método se llamará y no tienen que pasar por la tabla de métodos virtuales para verificar la herencia.

    
respondido por el Michał Kosmulski 12.05.2016 - 11:43
7

El segundo motivo es performance . La primera razón es porque algunas clases tienen comportamientos o estados importantes 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 controles bien estudiados y definidos. Permitir que un nuevo empleado recién egresado de la universidad haga una clase de "TrustMePasswordCheck" que extienda la clase anterior podría ser muy perjudicial para mi sistema; esos métodos no deben ser anulados, eso es todo.

    
respondido por el JoulinRouge 12.05.2016 - 11:06
7

Cuando necesito una clase, escribiré una clase. Si no necesito subclases, no me importan las subclases. Me aseguro de que mi clase se comporte como es debido, y los lugares donde uso la clase asumen que la clase se comporta como es debido.

Si alguien quiere subclasificar a mi clase, quiero negar completamente cualquier responsabilidad por lo que sucede. Lo logro haciendo la clase "final". Si desea crear una subclase, recuerde que no tomé en cuenta la subclase mientras escribí la clase. Por lo tanto, debe tomar el código fuente de la clase, eliminar el "final" y, a partir de ese momento, cualquier cosa que suceda será totalmente de su responsabilidad .

¿Crees que es "no 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 ser subclasificada. 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 llama a un método se ejecutará, y puede reemplazar el envío dinámico con el envío estático (pequeño beneficio) y, a menudo, reemplazará el envío estático con la alineación (posiblemente un gran beneficio).

adelphus: ¿Qué es tan difícil de entender acerca de "si desea crear una subclase, tomar el código fuente, eliminar el 'final' y es su responsabilidad"? "final" es igual a "aviso justo".

Y no me pagan para hacer un código reutilizable. Me pagan por escribir un código que hace lo que se supone que debe hacer. Si me pagan por hacer dos partes de código similares, extraigo las partes comunes porque son más baratas y no me pagan por perder mi tiempo. Hacer un código reutilizable que no se reutiliza es una pérdida de tiempo.

M4ks: siempre hace que todo sea privado al que no se debe acceder desde el exterior. Nuevamente, si desea crear una subclase, tome el código fuente, cambie las cosas a "protegidas" si las necesita y asuma la responsabilidad de lo que hace. Si crees que necesitas acceder a cosas que he marcado como privadas, es mejor que sepas lo que estás haciendo.

Ambos: la subclasificación es una pequeña porción de código de reutilización. Crear bloques de construcción que se puedan adaptar sin crear subclases es mucho más poderoso y se beneficia enormemente de "final" porque los usuarios de los bloques pueden confiar en lo que obtienen.

    
respondido por el gnasher729 13.05.2016 - 10:49
4

Imaginemos que el SDK para una plataforma incluye la siguiente clase:

class HTTPRequest {
   void get(String url, String method = "GET");
   void post(String url) {
       get(url, "POST");
   }
}

Una aplicación subclasifica esta clase:

class MyHTTPRequest extends HTTPRequest {
    void get(String url, String method = "GET") {
        requestCounter++;
        super.get(url, method);
    }
}

Todo está bien, pero alguien que trabaja en el SDK decide que pasar un método a get es una tontería, y hace que la interfaz sea mejor asegurándose de aplicar la compatibilidad hacia atrás.

class HTTPRequest {
   @Deprecated
   void get(String url, String method) {
        request(url, method);
   }

   void get(String url) {
       request(url, "GET");
   }
   void post(String url) {
       request(url, "POST");
   }

   void request(String url, String method);
}

Todo parece estar bien, hasta que la aplicación desde arriba se vuelve a compilar con el nuevo SDK. De repente, ya no se llama al método Overriden Get y no se contabiliza la solicitud.

Esto se conoce como el problema de clase base frágil, porque un cambio aparentemente inocuo resulta en una ruptura de subclase. Cualquier cambio a los métodos a los que se llama dentro de la clase puede hacer que una subclase se rompa. Eso tiende a significar que casi cualquier cambio puede hacer que una subclase se rompa.

La final impide que alguien subclasifique tu clase. De esa manera, los métodos dentro de la clase pueden cambiarse sin preocuparse de que en algún lugar alguien dependa exactamente de qué método se realizan las llamadas.

    
respondido por el Winston Ewert 13.05.2016 - 00:58
1

Final efectivamente significa que su clase es segura de cambiar en el futuro sin afectar a ninguna clase basada en herencia descendente (porque no hay ninguna), o cualquier problema relacionado con la seguridad de subprocesos de la clase (creo que hay casos en los que la palabra clave final aparece en un campo evita que algunos subprocesos estén basados en jinx alto).

Final significa que puedes cambiar la forma en que funciona tu clase sin que se produzcan cambios no intencionados en el comportamiento del código de otras personas que se basa en el tuyo como base.

Como ejemplo, escribo una clase llamada HobbitKiller, lo cual es genial, porque todos los hobbits son tramposos y probablemente deberían morir. Rasguen eso, todos ellos definitivamente necesitan morir.

Usas esto como una clase base y agregas un nuevo e impresionante método para usar un lanzallamas, pero usas mi clase como una base porque tengo un método excelente para apuntar a los hobbits (además de ser tricksie, son rápidos ), que usas para apuntar a 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 del 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).

Para que yo sea un desarrollador concienzudo y garantice la muerte perfecta de hobbit en el futuro usando mi clase, tengo que ser muy, muy cuidadoso con cualquier cambio que realice en cualquier clase que pueda extenderse.

Al eliminar la capacidad de extensión, excepto en los casos en los que tengo la intención específica de ampliar la clase, me ahorro (y, con suerte, a otros) muchos dolores de cabeza.

    
respondido por el Scott Taylor 12.05.2016 - 17:37
0

El uso de final no es en modo alguno una violación de los principios SOLID. Desafortunadamente, es extremadamente común interpretar el Principio Abierto / Cerrado ("las entidades de software deben estar abiertas para extensión pero cerradas para modificación") como "en lugar de modificar una clase, crear una subclase y agregar nuevas características". Esto no es lo que originalmente significaba, 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 al inyectar una dependencia al objeto (por ejemplo, utilizando el patrón de diseño de la Estrategia). Estos comportamientos deben diseñarse para utilizar una interfaz de modo que las nuevas implementaciones no se basen en 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 se puede conectar a los mismos clientes. Si su nueva interfaz requiere un comportamiento similar al original, puede:

  • use el patrón de diseño de Decorator para reutilizar el comportamiento existente del original, o
  • refactorice las partes del comportamiento que desea mantener en un objeto auxiliar y use el mismo ayudante en su nueva implementación (la refactorización no es una modificación).
respondido por el Jules 16.05.2016 - 10:19
0

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 proporcione un día gratis solo para sus calendarios.

¿Por qué? Sencillo. Digamos que un desarrollador desea heredar la clase base WorkingDaysUSA en una clase WorkingDaysUSAmyCompany y modificarla para reflejar que su empresa estará cerrada por huelga / mantenimiento / cualquier razón el 2 de marzo.

Los cálculos para pedidos y entregas de clientes reflejarán el retraso y funcionarán en consecuencia cuando en tiempo de ejecución llamen a WorkingDaysUSAmyCompany.getWorkingDays (), pero ¿qué sucede cuando calculo el tiempo de vacaciones? ¿Debo agregar el 2 de marzo como un día festivo para todos? No. Pero como el programador usó la herencia y no protegí a la clase, esto puede llevar a una 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 la mitad del tiempo el sábado. Luego, un terremoto, una crisis eléctrica o alguna circunstancia hacen 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 son compatibles.

¿Solución? Finalice la clase 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

    
respondido por el bns 12.05.2016 - 17:43

Lea otras preguntas en las etiquetas