Clases, objetos, funciones e Information Hiding

Hablaba hace unos días en twitter con Pablo Iglesias (el de verdad, no el otro, que ya tuve bastante de políticos al hablar de viejos de 30 años) sobre los mecanismos que existen en C# para controlar la visibilidad de atributos, propiedades y métodos.

A mi siempre me ha resultado curioso que en lenguaje más rígido como C# fuese más complicado (o menos idiomático) crear cosas realmente privadas dentro de una clase, y a Pablo no le gustaba mucho que el encapsulado se hiciese a nivel de clase, en lugar de a nivel de objeto.

Es un tema entretenido para dedicarle un rato y en este post vamos a ver qué nos ofrece C# (o casi cualquier lenguaje basado en clases, todos son muy similares) para ocultar información y cuál es el enfoque en otros lenguajes.

Privacidad con clases

Todos conocemos de sobre los modificadores que existen en C# para controlar la visibilidad de un atributo, propiedad o función: private, protected, internal, protected internal y public. Me voy a ahorrar detallar cómo funciona cada uno, el que tenga dudas al respecto puede encontrar fácilmente esa información en internet.

Vamos a centrarnos en el más restrictivo de todos, private, y vamos a limitar la discusión a atributos (fields) de una clase, aunque el razonamiento sería análogo para propiedades y métodos.

Empezamos con un ejemplo el que tenemos un atributo marcado como private:

public class Person {
  private string name;
  private DateTime birthDate;

  public Person(string name, DateTime birthDate) {
    this.name = name;
	this.birthDate = birthDate;
  }

  public string Name {
    get { return name; }
  }

  public int Age {
    get { return new DateTime(DateTime.Now.Ticks - birthDate.Ticks).Year; }
  }
}

Tal y como está diseñada esta clase, la fecha de nacimiento de una persona sólo es visible para la persona en la que está declarada, ¿verdad? Pues no, es accesible para cualquier objeto de la clase Person. Podríamos tener algo como esto y sería perfectamente válido:

public class Person {
  // ... la declaración de antes

  public bool IsOlderThan(Person other) {
    return this.birthDate < other.birthDate;
  }
}

Desde un objeto podemos toquetear todo lo que queramos a otros objetos de esa clase, independientemente de que declaremos las cosas como public o private. Esto es, como decía, habitual en lenguajes basados en clases y tampoco es algo tan grave, a fin de cuentas la clase Person ya está acoplada al atributo birthDate por lo que no tiene mucho sentido ocultar esa información.

Cuando sí podemos encontrar más problemas es cuando ese atributo es mutable, hay unos invariantes que mantener, y podemos saltarnos el acceso entre objetos de la misma clase, pero eso es algo que, en muchas ocasiones, está motivado por un caso de primitive obsession y se puede resolver introduciendo una clase nueva que encapsule ese atributo y sus invariantes.

Además de esta (cuestionable) violación de privacidad entre objetos de la misma clase, en C# contamos con el uso de Reflection que nos permite hacer cosas incluso peores, como saltarnos la privacidad entre clases, pero no creo que sea equiparable porque en este post estamos hablando de la semántica del lenguaje y Reflection está a otro nivel.

Privacidad con objetos

Javascript es un lenguaje que, para muchos, representa el caos (y no les falta parte de razón). Entre el tipado dinámico, la coerción de tipos, los ámbitos de declaración, el hoisting de variables y el imaginativo uso de this, pocos podrían imaginarse que en Javascript es más fácil (o al menos más idiomático) construir objetos con información realmente privada.

¿Cómo? Con closures, claro:

var Person = function(name, birthDate) {

  this.getName = function() {
    return name;
  };
  
  this.getAge = function() {
    return new Date(new Date() - new Date(birthDate)).getFullYear() - 1970;
  };
};

En la función constructora Person estamos creando un objeto con dos funciones para acceder a él, getName y getAge equivalentes a los getters de la clase Person en C# que veíamos antes, pero no estamos almacenando en ningún momento name y birthDate como atributos de la clase, únicamente estamos capturando sus valores al construir las funciones getName y getAge.

Esto hace que no haya ninguna forma de obtener el valor de birthDate desde ningún otro objeto Person y de hecho ni siquiera sería posible hacerlo desde otras funciones de Person que no hayamos declarado en el constructor, como por ejemplo funciones declaradas a través de Person.prototope:

Person.prototype.someOtherFunc = function() {
  var x = this.birthDate; // ... undefined
}

