Programación orientada a objetos

Desde que con PHP 5.0 se agregara el soporte inicial al paradigma orientado objetos, en las sucesivas versiones se ha ido enriqueciendo y mejorando este soporte. Hoy día este paradigma es el paradigma dominante a la hora de desarrollar con PHP. Muchísimas bibliotecas, frameworks y proyectos emplean este paradigma; por tanto, a todas luces, conocer la programación orientada a objetos con PHP es fundamental. Hoy por hoy, PHP soporta bastante bien este paradigma, proveyendo no sólo un conjunto amplio de sus características y principios, sino también un rico conjunto de clases básicas de gran utilidad cuando se desarrolla empleando esta técnica de programación.

Síntesis

El paradigma orientado a objetos se fundamenta en la abstracción semántica con el fin de representar un sistema de cosas (objetos), sean concretas o abstractas. La idea es permitir una ingeniería en el que lo(a)s desarrolladores puedan programar sistemas de una forma más natural respecto a nuestra experiencia cotidiana en contraste con otros paradigmas.

En dicho sistema de objetos, estos sufren una secuencia de transformaciones en sus estados por medio de mensajes que se envían mediante interfaces unos a otros. Para poder implementar este paradigma los lenguajes deben soportar una serie de estructuras y relaciones que permitan la construcción de (básicamente) clases e instancias. No obstante, el paradigna orientado a objeto es más que sólo tales clases e instancias, también implica el soporte de interfaces, herencia, alcance, constructores, clonación, excepciones, etc.

El paradigma de la programación orientada a objetos se sustenta en cuatro principios fundamentales:

  • Abstracción

  • Encapsulamiento

  • Composición

  • Generalización

Abtracción

La abstraciión en una de las metodologías más usuales para abordar la complejidad. Se basa en la idea de descomponer o simplificar un concepto con el fin de enfatizar los elementos de interés y descartar el resto, por lo general, dentro de los límites establecidos por un contexto.

En la ingeniería informática se pretende seguir el principio de mínima sorpresa, que implica capturar aquellos atributos y comportamientos que no deberían generar sorpresas o ambigüedades dentro de un contexto dado. La idea es prevenir la implementación de características irrelevantes dentro de la abstracción y garantizar que la abstracción tenga sentido para el propósito del contexto.

La estructura central objeto de la abstracción es la clase. Como producto de la abstracción, se determinan los elementos esenciales de un concepto, dichos elementos son empleados para definir una clase. Luego cualquier objeto creado de dicha clase representa una instancia del concepto abstraído, así como (usualmente) ciertas características particulares.

So observará que los conceptos abstraídos en clases tienden a representar «categorías» y que plantean una suerte de «declaración de intenciones», una especie de plantilla o estereotipo, en tanto que declara como puede ser una entidad y como puede comportarse, pero no define entidad alguna.

También puede entenderse una clase como una declaración particular de tipo de datos. Ya se han visto los tipos primitivos de PHP, por ejemplo los enteros (int). Dicho tipo declara como es un entero cualesquiera, como se opera y cuales son las limitaciones que le atañen, pero no define ningún entero en particular. Luego, al observar las limitaciones de los tipos primitivos se manifiesta evidente que difícilmente pueden representar por sí mismos abstracciones más complejas, por ejemplo un Gato o un Planeta, y por una buena razón: no todos los sistemas necesitan representar gatos o planetas, o cualquier otro sustantivo que al(a la) lector(a) le venga a la mente. Y esto precisamente es lo que permiten las clases, declarar esas estructuras específicas, esos tipos de datos particulares para el sistema que se está atendiendo.

Las clases tienden a representar sustantivos, cuando se realiza un análisis de requerimientos, los sustantivos y/o categorías identificadas son potenciales clases del sistema. A partir de allí, declarar una clase no es algo difícil.

Clases y objetos

Una clase, pues, es una definición de tipo tal que se compone de un conjunto de miembros y que expone una interfaz accesible externamente a la clase, en tanto que oculta la implementación interna de de su funcionalidad.

Declaración

