Introducción a la programación orientada a objetos

El término “POO” significa Programación Orientada a Objetos, que es una forma de organizar y estructurar el código. La POO nos permite ver un programa como una colección de objetos que se comunican entre sí, en lugar de como una secuencia de comandos y funciones.

En la programación orientada a objetos, un “objeto” es una unidad que contiene datos y funciones que operan con esos datos. Los objetos se crean a partir de “clases”, que pueden entenderse como planos o plantillas de objetos. Una vez que tenemos una clase, podemos crear su “instancia”, que es un objeto específico hecho a partir de esa clase.

Veamos cómo podemos crear una clase simple en PHP. Cuando definimos una clase, usamos la palabra clave “class”, seguida del nombre de la clase, y luego llaves que encierran las funciones de la clase (llamadas “métodos”) y las variables de la clase (llamadas “propiedades” o “atributos”):

class Car
{
	function honk()
	{
		echo 'Beep beep!';
	}
}

En este ejemplo, hemos creado una clase llamada Car con una función (o “método”) llamada honk.

Cada clase debe resolver una sola tarea principal. Si una clase hace demasiadas cosas, puede ser conveniente dividirla en clases más pequeñas y especializadas.

Las clases suelen almacenarse en archivos separados para mantener el código organizado y facilitar la navegación. El nombre del archivo debe coincidir con el nombre de la clase, por lo que para la clase Car, el nombre del archivo sería Car.php.

Cuando se nombran las clases, es bueno seguir la convención “PascalCase”, lo que significa que cada palabra en el nombre comienza con una letra mayúscula, y no hay guiones bajos u otros separadores. Los métodos y propiedades siguen la convención “camelCase”, lo que significa que empiezan con minúscula.

Algunos métodos en PHP tienen roles especiales y están prefijados con __ (dos guiones bajos). Uno de los métodos especiales más importantes es el “constructor”, etiquetado como __construct. El constructor es un método que es llamado automáticamente cuando se crea una nueva instancia de una clase.

A menudo utilizamos el constructor para establecer el estado inicial de un objeto. Por ejemplo, al crear un objeto que representa a una persona, puedes utilizar el constructor para establecer su edad, nombre u otros atributos.

Veamos como usar un constructor en PHP:

class Person
{
	private $age;

	function __construct($age)
	{
		$this->age = $age;
	}

	function howOldAreYou()
	{
		return $this->age;
	}
}

$person = new Person(25);
echo $person->howOldAreYou(); // Outputs: 25

En este ejemplo, la clase Person tiene una propiedad $age y un constructor que establece esta propiedad. El método howOldAreYou() proporciona acceso a la edad de la persona.

La palabra clave new se utiliza para crear una nueva instancia de una clase. En el ejemplo anterior, hemos creado una nueva persona de 25 años.

También puedes establecer valores por defecto para los parámetros del constructor si no se especifican al crear un objeto. Por ejemplo:

class Person
{
	private $age;

	function __construct($age = 20)
	{
		$this->age = $age;
	}

	function howOldAreYou()
	{
		return $this->age;
	}
}

$person = new Person;  // if no argument is passed, parentheses can be omitted
echo $person->howOldAreYou(); // Outputs: 20

En este ejemplo, si no se especifica una edad al crear un objeto Person, se utilizará el valor por defecto de 20.

Por último, la definición de propiedades con su inicialización a través del constructor se puede acortar y simplificar así:

class Person
{
	function __construct(
		private $age = 20,
	) {
	}
}

Espacios de nombres

Los espacios de nombres nos permiten organizar y agrupar clases, funciones y constantes relacionadas evitando conflictos de nombres. Puedes pensar en ellos como carpetas en un ordenador, donde cada carpeta contiene archivos relacionados con un proyecto o tema específico.

Los espacios de nombres son especialmente útiles en proyectos grandes o cuando se utilizan librerías de terceros donde pueden surgir conflictos de nombres de clases.

Imagina que tienes una clase llamada Car en tu proyecto, y quieres colocarla en un espacio de nombres llamado Transport. Lo harías de la siguiente manera

