Una breve descripción del diseño de software orientado a objetos

Demostrado al implementar las clases de un juego de rol

Zeppelin de Richard Wright

Introducción

La mayoría de los lenguajes de programación modernos admiten y fomentan la programación orientada a objetos (OOP). A pesar de que últimamente parece que estamos viendo un ligero cambio, ya que las personas comienzan a usar lenguajes que no están muy influenciados por OOP (como Go, Rust, Elixir, Elm, Scala), la mayoría todavía tiene objetos. Los principios de diseño que vamos a esbozar aquí se aplican también a los idiomas que no son OOP.

Para tener éxito al escribir código claro, de alta calidad, mantenible y extensible, necesitará conocer los principios de diseño que han demostrado su eficacia durante décadas de experiencia.

Divulgación: El ejemplo por el que vamos a estar pasando será en Python. Los ejemplos están ahí para probar un punto y pueden ser descuidados en otras formas obvias.

Tipos de objeto

Dado que vamos a modelar nuestro código en torno a objetos, sería útil diferenciar entre sus diferentes responsabilidades y variaciones.

Hay tres tipos de objetos:

1. Objeto de entidad

Este objeto generalmente corresponde a alguna entidad del mundo real en el espacio del problema. Digamos que estamos construyendo un juego de rol (RPG), un objeto de entidad sería nuestra clase de héroe simple:

Estos objetos generalmente contienen propiedades sobre sí mismos (como salud o maná) y se pueden modificar a través de ciertas reglas.

2. Objeto de control

Los objetos de control (a veces también llamados objetos Manager) son responsables de la coordinación de otros objetos. Estos son objetos que controlan y hacen uso de otros objetos. Un gran ejemplo en nuestra analogía de RPG sería la clase Fight, que controla a dos héroes y los hace luchar.

Encapsular la lógica de una pelea en una clase de este tipo te brinda múltiples beneficios: uno de los cuales es la fácil extensibilidad de la acción. Puedes pasar fácilmente un tipo de personaje que no sea jugador (NPC) para que el héroe pelee, siempre que exponga la misma API. También puede heredar fácilmente la clase y anular algunas de las funciones para satisfacer sus necesidades.

3. Objeto límite

Estos son objetos que se encuentran en el límite de su sistema. Cualquier objeto que recibe información de otro sistema o la produce, independientemente de si ese sistema es un Usuario, Internet o una base de datos, puede clasificarse como un objeto límite.

Estos objetos de límite son responsables de traducir la información dentro y fuera de nuestro sistema. En un ejemplo en el que tomamos comandos de usuario, necesitaríamos el objeto límite para traducir una entrada de teclado (como una barra espaciadora) en un evento de dominio reconocible (como un salto de caracteres).

Bonus: Objeto de valor

Los objetos de valor representan un valor simple en su dominio. Son inmutables y no tienen identidad.

Si fuéramos a incorporarlos a nuestro juego, una clase de Dinero o Daño sería una gran opción. Dichos objetos nos permiten distinguir, encontrar y depurar fácilmente la funcionalidad relacionada, mientras que el enfoque ingenuo de usar un tipo primitivo, una matriz de enteros o un entero, no lo hace.

Se pueden clasificar como una subcategoría de objetos de entidad.

Principios clave de diseño

Los principios de diseño son reglas en el diseño de software que han demostrado ser valiosas a lo largo de los años. Seguirlos estrictamente lo ayudará a garantizar que su software sea de la mejor calidad.

Abstracción

La abstracción es la idea de simplificar un concepto a lo esencial en algún contexto. Le permite comprender mejor el concepto reduciéndolo a una versión simplificada.

Los ejemplos anteriores ilustran la abstracción: observe cómo se estructura la clase Fight. La forma en que lo usa es lo más simple posible: le da dos héroes como argumentos en instanciación y llama al método fight (). Nada más y nada menos.

La abstracción en su código debe seguir la regla de la menor sorpresa. Su abstracción no debería sorprender a nadie con comportamiento / propiedades innecesarios y no relacionados. En otras palabras, debería ser intuitivo.

