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

44

Al leer varias preguntas de Stack Overflow y el código de otros, se cierra el consenso general de cómo diseñar 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 implementación, lo cual es una muy buena razón. Sin embargo, con la existencia de la protectedmayoría de los lenguajes OOP y el polimorfismo, esto no funciona.

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

  • No amplíe la clase, solo evite el problema que conduce a un código que es más complejo
  • Copie y pegue toda la clase, eliminando la reutilización del código
  • Bifurca el proyecto

Esas no son buenas opciones. ¿Por qué no se protectedusa en proyectos escritos en idiomas que lo admiten? ¿Por qué algunos proyectos prohíben explícitamente heredar de sus clases?

TheLQ
fuente
1
Estoy de acuerdo, he tenido este problema con los componentes de Java Swing, es realmente malo.
Jonas
20
Obl .
Shog9
3
Existe una buena posibilidad de que si tiene este problema, las clases se diseñaron mal en primer lugar, o tal vez esté tratando de usarlas incorrectamente. No le pide información a una clase, le pide que haga algo por usted, por lo tanto, generalmente no debería necesitar sus datos. Aunque esto no siempre es cierto, si encuentra que necesita acceder a los datos de una clase, hay muchas posibilidades de que algo salga mal.
Bill K
2
"la mayoría de los idiomas OOP?" Sé mucho más donde las clases no pueden cerrarse. Dejaste la cuarta opción: cambiar idiomas.
Kevin Cline
@Jonas Uno de los principales problemas de Swing es que expone demasiada implementación. Eso realmente mata el desarrollo.
Tom Hawtin - tackline

Respuestas:

55

Diseñar clases para que funcionen correctamente cuando se extiende, especialmente cuando el programador que hace la extensión no comprende completamente cómo se supone que funciona la clase, requiere un esfuerzo adicional considerable. No puede simplemente tomar todo lo que es privado y hacerlo público (o protegido) y llamarlo "abierto". Si permite que otra persona cambie el valor de una variable, debe considerar cómo afectarán todos los valores posibles a la clase. (¿Qué pasa si establecen la variable en nulo? ¿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.

Entonces, no es tanto que las clases no deberían estar abiertas, sino que a veces no vale la pena hacerlas abiertas.

Por supuesto, también es posible que los autores de la biblioteca solo fueran flojos. Depende de la biblioteca de la que estés hablando. :-)

Aaron
fuente
13
Sí, es por eso que las clases están selladas por defecto. Debido a que no puede predecir las muchas formas en que sus clientes pueden extender la clase, es más seguro sellarla que comprometerse a respaldar la cantidad potencialmente ilimitada de formas en que la clase podría ampliarse.
Robert Harvey
18
No tiene que predecir cómo se anulará su clase, solo debe suponer que las personas que extienden su clase saben lo que están haciendo. Si extienden la clase y establecen algo en nulo cuando no debería ser nulo, entonces es su culpa, no la suya. El único lugar donde este argumento tiene sentido es en aplicaciones súper críticas donde algo saldrá terriblemente mal si un campo es nulo.
TheLQ
55
@TheLQ: Esa es una suposición bastante grande.
Robert Harvey
99
@TheLQ De quién es la culpa es una pregunta muy subjetiva. Si establecieron una variable en un valor aparentemente razonable, y usted no documentó el hecho de que eso no estaba permitido, y su código no arrojó una ArgumentException (o equivalente), pero hace que su servidor se desconecte o conduzca a una hazaña de seguridad, entonces diría que es tanto tu culpa como la de ellos. Y si documentó los valores válidos y estableció una verificación de argumento, entonces ha presentado exactamente el tipo de esfuerzo del que estoy hablando, y tiene que preguntarse si ese esfuerzo fue realmente un buen uso de su tiempo.
Aaron
24

Hacer que todo sea privado por defecto suena duro, pero míralo desde el otro lado: cuando todo es privado por defecto, se supone que hacer algo público (o protegido, que es casi lo mismo) 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 que la interfaz permanezca sin cambios; y usted sabe exactamente en qué partes de la clase puede confiar y cuáles están sujetas 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, también se reduce la cantidad de dependencia cruzada entre ellos; 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 valen la pena extender a través de la herencia tienen miembros públicos y protegidos en los lugares adecuados, y ocultan todo lo demás.

