¿Por qué una clase debería ser algo más que "abstracto" o "final / sellado"?

29

Después de más de 10 años de programación en Java / C #, me encuentro creando:

  • clases abstractas : contrato no destinado a ser instanciado tal cual.
  • clases finales / selladas : la implementación no pretende servir como clase base para otra cosa.

No puedo pensar en ninguna situación en la que una simple "clase" (es decir, ni abstracta ni final / sellada) sea una "programación inteligente".

¿Por qué una clase no debe ser "abstracta" o "final / sellada"?

EDITAR

Este excelente artículo explica mis preocupaciones mucho mejor de lo que puedo.

Nicolas Repiquet
fuente
21
Porque se llama Open/Closed principle, no elClosed Principle.
StuperUser
2
¿Qué tipo de cosas estás escribiendo profesionalmente? Es muy posible que influya en su opinión al respecto.
Bueno, conozco algunos desarrolladores de plataformas que sellan todo, porque quieren minimizar la interfaz de herencia.
K ..
44
@StuperUser: Esa no es una razón, es un lugar común. El OP no pregunta cuál es el lugar común, pregunta por qué . Si no hay una razón detrás de un principio, no hay razón para prestarle atención.
Michael Shaw
1
Me pregunto cuántos marcos de interfaz de usuario se romperían al cambiar la clase Window a sellada.
Reactgular

Respuestas:

46

Irónicamente, encuentro lo contrario: el uso de clases abstractas es la excepción más que la regla y tiendo a fruncir el ceño en las clases finales / selladas.

Las interfaces son un mecanismo de diseño por contrato más típico porque no especifica ninguna parte interna, no está preocupado por ellas. Permite que cada implementación de ese contrato sea independiente. Esto es clave en muchos dominios. Por ejemplo, si estuviera creando un ORM, sería muy importante que pueda pasar una consulta a la base de datos de manera uniforme, pero las implementaciones pueden ser bastante diferentes. Si usa clases abstractas para este propósito, terminará el cableado en componentes que pueden o no aplicarse a todas las implementaciones.

Para las clases finales / selladas, la única excusa que puedo ver para usarlas es cuando es realmente peligroso permitir la anulación, tal vez un algoritmo de cifrado o algo así. Aparte de eso, nunca se sabe cuándo es posible que desee ampliar la funcionalidad por razones locales. Sellar una clase restringe sus opciones de ganancias que no existen en la mayoría de los escenarios. Es mucho más flexible escribir sus clases de manera que puedan ampliarse más adelante en la línea.

Esta última visión se ha consolidado para mí al trabajar con componentes de terceros que sellaron las clases, evitando así una integración que habría hecho la vida mucho más fácil.

Miguel
fuente
17
+1 para el último párrafo. Con frecuencia, he encontrado demasiada encapsulación en bibliotecas de terceros (¡o incluso clases de bibliotecas estándar!) Como una causa mayor de dolor que muy poca encapsulación.
Mason Wheeler
55
El argumento habitual para sellar clases es que no puede predecir las diferentes formas en que un cliente podría anular su clase y, por lo tanto, no puede hacer ninguna garantía sobre su comportamiento. Ver blogs.msdn.com/b/ericlippert/archive/2004/01/22/…
Robert Harvey
12
@RobertHarvey Estoy familiarizado con el argumento y suena muy bien en teoría. Usted, como diseñador de objetos, no puede ver cómo las personas pueden extender sus clases, es precisamente por eso que no deben sellarse. No puedes soportar todo, está bien. No lo hagas Pero tampoco quite las opciones.
Michael
66
@RobertHarvey, ¿no es solo que las personas que rompen el LSP obtienen lo que se merecen?
StuperUser
2
Tampoco soy un gran admirador de las clases selladas, pero pude ver por qué algunas compañías como Microsoft (que con frecuencia tienen que apoyar cosas que no rompieron) las encuentran atractivas. Se supone que los usuarios de un marco no necesariamente tienen conocimiento de los componentes internos de una clase.
Robert Harvey
11

Ese es un gran artículo de Eric Lippert, pero no creo que respalde su punto de vista.

Argumenta que todas las clases expuestas para el uso de otros deben estar selladas o de otro modo no extensibles.

