Cuando era chico y estaba aburrido -casi siempre- me inventaba un juego, el cual radicaba en que andaba por la casa buscando
objetos hasta que escogía una característica de algo que me gustaba: el color de algo, su textura, su olor? y con fundamento en esa característica me ponía a comparar os
objetos que tenía a mi alrededor, y os diferenciaba
entre sí. Veía que característica era la que ganaba al final. En una ocasión, para desmayo de mis padres, la característica fuese la resistencia de os
objetos a un impacto tras una trayectoria de varios metros. Descubrí que las jarras, floreros y la colección de discos de vinil no eran resistentes? Durante mi infancia, siempre pensé que era un juego ingenioso, hasta que me di cuenta que es un proceso natural de cualquier ser humano, y que seguramente es el que nos distingue del resto de la fauna de este planeta: la clasificación de objetos. Clasificando al mundo En efecto, la clasificación de un
objeto necesita un nivel de abstracción. Clasificar implica beber un atributo general de un conjunto de objetos, y compararos con otros objetos. Así, por ejemplo, poseemos una mesa de madera, una vasija con agua y una llanta de auto. Yo podría decir que tanto la mesa
como la botella son productos caseros que se encuentran en cocinas, y distinguirlo de la llanta, la cual no es un producto doméstico. En este caso, estoy clasificando os objetos por su utilidad en el hogar. Claramente la mesa y la vasija son útiles en el hogar, y se aguarda que estén ahí por lo mismo. Sin embargo, si entro a la cocina y veo una llanta
sobre la barra, no me hará mucho sentido: una llanta no tiene sentido en un hogar. La clasificación está extendidísima en el quehacer humano. Os biólogos clasifican a os seres vivos en reinos. Os geólogos clasifican las eras de la Tierra, os historiadores clasifican os sucesos por períodos o etapas, os literatos clasifican las novelas por movimientos literarios, e inclusive os jóvenes de hoy (snif) clasifican su música por género. Esa capacidad de abstracción que poseemos nos da muchísimas ventajas, ya que que las clasificaciones nos facultan tratar objetos con
atributos parecidas (y por tanto, dentro de una clasificación determinada) de manera idéntica. Así, conocemos que os mamíferos tienen ciertos comportamientos comunes,
como la sociabilidad y la protección mutua. Por tanto, respecto a esto, podemos establecer lineamientos globales sin importar si tratamos con perros, leones o humanos. Por otra parte, también podemos abstraer comportamientos. Por ejemplo, todos os mamíferos tienen el comportamiento de comer, aunque la manera de realizarlo varíe: el león probablemente cazará a su presa y la comerá cruda, entretanto que el perro esperará a que su dueño le alimente con croquetas y huesos; el humano probablemente irá a determinado restaurante. Pero el comportamiento es el mismo: el comportamiento aplica al sentido o intención de la acción, no tanto a cómo ésta se lleva a cabo. Otro ejemplo: todos os animales tienen el comportamiento de respirar. Por tanto, dado que un pez, un cangrejo y una gaviota son animales, podemos asegurar que respiran. Sin embargo, el pez respira separando el oxígeno del agua que le rodea, entretanto que la gaviota coge el oxígeno disuelto del aire. El cangrejo hace ambos procesos, dependiendo de si se descubre en tierra o mar. Clasificando dificultades La abstracción y clasificación parecen ser una dispositivo poderosa. De hecho lo es tanto que la usamos también para la resolución de dificultades de todo tipo en vuestra vida cotidiana. Pensemos en determinado ejemplo. Un encargado de una fábrica de refrescos tiene que hacer variadas tareas. El área de planeación estratégica puede solicitarle que produzca más refrescos. El área de
ventas puede solicitarle que le envíe refrescos a tal o cual supermercado. El área de calidad puede solicitarle que determinado lote de refrescos sea destruido, pues no pasó la prueba de control. Y por último, el área de almacén puede solicitarle que traslade un lote de refrescos de una ubicación a otra. ¿Qué
pueden tener estas tareas en común? 1.- Todas las tareas implican
realizar algo con refrescos. Os refrescos están agrupados por sabores y presentaciones, y por
lotes (i.e. fecha de creación). 2.- Todas las tareas implican un movimiento contable de almacén. entrada, salida a tienda, salida a desperdicio y movimiento interno. 3.- Todas las tareas conllevan un movimiento financiero: crear refrescos significa que se gastará materia prima, por lo que el movimiento será un costo. Lo mismo el destruir un lote porque no pasó las pruebas de calidad. Por otra parte, la venta al supermercado significa un movimiento positivo, pues entrará dinero a la fábrica por concepto de ventas. Por último, el movimiento
entre almacén genera un movimiento neutral, pues no se gasta ni recibe nada. 4.- Todas las tareas tienen una persona quien la solicita y se hace responsable, y otra que la ejecuta y se hace responsable de llevarla a cabo. 5.- Todas las tareas deben realizarse en un período contable válido y en horarios laborales válidos. Supongo que hay más, pero creo que con esas podemos ilustrar el tema. Si
nosotros fuéramos el encargado de la fábrica, ¿qué podríamos hacer? Si no clasificamos, tendríamos que
generar un proceso independiente para cada tarea. Pero no es el caso, podemos re
utilizar si clasificamos bien. De entrada, consideremos cada una de las tareas
como una orden. La orden debe tener tres propiedades: el nombre de quien la solicita y el nombre de quien es responsable de ejecutarla (por el
punto 4). De idéntico forma, debe tener una fecha de origen y una fecha de fin (por 5). Por otra parte, deberá tener un objeto Inventario. El objeto Inventario es en verdad una lista que lleva el nombre de un refresco, su presentación y lote, y su cantidad. Todas las órdenes tienen esta lista, por tanto todas tienen un inventario. Finalmente, todas las órdenes deberán tener un comportamiento similar, llamado "hacer movimiento contable" y "hacer movimiento financiero". Ya con esto, podemos decir que poseemos cuatro
tipos de órdenes: de trabajo, órdenes de venta, orden de salida a desperdicio y orden de traslado. Comparten ciertas propiedades comunes,
como el inventario o os responsables. Pero es evidente que realizar un movimiento contable y realizar un movimiento financiero tienen implicados diferentes. Es decir, para os cuatro implica que van a afectar el inventario del almacén, y que van a afectar las finanzas de la fábrica. Pero para una orden de trabajo, implica que tendrán que gastar dinero en la producción , pero que ingresará nuevo artículo al almacén; para una orden de venta es lo contrario: sale artículo del almacén pero ingresa dinero a las arcas de la fábrica; la orden de salida a desperdicio es doble pérdida: sale material del almacén y no entra dinero en la billetera de la fábrica; por último, la orden de traslado mueve material, pero no sale ni entra, y no tiene movimiento financiero. El haber hecho esta clasificación ayudará en mucho al encargado de la fábrica. Por ejemplo, posiblemente tendrá que crear un
formato para llevar el registro. Si no debiera clasificado habría hecho cuatro formatos: uno por cada tarea. Pero ahora sabe que puede
usar el mismo formato con os responsables, las fechas y el inventario. Ahora, quizás tenga que crear una hojita para llevar el registro especial de cada orden, el equivalente a "hacer movimiento contable" y "hacer movimiento financiero", pues posiblemente tendrá que recolectar distintos firmas, etc. Respecto a políticas y procedimientos, podrá crear las generales, y sólo las propias para cada una de ellas, en espacio de crear una política y procedimiento para cada orden. Y así sucesivamente. Clasificando bits Hasta ahora hemos visto puros ejemplos de la vida real, aplicados a objetos físicos y a escenarios de uso en un negocio hipotético. Pero ¿y eso qué tiene que ver con la programación? Para responder esa pregunta, poseemos que hacer un poco de anécdota de la computación. Recordemos que el objetivo de una computadora es computar. Es decir, hacer
operaciones que cuantifiquen. Entre estas operaciones, podemos computar valores lógicos y relacionarlos (i.e. álgebra booleana). Y gracias a esto, podemos hacer interpretaciones de hardware, lo cual se traduce en una instrucción a determinado dispositivo. Un conjunto de instrucciones, una tras otra, forman un programa. Lo que desprende de lo previo es que un proyecto es un conjunto secuencial de instrucciones. Podemos tener unas diez instrucciones secuenciales, y la computadora lee la primera, la ejecuta y devuelve un fruto; lee la segunda, la ejecuta y devuelve un fruto; lee la tercera? y así sucesivamente. Es perfectamente natural para una computadora: lo único que requiere es tener las sentencias, el hecho de que sean secuenciales es fracción inherente a su naturaleza: las computadoras no pueden pensar de otra forma. Se comen una sentencia, escupen el fruto y van por la siguiente. Así las cosas, pues es perfectamente natural y entendible que mientras años,
cuando surgieron os lenguajes de programación, éstos se amoldaran a la manera de pensar de una computadora. Lenguajes
como Cobol, Fortran y C redactan instrucción tras instrucción, una tras otra. Esto hizo imposible que el mismo código pudiera reutilizarse en más de un lugar. Esta necesidad provocó que salieran lenguajes estructurados, que facultan escribir subrutinas parametrizadas. Pero a pesar de esto, el hecho Seguid siendo el mismo: instrucción tras instrucción, todo secuencial: llamar a una subrutina tras otra, etc. El asunto de pensar en instrucción tras instrucción es que tras miles de líneas de código, inicia a hacerse difícil de administrar, y por tanto, el código se vuelve propenso a errores. Pero lo peor es que esta manera de pensar no es natural en un humano. ¡Pues claro: si nosotros pensamos clasificando! Para nosotros sería mucho más natural enfocar os asuntos de software mediante clasificaciones, en espacio de ejecutar tareas una tras otra. Estas necesidades son las que han provocado que surgieran os lenguajes orientados a objetos. En otras palabras, os lenguajes orientados a objetos deben permitir hacer clasificaciones. Por supuesto, os lenguajes modernos
como Java o C# facultan mucho más que crear clasificaciones, pero es el motivador principal. En C# se clasifica formando -tatatáaaan- clases. ¡Así es! Las
clases y estructuras, y en común os tipos de datos, son las dispositivos mediante las cuales creamos nuestras clasificaciones. Sin embargo, crear una
clase no es suficiente, pues éstas, por sí mismas, no nos facultan crear vinculos de identidad. Relaciones de identidad Cuando hablábamos de os objetos y sus atributos y comportamientos, y cómo clasificaros, pasamos por algo un concepto muy importante: las relaciones. Aunque las mencionamos brevemente no ahondamos mucho en ellas. Pues bien, es el momento de hacerlo. Para poder clasificar, poseemos que comparar. Y para comparar, poseemos que relacionar propiedades y atributos, si no, no podríamos llevar a cabo la clasificación. Así, es significativo que nos detengamos un momento para estudiar este concepto. El primer tipo de relación, quizás el más sencillo, es el de asociación. La relación de asociación desea decir que un objeto conoce a otro. Por ejemplo, un vaso debe
conocer a una mesa, pues es donde se posa. Una mesa, sin embargo, no necesariamente conoce un vaso. El segundo tipo de relación, más interesante, es el de la agregación. Es una relación de "x contiene un y". Por ejemplo, una computadora contiene un monitor, un teclado y un ratón. Otro tipo de relación, parecida a la agregación, es la composición. Ésta es una relación de "x está integrado por a, b y c". Por ejemplo, un automóvil está integrado por un motor, unas llantas, un chasis, etc. El último tipo de relación significativo es el de la identidad. Esta relaciona las propiedades de un objeto con otro y crea generalizaciones. Es una relación de tipo "x es un tipo de y". Por ejemplo, un auto es un vehículo, un autobús es un vehículo, y una motocicleta es también un vehículo. Por otra parte, un vehículo es una máquina, una computadora es una máquina. Por lo tanto, un automóvil es una máquina. Y así sucesivamente. Las vinculos de identidad quedan
como anillo al dedo para todo lo que hemos visto. En efecto, podemos ver claramente que la clasificación que podemos realizar
sobre ciertos objetos queda complementada cuando podemos realizar vinculos "x es un tipo de y". Más aún, según podemos ver en el
ejemplo anterior, conforme establecemos vinculos de identidad, éstas se trasladan hacia encima y hacia abajo, y entonces manifestamos que hemos creado una jerarquía. Ejemplo: El
ejemplo previo es un es
caso somero, pero ilustra vincuos de identidad y una jerarquía de clases. Os animales, plantas y hongos son seres vivos, así que siempre que hablemos de un ser vivo, inferimos que es sdeterminados de estos tres (por lo menos). Comportamiento general de os seres vivos: vivir, reproducir, comer, morir. Luego, un insecto, un mamífero, un ave, molusco o pez son un tipo de animal. Os homínidos, canes y felinos son un tipo de mamífero. Un humano es un tipo de homínido, y un león, tigre y un gato son tipos de felinos. Pues bien, ya con clasificación y con vinculos de identidad entendidas, podemos volver a C#. En .NET, las vinculos de identidad se hacen mediante la herencia. Herencia en C# y .NET El concepto de herencia, que no es otra cosa que establecer una relación de identidad
entre dos clases, es medular en la plataforma .NET y en general, en cualquier plataforma / lenguaje de programación enfocada a objetos. Junto con la encapsulación y el polimorfismo, conforman os pilares de la misma. En .NET cada lenguaje determina cómo implementar la herencia. En el caso de C#, se hace poniendo dos puntos tras el nombre de la
clase, seguido del nombre de la
clase con la cual deseamos establecer la relación. class ClaseDerivada : ClaseBase { ? Decimos que la clase con la que relacionamos es una clase fundamento de la actual, a la cual se le conoce
como clase derivada. También se les conoce
como clase generalizada y clase especializada, respectivamente. En C# y .NET, una clase tiene una y sólo una relación de identidad (si una clase no especifica su clase base, ésta en automático será la clase System.Object). En C++, por ejemplo, una clase puede tener cero, una o múltiples vincuos de identidad. A esta característica se le conoce
como herencia múltiple. Las razones por las que C# no implementa
herencia múltiple es que ésta motivo dificultades cuando no se implementa bien (lo cual lo convierte en un asunto de un programador no muy hábil, más que una falla del lenguaje? pero bueno). Asimismo, la
herencia siempre es pública (es decir, todos os métodos públicos se heredan
como públicos), en contraste con C++ donde la
herencia puede ser pública, protegida o privada. Por supuesto, al momento de heredar, heredamos métodos y propiedades y atributos protegidos y públicos. class ClaseBase { public void Foo() { public
string Goo { get; set; class ClaseDerivada : ClaseBase { public void Hoo() { ClaseDerivada c = new ClaseDerivada(); c.Foo(); c.Goo = "Goo"; c.Hoo(); En este tenor, podemos realizar uso de la relación de la próximo forma: ClaseBase b = new ClaseDerivada(); b.Foo(); b.Goo = "Goo"; b.Hoo(); // no compila Dado que ClaseDerivada es un tipo de ClaseBase, podemos asignar a una variable de tipo ClaseBase una instancia de ClaseDerivada. Podemos invocar al método Foo y a la propiedad Goo, ya que que ClaseBase las define. No podemos, sin embargo, invocar al método Hoo porque esa es particular de ClaseDerivada. La
herencia de clase puede seguir y seguir y seguir: class SegundaClaseDerivada : ClaseDerivada { ? class TerceraClaseDerivada : SegundaClaseDerivada { ? class CuartaClaseDerivada : TerceraClaseDerivada { ? Conforme vamos formando especializaciones, manifestamos que vamos formando una jerarquía de clases. Por supuesto, no importa qué tan profunda sea la jerarquía, la relación siempre se mantiene: ClaseBase b = new CuartaClaseDerivada(); b.Foo(); // ok b.Goo = "Goo"; // ok A veces querremos que una clase ya no pueda seguir heredándose. En estos casos, manifestamos que la clase es final, o que está sellada. Para sellar una clase, usamos la palabra reservada "sealed" en C#. Cualquier intento por heredar de una clase sellada, generará un yerro de compilación. sealed class QuintaClaseDerivada : CuartaClaseDerivada { ? class SextaClaseDerivada : QuintaClaseDerivada { ? // yerro de compilación Hay ocasiones en las que deseamos crear clases que sólo sirvan
como base. Es decir, que puedan ser heredadas, pero no instanciadas. A estas clases las llamamos abstractas, y se marcan con la palabra reservada "abstract". abstract class ClaseBase { ? class ClaseDerivada : ClaseBase { ? ClaseDerivada d1 = new ClaseDerivada(); // ok ClaseBase b1 = new ClaseDerivada(); // ok ClaseBase b2 = new ClaseBase(); // yerro de compilación Comportamientos y contratos Como decíamos arriba, una clase abstrae comportamientos y atributos. Esto, traducido a C#, no es otra cosa que métodos y propiedades. Un conjunto de métodos determina un comportamiento: esto es, de qué manera se modifica el estado de un objeto. Ahora bien, ¿qué pasa cuando deseamos abstraer varios comportamientos? Os comportamientos en común determinan cómo se interactúa y maneja el estado de un objeto. Pongamos un ejemplo para comprender la pregunta anterior. class Point { public int X { get; set; public int Y { get; set; public static readonly Zero; public Point(int x, int y) { X = x; Y = y; static Point() { Zero = new Point(0, 0); class Line { public Point Start { get; set; public Point End { get; set; public Line(Point start, Point end) { Start = start; End = end; class Circle { public Point Center { get; set; public int Radius { get; set; public Circle(Point center, int radius) { Center = center; Radios = radios; Bien, poseemos unas tres clases que representan objetos en un plano euclídeo: un punto, una línea y un círculo. Dado que son figuras, podríamos pensar que tiene lugar una clase base, llamada Shape, y que ésta puede agrupar comportamientos comunes. Pongamos por ejemplo que deseamos tener un mecanismo para comparar dos objetos y
saber si son iguales. Podemos crear un método, llamado IsEqualTo, que tome como parámetro un Shape y regrese true cuando son iguales y false en caso contrario. class Shape { public abstract bool IsEqualTo(Shape other); class Point : Shape { ? public override bool IsEqualTo(Shape other) { bool equals = false; Point pt = other as Point; if (pt != null) { equals = X == pt.X && Y == pt.Y; return equals; class Line { ? public override bool IsEqualTo(Shape other) { bool equals = false; Line ln = other as Line; if (ln != null) { equals = Start.IsEqualTo(ln.Start) && End.IsEqualTo(ln.End); return equals; class Circle { ? public override bool IsEqualTo(Shape other) { bool equals = false; Circle cc = other as Circle; if (cc != null) { equals = Center.IsEqualTo(cc.Center) && Radius == cc.Radius; return equals; De esta forma, a través e la jerarquía de clases, hemos asegurado un comportamiento: todas las figuras que hereden de Shape pueden ser comparables entre sí. Gracias a esto, podemos implementar algoritmos genéricos para la clase Shape, como por ejemplo, un método que busque en os fundamentos de un Array o List de objetos Shape, y que pueda utilizarse para cualquiera de las tres clases. static class Search { public static bool Exists(ListShape> shapes, Shape shapeToFind) { foreach (Shape shape in shapes) { if (shape.IsEqualTo(shapeToFind)) return true; return false; ? ListShape> shapes = new ListShape>(); shapes.Add(new Point(10, 15)); shapes.Add(new Line(new Point(42, 42), new Point(25, -14))); shapes.Add(new Circle(new Point(17, 15), 12)); shapes.Add(new Point(42, 42)); bool exists = Search.Exists(new Point(42, 42)); Console.WriteLine("Punto 42, 42 encontrado: {0", exists); Sin embargo, esto no es suficiente. Resulta que yo tengo, aparte, una clase Color, que faculta establecer el color de una figura (Shape). class Color { public R { get; set; public { get; set; public B { get; set; Evidentemente un Color no es una figura, por lo que no podemos establecer una relación de identidad (y por tanto no podemos heredar de Shape: realizarlo sería un yerro de diseño). Pero a todas luces, un color también puede ser comparable entre sí: class Color { public R { get; set; public { get; set; public B { get; set; public bool IsEqualTo(Color c) { bool equals = false; if (c != null) equals = R == c.R && == c. && B == c.B; return equals; Con esto vemos algo esencial: a pesar de las diferencias existentes entre Shape (y sus derivadas) y Color, ambas determinan un mismo comportamiento: ambas pueden compararse. Volviendo a mi método de búsqueda? como éste se hizo para Shape, ya nos amolamos. Podríamos realizar que Color herede de Shape, pero no tiene sentido, porque no hay realmente una relación de identidad. Podríamos cambiar al método Exists para que tome un object, y realizar la conversión correspondiente entre Shape, y si falla, despues a Color? el asunto de este enfoque es que si poseemos una tercera clase base: Pencil, que también sea comparable, pues ahora tendremos que triplicar el código de Exists. Y de todas maneras no sería nada extensible. Otro enfoque para solucionar el asunto sería crear una clase base, Comparable, de las cuales hereden tanto Color como Shape. class Comparable { public abstract bool IsEqualTo(Comparable c); class Shape : Comparable { ? class Color : Comparable { ? Esto podría resolver el asunto momentáneo. Pero? supongamos que aparte del comportamiento de comparabilidad, deseamos agregar cualquier otro comportamiento, que no lo compartan Shape y Color, pero sí Color y Pen? Vuestra jerarquía de clases se convertiría en una sumamente compleja, difícil de gestionar, y en resumen terminaríamos duplicando código y con un mal diseño entre manos. Y encima, sólo podemos tener una clase base, pues la herencia múltiple no está soportada en .NET. ¿Cómo podemos hacerle entonces para definir comportamientos que trasciendan las vinculos de identidad? La respuesta es: mediante contratos. Un contrato básicamente es la promesa de un comportamiento. Cuando uno hace un contrato comercial: quedamos en venderle a la compañía fulana una porción equis de refrescos. Pues bien, hacemos un contrato: ahí se estipula que nosotros entregaremos 200 cajas de refresco, cada caja con 10 refrescos cada una, entregables el siguiente lunes. Asimismo, establecemos que el cliente nos pagará $10 pesos por refresco, totalizando $20,000, pagaderos contra entrega. Al firmar ambas fracciónes el contrato, garantizamos que vamos a realizar lo ahí estipulado. ¿Cómo lo hagamos? No importa: yo puedo llevar mis refrescos cargando, puedo emplear a determinado camión repartidor, etc., entretanto que el cliente puede pagarme en efectivo, con cheque, con tarjeta de crédito, etc. El chiste es que se cumpla lo establecido en el contrato. Pues bien, cuando hablamos de contratos relacionados con clasificaciones, estamos queriendo decir lo mismo: garantizamos que el contrato va a realizar lo que quedamos. De cierta manera, os comportamientos de una clase constituyen un contrato en sí mismo. En efecto, garantizan un comportamiento gracias a la relación de identidad existente. Pero estos contratos no trascienden felicidad relación. Os contratos, en general, siempre trascenderán las vinculos de identidad, y facultan que clases que se adhieran a un contrato particular garantizan el comportamiento, sin importar cómo lo implemente de manera interna. En C# y .NET os contratos se determinan mediante las interfaces. Interfaces en C# y .NET Una interfaz es un contrato, el cual determina un conjunto de métodos, propiedades y eventos. Una interfaz no determina atributos y os métodos y propiedades son unicamente declarativos, no determinan un cuerpo en particular; asimismo, tampoco pueden tener modificadores de acceso. Sencillamente tipo de dato de
retorno y parámetros. Las
interfaces se declaran mediante la palabra reservada "interface" en C#. La próximo interfaz presenta cómo declarar una interfaz con un método, una propiedad y un evento. Nota que el nombre de la interfaz inicia con una letra I. Esto es una convención de codificación impuesto por el .NET Framework, pero está tan desarrollado que nosotros seguiremos esa convención. interface IAlgunaInterfaz { void Foo(); string Goo { get; set; event EventHandler Hoo; Cuando una clase se adhiere al contrato de una interfaz, se dice que la implementa. La implementación de una interfaz se hace parecida al de la herencia de una clase, con la salvedad que las interfaces siempre van después de la clase heredada (si existiese; separadas por comas). Además, puede implementarse más de una interfaz en una clase alguna (en cuyo caso se separan por una coma). Por otro lado, al implementar una interfaz, la clase en cuestión tiene que definir TODOS os fundamentos declarados por la interfaz, o si no recibirá un yerro de compilación. interface IComparable { bool IsEqualTo(
object obj); class Shape : IComparable { public abstract bool IsEqualTo(
object other); class Color : IComparable { public bool IsEqualTo(
object other) { ? Y entonces ahora sí, vuestro algoritmo de búsqueda podría quedar así: static class Search { public static bool Exists(ListIComparable> objects, object objToFind) { foreach (IComparable obj in objects) { if (obj.IsEqualTo(objToFind)) return true; return false; ? ListIComparable> objs = new ListIComparable>(); objs.Add(new Point(10, 15)); objs.Add(new Line(new Point(42, 42), new Point(25, -14))); objs.Add(new Color(255, 0, 255)); objs.Add(new Circle(new Point(17, 15), 12)); objs.Add(new Point(42, 42)); objs.Add(new Color(128, 99, 128)); bool exists = Search.Exists(new Point(42, 42)); Console.WriteLine("Punto 42, 42 encontrado: {0", exists); exists = Search.Exists(new Color(128, 99, 128)); Console.WriteLine("Color [128, 99, 128] encontrado: {0", exists); Por supuesto, siempre podremos referenciar una instancia de una clase por la interfaz que implementa: IComparable c = new Point(0, 0); Pero jamás podremos instanciar una interfaz directamente: IComparable c = new IComparable(); // yerro de compilación Ahora bien, hasta este momento hemos visto cómo una interfaz se implementa en una clase con métodos particulares. Pero ¿qué pasa cuando una clase implementa dos interfaces que tienen un mismo nombre? Digo, se supone que os nombres deben ser representativos, pero pues puede ser que dos nombres sean semánticamente distintos y queramos realizar la diferencia. Este es el caso. interface IShape { void Draw(); interface IControl { void Draw(); class Circle : IShape { public void Draw() { ? class Button : IControl { public void Draw() { ? class CircledButton : IControl, IShape { Circle circle; Button button; public void Draw() { ??? En el ejemplo previo hemos creado una interfaz para definir el funcionamiento de una figura, y el de un control. Definimos un círculo que implementa IShape, y un Button que implementa IControl. Por supuesto, ahora deseamos crear un botón circular, por tanto implementamos IControl e IShape. Sin embargo, al implementar Draw: ¿qué versión ha de llamar, la de Circle o la de Button? Debería llamar una u otra, dependiendo de si quien manda llamar está
pensando en un IControl o un IShape? Esto es probable mediante la implementación explícita de una interfaz. Para realizar la implementación explícita, en contraposición de la implícita que ya hemos visto, lo que hacemos es en el método, propiedad o evento, ubicar el nombre de la interfaz, seguida de un punto, seguida de? bueno, mejor un ejemplo: class CircledButton : IControl, IShape { Circle circle; Button button; void IControl.Draw() { button.Draw(); void IShape.Draw() { circle.Draw(); Como puedes ver, no hay modificadores. Esto es porque un método/propiedad/evento implementado explícitamente NO PUEDE ser invocado desde la clase, sino que tiene que ser invocado desde la interfaz. CircledButton cb = new CircledButton(); cb.Draw(); // no compila IControl b = cb; b.Draw(); // ok, se invoca a IControl.Draw IShape c = cb; c.Draw(); // ok, se invoca a IShape.Draw Por supuesto, si no nos importa y no deseamos diferenciarla, podemos abandonar la implementación implícita. class CircledButton : IControl, IShape { Circle circle; Button button; void Draw() { circle.Draw(); button.Draw(); Interfaces genéricas También cabe la pena recordad que las interfaces pueden ser genéricas, aplicando las mismas normas que para las clases. interface IComparableT> { bool IsEqualTo(T other); class Shape : IComparableShape> { public bool IsEqualTo(Shape other) { ? ... class Color : IComparableColor> { public bool IsEqualTo(Color other) { ? ... Ahora bien, las interfaces genéricas son sujetas a un concepto significativo de herencia que en .NET se implementó hasta la versión 4.0: la covarianza y contravarianza. Verdaderamente esta
acceso no entrará a fondo en ambos temas, dado que es uno extenso. Sin embargo, echémosle un vistazo. Cuando en determinado fundamento genérico definimos un parámetro genérico T, este puede aceptar tres tipos de parámetros cuando se instancia: 1.- El tipo de dato T tal cual. Esto es lo que hacemos desde .NET 1.0. 2.- Se convierte entre T y tipos más especializados (i.e. clases derivadas). 3.- Se convierte entre T y tipos más globales (i.e. clases base). Pues bien, al punto 2 se le llama covarianza, y al punto 3, contravarianza. Para declarar un parámetro genérico como covariante, se le agrega la palabra reservada out. Para declararlo como contravariante, se le agrega la palabra reservada in. class ClaseT> { ? // ni uno ni otro class ClaseCVin T> { ? // contravariante class ClaseCOout T> { ? // covariante // ejemplo de covarianza ListClaseCOobject>> co = new ListClaseCOobject>>(); co.Add(new ClaseCOobject>()); // ok co.Add(new ClaseCOstring>()); // ok, porque ClaseCO es covariante y string // deriva de object. ListClaseCOstring>> co = new ListClaseCOstring>>(); co.Add(new ClaseCOstring>()); // ok co.Add(new ClaseCOobject>()); // error, porque ClaseCO es covariante y object // no derivda de string // ejemplo de contravarianza ListClaseCVobject>> cv = new ListClaseCVobject>>(); cv.Add(new ClaseCVobject>()); // ok cv.Add(new ClaseCVstring>()); // error, porque ClaseCV es contravariante y // string es más especializada que object ListClaseCVstring>> cv = new ListClaseCVstring>>(); cv.Add(new ClaseCVstring>()); // ok cv.Add(new ClaseCVobject>()); // ok, porque ClaseCV es contravariante y object // es más genérica que string Más sobre covarianza y contravarianza en este enlace Bueno, pues las interfaces pueden declararse como covariantes o contravariantes de la misma forma. De hecho esto puede ayudarnos con vuestro algoritmo de búsqueda. Lo que necesitamos aquí es declarar al parámetro de vuestra interfaz como covariante. interface IComparableout T> { bool IsEqualTo(T other); class Shape : IComparableShape> { public bool IsEqualTo(Shape other) { ? ... class Color : IComparableColor> { public bool IsEqualTo(Color other) { ? ... static class Search { public static bool Exists(ListIComparableobject>> objects, object objToFind) { foreach (IComparableobject> obj in objects) { if (obj.IsEqualTo(objToFind)) return true; return false; ? ListIComparableobject>> objs = new ListIComparableobject>>(); objs.Add(new Point(10, 15)); // a pesar de que implementa IComparableShape> objs.Add(new Line(new Point(42, 42), new Point(25, -14))); objs.Add(new Color(255, 0, 255)); // a pesar de que implementa IComparableColor> objs.Add(new Circle(new Point(17, 15), 12)); objs.Add(new Point(42, 42)); objs.Add(new Color(128, 99, 128)); bool exists = Search.Exists(new Point(42, 42)); Console.WriteLine("Punto 42, 42 encontrado: {0", exists); exists = Search.Exists(new Color(128, 99, 128)); Console.WriteLine("Color [128, 99, 128] encontrado: {0", exists); Las interfaces de .NET Ya que hemos platicado de interfaces, como último tema, me gustaría exponer determinadas de las interfaces que ya tiene .NET. ¡En efecto! Hay muchas que son muy utilizadas a lo largo de la plataforma, por lo que es significativo conocerlas. La primera sobre la que quiero hablar es la interfaz IDisposable. Esta interfaz representa un objeto que tiene recursos que han de ser liberados en manera determinista, y que no puede esperar a que el colector de basura os libere. Esta interfaz tiene un solo método, llamado Dispose. En este método debe liberarse os recursos asociados. Supongamos que poseemos una clase, llamada DataLayer, que contiene un fundamento SqlConnection, que representa una conexión a una fundamento de
datos de SQL Server. Naturalmente deseamos que cuando ya no se ocupe DataLayer, se libere la conexión a la DB. Entonces implementamos IDisposable. class DataL
ayer : IDisposable { SqlConnection _cnn; ? void Dispose() { if (_cnn != null) { _cnn.Dispose(); _cnn = null; DataLayer d1 = new DataLayer(); ? // usamos el objeto d1.Dispose(); // liberamos recursos DataLayer d2 = null; try { d2 = new DataLayer(); ? // usamos el objeto catch { ? finally { if (d2 != null) d2.Dispose(); using (DataLayer d3 = new DataLayer()) { ? // usamos el objeto // no necesitamos llamar a Dispose DataLayer d4 = new DataLayer(); using (d4) { ? // usamos el objeto // no necesitamos llamar a Dispose Hemos ya que cuatro ejempos. El primero presenta cómo invocar Dispose de manera directa, y el segundo, dentro de un bloque try-catch-finally. Os próximos dos son especiales, pues emplean la palabra reservada using. Esta palabra acepta (dentro de os paréntesis) un objeto que debe implementar IDisposable. Si no implementa IDisposable, la sentencia motivo un yerro de compilación. La sentencia se encarga de llamar a Dispose cuando se consigue la llave de cierre del bloque. Si surgiese una excepción, el bloque using se encarga de llamar a Dispose antes de arrojar la excepción, parecida al bloque try-catch-finally. Por tanto, no es indispensable llamar a Dispose manualmente. Por cierto, la llamada a Dispose jamás debería arrojar una excepción. Vamos con la próximo interfaz. En os ejempos que mostramos hace rato, creábamos una interfaz llamada IComparable. Pues bien, esta interfaz ya tiene lugar y tiene el mismo nombre: IComparable. Esta interfaz tiene un método: CompareTo. Este método debe volver -1 cuando el objeto actual sea semánticamente menor que el objeto a comparar, 0 si son iguales, y 1 si el otro es gran. En el caso de
variables numéricas, es claro. Pero por ejemplo, una cadena de texto implementa el gran y menor respecto al orden alfabético de sus letras. class Point : IComparable { public int X { get; set; public int Y { get; set; public int CompareTo(object other) { Point pt = other as Point; if (pt == null) throw new ArgumentException("other no es un Point"); int val; if (X pt.X) val = -1; else if (X > pt.X) val = 1; else if (Y pt.Y) val = -1; else if (Y > pt.Y) val = 1; else val = 0; return val; La interfaz IComparable se emplea mucho en algoritmos para ordenar
colecciones de datos. Por tanto, es significativo implementarla en vuestros tipos básicos. Otra interfaz significativo es muy parecida a vuestra buena IComparable. Se llama: IEquatableT>. Esta interfaz tiene un método, Equals(T t), que indica si la instancia actual es idéntico a cierta otra insatncia. En otras palabras, que Equals regrese true equivaldría a que CompareTo regresara 0. class Point : IEquatablePoint> { public int X { get; set; public int Y { get; set; public bool Equals(Point other) { bool equals = false; if (other != null) { equals = X == other.X && Y == other.Y; return equals; Una interfaz clásica también lo es ICloneable. Esta interfaz determina un método, Clone, cuya finalidad consiste en crear una copia idéntica del objeto actual. class Point : ICloneable { public int X { get; set; public int Y { get; set; public Point Clone() { return new Point { X = this.X, Y = this.Y ; object ICloneable.Clone() { return new Point { X = this.X, Y = this.Y ; Nota que aquí implementamos dos Clones, uno de ellos explícitamente. Esto, porque el explícito regresa un object. El otro, regresa un Point fuertemente tipado. Así cumplimos con la interfaz, pero la clase recibe un valor tipado (Point). La última interfaz que es súper utilizada es IEnumerable (y su variante genérica IEnumerableT>). Esta interfaz expone un enumerador. Afortunadamente, ya he hablado de ella en otra entrada, así que hasta aquí la dejamos. También hay otras interfaces interesantes, que escaso a escaso iremos explorando. Pero por el momento, creo que son las más importantes. ¡Sigue explorando! Conclusiones Esta
acceso fuese larga. Comenzamos hablando de las clasificaciones del mundo, y escaso a escaso fuimos introduciendo el asunto desde el punto de vista del diseño de software. Pusimos varios ejemplos sobre cómo clasificar, y vimos el concepto de relación de identidad. Una vez dejada clara la teoría, vimos cómo se implementa la clasificación y relación de identidad en C# y .NET, y cómo podemos crear una jerarquía de clases. Posteriormente, vimos el concepto de comportamiento y contratos, y cómo se implementan en C# mediante el concepto de interfaces. Exploramos varios conceptos relacionados, como interfaces explícitas y covarianza/contrvarianza, y finalmente vimos determinadas interfaces
comunes en .NET. Ha sido larguito, espero que haya valido la pena. ¡No dudes en abandonar tus preguntas!