Programación orientada a objetos. Herencia y composición.

José Javier Blanco Rivero

En la lección anterior comentábamos que antes de crear una clase debemos detenernos y pensar en el diseño de nuestro programa; debemos enfocarnos en modelar nuestro problema de dominio. Esta es la idea fundamental detrás del principio de abstracción. Decíamos también que este modelo viene a ser como una gran cadena del ser, esto es, una ontología. Por ende, debemos pensar las relaciones entre clases en términos de género y especie.

Ciertamente, en las ciencias sociales existen fuertes críticas hacia la filosofía ontológica, por ejemplo, en la filosofía/sociología de la ciencia de B. Latour o en la sociología sistémica de N. Luhmann, entre otros. Y, en este orden de ideas, sería muy interesante trasladar estas ideas funcionales y constructivistas al diseño o arquitectura de software. Sin embargo, este es un tema que quedará para la investigación futura. Por ahora, si queremos obtener las ventajas que ofrecen la mayoría de lenguajes que soportan el POO, debemos pensar ontológicamente, esto es, en términos de esencias inmutables.

No obstante, también existen críticas dentro del mundo de las ciencias de la computación hacia el concepto de herencia. Algunos sugieren, apelando a los orígenes históricos de la idea de programación orientada a objetos (expresión cuyo acuñamiento se le atribuye a Alan Kay), que el concepto de herencia no pertenece al POO, sino que se trata de una idea espúrea, por así decirlo, que se incorporó en los lenguajes de la familia C por diversidad de razones y que, con el éxito posterior del POO, ha venido a considerarse como parte integrante del mismo. Pero las críticas van más allá. Se alega que la herencia introduce en nuestros modelos un conjunto de problemas de diseño los cuales, sin entrar en detalles, se derivan en última instancia de pensar un mundo dinámico y cambiante en términos de entes inmutables.

A la hora de pensar nuestro problema empleando las asbtracciones del POO, existe una herramienta estandarizada que asiste en el diseño de la arquitectura del software, esto es, el Unified Modelling Language (UML). Hoy en día casi cualquier software de dibujo o diagramación incluye componentes de UML. Se trata básicamente de un conjunto de convenciones para representar clases, sus atributos y relaciones. Es recomendable usar siempre el UML, tanto desde las primeras etapas de esbozo y diseño hasta las etapas más avanzadas de documentación.

Sin más preámbulos veamos cómo se pone en práctica el concepto de herencia.

En la lección pasada habíamos creado una clase Java que diseñamos con la idea de servir de matriz para una jerarquía de clases. En aquel momento, varias decisiones que tomamos estaban orientadas por las ideas que queríamos discutir entonces. Sin embargo, a la hora de pensar en la herencia en Java, debemos tomar en cuenta un par de cosas:

  • Existen dos tipos de clases, las clases propiamente dichas y las clases abstractas. La diferencia radica en que las clases abstractas no pueden ser instanciadas, es decir, no podemos crear un objeto de este tipo de clases (e.g. new ClaseAbstractaCualquiera( ) ). Aquí cabe reflexionar sobre lo siguiente: ¿nos conviene partir de una clase abstracta como matriz de nuestro modelo? O ¿nos conviene una clase convencional?

  • La herencia implica que las clases hijas o descendientes incluyen automáticamente todos los atributos, objetos o métodos definidos en las clases parientes siempre y cuando éstos hayan sido declarados con los modificadores de acceso public o protected. Como recordarán, para demostrar el concepto de encapsulamiento hicimos privados de todos los atributos en nuestra clase. En consecuencia, deberíamos convertir todos los campos a public o protected , ¿cierto? ¡No tan rápido! Si pensamos en las clases que podrían heredar de MiembroDeLaComunidadUniversitaria y miramos los atributos de la clase padre, podríamos pensar que el atributo rol estaría de más. Y es que, en cierta manera, esta propiedad prefigura las clases hijas. Esto puede ser un problema o no; depende del lenguaje y de la forma en que implementemos la herencia. Podríamos deshacernos de ese atributo o, sencillamente, podríamos dejarlo como privado.

Por supuesto, las decisiones que tomemos deben ser congruentes. Si decidimos que MiembroDeLaComunidadUniversitaria es una clase abstracta no tendría sentido incluir el atributo de rol, ya que no se puede instanciar.

En el caso del modelo que pensamos armar, como lo es un hipotético software para la administración de una universidad, tiene mucho sentido hacer de la clase MiembroDeLaComunidadUniversitaria una clase abstracta. No nos interesa que el género miembro de la comunidad universitaria sea instanciable; nos interesa que las especies de miembros de la comunidad lo sean.

El enumerable se queda. Declaremos nuestra clase como abstracta y los modificadores de acceso de sus miembros como protected. Y, como podrán deducir, una clase asbtracta no lleva constructor.

Breve nota sobre correr código Java en celdas:

Debemos tener en cuenta que las celdas no son el entorno natural del código Java, por lo que se aplican ciertas excepciones a la norma. Por ejemplo, en Java sólo está permitida una clase con modificador de acceso public por archivo .java y cuando queremos usar una clase que pertenece a otro paquete (los enumerables, por ejemplo, se suelen agrupar en su propio paquete) es necesario importarla (e.g. import paquetex.ClaseZ;)

public 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
}
public abstract class MiembroDeLaComunidadUniversitaria{
  protected String nombre;
  protected int dni;
  protected int nroCarnet;
  protected AdscripcionAdministrativa adscripcion_admin;
}
7.0s

Ahora generemos una clase que herede de ésta. En Java indicamos que una clase hereda de otra, agregando la palabra reservada extends en la misma línea de su declaración.

Así que creemos la clase Estudiante. Pero antes, ¿qué atributos propios le corresponderían (además de los que hereda de su clase padre o matriz, como gusten)?

Podríamos pensar en carrera, estatus (activo, inactivo, suspendido, egresado), materias (inscritas, aprobadas, reprobadas), récord académico y las actividades deportivas y de extensión de las que forma parte.

¿Listo? No tan rápido. Una materia tendría que ser una clase por derecho propio. Y si lo pensamos bien, provendría de otra jerarquía diferente (e.g. Facultad- > Escuela -> Carrera -> Pensum -> Materia). De momento hagamos abstracción de ello, y creemos solamente la clase Materia.


public class Materia{
  private String carrera; //Debería ser un objeto del tipo Carrera, pero hagamos de ella un String provisionalmente
  private String nombre;
  private String periodo_lectivo;
  private int codigo;
  private int [] prelaciones; // códigos de materias que deben ser cursadas antes que se permita ver la actual
  
public Materia(String carrera, String nombre, String periodo_lectivo, int codigo, int[] prelaciones){
    this.carrera=carrera;
    this.nombre=nombre;
    this.periodo_lectivo=periodo_lectivo;
    this.codigo=codigo;
    this.prelaciones=prelaciones;
  }
  public String getCarrera(){
    return carrera;
  }
  public String getNombre(){
    return nombre;
  }
  public String getPeriodo(){
    return periodo_lectivo;
  }
  public int getCodigo(){
    return codigo;
  }
  public int[] getPrelaciones(){
    return prelaciones;
  }
  
  @Override
  public String toString(){
    return nombre;
  }
}
0.4s

Ahora declaremos un par de enumerados que vamos a necesitar y luego nuestra clase Estudiante:

