¿Por qué las clases no deben estar diseñadas para ser "abiertas"?

44

Al leer varias preguntas de desbordamiento de pila y el código de otros, se cierra el consenso general de cómo diseñar las clases. Esto significa que, de forma predeterminada, en Java y C # todo es privado, los campos son finales, algunos métodos son finales y a veces las clases son incluso finales .

La idea detrás de esto es ocultar los detalles de la implementación, lo cual es una muy buena razón. Sin embargo, con la existencia de protected en la mayoría de los lenguajes OOP y el polimorfismo, esto no funciona.

Cada vez que deseo agregar o cambiar la funcionalidad de una clase, generalmente me veo obstaculizado por la colocación privada y final en todas partes. Aquí los detalles de la implementación son importantes: usted toma la implementación y la extiende, sabiendo perfectamente cuáles son las consecuencias. Sin embargo, como no puedo acceder a los campos y métodos privados y finales, tengo tres opciones:

  • No extienda la clase, solo solucione el problema que conduce a un código más complejo
  • Copie y pegue toda la clase, eliminando la reutilización del código
  • Ten en cuenta el proyecto

Esas no son buenas opciones. ¿Por qué no se usa protected en proyectos escritos en lenguajes que lo admiten? ¿Por qué algunos proyectos prohíben explícitamente la herencia de sus clases?

    
pregunta TheLQ 12.07.2011 - 20:01

8 respuestas

55

Diseñar clases para que funcionen correctamente cuando se extienden, especialmente cuando el programador que realiza la extensión no entiende completamente cómo se supone que funciona la clase, requiere un esfuerzo adicional considerable. No puedes simplemente tomar todo lo que es privado y hacerlo público (o protegido) y llamar a eso "abierto". Si permite que otra persona cambie el valor de una variable, debe considerar cómo afectarán a la clase todos los valores posibles. (¿Qué sucede si establecen la variable en nula? ¿Una matriz vacía? ¿Un número negativo?) Lo mismo se aplica cuando se permite que otras personas llamen a un método. Se necesita un pensamiento cuidadoso.

Así que no es tanto que las clases no deban ser abiertas, sino que a veces no vale la pena esforzarse por hacerlas abiertas.

Por supuesto, también es posible que los autores de la biblioteca simplemente estuvieran siendo perezosos. Depende de qué biblioteca estás hablando. :-)

    
respondido por el Aaron 12.07.2011 - 21:40
24

Por defecto, hacer que todo sea privado suena duro, pero míralo desde el otro lado: cuando todo es privado por defecto, hacer algo público (o protegido, que es casi lo mismo) se supone que es una elección consciente. es el contrato del autor de la clase con usted, el consumidor, sobre cómo usar la clase. Esto es una conveniencia para ambos: el autor es libre de modificar el funcionamiento interno de la clase, siempre y cuando la interfaz permanezca sin cambios; y usted sabe exactamente en qué partes de la clase puede confiar y cuáles están sujetos a cambios.

La idea subyacente es "acoplamiento suelto" (también conocido como "interfaces estrechas"); su valor radica en mantener baja la complejidad. Al reducir la cantidad de formas en que los componentes pueden interactuar, la cantidad de dependencia cruzada entre ellos también se reduce; y la dependencia cruzada es uno de los peores tipos de complejidad cuando se trata de mantenimiento y gestión de cambios.

En bibliotecas bien diseñadas, las clases que vale la pena extender a través de la herencia han protegido a los miembros públicos en los lugares correctos y ocultan todo lo demás.

    
respondido por el tdammers 12.07.2011 - 21:44
19

Todo lo que es no privado se supone más o menos que existe con un comportamiento sin cambios en todas las versiones futuras de la clase. De hecho, puede considerarse parte de la API, documentada o no. Por lo tanto, exponer demasiados detalles probablemente cause problemas de compatibilidad más adelante.

Respecto a la "final" resp. Clases "selladas", un caso típico son las clases inmutables. Muchas partes del marco dependen de que las cuerdas sean inmutables. Si la clase de String no fuera definitiva, sería fácil (incluso tentador) crear una subclase de String mutable que cause todo tipo de errores en muchas otras clases cuando se usa en lugar de la clase de String inmutable.

    
respondido por el user281377 13.07.2011 - 01:07
14

En OO hay dos formas de agregar funcionalidad al código existente.

El primero es por herencia: tomas una clase y derivas de ella. Sin embargo, la herencia debe ser usada con cuidado. Debes usar la herencia pública principalmente cuando tienes una relación isA entre la base y la clase derivada (por ejemplo, un Rectángulo es una forma ). En su lugar, debe evitar la herencia pública para reutilizar una implementación existente. Básicamente, la herencia pública se utiliza para asegurarse de que la clase derivada tenga la misma interfaz que la clase base (o una más grande), de modo que pueda aplicar el Principio de sustitución de Liskov .

