Hasta el anterior capítulo se ha hecho un repaso y se ha profundizado en algunos de los principios y herramientas de la Programación Estructurada, con esa base, a partir de este capítulo inicia el estudio de la Programación Orientada a Objetos (POO).
La programación orientada a objetos es un paradigma de programación, donde la solución de los problemas se concibe como conjuntos de objetos que cooperan entre sí para solucionar los problemas. Un paradigma de programación es un estilo, un enfoque, una forma de ver y modelar el mundo real dentro del código.
Un objeto en la POO es la representación simplificada de una "cosa" (algo o alguien). Esa "cosa" puede ser un objeto de la vida real (un auto, una casa, una persona, etc.), o un concepto simple o complejo (una venta, un préstamo, una hipoteca, etc.).
En analogía con los objetos de la vida real, como un auto, por ejemplo, que tiene un conjunto de características como marca, modelo, color, cilindrada, potencia, etc., y algunas funcionalidades como encender, acelerar, frenar, girar, apagar, etc., un objeto de la POO tienen también un conjunto de datos, denominadps atributos (o propiedades) y algunas funciones, denominadas métodos.
El conjunto de valores que tienen los atributos de un objeto en cualquier momento se conoce como el estado del objeto.
La diferencia fundamental entre la Programación Estructurada (PE) y la Orientada a Objetos (POO) es que en la PE los datos y las funciones están separadas, mientras que, en la POO, los datos (los atributos) y las funciones (los métodos) están dentro de una misma estructura: El objeto. Los atributos de un objeto son el equivalente a las variables de la PE y los métodos de un objeto son el equivalente a las funciones de la PE.
En resumen, en la POO, los datos y las funciones están dentro de una misma estructura: El objeto. En la POO los datos se conocen como atributos y las funciones como métodos y el estado del objeto es el conjunto de valores que tienen esos atributos en un momento dado.
Tanto la Programación Estructurada, como la Programación Orientada a Objetos, son paradigmas, es decir formas de ver y modelar el mundo real dentro del código.
En la PE se modela el mundo identificando las funciones que se requieren para resolver el problema, así como los datos que necesitan esas funciones para realizar sus tareas. En la POO se modela el mundo identificando el conjunto de objetos con los que se resuelve el problema, así como los métodos y atributos de esos objetos.
Debido a la diferencia de enfoques, en la PE la solución consta de un conjunto de funciones y un conjunto de datos/estructuras de datos, mientras que, en la POO, la solución consta de un conjunto de objetos, cada uno con sus propios atributos y métodos.
En consecuencia, al resolver un problema con el enfoque estructurado se piensa en las funciones que se requieren para resolver el problema y en los datos que requieren esas funciones. En el enfoque orientado a objetos, se piensa en los objetos con los que se puede resolver el problema y en los atributos y métodos de esos objetos.
En el paradigma de la Programación Orientada a Objetos, se emplean los términos clase y objeto y frecuentemente se emplean como sinónimos, sin embargo, son dos conceptos diferentes.
Como se dijo un objeto es la estructura que contiene tanto los datos (atributos) como las funciones (métodos), en consecuencia, si se crea una estructura que contiene tanto los datos como las funciones, se ha crea un objeto.
Las clases son un tipo especial de objetos que permiten crear objetos de un determinado tipo. Son como fábricas que producen, cada una de ellas, un determinado tipo de objeto. Por esa razón son conocidas también como Objetos Constructores.
En la clase se declaran todos los métodos y atributos que tendrán los objetos creados a partir de la misma, pero no se asignan los valores a los atributos (aunque sí es posible asignar valores por defecto). Los verdaderos valores de los atributos se asignan recién cuando se crea un objeto a partir de una clase.
La razón principal por la que se emplean clases en la POO es porque los objetos creados a partir de una clase pueden utilizar los métodos de esa clase. Todos los objetos de una clase (y pueden ser cientos o miles de objetos) emplean directamente los métodos de la clase, no se crean copias para cada objeto.
Si no se emplearían clases (u objetos constructores), se tendrían que volver a declarar los métodos en cada uno de los objetos del mismo tipo, lo que por una parte implicaría la duplicación irracional de código (con el consiguiente consumo de memoria y tiempo) y por otra, haría al sistema vulnerable a errores, porque cuando se corrija o modifique un método (algo que casi siempre ocurre) esa modificación/corrección tendría que ser replicada (manualmente) en cada uno de los objetos.
Los métodos de una clase trabajan exclusivamente con los atributos del objeto desde el cual son llamados, por lo que deben tener acceso al objeto. En el caso de JavaScript, los métodos tienen acceso al objeto mediante la variable this. Esta variable, que es creada automáticamente, es el nombre que le da JavaScript al objeto desde el cual ha sido llamado el método.
En conclusión, una clase es un objeto especializado en la creación de objetos de un determinado tipo. En una clase se declaran los métodos y los atributos del objeto, pero no se asignan los valores finales. Un objeto de una clase, por otra parte, es el resultado de crear un objeto a partir de una clase. El objeto, conocido como instancia de la clase, tiene sus atributos con los valores asignados (tiene un estado) y puede llamar directamente a los métodos de la clase, aunque esos métodos no le pertenecen (pertenecen a la clase).
La Programación Orientada a Objetos se rige por 4 fundamentos, 4 pilares que guían el diseño de software cuando se aplica este paradigma.
Este principio dice que al identificar un objeto el mismo debe ser representado de manera simplificada, mostrando únicamente los aspectos esenciales para el problema, pero ocultando los aspectos irrelevantes, así como los detalles de su implementación. Se debe pensar en "que hace el objeto" en lugar del "cómo lo hace".
Este principio dice que sólo debe ser posible acceder/modificar los atributos de un objeto mediante los métodos del objeto, protegiendo así su estado.
Este principio dice que una clase (la clase hija) puede heredar los atributos y métodos de otra (la clase padre) evitando así la duplicación de código, extendiendo y especializando su comportamiento a través de nuevos métodos y atributos.
Este principio dice que cuando un método es llamado desde un objeto, debe responder de diferentes formas en función a la clase a la que pertenece el objeto. El principio instruye implementar los métodos (sobre todo en las clases heredadas) de manera que repondan a las características específicas de la clase.
En JavaScript es posible declara un objeto sin que sea creado explícitamente a partir de una clase. Los objetos creados de esta manera se conocen como objetos literales y sen de utilidad práctica en aquellos casos donde sólo se requiere un objeto de un determinado tipo, es decir cuando sólo se necesita una instancia del objeto y en consecuencia, no se justifica crear una clase que será empleada una sola vez.
Para crear un objeto literal simplemente se escribe, entre llaves y separados con comas, los atributos y métodos del objeto. Con frecuencia, los objetos literales constan únicamente o de atribustos (datos) o de métodos (funciones) y pueden ser inclusive objetos vacíos.
Un objeto literal vacío se crea simplemente escribiendo llaves sin nada en su interior:
Una vez creado el objeto vacío, es posible añadir atributos y métodos empleando la notación de punto. Por ejemplo se pueden añadir los atributos "nombres" y "apellidos", así como el método "nombreCompleto" que empleando una plantilla literal muestre el nombre completo:
Note que en el método "nombreCompleto" se emplea this para acceder a los atributos del objeto. Como se dijo "this" es el nombre que JavaScript asigna automáticamente al objeto que llama al método (en este caso el objeto "persona").
Por supuesto, en lugar de crear un objeto literal vacío y luego añadirle atributos y métodos, se puede crear directamente el objeto con esos atributos y métodos, como sucede con el objeto "persona2":
Sin embargo, lo más frecuente en la práctica es crear objetos a partir de clases y solo ocasionalmente objetos literales. Pero incluso un objeto literal, aunque no se crea explícitamente a partir de una clase, internamente es creada por JavaScript a partir de la clase "Object" (la clase padre de todos los objetos en JavaScript).
En JavaScript las clases pueden ser creadas en diferentes formas, sin embargo, actualmente se crea principalmente con la palabra reservada class, de acuerdo a la siguiente sintaxis:
Todos los elementos de una clase (los elementos mostrados en la sintaxis) son opcionales. Así, al igual que ocurre con un objeto literal, es posible crear una clase sin ningún elemento: una clase vacía:
Y, aunque es una clase vacía, se pueden crear objetos (instancias) de esa clase.
Para crear un objeto (una instancia) de una clase se emplea la palabra reservada new, de acuerdo a la siguientes sintaxis:
Así, para crear el objeto 'obj1' de la clase Vacío se escribe:
Obviamente, al ser una clase vacía, se obtiene un objeto vacío. Cada uno de los objetos que se crean a partir de una clase son únicos, aunque tengan los mismos atributos, así, por ejemplo, si se crea otro objeto vacío:
Y se compara con el objeto creado previamente:
El resultado es falso (con los dos operadores de igualdad) porque, aunque ambos son objetos vacíos de la misma clase, cada uno tiene su propia dirección de memoria (está guardado en su propio espacio de memoria) y cuando se comparan objetos, se comparan esas direcciones de memoria. En consecuencia dos objetos son iguales únicamente cuando en realidad se trata del mismo objeto. Por ejemplo si el objeto "obj2" se guarda en la variable "obj3" y se compara con "obj2":
El resultado es verdadero (true), porque, aunque son dos variables diferentes, ambas apuntan a la misma dirección de memoria (ambas hacen referencia al mismo objeto, se trata de un objeto con dos nombres: "obj2" y "obj3").
Al igual que ocurre con un objeto literal, una vez creado un objeto a partir de una clase, es posible (en JavaScript) añadirle propiedades y métodos. Por ejemplo en las siguientes instrucciones se añaden los atributos "r=2.3" y "s=4.1" al objeto "obj1" y los atributos "x=5.2" y "y=7.1" al objeto "obj2":
Sin embargo, esa no es una buena práctica de programación. Sí es necesario añadir un atributo o método a un objeto, entonces se tienen que revisar el código para ver si ese atributo/método debería ser añadido a la clase.
Cuando se declara una clase (cuando se crea una clase) no puede volver a ser declarada. En ese sentido, las clases se comportan de manera similar a las variables declaradas con let o const.
Debido a que todo el código JavaScript en una página WEB corresponde a una sola sesión, todos los elementos que se declaran: variables, funciones, clases, etc., permanecen en memoria hasta que la página es cerrada o recargada, por lo tanto, todas las clases declaradas permanecen en memoria y como no pueden volver a ser declaradas, cuando se intenta modificarlas (algo que es necesario casi siempre), JavaScript lanza un error informado que la clase ya fue declarada..
Por esa razón, en la asignatura, las clases se declaran como funciones expresión en lugar de la forma estándar (de manera análoga a una función estándar y una función anónima):
Normalmente, cuando se crea una clase, se declara también su constructor. El constructor es un método especial (una función) que se ejecuta automáticamente cuando se crea una instancia de la clase (cuando se crea un objeto) y es empleado para asignar valores iniciales a los atributos del objeto, así como realizar algunas validaciones y ejecutar los procedimientos iniciales que sean necesarios para dejar el objeto listo para su uso.
Así para representar a una persona en la POO, se debe crear una clase, por ejemplo, la clase "Persona" y en aplicación del principio de la abstracción, debe ser una representación simplificada de una persona, que en este caso tendrá solo los siguientes atributos: nombres, apellidos, carnet de identidad y fecha de nacimiento, el constructor de la clase se emplea para permitir asignar valores a estos atributos cuando se crea un objeto.
Por otra parte, para aplicar correctamente el principio de la encapsulación los atributos deben ser privados, es decir que sólo deben poder ser leídos y modificados por los métodos de la clase. En JavaScript, para declarar un atributo como privado, el nombre del atributo debe estar precedido por el símbolo #.
Para que los atributos puedan ser leídos y modificados desde los objetos de la clase, se deben programar dos tipos de métodos especiales: los métodos getter y los métodos setter. Los métodos getter se declaran anteponiendo la palabra get al nombre del método y se caracterizan porque no tienen ningún parámetro (no reciben ningún dato) y devuelven un resultado (normalmente el valor de un atributo o el resultado de alguna operación). Los métodos setter, se declaran anteponiendo la palabra set al nombre del método y se caracterizan porque sólo tienen un parámetro (donde reciben el nuevo valor del atributo). Casi siempre, antes de asignar el nuevo valor, se debe validar que el valor recibido sea un valor válido.
Además, la clase contará con dos métodos: uno para devolver el nombre completo de la persona y otro para calcular y devolver la edad de la persona.
Finalmente, siguiendo el principio del polimorfismo, se sobrescriben los métodos "toString" y "valueOf" (del objeto "Object") que por defecto devuelven "[object Object]" y "{}", que no son de utilidad práctica, por lo que se sobrescriben para que devuelvan una cadena (toString) y un objeto literal (valueOf) con los datos de la persona.
Al programar con el paradigma Orientado a Objetos, se hace uso frecuente de las clases disponibles en el lenguaje. Así, para trabajar con fechas (y tiempos) se emplea la clase Date. Si no se le manda ningún dato crea un objeto con la fecha actual, si se le manda un texto con el formato "aaaa/mm/dd" crea un objeto con la fecha actual. Los años, los meses (con base 0) y los días, se obtienen con los métodos "getFullYear", "getMonth" y "getDate" y el método "toLocaleDateString" devuelve la fecha como un texto con el formato "dd/mm/aaaa".
Por otra parte, para que sea válida la fecha de nacimiento en formato local ("dd/mm/aaa") debe ser transformada a formato internacional ("aaaa/mm/dd"). Con ese fin se emplean los métodos "split", "reverse" y "join" de la clase String. "Split" divide la cadena en los lugares que coinciden con su cadena ("/") y devuelve un vector con las cadenas resultantes ([aaaa,mm,dd]), "reverse" invierte los elementos del vector ([dd,mm,aaaa]) y "join" vuelve a unir las cadenas del vector, intercalándolas con su cadena ("/"). Estos y otros métodos serán estudiados con más detalle en capítulos posteriores.
También, para validar que el número sea entero se emplea el método estático Number.prototype, que devuelve verdadero si el número que recibe es entero (positivo o negativo). Los métodos estáticos se caracterizan porque sólo pertenecen a la clase, no a los objetos de la clase (como ocurre con los métodos dinámicos). Por esa razón son llamados desde la clase (en este caso la clase Number) y no desde el objeto (en este caso, no desde un número). Los métodos estáticos se comportan más como funciones estructuradas que como métodos: No tienen acceso a los atributos, reciben sus datos en forma de parámetros y devuelven los resultados igual que una función estructurada (todos los métodos de la clase Math son métodos estáticos).
Ahora, con la clase elaborada, se pueden crear objetos de tipo "Persona". Por ejemplo, con la siguiente instrucción se crea el objeto "persona1":
Llamando desde este objeto a los métodos getter disponibles, se obtiene:
Con el método "valueOf" se obtiene:
El método "toString" es llamado automáticamente por JavaScript, cuando requiere una cadena que represente al objeto. Por ejemplo cuando se quiere mostrar el objet con la función "print":
Los métodos setter disponibles, permiten asignar nuevos valore a los atributos del objeto. Así, las siguientes instrucciones cambian los valores del objeto:
Como se dijo, uno de los fundamentos de la POO es la herencia. En JavaScript, para que una clase herede de otra se escribe la palabra reservada extends, de acuerdo a la siguiente sintaxis:
Si la clase padre (la clase base) tiene un constructor y la clase hija (la clase extendida) tiene atributos adicionales, se debe declarar un constructor en la clase hija y en ese constructor se debe llamar al constructor de la clase padre empleando la palabra reservada super: Ese es el nombre que JavaScript asigna automáticamente a la clase padre (por esa razón, super también permite llamar a otros métodos de la clase padre con la sintaxis: super.método()).
Por ejemplo, en la siguiente instrucción se crea la clase Estudiante, que hereda de la clase Persona (extiende a la clase Persona) añadiendo dos nuevos atributos: "carrera" y "cUniversitario":
Con esta clase se pueden crear objetos del tipo Estudiante, como el objeto "estudiante1":
Ahora el método "toString" devuelve:
Y el método "valueOf":
Y por supuesto, siguen funcionando correctamente los métodos de la clase padre:
Y como se han escrito los métodos setter respectivos, se puede cambiar también el valor de los nuevos atributos: