La abstracción dentro de la programación orientada a objetos

Dentro de la programación orientada a objetos, a  la hora de dar forma a los objetos de un programa se toma como base a los objetos del mundo real. Sin embargo estos objetos no representan a los objetos reales con demasiada exactitud. Más bien estos objetos del programa representan al objeto real mediante una serie de atributos y comportamientos específicos para el contexto que representará el programa que estamos realizando.

Por ejemplo una clase coche tendrá los atributos de nombre, modelo, color, velocidad máxima. Pero si la clase va a ser utilizada en una aplicación para la venta de vehículos además la clase contendrá información sobre el fabricante, precio, prestaciones y nivel de lujo. Por el contrario si la clase va a ser utilizada dentro de un videojuego de carreras de coches esta clase contendrá información sobre el combustible, peso, ángulo de giro en las curvas y atributos que permitan al videojuego simular el comportamiento del vehículo en un circuito de carreras.

Esto significa que el contexto de nuestro programa definirá el modelado de la clase.

La Abstracción es el modelo de un objeto o fenómeno del mundo real, limitado a un contexto específico, que representa todos los datos relevantes a este contexto con gran precisión, ignorando el resto de atributos no necesarios para el contexto.

Los pilares de la programación orientada a objetos

La programación orientada objetos posee 4 pilares que le ayudan a diferenciarse de otros paradigmas de la programación.

Estos pilares son la abstracción, la encapsulación, la herencia y el polimorfismo.

En los próximos artículos veremos cada uno de estos pilares en detalle.

Jerarquías de clase dentro de la Programación orientada a objetos

Como aprendimos en el artículo de fundamentos de la programación orientada a objetos existen objetos que son instancias de clases. Pero las aplicaciones reales, como la vida real, no se componen de una sola clase. Estas clases pueden estar relacionadas entre si para crear un diseño mas complejo que permita resolver problemas más complejos.

Cuando varias clases se relacionan entre si se dice que forman una jerarquía de clases.

De gatos y pájaros

Imaginemos que tu vecino tiene un gato llamado Silvestre. Los pájaros y los gatos tienen campos y métodos en común, ambos tienen un nombre, un sexo, edad, peso, tamaño, color, comen, duermen, observan, etc. Pero también tienen diferencias, unos tienen alas y otros sólo patas, unos pían y otros maúllan.

Observando los atributos comunes de las clases Pájaro y Gato podemos diseñar la clase Animal.

Clase Animal:

    Campos:

        nombre, edad, sexo, peso, tamaño, color, patas

    Métodos:

        respirar, comer, dormir, observar 

A partir de la clase Animal podemos crear las clases Pájaro y Gato atendiendo a sus diferencias.

Clase Pájaro

    Campos:

        alas, pico

    Métodos:

        piar, volar

Clase Gato

    Campos:

        dientes, orejas

    Métodos:

        maullar, correr

Las clases Pájaro y Gato heredan de la clase Animal el conjunto de campos y métodos. La clase Animal se conoce como clase padre o superclase.

Las clases hijas de la superclase Animal se denominan subclases. Heredan el comportamiento y el estado de su clase padre.

La relación que existe entre superclases y subclases se denomina jerarquía de clases. Estas relaciones pueden basarse en la herencia de métodos y campos e incluso en la modificación de alguno de esos métodos.

Sobreescritura de métodos

Cuando una clase hija modifica un método de la clase padre se dice que el método está sobreescrito (overwrite).

Un método sobreescrito puede limitarse a sustituir por completo el método de la clase padre o a mejorarlo ya que en la llamada al método de la clase hija, esta clase hija internamente primero ejecuta el método de la clase padre, copiando su comportamiento, y luego ejecutar más órdenes mejorando el comportamiento haciéndolo más específico para la clase hija.

Fundamentos de programación orientada a objetos

La Programación orientada a objetos (POO) es un paradigma de programación en el que la información y la funcionalidad de una aplicación software se agrupa en pequeños grupos especiales llamados objetos, estos objetos siguen un diseño planificado por el programador y estos objetos son representados en el diseño por las clases.

Pájaros y objetos

Muchos autores han explicado la POO hablando de gatos, perros o coches. En este artículo probaremos a explicar la POO hablando de pájaros.

Imaginemos que tienes un pájaro llamado Piolín. Piolín es un objeto, una instancia de la clase Pájaro.

Campos y métodos

