¿Nada de lo que muta finalmente manipula el estado?
Sí, pero si está detrás de una función miembro de una clase pequeña que es la única entidad en todo el sistema que puede manipular su estado privado, entonces ese estado tiene un alcance muy limitado.
¿Qué debe tener que lidiar con el menor estado posible?
Desde el punto de vista de la variable: tan pocas líneas de código deberían poder acceder a ella como sea posible. Limite el alcance de la variable al mínimo.
Desde el punto de vista de la línea de código: se debe poder acceder a la menor cantidad de variables posible desde esa línea de código. Reducir el número de variables que la línea de código puede , posiblemente, el acceso (no importa incluso que mucho si se hace acceder a ella, lo único que importa es si es posible ).
Las variables globales son muy malas porque tienen un alcance máximo. Incluso si se accede desde 2 líneas de código en una base de código, desde la línea de POV del código, siempre se puede acceder a una variable global. Desde el punto de vista de la variable, se puede acceder a una variable global con enlace externo a cada línea de código en toda la base de código (o cada línea de código que incluya el encabezado de todos modos). A pesar de que solo se tiene acceso a través de 2 líneas de código, si la variable global es visible a 400,000 líneas de código, su lista inmediata de sospechosos cuando encuentre que se configuró en un estado no válido tendrá 400,000 entradas (quizás se reduzca rápidamente a 2 entradas con herramientas, pero sin embargo, la lista inmediata tendrá 400,000 sospechosos y ese no es un punto de partida alentador).
Asimismo, es probable que incluso si una variable global comienza a modificarse solo por 2 líneas de código en toda la base de código, la desafortunada tendencia de las bases de código a evolucionar hacia atrás tenderá a aumentar drásticamente ese número, simplemente porque puede aumentar tantas Los desarrolladores, frenéticos por cumplir con los plazos, ven esta variable global y se dan cuenta de que pueden tomar atajos a través de ella.
En un lenguaje impuro como C ++, ¿no es realmente la administración del estado lo que está haciendo?
En gran medida, sí, a menos que esté usando C ++ de una manera muy exótica que lo haga lidiar con estructuras de datos inmutables personalizadas y programación funcional pura en todo momento: a menudo también es la fuente de la mayoría de los errores cuando la administración de estado se vuelve compleja y la complejidad es a menudo una función de la visibilidad / exposición de ese estado.
¿Y cuáles son otras formas de lidiar con el menor estado posible además de limitar la vida útil variable?
Todos estos están en el ámbito de limitar el alcance de una variable, pero hay muchas maneras de hacer esto:
- Evite variables globales sin procesar como la peste. Incluso alguna función global setter / getter tonta reduce drásticamente la visibilidad de esa variable, y al menos permite alguna forma de mantener invariantes (por ejemplo: si nunca se debe permitir que la variable global sea un valor negativo, el setter puede mantener esa invariante). Por supuesto, incluso un diseño setter / getter además de lo que de otra manera sería una variable global es un diseño bastante pobre, mi punto es que todavía es mucho mejor.
- Haga sus clases más pequeñas cuando sea posible. Una clase con cientos de funciones miembro, 20 variables miembro y 30,000 líneas de código que lo implementarían tendría variables privadas más bien "globales", ya que todas esas variables serían accesibles para sus funciones miembro que consisten en 30k líneas de código. Se podría decir que la "complejidad del estado" en ese caso, al descontar las variables locales en cada función miembro, es
30,000*20=600,000
. Si hubiera 10 variables globales accesibles además de eso, entonces la complejidad del estado podría ser similar 30,000*(20+10)=900,000
. Una "complejidad de estado" saludable (mi tipo personal de métrica inventada) debería estar en los miles o menos para las clases, no en decenas de miles, y definitivamente no en cientos de miles. Para funciones gratuitas, digamos cientos o menos antes de comenzar a tener serios dolores de cabeza en el mantenimiento.
- En la misma línea que anteriormente, no implemente algo como una función miembro o una función amiga que de otra manera puede ser no miembro, no amigo usando solo la interfaz pública de la clase. Dichas funciones no pueden acceder a las variables privadas de la clase y, por lo tanto, reducen el potencial de error al reducir el alcance de esas variables privadas.
- Evite declarar variables mucho antes de que realmente se necesiten en una función (es decir, evite el estilo C heredado que declara todas las variables en la parte superior de una función, incluso si solo se necesitan muchas líneas a continuación). Si de todos modos usa este estilo, al menos procure funciones más cortas.
Más allá de las variables: efectos secundarios
Muchas de estas pautas que enumeré anteriormente abordan el acceso directo al estado (variables) sin procesar y mutable. Sin embargo, en una base de código suficientemente compleja, limitar el alcance de las variables sin procesar no será suficiente para razonar fácilmente sobre la corrección.
Podría tener, por ejemplo, una estructura de datos central, detrás de una interfaz abstracta totalmente SÓLIDA, totalmente capaz de mantener perfectamente invariantes, y aún así terminar sufriendo mucho debido a la amplia exposición de este estado central. Un ejemplo de estado central que no es necesariamente accesible a nivel mundial sino que simplemente es ampliamente accesible es el gráfico de escena central de un motor de juego o la estructura de datos de la capa central de Photoshop.
En tales casos, la idea de "estado" va más allá de las variables en bruto, y solo a las estructuras de datos y cosas por el estilo. Asimismo, ayuda a reducir su alcance (reducir el número de líneas que pueden llamar a funciones que las mutan indirectamente).
Tenga en cuenta cómo marqué deliberadamente incluso la interfaz como roja aquí, ya que desde el nivel arquitectónico amplio y alejado, el acceso a esa interfaz sigue siendo un estado mutante, aunque indirectamente. La clase puede mantener invariantes como resultado de la interfaz, pero eso solo va tan lejos en términos de nuestra capacidad de razonar sobre la corrección.
En este caso, la estructura de datos central está detrás de una interfaz abstracta que puede no ser accesible globalmente. Simplemente se puede inyectar y luego mutar indirectamente (a través de funciones miembro) de una gran cantidad de funciones en su base de código compleja.
En tal caso, incluso si la estructura de datos mantiene perfectamente sus propios invariantes, pueden ocurrir cosas extrañas en un nivel más amplio (por ejemplo: un reproductor de audio puede mantener todo tipo de invariantes como ese, el nivel de volumen nunca sale del rango de 0% a 100%, pero eso no lo protege del usuario que presiona el botón de reproducción y que tiene un clip de audio aleatorio que no sea el que cargó más recientemente, comienza a reproducirse como un evento, lo que hace que la lista de reproducción se reorganice de manera válida, pero comportamiento indeseado y fallido desde la perspectiva del usuario).
La forma de protegerse en estos escenarios complejos es "bloquear" los lugares en la base de código que pueden invocar funciones que finalmente causan efectos secundarios externos, incluso desde este tipo de visión más amplia del sistema que va más allá del estado bruto y más allá de las interfaces.
Por extraño que parezca, puede ver que no se está accediendo a ningún "estado" (mostrado en rojo, y esto no significa "variable en bruto", solo significa un "objeto" y posiblemente incluso detrás de una interfaz abstracta). . Cada función tiene acceso a un estado local al que también puede acceder un actualizador central, y el estado central solo es accesible para el actualizador central (por lo que ya no es central sino más bien de naturaleza local).
Esto es solo para bases de código realmente complejas, como un juego que abarca 10 millones de líneas de código, pero puede ser de gran ayuda para razonar sobre la corrección de su software y descubrir que sus cambios producen resultados predecibles, cuando limita significativamente el número de lugares que pueden mutar estados críticos en los que gira toda la arquitectura para funcionar correctamente.
Más allá de las variables sin procesar están los efectos secundarios externos, y los efectos secundarios externos son una fuente de error, incluso si se limitan a un puñado de funciones miembro. Si una gran cantidad de funciones puede llamar directamente a esas pocas funciones miembro, entonces hay una gran cantidad de funciones en el sistema que pueden causar indirectamente efectos secundarios externos, y eso aumenta la complejidad. Si solo hay un lugar en la base de código que tiene acceso a esas funciones miembro, y esa ruta de ejecución no se desencadena por eventos esporádicos en todo el lugar, sino que se ejecuta de una manera muy controlada y predecible, entonces reduce la complejidad.
Complejidad del estado
Incluso la complejidad del estado es un factor bastante importante a tener en cuenta. Una estructura simple, ampliamente accesible detrás de una interfaz abstracta, no es tan difícil de estropear.
Una estructura de datos de gráfico compleja que representa la representación lógica central de una arquitectura compleja es bastante fácil de estropear, y de una manera que ni siquiera viola las invariantes del gráfico. Un gráfico es muchas veces más complejo que una estructura simple, por lo que se vuelve aún más crucial en tal caso reducir la complejidad percibida de la base de código para reducir al mínimo absoluto el número de lugares que tienen acceso a dicha estructura gráfica. y donde ese tipo de estrategia de "actualización central" que se invierte en un paradigma de extracción para evitar empujones esporádicos y directos a la estructura de datos gráficos de todo el lugar realmente puede dar sus frutos.