Los modificadores de acceso nos permiten cambiar la “visibilidad” y los “privilegios de acceso” de un miembro de una clase (o módulo). Estos se entienden mejor con un ejemplo.
JS++ tiene tres modificadores de acceso: privado, protegido y público.
Un miembro privado es el menos permisivo. Si un miembro se declara como ‘privado’, solo se puede acceder desde la clase o el módulo en el que se declaró. Aquí hay un ejemplo:
class Animal { private string name; string getName() { return name; // OK } } class Dog : Animal { string getName() { return name; // Error } } Animal animal = new Animal(); animal.name; // ERROR animal.getName(); // OK
Se puede acceder a un miembro protegido desde cualquier subclase o submódulo. Aquí hay un ejemplo:
class Animal { protected string name; string getName() { return name; // OK } } class Dog : Animal { string getName() { return name; // OK } } Animal animal = new Animal(); animal.name; // ERROR animal.getName(); // OK
Finalmente, está el modificador de acceso ‘público’. El modificador de acceso ‘público’ es el menos permisivo. Un miembro declarado como ‘público’ no tiene restricciones de acceso e incluso se puede acceder desde fuera de la clase (siempre que se acceda desde una instancia de clase). Aquí hay un ejemplo:
class Animal { public string name; string getName() { return name; // OK } } class Dog : Animal { string getName() { return name; // OK } } Animal animal = new Animal(); animal.name; // OK animal.getName(); // OK
Los modificadores de acceso permiten la encapsulación . La encapsulación es uno de los pilares de la programación orientada a objetos (como discutimos al comienzo de este capítulo) y se refiere a la agrupación de datos (campos) y los métodos que operan en esos datos (por ejemplo, métodos, getters/setters, etc.) . En términos más simples: oculte sus datos haciendo que los campos sean privados y solo habilite el acceso a ellos a través de métodos públicos/protegidos, getters o setters.
Las reglas de acceso predeterminadas de JS++ permiten la encapsulación. En JS++, los campos tienen un modificador de acceso predeterminado de ‘privado’. Todos los demás miembros de la clase tienen un modificador de acceso predeterminado de ‘público’. En otras palabras, las reglas de acceso de JS++ son «sensibles a los miembros», mientras que normalmente necesitaba especificar manualmente el modificador de acceso en lenguajes como Java y C# para lograr la encapsulación, lo que puede resultar en un código detallado.
¿Por qué necesitamos encapsulación? Piense en nuestros ejemplos de getter y setter donde tuvimos que definir métodos de getter y setter para leer y modificar el campo ‘nombre’ de nuestro gato. Hipotéticamente, supongamos que nuestros requisitos cambian y queremos poner el prefijo «Kitty» en todos los nombres de nuestros gatos. Con la encapsulación, solo necesitaríamos cambiar nuestro método de establecimiento. Si, en cambio, hiciéramos nuestro campo ‘público’ y el nombre tuviera que manipularse directamente a través de sus instancias, tendríamos que agregar manualmente el prefijo a cada manipulación directa del campo ‘nombre’ por parte de una instancia. A medida que los proyectos crecen en complejidad, esto no sería deseable.
Ahora que tenemos una comprensión firme de los modificadores de acceso y la encapsulación, regresemos a nuestro proyecto. Necesitamos que nuestra clase ‘Gato’ se renderice() de manera diferente a lo que proporciona la clase base ‘Animal’. El primer paso es editar nuestra clase base ‘Animal’ para hacer que el campo $elemento esté ‘protegido’ para que nuestras clases derivadas (como ‘Gato’) puedan acceder al campo:
external $; module Animals { class Animal { protected var $element = $( """ <div class="animal"> <i class="icofont icofont-animal-cat"></i> </div> """ ); void render() { $("#content").append($element); } } }
A continuación, restablezcamos el método render() a ‘Cat’:
external $; module Animals { class Cat : Animal { string _name; Cat(string name) { _name = name; } void render() { $element.attr("title", _name); $("#content").append($element); } } }
Si intenta compilar ahora mismo, obtendrá un error de compilación. El error en sí debería ser bastante descriptivo:
JSPPE0252: `void Animals.Cat.render()’ entra en conflicto con `void Animals.Animal.render()’. Cree un método con un nombre diferente o use el modificador ‘sobrescribir’
En este caso, nuestra clase derivada (‘Gato’) trató de definir un método llamado ‘renderizar’, pero la clase base (‘Animal’) ya tiene un método llamado ‘renderizar’. Por lo tanto, tenemos un conflicto. JS ++ también sugiere soluciones para nosotros: A) cree un método con un nombre diferente, o B) use el modificador ‘sobrescribir’.
Conceptualmente, ambos métodos describen un concepto: la representación en una página web. Por lo tanto, es posible que no queramos que dos nombres diferentes describan el mismo concepto. En su lugar, queremos decirle al compilador JS++ que esto fue intencional mediante el uso del modificador ‘sobrescribir’:
external $; module Animals { class Cat : Animal { string _name; Cat(string name) { _name = name; } overwrite void render() { $element.attr("title", _name); $("#content").append($element); } } }
En otros lenguajes orientados a objetos, esto se conoce como «ocultación de métodos» o «sombreado de métodos». La razón por la que JS ++ hace esto es para evitar posibles errores y errores tipográficos (especialmente para clases más complejas). Si tuviéramos dos conceptos diferentes, como ‘Gato’ en la memoria mientras que ‘Animal’ se muestra en la página web, no deberíamos tener los mismos nombres de método en tal caso.
Compile su código ahora. Debería tener éxito. Abra la página web y ahora debería poder pasar el mouse sobre los gatos nuevamente para ver sus nombres.
En esta etapa, todavía tenemos duplicación de código . Aquí hay un vistazo al método render() ‘Animal’:
void render() { $("#content").append($element); }
Y aquí está nuestro método render() ‘Cat’:
overwrite void render() { $element.attr("title", _name); $("#content").append($element); }
¿Notas la duplicación? ¿Qué pasaría si quisiéramos representar un elemento HTML diferente además del que tiene el ID ‘contenido’ más adelante? ¡Tendríamos que cambiar el código de representación en todas las clases relevantes!
Nuestra clase ‘Gato’ «extiende» el concepto de ‘Animal’. Del mismo modo, el método render() de nuestro Gato «extiende» el método render() de Animal agregando el atributo ‘título’ de HTML para que podamos pasar el mouse por encima y ver el nombre. Sin embargo, además de eso, nuestra lógica de representación es la misma: agregue el elemento al elemento HTML con ID ‘contenido’. Podemos hacerlo mejor. Vamos a «reutilizar» el código de representación de nuestra clase ‘Animal’ en nuestra clase ‘Gato’:
external $; module Animals { class Cat : Animal { string _name; Cat(string name) { _name = name; } overwrite void render() { $element.attr("title", _name); super.render(); } } }
Compile, ejecute y observe los resultados. Ahora, sin importar cómo cambie nuestra lógica de representación, se aplicará a todas las clases relevantes. La clave es la palabra clave ‘super’. La palabra clave ‘super’ se refiere a la superclase de la clase actual. En este caso, lo usamos para acceder al método ‘render’ de la clase ‘Animal’. Sin ‘super’, estaríamos llamando al método ‘render’ de la clase actual, ¡lo que resultaría en una recursividad infinita! (Por ejemplo, usar ‘esto’ en lugar de ‘super’ le permitirá referirse al método ‘renderizar’ de la clase ‘Gato’… pero dará como resultado una recurrencia infinita).
Hasta ahora, hemos aprendido sobre campos y métodos privados, protegidos y públicos, pero ¿qué pasa con los constructores? Abra main.jspp y agregue el siguiente código:
import Animals; Cat cat1 = new Cat("Kitty"); cat1.render(); Cat cat2 = new Cat("Kat"); cat2.render(); Animal animal = new Animal(); animal.render();
Compilar y ejecutar.
¡UH oh! Tenemos tres gatos representados en la página. Al menos cuando pasas el cursor sobre el último gato, no muestra un nombre. Sin embargo, un ‘Animal’ no es un ‘Gato’ (pero un ‘Gato’ es un ‘Animal’). La razón por la que tenemos tres íconos de gatos es porque tenemos esto en nuestro Animal.jspp:
protected var $element = $( """ <div class="animal"> <i class="icofont icofont-animal-cat"></i> </div> """ );
En otras palabras, cuando se inicializa nuestro campo $element, siempre se inicializa con un valor que nos da un icono de gato. En su lugar, es posible que deseemos definir un constructor en ‘Animal’ para parametrizar esta inicialización. Cambiemos Animal.jspp para que este campo se inicialice en el constructor:
external $; module Animals { class Animal { protected var $element; protected Animal(string iconClassName) { string elementHTML = makeElementHTML(iconClassName); $element = $(elementHTML); } public void render() { $("#content").append($element); } private string makeElementHTML(string iconClassName) { string result = '<div class="animal">'; result += '<i class="icofont ' + iconClassName + '"></i>'; result += "</div>"; return result; } } }
Agregué modificadores de acceso en todos los miembros de la clase para aclarar el código. También separé la construcción del texto HTML en una función separada para mayor claridad. Adquiera el hábito de practicar el principio de responsabilidad única: todas las clases hacen una cosa, todas las funciones/métodos hacen una cosa. En el código anterior, nuestro constructor hace una cosa: inicializa los campos; nuestro método render() hace una cosa: renderizar a la página web; y, finalmente, nuestro método ‘makeElementHTML’ hace una cosa: generar el HTML para nuestro elemento. Esto lleva a un código limpio , y JS++ fue diseñado con un código limpio en mente, así que intente beneficiarse del diseño.
Otro buen truco que puede haber notado es usar '
(comillas simples) para envolver strings HTML como se muestra en el código anterior. Esto es para evitar escapar de las "
(comillas dobles) utilizadas para rodear los atributos HTML en nuestro método ‘makeElementHTML’.
Es posible que haya notado que todos los modificadores de acceso nuevos son diferentes: constructor protegido, representación pública() y makeElementHTML privado. Analicemos esto de lo más restrictivo (privado) a lo menos restrictivo (público).
La razón por la que ‘makeElementHTML’ es privado es porque es un detalle de implementación . El único uso de ‘makeElementHTML’ está dentro de nuestra clase ‘Animal’. La clase ‘Cat’ no puede acceder al método y main.jspp no puede acceder al método (a través de la creación de instancias). La clase ‘Cat’ nunca necesitará llamar a ‘makeElementHTML’; en cambio, la clase ‘Cat’ hereda de la clase ‘Animal’. A través de la herencia, la clase ‘Gato’ llamará al constructor ‘Animal’. (Llegaremos a esto en breve ya que el código actualmente no puede compilarse, pero estos conceptos son más importantes de entender primero). Como consecuencia, la clase ‘Cat’ llamará a ‘makeElementHTML’ a través deel constructor de la clase ‘Animal’, pero no tiene acceso al método y no puede llamarlo directamente. De esta forma, ‘makeElementHTML’ es un detalle de implementación de la clase ‘Animal’ y no está expuesto a ninguna otra parte de nuestro código. Esta ocultación de detalles que son irrelevantes para otras clases y código se conoce como «abstracción» en la programación orientada a objetos.
Como comentábamos al principio de este capítulo, la abstracción es el otro pilar fundamental de la programación orientada a objetos (POO). Por ejemplo, imagina un coche. Cuando pisas el acelerador de un coche, no necesitas saber cómo funcionan los detalles del motor de combustión interna. Las complejidades del funcionamiento interno se le presentan a través de una interfaz simplificada: el pedal del acelerador. A través de la abstracción, estamos simplificando los sistemas complejos, una propiedad deseable de OOP.
Después del método privado ‘makeElementHTML’, el siguiente código con privilegios de acceso es el constructor ‘protegido’. Una vez más, el modificador de acceso ‘protegido’ es menos restrictivo que ‘privado’ pero no tan permisivo como ‘público’ (que no tiene restricciones de acceso más allá del alcance).
Específicamente, ¿qué significa hacer que un constructor esté ‘protegido’? Recuerde que el modificador de acceso ‘protegido’ permite el acceso a todos los miembros dentro de la clase, pero también incluye todas las clases derivadas. Recuerde también que la instanciación de una clase ejecuta el código especificado en el constructor. Lógicamente, podemos concluir que un constructor protegido significará que no se puede instanciar una clase fuera de contextos específicos.
¿Cuáles son estos contextos específicos? El caso obvio es que no podemos instanciar ‘Animal’ desde main.jspp. Si lo intenta ahora mismo, obtendrá un error de compilación. Sin embargo, dado que solo se puede acceder a ‘protegido’ desde dentro de la clase misma y de todas las clases derivadas , la intención del constructor protegido en nuestro código es restringir la clase solo a la herencia. Recuerde que la clase ‘Cat’ no puede llamar al ‘makeElementHTML’ privado directamente; este método se ejecuta a través del constructor ‘Animal’ durante la herencia . Durante la herencia, el constructor se ejecuta como en la creación de instancias.
Si hiciera que el constructor fuera ‘privado’, esencialmente evitaría la creación de instancias y la herencia de la clase. (Nota al margen: así es como se implementa la clase ‘System.Math’ de la biblioteca estándar de JS++). Recuerde: la regla de acceso predeterminada para todo, excepto los campos, es ‘pública’. En otras palabras, si dejamos el modificador de acceso para nuestro constructor sin especificar, se habría predeterminado en ‘público’.
La palabra clave ‘super’ que usamos anteriormente para acceder a los métodos de la superclase se refiere a una instancia de la superclase creada durante la creación de instancias. Cuando instanciamos ‘Gato’, también instanciamos ‘Animal’. Todos los constructores relevantes se ejecutarán en la string, comenzando desde la parte inferior de la string. En nuestro caso, primero ejecutamos el constructor ‘Gato’ cuando se crea una instancia de ‘Gato’, y avanzamos en la string de herencia y ejecutamos el constructor de la clase ‘Animal’ a continuación. (JS++ usa un «sistema de tipo unificado» donde ‘System.Object’ es la raíz de todos los tipos internos, por lo que también se llamará al constructor de esta clase, pero solo si se determina que es necesario y no es candidato para la «eliminación de código muerto». ” — pero esto está fuera del alcance de este capítulo y será discutido en los capítulos de la Biblioteca estándar.)
Sabiendo que se llama a los constructores durante la herencia, ahora podemos abordar el problema restante en nuestro código: notará que el código actualmente no se compila. El motivo es que dejamos de usar el constructor predeterminado implícito para la clase ‘Animal’ cuando definimos un constructor personalizado.
Nuestro constructor ‘Animal’ toma un parámetro:
protected Animal(string iconClassName) { string elementHTML = makeElementHTML(iconClassName); $element = $(elementHTML); }
Necesitamos cambiar el código del constructor ‘Cat’ para especificar cómo se debe llamar al constructor de la superclase. Podemos hacer esto una vez más a través de la palabra clave ‘super’. La clase ‘Animal’ quiere saber el nombre del ícono del tipo de animal que queremos representar. Si no recuerda el nombre del ícono, lo incluí aquí en la llamada ‘súper’ para su conveniencia:
external $; module Animals { class Cat : Animal { string _name; Cat(string name) { super("icofont-animal-cat"); _name = name; } overwrite void render() { $element.attr("title", _name); super.render(); } } }
Una llamada de función en la palabra clave ‘super’ ejecutará el constructor relevante de la superclase. La llamada ‘super’ siempre debe ser la primera declaración porque, semánticamente, el constructor de la superclase se ejecutará antes que el código constructor de sus clases derivadas.
Finalmente, necesitamos modificar main.jspp para eliminar la creación de instancias de la clase ‘Animal’. Recuerde que dado que hicimos que el constructor ‘Animal’ estuviera ‘protegido’, no podremos instanciar ‘Animal’ desde main.jspp de todos modos:
import Animals; Cat cat1 = new Cat("Kitty"); cat1.render(); Cat cat2 = new Cat("Kat"); cat2.render();
En este punto, puede compilar y el proyecto debería compilarse correctamente. Una vez más, deberíamos tener los dos gatos:
Por último, podemos añadir más animales.
Perro.jspp:
external $; module Animals { class Dog : Animal { string _name; Dog(string name) { super("icofont-animal-dog"); _name = name; } overwrite void render() { $element.attr("title", _name); super.render(); } } }
Dog.jspp es muy parecido a Cat.jspp porque un perro también es un animal domesticado que necesita un nombre.
Panda.jspp:
external $; module Animals { class Panda : Animal { Panda() { super("icofont-animal-panda"); } } }
A diferencia de Cat.jspp y Dog.jspp, Panda.jspp es significativamente más simple. Todo lo que hace la clase ‘Panda’ es heredar de ‘Animal’ y especificar el ícono para renderizar. No tiene nombre, y su método render() es exactamente el mismo que el de Animal, ya que no tiene que agregar un atributo de ‘título’ HTML al pasar el mouse para mostrar un nombre.
Rhino.jspp:
external $; module Animals { class Rhino : Animal { Rhino() { super("icofont-animal-rhino"); } } }
Al igual que Panda.jspp, Rhino.jspp también es una clase muy simple. Simplemente hereda de ‘Animal’ y no tiene necesidad de establecer o representar un nombre.
Finalmente, modifique main.jspp para instanciar los nuevos animales:
import Animals; Cat cat1 = new Cat("Kitty"); cat1.render(); Cat cat2 = new Cat("Kat"); cat2.render(); Dog dog = new Dog("Fido"); dog.render(); Panda panda = new Panda(); panda.render(); Rhino rhino = new Rhino(); rhino.render();
Compile todo el proyecto así:
$ js++ src/ -o build/app.jspp.js
Una vez más, en todas las plataformas (Windows, Mac y Linux), estamos operando desde la línea de comandos, por lo que las instrucciones de compilación deberían ser las mismas para todos. Además, es completamente innecesario especificar el «orden de compilación». No importa que ‘Gato’ dependa de ‘Animal’, por lo tanto, ‘Animal.jspp’ debe procesarse antes que ‘Cat.jspp’. JS++ resuelve automáticamente el orden de compilación incluso para los proyectos más complejos (por ejemplo, con importaciones cíclicas y dependencias complejas). Simplemente especifique los directorios de entrada y deje que JS ++ encuentre recursivamente los archivos de entrada y descubra el orden de compilación.
Abra index.html en su navegador web. El resultado debería verse así:
Verifique que sus dos gatos tengan nombres, su perro tenga un nombre, pero el panda y el rinoceronte no deberían tener nombres cuando pase el mouse sobre ellos.
Si todo funciona: ¡enhorabuena! En este punto, es posible que haya notado que podemos cambiar nuestra jerarquía de herencia de la siguiente manera:
Animal |_ DomesticatedAnimal |_ Cat |_ Dog |_ WildAnimal |_ Panda |_ Rhino
Sin embargo, esto se deja como ejercicio para el lector.
Ahora hemos cubierto tres de los cuatro conceptos fundamentales de OOP que discutimos en la introducción del capítulo: abstracción, encapsulación y herencia. El último pilar fundamental de la programación orientada a objetos es el polimorfismo, que trataremos en la siguiente sección.