public enum Estatus {
  ACTIVO, INACTIVO, SUSPENDIDO, EGRESADO
}
public enum EstatusMateria {
  INSCRITA, APROBADA, REPROBADA
}
public class Estudiante extends MiembroDeLaComunidadUniversitaria{
  private String carrera;
  private Estatus estatus;
  private Map<EstatusMateria, List<Materia>> materias;
  private double record_academico;
  private List<String> actv_depor_y_extn;
  
  public Estudiante(String nombre, int dni, int nroCarnet, AdscripcionAdministrativa adscripcion_admin, String carrera, Estatus estatus, double record_academico, List<String> actv_depor_y_extn){
    this.nombre=nombre;
    this.dni=dni;
    this.nroCarnet=nroCarnet;
    this.adscripcion_admin=adscripcion_admin;
    this.carrera=carrera;
    this.estatus=estatus;
    this.materias=prepararMapa();
    this.record_academico=record_academico;
    this.actv_depor_y_extn=actv_depor_y_extn;
  }
  private HashMap<EstatusMateria,List<Materia>> prepararMapa(){
    var mapa = new HashMap<EstatusMateria,List<Materia>>();
    mapa.put(EstatusMateria.INSCRITA, new ArrayList<Materia>());
    mapa.put(EstatusMateria.APROBADA, new ArrayList<Materia>());
    mapa.put(EstatusMateria.REPROBADA, new ArrayList<Materia>());
    return mapa;
  }
  public String getNombre(){
    return nombre;
  }
  public int getDni(){
    return dni;
  }
  public int getNroCarnet(){
    return nroCarnet;
  }
  public AdscripcionAdministrativa getAdscripcionAdmin(){
    return adscripcion_admin;
  }
  public String getCarrera(){
    return carrera;
  }
  public Estatus getEstatus(){
    return estatus;
  }
  public Map<EstatusMateria,List<Materia>> getMaterias(){
    return materias;
  }
  public double getRecordAcademico(){
    return record_academico;
  }
  public List<String> getActv_depor_y_extn(){
    return actv_depor_y_extn;
  }
  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 setCarrera(String carrera){
    this.carrera = carrera;
  }
  public void setMaterias(EstatusMateria estatus,List<Materia> lista_materias) throws IllegalArgumentException{
     var intrMateria = this.materias.computeIfPresent(estatus, (est, ls) -> {
      ls.addAll(lista_materias);
      return ls;
    });
    if (intrMateria == null) throw new IllegalArgumentException("Sólo puede elegirse una de as siguientes llaves: INSCRITA, APROBADA o REPROBADA"); 
  }
  public void setRecordAcademico(double record_academico){
    this.record_academico=record_academico;
  }
  public void setActv_depor_y_extn(List<String> actv_depor_y_extn){
    this.actv_depor_y_extn=actv_depor_y_extn;
  }
  @Override
  public String toString(){
    return "Nombre: "+nombre+" DNI: "+dni+" Número de carnet: "+nroCarnet+" Adscripción Administrativa: "+adscripcion_admin+" Carrera: "+carrera+" Estatus: "+estatus+" Materias: "+materias+" Récord académico: "+record_academico+" Actividades deportivas y de extensión: "+actv_depor_y_extn;
  }
}
 
1.6s

Notemos varias cosas acá:

  • Primero, en el constructor definimos los atributos presentes en la clase padre (nombre, dni, nroCarnet y adscripcion_admin) usando la palabra reserva this. También pudimos haber usado la palabra reservada super, la cual hace referencia justamente a los atributos de la clase matriz.

  • Segundo, si por alguna razón hubiésemos olvidado hacer referencia a estos atributos paternos en el constructor, no sería posible asignarle valor a estos campos a la hora de crear el objeto correspondiente.

  • Tercero, en nuestro constructor introdujimos una pequeña regla de negocio. No estamos permitiendo que el usuario defina por sí mismo las materias. Creamos un método privado (al cual, por ende, no se puede acceder fuera de la clase) que nos sirve para inicializar el mapa que clasifica las materias según sean incritas, aprobadas o reprobadas. La inicialización consiste en crear las únicas llaves permitidas en esta estructura de datos, mientras que para los valores se crea una lista vacía que después se llenará con las materias que correspondan según sea el caso.

  • Cuarto, el método setter que corresponde a las materias también tiene una configuración particular. Y es que lo hemos programado de tal manera para que un objeto del tipo Estudiante sólo pueda tener materias inscritas, aprobadas o reprobadas si se introducen a través de este método. Pedimos como parámetro un estatus y una lista de materias. Hemos usado un método de la interfaz Map llamado computeIfPresent() que no hace otra que verificar que una llave exista en el mapa y, si es así, inserta el dato correspondiente en esa llave. Dado que el método computeIfPresent() nos pide como segundo argumento la implementación de una interfaz funcional del tipo BiFunction (esto es, una función que pide dos parámetros), le pasamos una función lambda o función anónima cuyo primer parámetro es la llave misma y el segundo parámetro es la lista que ya se encuentra en nuestra estructura de datos. La clase List tiene un método llamado addAll() que nos permite insertar una colección entera en nuestra lista. Por último, dado que addAll() devuelve un booleano, necesitamos escribir return ls para que la función lambda nos devuelva la lista. De lo contrario, la operación nos arrojaría una excepción. La función computeIfPresent arroja null si no consigue la llave respectiva, por ende capturamos el resultado de la operación en una variable y aprovechamos para lanzar una excepción (IllegalArgumentException) en caso que arroje null, de esta manera el usuario sabrá qué salió mal si no observa el resultado esperado. (No se preocupen si este cuarto punto se les hace muy complicado, lo importante por ahora es que se capte la idea principal: que podemos utilizar los constructores, métodos de la clase y/o setters para implementar una lógica de negocio que regule cómo se crea nuestro objeto y cómo permitimos que se modifique su estado)

Pues bien, ahora generemos un objeto del tipo Estudiante (lo que nos exige que primero generemos objetos del tipo Materia y, finalmente, una lista de tipo String):

int[] p1 = {22,32};
int[] p2 ={232,32};
var materia1 = new Materia("Ingeniería de sistemas", "Cálculo I", "2022-I", 232, p1);
var materia2 = new Materia("Ingeniería de sistemas", "Laboratorio de Informática", "2022-I", 132, p2);
var actividades = new ArrayList<String>(Arrays.asList("Fútbol", "Teatro"));
var estudiante = new Estudiante("Juan Gallegos", 21434323, 232123, AdscripcionAdministrativa.FACULTAD_DE_INGENIERIA, "Ingeniería de sistemas", Estatus.ACTIVO, 0.0, actividades);
//Sólo podemos agregar materias a nuestro objeto, como explicamos arriba, a través del método setMaterias, así que hagámoslo:
estudiante.setMaterias(EstatusMateria.INSCRITA, Arrays.asList(materia1, materia2));
2.5s
//Inspeccionemos el objeto que recién creamos
estudiante
0.6s

Habrán notado más arriba que usamos var en vez de declarar el tipo de dato. Esta sintaxis está permitida desde Java 11 siempre y cuando sea posible para el compilador deducir por el contexto a qué tipo de dato nos estamos refiriendo.

¡Bien! Ahora digamos que este estudiante reprobó Cálculo I.

estudiante.setMaterias(EstatusMateria.REPROBADA, Arrays.asList(materia1))
0.4s
estudiante.getMaterias()
0.4s

¡Perfecto!