Su premisa es que todas las clases deben ser abstractas o selladas.

Gran diferencia.

El artículo de EL no dice nada sobre las (presumiblemente muchas) clases producidas por su equipo de las que usted y yo no sabemos nada. En general, las clases expuestas públicamente en un marco son solo un subconjunto de todas las clases involucradas en la implementación de ese marco.

Martín
fuente
Pero puede aplicar el mismo argumento a las clases que no forman parte de una interfaz pública. Una de las razones principales para hacer que una clase sea final es que actúa como una medida de seguridad para evitar el código descuidado. Ese argumento es igual de válido para cualquier base de código compartida por diferentes desarrolladores a lo largo del tiempo, o incluso si usted es el único desarrollador. Simplemente protege la semántica del código.
DPM
Creo que la idea de que las clases deben ser abstractas o selladas es parte de un principio más amplio, que es evitar el uso de variables de tipos de clases instanciables. Tal evitación hará posible crear un tipo que pueda ser utilizado por los consumidores de un tipo dado, sin que tenga que heredar todos los miembros privados del mismo. Desafortunadamente, tales diseños realmente no funcionan con la sintaxis de constructor público. En cambio, el código debería reemplazarse new List<Foo>()por algo así List<Foo>.CreateMutable().
supercat
5

Desde una perspectiva de Java, creo que las clases finales no son tan inteligentes como parece.

Muchas herramientas (especialmente AOP, JPA, etc.) funcionan con agitación del tiempo de carga, por lo que tienen que extender sus clases. La otra forma sería crear delegados (no los .NET) y delegar todo a la clase original, lo que sería mucho más complicado que extender las clases de los usuarios.

Uwe Plonus
fuente
5

Dos casos comunes donde necesitará clases de vainilla, no selladas:

  1. Técnico: si tiene una jerarquía que tiene más de dos niveles de profundidad y desea poder instanciar algo en el medio.

  2. Principio: a veces es deseable escribir clases que sean explícitamente diseñadas para extenderse de manera segura . (Esto sucede mucho cuando estás escribiendo una API como Eric Lippert, o cuando estás trabajando en equipo en un proyecto grande). A veces, desea escribir una clase que funcione bien por sí sola, pero está diseñada teniendo en cuenta la extensibilidad.

Los pensamientos de Eric Lippert sobre marcas de sellado sentido, pero también admite que se hacen diseño para la extensibilidad al dejar la clase "abierta".

Sí, muchas clases están selladas en el BCL, pero una gran cantidad de clases no lo están y pueden ampliarse de muchas maneras maravillosas. Un ejemplo que viene a la mente es en los formularios de Windows, donde puede agregar datos o comportamiento a casi cualquier Controlvía de herencia. Claro, esto podría haberse hecho de otras maneras (patrón de decorador, varios tipos de composición, etc.), pero la herencia también funciona muy bien.

Dos notas específicas de .NET:

  1. En la mayoría de los casos, las clases de sellado a menudo no son críticas para la seguridad, porque los herederos no pueden interferir con su falta de virtualfuncionalidad, con la excepción de las implementaciones explícitas de interfaz.
  2. A veces, una alternativa adecuada es crear el Constructor en internallugar de sellar la clase, lo que permite que se herede dentro de su base de código, pero no fuera de ella.
Kevin McCormick
fuente
4

Creo que la verdadera razón por la que muchas personas sienten que las clases deberían ser final/ sealedes que la mayoría de las clases extensibles no abstractas no están debidamente documentadas .

Déjame elaborar. Comenzando desde lejos, existe la opinión entre algunos programadores de que la herencia como herramienta en OOP es ampliamente utilizada en exceso y abusada. Todos hemos leído el principio de sustitución de Liskov, aunque esto no nos impidió violarlo cientos (tal vez incluso miles) de veces.

La cuestión es que a los programadores les encanta reutilizar el código. Incluso cuando no es una buena idea. Y la herencia es una herramienta clave en esta "reutilización" de código. Volver a la pregunta final / sellada.

La documentación adecuada para una clase final / sellada es relativamente pequeña: usted describe lo que hace cada método, cuáles son los argumentos, el valor de retorno, etc. Todas las cosas habituales.