namespace Transport;

class Car
{
	function honk()
	{
		echo 'Beep beep!';
	}
}

Si quieres utilizar la clase Car en otro archivo, tienes que especificar de qué espacio de nombres procede la clase:

$car = new Transport\Car;

Para simplificar, puede especificar al principio del archivo qué clase de un espacio de nombres concreto desea utilizar, lo que le permitirá crear instancias sin mencionar la ruta completa:

use Transport\Car;

$car = new Car;

Herencia

La herencia es una herramienta de la programación orientada a objetos que permite crear nuevas clases a partir de otras ya existentes, heredando sus propiedades y métodos, y ampliándolas o redefiniéndolas según sea necesario. La herencia garantiza la reutilización del código y la jerarquía de las clases.

En pocas palabras, si tenemos una clase y queremos crear otra derivada de ella pero con algunas modificaciones, podemos “heredar” la nueva clase de la original.

En PHP, la herencia se implementa utilizando la palabra clave extends.

Nuestra clase Person almacena información sobre la edad. Podemos tener otra clase, Student, que extienda Person y añada información sobre el campo de estudio.

Veamos un ejemplo:

class Person
{
	private $age;

	function __construct($age)
	{
		$this->age = $age;
	}

	function howOldAreYou()
	{
		return $this->age;
	}
}

class Student extends Person
{
	private $fieldOfStudy;

	function __construct($age, $fieldOfStudy)
	{
		parent::__construct($age);
		$this->fieldOfStudy = $fieldOfStudy;
	}

	function printInformation()
	{
		echo 'Age of student: ', $this->howOldAreYou();
		echo 'Field of study: ', $this->fieldOfStudy;
	}
}

$student = new Student(20, 'Computer Science');
$student->printInformation();

¿Cómo funciona este código?

  • Utilizamos la palabra clave extends para extender la clase Person, lo que significa que la clase Student hereda todos los métodos y propiedades de Person.
  • La palabra clave parent:: nos permite llamar a métodos de la clase padre. En este caso, hemos llamado al constructor de la clase Person antes de añadir nuestra propia funcionalidad a la clase Student.

La herencia está pensada para situaciones en las que existe una relación “es un” entre clases. Por ejemplo, un Student es un Person. Un gato es un animal. Nos permite, en casos en los que esperamos un objeto (por ejemplo, “Persona”) en el código, utilizar un objeto derivado en su lugar (por ejemplo, “Estudiante”).

Es esencial darse cuenta de que el propósito principal de la herencia no es evitar la duplicación de código. Al contrario, un mal uso de la herencia puede llevar a un código complejo y difícil de mantener. Si no existe una relación “es un” entre clases, deberíamos considerar la composición en lugar de la herencia.

Composición

La composición es una técnica en la que, en lugar de heredar propiedades y métodos de otra clase, simplemente utilizamos su instancia en nuestra clase. Esto nos permite combinar funcionalidades y propiedades de múltiples clases sin crear complejas estructuras de herencia.

Por ejemplo, tenemos una clase Engine y otra Car. En lugar de decir “Un coche es un motor”, decimos “Un coche tiene un motor”, que es una relación de composición típica.

class Engine
{
	function start()
	{
		echo "Engine is running.";
	}
}

class Car
{
	private $engine;

	function __construct()
	{
		$this->engine = new Engine;
	}

	function start()
	{
		$this->engine->start();
		echo "The car is ready to drive!";
	}
}

$car = new Car;
$car->start();

Aquí, la Car no tiene todas las propiedades y métodos de la Engine, pero tiene acceso a ella a través de la propiedad $engine.

La ventaja de la composición es una mayor flexibilidad de diseño y una mejor adaptabilidad a futuros cambios.

Visibilidad

En PHP, usted puede definir “visibilidad” para propiedades de clases, métodos y constantes. La visibilidad determina dónde se puede acceder a estos elementos.

  1. Público: Si un elemento está marcado como public, significa que puede acceder a él desde cualquier lugar, incluso fuera de la clase.
  2. Protegido: Un elemento marcado como protected sólo es accesible dentro de la clase y todos sus descendientes (clases que heredan de ella).
  3. Privado: Si un elemento es private, sólo se puede acceder a él desde dentro de la clase en la que se definió.