Ahora bien, existen ocasiones en las que queremos limitar la forma en que se usa la herencia en nuestro modelo; para ser más específicos, deseamos que ciertas clases no puedan tener herederos. Para estas ocasiones debemos utilizar la palabra reservada final en la declaración de nuestra clase. Por ejemplo:

public final class Vigilante extends MiembroDeLaComunidadUniversitaria {
  private String rango;
  private String turno; //Debería ser un enumerado
  
  public Vigilante(String nombre, int dni, int nroCarnet, AdscripcionAdministrativa adscripcion_admin, String rango, String turno){
    this.nombre=nombre;
    this.dni=dni;
    this.nroCarnet=nroCarnet;
    this.adscripcion_admin=adscripcion_admin;
    this.rango=rango;
    this.turno=turno;
  }
  //Le siguen getters y setters, toString, etc., etc.
  
}
0.3s

public class CuidadorDeAutos extends Vigilante {
  String zona;
  int tarifa;
  public CuidadorDeAutos(String nombre, int dni, int nroCarnet, AdscripcionAdministrativa adscripcion_admin, String zona, int tarifa){
    this.nombre=nombre;
    this.dni=dni;
    this.nroCarnet=nroCarnet;
    this.adscripcion_admin=adscripcion_admin;
    this.zona=zona;
    this.tarifa=tarifa;
  }
}
0.6s

Esta celda tiene una conducta anormal. Java no debería dejarte crear esta clase. Verías una excepción del tipo "cannot inherit from final Vigilante". Pueden probarlo en su jshell si se han descargado el JDK de Java en sus equipos. Sin embargo, por alguna razón, si se ejecuta esta celda algunas veces entra en un bucle infinito y en otras envía una excepción que no viene a lugar o que es secundario.

Acá les mostramos un snapshot de nuestra consola donde se muestra la excepción que debería aparecer acá.

Supongamos por un momento que deseamos que nuestra clase Estudiante herede de otra clase. Digamos que alguien implementó una clase con un conjunto de propiedades que nos vienen muy a propósito y deseamos utilizar las ventajas de la herencia para poder echar mano de estos métodos y atributos. Lamentablemente, no podremos hacerlo y es que Java no soporta la herencia múltiple sino la herencia simple (single inheritance), esto es, una clase sólo puede tener un pariente.

Si pensamos en la herencia tal como estamos acostumbrados, esto podrá parecernos un poco raro. Sin embargo, esto se hace para evitar tener que idear algún tipo de regla para decidir qué propiedad hereda la clase hija en caso de que las clases padres compartan un método con igual firma pero diferente implementación.

Sin embargo, si esto es lo que deseamos podemos crear una interfaz. Las interfaces se escapan al modelo de la herencia, pero una clase puede implementar más de una interfaz. Podríamos decir que hoy en día es más probable que un desarrollador Java realice el diseño de su problema de dominio partiendo de un conjunto de interfaces.

En una interfaz no se suelen colocar atributos, a menos que sean constantes que declaramos como static y final. Al hacerlas estáticas podemos invocarlas sin tener que generar ninguna instancia de un objeto (y es que, tal como una clase abstracta, no podemos instanciar una interfaz) y al hacerlas finales no podemos modificar su valor (e.g. public static final double PI = 3.14;) .

Las interfaces contienen un conjunto de declaraciones de métodos que las clases que la implementen deberán implementar, valga la redundancia (de manera similar en las clases abstractas se pueden declarar métodos abstractos --e.g. public abstract String hacerTalCosa();--). También es posible escribir métodos con su respectiva implementación, pero para que sean válidos habremos de utilizar la palabra restringida default y, así, las clases que implementan esta interfaz podrán invocarlo directamente sin tener que implementarlo.

Cada miembro de la comunidad universitaria tiene siempre que inscribirse en cursos, semestres, eventos, etc. La idea de inscripción es una constante. Dado que cada clase gestionaría su inscripción de forma diferente, hagamos de ella una interfaz. Nuestro método devolverá un número de trámite y recibirá como argumento una lista de recaudos. Adicionalmente, queremos que este método sólo pueda ser implementado por un objeto que extienda la clase MiembroDeLaComunidadUniversitaria:

public interface Inscripcion{
  <T extends MiembroDeLaComunidadUniversitaria> int inscribirse(List<String> recaudos);
}
0.1s

Noten que en la declaración del método inscribirse() hemos usado lo que en Java se llama un génerico. Esto ya lo hemos visto, pero no le habíamos dado nombre. Cuando escribimos un tipo de dato entre <> estamos usando un genérico. Y en este caso concreto, estamos introduciendo una letra que no representa ningún tipo en específico sino que, palabras más palabras menos, le indica al compilador: "aquí puede ir cualquier objeto siempre y cuando herede de la clase MiembroDeLaComunidadUniversitaria". Después colocamos el tipo de retorno int, luego el nombre del método.

Ahora creemos una clase que implemente esta interfaz:


public class PersonalAdministrativo extends MiembroDeLaComunidadUniversitaria implements Inscripcion {
  private String cargo;
  private int antiguedad;
  private String departamento;
  
  public PersonalAdministrativo(String nombre, int dni, int nroCarnet, AdscripcionAdministrativa adscripcion_admin, String cargo, int antiguedad, String departamento){
    this.nombre=nombre;
    this.dni=dni;
    this.nroCarnet=dni;
    this.adscripcion_admin=adscripcion_admin;
    this.cargo=cargo;
    this.antiguedad=antiguedad;
    this.departamento=departamento;
  }
public <PersonalAdministrativo extends MiembroDeLaComunidadUniversitaria> int inscribirse(List<String> recaudos){
  System.out.println("Recaudos obtenidos: "+recaudos);
    return 12133; //Imaginen que aquí insertamos una lógica de negocio para que el personal administrativo se inscriba dónde y cómo ustedes se figuren: el sindicato, la biblioteca, etc.
  }
}
0.2s

¡Bárbaro! Fíjense que en una clase podemos heredar de una clase determinada e implementar una interfaz al mismo tiempo. Recuerden también que podemos implementar cuántas interfaces querramos.

PersonalAdministrativo pa = new PersonalAdministrativo("Julian Perez", 21323,2313322,AdscripcionAdministrativa.FACULTAD_DE_INGENIERIA, "Secretario", 14, "Control de Estudios");
pa.inscribirse(Arrays.asList("DNI","Partida de Nacimiento", "Autógrafo de Maradona", "Autógrafo del Papa", "Tierra de TierraSanta", "Y mucho más"));
3.2s

Pues bien, vemos que podemos crear un objeto de la clase respectiva normalmente y que el método que hemos implementado de la interfaz también funciona correctamente.

----------------------------------------------------------------------------------

En Scala, siendo un lenguaje hospedado en la JVM de Java, tenemos a disposición todas las abstracciones que discutimos arriba. Pero también disponemos de otras que nos ofrece Scala en particular.

Una de ellas son los llamados Traits. Bajo cuerdas un Trait no es otra cosa que una interfaz de Java, sin embargo, los diseñadores de Scala se han servido de la herencia de interfaces de tal manera que sea posible que el desarrollador en Scala puede utilizar la herencia múltiple, esto es, que una clase tenga dos o más parientes.

En Scala los traits son más usados que las clases abstractas (las cuales se usan más que nada cuando deseamos crear una clase base con un constructor parametrizado -ni las interfaces Java, ni los traits de Scala llevan constructor con parámetros- ó cuando nuestro código Scala va a ser llamado desde código Java), así que empleemos un Trait para representar nuestro MiembroDeLaComunidadUniversitaria.