Tenga en cuenta que nuestra función Hero # take_damage () no hace algo inesperado, como eliminar nuestro personaje al morir. Pero podemos esperar que mate a nuestro personaje si su salud cae por debajo de cero.

Encapsulamiento

Se puede pensar que la encapsulación pone algo dentro de una cápsula: limita su exposición al mundo exterior. En software, restringir el acceso a objetos y propiedades internos ayuda con la integridad de los datos.

La lógica interna encapsula los recuadros negros y hace que sus clases sean más fáciles de administrar, porque sabe qué parte utilizan otros sistemas y qué no. Esto significa que puede reelaborar fácilmente la lógica interna mientras retiene las partes públicas y asegurarse de no haber roto nada. Como efecto secundario, trabajar con la funcionalidad encapsulada desde el exterior se vuelve más simple ya que tiene menos cosas en que pensar.

En la mayoría de los idiomas, esto se hace a través de los llamados modificadores de acceso (privados, protegidos, etc.). Python no es el mejor ejemplo de esto, ya que carece de estos modificadores explícitos integrados en el tiempo de ejecución, pero usamos convenciones para solucionar esto. El prefijo _ de las variables / métodos los denota como privados.

Por ejemplo, imagine que cambiamos nuestro método Fight # _run_attack para devolver una variable booleana que indica si la pelea ha terminado en lugar de generar una excepción. Sabremos que el único código que podríamos haber roto está dentro de la clase Fight, porque hicimos que el método fuera privado.

Recuerde, el código se cambia con más frecuencia que se escribe de nuevo. Poder cambiar su código con la menor repercusión posible y clara es la flexibilidad que desea como desarrollador.

Descomposición

La descomposición es la acción de dividir un objeto en varias partes más pequeñas separadas. Dichas partes son más fáciles de entender, mantener y programar.

Imagina que deseamos incorporar más funciones de RPG como beneficios, inventario, equipamiento y atributos de personaje en la parte superior de nuestro héroe:

Supongo que puedes decir que este código se está volviendo bastante desordenado. Nuestro objeto Hero está haciendo demasiadas cosas a la vez y este código se está volviendo bastante frágil como resultado de eso.

Por ejemplo, un punto de resistencia vale 5 de salud. Si alguna vez queremos cambiar esto en el futuro para que valga 6 de salud, tendremos que cambiar la implementación en varios lugares.

La respuesta es descomponer el objeto Héroe en múltiples objetos más pequeños, cada uno de los cuales abarca parte de la funcionalidad.

Una arquitectura mas limpia

Ahora, después de descomponer la funcionalidad de nuestro objeto Hero en HeroAttributes, HeroInventory, HeroEquipmenta y HeroBuff, agregar funciones futuras será más fácil, más encapsulado y mejor abstraído. Se puede decir que nuestro código es mucho más limpio y claro en lo que hace.

Hay tres tipos de relaciones de descomposición:

  • asociación: define una relación flexible entre dos componentes. Ambos componentes no dependen el uno del otro, pero pueden funcionar juntos.

Ejemplo: héroe y un objeto de zona.

  • agregación: define una relación débil "has-a" entre un todo y sus partes. Considerado débil, porque las partes pueden existir sin el todo.

Ejemplo: HeroInventory y Item.
Un HeroInventory puede tener muchos Artículos y un Artículo puede pertenecer a cualquier HeroInventory (como intercambiar artículos).

  • composición - Una fuerte relación "tiene-a" donde el todo y la parte no pueden existir el uno sin el otro. Las partes no se pueden compartir, ya que el todo depende de esas partes exactas.

Ejemplo: Hero y HeroAttributes.
Estos son los atributos del héroe: no puedes cambiar su propietario.

Generalización

La generalización podría ser el principio de diseño más importante: es el proceso de extraer características compartidas y combinarlas en un solo lugar. Todos conocemos el concepto de funciones y herencia de clase, ambos son una especie de generalización.

Una comparación podría aclarar las cosas: mientras que la abstracción reduce la complejidad al ocultar detalles innecesarios, la generalización reduce la complejidad al reemplazar múltiples entidades que realizan funciones similares con una sola construcción.

