Acerca de la programación orientada a objetos y C ++

¡Hola, Habr!



Nos gustaría llamar su atención sobre un artículo, cuyo autor no aprueba un enfoque puramente orientado a objetos cuando se trabaja con el lenguaje C ++. Te pedimos que evalúes, si es posible, no solo la argumentación del autor, sino también la lógica y el estilo.



Últimamente se ha escrito mucho sobre C ++ y hacia dónde se dirige el lenguaje y cuánto de lo que se llama "C ++ moderno" simplemente no es una opción para los desarrolladores de juegos.



Si bien comparto plenamente este punto de vista, tiendo a ver la evolución de C ++ como el resultado de arraigar ideas omnipresentes que guían a la mayoría de los desarrolladores. En este artículo, intentaré organizar algunas de estas ideas junto con mis propios pensamientos, y tal vez consiga algo delgado.



La programación orientada a objetos (OOP) como herramienta



Aunque C ++ se describe como un lenguaje de programación de múltiples paradigmas , en la práctica la mayoría de los programadores usan C ++ puramente como un lenguaje orientado a objetos (la programación genérica se usa para "complementar" la programación orientada a objetos).



Se supone que la programación orientada a objetos es una herramienta, uno de los muchos paradigmas que un programador puede usar para resolver problemas en el código. Sin embargo, en mi experiencia, la mayoría de los profesionales aceptan la programación orientada a objetos como el estándar de oro para el desarrollo de software. Básicamente, el desarrollo de una solución comienza determinando qué objetos necesitamos. La solución a un problema específico comienza después de que el código se ha distribuido entre los objetos. Con la transición a este tipo de pensamiento orientado a objetos, la POO pasa de ser una herramienta a una caja de herramientas completa.



Sobre la entropía como la fuerza secreta que alimenta el desarrollo de software



Me gusta pensar en una solución OOP como una constelación: es un grupo de objetos con líneas dibujadas al azar entre ellos. Tal solución también se puede considerar como un gráfico en el que los objetos son nodos y las relaciones entre ellos son bordes, pero el fenómeno de un grupo / cúmulo, que es transmitido por la metáfora de la constelación, está más cerca de mí (en comparación con él, el gráfico es demasiado abstracto).



Pero no me gusta la forma en que se componen tales "constelaciones de objetos". Según tengo entendido, cada una de estas constelaciones no es más que una instantánea de la imagen que se ha formado en la cabeza del programador y refleja cómo se ve el espacio de la solución en un momento particular. Incluso teniendo en cuenta todas las promesas que se dan en el diseño orientado a objetos sobre extensibilidad, reutilización, encapsulación, etc ... el futuro es impredecible, por lo que en cada caso podemos ofrecer una solución para exactamente el problema al que nos enfrentamos ahora.



Deberíamos alentarnos de que estamos "simplemente" resolviendo el problema que está directamente ante nosotros, pero en mi experiencia, un programador que utiliza principios de diseño en el espíritu de OOP crea una solución, mientras se ve limitado por la suposición de que el problema en sí no cambiará significativamente y en consecuencia, la solución puede considerarse permanente. Quiero decir que a partir de aquí se empieza a hablar de la solución en términos de los objetos que forman la constelación antes mencionada, y no en términos de datos y algoritmos; el problema en sí se abstrae.

Sin embargo, el programa está sujeto a entropía no menos que cualquier otro sistema y, por lo tanto, todos sabemos que el código cambiará. Además, de forma impredecible. Pero para mí, en este caso, está absolutamente claro que el código se degradará en cualquier caso, deslizándose hacia el caos y el desorden, si no lo luchas conscientemente.



He visto este manifiesto de muchas formas diferentes en las soluciones de programación orientada a objetos:



  • Aparecen nuevos niveles intermedios en la jerarquía, mientras que originalmente no se pretendía introducirlos.
  • Se agregan nuevas funciones virtuales con implementaciones vacías en la mayor parte de la jerarquía.
  • Uno de los objetos de la constelación requiere más procesamiento del planeado, por lo que las conexiones entre los otros objetos comienzan a deslizarse.
  • , , , .
  • .…


Todos estos son ejemplos de extensibilidad organizada incorrectamente. Además, el resultado es siempre el mismo, puede llegar en unos meses, o quizás en unos años. Con la ayuda de la refactorización, están tratando de eliminar las violaciones de los principios de diseño de OOP, que se hicieron cuando se agregaron nuevos objetos a la constelación, y se agregaron debido a la reformulación del problema en sí. A veces, la refactorización ayuda. Por un momento. La entropía es constante y los programadores no tienen tiempo para refactorizar cada constelación de OOP para superarla, por lo que cualquier proyecto se encuentra regularmente en la misma situación, cuyo nombre es caos.



En el ciclo de vida de cualquier proyecto POO, tarde o temprano llega un punto después del cual es imposible mantenerlo. Normalmente, en ese momento, se debe tomar una de dos acciones:



  • « »: - . , , , , , .
  • : -, , , .


Tenga en cuenta: la opción con una caja negra aún requerirá reescritura en caso de que el desarrollo de nuevas funciones deba continuar y / o persista la necesidad de eliminar errores.



La situación con la reescritura de soluciones nos devuelve al fenómeno de una instantánea del espacio de solución disponible en un momento determinado. Entonces, ¿qué ha cambiado entre el Diseño POO # 1 y la situación actual? Básicamente, eso es todo. El problema ha cambiado, por lo tanto, se requiere una solución diferente.



Mientras escribíamos la solución siguiendo los principios del diseño de programación orientada a objetos, resumimos el problema y, tan pronto como cambió, nuestra solución se vino abajo como un castillo de naipes.