Recuerden que para nuestra clase necesitábamos un enumerable para la adscripción administrativa. En Scala se pueden usar enumerables de forma muy similar a Java, pero (al menos en Scala 2) lo más común es usar un sealed trait:

sealed trait AdscripcionAdministrativa
  case object FACULTAD_DE_CIENCIAS_JURIDICAS_Y_POLITICAS extends AdscripcionAdministrativa
  case object FACULTAD_DE_INGENIERIA extends AdscripcionAdministrativa
  case object FACULTAD_DE_CIENCIAS_ACTUARIALES extends AdscripcionAdministrativa
  case object FACULTAD_DE_HUMANIDADES extends AdscripcionAdministrativa
  case object FACULTAD_DE_CIENCIAS_NATURALES extends AdscripcionAdministrativa
  case object DEPENDENCIA_DE_SERVICIOS extends AdscripcionAdministrativa
  case object SEGURIDAD extends AdscripcionAdministrativa
  case object BIBLIOTECA extends AdscripcionAdministrativa
10.4s
trait MiembroDeLaComunidadUniversitaria{
  var nombre: String
  var dni: Int 
  var nroCarnet: Int 
  var adscripcion_admin: AdscripcionAdministrativa
}
0.9s
sealed trait Estatus
  case object ACTIVO extends Estatus
  case object INACTIVO extends Estatus
  case object SUSPENDIDO extends Estatus
  case object EGRESADO extends Estatus
sealed trait EstatusMateria
  case object INSCRITA extends EstatusMateria
  case object APROBADA extends EstatusMateria
  case object REPROBADA extends EstatusMateria
  
class Materia(var carrera: String,var nombre: String,var periodo_lectivo: String,var codigo: Int, var prelaciones: Vector[Int])
  
class Estudiante(var carrera: String, var estatus: Estatus,var materias: Map[EstatusMateria, List[Materia]],var record_academico: Double, var actv_depor_y_extn: List[String]) extends MiembroDeLaComunidadUniversitaria
0.9s

¡Tenemos una excepción! La excepción nos dice que deberíamos hacer de la clase Estudiante una clase abstracta o, por el contrario, debemos implementar los 4 miembros de la clase de la que hereda. Pues bien, hagamos una de las dos, ¿no? ¡No tan rápido! Detengámos un momento. ¿Conviene declarar un trait sólo con un conjunto campos? No estamos describiendo ninguna conducta con esto, ningún rasgo. ¿Qué sentido tiene sobreescribir aquellos atributos cuando ni siquiera se les ha asignado un valor? No son ninguna clase de constantes.

Vale destacar que no es recomendable sencillamente trasladar un modelo que diseñamos en un lenguaje a otro lenguaje así sin más. Y es que de manera análoga a como en las ciencias sociales estimamos conveniente estudiar un problema mirándolo a través del lente de distintas teorías, cada lenguaje nos enfrenta a la situación de tener que replantearnos ciertas ideas o asunciones; cada lenguaje nos ofrece una manera de mirar al mundo.

¡Vale! Entonces pensemos en lo siguiente: ¿cuáles son los rasgos de los miembros de la comunidad universitaria? ¿Cuáles serían aquellas categorías que, como diagramas de Venn, convergen y se solapan cuando de un miembro de la comunidad universitaria se trata?

Podríamos decir que todo miembro de la comunidad universitaria tiene una identidad que le permite reconocerse y ser reconocido, que permite que la organización regule su acceso y participación, en fin, su membresía misma. También posee una o más adscripciones, entre las que puede cambiar en cualquier momento. Cada miembro detenta un tipo de relación con la universidad, bien sea laboral o como beneficiario o cliente de los servicios que la universidad presta. Finalmente, los miembros de la comunidad universitaria participan en una o muchas actividades a lo largo de su relación con la universidad.

Traslademos estas ideas a código:

trait Identidad{
  def obtenerCarnet(nombre: String, apellido: String, dni: Int) : Int
  def identificarse() : List[String]
}
trait Adscripcion[A]{
  def inscribirse(nombre: String, apellido: String, nroCarnet: Int, entidad: A) : Int
    
}
trait Actividad[B]{
  def inscribirse(nombre: String, apellido: String, nroCarnet: Int, actividad: B) : Int
  def crearActividad(): B
}
sealed trait TipoRelacion
  case object LABORAL extends TipoRelacion
  case object CLIENTE_BENEFICIARIO extends TipoRelacion
  
sealed trait AdscripcionAdministrativa
  case object FACULTAD_DE_CIENCIAS_JURIDICAS_Y_POLITICAS extends AdscripcionAdministrativa
  case object FACULTAD_DE_INGENIERIA extends AdscripcionAdministrativa
  case object FACULTAD_DE_CIENCIAS_ACTUARIALES extends AdscripcionAdministrativa
  case object FACULTAD_DE_HUMANIDADES extends AdscripcionAdministrativa
  case object FACULTAD_DE_CIENCIAS_NATURALES extends AdscripcionAdministrativa
  case object DEPENDENCIA_DE_SERVICIOS extends AdscripcionAdministrativa
  case object SEGURIDAD extends AdscripcionAdministrativa
  case object BIBLIOTECA extends AdscripcionAdministrativa
0.8s

Noten que en cada trait definimos métodos abstractos que deben ser implementados por las clases que hereden de estos rasgos. Habrán notado que los traits Adscripcion y Actividad contienen métodos homónimos, por lo que habrá un conflicto a la hora que una clase herede de ambos traits. Scala resuelve estos conflictos de forma automática de la siguiente manera: se hará una búsqueda del tipo Depth First Search (es decir, una búsqueda que atraviesa un árbol en profundidad) partiendo siempre de la derecha. De este modo, el rasgo más a la derecha será el que se trasmitirá al heredero. Sin embargo, con los generics tal como los hemos programado Scala no resolverá el conflicto. La alternativa sería quitar los genéricos. Hagamos ambas cosas.

Ahora vamos a crear la clase Estudiante que implementará todos estos rasgos, pero antes creemos la clase materia que heredará de actividad:

class Materia(var nombre: String,var periodo_lectivo:String,var adscripcion: AdscripcionAdministrativa) extends Actividad[String]{
   def inscribirse(nombre: String, apellido: String, nroCarnet: Int, actividad: String): Int = {
     val r = scala.util.Random
     r.nextInt
     return r.asInstanceOf[Int] //Con esto sencillamente devolvemos un número aleatorio y lo casteamos a Int
   }
   def crearActividad(): String  = {
     return nombre
   }
}
0.4s
class Estudiante(var nombre: String,var apellido: String,var dni: Int) extends Identidad with Adscripcion[AdscripcionAdministrativa] with Actividad[Materia]{
  var tiporelacion: TipoRelacion = CLIENTE_BENEFICIARIO
    
  override def obtenerCarnet(nombre: String, apellido: String, dni: Int) : Int = {
    return scala.util.Random.nextInt.asInstanceOf[Int]
  }
 override def identificarse() : List[String] = {
    return List(nombre, apellido, dni.asInstanceOf[String])
  }
  override def inscribirse(nombre: String, apellido: String, dni: Int, entidad: AdscripcionAdministrativa) : Int = {
    return scala.util.Random.nextInt.asInstanceOf[Int]
  }
  override def inscribirse(nombre: String, apellido: String, nroCarnet: Int, actividad: Materia) : Int = {
    return scala.util.Random.nextInt.asInstanceOf[Int]
  }
 override def crearActividad(): Materia = {
    var materia = new Materia("Calculo I", "2022-II", FACULTAD_DE_INGENIERIA)
    return materia
  }
}
0.5s
var estudent = new Estudiante("Felipe", "Villalba", 12123)
0.4s

