Programación orientada a objetos. Encapsulamiento y abstracción.

José Javier Blanco Rivero

En esta lección pondremos en práctica las ideas aprendidas sobre programación orientada a objetos. Comencemos creando nuestra primera clase.

Como han podido ver en los videos correspondientes, una clase es como una plantilla o molde para generar objetos de determinado tipo.

No debemos tomarnos a la ligera la creación de clases; y esto es algo que es mejor aclarar desde un principio. Si hemos decidido utilizar el paradigma POO para resolver nuestro problema de dominio, lo primero que tenemos que hacer es modelar ese dominio en una jerarquía ontológica.

Digamos que nuestro programa servirá para prestar diferentes servicios a una universidad. Entonces tenemos que pensar qué tipo de entidad será la matriz de todas las demás. Acá podemos incluso echar mano de nuestros conocimientos sociológicos y modelar nuestro sistema de acuerdo a determinada corriente teórico-metodológica. Por ejemplo, si adoptamos una perspectiva sistémico-funcional á la Luhmann (y dejando de lado, por ahora al menos, que la idea misma de ontología es contraria al constructivismo sistémico del sociólogo alemán) podemos partir del hecho de que una universidad no es otra cosa que una organización; y que las organizaciones se diferencian de su entorno social, entre otras cosas, distinguiendo entre miembros y no miembros (Luhmann, 1997).

Si reparamos en el hecho de que los roles que caben dentro de una comunidad universitaria dependen de la condición de miembro y del tipo de membresía (profesor, estudiante, administrativo, etc.), nos podremos dar cuenta que partir de la clase de membresía podría resultar bastante adecuado.

Ahora bien, pongamos que creamos la clase MiembroDeLaComunidadUniversitaria, ¿qué características comunes tienen todos los miembros de la comunidad universitaria?

Bien, podemos pensar en el nombre, el documento de identidad (el cual puede coincidir o no con la credencial o carnet universitario), la adscripción administrativa (digamos, facultades, dependencias, institutos, etc.) y el rol.

En Java nuestra clase luciría así:

public class MiembroDeLaComunidadUniversitaria{
  public String nombre;
  public int dni;
  public int nroCarnet;
  public String adscripcion_admin;
  public String rol;
}
7.1s

¡Tenemos nuestra primera clase! Ahora creemos nuestro primer objeto.

MiembroDeLaComunidadUniversitaria m1 = new MiembroDeLaComunidadUniversitaria()
0.4s

Ahora tratemos de darle una identidad a m1:

m1.nombre= "Barbara Merino";
m1.dni = 2232223;
m1.nroCarnet = 123214;
m1.adscripcion_admin = "Facultad de Medicina";
m1.rol = "Estudiante";
m1
1.7s

¡Bien! ¡Tenemos nuestro primer objeto!

Pero ¿qué fue lo que se imprimió en pantalla? ¿Por qué al llamar al objeto no vemos los datos que le asignamos? Probemos imprimiéndolo

System.out.println(m1)
0.8s

Lo mismo. ¿Qué ocurre?

En primer lugar, tenemos que aclarar que hemos hecho varias cosas mal aquí. Para empezar los campos de una clase suelen ser privados (private) para que no puedan cambiarse desde algún otro lugar en el programa (recuerden el concepto de encapsulamiento). Hagámoslo de nuevo:

public class MiembroDeLaComunidadUniversitaria{
  private String nombre;
  private int dni;
  private int nroCarnet;
  private String adscripcion_admin;
  private String rol;
}
0.6s

¡Perfecto! Intentemos de nuevo.

MiembroDeLaComunidadUniversitaria m1 = new MiembroDeLaComunidadUniversitaria()
0.3s
m1.nombre= "Barbara Merino";
0.3s

¡Esperen! Hemos obtenido una excepción. ¿Por qué?

La excepción nos dice que el campo nombre es privado, por lo que no podemos acceder a él.

Cuando creamos una clase en Java, el compilador crea automáticamente un constructor vacío por nosotros. ¿Qué es un constructor? Un constructor es un método que permite crear una nueva instancia de un objeto.

