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

Demostrado mediante la implementación de las clases de un juego de rol

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 de esto, a medida 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 describiremos aquí también se aplican a lenguajes que no son de programación orientada a objetos.

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

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

Tipos de objetos

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 Heroclase simple :

Estos objetos generalmente contienen propiedades sobre sí mismos (como healtho mana) y son modificables a través de ciertas reglas.

2. Objeto de control

Los objetos de control (a veces también llamados objetos de administrador ) son responsables de la coordinación de otros objetos. Estos son objetos que controlany hacer uso de otros objetos. Un gran ejemplo de nuestra analogía con los juegos de rol sería la Fightclase, que controla a dos héroes y los hace luchar.

Encapsular la lógica de una pelea en una clase de este tipo te proporciona múltiples beneficios: uno de los cuales es la fácil extensión de la acción. Puede pasar muy fácilmente un tipo de personaje no jugador (NPC) para que el héroe luche, siempre que exponga la misma API. También puede heredar muy 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 los límites de su sistema. Cualquier objeto que reciba entradas de otro sistema o produzca resultados en él, independientemente de si ese sistema es un Usuario, Internet o una base de datos, puede clasificarse como un objeto de límite.

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

Bono: Objeto de valor

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

Si tuviéramos que incorporarlos a nuestro juego, una clase Moneyo Damageserí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.

Pueden clasificarse como una subcategoría de Entityobjetos.

Principios clave de diseño

Los principios de diseño son reglas en el diseño de software que han demostrado su valor a lo largo de los años. Seguirlos estrictamente lo ayudará a asegurarse de 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 Fightestá estructurada la clase. La forma en que lo usa es lo más simple posible: le da dos héroes como argumentos en la instanciación y llama al fight()método. 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 comportamientos / propiedades innecesarios y no relacionados. En otras palabras, debería ser intuitivo.

Tenga en cuenta que nuestra Hero#take_damage()función 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 en la encapsulación como poner algo dentro de una cápsula: se limita su exposición al mundo exterior. En el software, restringir el acceso a objetos y propiedades internos ayuda con la integridad de los datos.

La encapsulación encapsula la lógica interna y hace que sus clases sean más fáciles de administrar, porque sabe qué parte usan otros sistemas y qué no. Esto significa que puede reelaborar fácilmente la lógica interna mientras conserva las partes públicas y se asegura 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 las que pensar.

