Inicio > .NET Framework, Apunte, C# > Conceptos OOP: encapsulación

Conceptos OOP: encapsulación


Uno de los conceptos importantes con los que contamos en la programación orientada a objetos es el de encapsulación. En esta entrada analizaremos un poco a fondo la teoría detrás de la encapsulación, y cómo podemos implementarla en nuestros lenguajes favoritos.

Comencemos por hacer un análisis que nos lleve a motivar la necesidad de la encapsulación.

Estado de objetos


Recordemos un poco algunas definiciones importantes. Un objeto es una instancia de un tipo de dato (i.e. clase) que contiene atributos (es decir, variables internas). Estos atributos determinan el estado de un objeto. ¿Qué quiere decir un estado? Es la combinación de posibles valores de sus atributos internos.

Por ejemplo, pensemos en el interruptor que nos permite encender y apagar una bombilla incandescente (a.k.a. foco). Si yo hago clic en el interruptor, el foco, que está apagado, se enciende. Si vuelvo a hacer clic, el foco se apaga. En este caso, no podemos hacer nada más que un clic, que enciende y apaga. Ahora bien, el foco sólo puede tener dos estados posibles: o está encendido o está apagado (digo, también puede estar fundido, pero consideremos ese un caso excepcional). Entonces decimos que el conjunto de posibles estados de un sistema foco-interruptor sólo puede tener dos estados posibles, y sólo puede haber dos transiciones posibles: de encendido a apagado, y de apagado a encendido. Además, bajo una regla: si está encendido, se apaga, y si está apagado, se enciende.

Pensemos ahora que queremos representar ese sistema con una clase. Tenemos entonces que crear un atributo (i.e. variable interna) que almacene el estado. Dado que sólo puede haber dos estados posibles, podemos usar una variable de tipo booleano. Este tipo de dato permite justamente dos valores: true o false.

¿Por qué usamos un bool y no un entero, por ejemplo? Podríamos hacerlo, y podríamos decir que un 0 representa "apagado", y un "1" o cualquier otro valor, sea el "encendido". Nos hace falta ahora representar las transiciones. En realidad sólo tenemos una transición: Click, que dependiendo del estado cambia entre encendido o apagado.


El diagrama anterior ilustra los posibles estados del objeto. Vemos que las dos transiciones son en realidad la misma, y que ésta varía dependiendo del estado actual.

Ahora bien, imagínate que un buen día decides cambiar tu sistema por uno de esos focos que permiten tener varios grados de luminosidad. Digamos, el foco puede estar apagado, pero cuando está encendido puede estar en tres modalidades: penumbra, normal y brillante. Entonces tenemos cuatro estados posibles. Pero ¿y cómo se hace la transición? Si el interruptor es una media perilla que recorres de izquierda a derecha, con el izquierdo el estado apagado y el derecho el estado brillante, entonces sólo podemos hacer estas transiciones: apagado a penumbra y viceversa, penumbra a normal y viceversa, normal y brillante y viceversa.

image

 

La cosa se comienza a complicar. Tenemos cuatro estados con seis transiciones. De éstas últimas, se agrupan en tres que aumentan el estado, y tres que lo disminuyen. Al implementarse, probablemente las transiciones sean dos métodos, y los estados una enumeración con las cuatro opciones.

Este interruptor podemos volverlo tan complejo como queramos. Por ejemplo, si el interruptor tiene sensores para detectar comandos de voz, tendríamos seis nuevas transiciones: una para mover la perilla y otra para el comando de voz. Y si pensamos en una perilla entera, en donde el número de grados representa un porcentaje entre la ausencia de luz y el 100% de luminosidad de la bombilla, ya entonces tenemos un estado no dictado por una enumeración, sino por un atributo de tipo entero corto (16 bits en .NET), con un rango que va de 0 a 100 (cada unidad representando el porcentaje deseado). Así, nuestra máquina de estados se vuelve mucho más compleja, pues tiene 101 estados válidos (contando al cero), peeeero ¡65435 estados inválidos! ¿Por qué? Pues porque un entero corto en .NET tiene 16 bits, esto es, puede almacenar 65536 valores (de -32767 a 32768). ¿Notas el problema?

Imaginemos que modelamos los interruptores anteriores como clases o estructuras de C#. Sin contar las transiciones, sino los puros estados, tendríamos algo así.

struct SimpleSwitch {
    bool on;
}