En el ejemplo dado, hemos generalizado la funcionalidad de nuestras clases comunes de Héroe y NPC en un ancestro común llamado Entidad. Esto siempre se logra a través de la herencia.

Aquí, en lugar de que nuestras clases NPC y Hero implementen todos los métodos dos veces y violen el principio DRY, redujimos la complejidad al mover su funcionalidad común a una clase base.

Como advertencia, no exagere la herencia. Muchas personas experimentadas recomiendan favorecer la composición sobre la herencia.

A menudo, los programadores aficionados abusan de la herencia, probablemente porque es una de las primeras técnicas de POO que captan debido a su simplicidad.

Composición

La composición es el principio de combinar múltiples objetos en uno más complejo. Dicho prácticamente: está creando instancias de objetos y utilizando su funcionalidad en lugar de heredarlo directamente.

Un objeto que usa composición puede llamarse un objeto compuesto. Es importante que este compuesto sea más simple que la suma de sus pares. Al combinar varias clases en una, queremos elevar el nivel de abstracción y hacer que el objeto sea más simple.

La API del objeto compuesto debe ocultar sus componentes internos y las interacciones entre ellos. Piense en un reloj mecánico, tiene tres manecillas para mostrar la hora y una perilla para configurar, pero internamente contiene docenas de partes móviles e interdependientes.

Como dije, la composición es preferible a la herencia, lo que significa que debes esforzarte por mover la funcionalidad común a un objeto separado que luego las clases usan, en lugar de guardarlo en una clase base que has heredado.

Vamos a ilustrar un posible problema con la funcionalidad de herencia excesiva:

Acabamos de agregar movimiento a nuestro juego.

Como aprendimos, en lugar de duplicar el código, utilizamos la generalización para colocar las funciones move_right y move_left en la clase Entity.

Bien, ¿y si quisiéramos introducir monturas en el juego?

una buena montura :)

Las monturas también necesitarían moverse hacia la izquierda y hacia la derecha, pero no tienen la capacidad de atacar. Ahora que lo pienso, ¡tal vez ni siquiera tengan salud!

Sé cuál es tu solución:

Simplemente mueva la lógica de movimiento a una clase separada MoveableEntity o MoveableObject que solo tenga esa funcionalidad. La clase Mount puede heredar eso.

Entonces, ¿qué hacemos si queremos monturas que tienen salud pero no pueden atacar? ¿Más división en subclases? Espero que puedan ver cómo nuestra jerarquía de clases comenzaría a volverse compleja a pesar de que nuestra lógica empresarial sigue siendo bastante simple.

Un enfoque algo mejor sería abstraer la lógica del movimiento en una clase de Movimiento (o algún nombre mejor) e instanciarla en las clases que pudieran necesitarla. Esto empaquetará muy bien la funcionalidad y la hará reutilizable en todo tipo de objetos no limitados a Entity.

¡Hurra, composición!

Descargo de responsabilidad de pensamiento crítico

A pesar de que estos principios de diseño se han formado a través de décadas de experiencia, sigue siendo extremadamente importante que pueda pensar críticamente antes de aplicar ciegamente un principio a su código.

Como todas las cosas, demasiado puede ser algo malo. A veces, los principios pueden llevarse demasiado lejos, puede ser demasiado inteligente con ellos y terminar con algo con lo que es más difícil trabajar.

Como ingeniero, su rasgo principal es su capacidad de evaluar críticamente el mejor enfoque para su situación única, no seguir ciegamente y aplicar reglas arbitrarias.

Cohesión, acoplamiento y separación de preocupaciones

Cohesión

La cohesión representa la claridad de las responsabilidades dentro de un módulo o, en otras palabras, su complejidad.

Si su clase realiza una tarea y nada más, o tiene un propósito claro, esa clase tiene una alta cohesión. Por otro lado, si no está claro en qué está haciendo o tiene más de un propósito, tiene poca cohesión.

