Programación Orientada a Objetos

Fundamentos de la programación

Abstracción

Es el proceso mental de extracción de las características esenciales de algo, ignorando los detalles superfluos.

Por ejemplo, para un autobús: tiene choffer, núm. plazas, tiene ticket para pagar, tiene una ruta establecida.

Esto depende del punto de vista. Por ejemplo, del punto de vista de un mecánico las características esenciales del autobús serán otras (motor, chasis, etc..).

Encapsulación

Es el proceso por el que se ocultan los detalles del soporte (cómo se guardan) de las características esenciales de una abstracción.

Por ejemplo, para una fecha, las características esenciales son: día, mes y año. Esa información la puedo guardar como quiera, por ejemplo guardando los segundos desde 1970, guardar el año como un string, etc… Es decir, se oculta al exterior cómo se guardan esas características esenciales.

Modularización

Consiste en descomponer un sistema en módulos («piezas») poco acopladas entre sí (independientes) y cohesivas (con significado propio).

Poco acopladas no significa nada acopladas. Siempre hay algo de acoplamiento ya que si una pieza no depende de otra es que esa otra pieza no pertenece al sistema.

Jerarquización

Consiste en estructurar de tal manera las piezas para producir una organización (jerarquía) en base a diferentes grados de responsabilidad, composición, etc.

Ejemplos: jerarquía de composición, jerarquía de clasificación

Evolución de los lenguajes de programación

BasesCódigo máquinaEnsambladorProgramación alto nivel
Abstracción0,1Identificadores (ADD, JUMP…)Subprogramas
EncapsulaciónNoNoReglas de ámbito
ModularizaciónNoMacrosSubprogramas
JerarquizaciónNoNoExpresiones, registros y subprogramas
BasesProgramación estructuradaProgramación modular
AbstracciónEstructuras de control de flujo de ejecuciónEspacios de nombres
EncapsulaciónEstructuras de control de flujo de ejecuciónPrivacidad de módulos
ModularizaciónEstructuras de control de flujo de ejecuciónMódulos
JerarquizaciónEstructuras de control de flujo de ejecuciónJerarquías de dependencia cliente/servidor entre los módulos

Elementos de la POO

Las bases de la programación: abstracción, encapsulación, modularización y jerarquización, son importantes para que el software cuando cambie (que cambiará) no se degrade y se pueda estructurar bien facilitando su mantenimiento.

Esto se traduce en POO en clases y objetos.

Clase

Es una descripción de los datos y operaciones que describen un comportamiento de un cierto conjunto de elementos homogéneos.

Una clase se escribe en singular.

Objeto

Es una instancia de una clase que responde al comportamiento definido por las operaciones de la clase con unos datos particulares.

Mensaje

Consiste en invocar una operación desde un objeto (agente activo) a otro objeto (agente pasivo). El receptor tiene que contemplar dicha operación. Los mensajes siempre se lanzan contra un objeto, no contra una clase. Si tengo un método static en una clase y lo llamo, no se llamaría lanzar mensaje si no llamar a una función.

Método

Es la definición de un mensaje.

Atributo

Cada uno de los datos de una clase.

Estado

El estado de un objeto es el conjunto de valores de cada uno de los atributos en un instante dado.

Herencia

Es el mecanismo que permite transmitir atributos y métodos de una clase a otra.

Polimorfismo

El mismo mensaje puede ejecutar distintos métodos dependiendo del objeto que reciba el mensaje. La herencia facilita el intercambio de estos objetos.

POO vs Programación estructurada

Programación estructuradaFomentando…Se convierte en… (POO)
Registro + funcionesAbstracción, Encapsulación y ModularidadClase
Variable de tipo registroAbstracción, Encapsulación y ModularidadObjeto
FunciónModularidadMétodo
Llamada a funciónAbstracciónMensaje
Campo de registroEncapsulación y ModularidadAtributo
Estado de una variable registroEncapsulaciónEstado
Registro de campos variablesAbstracción y modularidadHerencia
Punteros a funcionesAbstraccion y encapsulaciónPolimorfismo