Cada pájaro tiene una serie de atributos: nombre, edad, sexo, peso, tamaño, color de plumaje, tipo de alimentación, etc. Este grupo de atributos se denominan campos de la clase. Los campos representan la información que contendrá y manipulará el objeto.

Al conjunto de toda la información del objeto en un momento dado se la denomina estado del objeto. 

Además todos los pájaros se comportan de forma similar: respiran, comen, observan, crecen, duermen, etc. Estos son los métodos de la clase. Los métodos representan la funcionalidad que podrá realizar el objeto.

Al conjunto de campos y métodos de una clase se le conoce como el conjunto de miembros de una clase.

Instancias

Los objetos son instancias de clase. Piolín es una instancia de la clase Pájaro, Correcaminos es otra instancia de la clase pájaro.

Piolín y Correcaminos tienen el mismo grupo de campos y métodos. La diferencia está en los valores de estos atributos ya que permiten hacer diferente a cada instancia de la clase pájaro.

Una clase es como un plano de construcción que permite dar forma a la información y las acciones que podrá realizar un objeto.

Un ejemplo de definición de clase podría ser el siguiente:

Clase Pájaro:

    Campos:

        nombre, edad, sexo, peso, tamaño, color

    Miembros:

        respirar, comer, dormir, observar 

SOLID: Principio de inversión de dependencias o Dependency inversion principle

Este es el quinto y último principio de programación SOLID.

Significado

Este principio establece que las clases de alto nivel no deberían depender de las clases de bajo nivel. En su lugar las clases deberían depender de las abstracciones.

Además las abstracciones no deben depender de los detalles sino que los detalles deben depender de las abstracciones.

Con este principio se busca reducir las dependencias entre módulos buscando un menor nivel de acoplamiento entre clases.

Esto es indispensable en proyectos con muchos módulos en los que es necesario utilizar inyección de dependencias.

Ejemplo

Veamos el siguiente ejemplo en el que en nuestro proyecto tenemos una clase que nos permite el acceso a la base de datos en general que llamaremos CargadorDesdeBaseDeDatos y tenemos una clase PersonaEnBaseDeDatos que nos permite cargar datos de la clase Persona.

Para ello la clase PersonaEnBaseDeDatos tiene una propiedad de tipo CargadorDesdeBaseDeDatos que se utiliza como driver de acceso al sistema de almacenamiento de bases de datos de la aplicación.

 

class CargadorDesdeBaseDeDatos {

    funcion obtenerDatosGenerales()

}

 

class PersonaEnBaseDeDatos {

    propiedad datos = CargadorDesdeBaseDeDatos()

    propiedad listaDePersonas

 

    funcion cargaDatos() {

        listaGeneral = datos.obtenerDatosGenerales()

        listaDePersonas = convertirDatosGeneralesEnDatosDePersonas(listaGeneral)

    }

}

 

En el ejemplo en la clase PersonaEnBaseDeDatos dentro de la función cargaDatos() hay una dependencia de la clase AccesoABaseDeDatos. Esto es un problema porque si un día necesitamos cambiar de sistema de base de datos o de obtener datos de Internet tendríamos que modificar demasiado código ya que ambas clases tienen una fuerte dependencia entre ellas.

Imaginemos por ejemplo que queremos incluir la opción de descargar los datos desde una API rest en Internet. El código se complicaría demasiado en todas las clases.

Solución

La solución a este problema es utilizar una abstracción que permita identificar un acceso a datos mediante una interface o protocolo, dependiendo de las posibilidades del lenguaje de programación.

Veamos el código.

 

interface AccesoADatos {

    funcion cargaDatosDePersonas

}

 

class CargadorDesdeBaseDeDatos Implementa AccesoADatos {

    propiedad listaDeDatos

 

    funcion obtenerDatosGeneralesDesdeMySQL() {

        listaDeDatos = cargaDesdeMySQL()

    }

 

    funcion cargaDatosDePersonas() {

        obtenerDatosGeneralesDesdeMySQL()

        return convertirATipoPersona(listaDeDatos)

    }

}

 

class CargadorDesdeInternet Implementa AccesoADatos {

    propiedad listaDeDatos

 

    funcion obtenerDatosGeneralesDeInternet() {

        listaDeDatos = descargaDeLaNube()

    }

 

    funcion cargaDatosDePersonas() {

        obtenerDatosGeneralesDeInternet()

        return convertirATipoPersona(listaDeDatos)

    }

}

 

class PersonaEnBaseDeDatos {

    propiedad datos = AccesoADatos