Por cierto, si queréis profundizar en cómo funcionan las funciones constructoras y los prototipos de Javascript, no dejéis de echarle un ojo a este post de Eduard Tomàs.

Privacidad con funciones

Hasta ahora hemos visto dos alternativas para encapsular información, la de C# basada en clases y la de Javascript basada en objetos, pero ¿podemos encapsular información sin utilizar ni siquiera objetos?

Vamos a ver una forma de hacerlo empleando únicamente funciones y veremos que podemos obtener un nivel de aislamiento similar al de Javascript. Para ello voy a usar clojure por comodidad, pero cualquier lenguaje que soporte closures (como Javascript o C#) valdría. De hecho ésta ni siquiera es la forma más habitual de hacer esto en clojure.

Básicamente lo que tenemos hasta ahora con los otros ejemplos es un API que nos permite:

;; Crear una persona a partir de nombre y fecha de nacimiento
(defn new-person [name birthDate]  ...)

;; Consultar el nombre de una persona
(defn get-name [person]  ...)

;; Consultar la edad en años de una persona
(defn get-age [person]  ...)

Ahora sólo necesitamos implementar esas funciones sin utilizar objetos. A los que conozcan a Alyssa P. Hacker y compañía seguro que les suena la respuesta:

(defn new-person [name birth-date]
  (fn [selector]
    (condp = selector
        :name name
        :age (t/in-years (t/interval birth-date (t/now))))))

(defn get-name [person]
  (person :name))

(defn get-age [person]
  (person :age))

Para representar una persona no usamos un objeto, sino una función que al ser invocada con el parámetro :name devuelve el nombre de la persona y al ser invocada con el parámetro :age devuelve su edad en años.

Los clientes de esta «clase» no necesitan saber nada de esto, sólo conocen el interface formado por new-person, get-name y get-age que les permiten tratar con personas y, al igual que en el ejemplo con Javascript, name y birth-date quedan capturados dentro de un closure pero no son añadidos como atributos a ninguna estructura de datos.

Moraleja

La próxima vez que algún amante de un lenguaje orientados a objetos tradicional (en los tiempos que corren, eso es equivalente a decir Java o C#) defienda que son la mejor forma de encapsular información, recuerda lo que has visto en este post y como otros paradigmas pueden ofrecer mecanismos tan potentes (o incluso más) para conseguir esos objetivos.

En realidad los tres estilos expuestos en este post se pueden implementar en los tres lenguajes usados. Los tres son lenguajes muy flexibles y permiten hacer este tipo de cosas (y otras más raras); la diferencia es que unos dan más facilidades para trabajar de una forma y otros de otra.

Ninguna de las alternativas es intrínsecamente superior a las demás, son sólo formas de diferentes de hacer las cosas, y hay escenarios en que unas resultan más cómodas que otras, pero también hay un componente importante de gustos personales. Como siempre, lo importante es conocerlas y aprender a cuestionarnos lo que damos por sentado.

6 comentarios en “Clases, objetos, funciones e Information Hiding

  1. Yo personalmente nunca me había planteado el tema de la «privacidad» como la imposibilidad de acceder a los valores de los miembros del objeto, sino más bien como una «estructura organizativa»… por lo que la privacidad a nivel de clase de C# (o de C++, que salvo por internal lleva la misma) me parece más que adecuada.

    Pero lo cierto es que esto me viene de haber aprendido OOP con C++ y mi cabeza está bastante asentada en esos conceptos, no es que lo otro tenga nada de malo.

    Delphi tenía (o creo recordar, porque debe hacer 12 o 13 años que no lo toco) un mixto… tenía private, protected y public, que venían a ser como los de C#, pero con la «reflexión» (usando RTTI) no se podía acceder a la información tampoco, y luego tenía el published, que sí que lo permitía. Me pareció una curiosidad en su día, claro que el RTTI en Delphi o C++ era algo que no se solía usar por aquél entonces (más por performance que otra cosa)… pero como digo, incluso con eso, nunca me planteé el acceso como «privacidad para los valores», sino como algo organizativo que tenía más sentido a nivel de clase que de instancia.

    Siempre molan tus posts, porque como poco dan otros puntos de vista… luego el encontrarle la usabilidad y la efectividad, irá por casos (o contexto ;-) ), pero como mínimo te hace expandir un poco la mente, que siempre viene bien.

  2. Un ejemplo de la forma usada en javascript pero en c#:

    public class Person
    {
    private readonly Func getName;

    public Person(string name)
    {
    getName = () => name;
    }

    public string Name => getName() ;
    }

Comentarios cerrados.