¿Cuál es la forma común de manejar la visibilidad en las bibliotecas?

12

Esta pregunta sobre cuándo usar privado y cuándo usar protegido en clases me hizo pensar. (Extenderé esta pregunta también a las clases y métodos finales, ya que está relacionada. Estoy programando en Java, pero creo que esto es relevante para cada lenguaje OOP)

La respuesta aceptada dice:

Una buena regla general es: hacer que todo sea lo más privado posible.

Y otro:

  1. Haga que todas las clases sean finales a menos que necesite subclasificarlas de inmediato.
  2. Haga que todos los métodos sean finales a menos que necesite subclasificarlos y anularlos de inmediato.
  3. Haga que todos los parámetros del método sean finales a menos que necesite cambiarlos dentro del cuerpo del método, lo cual es un poco incómodo la mayoría de las veces.

Esto es bastante sencillo y claro, pero ¿qué sucede si escribo principalmente bibliotecas (Open Source en GitHub) en lugar de aplicaciones?

Podría nombrar muchas bibliotecas y situaciones, donde

  • Una biblioteca se extendió de una manera que los desarrolladores nunca habrían pensado
  • Esto tuvo que hacerse con la "magia del cargador de clases" y otros hacks debido a restricciones de visibilidad
  • Las bibliotecas se utilizaron de una manera para la que no fueron creadas y la forma necesaria de funcionalidad "pirateada" en
  • Las bibliotecas no se pudieron utilizar debido a un pequeño problema (error, falta de funcionalidad, comportamiento "incorrecto") que no se pudo cambiar debido a la visibilidad reducida
  • Un problema que no se pudo solucionar condujo a soluciones alternativas enormes, feas y con errores en las que anular una función simple (que era privada o final) podría haber ayudado

Y en realidad comencé a nombrarlos hasta que la pregunta se hizo demasiado larga y decidí eliminarlos.

Me gusta la idea de no tener más código del necesario, más visibilidad de la necesaria, más abstracción de la necesaria. Y esto podría funcionar al escribir una aplicación para el usuario final, donde el código solo lo usan quienes lo escriben. Pero, ¿cómo se sostiene esto si el código está destinado a ser utilizado por otros desarrolladores, donde es improbable que el desarrollador original haya pensado de antemano en todos los posibles casos de uso y que los cambios / refactores sean difíciles / imposibles de hacer?

Dado que las grandes bibliotecas de código abierto no son algo nuevo, ¿cuál es la forma más común de manejar la visibilidad en tales proyectos con lenguajes orientados a objetos?

piegames
fuente
dado que usted pregunta sobre el código abierto, tiene aún menos sentido doblar los principios de codificación adecuados para abordar los problemas que enumeró que el código cerrado, simplemente porque uno puede contribuir con las correcciones necesarias directamente en el código de la biblioteca o bifurcarlo y crear su propia versión con cualesquiera que sean las correcciones que quieran
mosquito
2
mi punto no es sobre esto, sino sobre su referencia al código abierto que no tiene sentido en este contexto. Me imagino cómo las necesidades pragmáticas pueden justificar la desviación de principios estrictos en algunos casos (también conocido como acumulación de deuda técnica ), pero desde esta perspectiva no importa si el código es de código cerrado o de código abierto. O más precisamente, importa en la dirección opuesta a la que imaginaste aquí porque el código de código abierto puede hacer que estas necesidades sean menos apremiantes que cerradas porque ofrece opciones adicionales para abordarlas
mosquito
1
@piegames: estoy totalmente de acuerdo con el mosquito aquí, es mucho más probable que ocurran los problemas que detectó en bibliotecas de código cerrado ; si se trata de una biblioteca de sistema operativo con una licencia permisiva, si los encargados ignoran una solicitud de cambio, uno puede bifurcar la biblioteca y cambiar la visibilidad por sí mismo, si es necesario.
Doc Brown
1
@piegames: No entiendo tu pregunta. "Java" es un lenguaje, no una lib. Y si "su pequeña biblioteca de código abierto" tiene una visibilidad demasiado estricta, extender la visibilidad después normalmente no interrumpe la compatibilidad con versiones anteriores. Solo al revés.
Doc Brown

Respuestas:

15