Para declarar una clase, se utiliza la sentencia «final» class NombreClase {}, donde «final» es un identificador opcional que determina que la clase no es heredable (más adelante se ahonda en este tema); luego el identificador class que es necesario para declarar que la estructura a continuación es una clase; y finalmente NombreClase representa un nombre arbitrario con el que se designa la clase en cuestión. Es usual que los nombres de las clases sean sustantivos, ya que su valor semántico brinda información inferible sobre la misma. También nótese como el nombre de la clase inicia con mayúscula, esto es una convención, pero una convención ampliamente extendida: los nombres de clases siempre deben comenzar con mayúscula [1].

// declaración de una clase
class NombreClase {
    // Propiedades
    // ...
    // Métodos
}

Se ha mencionado a Gato y Planeta como abstracciones complejas que pueden representarse como clases. Como puede verse, ambos nombres son sustantivos y cada uno expone una categoría, así como ninguno define ningún gato ni planeta en concreto.

// declarar la clase Gato
class Gato {
    // Propiedades
    // ...
    // Métodos
}

// declarar la clase Planeta
class Planeta {
    // Propiedades
    // ...
    // Métodos
}

Objetos

Por objeto ha de entenderse la instancia concreta de una clase. Luego, de una clase pueden existir muchos objetos. Es decir, estos se componen como entidades particulares de una clase; por ejemplo (es sencillo observar que) «Félix» es un gato específico del tipo Gato, y que «Marte» y «Venus» son planetas particulares del tipo Planeta.

Para instar un objeto se utiliza el operador new y una sentencia de asignación:

$objetoUno = new NombreClase();
$objetoDos = new NombreClase();

// Siguiendo los ejemplos de Gato y Planeta:
$felix = new Gato();
$venus = new Planeta();
$marte = new Planeta();

Miembros

Los elementos que caracterizan a una clase y determinan ese como puede ser Y como se comportan radican en dos construcciones: propiedades y métodos. A estos dos elementos se les conoce como miembros.

Toda clase debe componerse por propiedades y métodos; estos son los que le dan verdadero significado a una clase, los nombres de clase son arbitrarios y tienen poco más que valor semántico (fundamental, sea dicho, para una buena estructuración), pero son sus miembros los que le aportan identidad. Es fácil intuir que un gato carece momento angular y movimiento de traslación, y que un planeta no tiene patas ni maúlla; la identidad de estas instancias emerge de sus atributos y comportamientos: los gatos tienen patas y maúllan, los planetas tienen un diámetro y rotan.

Al conjunto de miembros expuestos por una clase se le llama interfaz de la clase; se ha utilizado el adjetivo «expuesto» en tanto que cada miembro es susceptible de un determinado «ámbito» o «alcance».

Ámbito o alcance

Se declara un ámbito o alcance por miembro, tal alcance determina la restricción de acceso o visibilidad del miembro respecto de otras clases. Naturalmente, todos los miembros de una clase son accesibles para sí misma, empero el cumplimiento del principio de encapsulamiento demanda que determinados miembros de la interfaz no sean accesibles para otras clases, y otros miembros sí. A la interfaz accesible por otras clases se le llama interfaz pública. Para restringir el acceso a los miembros se dispone de tres niveles de alcance:

  • Público (public)

  • Protegido (protected)

  • Privado (private)

Nota

Si no se declara alcance para un miembro, entonces tal se asume como público.

Alcance público

Establece que el miembro es accesible, además de la propia clase, por cualquier otra clase que tenga acceso a la clase en cuestión.

Alcance protegido

Establece que el miembro es accesible solo para la propia clase y clases derivadas, pero no de otras clases e instancias aunque tengan acceso a la clase en cuestión.

Alcance privado

Establece que el miembro es accesible exclusivamente por la propia clase, excluyendo incluso a las clases derivadas.

Propiedades

Las propiedades son los elementos que describen como es o como se compone una clase, es decir, representan un atributo de sus objetos.

Las propiedades, al final, no son más que variables asociadas al alcance de una clase, y por tanto comparten las características de las variables; dicho de otra forma: son de tipo dinámico (adoptan el tipo de dato del valor) y se nombran con las mismas reglas que una variable.