En la mayoría de los lenguajes, esto se realiza mediante los denominados modificadores de acceso (privado, protegido, 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, imagina que cambiamos nuestro Fight#_run_attackmétodo 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 Fightclase, 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 mayor claridad y la menor repercusión posible es la flexibilidad que desea como desarrollador.

Descomposición

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

Imagina que quisiéramos incorporar más funciones de RPG como mejoras, inventario, equipos y atributos de personajes además de nuestro Hero:

Supongo que puedes notar que este código se está volviendo bastante complicado. Nuestro Heroobjeto 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 la pena 6 de salud, tendríamos que cambiar la implementación en varios lugares.

La respuesta es descomponer el Heroobjeto en varios objetos más pequeños, cada uno de los cuales abarca parte de la funcionalidad.

Ahora, después de la descomposición de la funcionalidad de nuestro objeto en héroe HeroAttributes, HeroInventory, HeroEquipmenty HeroBuffobjetos, añadiendo funcionalidad futuro será más fácil, más y mejor encapsulado abstraído. 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 trabajar juntos.

Ejemplo:Hero y un Zoneobjeto.

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

Ejemplo:HeroInventory y Item.

A HeroInventorypuede tener muchos Itemsy Itempuede pertenecer a cualquiera HeroInventory(como artículos comerciales).

  • composición - Una fuerte relación "tiene-a" donde el todo y la parte no pueden existir 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 puede 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 clases; 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 nuestra funcionalidad común Heroy de NPC clases en un ancestro común llamado Entity. Esto siempre se logra mediante herencia.

Aquí, en lugar de tener nuestras clases NPCy Heroimplementando todos los métodos dos veces y violando el principio DRY, redujimos la complejidad moviendo su funcionalidad común a una clase base.

Como advertencia, no se exceda en la herencia. Mucha gente experimentada recomienda que prefiera la composición sobre la herencia.

Los programadores aficionados a menudo abusan de la herencia, probablemente porque es una de las primeras técnicas de programación orientada a objetos que comprenden debido a su simplicidad.

Composición

La composición es el principio de combinar varios objetos en uno más complejo. Prácticamente dicho: está creando instancias de objetos y usando su funcionalidad en lugar de heredarla directamente.

Un objeto que usa composición puede llamarse objeto compuesto . Es importante que este compuesto sea más simple que la suma de sus pares. Al combinar varias clases en una, queremos aumentar 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, se prefiere la composición a la herencia, lo que significa que debe esforzarse por mover la funcionalidad común a un objeto separado que luego usan las clases, en lugar de guardarlo en una clase base que ha heredado.

Ilustremos un posible problema con la funcionalidad de herencia excesiva:

Agregamos movimiento a nuestro juego.

Como aprendimos, en lugar de duplicar el código, usamos la generalización para poner las funciones move_righty move_leften la Entityclase.

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

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

Sé cuál es tu solución:

Basta con mover la movelógica en una por separado MoveableEntityo MoveableObjectde clase, que sólo tiene esa funcionalidad. La Mountclase, entonces puede heredar eso.

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

Un enfoque algo mejor sería abstraer la lógica del movimiento en una Movementclase (o un nombre mejor) y crear una instancia en las clases que puedan 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

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

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

Como ingeniero, su rasgo principal es evaluar críticamente el mejor enfoque para su situación particular, no seguir y aplicar ciegamente 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 qué está haciendo o tiene más de un propósito, tiene poca cohesión .

Quieres que tus clases tengan una alta cohesión. Deberían tener una sola responsabilidad y, si descubres que tienen más, podría ser el momento de dividirla.

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, de modo que pueda intercambiarlas en eventos futuros (como cambiar los marcos web). El objetivo es tener un acoplamiento suelto .

En muchos lenguajes, esto se logra mediante el uso intensivo de interfaces, que 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

Separation of Concerns (SoC) es la idea de que un sistema de software debe dividirse en partes que no se superpongan en 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 vuelve a mirar el Heroejemplo de RPG , verá que tenía muchas preocupaciones al principio (aplicar beneficios, calcular el daño de ataque, manejar el inventario, equipar elementos, administrar atributos). Separamos esas preocupaciones mediante la descomposición en clases más cohesivas que abstraen y encapsulan sus detalles. Nuestra Heroclase ahora actúa como un objeto compuesto y es mucho más simple que antes.

Saldar

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

Estos principios garantizan que nuestro sistema sea más:

  • Ampliable : la alta cohesión facilita la implementación de nuevos módulos sin preocuparse por la funcionalidad no relacionada. El acoplamiento bajo significa que un módulo nuevo tiene menos cosas a las que conectarse, por lo que es más fácil de implementar.
  • Mantenible : el acoplamiento bajo 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 la menor cantidad de clases posible.
  • Reutilizable : la alta cohesión garantiza que la funcionalidad de un módulo sea completa y esté bien definida. El acoplamiento bajo hace que el módulo dependa menos del resto del sistema, lo que facilita su reutilización en otro software.

Resumen

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

Luego aprendimos los principios clave para estructurar dichos objetos (Abstracción, Generalización, Composición, Descomposición y Encapsulación).

Para dar 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 proporcione una descripción general útil de algunos principios de diseño. Si desea educarse más en esta área, aquí hay algunos recursos que recomendaría.

Lecturas adicionales

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

Desarrollo de 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 efectivo: un blog de primer nivel que contiene mucho más que conocimientos de diseño.

Especialización en diseño y arquitectura de software: 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 le ha resultado informativa, considere darle la cantidad de aplausos que cree que merece para que más personas puedan encontrarla y obtener valor de ella.