La desafortunada verdad es que muchas bibliotecas se escriben , no se diseñan . Esto es triste, porque un poco de pensamiento previo puede evitar muchos problemas en el futuro.

Si nos proponemos diseñar una biblioteca, habrá un conjunto de casos de uso anticipados. Es posible que la biblioteca no satisfaga todos los casos de uso directamente, pero puede servir como parte de una solución. Por lo tanto, la biblioteca debe ser lo suficientemente flexible como para adaptarse.

La restricción es que generalmente no es una buena idea tomar el código fuente de la biblioteca y modificarlo para manejar el nuevo caso de uso. Para las bibliotecas propietarias, la fuente puede no estar disponible, y para las bibliotecas de código abierto puede ser indeseable mantener una versión bifurcada. Es posible que no sea factible fusionar adaptaciones altamente específicas en el proyecto aguas arriba.

Aquí es donde entra en juego el principio abierto-cerrado: la biblioteca debe estar abierta a la extensión sin modificar el código fuente. Eso no viene naturalmente. Este debe ser un objetivo de diseño intencional. Hay una gran cantidad de técnicas que pueden ayudar aquí, los patrones de diseño clásicos de OOP son algunas de ellas. En general, especificamos ganchos donde el código de usuario puede conectarse de forma segura a la biblioteca y agregar funcionalidad.

Hacer público cada método o permitir que cada clase se subclasifique no es suficiente para lograr la extensibilidad. En primer lugar, es realmente difícil extender la biblioteca si no está claro dónde puede engancharse el usuario a la biblioteca. Por ejemplo, anular la mayoría de los métodos no es seguro porque el método de la clase base se escribió con supuestos implícitos. Realmente necesitas diseñar para la extensibilidad.

Más importante aún, una vez que algo es parte de la API pública, no puede recuperarlo. No puede refactorizarlo sin romper el código descendente. La apertura prematura limita la biblioteca a un diseño subóptimo. Por el contrario, hacer que las cosas internas sean privadas pero agregar ganchos si más tarde es necesario es un enfoque más seguro. Si bien esa es una forma sensata de abordar la evolución a largo plazo de una biblioteca, esto no es satisfactorio para los usuarios que necesitan usar la biblioteca en este momento .

Entonces, ¿qué pasa en su lugar? Si hay un dolor significativo con el estado actual de la biblioteca, los desarrolladores pueden tomar todo el conocimiento sobre los casos de uso reales que se acumularon con el tiempo y escribir una Versión 2 de la biblioteca. ¡Será grandioso! ¡Solucionará todos esos errores de diseño! También tomará más tiempo de lo esperado, en muchos casos se esfumará. Y si la nueva versión es muy diferente a la versión anterior, puede ser difícil alentar a los usuarios a migrar. Luego te quedas manteniendo dos versiones incompatibles.

amon
fuente
Por lo tanto, necesito agregar ganchos para la extensión porque simplemente hacerlo público / reemplazable no es suficiente. Y también necesito pensar cuándo lanzar cambios / nueva API debido a la compatibilidad con versiones anteriores. Pero, ¿qué pasa con la visibilidad de los métodos en especial?
piegames
@piegames Con visibilidad, usted decide qué partes son públicas (parte de su API estable) y qué partes son privadas (sujetas a cambios). Si alguien evita eso con la reflexión, es su problema cuando esa característica se rompe en el futuro. Por cierto, los puntos de extensión a menudo tienen la forma de un método que se puede anular. Pero hay una diferencia entre un método que puede ser anulado, y un método que está destinado a ser anulado (véase también el Template Method).
amon
8

Cada clase / método público y extensible es una parte de su API que debe ser compatible. Limitar ese conjunto a un subconjunto razonable de la biblioteca permite la mayor estabilidad y limita la cantidad de cosas que pueden salir mal. Es una decisión de gestión (e incluso los proyectos de OSS se gestionan hasta cierto punto) en función de lo que razonablemente puede respaldar.

La diferencia entre OSS y el código cerrado es que la mayoría de las personas están tratando de crear y desarrollar una comunidad alrededor del código para que más de una persona mantenga la biblioteca. Dicho esto, hay varias herramientas de administración disponibles:

  • Las listas de correo discuten las necesidades del usuario y cómo implementar las cosas.
  • Los sistemas de seguimiento de problemas (problemas de JIRA o Git, etc.) rastrean errores y solicitudes de funciones
  • El control de versiones gestiona el código fuente.