Sin embargo, cuando documenta correctamente una clase extensible, debe incluir al menos lo siguiente :

  • Dependencias entre métodos (qué método llama a cuáles, etc.)
  • Dependencias de variables locales.
  • Contratos internos que la clase extendida debe cumplir
  • Convención de llamada para cada método (p. Ej., Cuando lo anula, ¿llama a la superimplementación? ¿Lo llama al principio del método o al final? Piense en constructor vs destructor)
  • ...

Estos están justo en la parte superior de mi cabeza. Y puedo proporcionarle un ejemplo de por qué cada uno de estos es importante y omitirlo arruinará una clase extendida.

Ahora, considere cuánto esfuerzo de documentación debería dedicarse a documentar adecuadamente cada una de estas cosas. Creo que una clase con 7-8 métodos (que podría ser demasiado en un mundo idealizado, pero es muy poco en el mundo real) también podría tener una documentación de 5 páginas, solo texto. Entonces, en cambio, nos agachamos hasta la mitad y no sellamos la clase, para que otras personas puedan usarla, pero tampoco la documenten correctamente, ya que tomará una gran cantidad de tiempo (y, ya sabes, podría nunca se extenderá de todos modos, entonces, ¿por qué molestarse?).

Si está diseñando una clase, puede sentir la tentación de sellarla para que la gente no pueda usarla de una manera que no haya previsto (y preparado). Por otro lado, cuando está usando el código de otra persona, a veces desde la API pública no hay una razón visible para que la clase sea final, y podría pensar "Maldición, esto solo me costó 30 minutos en busca de una solución".

Creo que algunos de los elementos de una solución son:

  • Primero, asegurarse de extender es una buena idea cuando es cliente del código, y favorecer realmente la composición sobre la herencia.
  • Segundo, lea el manual en su totalidad (nuevamente como cliente) para asegurarse de que no esté pasando por alto algo que se menciona.
  • Tercero, cuando está escribiendo un código que usará el cliente, escriba la documentación adecuada para el código (sí, a lo largo). Como ejemplo positivo, puedo dar los documentos de iOS de Apple. No son suficientes para que el usuario siempre se extienden adecuadamente sus clases, sino que incluyen al menos alguna información sobre la herencia. Lo cual es más de lo que puedo decir para la mayoría de las API.
  • Cuarto, intenta extender tu propia clase para asegurarte de que funcione. Soy un gran defensor de incluir muchas muestras y pruebas en las API, y cuando realiza una prueba, también podría probar las cadenas de herencia: ¡después de todo, son parte de su contrato!
  • Quinto, en situaciones en las que tenga dudas, indique que la clase no debe extenderse y que hacerlo es una mala idea (tm). Indique que no debe rendir cuentas por ese uso no intencionado, pero aún así no selle a la clase. Obviamente, esto no cubre los casos en que la clase debe estar sellada al 100%.
  • Finalmente, al sellar una clase, proporcione una interfaz como un enlace intermedio, de modo que el cliente pueda "reescribir" su propia versión modificada de la clase y trabajar alrededor de su clase 'sellada'. De esta manera, puede reemplazar la clase sellada con su implementación. Ahora, esto debería ser obvio, ya que es un acoplamiento suelto en su forma más simple, pero aún vale la pena mencionarlo.

También vale la pena mencionar la siguiente pregunta "filosófica": ¿Es si una clase es sealed/ finalparte del contrato para la clase, o un detalle de implementación? Ahora no quiero pisar allí, pero una respuesta a esto también debería influir en su decisión de sellar una clase o no.

K.Steff
fuente
3

Una clase no debe ser final / sellada ni abstracta si:

  • Es útil por sí solo, es decir, es beneficioso tener instancias de esa clase.
  • Es beneficioso que esa clase sea la subclase / clase base de otras clases.

Por ejemplo, tome la ObservableCollection<T>clase en C # . Solo necesita agregar el aumento de eventos a las operaciones normales de a Collection<T>, por eso subclases Collection<T>. Collection<T>es una clase viable por sí sola, y así es ObservableCollection<T>.

