Conceptos erróneos comunes sobre la POO

Hola Habr!

Hoy, le espera una publicación traducida, que en cierta medida refleja nuestras búsquedas relacionadas con nuevos libros sobre OOP y FI. Por favor participe en la votación.



¿Está muerto el paradigma OOP? ¿Se puede decir que la programación funcional es el futuro? Parece que muchos artículos escriben sobre esto. Me inclino a estar en desacuerdo con este punto de vista. Hablemos

Cada pocos meses me encuentro con una publicación en algún blog donde la autora hace afirmaciones aparentemente bien fundamentadas sobre la programación orientada a objetos, después de lo cual declara que la POO es una reliquia del pasado, y todos tenemos que cambiar a la programación funcional.

Anteriormente, escribí que OOP y FI no se contradicen entre sí. Además, pude combinarlos con mucho éxito.

¿Por qué los autores de estos artículos tienen tantos problemas con la POO y por qué la FA les parece una alternativa tan obvia?

Cómo enseñar POO


Cuando se nos enseña OOP, generalmente enfatizan que se basa en cuatro principios: encapsulación , herencia , abstracción , polimorfismo . Son estos cuatro principios los que generalmente se critican en los artículos en los que los autores razonan sobre el declive de la OLP.

Sin embargo, OOP, como FI, es una herramienta. Para resolver problemas. Se puede consumir, también se puede abusar. Por ejemplo, al crear la abstracción incorrecta, abusa de la POO.
Entonces, la clase Square nunca debe heredar la clase Rectangle . En un sentido matemático, están, por supuesto, conectados. Sin embargo, desde el punto de vista de la programación, no están en una relación de herencia. El hecho es que los requisitos para un cuadrado son más estrictos que para un rectángulo. Mientras que en un rectángulo hay dos pares de lados iguales, un cuadrado debe tener todos los lados iguales.

Herencia


Analicemos la herencia con más detalle. Probablemente recuerde ejemplos de libros de texto con hermosas jerarquías de clases heredadas, y todas estas estructuras funcionan para resolver el problema. Sin embargo, en la práctica, la herencia no se usa con tanta frecuencia como la composición.
Considera un ejemplo. Digamos que tenemos una clase muy simple, un controlador en una aplicación web. La mayoría de los marcos modernos suponen que trabajará de esta manera:

 class BlogController extends FrameworkAbstractController { } 

Se supone que de esta manera será más fácil para usted hacer llamadas como this.renderTemplate(...) , ya que dichos métodos se heredan de la clase FrameworkAbstractController .

Como se indica en muchos artículos sobre este tema, aquí surgen una serie de problemas tangibles. Cualquier función interna en la clase base en realidad se convierte en una API. Ella ya no puede cambiar. Cualquier variable protegida del controlador base ahora estará más o menos relacionada con la API.

No hay nada de qué confundirse. Y si elegimos el enfoque con la composición y la inyección de dependencia, habría resultado así:

 class BlogController { public BlogController ( TemplateRenderer templateRenderer ) { } } 

Verá, ya no depende de un FrameworkAbstractController brumoso, sino de algo muy bien definido y limitado, TemplateRenderer . De hecho, BlogController no hereda de ningún otro controlador, ya que no hereda ningún comportamiento.

Encapsulación


La segunda característica a menudo criticada de OOP es la encapsulación. En el lenguaje literario, el significado de la encapsulación se formula de la siguiente manera: los datos y la funcionalidad se entregan juntos, y el estado interno de la clase se oculta del mundo exterior.

Esta oportunidad, nuevamente, permite su uso y abuso. El principal ejemplo de abuso en este caso es un estado con fugas.

Relativamente hablando, suponga que la clase List<> contiene una lista de elementos, y esta lista se puede cambiar. Creemos una clase para procesar una cesta de pedidos de la siguiente manera:

 class ShoppingCart { private List<ShoppingCartItem> items; public List<ShoppingCartItem> getItems() { return this.items; } } 

Aquí, en la mayoría de los lenguajes orientados a OOP, sucederá lo siguiente: la variable de elementos se devolverá por referencia. Por lo tanto, más podemos hacer esto:

 shoppingCart.getItems().clear(); 

Por lo tanto, en realidad borraremos la lista de artículos en la cesta, y ShoppingCart ni siquiera lo sabrá. Sin embargo, si observa de cerca este ejemplo, queda claro que el problema no está en el principio de la encapsulación. Aquí se viola este principio porque el estado interno se filtra de la clase ShoppingCart .

En este ejemplo en particular, el autor de la clase ShoppingCart podría usar la inmutabilidad para sortear el problema y asegurarse de que no se viole el principio de encapsulación.

