El concepto
El mundo de la programación de computadoras, posee un vocabulario bastante extenso y en constante crecimiento. Tal es la necesidad de términos que constantemente estamos reusando palabras cotidianas (objeto, contenedores) o de otros campos del conocimiento (arquitectura, proceso), dándole nuevos significados a palabras ya usadas (clase, abstracción) o creando nuevas palabras con poca o mucha imaginación.
Uno de los términos que más se oyen cuando hablamos de Programación Orientada a Objetos (POO) es «encapsulamiento» y como muchos otros conceptos asociados a POO, resulta algo confuso.
El encapsulamiento puede referirse a dos características dentro de la programación:
- La Capacidad de unir (encapsular) datos y funciones.
- La acción de proteger partes sensibles de una estructura de datos.
Ambos significados tienen relación con la POO, pero, en el sentido más amplio, el encapsulamiento como mecanismo de protección, podría aplicarse a cualquier estructura de datos, no solo a objetos.
Encapsulando datos y funciones
Este sería el primer significado (algo olvidado en estos días), de la palabra «encapsulamiento» en el mundo de la POO.
En términos generales, «encapsular» significa unir o juntar algo dentro de una capsula. Dentro de la terminología de POO, el encapsulamiento se refiere a la característica de juntar en una sola estructura, a:
- Variables o datos, que en la terminología de POO se les llama atributos, propiedades, campos o miembros.
- Funciones o procedimientos, que en la terminología POO se les llama métodos o funciones miembros.
A la estructura, que encapsula a estas partes, se le llama clase, plantilla o definición de un objeto, dependiendo del lenguaje de programación a usar.
En este contexto, el encapsulamiento es la característica principal de la POO (tanto así que, un mejor nombre podría para la POO podría ser «Programación Encapsulada»), aunque no está claro hasta donde puede llegar este enlace o «cápsula», dentro de las capacidades de un lenguaje de programación que es la herramienta que usamos para definir objetos.
Consideremos la definición de una clase dentro de Python:
class MiClase:
"""A simple example class"""
edad = 23
nombre = 'juan'
def saludar(self):
return 'Hola mundo'
Esta definición nos permite llamar con un solo nombre («MiClase») a:
- Dos atributos («nombre» y «edad»), y,
- Un método («saludar»).
Y no es solo que podamos nombrar a una clase, sino que este nombre nos permite hacer diversas operaciones sobre esta clase tratándola como una sola entidad.
Tal vez alguien podría sostener que basta con poner a las variables y funciones en un módulo separado (como un archivo de cabecera de C, un módulo de VB o una unidad de Pascal) y con nombres relacionados, para decir que están encapsulados, y eso podría ser debatible, pero el concepto de encapsulamiento se asocia más a la idea de una clase. Para tener ver un ejemplo descriptivo de un caso con y sin objetos, puede ver este artículo.
La sintaxis de Python, como la de muchos lenguajes que soportan POO, tiene soporte para la definición de clases (con atributos y métodos) y esto puede parecernos algo trivial, pero, en su momento, representaron un gran avance dentro de los paradigmas de programación.
Si desea saber lo que es no poder encapsular los elementos de un objeto, solo basta con usar un lenguaje sin soporte a POO, como podría ser el lenguaje C o Pascal.
Ocultando para proteger
El otro significado de encapsulamiento, que es más conocido, se refiere a la capacidad de proteger o restringir el acceso a ciertas partes internas de una estructura de datos como puede ser un objeto.
¿Para qué proteger partes de una estructura de datos?
La respuesta es simple si usamos una analogía con el mundo concreto. Imaginemos que construimos un reloj mecánico, como los de antaño y que queremos venderlo para su uso masivo. A nadie sensato se le ocurriría venderlo sin protegerlo o cubrirlo con algún tipo de carcaza o tapa.
La maquinaria de un reloj es bastante complicada y nadie quiere que por un grano de arena o una gota de agua en alguna parte sensible del reloj, todo el mecanismo se detenga.
De manera análoga, los objetos en POO son estructuras complejas con numerosas variables de estado internas que se mueven a un ritmo perfecto y armonioso, dictado por el diseño del objeto, y que ningún otro objeto externo debería modificar si existe el riesgo de provocar un error.
Imaginemos el caso de una clase, en PHP, que usa un contador interno para temporizar algún tipo de proceso:
class ClaseContador
{
// Atributos
public $contador = 0;
// Métodos
public function contar_algo() {
// Hace algo delicado con $contador.
}
}
En esta clase, el atributo $contador es usado por la clase para temporizar algún evento y no debería ser modificado, para no interferir con la cuenta.
¿Cómo proteger?
El primer mecanismo que nos permite proteger lo que no queremos que se toque desde afuera, es restringiendo el acceso. Para ello, varios lenguajes incluyen, dentro de la sintaxis de creación de clases, los llamados «modificadores de acceso» o «especificaciones de visibilidad».
Aunque cada lenguaje puede tener diversas tipos de acceso para los miembros de un objeto, casi todos ellos soportan al menos los siguientes accesos de tipo PRIVATE, PROTECTED y PUBLIC. La siguiente tabla muestra el alcance de cada modificador de acceso:
Por ejemplo, si quisiéramos limitar el acceso, desde fuera, al atributo $contador de nuestra clase ejemplo «ClaseContador», tendríamos que definir a $contador con alcance privado:
class ClaseContador
{
// Atributos
private $contador = 0;
// Métodos
public function contar_algo() {
// Hace algo delicado con $contador.
}
}
Otro de los mecanismos para restringir el acceso a las partes de un objeto es mediante el uso de los llamados «getters» y «setters», los mismos que serán explicados más adelante.
¿Qué proteger?
La recomendación dice que se debe proteger las partes sensibles de un objeto. Pero la interpretación de «sensible» no es muy clara. En todo caso, hay mucho espacio para el criterio del programador o ingeniero que construye la clase.
Una recomendación general sería proteger todo lo posible. Eso equivale a hacer todos los atributos de tipo PRIVATE (y no crearle «getters» o «setters») y solo ir aumentándoles la visibilidad, conforme vayamos encontrando la necesidad.
Para entender mejor la necesidad de ocultar la mayor cantidad de partes, consideremos nuevamente el ejemplo del reloj. Un buen diseño debería cubrir toda la maquinaria interna y solo dejar acceso para «ver» (solo lectura) a las manecillas y también para darle cuerda (lectura y escritura) [1. En forma simplificada podríamos decir que el acceso es de solo escritura por cuanto la perilla de la cuerda no aporta información sobre la posición o el estado de la cuerda, pero si somos detallistas podríamos decir que sí nos indica el estado de la cuerda, por la dureza de giro, y también si se llegó al máximo de cuerda, por el bloqueo de la perilla]. Todo lo demás debería permanecer cubierto.
Dicho de otra forma, solo hacer accesible a lo que es estrictamente necesario. Aunque eso de «estíctamente» puede no ser todavía muy claro. Solo la experiencia nos dará el criterio necesario para saber qué ocultar y que no.
En general se hace accesible solo a los atributos o métodos que se requieren para hacer funcional a la clase, sin peligro de que puedan dar lugar a un funcionamiento erróneo.
Inclusive los atributos de una clase no se suelen acceder directamente (lo que sería un acceso de lectura/escritura) sino que su acceso se habilita mediante el uso de «Getters» y «Setters».
Los «Getters» y «Setters»
Los «Getters» y «Setters», a los que también se les llama Accesadores y Mutadores, son la forma más efectiva de restringir el acceso a los atributos sensibles de un objeto. Se presentan como una interfaz que evita el acceso directo al objeto.
Los «Getters» y «Setters» no necesitan un soporte especial del lenguaje o del compilador (Aunque algunos lenguajes como Visual Basic u Object Pascal añaden una sintaxis especial). Es posible crear «getter» y «setters» de forma común en cualquier lenguaje con soporte POO.
Por ejemplo, el siguiente código muestra la definición de «Getters» y «Setters» en una clase de Java:
public class Empleado {
private int edad;
private String nombre;
public String getNombre() {
return this.nombre;
}
public void setNombre(String nombre) {
this.nombre = nombre;
}
public int getEdad() {
return this.edad;
}
}
Notar que todos los atributos de esta clase se mantienen como privados y solo se permite el acceso mediante el uso de funciones (para lectura) o procedimientos (para escritura). Esta es la forma universal de restricción de acceso a los atributos de una clase, y se basa en el hecho de que una función/procedimiento es más fácil de controlar que simplemente hacer accesible el atributo.
De aquí nace una de las reglas del encapsulamiento: Mantener todos los atributos en privado y solo habilitar el acceso mediante el uso de los «Getters/Setters».
Esta simple clase de ejemplo nos muestra una de las posibilidades útiles de los «Getters/Setters» que es, el poder crear atributos de solo lectura, que en nuestra clase sería «edad». Esto se logra omitiendo la creación de un «Setter».
Puede parecer que la creación de Getters y Setters es una carga adicional de trabajo, que no vale la pena realizar, además de hacer el acceso a los atributos mas lento por la necesidad de crearle una capa de protección extra [2. En algunos lenguajes, como VB, el compilador creará siempre Getters y Setters, así el programa no los indique explícitamente, por lo tanto, en esos lenguajes no se gana velocidad por no usarlos.]. Y aunque en muchos casos estas desventajas puedan ser significativas, en mi experiencia, el uso de Getters y Setters ofrecen las siguientes ventajas:
- Permiten realizar validaciones antes de asignar valores a una propiedad. De este modo, se evitan los valores incongruentes o asignaciones innecesarias cuando nuestro atributo ya tiene el valor que pretendemos asignar.
- Permite realizar tareas adicionales, que sean necesarias, después de asignar valores a una propiedad, como por ejemplo, refrescar otras variables, o generar eventos de tipo «Atributo cambiado».
- Permiten crear propiedades que sean solo de lectura, solo de escritura o de lectura/escritura. Esta es una gran cualidad que no se puede lograr con los modificadores de acceso.
- Permiten interceptar, en depuración, todas las acciones que modifican el contenido de un atributo. Esta característica es muy útil cuando tenemos múltiples accesos, y desde distintas partes de un proyecto, a un atributo particular de la clase, que pueda estar generando errores.
- Permiten crear propiedades ficticias, que no existen como tal sino que se pueden crear a partir de una combinación de otras propiedades o simples escalamientos de valor.
- Permiten cambiar la implementación interna de una propiedad (como un cambio de nombre o forma de trabajo), sin modificar el comportamiento al exterior. De esta forma se facilita la refactorización o eliminación de los atributos internos, sin cambiar la interfaz de acceso externo.
Los «getters» y «setters» son realmente útiles por las razones ya expuestas, pero hay un detalle que podría parecer problemático, y que tiene que ver con su sintaxis.
Consideremos la clase «Empleado» del ejemplo anterior, y tratemos de asignar al atributo nombre, su valor concatenado con el nombre de otra clase. El código sería algo similar a este:
emp1.setNombre(emp1.getNombre() + emp2.getNombre());
Este código puede parecer algo ofuscado, si no nos llevamos bien con los paréntesis. Y claro que se puede hacer más legible con el uso de variables temporales, pero agregando más instrucciones para algo que parece no merecerlo.
Por este motivo, algunos lenguajes de programación más poderosos, son capaces de simplificar esta sintaxis a algo más natural como:
emp1.nombre = emp1.nombre + emp2.nombre;
Para lograr esta limpieza de código, manteniendo el encapsulamiento intacto, lenguajes como Object Pascal, Python o C++ (pero no Java), incluyen lo que se llama la creación de «propiedades» que van un paso más allá de los simples «getters» y «setters».
El siguiente código en Object Pascal nos muestra como se crea una propiedad llamada nombre en una calase llamada TEmpleado:
TEmpleado = class
private
Fnombre: String;
procedure Setnombre(AValue: String);
published
property nombre: String read Fnombre write Setnombre;
end;
procedure TEmpleado.Setnombre(AValue: String);
begin
if Fnombre = AValue then Exit;
Fnombre := AValue;
end;
Aquí se ha definido un «setter» (Setnombre) para la escritura pero para la lectura se accede directamente al atributo privado «Fnombre».
Con esta definición podemos acceder a «TEmpleado.nombre» como si fuera un atributo expuesto de la clase, pero por debajo de la mesa, se encuentra bien encapsulado y de forma transparente para quien vea a la clase desde el exterior.
Llegados a este punto, podemos preguntarnos ¿Vale la pena usar «Getters» y «Setters»?
Como en todo campo del conocimiento, existen seguidores incondicionales de los «Getters» y «Setters» y que no conciben un mundo sin ellos, y tampoco considerarían tal cosa. Pero, toda técnica tiene sus detractores y existen opiniones en contra del uso de esta técnica de acceso.
Mi recomendación personal sería: Si manejamos objetos sencillos y con pocos atributos que podemos controlar sin mayor problema o que no van a destruir la funcionalidad de una clase, puede que no sea necesario complicarnos con los «Getters» y «Setters». Pero si vamos a trabajar con objetos complejos y, especialmente, si vamos a crear librerías que vamos a distribuir, el uso de «getters» y «setters» es altamente recomendado.
Otra forma de enfocar la necesidad de usar o no a los «getters» y «setters», puede ser simplemente viendo las ventajas que me ofrecen y preguntarse si se adaptan a mi necesidad, o si las desventajas no me afectan.
¿Cómo citar este artículo?
- En APA: Hinostroza, T. (8 de junio de 2023). El encapsulamiento. Blog de Tito. https://blogdetito.com/2023/06/08/el-encapsulamiento/
- En IEEE: T. Hinostroza. (2023, junio 8). El encapsulamiento. Blog de Tito. [Online]. Available: https://blogdetito.com/2023/06/08/el-encapsulamiento/
- En ICONTEC: HINOSTROZA, TIto. El encapsulamiento [blog]. Blog de Tito. Lima Perú. 8 de junio de 2023. Disponible en: https://blogdetito.com/2023/06/08/el-encapsulamiento/
Dejar una contestacion