PescadoCanasta
fuente
Si estuviera a cargo de esto, probablemente haré CollectionBase(resumen), Collection : CollectionBase(sellado), ObservableCollection : CollectionBase(sellado). Si observa detenidamente la Colección <T> , verá que es una clase abstracta a medias.
Nicolas Repiquet
2
¿Qué ventaja obtienes de tener tres clases en lugar de solo dos? Además, ¿cómo es Collection<T>una "clase abstracta a medias"?
FishBasketGordo
Collection<T>expone muchas entrañas a través de métodos y propiedades protegidas, y claramente está destinado a ser una clase base para una colección especializada. Pero no está claro dónde se supone que debe poner su código, ya que no hay métodos abstractos para implementar. ObservableCollectionhereda de Collectiony no está sellado, por lo que puede volver a heredar de él. Y puede acceder a la Itemspropiedad protegida, lo que le permite agregar elementos en la colección sin generar eventos ... Agradable.
Nicolas Repiquet
@NicolasRepiquet: en muchos lenguajes y marcos, los medios idiomáticos normales para crear un objeto requieren que el tipo de la variable que contendrá el objeto sea el mismo que el de la instancia creada. En muchos casos, el uso ideal sería pasar referencias a un tipo abstracto, pero eso obligaría a una gran cantidad de código a usar un tipo para variables y parámetros y un tipo concreto diferente al llamar a constructores. Difícilmente imposible, pero un poco incómodo.
supercat
3

El problema con las clases finales / selladas es que están tratando de resolver un problema que aún no ha sucedido. Es útil solo cuando existe el problema, pero es frustrante porque un tercero ha impuesto una restricción. Raramente la clase de sellado resuelve un problema actual, lo que hace difícil argumentar que es útil.

Hay casos en que una clase debe ser sellada. Como ejemplo; La clase administra los recursos / memoria asignados de una manera que no puede predecir cómo los cambios futuros podrían alterar esa administración.

A lo largo de los años, he encontrado que la encapsulación, las devoluciones de llamada y los eventos son mucho más flexibles / útiles que las clases abstractas. Veo mucho código con una gran jerarquía de clases donde la encapsulación y los eventos habrían simplificado la vida del desarrollador.

Reactgular
fuente
1
El material sellado final en el software resuelve el problema de "Siento la necesidad de imponer mi voluntad a los futuros mantenedores de este código".
Kaz
No fue final / sellar algo agregado a OOP, porque no recuerdo que existiera cuando era más joven. Parece una característica del tipo de pensamiento posterior.
Reactgular
La finalización existía como un procedimiento de cadena de herramientas en los sistemas OOP antes de que OOP se volviera tonto con lenguajes como C ++ y Java. Los programadores trabajan lejos en Smalltalk, Lisp con la máxima flexibilidad: cualquier cosa puede extenderse, nuevos métodos agregados todo el tiempo. Luego, la imagen compilada del sistema está sujeta a una optimización: se supone que el sistema no será extendido por los usuarios finales, por lo que esto significa que el envío del método puede optimizarse en función de hacer un balance de qué métodos y Las clases existen ahora .
Kaz
No creo que sea lo mismo, porque esta es solo una función de optimización. No recuerdo estar sellado en todas las versiones de Java, pero podría estar equivocado ya que no lo uso mucho.
Reactgular
El hecho de que se manifieste como algunas declaraciones que debe incluir en el código no significa que no sea lo mismo.
Kaz
2

"Sellar" o "finalizar" en los sistemas de objetos permite ciertas optimizaciones, porque se conoce el gráfico de despacho completo.

Es decir, dificultamos que el sistema se cambie a otra cosa, como una compensación por el rendimiento. (Esa es la esencia de la mayoría de las optimizaciones).

En todos los demás aspectos, es una pérdida. Los sistemas deben ser abiertos y extensibles por defecto. Debería ser fácil agregar nuevos métodos a todas las clases y extenderlos arbitrariamente.

Hoy no obtenemos ninguna funcionalidad nueva al tomar medidas para evitar futuras extensiones.

Entonces, si lo hacemos en aras de la prevención en sí, entonces lo que estamos haciendo es tratar de controlar la vida de los futuros mantenedores. Se trata del ego. "Incluso cuando ya no trabajo aquí, este código se mantendrá a mi manera, ¡maldición!"