enum DimSwitchState {
    Off, Gloom, Normal, Bright
}


struct DimSwitch {
    DimSwitchState state;
}

struct FullSwitch {
    short brightness;
}

 

Más o menos. Si queremos usar SimpleSwitch, sólo podemos asignarle dos posibles valores, y ambos valores son totalmente válidos. Esto quiere decir que SimpleSwitch nunca puede estar en un estado inválido. Por tanto, podemos estar seguros que esta estructura no nos generará problemas o bugs.

En el caso de DimSwitch, la cosa varía. La enumeración hace un muy buen trabajo al limitar el posible número de estados para la estructura. En este sentido, estableciendo la estructura a Off, Gloom, Normal o Bright tiene el mismo efecto que establecer los valores true o false: el objeto siempre estará en un estado válido. Sin embargo, a diferencia de un bool, las enumeraciones pueden convertirse a partir de valores numéricos. ¡Zaz!

// nada puede malir sal
SimpleSwitch s1;
s1.on = true;
s1.on = false;
s1.on = 42; // ¡no compila!

// hum…
DimSwitch s2;
s2.state = DimSwitchState.Off; // ok
s2.state = DimSwitchState.Gloom; // ok
s2.state = DimSwitchState.Bright; // ok
s2.state = DimSwitchState.Normal; // ok

// so far so good…
s2.state = (DimSwitchState)42; // ¡madres!

 

 

Y valió queso. El idiota programador convirtió un valor numérico cualquiera en la enumeración. Es decir, introduje un valor inválido. Y ahora mi objeto s2 se ha corrompido, y seguramente eso causará errores y bugs más adelante.

El caso anterior es extremo, por supuesto. Ningún programador en su sano juicio haría algo así. Pero sí pueden ocurrir situaciones similares. Por ejemplo, si ese valor se guardara en alguna base de datos, tendría que guardarse en formato numérico. En teoría, nunca se guardaría un valor inválido, pero algún DBA no muy brillante podría actualizar la tabla y meter un 42 por ahí. Entonces ya nos veríamos con este mismo escenario. ¡No muy recomendable!

Hay formas de paliarlo, sin embargo. Por ejemplo, limitando acceso a DBAs que no saben lo que hacen. O bien, podemos hacer uso de el método Enum.IsDefined, al cual le pasamos un valor numérico que nos dice si el valor está definido o no. Algo así.

var value = 42;

if (Enum.IsDefined(value))
    s2.state = (DimSwitchState)value;
else
    throw InvalidOperationException("Enumeración con valor inválido.");

 

 

Y el mundo vuelve a la normalidad. Sin embargo, ya vimos que es necesario siempre realizar un cierto tipo de validación. ¡Esto es dramáticamente cierto para el siguiente ejemplo!

 

FullSwitch s3;
s3.brightness = 50; // ok
s3.brightness += 25; // ok, estamos en 75
s3.brightness += 30; // madres, ya s3 es inválido
s3.brightness = -10; // otra vez, es inválido
s3.brightness = 55;
s3.brightness *= 2; // otra vez inválido



 

Y así podríamos seguirnos. La cantidad de valores inválidos es enorme. Pero más aún, hay muchas operaciones que pueden provocar valores inválidos. Si no tenemos cuidado, esta estructura luce como fuente inagotable de bugs. Por supuesto, hay una forma muy sencilla de evitar caer en estado inválido.

 

var value = -10;
if (value >= 0 && value <= 100)
    s3.brightness = value;
else
    throw new InvalidOperationException("El valor no está en un rango válido.");

 

Y nuevamente hemos paliado la situación, aunque hemos de decir que el riesgo latente siempre estará presente.

Una motivación



 

Estos escenarios que hemos analizado son realmente sencillos. En efecto, una clase con un solo atributo no es nada. Las clases que escribimos en la vida real suelen tener decenas de atributos. Las combinaciones son catastróficas. Una clase con doubles, decimals, strings y longs tienen millones de posibles estados. Y el asunto es combinatorio: si tienes una clase con dos bools, entonces tienes cuatro posibles estados. Y así sucesivamente.