En este caso, el constructor no tiene información de cómo crear un objeto con los campos que le pasamos. Veamos: si le asignamos el valor correspondiente a cada campo al constructor de la clase obtendremos una excepción.

MiembroDeLaComunidadUniversitaria m0 = new MiembroDeLaComunidadUniversitaria("Julian Perez", 21323,2313322,"Estudiante");
0.2s

Noten que la excepción dice que el constructor no requiere argumentos.

Lo que debemos hacer entonces es crear un constructor que nos permita crear un objeto con todos los campos o atributos correspondientes.

public class MiembroDeLaComunidadUniversitaria{
  private String nombre;
  private int dni;
  private int nroCarnet;
  private String adscripcion_admin;
  private String rol;
  
  public MiembroDeLaComunidadUniversitaria(String nombre, int dni, int nroCarnet, String adscripcion_admin, String rol){
    this.nombre=nombre;
    this.dni=dni;
    this.nroCarnet=dni;
    this.adscripcion_admin=adscripcion_admin;
    this.rol=rol;
  }
}

Ahora si intentamos algo como lo de arriba tendremos éxito:

MiembroDeLaComunidadUniversitaria m0 = new MiembroDeLaComunidadUniversitaria("Julian Perez", 21323,2313322,"Estudiante","Facultad de Ingeniería");

Sin embargo, todavía le faltan cosas a nuestra clase. Para que nuestra clase esté correctamente encapsulada debemos escribir dos tipos de métodos llamados getters y setters, es decir, la interfaz que nos permitirá fijar y obtener los valores de nuestro objeto.

public class MiembroDeLaComunidadUniversitaria{
  private String nombre;
  private int dni;
  private int nroCarnet;
  private String adscripcion_admin;
  private String rol;
  
  public MiembroDeLaComunidadUniversitaria(String nombre, int dni, int nroCarnet, String adscripcion_admin, String rol){
    this.nombre=nombre;
    this.dni=dni;
    this.nroCarnet=dni;
    this.adscripcion_admin=adscripcion_admin;
    this.rol=rol;
  }
  public String getNombre(){
    return nombre;
  }
  public int getDni(){
    return dni;
  }
  public int getNroCarnet(){
    return nroCarnet;
  }
  public String getAdscripcion_admin(){
    return adscripcion_admin;
  }
  public String getRol(){
    return rol;
  }
  public void setNombre(String nombre){
    this.nombre=nombre;
  }
  public void setDni(int dni){
    this.dni=dni;
  }
  public void setNroCarnet(int nroCarnet){
    this.nroCarnet=nroCarnet;
  }
  public void setAdscripcion_admin(String adscripcion_admin){
    this.adscripcion_admin=adscripcion_admin;
  }
  public void setRol(String rol){
    this.rol=rol;
  }
}

¡Bien! Ahora podemos crear un objeto, obtener el valor de sus campos y cambiárselos a través de esta pequeña API (Application Programming Interface) que hemos creado para nuestro objeto.

MiembroDeLaComunidadUniversitaria m1 = new MiembroDeLaComunidadUniversitaria("Juan Ron", 218323,231390322,"Profesor","Facultad de Ciencias Políticas");
m1.getNombre();
m1.getDni();
m1.setDni(19098234);
m1.getDni();

¡Genial! Pero todavía falta algo. Si lo intentamos de nuevo no vamos a poder ver impreso en pantalla todos los datos de nuestro objeto. Para esto debemos sobreescribir el método toString() que se encarga de representar en caracteres nuestro objeto. (También se suele sobreescribir el método equals(), sobretodo cuando deseamos estipular cómo se va a comportar nuestra clase cuando una instancia sea comparada con otra -pero esto no lo veremos aquí).

public class MiembroDeLaComunidadUniversitaria{
  private String nombre;
  private int dni;
  private int nroCarnet;
  private String adscripcion_admin;
  private String rol;
  