Tal y como anticipábamos Scala nos arroja una excepción. Concretamente nos indica que tenemos un método duplicado. Si intentáramos quitar alguno, nos obligaría a sobreescribirlo o a declarar abstracta a nuestra clase.

Ahora probemos sin los genéricos. Y aprovechemos para hacer un par de modificaciones: a) si lo pensamos no tiene mucho sentido parametrizar el método obtenerCarnet(), así que quitémosle los parámetros y quitemos también de ambas variantes de inscribirse() los datos personales, ya que se los puede pasar el objeto directamente en la implementación; y b) no podemos devolver una lista de Strings en con el método identificarse() porque dni es entero y no podemos convertir un entorno a String tan sencillamente en Scala, así que hagamos que devuelva lo que se conoce como una tupla (Tuple) de String, String e Int:

trait Identidad{
  def obtenerCarnet() : Int
  def identificarse() : (String, String, Int)
}
trait Adscripcion{
  def inscribirse(entidad: String) : Int
    
}
trait Actividad{
  def inscribirse(actividad: String) : Int
  def crearActividad(): String
}
sealed trait TipoRelacion
  case object LABORAL extends TipoRelacion
  case object CLIENTE_BENEFICIARIO extends TipoRelacion
  
sealed trait AdscripcionAdministrativa
  case object FACULTAD_DE_CIENCIAS_JURIDICAS_Y_POLITICAS extends AdscripcionAdministrativa
  case object FACULTAD_DE_INGENIERIA extends AdscripcionAdministrativa
  case object FACULTAD_DE_CIENCIAS_ACTUARIALES extends AdscripcionAdministrativa
  case object FACULTAD_DE_HUMANIDADES extends AdscripcionAdministrativa
  case object FACULTAD_DE_CIENCIAS_NATURALES extends AdscripcionAdministrativa
  case object DEPENDENCIA_DE_SERVICIOS extends AdscripcionAdministrativa
  case object SEGURIDAD extends AdscripcionAdministrativa
  case object BIBLIOTECA extends AdscripcionAdministrativa
  
class Materia(var nombre: String,var periodo_lectivo:String,var adscripcion: AdscripcionAdministrativa) extends Actividad{
   def inscribirse(actividad: String): Int = {
     val r = scala.util.Random
     r.nextInt
     return r.asInstanceOf[Int]
   }
   def crearActividad(): String  = {
     return nombre
   }
}
  
class Estudiante(var nombre: String,var apellido: String,var dni: Int) extends Identidad with Adscripcion with Actividad{
  var tiporelacion: TipoRelacion = CLIENTE_BENEFICIARIO
    
  override def obtenerCarnet() : Int = {
    return scala.util.Random.nextInt.asInstanceOf[Int]
  }
  override def identificarse() : (String, String, Int) = {
    return (nombre, apellido, dni)
  }
  override def inscribirse(actividad: String) : Int = {
    return scala.util.Random.nextInt.asInstanceOf[Int]
  }
  override def crearActividad(): String = {
    var materia = new Materia("Calculo I", "2022-II", FACULTAD_DE_INGENIERIA)
    return materia.nombre
  }
}
0.6s
var studente = new Estudiante("Felipe", "Villalba", 12123)
0.8s
studente.inscribirse("Artes")
0.4s

Ahora sí compila.

studente.nombre
0.5s
studente.obtenerCarnet()
0.4s
studente.identificarse()
0.4s

Un último punto sobre Scala: supongamos que no deseamos crear una clase como la de arriba que herede de determinados rasgos, sino que deseamos que sea sólo un objeto de una clase determinada la que herede ciertos rasgos. Pues bien, Scala te permite declarar que un objeto hereda de determinados rasgos al declararlo. Veamos:

Primero creemos una clase, digamos Persona (noten que si declaramos los campos en el constructor de la clase con val en vez de var, el estado de nuestro objeto se hará inmutable)

class Persona(var nombre_completo: String, var intereses: String)
0.3s
var p = new Persona("Juan Valverde", "Muchos") with Identidad with Adscripcion
0.7s

¡Ups! No puede ser un trait cualquiera porque nuestra clase tiene que implementar los métodos de los respectivos traits. Esto podría ser útil cuando el trait contiene sólo valores constantes y/o métodos defaults. Ejemplo:

trait RasgoX {
  val valorConstante: Int = 1223
  def hacerUnCalculo(): String = "Haciendo el cálculo"
 }   
0.3s
val p = new Persona("Juan Valverde", "Muchos") with RasgoX
p.hacerUnCalculo()
0.4s
p.valorConstante
0.4s

¡Genial! Esta propiedad es muy poderosa.

----------------------------------------------------------------------------------------

La herencia en Python, por lo general, se parece mucho a lo que hemos visto hasta ahora. Sin embargo, como cada lenguaje, Python tiene sus particularidades. En primer lugar, tiene la singularidad de que admite la herencia múltiple. Y en segundo lugar, tiene lo que se denomina virtual base class que es una forma de herencia dinámica si se quiere.

Comencemos con nuestro modelado.

También en Python podemos hacer de la clase MiembroDeLaComunidadUniversitaria una clase abstracta. Para declarar una clase abstracta debemos importar del modulo abc (Abstract Base Classes) la clase ABC, una clase auxiliar para generar clases abstractas. De igual modo, debemos importar abstractmethod, el cual utilizaremos como decorador para marcar un método como abstracto.

Sin embargo, a diferencia de Java, no podemos declarar campos en Python si no es en el constructor, y dado que las clases abstractas no se pueden instanciar, no podemos crear un constructor. Tampoco tendría sentido colocarlas como constantes, de modo que nos toca pensar qué método significativo podría tener nuestra clase.

Tal y como lo hicimos con Scala tenemos que pensar en Python. ¿Qué métodos serían comunes a todos los miembros de la comunidad universitaria? Si lo vemos en términos estrictamente de membresía, resulta fundamental la forma y manera en la que alguien se hace miembro y las condiciones y el momento en el que deja de serlo. Así que hagamos de inscribirse() y renunciar() los dos métodos abstractos de nuestra clase:

from abc import ABC, abstractmethod
class MiembroDeLaComunidadUniversitaria(ABC):
  @abstractmethod
  def inscribirse(self):
    pass
  
  @abstractmethod
  def renunciar(self):
    pass
0.0s

Es una particularidad de Python que el método abstracto no pueda quedar sin implementación. Por eso utilizamos la palabra reservada pass, para indicarle al intérprete que todo está bien, que siga adelante. Noten que indicamos la herencia colocando la clase padre entre paréntesis justo al lado del nombre de la nueva clase.

Sigamos.

class Estudiante(MiembroDeLaComunidadUniversitaria):
  def __init__(self, nombre, apellido, dni):
    self.nombre=nombre
    self.apellido=apellido
    self.dni=dni
    
  def inscribirse(self, facultad):
    return "Inscripción completada en la "+facultad
  
  def renunciar(self):
    return nombre+" "+apellido+" ha terminado la relación con la universidad"
  
0.0s