Por supuesto, el asunto no es tan catastrófico. No todos los atributos tienen rangos restringidos. Pero la causa de muchos bugs es que aquellos datos que sí lo están, no se le pone la suficiente atención para prevenir los estados inválidos. Clásico: el nombre de un empleado está limitado a 255 caracteres en una base de datos, y tu pones un string, que puede almacenar mucho más que 255 caracteres. Si no validas, ¡pum! Un SqlException seguro al querer guardar en la DB. O tienes datos que no hacen sentido. En alguna ocasión, en un sistema, vi que podía meter mi edad con un valor negativo. O puedes poner en riesgo la seguridad del sistema. En efecto, cuando actualizamos a base de datos, es de todos sabido que hay ciertos caracteres que no deben utilizase, como la comilla simple, pues puede provocar SQL Injection. Ese es un ejemplo de un estado restringido que no validamos.

El problema de los estados inválidos de objetos es muy grande. Tanto, que han surgido paradigmas y lenguajes que ¡no permiten que los objetos cambien de estado! Es decir, una vez que un objeto se crea, y por tanto, tiene un único estado, ya no puede cambiarse. Puede crearse un nuevo objeto a partir del anterior, pero no cambiarse. Un ejemplo de esto son los lenguajes funcionales, como Ocaml, Haskell o el mismo F# de .NET. Vaya, C# y .NET tienen algunas clases inmutables. La clase String es quizás un ejemplo claro. Una vez que creas el objeto, no hay método que lo modifique. Métodos como Append o Trim devuelven un objeto nuevo. ¿Qué tal?

Pero entonces, ante somera preocupación, surge la duda: ¿quién es el encargado de hacerse responsable del estado del objeto?

No es pregunta sencilla de responder. Cuando todos programábamos en C, allá en los 70s, se decía que el encargado era el método que usaba un objeto. Sin embargo, eso creaba ciertas complicaciones: si José creaba la función F1 y modificaba la estructura S1, y luego llegaba Juan y creaba la función F2, que invocaba a F1 y también cambia el estado de S1, y encima llega Pedro y modifica la F1 creada por José e introduce un estado inválido, ya le dio en la mother tanto a F1 como a F2. Estos escenarios intentaron paliarse con documentación, sin mucho éxito que digamos.

Ante esto, alguien sugirió que el dueño del estado debía ser quien crease el objeto. Pero una vez que éste comienza a ser distribuido por funciones, es imposible que el dueño haga un rastreo a cualquier oportunidad. Entonces, ¿qué puede hacerse?

Estas situaciones propiciaron una respuesta diferente: el objeto mismo debería tener un mecanismo para validar su propio estado, al limitar los rangos de posibles valores que recibe. Cuando existía C, no había forma, pero luego se creó C++, que daba la posibilidad de crear métodos sobre clases. Un método es una función que tiene acceso al estado del objeto al que pertenece. Cuando una función cambia algún valor de alguna variable interna, está cambiando el estado del objeto y se dice que es una transición. De esta forma, comenzó a ser posible llegar a códigos como el siguiente.

 

struct FullSwitch
{
    short brightness;

    void SetBrightness(short value)
    {

        if (value < 0 || value > 100)
            throw new InvalidOperationException("Rango inválido. ");

        brightness = value;
    }
}
...

FullSwitch s;
s.SetBrightness(42); // ok
s.SetBrightness(-10); // oops, ¡excepción!

 

De esta forma nos aseguramos que al invocar SetBrightness no tengamos estados inválidos. Tendremos excepciones, que indicarán bugs durante el desarrollo, pero aseguramos que al menos mientras se invoque dicha función, s será un objeto con estado válido.

Sin embargo, a pesar de lo anterior, nada impide que algún sonso haga esto:

FullSwitch s;
s.SetBrightness(42); // ok
s.brightness = -10; // sigh :(

 

And we’re back to square one. Vamos, que no creo que alguien haga eso voluntariamente. Puede hacerse por desconocimiento, por ejemplo. Clases o estructuras con decenas de atributos, decenas de métodos hacen la vida más difícil. Y luego, si tiene decenas de clases que aprenderte… bueno, digamos que puede ser mucho.

Es por eso que comenzó a surgir el siguiente concepto: ocultar los atributos de una clase. Es decir, que estén disponibles para las transiciones de la clase, pero que noooo estén disponibles para actores externos, a menos que sea necesario. El ocultar, o permitir acceso restringido, a los atributos de una clase comenzó a hacerse más y más necesario, y muy pronto lenguajes como C++ comenzaron a implementarlo. A esto se le llamó encapsulación.

Encapsulación en C#