Clases y objetos

Vista pública: es lo que se conoce en «todo» el sistema.

Vista privada: únicamente se conocen los elementos dentro de la clase, en la implantación.

Buenas prácticas

  • Un método que devuelve void debería cambiar el estado del objeto, mientras que los métodos que devuelven un valor no deberían cambiar nada, solo devolver datos.
  • Usar asserts para validar los datos de entrada de una función. Por ejemplo, si tengo un método factorial y alguien lo llama con un valor negativo, este se comprueba que es positivo con un assert. Si no pasa el assert peta el programa.
    Aquí no se comprueba si el número es negativo con un if (programación defensiva) ya que no hay nada que hacer si es negativo. No puedo mostrar un error ya que no sé si el programa tendrá interfaz. Tampoco se lanzaría una excepción ya que su uso está destinado a cuando el problema está fuera de mi control, por ejemplo llamar a una BD y esta no está disponible.
    Otro ejemplo. En la clase Intervalo, si tengo un método interseccion que devuelve la intersección de mi intervalo con uno dado por parámetros, si el intervalo dado no intersecta, petaré el programa con un assert.
    Los asserts se dejan en el código pero a la hora de publicar la aplicación al compilar en Release no los incluirá.
    En el caso de que sea el usuario quien introduce los datos, la validación de estos sería responsabilidad de la vista, no de la clase de la cual estoy usando el factorial.
  • Es un code smell usar muchos getters ya que se está quitando de responsabilidad a la clase. Por ejemplo, en la clase Intervalo no tener un método getInferior ya que alguien sabiendo los límites del intervalo los usaría para calcular la longitud por ejemplo. Esto quita responsabilidad a la clase ya que ella es quien tiene que hacerlo.
  • Definir primero las cabeceras de los métodos de una clase para abstraerme de la implementación. Aplicar la metáfora de que yo soy un objeto de esa clase para facilitar la abstracción y saber qué métodos debo crear.
  • Para la clase Intervalo si tengo un método interseccion, no comprobaré dentro del método si el intervalo intersecta o no, esto tiene que comprobarlo previamente quien me llama. No vale declarar un método privado intersecta que se llame dentro del método interseccion. Esto sería una mala práctica ya que si no intersecta, ¿qué devuelvo?
  • En una clase para saber si tengo que crear un método como void o que devuelva datos, un truco es ver si el método pregunta algo a la clase, en este caso el método debería devolver datos.
  • Los constructores no construyen nada sino que inicializar (dar valores iniciales). Se llaman constructores porque se ejecutan cuando se construye el objeto.
  • Los destructores no destruyen nada sino que sirven para liberar recursos. No se suelen usar ya que para eso está el garbage collector.
  • En Java (extrapolable a otros lenguajes), para las cadenas de caracteres constantes debería usar String ya que optimiza memoria. Y para las cadenas variables StringBuffer.
  • En Java un string primitivo definido con comillas («cadena») es un objeto. Si tengo una cadena «hola» y la escribo en otra parte del código se refiere al mismo objeto. ¿Ocurre esto en otros lenguajes?
  • En Java para reutilizar un constructor se utiliza this en la primera línea de implementación del constructor. Por ejemplo, si tengo el constructor Intervalo(double inferior, double superior) y quiero reutilizarlo en otro constructor, escribiré this(9,22);
  • Una clase debe tener 20 métodos y 5 atributos como mucho. Cada método no debería tener más de 10-15 líneas. Esto es aproximado, no hay una ciencia exacta.
  • Si tengo un problema y lo divido en varias partes (clases), el reparto de responsabilidad debe de ser más o menos equitativo. De nada sirve tener una clase muy compleja que resuelve la gran mayoría del problema y otra clase que apenas hace nada, no tiene responsabilidad.

