Uno de los principios más importantes de la programación orientada a objetos: delimitar la interfaz interna de la externa.
Esta es una práctica "imprescindible" al desarrollar algo más complejo que una aplicación de "hola mundo".
Para entender esto, rompamos con el desarrollo y volvamos nuestros ojos al mundo real.
Normalmente, los dispositivos que utilizamos son bastante complejos. Pero delimitar la interfaz interna de la externa permite utilizarlos sin problemas.
Por ejemplo, una máquina de café. Sencillo desde fuera: un botón, una pantalla, unos cuantos agujeros… Y, seguro, el resultado: ¡un café estupendo! :)
Pero por dentro… (una imagen del manual de reparación)
Muchos detalles. Pero podemos usarlo sin saber nada.
Las cafeteras son bastante fiables, ¿no? Podemos utilizar uno durante años y, sólo si algo sale mal, llevarlo a reparar.
El secreto de la fiabilidad y la sencillez de una máquina de café: todos los detalles están bien ajustados y escondidos en su interior.
Si quitamos la funda protectora de la cafetera, entonces su uso será mucho más complejo (¿dónde presionar?), y peligroso (puede electrocutarse).
Como veremos, en programación los objetos son como las máquinas de café.
Pero para ocultar detalles internos, no usaremos una cubierta protectora, sino una sintaxis especial del lenguaje y las convenciones.
En la programación orientada a objetos, las propiedades y los métodos se dividen en dos grupos:
Interfaz interna : métodos y propiedades, accesibles desde otros métodos de la clase, pero no desde el exterior.
Interfaz externa : métodos y propiedades, accesibles también desde fuera de la clase.
Si seguimos con la analogía con la máquina de café, lo que se esconde en su interior: el tubo de la caldera, el elemento calefactor, etc., es su interfaz interna.
Se utiliza una interfaz interna para que el objeto funcione, sus detalles se utilizan entre sí. Por ejemplo, al elemento calefactor se le fija un tubo de caldera.
Pero una máquina de café está cerrada desde fuera con una cubierta protectora para que nadie pueda alcanzarla. Los detalles están ocultos e inaccesibles. Podemos utilizar sus funciones a través de la interfaz externa.
Entonces, todo lo que necesitamos para usar un objeto es conocer su interfaz externa. Es posible que desconozcamos por completo cómo funciona por dentro, y eso es genial.
Esa fue una introducción general.
En JavaScript, existen dos tipos de campos de objetos (propiedades y métodos):
Público: accesible desde cualquier lugar. Constituyen la interfaz externa. Hasta ahora solo usábamos propiedades y métodos públicos.
Privado: accesible sólo desde el interior de la clase. Estos son para la interfaz interna.
En muchos otros idiomas también existen campos "protegidos": accesibles sólo desde dentro de la clase y aquellos que la extienden (como los privados, pero con acceso desde clases heredadas). También son útiles para la interfaz interna. En cierto sentido, están más extendidos que los privados, porque normalmente queremos que las clases heredadas tengan acceso a ellos.
Los campos protegidos no están implementados en JavaScript a nivel de lenguaje, pero en la práctica son muy convenientes, por lo que se emula.
Ahora haremos una máquina de café en JavaScript con todos estos tipos de propiedades. Una máquina de café tiene muchos detalles, no los modelaremos para que sean simples (aunque podríamos hacerlo).
Primero hagamos una clase simple sobre máquinas de café:
clase Cafetera { cantidad de agua = 0; // la cantidad de agua dentro constructor(poder) { this.power = poder; alert(`Creé una máquina de café, potencia: ${power}`); } } // crear la maquina de cafe let CoffeeMachine = new CoffeeMachine(100); // agregar agua cafeMachine.waterAmount = 200;
En este momento las propiedades waterAmount
y power
son públicas. Podemos obtenerlos/configurarlos fácilmente desde el exterior a cualquier valor.
Cambiemos la propiedad waterAmount
a protected para tener más control sobre ella. Por ejemplo, no queremos que nadie lo ponga por debajo de cero.
Las propiedades protegidas suelen tener como prefijo un guión bajo _
.
Esto no se aplica a nivel del lenguaje, pero existe una convención bien conocida entre los programadores de que no se debe acceder a dichas propiedades y métodos desde el exterior.
Entonces nuestra propiedad se llamará _waterAmount
:
clase Cafetera { _cantidaddeagua = 0; establecer cantidad de agua (valor) { si (valor < 0) { valor = 0; } this._waterAmount = valor; } obtener cantidad de agua() { devolver this._waterAmount; } constructor(poder) { this._power = poder; } } // crear la maquina de cafe let CoffeeMachine = new CoffeeMachine(100); // agregar agua cafeMachine.waterAmount = -10; // _waterAmount se convertirá en 0, no en -10
Ahora el acceso está bajo control, por lo que resulta imposible ajustar la cantidad de agua por debajo de cero.
Para la propiedad power
, hagámosla de solo lectura. A veces sucede que una propiedad debe establecerse sólo en el momento de su creación y luego nunca modificarse.
Ese es exactamente el caso de una máquina de café: la potencia nunca cambia.
Para hacerlo, sólo necesitamos hacer el getter, pero no el setter:
clase Cafetera { //... constructor(poder) { this._power = poder; } obtener poder() { devolver esto._power; } } // crear la maquina de cafe let CoffeeMachine = new CoffeeMachine(100); alert(`La potencia es: ${coffeeMachine.power}W`); // La potencia es: 100W maquinadecafe.potencia = 25; // Error (sin configurador)
Funciones getter/setting
Aquí utilizamos la sintaxis getter/setter.
Pero la mayoría de las veces se prefieren las funciones get.../set...
, como esta:
clase Cafetera { _cantidaddeagua = 0; establecerMontoAgua(valor) { si (valor < 0) valor = 0; this._waterAmount = valor; } obtenerMontoAgua() { devolver this._waterAmount; } } nueva Cafetera().setWaterAmount(100);
Parece un poco más largo, pero las funciones son más flexibles. Pueden aceptar múltiples argumentos (incluso si no los necesitamos en este momento).
Por otro lado, la sintaxis get/set es más corta, por lo que, en última instancia, no existe una regla estricta, tú decides.
Los campos protegidos se heredan
Si heredamos class MegaMachine extends CoffeeMachine
, entonces nada nos impide acceder a this._waterAmount
o this._power
desde los métodos de la nueva clase.
Por tanto, los campos protegidos son naturalmente heredables. A diferencia de los privados que veremos a continuación.
Una adición reciente
Esta es una adición reciente al idioma. No es compatible con motores de JavaScript, o es compatible parcialmente todavía, requiere polillenado.
Hay una propuesta de JavaScript terminada, casi en el estándar, que proporciona soporte a nivel de lenguaje para propiedades y métodos privados.
Los privados deben comenzar con #
. Sólo son accesibles desde el interior de la clase.
Por ejemplo, aquí hay una propiedad privada #waterLimit
y el método privado de verificación de agua #fixWaterAmount
:
clase Cafetera { #límitedeagua = 200; #fixWaterAmount(valor) { si (valor <0) devuelve 0; si (valor > this.#waterLimit) devuelve esto.#waterLimit; } establecerMontoAgua(valor) { this.#waterLimit = this.#fixWaterAmount(valor); } } let CoffeeMachine = new CoffeeMachine(); // no puedo acceder a privados desde fuera de la clase máquina de café.#fixWaterAmount(123); // Error maquinadecafe.#waterLimit = 1000; // Error
A nivel de idioma, #
es una señal especial de que el campo es privado. No podemos acceder a él desde fuera o desde clases heredadas.
Los campos privados no entran en conflicto con los públicos. Podemos tener campos privados #waterAmount
y públicos waterAmount
al mismo tiempo.
Por ejemplo, hagamos que waterAmount
sea un descriptor de acceso para #waterAmount
:
clase Cafetera { #cantidaddeagua = 0; obtener cantidad de agua() { devuelve esto.#waterAmount; } establecer cantidad de agua (valor) { si (valor < 0) valor = 0; this.#waterAmount = valor; } } let machine = new CoffeeMachine(); máquina.waterAmount = 100; alerta(máquina.#cantidaddeagua); // Error
A diferencia de los protegidos, los campos privados los aplica el propio lenguaje. Eso es algo bueno.
Pero si heredamos de CoffeeMachine
, entonces no tendremos acceso directo a #waterAmount
. Tendremos que confiar en el captador/establecedor waterAmount
:
clase MegaCoffeeMachine extiende CoffeeMachine { método() { alerta (esto.#cantidaddeagua); // Error: sólo se puede acceder desde CoffeeMachine } }
En muchos escenarios, dicha limitación es demasiado severa. Si ampliamos una CoffeeMachine
, es posible que tengamos motivos legítimos para acceder a sus componentes internos. Es por eso que los campos protegidos se usan con más frecuencia, aunque no sean compatibles con la sintaxis del idioma.
Los campos privados no están disponibles como este [nombre]
Los campos privados son especiales.
Como sabemos, normalmente podemos acceder a los campos usando this[name]
:
usuario de clase { ... decir Hola() { let campoNombre = "nombre"; alert(`Hola, ${this[fieldName]}`); } }
Con campos privados eso es imposible: this['#name']
no funciona. Esa es una limitación de sintaxis para garantizar la privacidad.
En términos de programación orientada a objetos, la delimitación de la interfaz interna de la externa se denomina encapsulación.
Da los siguientes beneficios:
Protección para los usuarios, para que no se disparen en el pie.
Imagínese, hay un equipo de desarrolladores usando una máquina de café. Fue fabricado por la empresa "Best CoffeeMachine" y funciona bien, pero se quitó la cubierta protectora. Entonces la interfaz interna queda expuesta.
Todos los desarrolladores son civilizados: utilizan la máquina de café según lo previsto. Pero uno de ellos, John, decidió que él era el más inteligente e hizo algunos ajustes en el interior de la máquina de café. Entonces la máquina de café falló dos días después.
Seguramente no es culpa de John, sino de la persona que quitó la cubierta protectora y dejó que John hiciera sus manipulaciones.
Lo mismo en programación. Si un usuario de una clase cambia cosas que no están destinadas a cambiar desde el exterior, las consecuencias son impredecibles.
Soportable
La situación en la programación es más compleja que con una máquina de café real, porque no la compramos una sola vez. El código se desarrolla y mejora constantemente.
Si delimitamos estrictamente la interfaz interna, entonces el desarrollador de la clase puede cambiar libremente sus propiedades y métodos internos, incluso sin informar a los usuarios.
Si es un desarrollador de dicha clase, es fantástico saber que se puede cambiar el nombre de los métodos privados de forma segura, se pueden cambiar sus parámetros e incluso eliminarlos, porque ningún código externo depende de ellos.
Para los usuarios, cuando sale una nueva versión, puede ser una revisión interna total, pero aún así es fácil de actualizar si la interfaz externa es la misma.
Ocultar la complejidad
A la gente le encanta usar cosas que son simples. Al menos desde fuera. Lo que hay dentro es otra cosa.
Los programadores no son una excepción.
Siempre es conveniente cuando los detalles de implementación están ocultos y hay disponible una interfaz externa simple y bien documentada.
Para ocultar una interfaz interna utilizamos propiedades protegidas o privadas:
Los campos protegidos comienzan con _
. Esa es una convención bien conocida, que no se aplica a nivel del idioma. Los programadores solo deben acceder a un campo que comience con _
de su clase y a las clases que hereden de él.
Los campos privados comienzan con #
. JavaScript se asegura de que solo podamos acceder a aquellos desde dentro de la clase.
En este momento, los campos privados no son bien compatibles entre los navegadores, pero se pueden rellenar en múltiples ocasiones.