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)
Una buena regla general es: hacer que todo sea lo más privado posible.
- Haga que todas las clases sean finales a menos que necesite subclasificarlas de inmediato.
- Haga que todos los métodos sean finales a menos que necesite subclasificarlos y anularlos de inmediato.
- 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?
Respuestas:
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.
fuente
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:
En proyectos maduros, lo que verá es algo en este sentido:
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.
fuente
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:
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 .
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.
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).
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.
fuente