    propiedad listaDePersonas

 

    funcion constructor(inyeccion: AccesoADatos) {

        datos = inyeccion

    }

 

    funcion cargaDatos() {

        listaDePersonas = datos.cargaDatosDePersonas()

    }

}

 

Con esta solución podemos inyectar cualquier clase del tipo interface AccesoADatos en el constructor de la clase PersonaEnBaseDeDatos. De esta forma podemos cambiar cómo accedemos a esos datos inyectando cualquier clase que implemente la interfaz AccesoADatos consiguiendo un mayor nivel de desacoplamiento entre clases. De esta forma tanto el módulo de alto nivel como el de bajo nivel dependen de abstracciones, por lo que se cumple el principio de inversión de dependencias y además, esto nos obliga a cumplir el principio de Liskov, ya que las clases que accederán a cualquier base de datos son intercambiables entre ellas ya que todas implementan la interfaz de AccesoADatos.

SOLID: Principio de segregación de interfaz o Interface segregation principle

En este artículo hablamos del cuarto principio de los principios de programación SOLID.

Significado

Este principio establece que las clases clientes de otras no deberían verse forzados a depender de interfaces que no usan. En su lugar se apoya la definición de interfaces más específicas para cada caso.

Cuando hablamos de interfaces estamos hablando del concepto de interfaz de clase del lenguaje de programación Java así como la adaptación a cualquier otro tipo de lenguaje de programación. Por ejemplo en Swift estaríamos hablando de protocolos.

En pocas palabras una interface es un acuerdo entre las clases que implementan la interfaz y la propia interfaz definida con sus propiedades y funciones. El acuerdo consiste en que cada clase que implemente dicha interfaz deberá codificar cada una de las propiedades y funciones definidas en la interfaz.

Ejemplo

Este principio es más sencillo de entender con un ejemplo.

Imaginemos que estamos modelando una interfaz de acciones para distintas clases de aves.

 

interface Ave {

    funcion comer()

    funcion cantar()

    funcion volar()

}

 

class Loro implements Ave {

    propiedad nombre

    propiedad color

    propiedad tamaño

 

    funcion comer() {

        // código para comer

    }

 

    funcion cantar() {

        // código para cantar

    }

 

    funcion volar() {

        // código para volar

    }

}

 

class Aguila implements Ave {

    propiedad nombre

    propiedad color

    propiedad tamaño

 

    funcion comer() {

        // código para comer

    }

 

    funcion cantar() {

        noHacerNada()

    }

 

    funcion volar() {

        // código para volar

    }

}

 

class Gallina implements Ave {

    propiedad nombre

    propiedad color

    propiedad tamaño

 

    funcion comer() {

        // código para comer

    }

 

    funcion cantar() {

        // código para cantar

    }

 

    funcion volar() {

        noHacerNada()

    }

}

 

En este ejemplo tenemos una interfaz llamada Ave con 3 acciones pero no todas las aves realizan esas tres acciones. Por ejemplo las águilas no cantan y las gallinas no vuelan. Pero si una clase implementa una interfaz está obligada a incluir esas funciones aunque no hagan nada.

Solución

La solución consiste en segregar la interfaz Ave en 3 interfaces más específicas.

 

interface Ave {

    funcion comer()

}

 

interface AveCantora {

    funcion cantar()

} 

 

interface AveVoladora {

    funcion volar()

}

 

class Loro implements Ave, AveCantora, AveVoladora {

    propiedad nombre

    propiedad color

    propiedad tamaño

 

    funcion comer() {

        // código para comer

    }

 

    funcion cantar() {

        // código para cantar

    }

 

    funcion volar() {

        // código para volar

    }

}

 

class Aguila implements Ave, AveVoladora {

    propiedad nombre

    propiedad color

    propiedad tamaño

 

    funcion comer() {

        // código para comer

    }

 

    funcion volar() {

        // código para volar

    }

}

 

class Gallina implements Ave, AveCantora {

    propiedad nombre

    propiedad color

    propiedad tamaño

 

    funcion comer() {

        // código para comer

    }

 

    funcion cantar() {

        // código para cantar

    }

}

 

Ahora cada clase sólo implementa las interfaces necesarias a sus capacidades por lo que no hay funciones que no hacen nada.

Con esta solución es sencillo incluir en un futuro por ejemplo aves que puedan nadar y cada clase sólo implementará las funciones necesarias.

Además con esta solución se mejora aún más el principio de responsabilidad única.