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.
object-oriented
programming-practices
Nicolas Repiquet
fuente
fuente
Open/Closed principle
, no elClosed Principle.
Respuestas:
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.
fuente
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.
fuente
new List<Foo>()
por algo asíList<Foo>.CreateMutable()
.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.
fuente
Dos casos comunes donde necesitará clases de vainilla, no selladas:
Técnico: si tiene una jerarquía que tiene más de dos niveles de profundidad y desea poder instanciar algo en el medio.
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
Control
ví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:
virtual
funcionalidad, con la excepción de las implementaciones explícitas de interfaz.internal
lugar de sellar la clase, lo que permite que se herede dentro de su base de código, pero no fuera de ella.fuente
Creo que la verdadera razón por la que muchas personas sienten que las clases deberían ser
final
/sealed
es 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 :
super
implementació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:
También vale la pena mencionar la siguiente pregunta "filosófica": ¿Es si una clase es
sealed
/final
parte 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.fuente
Una clase no debe ser final / sellada ni abstracta si:
Por ejemplo, tome la
ObservableCollection<T>
clase en C # . Solo necesita agregar el aumento de eventos a las operaciones normales de aCollection<T>
, por eso subclasesCollection<T>
.Collection<T>
es una clase viable por sí sola, y así esObservableCollection<T>
.fuente
CollectionBase
(resumen),Collection : CollectionBase
(sellado),ObservableCollection : CollectionBase
(sellado). Si observa detenidamente la Colección <T> , verá que es una clase abstracta a medias.Collection<T>
una "clase abstracta a medias"?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.ObservableCollection
hereda deCollection
y no está sellado, por lo que puede volver a heredar de él. Y puede acceder a laItems
propiedad protegida, lo que le permite agregar elementos en la colección sin generar eventos ... Agradable.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.
fuente
"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!"
fuente
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.
fuente
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í:
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
Table
clase común que implementa consultas de tabla de base de datos mientras que las subclasesTable
implementan los detalles específicos de cada tabla usando unenum
para definir los campos. Dejar queenum
implemente una interfaz definida en laTable
clase tiene todo tipo de sentido.fuente
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.
fuente
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
fuente