Kaz
fuente
1

Las clases de prueba parecen venir a la mente. Estas son clases que se llaman de manera automatizada o "a voluntad" en función de lo que el programador / evaluador está tratando de lograr. No estoy seguro de haber visto u oído hablar de una clase terminada de pruebas que sea privada.

joshin4colours
fuente
De que estas hablando ¿De qué manera las clases de prueba no deben ser finales / selladas / abstractas?
1

El momento en que necesita considerar una clase que se pretende extender es cuando está haciendo una planificación real para el futuro. Déjame darte un ejemplo de la vida real de mi trabajo.

Paso mucho tiempo escribiendo herramientas de interfaz entre nuestro producto principal y sistemas externos. Cuando realizamos una nueva venta, un componente importante es un conjunto de exportadores diseñados para ejecutarse a intervalos regulares que generan archivos de datos que detallan los eventos que ocurrieron ese día. Estos archivos de datos son luego consumidos por el sistema del cliente.

Esta es una excelente oportunidad para extender las clases.

Tengo una clase de exportación que es la clase base de cada exportador. Sabe cómo conectarse a la base de datos, averiguar dónde había llegado la última vez que se ejecutó y crear archivos de los archivos de datos que genera. También proporciona administración de archivos de propiedades, registro y un manejo simple de excepciones.

Además de esto, tengo un exportador diferente para trabajar con cada tipo de datos, tal vez haya actividad del usuario, datos de transacciones, datos de gestión de efectivo, etc.

Encima de esta pila coloco una capa específica del cliente que implementa la estructura de archivos de datos que el cliente necesita.

De esta manera, el exportador base rara vez cambia. Los exportadores principales de tipos de datos a veces cambian, pero rara vez, y generalmente solo para manejar cambios en el esquema de la base de datos que de todos modos deberían propagarse a todos los clientes. El único trabajo que tengo que hacer para cada cliente es esa parte del código que es específica para ese cliente. Un mundo perfecto!

Entonces la estructura se ve así:

Base
 Function1
  Customer1
  Customer2
 Function2
 ...

Mi punto principal es que al diseñar el código de esta manera, puedo hacer uso de la herencia principalmente para la reutilización del código.

Tengo que decir que no se me ocurre ninguna razón para pasar tres capas.

He usado dos capas muchas veces, por ejemplo, para tener una Tableclase común que implementa consultas de tabla de base de datos mientras que las subclases Tableimplementan los detalles específicos de cada tabla usando un enumpara definir los campos. Dejar que enumimplemente una interfaz definida en la Tableclase tiene todo tipo de sentido.

OldCurmudgeon
fuente
1

He encontrado que las clases abstractas son útiles pero no siempre necesarias, y las clases selladas se convierten en un problema al aplicar pruebas unitarias. No puedes burlarte o tropezar con una clase sellada, a menos que uses algo como Teleriks justmock.

Dan H
fuente
1
Este es un buen comentario, pero no responde directamente a la pregunta. Por favor, considere expandir sus pensamientos aquí.
1

Estoy de acuerdo con tu punto de vista. Creo que en Java, por defecto, las clases deberían declararse "finales". Si no lo hace definitivo, prepárelo y documente específicamente para su extensión.

La razón principal de esto es para garantizar que cualquier instancia de sus clases cumpla con la interfaz que originalmente diseñó y documentó. De lo contrario, un desarrollador que use sus clases puede crear código quebradizo e inconsistente y, a su vez, pasarlo a otros desarrolladores / proyectos, haciendo que los objetos de sus clases no sean confiables.

Desde una perspectiva más práctica, hay una desventaja en esto, ya que los clientes de la interfaz de su biblioteca no podrán hacer ningún ajuste y usar sus clases de formas más flexibles de lo que originalmente pensaron.

Personalmente, para todo el código de mala calidad que existe (y dado que estamos discutiendo esto en un nivel más práctico, diría que el desarrollo de Java es más propenso a esto) Creo que esta rigidez es un pequeño precio a pagar en nuestra búsqueda para que sea más fácil mantener el código

DPM
fuente