Para declarar propiedades se emplea la sentencia «alcance» $nombrePropiedad « = defecto»; listadas a nivel de estructura de una clase, donde «alcance» corresponde a uno de los tres niveles de alcance anteriormente expuestos (recordando que de omitirlo, por defecto se declara como public), $nombrePropiedad representa un nombre arbitrario que identifique a la propiedad y, finalmente, « = defecto» una asignación opcional para inicializar la propiedad con un valor determinado:

class NombreClase {
    public $nombrePrapiedadA = null;
    protected $_nombrePropiedadB = 'B';
    private $__nombrePropiedadC = ['C'];
    // ...
    // Métodos
}

El siguiente ejemplo ilustra mejor las propiedades de Gatos y Planetas:

class Gato {
    private $__colorDeOjos;
    private $__nombre;
    private $__nacimiento;
}

class Planeta {
    private $__nombre;
    private $__diametro;
}

Métodos

Los métodos son los elementos que definen el comportamiento de un objeto, es decir, que hacen, como operan y como funcionan. Semejante a las propiedades, en el caso de los métodos estos no son más que funciones asociadas al alcance de una clase, y de igual manera comparten las reglas de declaración de cualquier función de PHP.

Para declarar un método se utiliza la sentencia «alcance» nombreMetodo(«parámetros») « : tipoRetorno» {} donde el alcance aplica igual que para las propiedades y el nombre del método también es un nombre arbitrario que identifique al método, luego, entre paréntesis se puede declarar una lista opcional de parámetros y, finalmente, un tipo de retorno opcional.

class NombreClase {
    public $nombrePrapiedadA = null;
    protected $_nombrePropiedadB = 'B';

    // ...
    public function doSomething() : bool {
      // ...
    }
}

Un comportamiento típico de los gatos, por ejemplo, es maullar; y de los planetas, rotar:

class Gato {
    // Propiedades
    // ...

    public function maullar() {
        echo "¡Miau!";
    }
}

class Planeta {
    // Propiedades
    // ...

    public function rotar($grados) {
      echo "Rotando $grados grados";
    }
}

Encapsulamiento

Los niveles de alcance permiten el cumplimiento del principio de encapsulamiento. Este principio pretende aislar el estado o datos de un objeto y ocultar los detalles de la implementación, proveyendo el acceso mediante interfaces. La interfaz de una clase no es más que el conjunto de miembros accesibles de dicha clase. Así, existe una interfaz pública compuesta por todos los miembros cuyo alcance es público, y lo mismo para los miembros protegidos y privados.

Al envolver (y con ello, «aislar» de cierta forma) la implementación se procura mantener el principio de responsabilidad mínima, es decir, que una pieza de funcionalidad sea responsable de gestionar nada más que su propio estado, o de ejecutar una sola tarea. Si se cambia el mecanismo de una funcionalidad (por ejemplo, por una versión más eficiente) pero se mantiene la misma interfaz, como consecuencia se obtiene una mayor transparencia, en tanto que los usuarios de dicha interfaz no deben realizar ningún cambio y aun así se ven beneficiados por la mejora en la funcionalidad que consumen.

Un ejemplo típico de la implementación del encapsulamiento son los establecedores (setters) y recuperadores (getters), mediante los cuales se pueden garantizar ciertos comportamientos, como el tipo de un dato, la estructura o patrón de una cadena, límites numéricos, etc. Ejemplos:

class Gato {
  // Propiedades
  // ...

  /**
   * Garantiza que solo se establecezca uno de los tres colores posibles
   */
  public function setColorDeOjos(string $color) {
    if(in_array($color, ['negro', 'verde', 'amarillo'])) $this->__colorDeOjos = $color;
    else echo "Color de ojos inválido";
  }

  public function getColorDeOjos() : string {
    return $this->__colorDeOjos;
  }

  /**
   * Comprueba el tamaño de la cadena, y si es mayor a 65 lo trunca.
   */
  public function setNombre(string $nombre) {
    if(strlen($nombre) > 65) $nombre = substr($nombre, 0, 65);
    $this->__nombre = $nombre;
  }

  public function getNombre() : string {
    return $this->__nombre;
  }

  /**
   * Garantiza que la fecha se establezca como un tipo \DateTime
   */
  public function setFechaNacimiento(\DateTime $nacimiento) {
    $this->__nacimiento = $nacimiento;
  }