tdammers
fuente
Eso tiene sentido. Sin embargo, todavía existe el problema cuando necesita algo muy similar pero con 1 o 2 cosas cambiadas en la implementación (funcionamiento interno de la clase). También me cuesta entender que si anula la clase, ¿ya no depende en gran medida de su implementación?
TheLQ
55
+1 por los beneficios del acoplamiento suelto. @TheLQ, es la interfaz de la que deberías depender mucho, no la implementación.
Karl Bielefeldt
19

Se supone que todo lo que no es privado existe más o menos con un comportamiento sin cambios en cada versión futura de la clase. De hecho, puede considerarse parte de la API, documentada o no. Por lo tanto, exponer demasiados detalles probablemente esté causando problemas de compatibilidad más adelante.

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

usuario281377
fuente
44
Esta es la verdadera respuesta. Otras respuestas tocan cosas importantes, pero la parte relevante real es esta: todo lo que no está finaly / o privateestá bloqueado en su lugar y nunca se puede cambiar .
Konrad Rudolph el
1
@Konrad: Hasta que los desarrolladores decidan renovar todo y no sean compatibles con versiones anteriores.
JAB
Eso es cierto, pero vale la pena señalar que es un requisito mucho más débil que para los publicmiembros; protectedlos miembros forman un contrato solo entre la clase que los expone y sus clases derivadas inmediatas; Por el contrario, los publicmiembros de contratos con todos los consumidores en nombre de todas las posibles clases heredadas presentes y futuras.
supercat
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 usarse con cuidado. Debe usar la herencia pública principalmente cuando tiene 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), para 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á tratando de usar. Probablemente los programadores querían asegurarse de que no heredarías una nueva clase de las suyas, porque esa no es la mejor manera de extender su código.

golpear
fuente
55
Gran punto con la composición frente a la herencia.
Zsolt Török
+1, pero nunca me gustó el ejemplo de "es una forma" para la herencia. Un rectángulo y un círculo podrían ser dos cosas muy diferentes. Me gusta más usar, un cuadrado es un rectángulo que está restringido. Los rectángulos se pueden usar como formas.
tylermac
@tylermac: de hecho. El caso Cuadrado / Rectángulo es en realidad uno de los contraejemplos del LSP. Probablemente, debería comenzar a pensar en un ejemplo más efectivo.
knulp
3
Cuadrado / Rectángulo es solo un contraejemplo si permite la modificación.
starblue
11

La mayoría de las respuestas tienen razón: las clases deben estar selladas (final, no superable, etc.) cuando el objeto no está diseñado o destinado a extenderse.

Sin embargo, no consideraría simplemente cerrar todo el diseño de código adecuado. La "O" en SOLID es para "Principio Abierto-Cerrado", indicando 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 anterior. En cambio, uno debería poder usar 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 cierto caso, 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 de escritura de archivo adicional al lado del código de escritura de la consola generalmente se consideraría algo malo.

En cambio, 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 también escribir en un archivo. O bien, 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 "transmitirá" cualquier llamada realizada a sus métodos a todos los escritores dados. El que elijas depende de lo que vas a necesitar; la derivación simple es, bueno, más simple, pero si tiene una previsión tenue de eventualmente necesitar enviar el mensaje a un cliente de red, tres archivos, dos canales con nombre y la consola, continúe y tome la molestia de extraer la interfaz y crear el "Adaptador en Y"; eso'