El otro método para agregar funcionalidad es usar delegación o composición. Usted escribe su propia clase que usa la clase existente como un objeto interno y le delega parte del trabajo de implementación.

No estoy seguro de cuál fue la idea detrás de todas esas finales en la biblioteca que estás tratando de usar. Probablemente los programadores querían asegurarse de que no iban a heredar una nueva clase de los suyos, porque esa no es la mejor manera de extender su código.

    
respondido por el knulp 12.07.2011 - 22:35
11

La mayoría de las respuestas lo tienen correcto: las clases deben estar selladas (final, NotOverridable, etc.) cuando el objeto no está diseñado o pretende extenderse.

Sin embargo, no consideraría simplemente cerrar todo el diseño de código adecuado. La "O" en SOLID es para el "Principio Abierto-Cerrado", que indica que una clase debe estar "cerrada" a la modificación, pero "abierta" a la extensión. La idea es que tienes código en un objeto. Funciona bien Agregar funcionalidad no debería requerir abrir ese código y realizar cambios quirúrgicos que pueden romper el comportamiento de trabajo previo. En su lugar, uno debería poder utilizar la inyección de herencia o dependencia para "extender" la clase para hacer cosas adicionales.

Por ejemplo, una clase "ConsoleWriter" puede obtener texto y enviarlo a la consola. Lo hace bien. Sin embargo, en un caso determinado, TAMBIÉN necesita la misma salida escrita en un archivo. Abrir el código de ConsoleWriter, cambiar su interfaz externa para agregar un parámetro "WriteToFile" a las funciones principales, y colocar el código adicional de escritura de archivos junto al código de escritura de la consola generalmente se considerará algo malo.

En su lugar, podría hacer una de dos cosas: podría derivar de ConsoleWriter para formar ConsoleAndFileWriter, y extender el método Write () para llamar primero a la implementación base, y luego escribir en un archivo. O, puede extraer una interfaz IWriter de ConsoleWriter y volver a implementar esa interfaz para crear dos nuevas clases; un FileWriter y un "MultiWriter" que puede recibir otros IWriters y "difundirá" cualquier llamada a sus métodos a todos los escritores dados. El que elija dependerá de lo que vaya a necesitar; La derivación simple es, bueno, más simple, pero si tiene una tenue previsión de que eventualmente necesite enviar el mensaje a un cliente de red, tres archivos, dos tuberías con nombre y la consola, continúe y tome la molestia de extraer la interfaz y crear el "Adaptador Y"; te ahorrará tiempo en el back-end.

Ahora, si la función Write () nunca se había declarado virtual (o estaba sellada), ahora está en problemas si no controla el código fuente. A veces incluso si lo haces. En general, esta es una mala posición y frustra a los usuarios de las API de código cerrado o de fuente limitada para que no se acaben cuando sucede. Pero, hay razones legítimas por las que Write (), o la clase completa ConsoleWriter, están selladas; puede haber información confidencial en un campo protegido (se reemplaza a su vez de una clase base para proporcionar dicha información). Puede haber validación insuficiente; cuando escribe una api, debe asumir que los programadores que consumen su código no son más inteligentes ni benevolentes que el "usuario final" promedio del sistema final; De esa manera nunca te decepcionarás. Puede tomarse el tiempo para validar todo lo que su consumidor pueda hacer para ampliar su API, o puede bloquear la API como Fort Knox, por lo que solo pueden hacer exactamente lo que usted espera.

    
respondido por el KeithS 13.07.2011 - 01:27
2

Creo que probablemente sea de todas las personas que utilizan un lenguaje oo para la programación en c. Te estoy mirando, java. Si su martillo tiene la forma de un objeto, pero quiere un sistema de procedimiento, y / o no realmente entiende las clases, entonces su proyecto deberá estar bloqueado.

Básicamente, si vas a escribir en un idioma oo, tu proyecto debe seguir el paradigma oo, que incluye la extensión. Por supuesto, si alguien escribió una biblioteca en un paradigma diferente, tal vez no debas extenderla de todos modos.

    
respondido por el Spencer Rathbun 12.07.2011 - 20:09
2

Porque no saben nada mejor.

Los autores originales probablemente se aferran a un malentendido de los principios SOLID que se originaron en un mundo confuso y complicado de C ++.