  public function getFechaNcimiento() : \DateTime {
    return $this->__nacimiento;
  }
}

class Planeta {
    // Propiedades
    // ...

    /**
     * Comprueba el tamaño de la cadena, y si es mayor a 255 caracteres lo trunca.
     */
    public function setNombre(string $nombre) {
      if(strlen($nombre) > 255) $nombre = substr($nombre, 0, 255);
      $this->__nombre = $nombre;
    }

    public function getNombre() : string {
      return $this->__nombre;
    }

    /**
     * Establece el diámetro a partir del radio.
     */
    public function setDiametro(float $radio) {
      $this->__radio = $radio;
      $this->__diametro = 2 * $this->__radio;
    }

    public function getDiametro() : float {
      return $this->__diametro;
    }
}

El código anterior puede parecer al principio un poco extenso, pero es bastante sencillo. El método setColorDeOjos() de la clase Gato garantiza no sólo que el parámetro proveido sea una cadena, sino que además verifica que sea una de tres posibles. Luego, si se necesita, por ejemplo, soportar más colores de ojos, se podrían agregrar a la sentencia de verificación, manteniendo la interfaz pero ampliando la funcionalidad.

Por otra parte, el método setDiametro() espera un valor de radio como parámetro y en su implementación calcula el diámetro; y similar a lo dicho anteriormente, es posible ampliar la funcionalidad del método sin romper la compatibilidad mientras se mantenga la interfaz.

Puede notarse que para los métodos get se ha indicado un tipo de retorno. En PHP, como ya se dijo en el apartado de funciones, los tipos de retorno son opcionales, pero su uso permite mantener un código mucho más cohesionado y unas interfaces más estables, lo que facilita el mantenimiento del propio código.

Dicho lo anterior, no todos los método tienen que ser del tipo set o get, por ejemplo, los planetas rotan:

  class Planeta {
    // Propiedades
    // ...

    public function rotar(int $grados) {
        if($grados < 1 || $grados > 360) echo "Grados de rotación fuera de rango.";
        else echo "Rotando $grados"."° grados";
    }

    // otros métodos...
}

El método rotar() no cambia ningún estado, sólo imprime el dato en pantalla, pero verifica que los grados proveidos correspondan a un valor entre 1° y 360°, que puede considerarse válido.

Es importante que exista un análisis y diseño previo a la redacción de las clases, a fin de que el diseño provea de una abstracción lo bastante amplia y permita un desarrollo más sencillo del código. Esto, por supuesto, no es siempre posible, pero del diseño se pueden obtener al menos unas interfaces lo más cercanas posibles a la implementación necesaria, y gracias al encapsulamiento, si se respetan las interfaces, los cambios dentro de los métodos pueden ser incrementales y mantener la funcionalidad del código a lo largo del desarrollo.

Interfaces

Existen escenarios en los que diferentes clases pueden exponer una misma interfaz, pero que en efecto se tratan de tipos diferentes y por eso son clases distintas, no obstante es posible observar que pertenecen a un sólo tipo abstracto. Interesa muchas veces poder garantizar un contrato tal en que una clase implemente una interfaz concreta, y dicho contrato es posible gracias a las Interfaces. En PHP las Interfaces se declaran mediante la sentencia interface NombreInterfaz {} donde interface es el identificador de declaración y NombreInterfaz es un nombre arbitrario que la identifica.

Advertencia

Es importante no confundir interfaz (nótese la «i» minúscula) como el concepto de los «miembros expuestos de una clase» con Interfaz (nótese la «I» mayúscula), el concepto de la construcción del lenguaje que se expone mediante el identificador interface y permite la declaración de estas construcciones.

Una Interfaz es una suerte de «declaración de intenciones». Se compone de definiciones de constantes y declaraciones abstractas de métodos públicos; se le llama «abstracta» porque todos los métodos de una Interfaz carecen de implementación; como «declaración de intenciones» tiene por finalidad servir como ese contrato que debe cumplir una clase, garantizando así el uso transparente de diferentes objetos con la misma interfaz.

Por ejemplo, un Gato es un ser vivo, e interesa abstraer ciertos comportamientos de cualquier ser vivo, como caminar, comer o emitir sonidos; o bien un planeta es una forma muy concreta de esfera que tiene la capacidad de rotar o de devolver las cantidades de sus magnitudes, como el diámetro o el radio. Así, podrían definirse las siguientes interfaces:

interface SerVivo {
    public function caminar($distancia);
    public function emitirSonido();
}

interface Esfera {
    public function rotar(int $grados);
    public function getDiametro();
}

Posteriormente, en la sentencia de declaración de clase, mediante el identificador implements, se establece el contrato, y con ello la obligación de definir todas las implementaciones que la Interfaz declara.

class Gato implements SerVivo {
    // propiedades
    // ...

    public function caminar($distancia) {
        echo "Avanza $distancia";
    }

    public function emitirSonido() {
        echo "¡Miau!";
    }
}

class Planeta implements Esfera {
    // propiedades
    // ...

    public function rotar() {
        echo "Rotando $grados"."° grados";
    }

    public function getDiametro() : float {
        return $this->__diametro;
    }
}

Reutilización

La reutilización es uno de los principios fundamentales y más útiles de la programación orientada a objetos. Una funcionalidad tiende a ser útil en más de un contexto, y resulta provechoso poder utilizar un código que ya existe en lugar de repetir la misma funcionalidad por cada contexto que se aborda. En el paradigma orientado a objetos la reutilización se presta en dos conceptos relacionados: herencia y polimorfismo.

Herencia

Muchos elementos que se abstraen mediante el paradigma orientado a objetos obedecen a una jerarquía de categorías. Por ejemplo, en biología un gato es también un mamífero, e inmediatamente tenemos conciencia de que existen muchos tipos de mamíferos, como los perros, las focas y un enorme etcétera. Lo mismo puede aplicar al resto de la realidad, a las matemáticas, la geometría. etc.

Recordando que el paradigma de la POO plantea el modelado de la realidad por medio de la abstracción, mediante la herencia es posible modelar esa jerarquía de categorías. Naturalmente, dependiendo del sistema que se modela puede interesar una mayor o menor profundidad en el modelado de dichas categorías, por ejemplo si entre «Mamífero» y «Gato» interesa abstraer «Felino» o no.

La herencia es también un mecanismo de especialización. Si se tiene una funcionalidad general en una clase y se necesita una funcionalidad específica al tiempo que se debe mantener el resto de funcionalidad general, entonces la herencia permite implementar clases con esas funcionalidades especializadas.

La herencia plantea a las clases superiores o generales como clases base, y las menores o especializadas como clases derivadas. También se entiende que las clases derivadas extienden de las clases bases, y que una clase derivada es un tipo de la clase base. Para definir una clase que hereda de otra se emplea el identificador extends en la sentencia de declaración de clase: class ClaseDerivada extends ClaseBase {}.

class Mamifero {
    private $__colorDeOjos;
    private $__nombre;
    private $__nacimiento;

    // Métodos
    // ...

    public function emitirSonido() {
        echo "«Un mamífero cualquiera»";
    }
}

class Gato extends Mamifero {
    public function emitirSonido() {
        echo "¡Miau!";
    }
}

class Perro extends Mamifero {
    public function emitirSonido() {
        echo "¡Guau!";
    }
}

$gato = new Gato();
$perro = new Perro();

$gato->emitirSonido(); // ¡Miau!
$perro->emitirSonido(); // ¡Guau!

En el ejemplo se puede observar como en la clase Mamifero se define e implementa todo aquello (que interesa y) que es común a un mamífero cualesquiera, y luego las clases Gato y Perro extienden de Mamifero reimplementado o especializando el método emitirSonido().

Nota

Existen dos tipos de herencia: simple y múltiple. La mayoría de lenguajes que soportan POO, como PHP, suelen implementar únicamente la herencia simple. Otros lenguajes, como C++ soportan la herencia múltiple. El criterio suele ser el factor de complejidad, en tanto los conflictos tienden a ser mayores y más complejos en la herencia múltiple que en la herencia simple.

Rasgos Traits