Ahora, si la función Write () nunca se declaró virtual (o se selló), ahora tiene problemas si no controla el código fuente. A veces incluso si lo haces. En general, esta es una mala posición para estar, y frustra a los usuarios de API de código cerrado o de código limitado cuando esto sucede. Pero, hay una razón legítima por la que Write (), o toda la clase ConsoleWriter, están sellados; puede haber información confidencial en un campo protegido (anulado a su vez desde 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 o benevolentes que el "usuario final" promedio del sistema final; de esa manera nunca te decepcionarás.

KeithS
fuente
Buen punto. La herencia puede ser buena o mala, por lo que es incorrecto decir que cada clase debe estar sellada. Realmente depende del problema en cuestión.
knulp
2

Creo que probablemente sea de todas las personas que usan un lenguaje oo para la programación en C. Te estoy mirando, Java. Si su martillo tiene la forma de un objeto, pero desea un sistema de procedimiento y / o no entiende realmente las clases, entonces su proyecto deberá bloquearse.

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.

Spencer Rathbun
fuente
77
Por cierto, OO y procesal son enfáticamente NO mutuamente excluyentes. No puedes tener OO sin procedimientos. Además, la composición es tanto un concepto OO como una extensión.
Michael K
@Michael, por supuesto que no. Dije que es posible que desee un sistema de procedimiento , es decir, uno sin conceptos OO. He visto a personas usar Java para escribir código puramente de procedimiento, y no funciona si intentas interactuar con él de una manera OO.
Spencer Rathbun
1
Eso podría resolverse si Java tuviera funciones de primera clase. Meh
Michael K
Es difícil enseñar idiomas Java o DOTNet a nuevos estudiantes. Por lo general, enseño Procedural con Pascal de procedimiento o "Plain C", y más tarde, cambio a OO con Object Pascal o "C ++". Y deja Java para más tarde. La programación de enseñanza con programas de procedimiento parece un solo objeto (singleton) que funciona bien.
umlcat el
@ Michael K: En OOP, una función de primera clase es solo un objeto con exactamente un método.
Giorgio
2

Porque no saben nada mejor.

Los autores originales probablemente se están aferrando a un malentendido de principios SÓLIDOS que se originó en un mundo C ++ confuso y complicado.

Espero que noten que los mundos rubí, pitón y perl no tienen los problemas que las respuestas aquí dicen ser la razón del sellado. Tenga en cuenta que es ortogonal a la escritura 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 modificar mediante la conversión a otro tipo (C ++ es más débil). Java y C # pueden usar la reflexión. Los modificadores de acceso hacen las cosas 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 deberían ser simples y las cosas difíciles deberían ser posibles. De repente, las cosas que deberían ser simples, no lo son.

Te animo a que intentes comprender 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 o biblioteca en la que algunos desarrolladores en algún lugar no quisieran que funcionara de manera ligeramente diferente y no tuvieran buenas razones para cambiarlo. Hay dos posibilidades que pueden haber afectado a los desarrolladores de software originales que sellaron y convirtieron a los miembros en privados.

  1. Arrogancia: realmente creían que estaban abiertos para la extensión y cerrados para la 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 mundial, # 2 es probablemente el caso. Esos marcos de trabajo de C ++, Java y .NET deben "terminarse" y deben seguir ciertas pautas. Esas pautas generalmente significan tipos sellados a menos que el tipo se haya diseñado explícitamente como parte de una jerarquía de tipos y 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 para soportar, documentar, etc.

Toda la idea detrás de los modificadores de acceso es que los programadores deben protegerse 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 enfoque de destrozar el nombre de Python. Puede reemplazar fácilmente los elementos privados (mucho más fácil que la reflexión) si lo necesita. Una gran reseña sobre ella está disponible aquí: http://bytebaker.com/2009/03/31/python-properties-vs-java-access-modifiers/

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: http://www.ruby-lang.org/en/documentation/ruby-from-other-languages/

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

jrwren
fuente
55
"la encapsulación nunca ha demostrado un éxito absoluto". Pensé que todos estarían de acuerdo en que la falta de encapsulación ha causado muchos problemas.
Joh
55
-1. Los tontos se apresuran donde los ángeles temen pisar. Celebrar la capacidad de cambiar las variables privadas muestra una falla grave en su estilo de codificación.
riwalk
2
Además de ser discutible y probablemente erróneo, esta publicación también es bastante arrogante e insultante. Bien hecho.
Konrad Rudolph
1
Hay dos escuelas de pensamiento cuyos defensores no parecen llevarse bien. La gente que escribe estática quiere que sea fácil razonar sobre el software, la gente dinámica quiere que sea fácil agregar funcionalidad. Cuál es mejor depende básicamente de la duración esperada del proyecto.
Joh
1

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

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

Compré / descargué bibliotecas de terceros con clases (la mayoría de los controles visuales), y estoy realmente agradecido de que ninguna de esas bibliotecas usara "final", incluso si es compatible con el lenguaje de programación, en el que estaban codificadas, porque terminé extendiendo 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 contactando a 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 Structured Progr. de programación orientada a objetos, que empecé a usar privados miembros de la clase, pero, después de muchos proyectos, terminé usando protegido , y, finalmente, la promoción de esas propiedades o métodos a pública , si necesariamente.

PD 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 ambigua utilizada en varios contextos.

umlcat
fuente
-1: Usted dijo que le gustan los diseños abiertos, pero esto no responde a la pregunta, que era por qué las clases no deberían estar diseñadas para ser abiertas.
Joh
@Joh Lo siento, no me expliqué bien, traté de expresar la opinión contraria, no debería estar sellado. Gracias por describir por qué no estás de acuerdo.
umlcat el