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

12

Esta pregunta sobre cuándo usar el servicio privado y cuándo usarlo Protegido en clases me hizo pensar. (Extenderé esta pregunta también a las clases y métodos finales, ya que está relacionado. Estoy programando en Java, pero creo que esto es relevante para todos los lenguajes OOP)

La respuesta aceptada es:

  

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.   
  3. Haga que todos los métodos sean definitivos a menos que necesite crear una subclase y anularlos de inmediato.
  4.   
  5. 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 para la mayoría de los parámetros.   veces de todos modos.
  6.   

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

Podría nombrar muchas bibliotecas y situaciones, donde

  • Una biblioteca se amplió de una forma que los desarrolladores nunca habrían pensado
  • Esto se tuvo que hacer con "magia del cargador de clases" y otros hacks debido a restricciones de visibilidad
  • Las bibliotecas se utilizaron de una manera en la que no se crearon y la funcionalidad necesaria se "piratea" en
  • No se pudieron usar las bibliotecas debido a un pequeño problema (error, funcionalidad faltante, comportamiento "incorrecto") que no se pudo cambiar debido a la menor visibilidad
  • Un problema que no pudo solucionarse dio lugar a soluciones enormes, feas y con errores en las que la anulación de una función simple (que fue privada o definitiva) 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 que 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 mantiene esto si el código está destinado a ser usado por otros desarrolladores, donde es improbable que el desarrollador original haya pensado de antemano en todos los posibles casos de uso y sea difícil / imposible hacer cambios / refactores?

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 este tipo de proyectos con lenguajes orientados a objetos?

    
pregunta piegames 23.08.2017 - 14:05

3 respuestas

15

La desafortunada verdad es que muchas bibliotecas están escritas , no diseñadas . Esto es triste, porque un poco de pensamiento previo puede prevenir muchos problemas en el futuro.

Si nos proponemos diseñar una biblioteca, habrá algunos conjuntos 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. Así que 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, es posible que la fuente no esté disponible, y para las bibliotecas de código abierto puede no ser conveniente mantener una versión bifurcada. Puede que no sea factible fusionar adaptaciones altamente específicas en el proyecto ascendente.

Aquí es donde entra el principio de abrir y cerrar: 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 OOP clásicos son algunos de ellos. En general, especificamos enlaces donde el código de usuario se puede conectar de forma segura a la biblioteca y agregar funcionalidad.

El simple hecho de hacer público cada método o permitir que cada clase sea subclasificada no es suficiente para lograr la extensibilidad. En primer lugar, es realmente difícil extender la biblioteca si no está claro dónde el usuario puede conectarse a la biblioteca. P.ej. 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.

Y, lo que es más importante, una vez que algo forma parte de la API pública, no se puede recuperar. No puede refactorizarlo sin romper el código de flujo 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 hay necesidad de ellas es un enfoque más seguro. Si bien esa es una forma sensata de abordar la evolución a largo plazo de una biblioteca, no es satisfactoria para los usuarios que necesitan usar la biblioteca en este momento .

Entonces, ¿qué sucede 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! ¡Arreglará todos esos errores de diseño! También tardará más de lo esperado, en muchos casos se desvanece. Y si la nueva versión es muy diferente a la versión anterior, puede ser difícil animar a los usuarios a migrar. Entonces te quedas manteniendo dos versiones incompatibles.

    
respondido por el amon 23.08.2017 - 14:59
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 administran hasta cierto punto) en función de lo que usted puede apoyar razonablemente.

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

  • Las listas de correo discuten las necesidades de los usuarios y cómo implementar cosas
  • Los sistemas de seguimiento de problemas (problemas de JIRA o Git, etc.) hacen un seguimiento de los errores y las solicitudes de funciones
  • El control de versión administra el código fuente.

En proyectos maduros, lo que verás es algo como esto:

  1. Alguien quiere hacer algo con la biblioteca para la cual no fue diseñado originalmente.
  2. Agregan un ticket al seguimiento de problemas
  3. El equipo puede discutir el problema en la lista de correo o en los comentarios, y siempre se invita al solicitante a unirse a la discusión
  4. El cambio de API se acepta y se prioriza o se rechaza por alguna razón

En ese momento, si el cambio fue aceptado pero el usuario desea acelerarlo para que se arregle, puede hacer el trabajo y enviar una solicitud de extracción o un parche (según la herramienta de control de versión).

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

    
respondido por el Berin Loritsch 23.08.2017 - 14:51
0

Volveré a redactar mi respuesta, ya que parece que golpeó un nervio con algunas personas.

La visibilidad de la propiedad / método de la 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 utilizar bloqueos / monitores junto con captadores y configuradores que hacen que otros subprocesos 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 necesitas 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 interrumpirá si lo encapsuló desde el principio.

  1. Ayude a evitar que los usuarios se disparen a sí mismos con el uso eficiente de la interfaz. En esencia, te ayuda a controlar las invariantes del objeto.

Cada objeto tiene un montón de cosas que requiere que sean verdaderas para estar en un estado consistente. Desafortunadamente, estas cosas viven en el espacio visible del cliente porque es caro mover cada objeto a su propio proceso y hablar con él a través de mensajes. Esto significa que es muy fácil para un objeto bloquear 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 en sus servicios, lo que evita accidentes 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 Mucho más robusto. Esto no significa que el usuario no pueda corromper intencionalmente las 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 deben almacenarse en el lado del cliente ).

Otro buen ejemplo donde puedes mejorar la usabilidad de tus 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 arroje un error de tiempo de compilación que usted 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 puede construirlo, o tomar una función de devolución de llamada para manejar el error, lo que hace que el programa sea más fácil de usar.

  1. Contaminación del alcance

Muchas clases tienen muchos estados y métodos y es fácil sentirse abrumado tratando de desplazarse a través de ellos; Muchos de estos métodos son solo ruido visual, como funciones de ayuda, estado. hacer que las variables y los métodos sean privados ayuda a reducir la contaminación del alcance y facilita que el usuario encuentre los servicios que está buscando.

En esencia, te permite salir adelante 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 conjunto de servicios que el usuario nunca debería usar, por lo que puede salirse con los métodos de desglose en un montón de métodos de ayuda (aunque seguirá contaminando 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 se darán cuenta.

Por otra parte, si no hace esto, puede caer fácilmente en hacer que sea imposible cambiar sus dependencias, ya que están expuestas y el código depende de ellas. Con un sistema lo suficientemente grande, el costo de la migración puede llegar a ser inasequible, mientras que un encapsulado puede proteger a los usuarios clientes que se comportan bien de las decisiones futuras para cambiar las dependencias.

    
respondido por el Dmitry 23.08.2017 - 14:17

Lea otras preguntas en las etiquetas