Acá hemos creado nuestra clase Estudiante y no sólo hemos implementado los métodos abstractos, sino que también hemos sobrecargado (sobrecargar un método significa cambiar su firma al agregarle o quitarle parámetros) al método inscribirse() -noten que no recibía parámetros y lo hicimos recibir uno, facultad.

Ahora probemos:

est = Estudiante("Pedro", "Perez", 333221)
0.0s
est.inscribirse("Facultad de Derecho")
0.1s
issubclass(Estudiante, MiembroDeLaComunidadUniversitaria)
0.0s

En Python también podemos crear interfaces, sin embargo, la distinción entre interfaz y clase abstracta dependerá del programador, de sus intenciones y de cómo las haga explícitas en la documentación, y es que formalmente Python sólo dispone de clases abstractas.

Sin embargo, si queremos obtener algo parecido a las interfaces de Java podríamos hacer lo siguiente:

import abc
class MiembroDeLaComunidadUniversitaria(metaclass=abc.ABCMeta):
  @classmethod
  def __subclasshook__(cls, subclass):
    return(hasattr(subclass, 'inscribirse') and
          callable(subclass.inscribirse) and
          hasattr(subclass, 'registrarse') and
          callable(subclass.registrarse))
  
  @abc.abstractmethod
  def inscribirse(self):
    raise NotImplementedError
  
  @abc.abstractmethod
  def registrarse(self):
    raise NotImplementedError
0.0s

¡Bien! Revisemos qué hemos hecho acá. Primero notarán que la declaración de importación, la forma en que referimos la clase padre y el uso decorador son distintos. En efecto, sin embargo, no existe ninguna diferencia semántica con respecto a lo que hicimos más arriba. La verdadera diferencia está en el método __subclasshook__( ) y en enunciado raise dentro de cada método abstracto.

__subclasshook __( ) es un método interno de Python que al sobreescribirlo nos permite establecer un contrato, por decirlo así, con las clases que implementen esta <interfaz>. Y el contrato no es otro, como en otros lenguajes, que la clase implemente todos los métodos abstractos (si no deseamos esta conducta no sobreescribimos este método y nuestra clase se comportará más como una clase abstracta).

Por otra parte, el enunciado raise NotImplementedError es un complemento de la anterior, ya que justamente este va a ser el error que veremos si olvidamos implementar alguno de los métodos abstractos declarados.

Para probar vamos a crear una clase abstracta que heredará de nuestra interfaz, pero le agregará un constructor que nos será de mucha utilidad cuando deseemos crear instancias de nuestro objeto:

class ProfesorImpl(MiembroDeLaComunidadUniversitaria):
  def __init__(self, nombre, apellido, especialidad, materia):
    self.nombre=nombre
    self.apellido=apellido
    self.especialidad=especialidad
    self.materia=materia
    
    def inscribirse(self):
      pass
    
    def registrarse(self):
      pass
0.0s
class Profesor(ProfesorImpl):
  def __init__(self, nombre, apellido, especialidad, materia):
    super().__init__(nombre, apellido, especialidad, materia)
  
  def inscribirse(self):
      return "El profesor "+self.apellido+" ha completado su inscripción satisfactoriamente"
    
  def registrarse(self):
      return "El experto con la especialidad "+self.especialidad+" se ha registrado exitosamente en el evento"
0.0s
prof = Profesor("Juan", "Serrano", "Teoría de Cuerdas", "Física")
0.0s
prof.inscribirse()
0.0s
prof.registrarse()
0.1s

Si creamos una clase que no implemente ambos métodos abstractos obtendremos una excepción. Pero está permitido sobrecargar un método:

class EmpleadoAdministrativo(MiembroDeLaComunidadUniversitaria):
  def registrarse(self, nombre, actividad):
    return nombre+" se ha registrado satisfactoriamente en la actividad "+actividad
  
  def inscribirse(self):
    return "Inscripción completada"
0.0s
empleado = EmpleadoAdministrativo()
0.0s
empleado.inscribirse()
0.0s
empleado.registrarse("Juan", "Boxeo")
0.0s

También tenemos las denominadas Virtual Base Classes. Si nos tomamos a pecho la metaforología de la herencia, una clase base virtual es una especie de hijo bastardo. Si una clase implementa y/o sobreescribe los métodos abstractos de otra, se convierte en una heredera virtual de ésta y aquella se convierte en su clase base virtual.

En Python el orden en que se llaman los métodos en un árbol genealógico de clases se guarda en lo que se conoce como el MRO (method resolution order). Una clase virtual no aparece en este registro, por eso, aquí entre nos, la llamamos bastarda.

Ahora bien, nosotros implementamos la interfaz de la manera formal; y al hacerlo así, si queremos crear una subclase virtual debemos registrarla.

class Vigilante:
  def inscribirse(self):
    pass
  def registrarse(self):
    pass
 
MiembroDeLaComunidadUniversitaria.register(Vigilante)
0.0s
issubclass(Vigilante, MiembroDeLaComunidadUniversitaria)
0.0s
vig = Vigilante()
0.0s
isinstance(vig, MiembroDeLaComunidadUniversitaria)
0.0s

En efecto, vemos que Vigilante es subclase de MiembroDeLaComunidadUniversitaria y que el objeto que creamos de él es considerado una instancia de la interfaz en cuestión.

Bien puede parecerles que el tema de la herencia en Python está lleno de sutilezas. Si desean leer más sobre el tema de las interfaces y las clases abstractas, visiten estos vínculos 1 y 2.

----------------------------------------------------------------------------------------

En Julia no existe la herencia propiamente dicha, y es que sus diseñadores han preferido las ventajas que da la composición por encima de la herencia. Podemos distinguir entre composición y herencia asociando cada concepto a un tipo de relación. La composición define relaciones del tipo tiene un, mientras que la herencia define relaciones del tipo es un. La herencia es una genealogía de tipos de datos, tanto nativos como creados por el usuario, mientras que la composición apunta a la definición de relaciones entre objetos.

Por ejemplo, cuando decimos que la clase Ruiseñor hereda de la clase Ave, estamos diciendo que el Ruiseñor es un Ave. Distinto es cuando decimos que un objeto de la clase Materia tiene uno o más Estudiantes. Ambos objetos están relacionados por el hecho que las materias son cursadas por los estudiantes. Esta idea no se puede traducir correctamente en el lenguaje de la herencia. En algunos círculos de la ciencia de la computación se tiene mayor aprecio por la composición dada su versatilidad, aunque también es cierto que muchos ven a la herencia y a la composición como complementarios.

Julia nos brinda herramientas para componer objetos o structs, así como también herramientas para aplicar polimorfismo (un tema que veremos en la próxima lección), de tal modo que podemos definir una conducta y crear contextos en los que esta conducta, inicialmente abstracta, cobra significados específicos. En este orden de ideas, Julia se precia de ser un lenguaje apuntalado sobre el concepto de multiple dispatch, el cual no es otra cosa que una forma especializada de polimorfismo. Pero, como dijimos, más sobre esto en la próxima lección.

Aunque existen librerías que te permiten usar el lenguaje de clases y herencia en Julia, el lenguaje crudo te ofrece lo que denomina interfaces informales. Esto no es otra cosa que un conjunto de funciones (principalmente, en el modulo Base) que un struct cualquiera puede sobreescribir en sus funciones constructoras. Con esto, al implementar esas funciones, el struct participa de la conducta general que define la interface. Sin embargo, Julia no tiene un palabra reservada para las interfaces y no puedes crear interfaces customizadas de la forma en que lo hemos venido haciendo con otros lenguajes. Básicamente sólo se pueden extender de Iteration y Abstract Array.