Los programadores sin experiencia a menudo violan el principio de encapsulación de otra manera: introducen un estado en el que no es necesario. Tales programadores inexpertos a menudo usan variables de clase privada para transferir datos de una función a otra dentro de la misma clase, mientras que sería más apropiado usar Objetos de transferencia de datos para transferir una estructura compleja a otra función. Como resultado de tales errores, el código es innecesariamente complicado, lo que puede provocar errores.

En general, sería bueno prescindir del estado por completo: almacenar datos mutables en clases siempre que sea posible. Al hacerlo, debe garantizar una encapsulación confiable y asegurarse de que no haya fugas en ningún lado.

Abstracción


La abstracción, nuevamente, se entiende de muchas maneras de manera incorrecta. En ningún caso debe rellenar el código con clases abstractas y crear jerarquías profundas en él.

Si haces esto sin una buena razón, entonces solo estás buscando problemas en tu propia cabeza. No importa cómo se realice la abstracción, como una clase abstracta o como una interfaz; en cualquier caso, aparecerá una complejidad adicional en el código. Esta complejidad debe estar justificada.
En pocas palabras, una interfaz solo se puede crear si está dispuesto a pasar tiempo y documentar el comportamiento que se espera de la clase que lo implementa. Sí, me leíste bien. No basta con hacer una lista de las funciones que necesita implementar; también describa cómo (idealmente) deberían funcionar.

Polimorfismo


Finalmente, hablemos del polimorfismo. Sugiere que una clase puede implementar muchos comportamientos. Un mal ejemplo de libro de texto es escribir que el Square con polimorfismo puede ser un Rectangle o un Parallelogram . Como ya he señalado anteriormente, esto en la OOP es decididamente imposible, ya que los comportamientos de estas entidades son diferentes.

Hablando de polimorfismo, uno debe tener en cuenta los comportamientos , no el código . Un buen ejemplo es la clase Soldier en un juego de computadora. Puede implementar tanto el comportamiento Movable (situación: puede moverse) como el comportamiento Enemy (situación: te dispara). Por el contrario, la clase GunEmplacement solo puede implementar el comportamiento Enemy .

Entonces, si escribe Square implements Rectangle, Parallelogram , esta afirmación no se vuelve verdadera. Sus abstracciones deberían funcionar de acuerdo con la lógica empresarial. Debería pensar más en el comportamiento que en el código.

¿Por qué FP no es una bala de plata?


Entonces, cuando repetimos los cuatro principios básicos de OOP, pensemos en cuál es la característica de la programación funcional y por qué no usarla para resolver todos los problemas en su código.

Desde el punto de vista de muchos seguidores de FP, las clases son un sacrilegio , y el código debe presentarse en forma de funciones . Dependiendo del idioma, los datos se pueden transferir de una función a otra utilizando tipos primitivos, o en forma de uno u otro conjunto de datos estructurados (matrices, diccionarios, etc.).

Además, la mayoría de las funciones no deberían tener efectos secundarios. En otras palabras, no deben cambiar los datos en ningún lugar inesperado en el fondo, sino que solo funcionan con parámetros de entrada y producen resultados.

Este enfoque separa los datos de lo funcional : a primera vista, este FP difiere radicalmente de la POO. FP enfatiza que de esta manera el código sigue siendo simple. Si quieres hacer algo, escribe una función para este propósito, eso es todo.

Los problemas comienzan cuando algunas funciones deben depender de otras. Cuando la función A llama a la función B, y la función B llama a otras cinco o seis funciones, y al final se encuentra una función de relleno de cero que puede romperse; aquí es donde no será envidiado.

La mayoría de los programadores que se consideran defensores de FP aman FP por su simplicidad y no consideran que tales problemas sean serios. Esto es bastante honesto si su tarea es simplemente pasar el código y nunca volver a pensar en ello. Si desea construir una base de código que sea conveniente en el soporte, es mejor adherirse a los principios del código puro , en particular, aplicar la inversión de dependencia , en la cual el FI en la práctica también se vuelve mucho más complicado.

OOP o FP?


OOP y FI son herramientas . En definitiva, no importa qué paradigma de programación uses. Los problemas descritos en la mayoría de los artículos sobre este tema se relacionan con la organización del código.

En mi opinión, la macroestructura de la aplicación es mucho más importante. ¿Cuáles son los módulos que contiene? ¿Cómo intercambian información entre ellos? ¿Qué estructuras de datos son más comunes contigo? ¿Cómo se documentan? ¿Qué objetos son más importantes en términos de lógica empresarial?

Todos estos problemas no están relacionados de ninguna manera con el paradigma de programación utilizado; a nivel de dicho paradigma ni siquiera pueden resolverse. Un buen programador estudia el paradigma para dominar las herramientas que ofrece, y luego elige cuáles son las más adecuadas para resolver la tarea.

Source: https://habr.com/ru/post/469135/


All Articles