En proyectos maduros, lo que verá es algo en este sentido:

  1. Alguien quiere hacer algo con la biblioteca para la que no fue diseñada originalmente
  2. Añaden un ticket al seguimiento del problema
  3. El equipo puede discutir el tema en la lista de correo o en los comentarios, y el solicitante siempre está invitado a unirse a la discusión.
  4. El cambio de API es aceptado y priorizado o rechazado por alguna razón

En ese momento, si el cambio fue aceptado pero el usuario quiere acelerar su reparación, puede hacer el trabajo y enviar una solicitud de extracción o un parche (dependiendo de la herramienta de control de versiones).

Ninguna API es estática. Sin embargo, su crecimiento tiene que ser conformado de alguna manera. Al mantener todo cerrado hasta que haya una necesidad demostrada de abrir las cosas, evitará la reputación de una biblioteca con errores o inestable.

Berin Loritsch
fuente
1
Totalmente de acuerdo, y he practicado con éxito el proceso de solicitud de cambio que seleccionó para bibliotecas de código cerrado de terceros, así como para bibliotecas de código abierto.
Doc Brown
En mi experiencia, incluso los pequeños cambios en las bibliotecas pequeñas son mucho trabajo (incluso si es solo para convencer a los demás) y pueden tomar algo de tiempo (esperar la próxima versión si no puede permitirse el lujo de usar una instantánea hasta ese momento). Entonces esto claramente no es una opción para mí. Sin embargo, me interesaría: ¿hay bibliotecas más grandes (en GitHub) que realmente utilicen ese concepto?
piegames
Siempre es mucho trabajo. Casi todos los proyectos a los que he contribuido tienen un proceso similar a ese. En mis días de Apache, podríamos discutir algo durante días porque nos apasionaba lo que creamos. Sabíamos que mucha gente iba a usar las bibliotecas, así que tuvimos que discutir cosas como si el cambio propuesto iba a romper la API, ¿vale la pena la característica propuesta? ¿Cuándo deberíamos hacerlo? etc.
Berin Loritsch
0

Reformularé mi respuesta ya que parece que afectó a algunas personas.

La visibilidad de propiedad / método de clase no tiene nada que ver con la seguridad ni la apertura de la fuente.

La razón por la que existe visibilidad es porque los objetos son frágiles a 4 problemas específicos:

  1. concurrencia

Si construye su módulo sin encapsular, sus usuarios se acostumbrarán a modificar el estado del módulo directamente. Esto funciona bien en un entorno de un solo subproceso, pero una vez que incluso piensa en agregar subprocesos; se verá obligado a hacer que el estado sea privado y a usar bloqueos / monitores junto con getters y setters que hagan que otros hilos esperen los recursos, en lugar de competir con ellos. Esto significa que los programas de sus usuarios ya no funcionarán porque no se puede acceder a las variables privadas de manera convencional. Esto puede significar que necesita muchas reescrituras.

La verdad es que es mucho más fácil codificar con un solo tiempo de ejecución de subprocesos en mente, y la palabra clave privada le permite simplemente agregar la palabra clave sincronizada o algunos bloqueos, y el código de sus usuarios no se romperá si lo encapsula desde el principio .

  1. Ayude a evitar que los usuarios se disparen en el uso de la interfaz a pie / aerodinámico. En esencia, te ayuda a controlar los invariantes del objeto.

Cada objeto tiene un montón de cosas que requiere para ser verdad para estar en estado consistente. Desafortunadamente, estas cosas viven en el espacio visible del cliente porque es costoso mover cada objeto a su propio proceso y hablar con él a través de mensajes. Esto significa que es muy fácil que un objeto bloquee todo el programa si el usuario tiene plena visibilidad.

Esto es inevitable, pero puede evitar poner accidentalmente un objeto en un estado inconsistente al cerrar la interfaz de sus servicios para evitar bloqueos accidentales al permitir que el usuario interactúe con el estado del objeto a través de una interfaz cuidadosamente diseñada que hace que el programa sea mucho más robusto . Esto no significa que el usuario no pueda corromper intencionalmente a los invariantes, pero si lo hacen, es su cliente el que falla, todo lo que tienen que hacer es reiniciar el programa (los datos que desea proteger no deberían almacenarse en el lado del cliente )