El concepto de encapsulación, como vemos, es viejo. Hoy día todos los lenguajes orientados a objetos lo implementan de una u otra forma: C++, Java, C#, ¡hasta Visual Basic! Sin embargo, los lenguajes dan la posibilidad de NO implementar la encapsulación. Creo que ya hemos dado argumentado suficiente a favor de este concepto. Definitivamente implementar la encapsulación hará que asegures que una clase tenga puros estados válidos, y esto hará que disminuyan los posibles bugs cuando estés creando modelos de datos, capas de negocio, etc.

En particular, C# implementa varios conceptos de encapsulación, e incluso añade algunos conceptos novedosos no presentes en otros lenguajes. Incluso, la encapsulación aplica a miembros de una clase que no sean atributos, pero que de alguna forma puedan modificar el estado de un objeto. Esto incluye métodos, delegados, eventos, etc.

Veamos.

Modificadores de acceso



Comenzamos por los modificadores de acceso. En la sección anterior, veíamos este ejemplo.

FullSwitch s;
s.SetBrightness(42); // ok
s.brightness = -10; // sigh :(



Aquí, vemos que a pesar de tener nuestro método SetBrightness, podemos cambiar el atributo asignándole un valor directamente. Entonces, lo que quisiéramos es que el atributo estuviera disponible para los métodos de la clase, pero no para los externos. En C#, esto es posible con los modificadores de acceso, que tienen los siguientes niveles de acceso.

1.- Acceso privado. Sólo los métodos que estén dentro de la propia clase pueden acceder a estos atributos. Ni siquiera las clases anidadas o clases derivadas pueden hacerlo. Se identifica mediante la palabra reservada "private".

struct FullSwitch {
    private int brightness;

    int Get() { return brightness; } // ok

    void Set(int value) {

        if (value < 0 && value > 100) 
            throw new ArgumentException("Rango inválido.");

        brightness = value; // ok
    }
}

...

FullSwitch s;
s.Set(42); // ok
int i = s.Get(); // ok, i == 42
s.brightness = -10; // ¡no compila!
s.Set(-10); // ok, pero lanza excepción: ¡s nunca queda en estado inválido!

 

2.- Acceso público. Cualquier elemento fuera de la clase en cualquier ubicación podrá ver el atributo, clase, método, delegado, evento, etc. Es el nivel más alto de acceso, y por tanto sólo debe exponerse para miembros que no vayan a dejar en estado inválido nuestra clase. Se identifica mediante la palabra reservada "public".

Por supuesto, nuca querremos declarar públicos nuestros atributos. Esto violaría el principio de encapsulación, y nos daría en la torre por todo lo ya expuesto.

Hay algunos casos, sin embargo, donde se considera válido que un atributo sea público. Esto es para aquellos atributos que no cambian: estáticos de sólo lectura o constantes. Se considera admisible porque al ser constantes o de solo lectura, su estado es siempre inmutable, y por tanto no afectan el estado general del objeto. Algunos ejemplos que implementa el propio .NET Framework.

 

  • String.Empty. Representa una cadena de texto vacía. Es equivalente a poner un par de comillas: "".
  • Int32.MinValue e Int32.MaxValue. Representan los valores mínimos y máximos de un número entero.
  • Uri.UriSchemeHttp. Representa el esquema de un enlace de HTTP. Es decir, la cadena "http://".
  • DBNull.Value. Representa el valor nulo, no asignado, que puede darse a un campo en base de datos.

Como puedes ver, todas esas son inofensivas, desde el punto de vista del estado de sus objetos, y por tanto son aceptables aunque rompan el principio de encapsulación.

3.- Acceso protegido. Los elementos así declarados, mediante la palabra reservada "protected", no están accesibles fuera de la clase, igual que con "private". Pero sí están accesibles dentro de la clase, y además, están accesibles para elementos derivados. Un ejemplo:

class FullSwitch {    
    protected int brightness;

    public int Get() { ... }
    public void Set(int value) { … }
}


class PrintFullSwitch : FullSwitch
{
    public void Print() {
        Console.WriteLine(brightness); // ok!
    }
}

PrintFullSwitch s;
s.Print(); // ok!
s.brightness = 42; // no compila

 

En este ejemplo hemos declarado a brightness como protegida, así que la clase derivada PrintFullSwitch puede acceder a ella, como se vio en el ejemplo.

