Conceptos básicos de la Programación Orientada a Objetos (OOP o POO)
Conceptos básicos de la Programación Orientada a Objetos (OOP o POO)
OOP vs. Programación estructurada
- La programación orientada a objetos (POO) surge como una evolución de la programación estructurada.
- Busca modelar los problemas del mundo real utilizando objetos que combinan datos y comportamientos.
- En programación, el paradigma imperativo se basa en funciones y datos.
- El paradigma orientado a objetos se basa en objetos.
- Los objetos son el elemento básico y central de la programación orientada a objetos (OOP o POO).
( : Cada objeto en el programa representa algo del mundo real:
una persona, una cuenta bancaria, un pedido, una factura, etc.)
- Podemos hablar de un universo de discurso, entendido como el sistema que queremos automatizar mediante software.
- Un objeto es una entidad (concreta o abstracta) que desarrolla una actividad en un entorno determinado, dentro de un universo de discurso específico.
Definición
|
Objeto: cada elemento activo que identificamos dentro de un determinado universo de discurso.
- Serán nuestros componentes software para construir y ensamblar nuestros programas.
|
|
|
Ejemplo
|
|
|
- En un banco hay cuentas bancarias (objeto)
-
- Las cuentas bancarias se identifican con un número y un titular (nombre, apellidos y DNI) → atributos.
- Las cuentas pueden darse de alta o de baja, hacer extracciones, ingresos y transferencias → métodos.
|
|
|
Ejemplo
|
|
|
- En la gestión de una empresa, a nivel de información tenemos
-
- Empleados
- Nóminas
- Bases de datos
- Proveedores
- Facturas
- Pedidos
|
|
|
Comparativa con la aplicación del juego Mastermind
|
|
|
|
- Puede parecer una forma más compleja de programar, pero en realidad es una manera de dividir la naturaleza del problema en unidades independientes que pueden interactuar entre sí.
- Cada una de ellas tendrá una identidad propia, definida por los valores de sus atributos.
- Cada una tendrá también un comportamiento concreto, es decir, lo que sabe hacer y que el resto del programa o de los objetos podrá utilizar.
Elementos en la Programación Orientada a Objetos
- De lo dicho anteriormente deducimos que en la POO tenemos dos elementos fundamentales:
- Los atributos o características de la clase.
- Los métodos o comportamientos de la clase.
- Para crear objetos, previamente hay que definir su estructura.
- La definición de la estructura (atributos y métodos) de los componentes software se denomina clase.
Clase
- La descripción y especificación de los componentes software para su posterior uso en los programas.
- Una clase es la estructura o plantilla que define un tipo concreto de objetos.
- Los objetos son elementos concretos de mi sistema: instancias de esa clase, cargadas en memoria para ser utilizadas por un programa.
|
- Elementos de la POO
Atributo
- Son las características o datos de un objeto.
- Sus valores determinan el estado del objeto en un momento dado.
- Normalmente, al instanciar un objeto en memoria, lo primero que hacemos es asignar valores a sus atributos.
- Es recomendable que los atributos estén encapsulados, es decir, que sólo sean accesibles dentro del propio objeto (privados).
|
Método
- Especifican el comportamiento de los objetos.
- Permiten modificar o conocer el estado de un objeto mediante métodos getter y setter.
- Permiten que un objeto realice acciones dentro del sistema y se comunique con otros objetos.
- Los métodos representan las acciones que el objeto sabe hacer, los servicios que ofrece al sistema.
- También pueden incluir acciones internas que facilitan el funcionamiento del propio objeto.
- En PHP, los métodos se definen dentro de la clase y se invocan mediante el operador ->.
|
Sobrecarga
Es un concepto muy importante y básico en la programación orientada a objetos.
La sobrecarga es una concreción del principio de polimorfismo.
Polimorfismo
- Podemos tener varios métodos con el mismo nombre, pero con diferente número de parámetros o con parámetros de distinto tipo.
- En tiempo de ejecución se ejecutará uno u otro en función de los parámetros reales que pasemos en la invocación del método.
|
- Sin embargo, este aspecto en PHP no es del todo intuitivo: no existe la sobrecarga como la entendemos en otros lenguajes.
- No obstante, disponemos de técnicas para poder simular la sobrecarga.
- En muchos casos resulta fundamental, especialmente al sobrecargar el constructor de una clase.
- Para simular la sobrecarga en PHP, aprovechamos que una variable que no tenga valor se considera del tipo null.
- Lo veremos con una serie de ejemplos para dejar claro este concepto.
- Tomamos como ejemplo una función:
|
|
Ejemplo
|
|
|
function verTipoParametros($a = null, $b = null, $c = null){
echo "Primer parámetro: ";
var_dump($a);
echo "Segundo parámetro: ";
var_dump($b);
echo "Tercer parámetro: ";
var_dump($c);
}
|
- Ahora la invocamos de diferentes maneras y observamos el resultado:
|
|
Invocar sin parámetros reales
|
|
|
echo "Invocando a <strong>verTipoParametros()</strong><hr />";
verTipoParametros();
- A pesar de que tiene tres parámetros, la invocamos sin argumentos.
- El resultado será que cada parámetro, al ejecutar la función, será de tipo null con valor null (es un tipo válido en PHP).
|
|
|
Invocar con 1 parámetro real
|
|
|
echo "Invocando a <strong>verTipoParametros(5)</strong><hr />";
verTipoParametros(5);
- En este caso invocamos con un solo parámetro de tipo entero.
|
|
|
Invocar con 2 parámetros reales
|
|
|
echo "Invocando a <strong>verTipoParametros(5,7)</strong><hr />";
verTipoParametros(5,7);
- En este caso invocamos con dos parámetros de tipo entero.
- Los parámetros dentro de la función serán tres: dos con valor entero y el tercero con valor y tipo null.
|
|
|
Invocar con 3 parámetros reales
|
|
|
echo "Invocando a <strong>verTipoParametros('pedro',5,9)</strong><hr />";
verTipoParametros('pedro',5,9);
- En este caso pasamos tres parámetros: el primero de tipo string y los otros dos de tipo entero.
|
|
|
Invocar con 3 parámetros reales, uno de ellos un array
|
|
|
echo "Invocando a <strong>verTipoParametros([1,4,'maría'], true, 'sonia')</strong><hr />";
verTipoParametros([1,4,'maría'], true, 'sonia');
- Ahora igualmente pasamos tres parámetros, pero uno de ellos es un array.
|
Sobrecargando el constructor
- Usando esta forma de trabajar, vamos a sobrecargar el constructor de una clase.
- Tomemos como ejemplo una clase Racional. Un número racional es un objeto que tiene numerador y denominador.
- Queremos permitir crear el objeto de distintas formas:
$r1 = new Racional("8/5"); /* 8/5 */
$r2 = new Racional(5,4); /* 5/4 */
$r3 = new Racional(5); /* 5/1 */
$r4 = new Racional(); /* 1/1 */
- Necesitamos que el constructor pueda responder a todas las situaciones.
- Aplicando los conceptos vistos, solo tenemos que comprobar de qué tipo son los parámetros.
Tip: Recordar que null también es un tipo.
- Vemos que podemos tener 0, 1 o 2 parámetros.
Por lo tanto, el constructor tendrá dos parámetros opcionales.
public function __construct($num = 1, $den = 1) {
...
}
- Especificamos el código de cómo se podría implementar:
class Racional {
private $num;
private $den;
public function __construct($num = 1, $den = 1) {
// opciones:
// new Racional() => 1/1
// new Racional(5) => 5/1
// new Racional("5/2") => 5/2
// new Racional(5,2) => 5/2
// Otra situación: no se instancia correctamente
if (is_string($num)) {
$numero = explode("/", $num);
$num = $numero[0];
$den = $numero[1];
}
$this->num = $num;
$this->den = $den;
}
/* Método para visualizar el objeto como cadena de caracteres */
public function __toString() {
return ($this->num . "/" . $this->den);
}
// Métodos privados para asignar valores según la forma de invocación
private function racionalNum($num) {
$this->num = $num;
$this->den = 1;
}
/**
* @param string $num número racional del tipo "a/b"
* Hay muchas formas de poder descomponer esa cadena en dos números.
*/
public function racionalCadena($num) {
$partes = explode("/", $num);
$this->num = (int) $partes[0];
$this->den = (int) $partes[1];
}
public function racionalVacio() {
$this->num = 1;
$this->den = 1;
}
/* En este caso, si los valores son incorrectos, asigno el racional 1/1 */
public function racionalNumDen($num, $den) {
if (is_numeric($num) && is_numeric($den)) {
$this->num = $num;
$this->den = $den;
} else {
$this->num = 1;
$this->den = 1;
}
}
}
- Esta sería una posibilidad, pero podemos crear un código más compacto.
En la siguiente versión inicializamos los parámetros con un valor por defecto (1).
Posteriormente, si el primer parámetro es una cadena, se divide en numerador y denominador.
Hecho esto, solo queda asignar los valores a los atributos.
public function __construct($num = 1, $den = 1){
if (is_string($num)){
$numero = explode("/", $num);
$num = $numero[0];
$den = $numero[1];
}
$this->num = $num;
$this->den = $den;
}
- Probamos este constructor con el siguiente código:
$a = new Racional();
$b = new Racional(5);
$c = new Racional(5,6);
$d = new Racional("6/6");
echo "Valor del racional \$a = $a <br />";
echo "Valor del racional \$b = $b <br />";
echo "Valor del racional \$c = $c <br />";
echo "Valor del racional \$d = $d <br />";
- Mostrando los siguientes resultados:
Valor del racional $a = 1/1
Valor del racional $b = 5/1
Valor del racional $c = 5/6
Valor del racional $d = 6/6
Sobrecarga con __call(...)
- Otra forma de lograr un comportamiento similar es usando el método mágico __call($funcion, $parametros).
Tip: El método mágico __call(...) se ejecuta cuando invocamos un método que no existe en la clase.
- Queremos usar un método llamado asigna() que nos permita cambiar el valor de un racional.
- La forma de aportar el nuevo valor será la misma que la de construir el objeto.
$r1 = new Racional(); // construye el objeto 1/1
$r1->asigna("6/4"); // ahora el objeto vale 6/4
$r1->asigna(); // ahora el objeto vale 1/1
$r1->asigna(8); // ahora el objeto vale 8/1
$r1->asigna(124, 6); // ahora el objeto vale 124/6
- La forma de proceder será usando los métodos privados creados anteriormente.
- El siguiente código implementa la solución:
public function __call($metodo, $argumentos) {
if ($metodo == "asigna") {
switch (count($argumentos)) {
case 0:
$this->racionalVacio();
break;
case 2:
$this->racionalNumDen($argumentos[0], $argumentos[1]);
break;
case 1:
if (is_int($argumentos[0])) {
$this->racionalNum($argumentos[0]);
} else {
$this->racionalCadena($argumentos[0]);
}
break;
}
}
}
Métodos static vs no static
- En PHP, la idea de static es igual que en cualquier lenguaje de programación orientado a objetos.
- Cuando un elemento (atributo o método) es estático, ese elemento es compartido por todos los objetos de la clase y persiste en memoria con su contenido mientras exista al menos un objeto de esa clase.
- Como no pertenece a cada objeto individual, sino a la clase en general, también se les llama atributos o métodos de clase.
- Para acceder a un elemento estático, necesitamos nombrar la clase (no el objeto). En PHP podemos hacerlo usando el operador self o el propio nombre de la clase junto con el operador de resolución de ámbito ::.
Usar constantes y elementos estáticos
- Como ejemplo, vamos a implementar una clase Factura con los siguientes requisitos
-
- La factura tendrá una constante llamada IVA.
- Tendremos un atributo estático que especificará el número de facturas creadas.
- Los atributos de cada factura serán importe_bruto y fecha.
- Tendrá un método generarFactura() que mostrará:
- Factura de XXXX (el nombre se recibirá como argumento).
- Fecha (atributo).
- Importe base (atributo).
- IVA aplicado (constante de la clase).
- Total bruto (importe base más el IVA).
- En el archivo index.php crearemos 5 facturas, visualizaremos el número de facturas, eliminaremos dos de ellas y volveremos a visualizar el número de facturas. Finalmente imprimiremos las dos facturas restantes.
| Posible solución |
|---|
<?php
// Factura.php
class Factura
{
// Constante IVA
const IVA = 0.21;
// Atributo estático para contar el número de facturas
private static $numeroFacturas = 0;
// Atributos de instancia
private $importe_bruto;
private $fecha;
// Constructor
public function __construct($importe_bruto, $fecha)
{
$this->importe_bruto = $importe_bruto;
$this->fecha = $fecha;
self::$numeroFacturas++; // Incrementa el contador al crear una nueva factura
}
// Destructor
public function __destruct()
{
self::$numeroFacturas--; // Decrementa el contador al eliminar una factura
}
// Método para generar una factura
public function generarFactura($nombreCliente)
{
$ivaAplicado = $this->importe_bruto * self::IVA;
$totalBruto = $this->importe_bruto + $ivaAplicado;
echo "Factura de $nombreCliente<br/>";
echo "Fecha: {$this->fecha}<br/>";
echo "Importe base: {$this->importe_bruto} €<br/>";
echo "IVA aplicado (" . (self::IVA * 100) . "%): {$ivaAplicado} €<br/>";
echo "Total bruto: {$totalBruto} €<br/>";
echo "-----------------------------<br/>";
}
// Método estático para obtener el número de facturas actuales
public static function obtenerNumeroFacturas()
{
return self::$numeroFacturas;
}
}
// index.php
// Crear 5 facturas
$factura1 = new Factura(100, '2024-11-18');
$factura2 = new Factura(200, '2024-11-18');
$factura3 = new Factura(150, '2024-11-18');
$factura4 = new Factura(300, '2024-11-18');
$factura5 = new Factura(250, '2024-11-18');
// Visualizar el número de facturas
echo "Número de facturas actuales: " . Factura::obtenerNumeroFacturas() . "<br/><br/>";
// Eliminar dos facturas
unset($factura4);
unset($factura5);
// Visualizar el número de facturas tras eliminar dos
echo "Número de facturas tras eliminar: " . Factura::obtenerNumeroFacturas() . "<br/><br/>";
// Imprimir las facturas restantes
$factura1->generarFactura("Cliente 1");
$factura2->generarFactura("Cliente 2");
?>
|
|
- Es muy común tener un atributo static que cuente cuántos objetos existen de una clase.
- En la siguiente imagen se ilustra cómo al crear varios objetos, cada uno se instancia por separado en memoria:
- Cuando añadimos un elemento estático, solo habrá una copia en memoria, compartida por todos los objetos de la clase:
- Vemos el código y cómo se accede al elemento estático:
<?php
class Racional {
static public $cuenta_racionales = 0;
private $num; // Numerador
private $den; // Denominador
public function __construct($num, $den) {
self::$cuenta_racionales++;
$this->num = $num;
$this->den = $den;
}
}
?>
<?php
require_once "Racional.php";
$r1 = new Racional(5,4);
$r2 = new Racional(5,4);
$r3 = new Racional(5,4);
echo "<h1>Ahora tenemos " . Racional::$cuenta_racionales . " objetos Racional</h1>";
$r4 = new Racional(5,4);
$r5 = new Racional(5,4);
echo "<h1>Ahora tenemos " . Racional::$cuenta_racionales . " objetos Racional</h1>";
$r6 = new Racional(5,4);
$r7 = new Racional(5,4);
// Observa (y esto es propio de PHP) que puedo acceder a un elemento estático
// tanto a través del nombre de la clase como desde un objeto
echo "<h2>Podemos acceder con los objetos:</h2>";
echo "<h3>Según r1: " . $r1::$cuenta_racionales . "</h3>";
echo "<h3>Según r2: " . $r2::$cuenta_racionales . "</h3>";
echo "<h3>Según r3: " . $r3::$cuenta_racionales . "</h3>";
echo "<h3>Según la clase: " . Racional::$cuenta_racionales . "</h3>";
?>
---
- Vamos a implementar las operaciones con números racionales.
Antes, recordemos las operaciones básicas:
- Sumar