Si no especifica la visibilidad, PHP la establecerá automáticamente en public.

Veamos un ejemplo de código:

class VisibilityExample
{
	public $publicProperty = 'Public';
	protected $protectedProperty = 'Protected';
	private $privateProperty = 'Private';

	public function printProperties()
	{
		echo $this->publicProperty;     // Works
		echo $this->protectedProperty;  // Works
		echo $this->privateProperty;    // Works
	}
}

$object = new VisibilityExample;
$object->printProperties();
echo $object->publicProperty;        // Works
// echo $object->protectedProperty;   // Throws an error
// echo $object->privateProperty;     // Throws an error

Continuando con la herencia de clases:

class ChildClass extends VisibilityExample
{
	public function printProperties()
	{
		echo $this->publicProperty;     // Works
		echo $this->protectedProperty;  // Works
		// echo $this->privateProperty;   // Throws an error
	}
}

En este caso, el método printProperties() de ChildClass puede acceder a las propiedades públicas y protegidas pero no puede acceder a las propiedades privadas de la clase padre.

Los datos y métodos deben estar lo más ocultos posible y sólo se puede acceder a ellos a través de una interfaz definida. Esto permite cambiar la implementación interna de la clase sin afectar al resto del código.

Palabra clave final

En PHP, podemos usar la palabra clave final si queremos evitar que una clase, método o constante sea heredada o sobrescrita. Cuando una clase es marcada como final, no puede ser extendida. Cuando un método es marcado como final, no puede ser sobrescrito en una subclase.

Ser conscientes de que una determinada clase o método ya no se modificará nos permite realizar cambios más fácilmente sin preocuparnos por posibles conflictos. Por ejemplo, podemos añadir un nuevo método sin temor a que un descendiente pueda tener ya un método con el mismo nombre, provocando una colisión. O podemos cambiar los parámetros de un método, de nuevo sin el riesgo de causar inconsistencia con un método anulado en un descendiente.

final class FinalClass
{
}

// The following code will throw an error because we cannot inherit from a final class.
class ChildOfFinalClass extends FinalClass
{
}

En este ejemplo, si se intenta heredar de la clase final FinalClass se producirá un error.

Propiedades estáticas y métodos

Cuando hablamos de elementos “estáticos” de una clase en PHP, nos referimos a métodos y propiedades que pertenecen a la clase misma, no a una instancia específica de la clase. Esto significa que no tiene que crear una instancia de la clase para acceder a ellos. En su lugar, se les llama o accede directamente a través del nombre de la clase.

Ten en cuenta que como los elementos estáticos pertenecen a la clase y no a sus instancias, no puedes utilizar la pseudo-variable $this dentro de métodos estáticos.

El uso de propiedades estáticas conduce a código ofuscado lleno de trampas, por lo que nunca deberías usarlas, y no mostraremos un ejemplo aquí. Por otro lado, los métodos estáticos son útiles. Aquí tienes un ejemplo:

class Calculator
{
	public static function add($a, $b)
	{
		return $a + $b;
	}

	public static function subtract($a, $b)
	{
		return $a - $b;
	}
}

// Using the static method without creating an instance of the class
echo Calculator::add(5, 3); // Output: 8
echo Calculator::subtract(5, 3); // Output: 2

En este ejemplo, creamos una clase Calculator con dos métodos estáticos. Podemos llamar a estos métodos directamente sin crear una instancia de la clase utilizando el operador ::. Los métodos estáticos son especialmente útiles para operaciones que no dependen del estado de una instancia específica de la clase.

Constantes de clase

Dentro de las clases, tenemos la opción de definir constantes. Las constantes son valores que nunca cambian durante la ejecución del programa. A diferencia de las variables, el valor de una constante permanece igual.

class Car
{
	public const NumberOfWheels = 4;