Metodología top down para POO

Consiste en comenzar creando la clase principal de la aplicación con su método de entrada. Por ejemplo, para el juego tres en raya, la clase sería TresEnRaya y tendría un único método público jugar.

Sobre esta clase se piensan en las diferentes clases en las que se apoyará para delegar las diferentes funciones: Tablero, Jugador y Turno.

Se inicializan los objetos en el constructor y en el método jugar se comienzan a programar las interacciones (mensajes) que se enviarán a estos objetos. Se van creando todos los mensajes (métodos) pero sin su implementación.

Una vez acabada la clase TresEnRaya pasaremos a implementar las funciones públicas que creamos para las diferentes clases.

Es un método de desarrollo válido para desarrollar pequeñas cosas, no para un desarrollo grande con varias personas.

Pasos a seguir:

  1. Definir clase
  2. Definir interfaz (métodos públicos)
  3. Definir atributos
  4. Definir métodos de los objetos que interactúan en dicha clase
  5. Ver qué clases faltan por implementar, escoger la más complicada de programar e iterar de nuevo (volver al punto 1).

Herencia

  • Existe una clase padre (clase base, superclase) que transmite todos sus métodos y atributos a la clase hija (clase derivada, subclase). Básicamente es un copy/paste de la clase padre hacia la clase hija.
  • Herencia múltiples: cuando una clase derivada hereda de varias clases base.
  • Para saber si hay herencia pensar si la clase derivada es un subconjunto de los elementos del mundo real de la clase base. Otra regla es preguntarse si un objeto de la clase hija es un objeto de la clase padre: «un mamífero es un animal?».
  • Una clase hija no tiene acceso a los métodos/atributos privados de la clase padre. Los atributos/métodos privados también se heredan pero no tengo acceso a ellos directamente. Se usarán de forma indirecta. Por ejemplo en un método público de la clase padre que use un método privado. En la clase hija podré ejecutar el método público e indirectamente se ejecutará el privado asociado.
  • Si una clase hija necesita hacer cosas sobre los atributos privados de la clase padre puedo crear getters y setters con visibilidad protected que acceda a dichos atributos. Aunque lo más común es crear directamente los atributos como protected.
  • No hay forma de anular métodos o atributos.
  • Puedo redefinir métodos en la clase hija para cambiar su funcionamiento o redefinir su visibilidad.
  • Con super puedo mandar mensajes a un método de la clase padre. Por ejemplo, si redefino un método y quiero usar cierta funcionalidad del padre que me gusta, puedo llamarlo a este con super al inicio del método que estoy implementando en la clase hija.
  • En el constructor, para reutilizar un constructor de la clase padre se indica con super(arg1, argN…).
  • El modificador abstract hace que pueda definir un método de una clase sin su implementación. Automáticamente la clase se convierte en abstracta también y no se pueden crear instancias. El método abstracto lo redefinirán las clases hijas.
    Si tengo la clase persona y las clases hombre y mujer, en la clase persona tendré un método miccionar. Este método no lo puedo implementar en la clase persona, por tanto será abstract. Lo que me obliga a definir la clase persona como abstract.

Polimorfismo

  • El polimorfismo es una relajación del sistema de tipos. Si defino una referencia de una clase base, puedo asignar objetos de las clases derivadas.
  • La abstracción es una de las grandes ventajas del polimorfismo.
  • Una interfaz es una clase abstracta no tiene atributos y todos sus métodos son abstractos. La interfaz es una manera simplificada de escribir esto.
  • Una interfaz no puede heredar de una clase pero sí puede heredar de otra interfaz.
  • No usar instanceOf para saber de qué tipo es una clase. En lugar de ello usar un diseño inteligente para evitar tener que usar este operador (técnica de doble despacho ?).
  • Las clases no son polimórficas, el polimorfismo está en la clase que admite varios tipos de clases.

👇Tu comentario