En Julia podemos realizar lo que se denomina subtipado, a saber, el marcado de un símbolo como supeditado a otro símbolo. En este orden de ideas, se puede realizar algo muy parecido a lo que sería una estructura de clases en Java, aunque quedándose sólo con los nombres de las clases.

Para poder subtipar en Julia debemos crear uno o más abstract types, los cuales vendrán a ser la cúspide de la estructura o grafo de tipos. El grafo interno de Julia, por ejemplo, tiene como tipo tope o superior a Any: todo objeto en Julia es una instancia de Any, así como todo tipo es un subtipo del mismo. En el fondo del grafo de tipos se encuentra Union{}, del cual ningún objeto es instancia y del que todos los tipos son super-tipos.

Hagamos un ejemplo:

abstract type MiembroDeLaComunidadUniversitaria end
0.3s

Crear un tipo abstracto es tan simple como esto. Pero, atención, no estamos haciendo nada parecido a lo que hacíamos en Java, Python y Scala, a no ser por el simple hecho de que hemos definido un tipo al cual otros se remitirán como su padre o ancestro.

Ahora creemos un struct, el cual recordemos en Julia es un tipo compuesto, que extienda a nuestro tipo abstracto (por cierto, así como en Java se suele trabajar en clases, es recomendable en Julia trabajar dentro de módulos) y creemos, a través de la composición una pequeña estructura que represente nuestro problema de dominio.

module universidad
export Alumno, Materia, Profesor
abstract type MiembroDeLaComunidadUniversitaria end
struct Alumno <: MiembroDeLaComunidadUniversitaria
  nombre::String
  apellido::String
  dni::Int64
Alumno(nombre,apellido,dni) = new(nombre,apellido,dni)
end
struct Materia 
  codigo::Int
  titulo::String
  carrera::String
  ano_semestre::String
  prelacion::Array{Int,1}
  alumnos::Array{Alumno,1}
  Materia(codigo, titulo, carrera, ano_semestre, prelacion,alumnos) = new(codigo, titulo, carrera, ano_semestre, prelacion, alumnos)
end
struct Profesor <: MiembroDeLaComunidadUniversitaria
  nombre::String
  apellido::String
  materia::Materia
  Profesor(nombre,apellido, materia)=new(nombre,apellido, materia)
end
end
0.4s
est = universidad.Alumno("Juan", "Moreno", 1231923)
est1 = universidad.Alumno("Juana", "Matanza", 1331223)
est2 = universidad.Alumno("Fabiola", "Lopez", 90231923)
est3 = universidad.Alumno("Juan", "Manzano", 1231923)
est4 = universidad.Alumno("Jim", "Marquez", 9233232)
alumnos = [est,est1,est2,est3,est4]
m = universidad.Materia(1000, "Matematicas", "Ingenieria", "I", [900, 901], alumnos)
pr = universidad.Profesor("Paco", "Puebla", m)
0.6s

Intenten agregar un nuevo struct dentro del módulo usando composición, vuélvanlo a ejecutar y creen un objeto de ese tipo acá abajo

0.2s

----------------------------------------------------------------------------------------

El caso de Racket es especial, ya que en este lenguaje nos encontramos con todas las abstracciones que hemos visto hasta acá y mucho más. Se suele considerar a los lenguajes de la familia LISP como puramente funcionales, sin embargo, esto no es del todo cierto. Lenguajes como Common Lisp y Racket tienen complejos sistemas de objetos que cualquier lenguaje tradicionalmente orientado a objetos envidiaría.

En Racket tenemos structs, clases, interfaces, mixins y traits. A continuación vamos ver cómo funcionan cada una de estas abstracciones en Racket.

Comencemos por definir nuestra clase base, como lo hemos venido haciendo con cierta consistencia hasta ahora.

Nota sobre la ejecución del código Racket en las celdas:

El código arroja algunos mensajes de advertencia, sin embargo, parece dar el código por bueno. Esto indica que hay algo que no va bien en la evaluación del código en estas celdas. Por ende, les recomiendo que corran este código localmente en sus máquinas, para lo cual puede descargarse Racket y abrir el REPL en DrRacket.

(define miembros-de-la-comunidad-universitaria%
  (class object%
    (init-field nombre dni ano-ingreso)
    (super-new)
    (define/public (inscribirse)
      (list nombre dni ano-ingreso))))
0.4s

En Racket, por convención, las clases se nombre con un signo de porcentaja al final (%). En Lisp los paréntesis son como las capas de una cebolla. Acá la capa más externa sencillamente otorga un nombre a la expresión más interna. Seguidamente tenemos a la expresión class (class* en caso que nuestra clase vaya a implementar una interfaz). Una expresión de clase está compuesta por: 1) la palabra reservada class; 2) la superclase de la cual ésta hereda (vemos en este caso la referencia a object% ya que esta es la clase de las cuales todas las clases heredan, como Any en Julia y Scala u Object en Java); 3) los campos de la clase (init-field ...); 4) la referencia a la inicialización de la superclase (super-new); y 5) cualquier cantidad de métodos y constantes que deseemos agregar.

En este caso para nuestra clase miembros-de-la-comunidad-universitaria% hemos creado 3 campos, a saber, nombre, dni y ano-ingreso los cuales deberán ser iniciados no sólo por cada instancia de esta clase sino también por las que hereden de ella. A continuación definimos un método público que sencillamente devuelve una lista con los datos del miembro.

Ahora definamos algunas clases que hereden de ésta:

(define profesor% (class miembros-de-la-comunidad-universitaria%
                    (super-new)
                    (init-field materia titulo)))
(define profesor-agregado% (class profesor%
                            (inherit-field) ; indica que heredamos los campos de la superclase
                            (init-field antiguedad)
                            (super-new)))
0.4s

Podríamos seguir y crear la clase estudiante, sin embargo, detengámonos por un momento. Y ¿si pensamos al estudiante como el gerundio que la etimología misma de la palabra nos sugiere? y ¿si además lo desontologizamos y lo desvinculamos de los atributos que normalmente van unidos a una persona? Capaz así podríamos ver en el estudiante una interfaz con un conjunto de métodos o funciones (expresión que, como hemos visto, varía de lenguaje en lenguaje), tales como cursar, calificar (aprobar o reprobar) y darse de baja.

Creemos entonces la interfaz alumno:

(define alumno-interface<%>
  (interface () cursar calificar darse-de-baja))
0.6s

Bien, en Racket, crear una interfaz es tan simple como crear una definición de una expresión interface cuyos argumentos son, primero, las interfaces de las que la presente va a heredar (en este caso ninguna, por eso escribimos () ) y, después, el nombre de todos los métodos que deseemos que compongan esta interfaz. Acá están como podemos ver: cursar, calificar y darse-de-baja. Ahora creemos la clase estudiante:

(define estudiante%
  (class* miembros-de-la-comunidad-universitaria% (alumno-interface<%>)
                     (super-new)
                     (define/public (cursar materia)
                       (displayln (string-append "Se ha generado la inscripción
para la materia " materia)))
                      (define/public (calificar materia nota)
                        (if (> nota 5)
                            (string-append "Se ha aprobado la materia "
                                           materia
                                           " con una calificación de "
                                           (~a nota))
                            (string-append "Se ha reprobado la materia "
                                           materia
                                           " con una calificación de "
                                           (~a nota))))
                      (define/public (darse-de-baja materia)
                        "Baja exitosa")))
0.4s

Notemos que en la expresión clase estamos agregando un asterisco para indicar que nuestra clase implementa una interfaz. Seguidamente indicamos que nuestra clase hereda de miembros-de-la-comunidad-universitaria% y posteriormente indicamos que implementa la interfaz alumno-interface<%> (el <%> al final del nombre es también una convención para nombrar interfaces). Finalmente, implementamos los 3 métodos especificados en nuestra interfaz.

Creemos algunas instancias de las clases que hemos creado.

(define juan (make-object profesor%
               "Derecho"
               "Doctor en ciencias jurídicas"
               "Juan Mariana"
               1249349
               1980))
(define marcos (make-object profesor-agregado%
                 10
                 "Geografia"
                 "Master en Geografía Política"
                 "Marcos Porras"
                 122323
                 2012))
(define fernando (new profesor-agregado%
                      [dni 13133]
                      [nombre "Fernando Kim"]
                      [materia "Historia"]
                      [titulo "Doctor en Ciencias Históricas"]
                      [antiguedad 12]
                      [ano-ingreso 2010]))
(define pedrito (new oyente%
                     [nombre "Pedro Falaz"]
                     [intereses "Tecnología"]
                     [dni 134132312]
                     [ano-ingreso 2021]))
(define pedro (new estudiante%
                   [nombre "Pedro Martinez"]
                   [dni 213132]
                   [ano-ingreso 2020]))
(define martin (make-object estudiante%
                 "Martin Parejo"
                  131243
                  2015))
(define rigoberto (instantiate estudiante%
                    ("Rigoberto Martinez"
                     3314134331
                     2019)))
0.5s

En Racket tenemos tres formas alternativas de instanciar una clase o de crear un objeto. Todo depende de si queremos introducir los valores de los campos en un orden posicional o si queremos introducirlos en pares [campo valor]. De modo que tenemos la formas make-object, new e instantiate.

Ahora creemos otra interfaz y un par de clases que nos permitirán representar un conjunto de abstracciones relacionadas a la administración universitaria.

(define control-de-estudios-interface<%>
  (interface () matricular))  
(define carrera%
  (class object%
    (init-field nombre)
    (define pensum '())
    (super-new)
    (define/public (actualizar-pensum materias)
      (set! pensum (append pensum materias)))
    (define/public (imprimir-pensum)
      pensum)))
(define materia%
  (class* carrera% (control-de-estudios-interface<%>)
    (init-field titulo codigo periodo-lectivo)
    (super-new)
    (define lista-alumnos '())
    (define/public (matricular estudiante)
      (set! lista-alumnos (append lista-alumnos estudiante)))
    (define/public (imprimir-listado)
      lista-alumnos)))
0.4s

Tenemos entonces la clase carrera que no hereda sino que object%, mientras que la clase materia hereda de carrera e implementa la interfaz control-de-estudios-interface<%>. De nuevo, noten que la clase materia implementa los métodos de la interfaz en cuestión.

Ahora creemos algunas instancias de estos objetos:

(define ciencias_politicas
  (new carrera%
       [nombre "Ciencias Politicas"]))
(send ciencias_politicas actualizar-pensum
      (list "Teoria del Estado"
            "Historia de las ideas"
            "Teoria Politica"
            "Sistemas politicos"
             "Sistemas electorales"
             "Historia de las RRII"
             "Teoria de las RRII"
             "Fundamentos de la Administracion Publica"))
(define teoria_del_estado
  (new materia%
  [titulo "Teoria del estado"]
  [codigo 1431431]
  [periodo-lectivo "2022-I"]
  [nombre "Ciencias Políticas"]))
(send ciencias_politicas imprimir-pensum)
(send ciencias_politicas actualizar-pensum "Teoría de la Organizacion")
(send teoria_del_estado matricular (list pedrito pedro martin rigoberto))
(send teoria_del_estado imprimir-listado)
0.5s

Ahora hablemos de los mixins. Un mixin, dicho con simplicidad, es un objeto que tiene rasgos de diferentes clases las cuales provienen de diferentes jerarquías de clases. Como su nombre lo sugiere, se trata de una mezcla.

En Racket se puede crear un mixin de dos maneras, a saber, como una clase parametrizada con respecto a su clase padre y utilizando el macro mixin. Veamos:

(define bedel%
  (mixin (alumno-interface<%>)
         (control-de-estudios-interface<%>)))
0.6s

Por último tenemos los traits, los cuales se usan en Racket como conjuntos de mixins. Sin embargo, este tema no lo veremos por ahora. No se preocupen si esta parte les ha costado ya que es compleja, puesto que requiere un gran dominio de todos los conceptos que hemos visto hasta acá.

Para concluir con Racket lo importante es tener en cuenta que el lenguaje brinda un conjunto de abstracciones que nos permiten heredar, componer y mezclar métodos y propiedades de objetos de casi cualquier manera. Existe mucho más por explorar, pero por ahora está más que bien.

----------------------------------------------------------------------------------------

Finalmente, hablemos de Clojure. Es importante destacar que la filosofía de este lenguaje antagoniza con ciertos rasgos de la programación orientada a objetos, por ejemplo, la herencia, la mutabilidad de la variables y el modelo de datos que subyace a la idea de encapsulamiento.

Por ende, Clojure no soporta la herencia. Sin embargo, es posible usar la composición como podemos ver en el siguiente ejemplo.

(defrecord Estudiante [nombre dni carrera])
0.1s
(defrecord Materia [Estudiante titulo carrera ano])
0.2s
(def calculo (->Materia est "Calculo" "Economía" 2020))
(def est (->Estudiante "Pedro Gonzalez" 231233 "Economía"))
0.1s
calculo
(get-in calculo [:Estudiante :nombre])
0.1s

No obstante, de ser necesario Clojure te permite crear sistemas de jerarquías. A diferencia de otros lenguajes donde la base de esta jerarquía son clases o structs, en Clojure son nombres (esto es keywords o symbols) y, en este orden de ideas, se parece mucho a la jerarquía de tipos en Julia. Aunque a diferencia de Julia, no existen tipos base y tope predefinidos. Veamos.

::miembro-de-la-comunidad-universitaria
(derive ::estudiante ::miembro-de-la-comunidad-universitaria)
(derive ::profesor ::miembro-de-la-comunidad-universitaria)
(derive ::vigilante ::miembro-de-la-comunidad-universitaria)
(derive ::personal-administrativo ::miembro-de-la-comunidad-universitaria)
::facultad
(derive ::consejo-de-facultad ::facultad)
(derive ::escuela ::facultad)
(derive ::consejo-de-escuela ::facultad)
0.1s
(descendants ::miembro-de-la-comunidad-universitaria)
0.1s
(ancestors ::personal-administrativo)
0.1s
(isa? ::consejo-de-facultad ::facultad)
0.1s
(parents ::consejo-de-facultad)
0.1s
(parents ::estudiante)
0.1s

Una vez que tenemos nuestra jerarquía usamos un multimethod para definir un método abstracto y, en otra expresión, su implementación según los nombres presentes en la jerarquía.

Si bien Clojure no soporta la herencia, existe una idea de la programación orientada a objetos que el diseñador de Clojure, Rich Hickey, consideró que bien valía la pena incluir: el polimorfismo. Y este será el tema de nuestra próxima lección.

Como es usual, crean algunas celdas en su(s) lenguaje(s) favorito(s) y a practicar

0.0s
Runtimes (6)
Runtime Languages (12)