	public function displayNumberOfWheels(): int
	{
		echo self::NumberOfWheels;
	}
}

echo Car::NumberOfWheels;  // Output: 4

En este ejemplo, tenemos una clase Car con la constante NumberOfWheels. Al acceder a la constante dentro de la clase, podemos utilizar la palabra clave self en lugar del nombre de la clase.

Interfaces de objetos

Las interfaces de objetos actúan como “contratos” para las clases. Si una clase va a implementar una interfaz de objetos, debe contener todos los métodos que la interfaz define. Es una gran manera de asegurar que ciertas clases se adhieren al mismo “contrato” o estructura.

En PHP, las interfaces se definen usando la palabra clave interface. Todos los métodos definidos en una interfaz son públicos (public). Cuando una clase implementa una interfaz, usa la palabra clave implements.

interface Animal
{
	function makeSound();
}

class Cat implements Animal
{
	public function makeSound()
	{
		echo 'Meow';
	}
}

$cat = new Cat;
$cat->makeSound();

Si una clase implementa una interfaz, pero no todos los métodos esperados están definidos, PHP arrojará un error. Una clase puede implementar múltiples interfaces a la vez, que es diferente de la herencia, donde una clase sólo puede heredar de una clase.

Clases Abstractas

Las clases abstractas sirven como plantillas base para otras clases, pero no se pueden crear instancias directamente. Contienen una mezcla de métodos completos y métodos abstractos que no tienen un contenido definido. Las clases que heredan de clases abstractas deben proporcionar definiciones para todos los métodos abstractos del padre.

Utilizamos la palabra clave abstract para definir una clase abstracta.

abstract class AbstractClass
{
	public function regularMethod()
	{
		echo 'This is a regular method';
	}

	abstract public function abstractMethod();
}

class Child extends AbstractClass
{
	public function abstractMethod()
	{
		echo 'This is the implementation of the abstract method';
	}
}

$instance = new Child;
$instance->regularMethod();
$instance->abstractMethod();

En este ejemplo, tenemos una clase abstracta con un método normal y otro abstracto. Luego tenemos una clase Child que hereda de AbstractClass y proporciona una implementación para el método abstracto.

Comprobación de tipos

En programación, es crucial asegurarse de que los datos con los que trabajamos son del tipo correcto. En PHP, tenemos herramientas que proporcionan esta seguridad. Verificar que los datos son del tipo correcto se llama “comprobación de tipo”.

Tipos que podemos encontrar en PHP:

  1. Tipos básicos: Estos incluyen int (enteros), float (números de punto flotante), bool (valores booleanos), string (cadenas), array (matrices), y null.
  2. Clases: Cuando queremos que un valor sea una instancia de una clase concreta.
  3. Interfaces: Define un conjunto de métodos que una clase debe implementar. Un valor que cumpla una interfaz debe tener estos métodos.
  4. Tipos mixtos: Podemos especificar que una variable puede tener múltiples tipos permitidos.
  5. Void: Este tipo especial indica que una función o método no devuelve ningún valor.

Veamos cómo modificar el código para incluir tipos:

class Person
{
	private int $age;

	public function __construct(int $age)
	{
		$this->age = $age;
	}

	public function printAge(): void
	{
		echo "This person is " . $this->age . " years old.";
	}
}

/**
 * A function that accepts a Person object and prints the person's age.
 */
function printPersonAge(Person $person): void
{
	$person->printAge();
}

De esta forma, nos aseguramos de que nuestro código espera y trabaja con datos del tipo correcto, ayudándonos a prevenir posibles errores.

Comparación e identidad

En PHP, puede comparar objetos de dos maneras:

  1. Comparación de valores ==: Comprueba si los objetos son de la misma clase y tienen los mismos valores en sus propiedades.
  2. Identidad ===: Comprueba si se trata de la misma instancia del objeto.
class Car
{
	public string $brand;

	public function __construct(string $brand)
	{
		$this->brand = $brand;
	}
}

$car1 = new Car('Skoda');
$car2 = new Car('Skoda');
$car3 = $car1;