  public MiembroDeLaComunidadUniversitaria(String nombre, int dni, int nroCarnet, String adscripcion_admin, String rol){
    this.nombre=nombre;
    this.dni=dni;
    this.nroCarnet=dni;
    this.adscripcion_admin=adscripcion_admin;
    this.rol=rol;
  }
  public String getNombre(){
    return nombre;
  }
  public int getDni(){
    return dni;
  }
  public int getNroCarnet(){
    return nroCarnet;
  }
  public String getAdscripcion_admin(){
    return adscripcion_admin;
  }
  public String getRol(){
    return rol;
  }
  public void setNombre(String nombre){
    this.nombre=nombre;
  }
  public void setDni(int dni){
    this.dni=dni;
  }
  public void setNroCarnet(int nroCarnet){
    this.nroCarnet=nroCarnet;
  }
  public void setAdscripcion_admin(String adscripcion_admin){
    this.adscripcion_admin=adscripcion_admin;
  }
  public void setRol(String rol){
    this.rol=rol;
  }
  @Override
  public String toString(){
    return "Nombre: "+nombre+" DNI: "+dni+" Número de carnet: "+nroCarnet+" Adscripción Administrativa: "+adscripcion_admin+ "Rol: "+rol;
  }
}
MiembroDeLaComunidadUniversitaria m1 = new MiembroDeLaComunidadUniversitaria("Federico Brito", 218323,231390322,"Vigilante","Seguridad");
m1
System.out.println(m1);

¡Genial! Ahora sí tenemos nuestra primera clase y nuestro primer objeto en Java. Esto es lo que se llama un POJO, es decir, Plain Old Java Object.

Hay cosas aún que podemos mejorar, sin embargo. Cada lenguaje ofrece una serie de abstracciones que le facilitan al programador el diseño y representación de su modelo de dominio.

Pensemos en los campos de nuestra clase:

  • Nombre

  • DNI

  • Número de carnet

  • Adscripción administrativa

  • Rol

Si pensamos sobre las características de los dos últimos, nos damos cuenta que si bien pueden existir variaciones, estos atributos suelen ser muy estables a lo largo del tiempo. Una universidad no crea facultades a cada instante, ni tampoco inventa nuevos cargos y roles. Si pudiéramos de alguna manera definir esos atributos nos evitaríamos también cierto tipo de errores y obtendríamos gratuitamente una validación.

Pues bien, para estas ocasiones Java nos ofrece los enumerados o enum. Un enumerado es como una clase con número fijo e inmutable de elementos enumerables. En la jerga de Java, concretamente en términos de modificadores de acceso, los atributos de un enum son public (accesibles desde cualquier paquete dentro del programa), static (se pueden invocar sin crear una instancia del objeto) y final (no se pueden modificar). Creemos entonces un enum:

enum AdscripcionAdministrativa{
  FACULTAD_DE_CIENCIAS_JURIDICAS_Y_POLITICAS,
  FACULTAD_DE_INGENIERIA,
  FACULTAD_DE_CIENCIAS_ACTUARIALES,
  FACULTAD_DE_HUMANIDADES,
  FACULTAD_DE_CIENCIAS_NATURALES,
  DEPENDENCIA_DE_SERVICIOS,
  SEGURIDAD,
  BIBLIOTECA
}
0.3s

Se estila escribir en mayúsculas los elementos de un enumerable. Nos falta otro.

enum Rol {
  ESTUDIANTE,
  PROFESOR,
  INVESTIGADOR,
  VIGILANTE,
  JEFE_DE_VIGILANCIA,
  BIBLIOTECARIO,
  PERSONAL_ADMINISTRATIVO,
  RECTOR,
  VICERRECTOR,
  DECANO,
  DIRECTOR_DE_ESCUELA
}
0.2s

Ahora que tenemos nuestros enumerables, declaremos de nuevo la clase agregándole las modificaciones correspondientes:

public class MiembroDeLaComunidadUniversitaria{
  private String nombre;
  private int dni;
  private int nroCarnet;
  private AdscripcionAdministrativa adscripcion_admin;
  private Rol rol;
  
