Los conceptos de la POO
Modelado
La programación orientada a objetos es un paradigma de desarrollo, al igual que la programación funcional o la programación imperativa.
El principio es que dentro del programa, todos los conceptos de negocio, las entidades de la "vida real", los "actores" de los algoritmos, se representan por piezas de software llamadas "objetos". Los distintos objetos del programa contienen datos y tienen un comportamiento definido por su implementación. También interactúan entre sí, utilizándose mutuamente para llevar el programa a cumplir su objetivo.
Una de las etapas más importantes (si no la más importante), es el modelado del programa. Consiste en enumerar los objetos que necesitará la aplicación y definir las relaciones entre ellos. Esta etapa puede incluir varios niveles de abstracción más o menos altos: el nivel más alto puede incluir solo las grandes partes de la arquitectura, el nivel más bajo solo presenta los objetos "finales" que realmente se desarrollarán, con tantos niveles intermedios como sea necesario dependiendo de la complejidad del proyecto.
Las partes de alto nivel no se preocupan por los problemas técnicos de la implementación: se trata de ordenar los objetos que representan el problema que el software intentará resolver. También es muy útil llamar...
Objeto y clase
Un objeto representa un concepto de negocio, una pieza de abstracción del software cuyo objetivo es resolver un problema único y concreto, manteniendo la simplicidad y la manipulación intuitiva.. Es una entidad fácil de visualizar mentalmente, cuyo papel en el gran tablero de ajedrez de la arquitectura del software está definido y restringido, y cuyas relaciones con otros objetos son lógicas y armoniosas. Si durante el diseño un objeto suscita la más mínima vacilación, entonces será necesario explicar su papel, argumentar, reflexionar y posiblemente aplicarle algunos cambios para legitimar su utilidad y su lugar.
Una clase es la definición de un objeto e incluye:
-
su nombre;
-
sus atributos, es decir, los datos que definen su estado;
-
sus métodos, es decir, las funciones que implementa y que definen su comportamiento.
Los atributos de una clase representan las propiedades del concepto que representa la clase. Dentro de la clase, sus propiedades tienen un nombre y se les puede asignar un valor, que puede cambiar con el tiempo según sea necesario. Con mucha frecuencia este valor es tipado, ya sea con un tipo simple, como un número entero o una cadena de caracteres, o con un tipo complejo, con mayor frecuencia otra clase definida por un desarrollador o que ya forma parte del lenguaje.
Si queremos representar a una persona, entonces podemos...
Encapsulación
Uno de los paradigmas de la programación orientada a objetos es que cada clase debe tener acceso al mínimo estricto de información necesaria para cumplir su función. Una gestión demasiado laxa del acceso a la información puede provocar fallos, malentendidos en el código y dependencias innecesarias, incluso perjudiciales para el proyecto. De ahí el interés de que las clases controlen la exposición de sus miembros.
Hay tres tipos de visibilidad, representadas por diferentes símbolos en UML:
-
Pública (símbolo +): el miembro es accesible para todas las demás clases del programa.
-
Protegida (símbolo #): el miembro es accesible solo para las clases derivadas de la clase en cuestión (las clases derivadas se presentan en la sección Herencia).
-
Privada (símbolo -): el miembro es accesible solo para la clase en cuestión.
El interés de la visibilidad es controlar el acceso del resto del programa a los miembros de una clase. Hay datos que deben ser accesibles en modo lectura, pero no en modo escritura, por ejemplo. En este caso, es necesario no ofrecer una interfaz para modificar estos datos. O bien hay atributos que son útiles solo para la clase que los define, para realizar cálculos complicados. La modificación de estos atributos podría provocar un error de cálculo, de ahí el interés de enmascararlos. Y si no le interesan a nadie más que a la clase que los usa, ¿por qué implementar un método de acceso que no servirá de nada? Nuevamente: una clase debe tener acceso solo a lo que le permita cumplir su función. Cualquier relación o conocimiento superfluo...
Agregación y composición
1. Agregación
Una agregación es una relación entre dos clases donde una "posee" a otra. En UML, esta relación se simboliza con una línea que conecta las dos clases, donde el final vinculado a la clase del "contenedor" termina con un diamante vacío. También es posible especificar la cardinalidad de esta relación. En UML, generalmente se elige una cardinalidad entre los siguientes valores:
-
0..1: "A tiene una cardinalidad 0..1 hacia el objeto B", significa que el objeto A puede contener cero o solo un objeto B.
-
1: "A tiene una cardinalidad de 1 hacia el objeto B", significa que el objeto A contiene necesariamente uno y solo un objeto B.
-
* o 0..*: "A tiene una cardinalidad de * hacia el objeto B", significa que el objeto A puede contener cero, uno o más objetos B.
-
1..*: "A tiene una cardinalidad de 1 .. * hacia el objeto B", significa que el objeto A debe contener al menos un objeto B.
Algunas cardinalidades pueden ser más precisas (2..5 por ejemplo), pero ciertas herramientas de modelado solo permiten los cuatro valores enumerados anteriormente.
Si se especifica una cardinalidad desde el contenido hasta el contenedor, proporciona información sobre la cantidad de contenedores potenciales que pueden poseer el contenido (consulte el siguiente ejemplo).
La cardinalidad en UML es importante...
Interfaz
Una interfaz es un conjunto de declaraciones. Por declaración, nos referimos a la presentación de los componentes de un método, típicamente su nombre, el tipo de valor que devuelve (opcional), la lista de sus parámetros así como su tipo (opcional). Una declaración se distingue de una implementación, que es el cuerpo del método, donde reside y se ejecuta el código. Por lo tanto, una interfaz no se preocupa por los comportamientos y las implementaciones de los métodos que declara.
La interfaz es un concepto extremadamente importante, porque resalta las interacciones entre los diferentes actores de un sistema. Un ejemplo "concreto" muy sencillo: el concepto de un interruptor es una interfaz entre su dedo y el circuito eléctrico que desea cerrar o abrir. Este concepto de interruptor “declara” un “método” que podríamos llamar presionar(), "llamado" por una presión del dedo y que activará un mecanismo, específico del modelo de interruptor instalado, encargado de abrir o cerrar el circuito. Es importante observar la distinción entre el concepto de interruptor y el modelo utilizado en el circuito. De hecho, una interfaz es declarativa y debe evitar detalles técnicos. Esta es la razón por la que las interfaces son importantes durante la fase de modelado de un proyecto: olvidar...
Enumeración
Una enumeración es simplemente un conjunto finito de valores.
Tomemos un concepto como los días de la semana. Hay varias posibilidades para representar un atributo de este tipo:
-
Un número entero: 0 = lunes y 6 = domingo. Pero, ¿qué es 32? ¿y -1? Debe asegurarse de que al atributo siempre se le asignará un valor entre 0 y 6, pero esto no es posible en UML que manipula entidades de negocio.
-
Una cadena de caracteres. Esta opción plantea el problema de la ortografía y la grafía. ¿Mayúsculas o no? ¿Qué pasa si el software está traducido? ¿Qué pasa si el atributo recibe una cadena que no tiene nada que ver con los días de la semana?
-
Una clase que, internamente y por tanto invisible al mundo, gestionara los días de la semana según una de las dos posibilidades anteriores, pero que ofreciera métodos que permitieran manejarlos de forma intuitiva. Pero ¿no es demasiado esfuerzo para representar solo siete valores agrupados?
Una enumeración proporciona una solución simple y elegante a este problema. Una enumeración es un tipo (como el número entero, el flotante, el carácter, etc.) y se maneja como tal. Un atributo de tipo enumeración solo puede tomar como valor los definidos en esta enumeración. Por lo tanto, no hay problema de asignación fuera del dominio...
Herencia
1. Herencia simple
Cuando una clase hereda de otra, entonces la clase llamada "derivada" (o "hija") recupera todos los miembros de la clase "base" (o "madre"), posiblemente puede volver a implementarlos (hablamos de "especialización "o" sobrecarga ") o añadir nuevos.
En UML, una herencia se representa de la siguiente manera:
Un Circulo es una Forma: hereda todos sus atributos (centro_x, centro_y), todos sus métodos (trasladar()) y los completa (radio).
Para evitar sobrecargar los esquemas, los atributos de la clase base no se repiten en la clase derivada. En cuanto a los métodos, es común indicarlos solo si están sobrecargados (es decir, la clase hija implementa el método a su manera) y omitir aquellos que no lo están.
La principal ventaja de la herencia es poder factorizar la lógica "empresarial" de diferentes clases, en una sola: la clase base. Esta es la que agrupa los comportamientos genéricos y depende de las diferentes clases derivadas implementar los comportamientos específicos. Sin embargo, estos conservan la posibilidad de invocar los mismos métodos y acceder a los mismos atributos que la clase madre.
En algunos casos, no tiene sentido propagar a las clases hija ciertos miembros (atributos de cálculo, métodos de utilidad, etc.). Por tanto, es posible precisar cuáles son los miembros que se desea heredar en las clases derivadas, modificando su visibilidad. Como recordatorio: las clases derivadas pueden acceder automáticamente a los miembros públicos y protegidos, mientras que a los miembros privados no. Nuevamente, el control de la información es esencial en POO, y si es posible evitar que una clase tenga acceso a datos que no necesita, entonces se debe hacer.
2. Clase abstracta
Opcionalmente, una clase se puede declarar abstracta, es decir, no representa un concepto de negocio que se pueda manejar como tal.
No se puede dibujar una Forma. Se puede dibujar un Cuadrado, un Círculo, una línea cerrada a mano alzada, pero no una Forma. Una Forma es una generalidad, una forma de designar estas diferentes clases bajo un solo nombre, pero no representa un caso concreto en sí mismo.
El interés de la clase abstracta es aprovechar las ventajas de la herencia, es decir, factorizar los datos y los comportamientos...
Diagrama UML
1. Estructura vs. comportamiento
Desde el comienzo de este libro, solo se han utilizado diagramas de clases. En UML, hay varios tipos diferentes de diagramas que se pueden dividir en dos categorías:
-
Diagramas de estructura, que representan las diferentes entidades que componen el software. El diagrama de clases es un diagrama de estructura. También puede crear diagramas de componentes, que detallan las partes principales de una arquitectura de software o un sistema, así como sus dependencias.
-
Diagramas de comportamiento, que se centran en las respuestas que el programa va a proporcionar, así como en los pasos algorítmicos para lograrlo.
Dado que aún no se ha visto ningún diagrama de comportamiento, las siguientes secciones presentarán dos: el diagrama de casos de uso y el diagrama de secuencia. Como parte de una introducción a la POO, estos dos diagramas son los más relevantes porque manipulan los conceptos principales de POO estar demasiado desarrollados. Su presentación no será lo más completa posible porque esto iría más allá del alcance de este libro. Sin embargo, debería ser suficiente para poder utilizar estos diagramas en proyectos.
2. Diagrama de casos de uso
No debemos olvidar que el software no es un fin: es un medio, una herramienta para realizar tareas, funciones que responden a una necesidad de negocio....
Ejercicios corregidos
1. Clase simple
Enunciado: modelar una clase para la que queremos que pueda recibir una lista de objetos cualesquiera, para poder recuperar posteriormente su longitud (el número de objetos en la lista). El esquema debe enumerar explícitamente todos los miembros necesarios.
Solución
La clase Contador tiene un método contar() que recibe una lista de objetos como argumento. No devuelve nada, porque el enunciado no menciona el hecho de devolver el número de elementos inmediatamente, sino más tarde, lo que implica la necesidad de almacenar este resultado para un uso futuro. De ahí la presencia de un atributo numero_elementos, ni público ni protegido, sino privado, para evitar el riesgo de que una clase externa pueda cambiar su valor.
Si el método contar() se encarga de calcular el valor del atributo, la responsabilidad de actualizar este contador recae únicamente en su descriptor de acceso de escritura, tan acertadamente llamado escribir_numero_elementos, que también es privado. De hecho, la encapsulación también se aplica dentro de una clase. El principio de responsabilidad única de una clase también debe ser válido para los métodos que la componen, exactamente por las mismas razones: es más fácil dar soporte al cambio si cada "actor" solo tiene un único rol. En esta clase Contador, si asumimos que hay n métodos que modifican directamente numero_elementos, cambiar el atributo implicaría modificar la implementación de estos n métodos para reflejar este cambio. Si estos n métodos usaran un descriptor de acceso, entonces un cambio en el atributo que desean escribir no tendría ningún impacto. La separación de responsabilidades implica un menor desarrollo durante los cambios evolutivos.
Finalmente, el acceso de lectura recuperar_numero_elementos() devuelve el valor del atributo en cuestión, y es público porque es el papel de la clase Contador exponer este valor.
2. Relaciones entre clases
Enunciado: Para cada uno de los siguientes pares de conceptos, indique si, desde el punto de vista del modelado de software, estos pares deben estar vinculados por una agregación o una composición, y por qué.
1. |
Aeropuerto / Avión |
2. |
Continente / País |
3. |
Molécula... |