Hay que tener cuidado, sin embargo, con todo lo protegido. Esto, porque abre la posibilidad de que clases derivadas cambien el estado del objeto. Y en general abrimos la puerta a que alguien derive y comience a causar estragos, si no sabe lo que hace. Por eso, en general, vamos a preferir declarar nuestros atributos como privados, y métodos y demás objetos que acceden a ellos, como protegidos o públicos.

class Switch {
    private int brightness;

    public int Get() { ... }

    protected void Set(int value) { 
        if (value < 0 || value > 100)
            throw new ArgumentException("Rango inválido.");
    }
}


...
   
class FullSwitch {
    public void ChangeValue(int value) {
        Set(value); // ok, si value es inválido, Set se 
        // encarga de lanzar la excepción. 
        ...
    }

    public void ScrewState() {
        Set(-10); // aunque queramos echar a perder el estado,
                      // Set lo impide siempre. 
    }
}

 

En general, deberás tener muy en cuenta hasta qué punto quieres que clases derivadas modifiquen el estado de tu objeto.

4.- Acceso interno. Quiere decir que un miembro es público para el ensamblado que lo contiene, pero es privado fuera de ese ensamblado. Se identifica por la palabra reservada "internal".

Piensa este escenario. Tienes un equipo de trabajo, y entre todos están construyendo un ensamblado para un proyecto. Ese ensamblado contienen clases que pueden ser usadas algún día por algún tercero de otra empresa. En esta situación, te das cuenta que tienes ciertos métodos o atributos que son peligrosos para la integridad del estado de una clase determinada, pero sabes que tus colegas no van a cometer errores porque están entrenados, conocen el proyecto, etc. Sin embargo, te da miedo que el tercero llegue y cometa errores. ¿Qué hacer?

Justo para esto se inventó el "internal". Si declaras como internal un miembro cualquiera, éste estará disponible como si fuera público para cualquier otro objeto dentro del mismo ensamblado (i.e. programa ejecutable o DLL), y sin embargo será privado para cualquier otro ensamblado. Ahora bien, quien puede crear objetos desde el mismo ensamblado es quien tiene el código fuente: tú o tu equipo de trabajo. ¡Entonces un internal resuelve el problema!

 

// switch.cs dentro de Switch.dll
class Switch {
    public void Click() { … }

    internal int brightness;
    ...
}