  public MiembroDeLaComunidadUniversitaria(String nombre, int dni, int nroCarnet, AdscripcionAdministrativa adscripcion_admin, Rol rol){
    this.nombre=nombre;
    this.dni=dni;
    this.nroCarnet=dni;
    this.adscripcion_admin=adscripcion_admin;
    this.rol=rol;
  }
  public String getNombre(){
    return nombre;
  }
  public int getDni(){
    return dni;
  }
  public int getNroCarnet(){
    return nroCarnet;
  }
  public AdscripcionAdministrativa getAdscripcion_admin(){
    return adscripcion_admin;
  }
  public Rol getRol(){
    return rol;
  }
  public void setNombre(String nombre){
    this.nombre=nombre;
  }
  public void setDni(int dni){
    this.dni=dni;
  }
  public void setNroCarnet(int nroCarnet){
    this.nroCarnet=nroCarnet;
  }
  @Override
  public String toString(){
    return "Nombre: "+nombre+" DNI: "+dni+" Número de carnet: "+nroCarnet+" Adscripción Administrativa: "+adscripcion_admin+ "Rol: "+rol;
  }
}
0.3s

Dos cosas a destacar:

  • Primero, nos deshicimos de los setters en adscripcion_admin y rol, ya que como explicamos los atributos de un enumerado son inmutables.

  • Segundo, dado que estamos en un entorno virtual no aplica, pero normalmente declaramos los enumerables en archivos java en su propio paquete y debemos importarlos (import) adonde van a ser usados.

Generemos un nuevo objeto que refleje las modificaciones que hemos realizado:

MiembroDeLaComunidadUniversitaria m3 = new MiembroDeLaComunidadUniversitaria("Fede Bribon", 218321123,23199922,AdscripcionAdministrativa.FACULTAD_DE_HUMANIDADES,Rol.ESTUDIANTE);
0.2s
m3.getAdscripcion_admin()
0.4s

¡Excelente! Ahora ¡¿por qué no se animan a crear su propia clase?!

En Python podemos crear una clase e instanciar objetos de la misma de la siguiente manera:

class MiembroDeLaComunidadAcademica:
  def __init__(self, nombre, dni, nroCarnet, adscripcion_admin, rol):
    self.nombre=nombre
    self.dni=dni
    self.nroCarnet=nroCarnet
    self.adscripcion_admin=adscripcion_admin
    self.rol=rol
alumno = MiembroDeLaComunidadAcademica("Juan Fiannello", 1233434, 2314313,"Facultad de Agricultura", "Estudiante")
alumno.nroCarnet
alumno

En Python el método __init__ es el constructor y recibe como primer parámetro (self) una referencia que le permite hacer lugar para cada instancia particular que se cree del objeto.

En nuestra última celda de código Python nos encontramos con el mismo problema que en Java. No obstante, a diferencia de Java en Python no existe un método toString(). De hecho, hacer algo como esto es poco idiomático en Python. En todo caso, así es como se haría:

class MiembroDeLaComunidadAcademica:
  def __init__(self, nombre, dni, nroCarnet, adscripcion_admin, rol):
    self.nombre=nombre
    self.dni=dni
    self.nroCarnet=nroCarnet
    self.adscripcion_admin=adscripcion_admin
    self.rol=rol
    
  def __str__(self):
     return "Nombre: "+self.nombre+" DNI: "+str(self.dni)+" Número de Carnet: "+str(self.nroCarnet)+" Adscripción administrativa: "+self.adscripcion_admin+" Rol: "+self.rol
0.0s

Noten que en el método __str__ tuvimos que castear a string las variables enteras con el método str(). De lo contrario obtendríamos una excepción.

Ahora creamos un objeto de esta clase:

alumno = MiembroDeLaComunidadAcademica("Juan Fiannello", 1233434, 2314313,"Facultad de Agricultura", "Estudiante")
0.0s

Imprimamos:

print(alumno)

También podemos llamar a la clase y a una de sus funciones declaradas, como el __str__ y a continuación le pasamos en el lugar del self la referencia a un objeto que ya hemos instanciado, como alumno:

MiembroDeLaComunidadAcademica.__str__(alumno)
0.0s

