Objetos Inmutables: Se Mira y No Se Toca

 La  idea de este post surgió de casualidad, por una discusión que nada que ver -o, en realidad, aparentemente nada que ver- en el Foro de Arquitectura MSDN

El que abría el debate preguntaba si era objetable adoptar como estándar de constructores de clase la llamada al constructor base, es decir el clásico
 

    public class Pepe : Juan
    {
// ... campos y propiedades de la clase


public Pepe() : base() // llamada explícita al constructor por defecto de la clase base { // ... lógica de inicialización de la clase }

// ... sigue la definición de constructores y métodos de la clase
}

 
A él lo motivaba el hecho de que, al explicitar la llamada al constructor base sin parámetros (algo que de todas formas ocurrirá si no se explicita otra llamada a constructores de la clase base, los programadores iban a estar más conscientes de lo que iba a ocurrir entre bambalinas

La discusión siguió y no viene al caso reproducirla (está disponible haciendo click acá si te interesa), pero la parte que yo quiero destacar, la que me pareció seria, es que -aunque el autor no necesariamente afirmaba pensar eso- todas las clases heredables tendrían que incluir el constructor por defecto (así se llama al constructor sin parámetros)

 

A ver, repasemos el concepto: si a una clase A no se le define ningún constructor, por defecto la clase llevará un constructor cuya visibilidad será pública –public– en el caso en que la clase sea instanciable, y protegida –protected– cuando la clase sea abstracta

Este constructor por defecto no recibe parámetros y, en ejecución, sólo llama al constructor sin parámetros de la clase base (que deberá existir para que la clase A compile normalmente)

Por supuesto, esto no correrá si la clase tiene definido algún otro constructor que sí reciba parámetros. En ese caso, el constructor por defecto, para estar disponible, deberá ser explícitamente definido

Más información en la especificación del lenguaje C# (apartado 10.10.4, Constructores por defecto)

 

El problema de que una norma de buena programación conlleve a que todas las clases -al menos las heredables- lleven un constructor por defecto choca de frente con la posibilidad de tener clases inmutables (immutable classes)

Qué son estas clases, para qué podríamos quererlas? Te lo voy a ilustrar con un ejemplo:

Imaginate que estás trabajando en un proyecto grande donde tu equipo se encarga de codificar una parte y otros equipos se hacen cargo de otras partes, etc

Suponete que tu equipo va a generar una API financiera que otros equipos van a usar, pero sin abusar (attenti, ahora te explico). Tu API, a grandes rasgos, consiste en una clase principal llamada Cotizador, con un método Cotizar() de las siguientes características
 

    public class Cotizador
    {
        // ... campos, propiedades y constructores de la clase

        public ICotizacion Cotizar(double monto, 
            DateTime fechaPrestamo, 
            DateTime fechaPrimerPago,
            int meses,
            double tasa)
        {
            ICotizacion cotizacion;

            // ... sigue la implementación, que en base a los argumentos
// estima las distintas cuotas, con sus respectivas fechas de // vencimiento y el capital e interés cancelados en cada una return cotizacion; }


// ... otros métodos de la clase }

 
Concretamente lo que Cotizador.Cotizar() devuelve es una interfaz, ICotizacion, con las siguientes propiedades read-only (en seguida veremos por qué)
 

    public interface ICotizacion
    { 
        int Cuotas { get; }
double Tasa { get; } double ValorCuota { get; } DateTime InicioPrestamo { get; } DateTime FinPrestamo { get; } ICuota this[int index] { get; } }

 
Estimo que los nombres de las propiedades de ICotizacion son autodescriptivos. La interfaz prevé un indexador sobre las distintas cuotas, cuyas propiedades (de nuevo read-only!) son
 

    public interface ICuota
    {
        DateTime FechaVencimiento { get; }
        double CapitalCancelado { get; }
        double InteresCancelado { get; }
    }

 
Te decía que las propiedades de ICotizacion y de ICuota deben ser de lectura unicamente porque si se pudieran cambiar una vez que el cotizador las creó, cualquier otra clase puede acceder a sus propiedades, falseando los resultados originales
 

A ver si nos entendemos:

  • Pongamos por un minuto la PC en stand-by y pensemos en lo que una cotización representa
  • Una cotización es un documento, es la palabra de quien la otorga de que por un cierto plazo va a mantener una oferta de préstamo de dinero a cierta tasa, con determinado plan de pagos, etc
  • A nivel de negocio, por ende, una cotización es algo que no cualquiera puede manipular. Por ejemplo, si el cliente se está por ir porque rechaza la tasa de interés ofrecida, el agente puede ofrecerle una tasa menor hasta cierto margen
  • Si aún el cliente rechaza y a la financiera le interesaría no perderlo, ya debería venir el jefe del agente a proponer una tasa aún más chica
  • Está más que claro, por consiguiente, que si alterar una cotización, en la vida real, es algo tan delicado (y eventualmente no factible), en la vida virtual no debería permitirse que la tasa o la cantidad de cuotas o lo que fuere de una cotización se altere con una simple asignación "propiedad x = valor y"

 

En este punto no falta quien me pueda tildar de exagerado, de que alcanza con avisarle a los desarrolladores que no deben cambiar los valores de una ICotizacion y sus ICuota una vez que fue creada y ya está. Pero mi punto no es ése

Sin ser peronista, quiero citar una frase de Perón que decía "los hombres son todos buenos, pero si se los vigila son mejores"

En proyectos grandes donde la gente viene y va, unos pasan, otros quedan, esas "puertas traseras" que uno va dejando en el código son las vulnerabilidades que pueden a la postre ser explotadas por quienes se propongan cometer fraudes

Hay que salir al cruce de las mismas sin caer en tontos positivismos de "acá nunca va a pasar eso"

 

Sólo para curiosos, incluyo una posible implementación de ICotizacion, en la que preferí usar struct en lugar de class, que me almacena en forma contigua en memoria el contenido de una cotización en forma consecutiva (si en cambio fuera una clase, lo que se almacenaría en forma contigua serían las referencias a sus campos. Fuera de eso no vas a encontrar nada paranormal, excepto quizás el modificador de acceso del constructor de la clase (internal en lugar de public, ahora te sigo contando qué es eso)
 

    public struct Cotizacion : ICotizacion
    {
        // campos privados
        private int cuotas; 
        private double tasa; 
        private double valorCuota;
        private ICuota[] vectorCuotas;

        // propiedades
        public int Cuotas { get { return cuotas; } } 
        public double Tasa { get { return tasa; } } 
        public double ValorCuota { get { return valorCuota; } } 
        
        public DateTime InicioPrestamo
        {
            get { return this[0].FechaVencimiento; }
        }

        public DateTime FinPrestamo
        {
            get { return this[cuotas-1].FechaVencimiento; }
        }

        public ICuota this[int index]
        {
            get { return vectorCuotas[index]; }
        }

        // constructor
        internal Cotizacion(int cuotas,
            double valorCuota,
            ICuota[] vectorCuotas)
        {
            this.cuotas = cuotas;
            this.valorCuota = valorCuota;
            this.vectorCuota = vectorCuota;
        }
    }

 
Quiero que notes que en la estructura Cotizacion los valores de sus propiedades públicas sólo se pueden asignar al momento de construirla. Una vez creada ya no hay forma de alterarlos

Por esto se dice que Cotizacion es inmutable (immutable). Se mira pero no se toca. El responsable de la clase (o de la lógica de negocio de este módulo) puede quedarse tranquilo de que, una vez que Cotizador.Cotizar() devolvió una instancia de Cotizacion, al menos esa instancia no va a poder ser manipulada o adulterada en ninguna forma 
 

Este tipo de amenaza a la seguridad de la aplicación se conoce como Tampering o Violación de Precinto, según vimos hace poco más de un año en el webcast sobre Arquitecturas Seguras

 
Lo de internal es algo tan revolucionario como polémico. Ese modificador de acceso indica que el constructor es público… dentro del mismo ensamblado. Con eso nos prevenimos de que nadie por fuera de Cotizador.Cotizar() crée una instancia de Cotizacion asignándole sus argumentos a las propiedades, y luego la cambie por la que nuestro método le había devuelto  

La polémica se inicia cuando, como requisito, se pide poder persistir una Cotizacion (algo que seguramente ocurrirá). O más precisamente, la polémica se inicia cuando haya que recuperar una Cotizacion que se había persistido ya que, si la arquitectura de la aplicación separa la capa de acceso a datos en otro ensamblado… Cómo construir desde aquella capa las instancias recuperadas de cotizaciones?

Entonces mejor lo volvemos a dejar público al constructor, pero ahora de nuevo corremos el riesgo de que alquien, en lugar de llamar al Cotizador, se crée una Cotizacion a mano y nos la haga pasar por buena! smile_baringteeth

Parece el cubo mágico, no? Para armar una cara tenés que desarmar lo que tengas hecho de las otras. Cómo salir de aquí?

Yo en verdad opté por dejar de lado el internal, volviendo a contar con un acceso público al constructor de Cotizacion pero impidiendo, vía Code-Access Security (CAS), que desde ningún ensamblado se lo pueda llamar, excepto el del módulo Cotizador y el del DAO de ICotizacion. Yo mostré CAS en acción en el webcast sobre Arquitecturas Seguras

 

Si, como parecía plantear el participante del foro al principio, las clases tuvieran que tener un constructor por defecto (esto es, sin argumentos) de esa manera estaríamos obligados a definir las propiedades como lectura/escritura para poder asignar un valor inicial. Pero una vez que las propiedades son writables (sí: accesibles para escritura), el riesgo de tampering está presente

Si bien la inmutabilidad (immutability) juega un rol clave en evitar tampering, debe ser aplicada junto a otras medidas que refuercen la barrera contra la violación de precinto a lo largo de todo el ciclo de vida de una instancia 
 

Un par de datos más sobre inmutabilidad:

  • Por la forma en que las interfaces y las implementaciones del ejemplo fueron definidas, no es posible reimplementar la interfaz redefiniendo propiedades mediante el agregado de set(), ya que no compila si la interfaz implementada no tenía tal modificador. Del mismo modo, la estructura Cotizacion no se puede extender añadiendo setters a sus propiedades read-only, ya que éstas no tienen el modificador virtual que explicita la habilitación de sobre escritura. No obstante, no es mala idea que ciertos procesos delicados que van a trabajar con una Cotizacion recibida como argumento, se garanticen que su tipo se corresponda a su nombre fuertemente firmado (y no, por caso, una posible reimplementación trucha de ICotizacion)
  • El otro beneficio que tienen las clases inmutables es que, en escenarios de multitarea y concurrencia, al ser sus propiedades invariables no es necesario sincronizar el acceso a sus miembros mediante el comando lock(). Por consiguiente, serían un foco menos de generación de contención de hilos

 

Será hasta la próxima (me voy al cobán a ver si me aprobaron el crédito)

Esta entrada fue publicada en Software Architecture. Guarda el enlace permanente.

7 respuestas a Objetos Inmutables: Se Mira y No Se Toca

  1. Patrick dijo:

    Master, 
     
    tengo entendido, pero no lo he comprobado empíricamente, que la validación de CAS que mencionas (link demand), sólo es válida en framework 1.1.
     
    En 2.0, ya no es posible hacerlo y link demand siempre dejará que se acceda el método/clase, independiente de que el llamante no esté firmado con la key requerida.
     
    Insisto en que no lo he probado, pero lo leí por ahí. Aún no entiendo el transfondo de remover esa funcionalidad.
     
    Patrick.

  2. Diego dijo:

    Será?😐
     
    Yo me acuerdo que para el webcast la configuré y no fue nada fácil -para mí, archi de 10000 pies de altura-, pero me funcó finalmente. Me acuerdo que una vez que le ponía esa restricción de seguridad el CLR entraba a sospechar de todo el mundo y le tiraba excepción de seguridad hasta al garbage collector (no, bueno, ya tanto no, pero creo que ni el método Main se salvaba de que lo atajen en mesa de entradas y le hagan abrir los bolsos)

  3. Patrick dijo:

    En todo caso, con un debugger puedes saltarte todas esas validaciones :) 
     
    Interesante, no?
     
    Saludos,

  4. Diego dijo:

    Buenas don Patrick, cacho que no te debo haber entendido el comment (y no digo con eso q sea desacertado, por supuesto) Yo no pude, con Visual Studio 2005, framework .NET 2.0, eludir la seguridad desde el debugger (mas bien todo lo contrario). Me estare volviendo impotente? 😛

  5. Patrick dijo:

    Para nada… no dude de su potencia, al menos no todavía
     
    Con un debugger como Visual Studio es más difícil, aunque no se si se puede hacer.
     
    Con uno como Windbg, haces lo que quieres. Escribes la memoria "a piacere". Ya sé que algunos dirán "como se te ocurre lanzar el debugger en producción", y que para poder hacerlo "necesitaría acceso a la máquina con permisos elevados".
     
    Lo primero no es una barrera así que se puede. Lo segundo, bueno, un poco más complejo, pero también se puede.
     
    Mi intención no es ser aburrido ni hacernos perder tiempo en una conversación fútil, pero quería quitarle validez a la frase que alguien podría dijo, y que dice "acá nunca va a pasar eso".
     
    Todo se puede quebrar. Lo importante es protegerse lo mejor posible, tanto de los desarrolladores como de los usuarios. Y esto lo digo como desarrollador.
     
    Saludos,

  6. Bruno dijo:

    Patrick tiene razon … todo es posible. Diegum tb esta en lo cierto … "acá nunca va a pasar eso"
    y claro frente a 2 posiciones encontradas lo mejor es un punto medio virtuosos que nos permita evitar este tipo de problemas. Es bueno que los developers seamos conscientes del alcance real y de la potencia de .Net, ya que sino WinDbg o depurador X de por medio mis objetos son accesibles … ahora bien, mi pregunta es: ¿ realmente es necesario aportar esta sobrecargar de trabajo? o es mas fáci comprar un candado más grande para la puerta donde este el server ?
     
    depende del contexto ¿ no ?

  7. Diego dijo:

    Gracias por los comments, boys 
     
    Aclaración para El Bruno: yo no afirmé "acá esto nunca va a pasar". Si leiste bien el párrafo justamente decía lo contrario (lo replico acá)
     

    En proyectos grandes donde la gente viene y va, unos pasan, otros quedan, esas "puertas traseras" que uno va dejando en el código son las vulnerabilidades que pueden a la postre ser explotadas por quienes se propongan cometer fraudes. Hay que salir al cruce de las mismas sin caer en tontos positivismos de "acá nunca va a pasar eso"

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s