- Restar

- Multiplicar

- Dividir

- Si la operación la implementamos como un método no estático, lo que estaremos haciendo es sumar al objeto actual otro objeto Racional que pasamos como argumento.
Podemos modificar el objeto actual o devolver un nuevo objeto (lo más correcto).
/**
* Suma al racional actual el racional recibido como parámetro.
* @param Racional $n1
* @return Racional
*/
public function sumar(Racional $n1) {
$den = $this->den * $n1->getDen();
$num = $this->num * $n1->getDen() + $this->den * $n1->getNum();
return new Racional($num, $den);
}
$r1 = new Racional(7,6);
$r2 = new Racional(9,4);
$r3 = $r1->sumar($r2);
echo "$r1 + $r2 = $r3";
---
- Si la operación la realizamos como un método estático, estaremos sumando dos objetos Racional y devolviendo un nuevo objeto como resultado:
static public function sum_static(Racional $r1, Racional $r2){
$n = $r1->getNum() * $r2->getDen() + $r1->getDen() * $r2->getNum();
$d = $r1->getDen() * $r2->getDen();
return new Racional($n, $d);
}
$r1 = new Racional(7,6);
$r2 = new Racional(9,4);
$r3 = Racional::sum_static($r1, $r2);
echo "$r1 + $r2 = $r3";
Herencia
- La herencia es un mecanismo de programación que permite crear una jerarquía en los componentes software, que se van especializando.
- Es un principio de abstracción mediante el cual podemos crear una jerarquía de clases, con una raíz que contiene los elementos comunes y nodos que representan clases especializadas.
- La idea es definir una clase con ciertas características comunes (atributos, métodos). Posteriormente, crearemos otras clases a partir de la ya existente, heredando implícitamente los atributos y métodos como parte de su estructura o composición.
- Es una característica muy natural. Por ejemplo:
- Personas → (Médicos, Bailarines)
- Vehículos → (Terrestres → Coche, Moto) / (Acuáticos → Barco, Lancha)
- La herencia es una forma de obtener características comunes por separado y luego especializarlas, evitando redundancias.
- Facilita la reutilización y la adaptación del código.
- La herencia implica declarar jerarquías de clases
- En la raíz de la jerarquía establecemos la parte o estructura común a todas las clases, y posteriormente vamos especializando las diferencias de cada una.
- Todos los atributos y métodos de una clase superior (supertipo) que sean públicos o protegidos son heredados por todas las clases derivadas (subtipos).
|
- Para establecer una jerarquía, usamos la palabra reservada extends en las clases que heredan.
- Vamos a verlo con un ejemplo:
- Primero establecemos la clase Persona:
<?php
class Persona {
protected $nombre;
protected $direccion;
protected $edad;
protected $frase;
public function __construct(string $n, string $d, int $e){
$this->nombre = $n;
$this->direccion = $d;
$this->edad = $e;
}
public function establecer_frase(string $frase){
$this->frase = $frase;
}
public function hablar(){
echo $this->frase;
}
}
?>
- Ahora establecemos la clase que hereda de Persona:
Principios del uso de la herencia
- Todos los atributos y métodos públicos y protegidos del supertipo Persona son también de Sanitario.
- En un momento dado, podemos invocar métodos del supertipo usando el operador parent junto con el operador de resolución de ámbito ::.
|
<?php
class Sanitario extends Persona {
protected $centroSalud;
protected $yearTitulacion;
public function __construct(string $n, string $d, int $e, string $centro, int $year) {
parent::__construct($n, $d, $e);
$this->centroSalud = $centro;
$this->yearTitulacion = $year;
}
public function mostrarInfo(){
echo "$this->nombre trabaja en $this->centroSalud y obtuvo su título en $this->yearTitulacion.";
}
}
?>
---
- Vemos dos ejemplos para explicar de forma práctica este concepto.
|
|
Herencia: gestión de personal en un ambulatorio
|
|
|
- Se pide gestionar un ambulatorio.
- Para ello haremos solo el diagrama de clases y su implementación a nivel básico (sin entrar en detalles).
- Tras realizar el análisis, se determina que se pretende gestionar los datos de los empleados y anotar las acciones básicas que realizan.
- Encontramos los siguientes elementos, que especificamos como clases
-
- Conserjes
- Enfermeras
- Médicas
Las propiedades (atributos) y métodos de cada clase se representan en los siguientes diagramas:
- Claramente vemos que todos ellos comparten varios elementos comunes.
- Esto nos permite crear una clase genérica, por ejemplo personalAmbulatorio.
- Posteriormente crearemos especializaciones de esta clase con los elementos particulares.
El diagrama podría quedar así:
- Ver la aplicación ejecutándose:
http://manuel.infenlaces.com/dwes/ejercicios/T6_Ambulatorio/
| Posible index de uso |
|---|
<?php
spl_autoload_register(function ($nombre_clase) {
include $nombre_clase . '.php';
});
$medico1 = new Medica("María", "Martínez", "Casa de María", 29, "Cardiología");
$medico2 = new Medica("Luis", "Pérez", "Casa de Luis", 38, "Pediatría");
$medico3 = new Medica("Nieves", "Ruiz", "Casa de Nieves", 44, "Dermatología");
$conserje = new Conserje("Soledad", "Viruela", "Casa de Soledad", 58, "Mostrador Entrada");
$enfermera1 = new Enfermera("Javier", "Moreno", "Casa de Javier", "General", 1990);
$enfermera2 = new Enfermera("Luis", "Pérez", "Casa de Javier", "General", 1990);
$conserje->avisoEnfermera($enfermera1, "Realizar cura en brazo del Sr. Martínez NSS 50/2155441/35");
$enfermera1->avisoMedico($medico3, "Paciente con tos y fiebre");
$enfermera2->avisoMedico($medico2, "Paciente con fiebre y vómitos");
$enfermera2->avisoMedico($medico3, "Paciente con pie torcido; posible fractura");
$conserje->avisoMedico($medico1, "Visitar en casa por fiebre alta", "Visita");
$conserje->avisoMedico($medico2, "Persona mayor con poca movilidad", "Visita");
$conserje->avisoMedico($medico3, "Niño pequeño con fiebre", "Consulta");
?>
<html>
<head>
<meta charset="UTF-8">
<title>Gestión Ambulatorio</title>
<link rel="stylesheet" href="./stilo.css" type="text/css"/>
</head>
<body>
<?php
echo $medico3;
echo $medico1;
echo $medico2;
?>
</body>
</html>
|
|
Clases Abstractas
- Cuando realizamos jerarquías, muchas veces encontramos métodos comunes a varias clases.
Esto implicaría que ese método debería pertenecer a una superclase (o clase padre), de la que luego se heredará.
- Pero puede ocurrir que, aunque el concepto del método sea común a todas las clases, la forma de implementarlo sea diferente en cada una.
- En este caso, la forma correcta de proceder es especificar el método en la clase superior y dejar su implementación a las clases derivadas.
- El método especificado en la clase superior será un método sin código, conocido como método abstracto, y la clase donde se define pasa a ser una clase abstracta.
Clase abstracta
- Es aquella clase que tiene uno o más métodos abstractos.
|
Método abstracto
- Es un método que no tiene código asociado.
- Su implementación se realizará en las clases derivadas.
|
- Nunca podremos instanciar un objeto de una clase abstracta.
- Esto es lógico, ya que ese objeto no tendría instrucciones para ejecutar sus métodos abstractos.
|
- Vamos a plantear un ejemplo práctico:
|