Otro buen ejemplo en el que puede mejorar la usabilidad de sus módulos es hacer que el constructor sea privado; porque si el constructor lanza una excepción, matará el programa. Un enfoque perezoso para resolver esto es hacer que el constructor le arroje un error de tiempo de compilación que no puede construirlo a menos que esté en un bloque try / catch. Al hacer que el constructor sea privado y agregar un método de creación estático público, puede hacer que el método de creación devuelva nulo si no lo construye, o tomar una función de devolución de llamada para manejar el error, haciendo que el programa sea más fácil de usar.

  1. Alcance de la contaminación

Muchas clases tienen muchos estados y métodos, y es fácil sentirse abrumado al tratar de desplazarse por ellas; Muchos de estos métodos son solo ruido visual, como funciones auxiliares, estado. Hacer que las variables y los métodos sean privados ayuda a reducir la contaminación del alcance y hace que sea más fácil para el usuario encontrar los servicios que está buscando.

En esencia, le permite salir con funciones de ayuda dentro de la clase en lugar de fuera de la clase; sin control de visibilidad sin distraer al usuario con un montón de servicios que el usuario nunca debería usar, por lo que puede evitar dividir los métodos en un montón de métodos auxiliares (aunque aún contaminará su alcance, pero no el del usuario).

  1. estar atado a dependencias

Una interfaz bien diseñada puede ocultar sus bases de datos internas / ventanas / imágenes de las que depende para hacer su trabajo, y si desea cambiar a otra base de datos / otro sistema de ventanas / otra biblioteca de imágenes, puede mantener la interfaz igual y los usuarios No lo notaremos.

Por otro lado, si no hace esto, puede caer en la imposibilidad de cambiar sus dependencias, ya que están expuestas y el código depende de ello. Con un sistema lo suficientemente grande, el costo de la migración puede volverse inasequible, mientras que encapsularlo puede proteger a los usuarios clientes que se comportan bien de futuras decisiones para intercambiar dependencias.

Dmitry
fuente
1
"No tiene sentido esconder nada", entonces, ¿por qué pensar en encapsular? En muchos contextos, la reflexión requiere privilegios especiales.
Frank Hileman el
Piensa en la encapsulación porque le da espacio para respirar mientras desarrolla su módulo y reduce la probabilidad de mal uso. Por ejemplo, si tiene 4 subprocesos que modifican el estado interno de una clase directamente, fácilmente causará problemas, mientras que hacer que la variable sea privada, alienta al usuario a usar los métodos públicos para manipular el estado mundial, que puede usar monitores / bloqueos para evitar problemas . Este es el único beneficio real de la encapsulación.
Dmitry el
Ocultar cosas por razones de seguridad es una manera fácil de hacer un diseño en el que termines teniendo agujeros en tu API. Un buen ejemplo de esto son las aplicaciones de documentos múltiples, donde tiene muchas cajas de herramientas y muchas ventanas con subventanas. Si te vuelves loco con la encapsulación, terminarás teniendo una situación en la que dibujar algo en un documento, tienes que pedirle a la ventana que le pida al documento interno que le pida al documento interno que le pida al documento interno que haga una solicitud para dibujar algo e invalidar su contexto. Si el lado del cliente quiere jugar con el lado del cliente, no puede evitarlos.
Dmitry
Bien, eso tiene más sentido, aunque la seguridad se puede lograr a través del control de acceso si el entorno lo admite, y fue uno de los objetivos originales en el diseño del lenguaje OO. También está promoviendo la encapsulación y diciendo que no la use al mismo tiempo; un poco confuso.
Frank Hileman el
Nunca quise no usarlo; Quise decir que no lo uses por seguridad; úselo estratégicamente para mejorar la experiencia de sus usuarios y para darse un entorno de desarrollo más fluido. Mi punto es que no tiene nada que ver con la seguridad ni la apertura de la fuente. Los objetos del lado del cliente son, por definición, vulnerables a la introspección, y sacarlos del espacio de proceso del usuario hace que las cosas no encapsuladas sean igualmente inaccesibles como encapsuladas.
Dmitry