// foo.cs dentro de Switch.dll
Switch s = new Switch(); {
    s.Click(); // ok
    s.brightness = 42; // ok

// goo.cs dentro de ThirdParty.dll referenciando a Switch.dll
Switch s = new Switch();
s.Click(); // ok porque es público

s.brightness = 42; // ¡error! para este ensamblado es privado.

 

5.- Acceso interno protegido. Un caso muy similar al anterior. Quiere decir que un miembro es protegido para el ensamblado que lo contiene, pero es privado fuera de ese ensamblado. Se identifica con las palabras "protected internal".

Si no quieres hacer "internal" un atributo, por ejemplo, pero sí te gustaría que estuviera protegido para clases derivadas, pero privado para clases que deriven en otros ensamblados, entonces usa protected intenal.

 

// switch.cs dentro de Switch.dll
class Switch {
    protected internal int brightness;
    ...
}


// foo.cs dentro de Switch.dll
class FullSwitch : Switch {
    void Click() { 
    brightness = 42; // ok
}


// goo.cs dentro de ThirdParty.dll referenciando a Switch.dll
class OtherSwith : Switch {
void OtherClick() { 
brightness = 42; // ¡no compila! para este ensamblado es privado
}


}


 

Bueno, con eso cubrimos los cinco modificadores de acceso. No dejes de utilizarlos, puesto que representan la mitad del encapsulamiento existente en C#. La otra mitad corresponde a los accesos.

Accediendo los atributos



Bueno, ya hemos visto que con los modificadores ocultamos los atributos y miembros y sólo exponemos aquellos que sabemos no pueden ser utilizados de mala forma por agentes externos. Ahora queda la pregunta: ¿cómo exponemos los atributos, parcial o totalmente, a los externos? Lo primero que nos viene a la mente son los métodos. Ya hemos visto algunos de ellos, pero pongamos uno que sirva de base para posteriores ejemplos.

class Switch
{
    private bool _turnOn;

    public bool GetTurnOn() { return _turnOn; }

    public void SetTurnOn(bool value) { _turnOn = value; }
}

 

Este es un ejemplo básico. Tenemos un par de funciones que obtienen y establecen el valor del atributo. Estamos respetando las reglas de encapsulamiento a la perfección. De hecho esta forma es muy popular en lenguajes como C++ y Java, tanto que este tipo de métodos tienen sus nombres: métodos "getter" y métodos "setter", precisamente porque siempre se llaman igual que el atributo (TurnOn) pero con el "Get" y "Set" como prefijos.

Utilizar métodos getters y setters no tiene nada de malo, de hecho. Sin embargo, tienen un inconveniente que es más semántico que otra cosa. Un método, por su naturaleza, tiene en potencia la capacidad de alterar el estado del objeto. Así, para un agente externo, invocar un Get o un Set no garantiza que se respete el estado del objeto. En C++ existe una construcción que permiten declarar métodos constantes. Un método constante no puede cambiar atibutos (a menos que el atributo se le declare como variable "mutable"). Ejemplo:

 

class switch() {
    private:
        bool _turnOn;

    public:
        bool get() const { 
            return _turnOn;
        }

        void set(bool value) {
            _turnOn = value;
        }

        void click() const { 
            _turnOn = !_turnOn; // oh-oh!
        }
};



 

 

En la clase anterior de C++, nota que los métodos get y click tienen la palabra "const" después de los paréntesis. Esto quiere decir que esos métodos no pueden cambiar sus atributos. En el caso de get, no pasa nada porque nada más se hace el return. Sin embargo, el método click provoca un error de compilación, puesto que el método es constante y se intenta cambiar un miembro: el atributo _turnOn. En general, se conoce como "const-correctness" esta técnica, y es muy útil para asegurar métodos que no cambien el estado de un objeto.

En C#, este concepto no existe. Sin embargo, se creó algo para ayudar un poco a acercarnos a esta construcción: las propiedades.

Una propiedad en C# se "parece" mucho a un atributo, con la diferencia que las propiedades no generan un espacio en memoria. En contraposición, las propiedades cuentan con un cuerpo, el cual puede tener dos "accesores": un get y un set (o puede tener sólo uno). Cada uno de éstos tiene su propio cuerpo, y en éste podemos poner tareas normales que pondrías en cualquier método. El único requisito es que para los get, debe haber un return, y para los set, uno puede usar la palabra reservada "value" para referir al valor que se está estableciendo.

 

class Switch {
    private bool _turnOn;


    public bool TurnOn {
        get {
            return _turnOn;
        }
        set {
            _turnOn = value;
        }
    }
}


Switch s = new Switch();
s.TurnOn = true;
var val = s.TurnOn;

 

Como puedes ver, es muy similar a tener un atributo público llamado "TurnOn". Sin embargo, ¡siempre puedes realizar validaciones en el set, o puedes computar valores en el get!

 

class Switch {
    private int _brightness;

    public int Brightness{
        get {
            return _brightness;
        }
        set {
            if (value < 0 || value > 100)
                throw new ArgumentException("Rango inválido. ");

            _brightness = value;
        }
    }
    
    public bool IsOn {
        get {
            return _brightness != 0
        }
    }

Switch s = new Switch();
s.Brightness = 42;
var val = s.IsOn; // val es true        
val = s.IsOn; // val es false


 

Ahora, como puedes ver no hay mucha diferencia entre usar propiedades y usar métodos. Digo, la sintaxis sí es diferente, pero ¿qué ventajas tenemos de usar propiedades? La verdad es que la ventaja es por convención. Si un getter puede cambiar el estado de un objeto, o bien tiene que realizar computaciones muy grandes que pueden consumir significativamente memoria o procesamiento, usamos un método de la forma "GetPropiedad". En caso contrario, usamos una propiedad "Propiedad { get; }". Por otro lado, todos los setters cambian el estado del objeto (esa es su función). La regla es: si un setter cambia el estado del objeto en más de un atributo, o bien se realiza una tarea de procesamiento intensivo, utilizamos un método de la forma "SetPropiedad". En caso contrario, usamos una propiedad "Propiedad { set; }".

 

class Employee {
        private string _name;
        
        private Dictionary<DateTime, double> _salaryHistory;
        public event EventHandler NameChanged;

    public string Name{
        get {    return _name; }

    set {
        if (value == null)
            throw new ArgumentNullException("value");

        if (_name != value) {
            _name = value;    
    }

    if (NameChanged != null)
        NameChanged(this, EventArgs.Empty);
    }
}


}


public IDictionary<DateTime, double> SalaryHistory {
    get {
        if (_salaryHistory == null)
            _salaryHistory = new Dictionary<DateTime, double>();

        return _salaryHistory;
    }
}


public double GetAverage() {
    double avg = 0.0;

    foreach (var item in _salaryHistory)
        avg += item.Value;

    return avg;
}

public void SetAverage(double avg) {
    foreach (var item in _salaryHistory)
        item.Value = avg;
}

    public bool IsOn {

    get {
        return _brightness != 0;
        }
    }
}

 

El código anterior muestra varios aspectos interesantes. La clase muestra una propiedad Name, cuyo getter simplemente regresa una variable local de la clase. El setter, sin embargo, hace cosas más interesantes. Primero, verifica que el valor asignado no sea nulo (si lo es, lanza un ArgumentNullException). Luego, revisa si el valor del atributo es diferente al valor asignado, en cuyo caso aparte de actualizar el atributo con el nuevo valor, dispara un evento, llamado NameChanged. Este uso de una propiedad para validar entrada de valores y para lanzar notificaciones es sumamente común.

Luego, tenemos una propiedad llamada SalaryHistory. Esta propiedad tiene como tipo de dato una interfaz IDictionary. Sin embargo, vemos que internamente todo se guarda en un objeto de tipo Dictionary, el cual por supuesto implementa IDictionary. Este es un uso común de las propiedades: no queremos que nuestro cliente sepa qué objeto interno utilizamos: basta con que se sepa que es un IDictionary. Otro ejemplo sería regresar un IEnumerable, sin importar si internamente se implementa como un Array, un List<T> o on Collection<T>.

Adicional a esto, tenemos que el getter revisa si el atributo es nulo. En caso de serlo, inicializa el objeto antes de regresarlo. Esta técnica, llamada "lazy initialization" o "inicialización retardada", es útil cuando tenemos objetos miembro que no necesariamente ocupamos desde que se instancia el objeto padre. Por último, nota que la propiedad no tiene setter, puesto que no es necesario cambiar el objeto diccionario, sino su contenido.

Posteriormente, tenemos un par de métodos GetAverage y SetAverage. Estos son métodos porque hacen cómputo (relativamente) intenso. Podríamos poner una propiedad, pero de esta forma le indicamos al cliente de la clase que se espera no esté invocando los métodos a cada rato. Este sería un mal uso:

 

Employee e = new Employee();
...
double[] vals { … };
foreach (double val in vals)
{
    if (val > e.Average)
        // hacer algo
}

Para cada iteración, hacemos un cálculo para ver si el valor es mayor al promedio provisto por la hipotética e.Average. Durante esta iteración, e.Average será siempre la misma. Sin embargo, ejecutamos el cálculo una y otra vez, lo cual no es muy eficiente. Sin embargo, como Average es una propiedad, no me da indicios de que se estén produciendo cálculos. Ahora considera esto:

Employee e = new Employee();
...
double[] vals { … };
foreach (double val in vals)
{
    if (val > e.GetAverage())
        // hacer algo
}

Así al menos estamos conscientes de que se ejecuta un método, el cual hace cálculos. Al revisar esto, veríamos que estamos haciendo algo no tan eficiente, y probablemente decidamos cambiar nuestro código a:

Employee e = new Employee();
...
double avg = e.GetAverage();

double[] vals { … };
foreach (double val in vals)
{
    if (val > avg)
        // hacer algo
}

Mucho mejor, ¿no?

Bueno, ya regresando al ejemplo anterior, tenemos la propiedad IsOn, la cual regresa un resultado basado en el cómputo de otro tipo de atributos. Este es otro ejemplo de uso común de las propiedades.

Y ya por último, tenemos una declaración rara de una propiedad, llamada Address. Esta declaración de propiedad define un getter y un setter, pero no define el cuerpo. Cuando declaramos una propiedad así, estamos declarando una propiedad abreviada, y el compilador lo que hace es generar un atributo privado y enlazarlo al cuerpo get y set de la propiedad. Es decir, al escribir:

public string Address { get; set; }

el compilador lo transforma en:

private _Address;
public string Address { 
    get { return _Address; }
    set { _Address = value; }
}

Solo que el compilador lo hace por nosotros. Es una forma de azúcar sintáctico para que escribamos menos .

Ahora bien, las propiedades tienen un modificador de acceso, por lo que también pueden ser públicos, privados, protegidos, internos e internos protegidos. Declararlos como tales provoca que tanto getter como setter tengan el mismo nivel de acceso. Sin embargo, ¿qué pasa si queremos que uno de los elementos tenga un nivel de acceso diferente? Por ejemplo, si tenemos una propiedad pública, pero queremos que el setter sea protegida, ¿cómo lo hacemos?

Lo que se hace en este caso es calificar el getter o setter en cuestión con el modificador deseado, siempre y cuando el nivel de acceso de éste sea menor que el de la propiedad:

public string Address { 
    get { return _Address; }
    protected set { _Address = value; }
}
…
Employee e = new Employee();
var a = e.Address; // ok
e.Address = "dirección"; // ¡error de compilación! sólo clases que hereden
                              // de Employee pueden modificar esa propiedad.

El tema importante aquí es el nivel de acceso. El nivel público tiene mayor nivel de acceso que protected, y éste tiene mayor nivel de acceso que privado, y así sucesivamente. Así, el siguiente código no compila:

protected string Address { 
    get { return _Address; }
    public set { _Address = value; }
}

Creo que ha quedado claro el uso de las propiedades como medida para garantizar un buen encapsulamiento. Ciertamente es mejor que exponer los atributos directamente. En .NET, aún cuando un atributo no requiera más que existir (i.e. no necesite procesamiento en el get o en el set adicional), es buena práctica el tener las propiedades.

Implementación explícita de interfaces

En C# y .NET, las interfaces son un mecanismo de herencia muy difundido en toda la plataforma. Las interfaces, sin embargo, también nos ayudan a mejorar el encapsulamiento, principalmente a través de la implementación explícita de interfaces.

Este tipo de implementación se hace prefijando el nombre de la propia interfaz seguida de un punto, seguida del nombre del método o la propiedad. Tras lo discutido en la sección anterior, nos interesa en particular las propiedades, que nos ayudan a encapsular los atributos. Esta implementación explícita siempre será pública, y no puede ser declarada como abstracta, virtual, sobrescrita u oculta (abstract, virtual, override, new). Y lo que es importante: no puede ser accedida directamente. Vamos por partes.

interface IContactable
{
    string Address { get; set; }
    string Phone { get; set; }
}

public Employe : IContactable
{
    public string Address { get; set; } // implementación implícita
    string IContactable.Phone { get; set; } // implementación explícita
}

Employee e = new Employee();
e.Address = "Dirección"; // ok, no problemo
e.Phone = "55156789"; // ¡no compila! no puede ser accedida directamente

IContactable c = e as IContactable; // convertimos
c.Phone = "55156789"; // ok, ¡ahora sí funciona!

Es muy importante hacer notar que Phone no puede ser accedido mediante la clase Employee, pero si convertimos a la interfaz, IContactable, entonces sí podemos acceder a dicha propiedad. Esto es importante, pues nos hace notar cómo la implementación explícita de interfaces puede ayudarnos a mantener el encapsulamiento, al permitirnos ocultar propiedades que no queramos que estén disponibles directamente.

…y otra cosa

Bueno, creo que hemos hablado mucho sobre el tema del encapsulamiento. Resumiendo, hemos comenzado por definir el estado de un objeto y sus implicaciones dentro de la programación orientada a objetos. Luego, intentamos motivar, a partir de ello, la necesidad del encapsulamiento de los atributos de nuestros objetos. Por último, vimos cómo C# y .NET implementan dicho encapsulamiento, mediante los modificadores de acceso, los métodos y las propiedades, y la implementación explícita de interfaces.

No queda más, pues, que hacer la recomendación a título personal: por favor, siempre asegúrate que tus atributos estén encapsulados. Nunca sabes cuándo otro programador va a meterle mano a tu código. Encapsular el estado de tu objeto puede ser vital para que el otro no la riegue. De igual forma, cuando tú metas mano en código ajeno y veas un atributo público, desconfía de él: busca dónde se utiliza y bajo que circunstancias, no sea que le des en la torre al estado del objeto.

Pues eso es todo. ¡Hasta la próxima!

Categorías:.NET Framework, Apunte, C# Etiquetas: ,
  1. Carlos
    febrero 3, 2015 a las 4:22 am

    Estupendo artículo! Muy claro y explicativo. Muy recomendable. :-)

  1. noviembre 27, 2012 a las 7:15 pm

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