Espero que se den cuenta de que los mundos ruby, python y perl no tienen los problemas que las respuestas aquí afirman ser la razón para el sellado. Tenga en cuenta que su escritura ortogonal a dinámica. Los modificadores de acceso son fáciles de trabajar en la mayoría de los idiomas (¿todos?). Los campos de C ++ se pueden eliminar mediante la conversión a algún otro tipo (C ++ es más débil). Java y C # pueden usar la reflexión. Los modificadores de acceso hacen que las cosas sean lo suficientemente difíciles como para evitar que lo hagas a menos que REALMENTE lo desees.

Sellar clases y marcar a cualquier miembro como privado viola explícitamente el principio de que las cosas simples deben ser simples y las difíciles deberían ser posibles. De repente, las cosas que deberían ser simples no lo son.

Le animo a tratar de entender el punto de vista de los autores originales. Gran parte de esto proviene de una idea académica de encapsulación que nunca ha demostrado un éxito absoluto en el mundo real. Nunca he visto un marco de trabajo o una biblioteca donde algún desarrollador en algún lugar no quisiera que funcionara de manera ligeramente diferente y no tenía una buena razón para cambiarlo. Hay dos posibilidades que pueden haber plagado a los desarrolladores de software originales que sellaron y convirtieron a los miembros en privados.

  1. Arrogancia: realmente creían que estaban abiertos para extensión y cerrados para modificación
  2. Complacencia: sabían que podría haber otros casos de uso, pero decidieron no escribir para esos casos de uso

Creo que en el marco corporativo wold, # 2 es probablemente el caso. Esos "marcos" de C ++, Java y .NET deben estar "hechos" y deben seguir ciertas pautas. Estas pautas generalmente significan tipos sellados, a menos que el tipo haya sido diseñado explícitamente como parte de una jerarquía de tipos y los miembros privados para muchas cosas que podrían ser útiles para el uso de otros ... pero como no se relacionan directamente con esa clase, no están expuestos. . Extraer un nuevo tipo sería demasiado costoso de soportar, documentar, etc ...

La idea completa detrás de los modificadores de acceso es que los programadores deben estar protegidos de sí mismos. "La programación en C es mala porque te permite dispararte en el pie". No es una filosofía con la que estoy de acuerdo como programador.

Prefiero el nombre de Python Mangling. Puedes fácilmente (mucho más fácil que la reflexión) reemplazar los datos privados si lo necesitas. Una gran reseña sobre el tema está disponible aquí: enlace

El modificador privado de Ruby en realidad es más como protegido en C # y no tiene un modificador privado como C #. Protegido es un poco diferente. Aquí hay una gran explicación: enlace

Recuerde, su lenguaje estático no tiene que ajustarse a los estilos anticuados del código escrito en ese idioma pasado.

    
respondido por el jrwren 13.07.2011 - 04:11
1

EDITAR: Creo que las clases deberían estar diseñadas para ser abiertas. Con algunas restricciones, pero no debe cerrarse por herencia.

Por qué: Las "clases finales" o las "clases selladas" me parecen extrañas. Nunca tengo que marcar una de mis propias clases como "final" ("sellada"), porque quizás tenga que heredar esas clases más adelante.

Compré / descargué bibliotecas de terceros con clases (la mayoría de los controles visuales), y realmente agradecí que ninguna de esas bibliotecas utilizara "final", incluso si era compatible con el lenguaje de programación, en el que estaban codificadas, porque terminé de extender esas clases, incluso si he compilado bibliotecas sin el código fuente.

A veces, tengo que lidiar con algunas clases "selladas" ocasionales, y terminé haciendo una nueva "envoltura" de clase que contiene la clase dada, que tiene miembros similares.

class MyBaseWrapper {
  protected FinalClass _FinalObject;

  public virtual DoSomething()
  {
    // these method cannot be overriden,
    // its from a "final" class
    // the wrapper method can  be open and virtual:
    FinalObject.DoSomething();
  }
} // class MyBaseWrapper


class MyBaseWrapper: MyBaseWrapper {

  public override DoSomething()
  {
    // the wrapper method allows "overriding",
    // a method from a "final" class:
    DoSomethingNewBefore();
    FinalObject.DoSomething();
    DoSomethingNewAfter();
  }
} // class MyBaseWrapper

Los clasificadores de alcance permiten "sellar" algunas características sin restringir la herencia.

Cuando cambié de Progtured estructurado. para POO, comencé a usar miembros de la clase privada , pero, después de muchos proyectos, terminé usando protegido y, finalmente, promoví esas propiedades o métodos a público , si es necesario.

P.S. No me gusta "final" como palabra clave en C #, prefiero usar "sellado" como Java o "estéril", siguiendo la metáfora de la herencia. Además, "final" es una palabra ambiguos usada en varios contextos.

    
respondido por el umlcat 13.07.2011 - 00:53

Lea otras preguntas en las etiquetas