var_dump($car1 == $car2);   // true, because they have the same value
var_dump($car1 === $car2);  // false, because they are not the same instance
var_dump($car1 === $car3);  // true, because $car3 is the same instance as $car1

El operador instanceof

El operador instanceof permite determinar si un objeto dado es una instancia de una clase específica, un descendiente de esa clase o si implementa una determinada interfaz.

Imaginemos que tenemos una clase Person y otra clase Student, que es descendiente de Person:

class Person
{
	private int $age;

	public function __construct(int $age)
	{
		$this->age = $age;
	}
}

class Student extends Person
{
	private string $major;

	public function __construct(int $age, string $major)
	{
		parent::__construct($age);
		$this->major = $major;
	}
}

$student = new Student(20, 'Computer Science');

// Check if $student is an instance of the Student class
var_dump($student instanceof Student);  // Output: bool(true)

// Check if $student is an instance of the Person class (because Student is a descendant of Person)
var_dump($student instanceof Person);   // Output: bool(true)

A partir de las salidas, es evidente que el objeto $student se considera una instancia tanto de la clase Student como de la clase Person.

Interfaces fluidas

Una “interfaz fluida” es una técnica de programación orientada a objetos que permite encadenar métodos en una sola llamada. Esto a menudo simplifica y clarifica el código.

El elemento clave de una interfaz fluida es que cada método de la cadena devuelve una referencia al objeto actual. Esto se consigue utilizando return $this; al final del método. Este estilo de programación se asocia a menudo con métodos llamados “setters”, que establecen los valores de las propiedades de un objeto.

Veamos cómo sería una interfaz fluida para enviar correos electrónicos:

public function sendMessage()
{
	$email = new Email;
	$email->setFrom('sender@example.com')
		  ->setRecipient('admin@example.com')
		  ->setMessage('Hello, this is a message.')
		  ->send();
}

En este ejemplo, los métodos setFrom(), setRecipient(), y setMessage() se utilizan para establecer los valores correspondientes (remitente, destinatario, contenido del mensaje). Después de establecer cada uno de estos valores, los métodos devuelven el objeto actual ($email), lo que nos permite encadenar otro método después de él. Finalmente, llamamos al método send(), que realmente envía el correo electrónico.

Gracias a las interfaces fluidas, podemos escribir código intuitivo y fácilmente legible.

Copiar con clone

En PHP, podemos crear una copia de un objeto utilizando el operador clone. De esta forma, obtenemos una nueva instancia con idéntico contenido.

Si necesitamos modificar algunas de sus propiedades al copiar un objeto, podemos definir un método especial __clone() en la clase. Este método se llama automáticamente cuando se clona el objeto.

class Sheep
{
	public string $name;

	public function __construct(string $name)
	{
		$this->name = $name;
	}

	public function __clone()
	{
		$this->name = 'Clone of ' . $this->name;
	}
}

$original = new Sheep('Dolly');
echo $original->name . "\n";  // Outputs: Dolly

$clone = clone $original;
echo $clone->name . "\n";     // Outputs: Clone of Dolly

En este ejemplo, tenemos una clase Sheep con una propiedad $name. Cuando clonamos una instancia de esta clase, el método __clone() se asegura de que el nombre de la oveja clonada obtenga el prefijo “Clone of”.

Rasgos

Los traits en PHP son una herramienta que permite compartir métodos, propiedades y constantes entre clases y evita la duplicación de código. Puede pensar en ellos como un mecanismo de “copiar y pegar” (Ctrl-C y Ctrl-V), donde el contenido de un trait se “pega” en las clases. Esto permite reutilizar código sin tener que crear complicadas jerarquías de clases.

Veamos un ejemplo sencillo de cómo usar traits en PHP:

trait Honking
{
	public function honk()
	{
		echo 'Beep beep!';
	}
}

class Car
{
	use Honking;
}

class Truck
{
	use Honking;
}

$car = new Car;
$car->honk(); // Outputs 'Beep beep!'

$truck = new Truck;
$truck->honk(); // Also outputs 'Beep beep!'