A esto se le llama function object, mientras que la instanciación más común donde colocamos primero la referencia al objeto y luego llamamos al método (e.g. alumno.metodo_imaginario() ) se le llama method object.

Python no es un lenguaje que nos restrinja demasiado. Se dejan muchas cosas a cargo de las convenciones y de la documentación (cosa de la que no hemos hablado tampoco, no queremos sobrecargarlos; pero ya hablaremos sobre eso).

Mirando hacia atrás lo que llevamos hecho hasta aquí, bien podrían pensar que lleva mucho trabajo crear una clase en Java, sobre todo comparándola con Python. ¡Tienen toda la razón! De hecho, no han sido los primeros en notarlo.

En Scala (uno de tantos lenguajes hospedados en la máquina virtual de Java, JVM), por ejemplo, han pensado en esto y han creado las case classes. Una case class es una expresión compacta que se ocupa de crear por nosotros los constructores, getters, equals, hashcode y el toString(). En fin, ¡un POJO con todo!

Veamos:

case class MiembroDeLaComunidadUniversitaria(nombre: String, dni: Int, nroCarnet: Int, adscripcion_admin: String, rol: String)
9.6s

En Scala el case class no genera setters ya que, según los principios de la programación funcional, nuestras estructuras de datos deben ser inmutables. Ya tendremos ocasión de conversar sobre esto. Tampoco es necesario que usemos la palabra clave new para generar una nueva instancia de nuestra clase caso. Sencillamente hacemos esto:

val profesorX = MiembroDeLaComunidadUniversitaria("Xavier", 12343,674654,"Facultad de Ciencias", "Profesor")
1.7s
profesorX
0.8s

Si queremos conocer el valor de uno de los campos en específico, digamos, el nombre:

profesorX.nombre
0.9s

En Scala también podemos usar enumerables, pero eso lo dejaremos para que ustedes lo investiguen y lo apliquen en la celda de abajo:

En Julia si queremos una estructura de datos como aquella con la que hemos venido trabajando hasta ahora, debemos crear un struct:

struct MiembroDeLaComunidadUniversitaria
  nombre::String
  dni::Int64
  nroCarnet::Int64
  adcripcion_admin::String
  rol::String
end
0.1s
miembro = MiembroDeLaComunidadUniversitaria("Juan Lovaglio", 123343, 452332, "Instituto de Investigaciones Avanzadas", "Investigador")
0.4s
miembro
0.2s
miembro.rol
0.5s

En Julia un struct es un constructor, el cual no es otra cosa que una función capaz de crear nuevos objetos. Se usa cuando necesitamos crear un tipo compuesto. Como vemos en este caso nuestro MiembroDeLaComunidadUniversitaria tendría el siguiente tipado (String, Integer, Integer,String, String), en fin, es un tipo compuesto.

De igual forma que en Scala y sus case classes, un struct es immutable. Por lo que si queremos cambiar el valor de uno de sus campos obtendremos una excepción:

miembro.nombre = "Martin Martinez"
1.0s

Aunque es desaconsejado, si por mor de nuestra lógica de negocio necesitamos cambiar el valor de los campos de un struct, debemos declararlo como mutable. Así:

mutable struct MiembroDeLaComunidadUniversitaria2
  nombre::String
  dni::Int64
  nroCarnet::Int64
  adcripcion_admin::String
  rol::String
end
0.3s
miembro2 = MiembroDeLaComunidadUniversitaria2("Juan Lovaglio", 123343, 452332, "Instituto de Investigaciones Avanzadas", "Investigador")
0.3s
miembro2.nombre = "Martin Martinez"

Y como ven, de esta manera pudimos cambiar el valor del campo nombre.

En Julia podemos crear constructores tanto fuera como dentro de la declaración del struct.

Digamos que ya hemos declarado nuestro struct en su modulo correspondiente, y como mucho código depende de él, no queremos modificarlo. Pero aún así necesitamos crear un objeto de este tipo compuesto que sólo cuente con dos campos, a saber, nombre y DNI. Es así como lo podríamos hacer:

MiembroDeLaComunidadUniversitaria(nombre, dni)= (nombre,dni)
0.8s
miembro_especial = MiembroDeLaComunidadUniversitaria("Manuel Garcia", 1277878)

¡Genial! Problema resuelto.

También podemos crear los constructores internos -los usuales en los lenguajes que hemos visto acá. En Julia se suelen usar si explícitamente necesitamos hacer una validación, de resto dejamos el struct como arriba.

Digamos que queremos validar que en el campo nombre se escriba tanto el nombre como el apellido separados por un espacio.

struct MiembroDeLaComunidadUniversitaria3
  nombre::String
  dni::Int64
  nroCarnet::Int64
  adscripcion_admin::String
  rol::String
  MiembroDeLaComunidadUniversitaria3(nombre, dni, nroCarnet, adscripcion_admin, rol) = occursin(r"[A-Z]{1}[a-z]+\s[A-Z]{1}[a-z]+", nombre) ? new(nombre,dni,nroCarnet,adscripcion_admin, rol) : error("No ha ingresado un nombre válido") 
end
0.2s
miembro_truch = MiembroDeLaComunidadUniversitaria3("Valeria manzano", 232133, 13112, "Dependencias de Servicios", "Personal de Limpieza")
0.4s
miembro_truch1 = MiembroDeLaComunidadUniversitaria3("valeria manzano", 232133, 13112, "Dependencias de Servicios", "Personal de Limpieza")
0.4s
miembro_truch2 = MiembroDeLaComunidadUniversitaria3("valeria Manzano", 232133, 13112, "Dependencias de Servicios", "Personal de Limpieza")
0.4s

Como pueden observar en los tres casos de prueba de arriba, se ha disparado la excepción que hemos generado. Si el nombre los escribimos de forma correcta, podríamos esperar que lo valide:

miembro_valido = MiembroDeLaComunidadUniversitaria3("Valeria Manzano", 232133, 13112, "Dependencias de Servicios", "Personal de Limpieza")
0.4s

¡Perfecto!

Imagino que habrán observado con espanto la siguiente expresión: r"[A-Z]{1}[a-z]+\s[A-Z]{1}[a-z]+". Se trata de una expresión regular. Las expresiones regulares conforman una suerte de lenguaje que nos permite generar patrones para encontrar coincidencias.

Este tema no pertenece a esta lección, pero básicamente acá estamos indicando que la coindencia debe cumplir con el siguiente patrón: la primera letra debe ser una mayúscula de la A a la Z [A-Z]{1}; a esta le seguirán 1 o más letras de la a a la z [a-z]+; seguidamente deberá haber un espacio \s; y por último, repetimos el mismo patrón del inicio.

Todo lenguaje de programación admite expresiones regulares y son sumamente prácticas, así que vale la pena aprenderlas. Sin embargo, es menester reparar en el hecho que cada lenguaje tiene una sintaxis particular para su introducción, por ejemplo, en Julia para que el intérprete reconozca que se trata de una expresión regular, ésta debe encontrarse dentro de la siguiente estructura: r" "

Por otra parte, también pudo haber causado sorpresa también la siguiente estructura: <predicado> ? <expresión> : <expresión>. Se trata del denominado operador terciario, que no es otra cosa que una forma de escribir (syntactic sugar, le dicen) un condicional en una sola línea. Es decir, si tal cosa, (?) entonces, (:) sino, tal otra.

También se trata de un recurso bastante común en diversos lenguajes.

Sigamos adelante.

Si nos volvemos hacia un lenguaje un poco más funcional como Clojure, observamos que se nos alienta a crear mapas, al menos, en las etapas incipientes de diseño. Y la idea es que, por lo general, muchas veces nos encontramos con que queremos añadir o quitar algún campo; cuando tenemos un clase esto puede dar dolores de cabeza, mientras que con un mapa es bastante trivial.

(def vigilante {:nombre "Marcos Parra"
                :dni 2231232
                :nroCarnet 121989
                :adscripcion_admin :seguridad
                :rol "Vigilante"})
0.2s