La herencia simple es la única soportada por PHP, pero existen también ciertas ventajas en la herencia múltiple. Los Traits (Rasgos) proveen un mecanismo que pretende proveer lo mejor de la herencia múltiple evitando sus problemas y complejidades. Los Traits son construcciones semejantes a las clases en tanto son una composición de propiedades y métodos. Se asemejan a una Interfaz en tanto exponen una interfaz y un contrato de composición y funcionalidad, pero un Rasgo no es un tipo, sino que se limita a componer el modo y forma de un tipo. Luego, las clases pueden incluir los Rasgos en su construcción. De esta forma es posible definir Rasgos de funcionalidad más atomizada en archivos específicos, utilizar tales Rasgos en tantas clases como haga falta y, en igual medida, redefinir en las clases lo que los Rasgos definen.

Por ejemplo, los planetas tienen un eje y rotan en torno al mismo, pero no solo los planetas rotan, también lo pueden hacer los círculos, los cilindros, los conos, etc. No obstante no sería la mejor práctica definir una clase base que implemente únicamente un método rotar() y luego toda la jerarquía de clases desde dicha clase base. Es mucho más eficiente definir un Rasgo Rotar y agregarlo a toda clase que haga falta.

trait RotarAware {
  public function rotar(int $grados) {
      if($grados < 1 || $grados > 360) echo "Grados de rotación fuera de rango.";
      else echo "Rotando $grados"."° grados";
  }
}

class Planeta {
    use RotarAware;
}

class Cilindro {
    use RotarAware;
}

$marte = new Planeta();
$unCilindro = new Cilindro();

$marte->rotar(120);
$unCilindro->rotar(180);

En el ejemplo puede advertirse otra ventaja, y es que mientras la funcionalidad sea exactamente la misma aun en clases diferentes, al ser un sólo lugar dónde se implementa tal funcionalidad se maximiza la reutilización de código.

Polimorfismo

El polimorfismo se relaciona estrechamente con la herencia, este principio del paradigma plantea precisamente lo que en el apartado de Herencia se expuso como especialización. Pero el polimorfismo se refiere a la idea de que un método de igual firma (en decir, nombre, retorno, número y tipo de parámetros) en diferentes clases implementen diferentes funcionalidades. En el ejemplo de herencia las clases Gato y Perro heredan de Mamífero, pero el método emitirSonido() en la clase Gato imprime un ¡Miau! y en la clase Perro imprime ¡Guau!.

Espacios de nombres (Namespaces)

Como se mencionó antes, la comunidad de PHP tiene un montón de desarrolladores creando un montón de código. Esto significa que una biblioteca de PHP puede utilizar el mismo nombre de clase que otra. Cuando ambas bibliotecas son usadas en el mismo espacio de nombre, colisionan causando problemas.

Los espacios de nombres solucionan estos problemas. Tal como se describe en el Manual de referencia de PHP, los espacios de nombres pueden compararse con los directorios de los sistemas operativos que dan espacios de nombres a los archivos; dos archivos con el mismo nombre coexisten en directorios separados. Semejantemente, dos clases de PHP con el mismo nombre pueden coexistir en distintos espacios de nombres. Tan sencillo como eso.

Es importante establecer el código dentro de espacios de nombres de tal manera que puedan ser utilizados por otros desarrolladores sin que tengan que preocuparse por colisiones con otras bibliotecas.

Una recomendación dada para el uso de espacios do nombres se encuentra en la PRS-4, que sugiere proveer una convención a los archivos, clases y espacios de nombres para permitir un código de «conectar y ejecutar».

En octubre de 2014 el PHP-FIG marcó obsolescente la anterior convención de autocarga: PSR-0. Tanto PSR-0 como PSR-4 permanecen perfectamente utilizables. Los últimos requerimientos con PHP5.3 y muchos proyectos exclusivos en PHP 5.2 implementan la PSR-0.

Si vas a utilizar una convención de autocarga para un nuevo proyecto o paquete, considerá PSR-4.

Estructurando un proyecto

La biblioteca estándar de PHP (Standard PHP Library [SPL])

La biblioteca estándar de PHP está empaquetada con el propio motor de PHP y provee una colección de clases e interfaces. Está construida principalmente para atender las típicas necesidades de clases de estructuras de datos (stack [lista], queue [cola], heap [pila], etc), e iteradores que pueder recorrer sobre esas estructuras de datos o sobre tus propias clases que implementen interfaces de la SPL.

Footnotes