Creo que es en este momento cuando empezamos a preguntarnos qué salió mal, tratamos de ir al revés y actualizar las estrategias para resolver el problema en base a los resultados del post mortem (debriefing). Sin embargo, cada vez que me encuentro con un escenario de "tiempo para reescribir", nada cambia: los principios de POO se utilizan de nuevo, de acuerdo con lo cual se implementa una nueva instantánea, correspondiente al estado actual del espacio del problema. Se repite todo el ciclo.



Facilidad de eliminación de código como principio de diseño



En cualquier sistema construido sobre el principio de OOP, son los objetos de la "constelación" los que reciben la atención principal. Pero creo que las relaciones entre los objetos son tan importantes, si no más, que los objetos mismos.



Prefiero soluciones simples en las que el gráfico de dependencia del código consta del número mínimo de nodos y bordes. Cuanto más simple sea la solución, más fácil será no solo cambiarla, sino también eliminarla. También descubrí que cuanto más fácil es eliminar el código, más rápido se puede reorientar la solución y adaptarla a las condiciones cambiantes del problema. Al mismo tiempo, el código se vuelve más resistente a la entropía, ya que se necesita mucho menos esfuerzo para mantenerlo en orden y evitar que se deslice hacia el caos.



Sobre el rendimiento por definición



Pero una de las principales consideraciones para evitar el diseño OOP es el rendimiento. Cuanto más código necesite ejecutar, peor será el rendimiento.



También es imposible no notar que las características de OOP, por definición, no brillan con el rendimiento. Implementé una jerarquía OOP simple con una interfaz y dos clases derivadas que anulan una sola llamada de función virtual pura en el Explorador de compiladores .



El código de este ejemplo imprime “¡Hola, mundo!” O no, dependiendo del número de argumentos pasados ​​al programa. En lugar de programar directamente todo lo que acabo de describir, se utilizará uno de los patrones estándar de diseño de OOP, la herencia, para resolver este problema en el código.



En este caso, lo más llamativo es la cantidad de código que generan los compiladores, incluso después de la optimización. Luego, mirando de cerca, puede ver cuán costoso y al mismo tiempo inútil dicho mantenimiento: cuando se pasa un número de argumentos distinto de cero al programa, el código aún asigna memoria (llamada new), carga las direcciones de vtableambos objetos, carga la dirección de la función Work()para ImplBy salta a ella, de modo que inmediatamente volver, ya que no hay nada que hacer allí. Finalmente, se llama deletepara liberar la memoria asignada.



Ninguna de estas operaciones fue necesaria en absoluto, pero el procesador las realizó todas correctamente.



Así, si uno de los objetivos primordiales de tu producto es el logro de un alto rendimiento (extraño si fuera de otro modo), entonces en el código debes evitar operaciones costosas innecesarias, prefiriendo las sencillas, que sean fáciles de juzgar, y utilizar construcciones que ayuden a lograr este objetivo.



Tome Unity , por ejemplo . Como parte de su práctica reciente, el rendimiento es correcto usando C #, un lenguaje orientado a objetos, ya que este lenguaje ya se usa en el motor mismo. Sin embargo, se decidieron por un subconjunto de C # , además, uno que no está rígidamente ligado a la programación orientada a objetos, y sobre esta base crean construcciones perfeccionadas para un alto rendimiento.



Dado que el trabajo de un programador es resolver problemas usando una computadora, es impensable que nuestra empresa dedique tan poca atención a escribir código que realmente haga que el procesador haga el trabajo en el que el procesador es particularmente bueno.



Sobre la lucha contra los estereotipos



En el artículo de Angelo Pesce "La complicación excesiva es la raíz de todo mal " , el autor da en el clavo (consulte la última sección: Personas) al admitir que la mayoría de los problemas de software son en realidad factores humanos.



Las personas del equipo deben interactuar y desarrollar un entendimiento común de cuál es el objetivo general y cuál es el camino para lograrlo. Si hay desacuerdo en el equipo, por ejemplo, sobre el camino hacia la meta, entonces para seguir avanzando es necesario desarrollar un consenso. Por lo general, esto no es difícil si las diferencias de opinión son pequeñas, pero es mucho más difícil de tolerar si las opciones difieren fundamentalmente, digamos "OOP o no OOP".

Cambiar de opinión no es fácil. Dudar de tu punto de vista, darte cuenta de lo equivocado que estabas y ajustar tu rumbo es difícil y doloroso. ¡Pero es mucho más difícil cambiar la opinión de otra persona!



He tenido muchas conversaciones con diferentes personas sobre la POO y sus problemas inherentes, y aunque creo que siempre he logrado explicar por qué pienso de esta manera y no de otra, no creo que haya logrado apartar a nadie de la POO.



Es cierto que a lo largo de los años de trabajo, he identificado tres argumentos principales para mí, debido a los cuales la gente no está lista para darle una oportunidad al otro lado:



  • « ». « ». « » . , , ( , - ). « …».
  • « , , , ». «» , , . , « ».
  • "Todo el mundo conoce la POO, es muy conveniente hablar con la gente en un idioma común, teniendo conocimientos generales". Este es un error lógico llamado "argumento a la gente", es decir, si casi todos los programadores usan los principios de la programación orientada a objetos, esta idea no puede ser inapropiada.


Soy plenamente consciente de que revelar errores lógicos en la argumentación no es suficiente para desacreditarlos. Sin embargo, creo que al ver las fallas en sus propios juicios, puede llegar al fondo de la verdad y encontrar la razón profunda por la que rechaza una idea inusual.



All Articles