En este ejemplo, tenemos un trait llamado Honking que contiene un método honk(). Luego tenemos dos clases: Car y Truck, las cuales usan el trait Honking. Como resultado, ambas clases “tienen” el método honk(), y podemos llamarlo en objetos de ambas clases.

Los traits permiten compartir código entre clases de forma fácil y eficiente. No entran en la jerarquía de herencia, es decir, $car instanceof Honking devolverá false.

Excepciones

Las excepciones en POO nos permiten manejar y gestionar los errores que puedan surgir durante la ejecución de nuestro código. Son esencialmente objetos diseñados para registrar errores o situaciones inesperadas en tu programa.

En PHP, tenemos la clase incorporada Exception para estos objetos. Tiene varios métodos que nos permiten obtener más información sobre la excepción, como el mensaje de error, el archivo y la línea donde se produjo el error, etc.

Cuando surge un problema, podemos “lanzar” una excepción (utilizando throw). Si queremos “atrapar” y procesar esta excepción, utilizamos los bloques try y catch.

Veamos cómo funciona:

try {
	throw new Exception('Message explaining the reason for the exception');

	// This code won't execute
	echo 'I am a message that nobody will read';

} catch (Exception $e) {
	echo 'Exception caught: '. $e->getMessage();
}

Es importante notar que una excepción puede ser lanzada más profundamente, durante la llamada a otros métodos.

Para un bloque try, se pueden especificar varios bloques catch si se esperan diferentes tipos de excepciones.

También podemos crear una jerarquía de excepciones, donde cada clase de excepción hereda de la anterior. Como ejemplo, considere una simple aplicación bancaria que permite depósitos y retiros:

class BankingException extends Exception {}
class InsufficientFundsException extends BankingException {}
class ExceededLimitException extends BankingException {}

class BankAccount
{
	private int $balance = 0;
	private int $dailyLimit = 1000;

	public function deposit(int $amount): int
	{
		$this->balance += $amount;
		return $this->balance;
	}

	public function withdraw(int $amount): int
	{
		if ($amount > $this->balance) {
			throw new InsufficientFundsException('Not enough funds in the account.');
		}

		if ($amount > $this->dailyLimit) {
			throw new ExceededLimitException('Daily withdrawal limit exceeded.');
		}

		$this->balance -= $amount;
		return $this->balance;
	}
}

$account = new BankAccount;
$account->deposit(500);

try {
	$account->withdraw(1500);
} catch (ExceededLimitException $e) {
	echo $e->getMessage();
} catch (InsufficientFundsException $e) {
	echo $e->getMessage();
} catch (BankingException $e) {
	echo 'An error occurred during the operation.';
}

En este ejemplo, es importante tener en cuenta el orden de los bloques catch. Dado que todas las excepciones heredan de BankingException, si tuviéramos este bloque en primer lugar, todas las excepciones se atraparían en él sin que el código llegara a los bloques catch posteriores. Por lo tanto, es importante tener las excepciones más específicas (es decir, aquellas que heredan de otras) más arriba en el orden del bloque catch que sus excepciones padre.

Buenas prácticas

Una vez que hayas aprendido los principios básicos de la programación orientada a objetos, es crucial centrarse en las mejores prácticas de la programación orientada a objetos. Éstas te ayudarán a escribir código que no sólo sea funcional, sino también legible, comprensible y fácil de mantener.

  1. Separación de responsabilidades: Cada clase debe tener una responsabilidad claramente definida y debe abordar sólo una tarea principal. Si una clase hace demasiadas cosas, puede ser conveniente dividirla en clases más pequeñas y especializadas.
  2. Encapsulación: Los datos y métodos deben estar lo más ocultos posible y ser accesibles únicamente a través de una interfaz definida. Esto permite cambiar la implementación interna de una clase sin afectar al resto del código.
  3. Inyección de dependencias: En lugar de crear dependencias directamente dentro de una clase, debes “inyectarlas” desde el exterior. Para una comprensión más profunda de este principio, recomendamos los capítulos sobre Inyección de Dependencias.
versión: 4.0