Desea que sus clases tengan una alta cohesión. Deberían tener una sola responsabilidad y, si los encuentra teniendo más, podría ser el momento de dividirlos.

Acoplamiento

El acoplamiento captura la complejidad entre conectar diferentes clases. Desea que sus clases tengan conexiones tan pequeñas y simples con otras clases como sea posible, para que pueda intercambiarlas en eventos futuros (como cambiar los marcos web). El objetivo es tener un acoplamiento flojo.

En muchos idiomas, esto se logra mediante el uso intensivo de interfaces: abstraen la clase específica que maneja la lógica y representan una especie de capa adaptadora en la que cualquier clase puede conectarse.

Separación de intereses

La separación de preocupaciones (SoC) es la idea de que un sistema de software debe dividirse en partes que no se superponen en la funcionalidad. O como su nombre lo dice - preocupación - Un término general sobre cualquier cosa que proporcione una solución a un problema - debe separarse en diferentes lugares.

Una página web es un buen ejemplo de esto: tiene sus tres capas (Información, Presentación y Comportamiento) separadas en tres lugares (HTML, CSS y JavaScript, respectivamente).

Si vuelves a mirar el ejemplo de RPG Hero, verás que tenía muchas preocupaciones al principio (aplica mejoras, calcula el daño de ataque, maneja el inventario, equipa artículos, administra atributos). Separamos esas preocupaciones a través de la descomposición en clases más coherentes que resumen y encapsulan sus detalles. Nuestra clase Hero ahora actúa como un objeto compuesto y es mucho más simple que antes.

Saldar

La aplicación de dichos principios puede parecer demasiado complicada para un fragmento de código tan pequeño. La verdad es que es imprescindible para cualquier proyecto de software que planee desarrollar y mantener en el futuro. Escribir ese código tiene un poco de sobrecarga al principio, pero vale la pena varias veces a largo plazo.

Estos principios aseguran que nuestro sistema sea más:

  • Extensible: la alta cohesión facilita la implementación de nuevos módulos sin preocuparse por la funcionalidad no relacionada. El bajo acoplamiento significa que un nuevo módulo tiene menos cosas para conectarse, por lo tanto, es más fácil de implementar.
  • Mantenible: el bajo acoplamiento asegura que un cambio en un módulo generalmente no afectará a otros. La alta cohesión asegura que un cambio en los requisitos del sistema requerirá modificar el menor número de clases posible.
  • Reutilizable: la alta cohesión garantiza que la funcionalidad de un módulo esté completa y bien definida. El bajo acoplamiento hace que el módulo sea menos dependiente del resto del sistema, lo que facilita su reutilización en otro software.

Resumen

Comenzamos introduciendo algunos tipos básicos de objetos de alto nivel (Entidad, Límite y Control).

Luego aprendimos los principios clave en la estructuración de dichos objetos (abstracción, generalización, composición, descomposición y encapsulación).

Para el seguimiento, presentamos dos métricas de calidad de software (Acoplamiento y Cohesión) y aprendimos sobre los beneficios de aplicar dichos principios.

Espero que este artículo haya proporcionado una descripción útil de algunos principios de diseño. Si desea seguir educándose en esta área, aquí hay algunos recursos que recomendaría.

Lecturas adicionales

Patrones de diseño: elementos de software orientado a objetos reutilizables, posiblemente el libro más influyente en el campo. Un poco anticuado en sus ejemplos (C ++ 98) pero los patrones e ideas siguen siendo muy relevantes.

Creciente software orientado a objetos guiado por pruebas: un gran libro que muestra cómo aplicar prácticamente los principios descritos en este artículo (y más) trabajando en un proyecto.

Diseño de software eficaz: un blog de primer nivel que contiene mucho más que ideas de diseño.

Diseño de software y especialización en arquitectura: una gran serie de 4 cursos de video que le enseñan un diseño efectivo a lo largo de su aplicación en un proyecto que abarca los cuatro cursos.

Si esta descripción general ha sido informativa para usted, considere darle la cantidad de aplausos que cree que merece para que más personas puedan tropezar con ella y obtener valor de ella.