Ahora bien, podemos crear un vector que contenga un mapa con cada miembro de comunidad universitaria si así lo queremos. Y podemos actualizar esa estructura de datos en la medida en que ingresan nuevos miembros y egresan otros tantos. Por ejemplo:

(def miembros-de-la-comunidad-universitaria [{:nombre "Julia Alvarez"
                                              :dni 3213433
                                              :nroCarnet 23143412
                                              :adcripcion_admin "Facultad de Medicina"
                                              :rol "Docente"}
                                             {:nombre "Maira Merino"
                                              :dni 23133423
                                              :nroCarnet 3323321
                                              :adscripcion_admin "Facultad de Derecho"
                                              :rol "Estudiante"}])
;; Y así sucesivamente...

Puede parecer tedioso tener que escribir una y otra vez cada campo. Sin embargo, esto tiene una ventaja. Es probable que existan miembros de la comunidad universitaria que, por alguna razón u otra, no cuenten con datos en uno de los campos. O bien, por ejemplo, en el caso del documento de identidad, puede que existan alumnos extranjeros que no tengan DNI sino pasaporte.

En Java u otros lenguajes tendríamos asignarle null a estos campos (lo cual puede traernos ciertos problemas más adelante) o bien habría que agregar un nuevo campo reformando la clase o transformar uno de los campos existentes (e.g. podríamos agregar los siguientes campos: String TipoDocumento, Int NumeroDocumento). Y definitivamente tendríamos un problema si creamos un enum, como arriba.

En Clojure, en cambio, si una entidad carece de datos en uno de los campos, sencillamente se excluye ese campo.

Veamos:

(def miembros-de-la-comunidad-universitaria [{:nombre "Julia Alvarez"
                                              :dni 3213433
                                              :nroCarnet 23143412
                                              :adcripcion_admin "Facultad de Medicina"
                                              :rol "Docente"}
                                             {:nombre "Maira Merino"
                                              :dni 23133423
                                              :nroCarnet 3323321
                                              :adscripcion_admin "Facultad de Derecho"
                                              :rol "Estudiante"}
                                             {:nombre "Fermin Toro"
                                              :pasaporte 4545115
                                              :nroCarnet 31234112
                                              :adscripcion_admin "Facultad de Ciencias"
                                              :rol "Profesor invitado"}
                                             {:nombre "Lucas Moura"
                                              :nroCarnet 31234112
                                              :adscripcion_admin "Facultad de Ingeniería"
                                              :rol "Estudiante"
                                              :status :suspendido}])
0.1s

Sin embargo, si estamos seguros de que nuestros campos son los que son y que preferimos utilizar una estructura de datos como una clase de Java, entonces podemos utilizar un record:

(defrecord MiembroDeLaComunidadUniversitaria [nombre dni nroCarnet adscripcion_admin rol])
0.3s

Bajo cuerdas un record en Clojure es una clase de Java con todas sus ventajas y sin su verbosidad; y su desempeño es mejor que el de un simple mapa. Para crear una instacia de un record o registro hacemos lo siguiente:

(def vicerrector (->MiembroDeLaComunidadUniversitaria "Jose Chirinos" 231234313 32432213 "Facultad de Veterinaria" "Vicerrector"))
0.1s

Si deseamos conocer el valor de un campo, colocamos en la posición de función el nombre del campo con notación de keyword (es decir, precedido de dos puntos : ) y, luego, el nombre de la variable que contiene al registro en cuestión. Veamos:

(:nombre vicerrector)
0.0s
(println vicerrector)
0.4s

Como vimos, también podemos imprimir el registro en pantalla, si así nos place.

Este registro lo podemos agregar sin problemas a nuestra estructura de datos anterior:

(conj miembros-de-la-comunidad-universitaria vicerrector)
0.1s

Si despliegan la estructura de datos que muestra como resultado, podrán ver que el registro es un mapa -sólo que Clojure se aprovecha de la eficiencia de las clases de Java.

Recordemos que Clojure en un lenguaje hospedado en la JVM de Java. Y claro está, bajo cuerdas también, toda estructura de datos y toda función en Clojure es un objeto Java.

