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

54

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 finalpalabra clave suena igual friendque: 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.

Vorac
fuente
43
¿Por qué crees que una clase está mal diseñada si está anotada con final? Mucha gente (incluyéndome a mí) considera que es un buen diseño hacer que cada clase no abstracta final.
Visto
20
Favorezca la composición sobre la herencia y puede tener todas las clases no abstractas final.
Andy
24
El principio de apertura / cierre es, en cierto sentido, un anacronismo del siglo XX, cuando el mantra era hacer una jerarquía de clases que heredaban de clases que a su vez heredaban de otras clases. Esto fue bueno para enseñar programación orientada a objetos, pero resultó crear problemas enredados e imposibles de mantener cuando se aplica a problemas del mundo real. Diseñar una clase para que sea extensible es difícil.
David Hammen
34
@DavidArno No seas ridículo. La herencia es la condición sine qua non de la programación orientada a objetos, y no es tan complicada o desordenada como a ciertas personas demasiado dogmáticas les gusta predicar. Es una herramienta, como cualquier otra, y un buen programador sabe cómo usar la herramienta adecuada para el trabajo.
Mason Wheeler
48
Como desarrollador en recuperación, me parece recordar que finalmente fue una herramienta brillante para evitar que el comportamiento dañino ingrese a las secciones críticas. También parece recordar que la herencia es una herramienta poderosa en una variedad de formas. Es casi como si jadeo herramientas diferentes tienen ventajas y desventajas, y nosotros como ingenieros que equilibrar esos factores como producimos nuestro software!
corsiKa

Respuestas:

136

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.

Kilian Foth
fuente
14
Yo también. Pero cualquiera que haya escrito una clase base ampliamente reutilizada (como un framework o una biblioteca de medios) sabe mejor que esperar que los programadores de aplicaciones se comporten de una manera sensata. Subvertirán, usarán mal y distorsionarán su invento de una manera que ni siquiera pensó que fuera posible a menos que lo bloquee con un agarre de hierro.
Kilian Foth
8
@KilianFoth Ok, pero honestamente, ¿cómo es ese tu problema lo que hacen los programadores de aplicaciones?
coredump
20
@coredump Las personas que usan mi biblioteca crean mal sistemas. Los malos sistemas generan malas reputaciones. Los usuarios no podrán distinguir el gran código de Kilian de la aplicación monstruosamente inestable de Fred Random. Resultado: pierdo en la programación de créditos y clientes. Hacer que su código sea difícil de usar es una cuestión de dólares y centavos.
Kilian Foth
21
La afirmación "Se supone que este elemento no debe cambiar, y si desea cambiarlo, no ha entendido el diseño existente". es increíblemente arrogante y no es la actitud que quisiera como parte de cualquier biblioteca con la que trabajé. Si solo tuviera un centavo por cada vez que una biblioteca ridículamente sobreencapsulada me dejara sin mecanismo para cambiar alguna parte del estado interno que necesita ser cambiado porque el autor no anticipó un caso de uso importante ...
Mason Wheeler
31
Regla general de @JimmyB: si crees que sabes para qué se usará tu creación, ya estás equivocado. Bell concibió el teléfono como esencialmente un sistema Muzak. Kleenex fue inventado con el propósito de eliminar el maquillaje de manera más conveniente. Thomas Watson, presidente de IBM, dijo una vez: "Creo que hay un mercado mundial para quizás cinco computadoras". El representante Jim Sensenbrenner, quien introdujo la Ley PATRIOTA en 2001, dice que tenía el propósito específico de evitar que la NSA haga cosas que está haciendo "con la autoridad de la ley PATRIOTA". Y así sucesivamente ...
Mason Wheeler
59

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:

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

Si permitimos que se anule esa clase, 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 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.

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 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 SpiffyPagesubclase 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:

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 topparámetro adicional es necesario para asegurarse de que las llamadas writeMainContentpasen 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:

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 cambio, crearon una subclase:

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

Esto de repente significa que toda la interfaz se ArrayListha convertido en parte de nuestra interfaz. Los usuarios pueden remove()cosas, o get()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

  • Nunca extenduna clase sin pensarlo detenidamente.
  • marque siempre sus clases como, finalexcepto 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 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:

  • Todos los métodos son finales por defecto y deben marcarse explícitamente como virtual
  • Proporcionan una herencia privada que no hereda la interfaz sino solo la implementación.
  • Requieren que los métodos de clase base se marquen como virtuales, y también se marcan todas las anulaciones. Esto evita problemas en los que una subclase definió un nuevo método, pero luego se agregó un método con la misma firma a la clase base pero no se pensó como virtual.
amon
fuente
3
Usted merece al menos +100 por mencionar el "problema de la clase base frágil". :)
David Arno
66
No estoy convencido por los puntos hechos aquí. Sí, la clase base frágil es un problema, pero final no resuelve todos los problemas al cambiar una implementación. Su primer ejemplo es malo porque asume que conoce todos los casos de uso posibles para PasswordChecker ("bloquear a todos o permitir el acceso de todos ... no está bien", ¿quién dice?). Su última lista de "por lo tanto aconsejable ..." es realmente mala: básicamente está abogando por no extender nada y marcar todo como final, lo que borra por completo la utilidad de OOP, herencia y reutilización de código.
adelphus
44
Su primer ejemplo no es un ejemplo del problema de la clase base frágil. En el problema de la clase base frágil, los cambios en una clase base rompen una subclase. Pero en ese ejemplo, su subclase no sigue el contrato de una subclase. Estos son dos problemas diferentes. (Además, en realidad es razonable que, en determinadas circunstancias, pueda deshabilitar un comprobador de contraseñas (por ejemplo, para el desarrollo))
Winston Ewert
55
"Evita el frágil problema de la clase base", de la misma manera que suicidarse evita tener hambre.
user253751
99
@immibis, es más como evitar comer para evitar la intoxicación alimentaria. Claro, nunca comer sería un problema. Pero comer solo en esos lugares en los que confía tiene mucho sentido.
Winston Ewert
33

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:

¿Pero qué hay de las clases concretas ordinarias? Tradicionalmente, no son finales ni están diseñados y documentados para la subclase, pero este estado de cosas es peligroso. Cada vez que se realiza un cambio en dicha clase, existe la posibilidad de que las clases de clientes que extienden la clase se rompan. Esto no es solo un problema teórico. No es raro recibir informes de errores relacionados con subclases después de modificar las partes internas de una clase concreta no final que no fue diseñada y documentada para la herencia.

La mejor solución a este problema es prohibir la subclase en clases que no están diseñadas y documentadas para ser subclasificadas de forma segura. Hay dos formas de prohibir la subclasificación. La más fácil de las dos es declarar la clase final. La alternativa es hacer que todos los constructores sean privados o privados de paquetes y agregar fábricas estáticas públicas en lugar de los constructores. Esta alternativa, que proporciona la flexibilidad para usar subclases internamente, se analiza en el Ítem 15. Cualquiera de los dos enfoques es aceptable.

Daniel Pryden
fuente
21

Una de las razones por las que finales ú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 lo finalimpida.

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 Stringy 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 vallo que corresponde aproximadamente a las variables finales en Java, y en realidad cualquier uso de un varo 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.

Michał Kosmulski
fuente
66
Finalmente, los compiladores pueden, al menos en teoría => Revisé personalmente el pase de desvirtualización de Clang, y confirmo que se usa en la práctica.
Matthieu M.
Pero, ¿no puede el compilador decir de antemano que nadie está anulando una clase o método, independientemente de si está marcado como final o no?
JesseTG
3
@JesseTG Si tiene acceso a todo el código a la vez, probablemente. Sin embargo, ¿qué pasa con la compilación de archivos por separado?
Restablece a Monica
3
La desvirtualización de @JesseTG o el almacenamiento en caché en línea (monomórfico / polimórfico) es una técnica común en los compiladores JIT, ya que el sistema sabe qué clases están cargadas actualmente y puede desoptimizar el código si la suposición de que no hay métodos de anulación resulta ser falsa. Sin embargo, un compilador anticipado no puede. Cuando compilo una clase Java y un código que usa esa clase, luego puedo compilar una subclase y pasar una instancia al código consumidor. La forma más simple es agregar otro jar al frente del classpath. El compilador no puede saber todo eso, ya que esto sucede en tiempo de ejecución.
amon
55
@MTilsted Puede suponer que las cadenas son inmutables ya que las cadenas mutantes a través de la reflexión pueden prohibirse a través de a 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.
maaartinus
7

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.

JoulinRouge
fuente
1
bancomat = ATM: en.wikipedia.org/wiki/Cash_machine
tar
La JVM es lo suficientemente inteligente como para tratar las clases como finales si no tienen subclases, incluso si no se declaran definitivas.
user253751
Votó en contra porque la afirmación de "rendimiento" ha sido refutada muchas veces.
Paul Smith
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 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.

gnasher729
fuente
44
-1 Esta respuesta describe todo lo que está mal con los desarrolladores de software. Si alguien quiere reutilizar su clase subclasificando, déjelo. ¿Por qué sería su responsabilidad cómo lo usan (o abusan)? Básicamente, estás usando final como af k, no estás usando mi clase. * "Nadie me pagó por hacer una clase que podría subclasificarse" . ¿En serio? Esa es exactamente la razón por la cual se emplean ingenieros de software: para crear código sólido y reutilizable.
adelphus
44
-1 Solo haz que todo sea privado, para que nadie piense siquiera en subclasificar ..
M4ks
3
@adelphus Si bien la redacción de esta respuesta es contundente y casi dura, no es un punto de vista "incorrecto". De hecho, es el mismo punto de vista que la mayoría de las respuestas a esta pregunta hasta ahora solo con un tono menos clínico.
NemesisX00
+1 por mencionar que puedes eliminar 'final'. Es arrogante reclamar conocimiento previo de todos los usos posibles de su código. Sin embargo, es humilde dejar en claro que no puede mantener algunos usos posibles, y que esos usos requerirían mantener un tenedor.
gmatht
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 y bien, pero alguien que trabaja en el SDK decide que pasar un método getes una tontería, y hace que la interfaz sea mejor asegurándose de aplicar la compatibilidad con versiones anteriores.

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 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.

Winston Ewert
fuente
1

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.

Scott Taylor
fuente
2
Si su método de orientación va a cambiar, ¿por qué lo hizo público? Y si cambia significativamente el comportamiento de una clase, necesita otra versión en lugar de sobreprotectorafinal
M4ks,
No tengo idea de por qué esto cambiaría. Los hobbits son engañosos y se requirió el cambio. El punto es que si lo construyo como final, evito la herencia que protege a otras personas de que mis cambios infecten su código.
Scott Taylor
0

El uso de finalno 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:

  • use el patrón de diseño Decorador para reutilizar el comportamiento existente del original, o
  • refactorice las partes del comportamiento que desea mantener en un objeto auxiliar y use el mismo auxiliar en su nueva implementación (la refactorización no es modificación).
Jules
fuente
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 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

bns
fuente