En Racket podemos ver similitudes con Julia y Clojure. Para crear un objeto utilizamos la función struct:

(struct MiembroDeLaComunidadUniversitaria (nombre dni nroCarnet adscripcion_admin rol))
0.2s

Crear un objeto de este tipo es bastante fácil también.

(define miembroZ (MiembroDeLaComunidadUniversitaria "Julian Alvarez" 134342 8903932 "Facultad de Deportes" "Entrenador"))
0.0s

Si queremos obtener el valor de uno de los campos utilizamos la notación struct-campo:

(MiembroDeLaComunidadUniversitaria-nombre miembroZ)
0.0s
(MiembroDeLaComunidadUniversitaria-adscripcion_admin miembroZ)
0.0s

Racket nos habilita un predicate homónimo al crear un struct. Esta función es muy útil si queremos verificar si una estructura de datos pertenece a este tipo. Por ejemplo (por cierto, en Racket #t significa true y #f false):

(MiembroDeLaComunidadUniversitaria? miembroZ)
0.0s
(MiembroDeLaComunidadUniversitaria? '("Pedro Hernandez" 2144656 4232434 "Facultad de Medicina" "Estudiante"))
0.0s

Como podemos observar en el ejemplo de arriba, aunque la lista '( ) tiene los mismos datos que se corresponderían con nuestro struct, no deja de ser una lista, y por ende, es una estructura de datos diferente. Un objeto del tipo que hemos creado sólo se puede generar con su constructor, el cual en este caso es una función cuyo nombre es el nombre del struct en cuestión y los parámetros correspondientes.

Al igual que en otros lenguajes de naturaleza funcional, los campos de un struct en Racket son inmutables. Si deseamos modificar algún campo debemos realizar lo que se denomina una actualización funcional (functional update), que no es otra cosa que una copia del struct original a la que se le añaden los cambios correspondientes. El resultado será también una estructura de datos inmutable.

(define miembroY (struct-copy MiembroDeLaComunidadUniversitaria miembroZ [nombre "Martin Palermo"] [dni 122314452]))
0.0s
miembroY
0.0s

Notas de cierre

Aunque hemos visto cómo se crean objetos en lenguajes que pertenecen a distintos paradigmas, es posible percibir ciertos patrones comunes y recurrentes.

  • En primer lugar, hemos de notar que siempre existe una abstracción (llamémosla clase, registro o struct) que sirve para engendrar nuevos objetos de un tipo definido por el usuario.

  • En segundo lugar, esta abstracción necesita un método o función (el cual puede ser provisto por el compilador o intérprete y/o puede ser creado también por el usuario) que le permite instanciar un objeto del tipo específico. Digamos que son las instrucciones para generar un objeto del tipo definido en la clase, registro o struct. Es posible introducir en el constructor cualquier tipo de restricción que consideremos conveniente a la hora de crear un objeto, aunque este tema en particular quedará para otra ocasión.

  • En tercer lugar, cada lenguaje según su filosofía y su estructura en particular ofrece mecanismos de encapsulamiento, es decir, constreñimientos que evitan y/o limitan las condiciones bajo las cuales los campos de un objeto pueden ser modificados. El encapsulamiento es muy importante dado que evita que otras funciones o bloques de código de nuestro programa modifiquen el estado interno de los objetos creados, causando problemas de inconsistencia sobre todo en contextos de concurrrencia y paralelismo.

  • Y en cuarto lugar, cada lenguaje nos ofrece un conjunto de abstracciones que nos asisten en el modelamiento de nuestro problema de dominio, es decir, en la traducción de un problema real en el mundo social a un conjunto de estructuras de datos informáticas, sobre las que nuestro computador puede operar y efectuar cálculos de tal modo de alcanzar los efectos que deseemos alcanzar.

Ahora no queda más que practicar. ¡Abran un par de celdas en los lenguajes que prefieran y tiren algo de código! ¡Experimenten! Y sobre todas las cosas ¡diviértanse!

Runtimes (6)
Runtime Languages (12)