Fundamentos de Programación Traducido
Fundamentos de Programación Traducido
Fundamentos de Programación Traducido
Programación
Construyendo Mejor Software
WWW.CODEBETTER.COM
Fundamentos de Programación 4
Licencia
El libro Fundamentos de Programación está licenciado bajo la licencia Attribution-NonCommercial-
Share-Alike 3.0 Unported.
Básicamente usted es libre de copiar, distribuir, y mostrar el libro. Sin embargo, pido que siempre se me
atribuya el libro a mí, Karl Seguin, no lo use para fines comerciales y comparta cualquier alteración que
haga bajo la misma licencia.
http://codebetter.com/blogs/karlseguin/archive/2009/05/25/revisiting-codebetter-canvas.aspx
Reconocimientos
Hay incontables personas que merecen las gracias. Este libro es una pequeña contribución al
incalculable tiempo donado y conocimiento compartido por la comunidad de software en general. Sin la
calidad de los libros, foros, grupos de usuarios, blogs, librerías y proyectos open source, estaría todavía
tratando de hacer que mi script ASP terminara en tiempo mientras se ciclaba en un recordset (un
estúpido MoveNext).
No es sorpresa que la comunidad de software ha aprovechado la aperture en internet más que otra
profesión para avanzar en nuestra causa. Lo que es sorprendente es como el fenómeno parece haberse
ido sin notarse. ¡Bien!
Claro, hay una persona especial sin la cual esto no hubiera ocurrido.
A Wendy,
La gente me llama suertudo por estar con alguien tan bonita e inteligente como tú. No saben ni la mitad
de ello. Tú no solo eres bonita e inteligente, pero me dejas estar demasiado tiempo en mi computadora,
ya sea trabajando, aprendiendo, escribiendo o jugando. Además eres por demás contenta de leer sobre
mis cosas o escucharme hablar de cosas sin sentido. No te tengo el aprecio suficiente como debería.
Tabla of Contenido
Acerca del autor..........................................................................................................................................6
ALT.NET.......................................................................................................................................................7
Objetivos.................................................................................................................................................8
Simplicidad..............................................................................................................................................8
YAGNI (You aren’t going to Need It – No vas a necesitarlo)....................................................................8
Ultimo momento de responsabilidad......................................................................................................9
DRY..........................................................................................................................................................9
Explicitud y cohesión...............................................................................................................................9
Acoplamiento..........................................................................................................................................9
Pruebas unitarias e integración continúa..............................................................................................10
En este capitulo.....................................................................................................................................10
.................................................................................................................................................................. 10
Diseño dirigido por dominios DDD............................................................................................................11
Diseño dirigido por Dominios/Datos......................................................................................................11
Usuarios, Clientes e Inversionistas.........................................................................................................12
El objeto de dominio.............................................................................................................................13
Interfaz de Usuario (IU).........................................................................................................................16
Trucos y pistas.......................................................................................................................................17
Patrones de fábrica............................................................................................................................17
Modificadores de acceso...................................................................................................................18
Interfaces...........................................................................................................................................18
Ocultar información y Encapsular......................................................................................................19
En este capítulo.....................................................................................................................................19
.................................................................................................................................................................. 20
Persistencia...............................................................................................................................................21
La brecha...............................................................................................................................................21
DataMapper..........................................................................................................................................22
Tenemos un problema.......................................................................................................................24
Limitaciones.......................................................................................................................................26
En este capítulo.....................................................................................................................................27
.................................................................................................................................................................. 27
Inyección de dependencias.......................................................................................................................28
No evites el acoplamiento como si fuera una plaga..............................................................................30
Inyección de dependencias....................................................................................................................31
Constructor de inyección...................................................................................................................31
Marcos de referencia.........................................................................................................................33
Una última mejora.............................................................................................................................34
En este capítulo.....................................................................................................................................35
.................................................................................................................................................................. 35
Pruebas de Unidad....................................................................................................................................36
ALT.NET
H ace algunos años fui afortunado al dar un giro en mi carrera de programación. La oportunidad
de un mentor solido se presentó por sí sola y la aproveche al máximo. En un periodo de pocos
meses mis habilidades en programación crecieron exponencialmente y a través de los últimos
años he continuado refinando mi arte. Sin duda, aún tengo mucho que aprender y en cinco años más
veré el código que escribí el día de hoy y me sentiré avergonzado. Yo solía estar seguro de mis
habilidades en programación pero solo una vez que acepte que sabía muy poco, y probablemente así
era, empecé a entender.
En realidad, los dos no son realmente comparables. El estilo MSDN libremente define una forma
específica de construir un sistema directo a cada llamado individual de un método. (Después de todo,
¿no es solamente la documentación de referencia de API la única razón por la que visitamos MSDN?).
Mientras que ALT.NET se centra en tópicos más abstractos mientras provee una implementación más
específica. Como Jeremy Miller lo dice: La comunidad .NET ha puesto mucho énfasis en aprender
detalles de API’s, marcos de referencia y no suficiente énfasis en diseñar y fundamentos de codificación.
Como ejemplo concreto y relevante, el estilo MSDN favorece fuertemente el uso de DataSets y
DataTables para toda la comunicación con bases de datos. ALT.NET sin embargo, centra su discusión en
patrones de diseño persistentes, desajuste en impedancia de objetos relacionales así como
implementaciones específicas tales como NHibernate (Mapeo O/R), MonoRail (ActiveRecord) así como
DataSets y DataTables. En otras palabras, a pesar de lo que mucha gente piensa, ALT.NET no es acerca
de ALTernativas al estilo MSDN, sino que una creencia de que los desarrolladores deben saber y
entender soluciones y aproximaciones alternativas de la cual el estilo MSDN forma parte.
Por supuesto, pensando en la descripción anterior es claro pensar que usar la ruta ALT.NET requiere un
gran compromiso así como un conocimiento más amplio. La curva de aprendizaje es considerable y
recursos útiles apenas empiezan a surgir (esta es la razón por la que decidí iniciar estas series). Sin
embargo, la recompensa vale la pena; para mí, mi éxito profesional me ha llevado una felicidad personal
mayor.
Objetivos
Aunque simplista, cada decisión de programación que hago está en gran parte basada en la capacidad
de mantenimiento. La capacidad de mantenimiento es la piedra angular del desarrollo empresarial.
Lectores frecuentes de CodeBetter están cansados de escuchar acerca de esto, pero hay una buena
razón por la que hablamos de la capacidad de mantenimiento tan seguido – es la llave para ser un gran
desarrollador de software. Primero, los estudios y experiencia de primera mano nos dicen que los
sistemas gastan una considerable cantidad de tiempo (Más del 50%) en mantenimiento, cambios,
solución a problemas o soporte. Segundo, la creciente adopción de desarrollos iterativos significa que
los cambios y características son constantemente hechos para código existente (incluso si no has
adoptado desarrollo iterativo tal como Agile, tus clientes probablemente continúan solicitándote hacer
todo tipo de cambios.). En corto, una solución de fácil mantenimiento no solo reduce tus costos, sino
también incrementa el número y la calidad de las características que vas a ser capaz de entregar.
Incluso si eres relativamente nuevo en la programación, hay una buena oportunidad de que ya te hayas
formado opiniones acerca de lo que es o no es una solución de fácil mantenimiento basado en tu
experiencia trabajando con otros, tomando la aplicación de alguien más o incluso tratando de arreglar
algo que escribiste unos meses atrás. Una de las cosas más importantes que puedes hacer es tomar
nota concienzudamente cuando algo no se vea correcto y buscar en internet por una mejor solución.
Por ejemplo, aquellos que hemos gastado años programando en ASP clásico, saben que la gran
integración entre código y HTML no era lo ideal.
Crear código de fácil mantenimiento no es la cosa más trivial. Al inicio, es necesario ser muy cuidadoso y
con el tiempo las cosas empiezan a ser más naturales. Como puedes imaginarte, no somos los primeros
en escribir sobre crear código de fácil mantenimiento. Hasta aquí, hay algunas ideologías con las que te
debes de ir familiarizando. Conforme avancemos, tomate el tiempo para analizarlas más
profundamente, busca en internet para obtener más información a detalle y lo más importante, trata de
ver como se podrían aplicar a algún proyecto en el que hayas trabajado recientemente.
Simplicidad
La mejor herramienta para hacer código de fácil mantenimiento es conservarlo tan simple como sea
posible. Una creencia común es que para que sea de fácil mantenimiento, un sistema debe de ser pre
construido para acomodar cualquier solicitud de cambio posible. He visto sistemas construidos en meta-
repositorios (tablas con una columna llave y una columna Valor) o configuraciones complejas en XML,
que son pensadas para manejar cualquier cambio que los clientes le pidan al equipo. Estos sistemas no
solo tienden a tener serias limitaciones técnicas (el desempeño puede verse reducido) pero casi siempre
fallan para lo que están hechas (Veremos más de esto cuando hablemos acerca de YAGNI). En mi
experiencia, el camino real para la flexibilidad es mantener el sistema tan simple como sea posible, para
que tú, u otro desarrollador, pueda fácilmente leer tu código, entenderlo, y hacer los cambios
necesarios. ¿Para qué construir un máquina de reglas configurable cuando todo lo que quieres que se
haga es verificar que el nombre de usuario es de la longitud correcta? más adelante veremos como un
desarrollo basado en pruebas nos puede ayudar a alcanzar altos niveles de simplicidad asegurándonos
que nos enfocamos en lo que nuestro cliente nos pagó para hacer.
“No vas a necesitarlo” es una creencia de programación extrema que dice que no deberías construir algo
ahora porque crees que lo vas a necesitar en un futuro. La experiencia nos dice que probablemente no
lo necesitarás o que necesites algo completamente diferente. Puedes gastar un mes construyendo un
sistema sorprendente y flexible para un cliente que tenga 2 líneas simples de correo que sea totalmente
inútil. Justo el otro día empecé a trabajar en un sistema de reporteo abierto hasta darme cuenta que
entendí mal un correo electrónico y lo que el cliente realmente quería era un reporte simple diario que
termino tomándome 15 minutos para construirlo.
DRY
La duplicación de código puede provocar dolores de cabeza a los programadores. No solo hacen difícil el
cambio de código (debido a que tienes que encontrar todos los lugares que hace lo mismo), además
también tiene el potencial de introducir serios errores y hacerle la vida innecesariamente difícil para
nuevos desarrolladores que se unan al desarrollo. Al seguir el principio No te repitas a ti mismo (DRY –
Don’t Repeat Yourself) a través del ciclo de vida de un sistema (historias de usuarios, diseño, código,
unidades de prueba y documentación) terminaras con un código más limpio y mucho más fácil de
mantener. Ten en mente que el concepto va más allá de copiar y pegar y apunta por eliminar
funcionalidad/comportamiento duplicado en todas las formas. Encapsulación de objetos y código
altamente cohesivo puede ayudarnos a reducir la duplicación.
Explicitud y cohesión
Suena sencillo, pero es importante cerciorarse de que tu código haga exactamente lo que dice que va a
hacer. Esto significa que las funciones y las variables deben ser nombradas apropiadamente y usar casos
estandarizados y cuando sea necesario proporcionar la documentación apropiada. Una clase de
Productores debe hacer exactamente lo que tú, otros desarrolladores en el equipo y tu cliente piensen
que debe ser. Además, tus clases y métodos deben ser altamente cohesivos – es decir, deben tener un
propósito único. Si estas escribiendo una clase Cliente que esté comenzando a manejar datos de
Órdenes hay una muy alta posibilidad de que necesites crear una clase Orden. Las clases responsables
de una multiplicidad de componentes distintos llegan a ser rápidamente inmanejables. En el capítulo
siguiente, veremos las capacidades de la programación orientada a objetos en lo que se refiere a crear
código explícito y cohesivo.
Acoplamiento
El acoplamiento se produce cuando dos clases dependen una de la otra. Cuando sea posible, vas a
querer reducir el acoplamiento con el fin de reducir al mínimo el impacto causado por cambios y
aumentar la capacidad de prueba del código. Reducir o incluso eliminar el acoplamiento es más fácil de
lo que la mayoría de las personas piensan; existen estrategias y herramientas que te ayudarán. El truco
es identificar el acoplamiento indeseable. Cubriremos el acoplamiento, en detalle más adelante.
En este capítulo
Aunque este capítulo no tenía nada de código, nos las hemos ingeniado para cubrir varios temas. Como
quiero que esto sea más experiencia de primera mano que teoría, nos zambulliremos primero en código
real a partir de aquí. Espero que ya hayamos aclarado algunas palabras comunes que has estado
escuchando mucho últimamente. Los siguientes capítulos crearan los fundamentos para el resto de
nuestro trabajo cubriendo OOP (programación orientada a objetos) y persistencia a un nivel alto. Hasta
entonces, espero que inviertas algo de tiempo investigando algunas de las palabras claves que he
lanzado. Dado que la mejor herramienta es tu propia experiencia, piensa en proyectos actuales y
recientes y trata de listar cosas que no trabajaron tan bien como los que si funcionaron.
E ra de esperarse que iniciara hablando acerca de diseño dirigido por dominios y programación
orientada a objetos. En un principio pensé que podría evitar el tema al menos por un par de
artículos, pero eso haría que ambos, tanto ustedes como yo, nos desanimáramos. Existe un
número limitado de maneras prácticas para diseñar el núcleo de su sistema. Un enfoque muy común
para los desarrolladores de .NET que consiste en utilizar un modelo centrado en datos. Es muy probable
que usted ya sea un experto en este enfoque – repeticiones anidadas dominadas, el evento
ItemDataBound siempre útil y habilidades para navegar con DataRelations. Otra solución, que es la regla
para los desarrolladores de Java y que rápidamente ha ido ganando terreno en la comunidad .NET,
favorece al enfoque centrado en dominios.
El diseño dominio-céntrico o como es nombrado generalmente, diseño dirigido por dominios (DDD), se
centra en el dominio del problema en general – lo cual no sólo involucra datos, sino todo el entorno.
Entonces no sólo nos enfocamos en el hecho de que un empleado tiene un Nombre, sino que en que él
puede tener un incremento de sueldo. El Dominio del Problema es una forma de expresar el negocio
para el cual se está construyendo un sistema. La herramienta que empleamos es programación
orientada a objetos – el emplear un lenguaje orientado a objetos como C# o VB.NET no significa que
necesariamente se esté usando programación orientada a objetos (OOP)- .
Las descripciones anteriores son algo engañosas – de cierta forma implican que si utilizara DataSets no
necesitaría preocuparse de, o estar preparado para ofrecer el comportamiento para incrementar el
sueldo de un empleado. Seguramente ese no es el caso – de hecho ha sido muy trivial presentarlo así.
Un sistema data–céntrico no está desprovisto de comportamientos ni los trata como una idea posterior.
El DDD simplemente se adapta mejor a la gestión de sistemas complejos de forma más fácil de
mantener por una serie de razones – que trataremos en los capítulos siguientes. Esto no lo hace mejor
que el dirigido a datos – esto simplemente hace al dirigido a dominios mejor que al dirigido a datos en
algunos casos y lo contrario también es cierto. Probablemente ha leído todo esto antes y al final,
simplemente tiene que dar un salto de fe y tentativamente aceptar lo que predicamos – al menos lo
suficiente para que usted pueda juzgar por sí mismo.
(Esto puede ser un tanto rudo y contradictorio a lo que dije en mi introducción, pero el debate entre el
camino MSDN y el de ALT.NET podría resumirse como una batalla entre desarrollar dirigiendo por datos
o desarrollar dirigiendo por dominios. No obstante de que los verdaderos ALT.NETeros deberían de
apreciar que el diseño dirigido a datos es sin duda una elección adecuada en algunas situaciones. Creo
que gran parte de la hostilidad entre los "campamentos" es que Microsoft favorece
desproporcionadamente el diseño dirigido a datos a pesar del hecho de que no se ajusta bien con lo que
la mayoría de los desarrolladores .NET están haciendo (desarrollo empresarial) y, cuando se utilizan
incorrectamente, produce menos código que es más fácil de mantener. Muchos programadores, tanto
dentro como fuera de la comunidad de .NET, probablemente se están rascando sus cabezas tratando de
comprender por qué Microsoft insiste en ir en contra de la sabiduría convencional y mantener
torpemente lo que siempre ha tenido.)
decisiones equivocadas, tal vez porque no entienden plenamente la necesidad de un usuario, tal vez
porque usted cometa un error en la información que le proporcione o tal vez porque ellos darán,
incorrectamente, mayor prioridad a sus propias necesidades que a cualquier otra. Como desarrollador,
es su trabajo apoyarlos para que cumplan con su función.
Ya sea que esté construyendo un sistema comercial o no, la medida definitiva de su éxito consistirá en
cómo se sienten los usuarios con él. Así que mientras esté trabajando estrechamente con su cliente, es
de esperar que ambos se preocupen en las necesidades de los usuarios. Si usted y su cliente toman en
serio la creación de sistemas para los usuarios, yo le recomiendo leer historias de usuario – un buen
punto de partida es el excelente libro “User Stories Applied 1” de Mike Cohn.
Por último, y como razón principal de la existencia de esta pequeña sección, están los expertos en el
dominio. Expertos en el dominio son las personas que conocen todos los pormenores sobre el mundo en
el que vive su sistema. Hace poco formé parte de un proyecto de desarrollo muy grande para un
Instituto financiero y hubo literalmente cientos de expertos en el dominio de los cuales la mayoría eran
economistas o contadores. Se trataba de personas que están tan entusiasmadas con lo que hacen así
como usted lo está acerca de la programación. Cualquier persona puede ser un experto en su dominio –
un cliente, un usuario, un inversionista y, eventualmente, incluso usted. Su dependencia de expertos en
los dominios crece con la complejidad de los sistemas.
El objeto de dominio
Como he dicho antes, la OOP es la herramienta que utilizaremos para darle vida a nuestro diseño
dominio-céntrico. En especial, confiaremos en que el poder de las clases y de la encapsulación. En este
capítulo nos concentraremos en los conceptos básicos referentes a las clases y en algunos trucos para
empezar a usarlas – muchos desarrolladores ya conocerán todo lo que se cubre aquí. No trataremos
persistencia (hablar a la base de datos) aún. Si es nuevo en este tipo de diseño, se verá a usted mismo
preguntándose constantemente sobre la base de datos y sobre el código para el acceso a datos. Intente
no preocuparse demasiado. En el siguiente capítulo trataremos los fundamentos de la persistencia, y en
los capítulos siguientes, examinaremos la persistencia en mayor profundidad.
La idea detrás del DDD es construir el sistema de manera que refleje el dominio del problema real que
está tratando de resolver. Aquí es donde entran los expertos en el dominio en acción – ellos lo ayudarán
a comprender cómo funciona el sistema actualmente (incluso si se trata de un proceso manual con
papel) y cómo debería trabajar. Al principio se sentirá abrumado por sus conocimientos – le hablará de
lo que usted nunca ha oído hablar y será sorprendido por su aspecto atónito. Utilizará tantas siglas y
palabras especiales que podrá comenzar a preguntarse si usted está o no a la altura. En última instancia,
esto es el verdadero propósito de un desarrollador empresarial –comprender el dominio del problema.
Ya sabe cómo programar, pero ¿sabe cómo programar el sistema de inventario para que haga lo que
deben hacer? Alguien tiene que aprender el mundo de la otra persona, y si el experto en el dominio
aprende a programar, nos quedaremos sin trabajo.
1
URL en Amazon: http://www.amazon.com/User-Stories-Applied-Development-Addison-Wesley/dp/0321205685
Cualquier persona que pase por lo anterior sabe que comprender un nuevo negocio es la parte más
complicada de cualquier trabajo de programación. Por ello, hay beneficios reales al hacer que nuestro
código se parezca, en la medida de lo posible, al dominio. Esencialmente estoy hablando de
comunicación. Si los usuarios están hablando de resultados estratégicos, lo cual hace un mes no
significaba nada para usted y ahora su código habla de resultados estratégicos, significa que algunas de
las ambigüedades y gran parte de la mala interpretación están borradas. Muchas personas,
incluyéndome a mí, creemos que una buena forma de iniciar es comenzar con los sustantivos claves que
utilizan sus expertos en el negocio y los usuarios. Si se estuviera creando un sistema para un
concesionario de automóviles y hablara con un vendedor (que es probable que sea un usuario y un
experto en el dominio), sin duda hablará de clientes, carros, modelos, paquetes y actualizaciones, pagos
y así sucesivamente. Ya que son el núcleo de su negocio, es lógico que sean el núcleo de su sistema. Más
allá de los sustantivos está la convergencia en el lenguaje de la empresa, que ha llegado a ser conocida
como el idioma ubicuo (ubicuo significa presente en todas partes). La idea es que un único idioma
compartido entre los usuarios y el sistema es más fácil de mantener y tiene menor probabilidad de ser
mal interpretado.
Cómo iniciar exactamente es algo que realmente tiene que decidir usted. Hacer DDD no significa que
necesariamente tiene que iniciar con el modelado del dominio (¡aunque es una buena idea!), más bien
significa que debe centrarse en el dominio y dejarlo que dirija sus decisiones. Al principio muy bien
puede iniciar con su modelo de datos, cuando exploremos el desarrollo dirigido por pruebas tomaremos
un enfoque diferente sobre la creación de un sistema que se adapta muy bien con DDD. Por ahora, no
obstante, supongamos que hemos hablado con nuestro cliente y con unos vendedores y hemos
obtenido que el punto neurálgico es mantener un seguimiento de la interdependencia entre las
opciones de actualización. Lo primero que haremos será crear cuatro clases:
Después agregaremos un poco de código basado en algunas cosas que hemos asumido de forma
correcta y segura:
Esto es algo un tanto simple. Hemos añadido algunos campos tradicionales (id, nombre), algunas
referencias (Carro y Modelo, Actualizaciones), y hemos añadido una función a la clase Carro. Por ahora
podemos olvidarnos de las modificaciones y empezar a escribir un poco sobre el comportamiento real.
Primero, hemos implementado el método Agregar. Después hemos implementado un método que nos
permite obtener todas las actualizaciones faltantes. Nuevamente, este es sólo el primer paso; el
siguiente paso podría ser rastrear qué actualizaciones son las responsables de las actualizaciones
faltantes, por ejemplo: usted debe seleccionar 4 X 4 que es el valor que corresponde a Tracción; sin
embargo, hasta aquí llegaremos por ahora. El propósito era sólo señalar cómo podríamos comenzar y
cómo se puede ver al inicio.
Recuerde que desea escribir código cohesionado. Su lógica de ASP.NET debería centrarse en hacer una
cosa y hacerla bien – cualquiera estará de acuerdo en que es administrar la página, lo cual significa que
no puede hacer la funcionalidad de dominio. También, la lógica localizada en código oculto viola el
principio de “No lo Repitas”, simplemente porque es difícil reutilizar el código dentro de un archivo
aspx.cs.
A pesar de lo dicho anteriormente, usted no puede esperar demasiado para trabajar en la interfaz de
usuario. Ante todo, queremos obtener retroalimentación de nuestros clientes y usuarios tan pronto
como sea posible. Dudo que ellos se queden impresionados si le enviamos un montón de archivos .cs
o .vb con nuestras clases. En segundo lugar, al hacer uso real de la capa de dominio se van a revelar
algunos errores y deslices. Por ejemplo, la naturaleza desconectada de la web podría significar que
tenemos que hacer pequeños cambios a nuestro mundo orientado a objetos puro con el fin de lograr
una mejor experiencia de usuario. En mi experiencia, las pruebas unitarias son demasiado estrechas
para detectar estas peculiaridades.
También le alegrará saber que ASP.NET y WinForms tratan con código dominio-céntrico, así como con
clases data-céntricas. Usted puede enlazar datos a cualquier colección .NET, sesiones de uso y cachés
como usted lo hacen normalmente y a hacer cualquier otra cosa a la que esté acostumbrado. De hecho,
en general, el impacto en la interfaz de usuario es probablemente el menos significativo. Por supuesto,
no debería sorprenderle saber que los ALT.NETeros también creen que usted debe mantener su mente
Trucos y pistas
Terminaremos este capítulo revisando algunas cosas útiles que podemos hacer con las clases. Sólo
cubriremos la punta del iceberg, pero esperando que la información le ayude a levantarse con el pie
derecho.
Patrones de fábrica
¿Qué hacemos cuando un cliente compra un carro nuevo? Obviamente necesitamos crear una instancia
nueva del Carro y especificar el modelo. La forma tradicional de hacerlo es utilizando un constructor y
simplemente instanciar un nuevo objeto con la nueva palabra clave. Un enfoque diferente es utilizar
una fábrica que cree la instancia:
private Carro()
{
_actualizaciones = new Lista <Actualización>();
}
public static Carro CrearCarro(Modelo modelo)
{
Carro carro = new Carro();
carro._modelo = modelo;
return carro;
}
}
Hay dos ventajas en este enfoque. En primer lugar, podemos regresar un objeto nulo, lo cual es
imposible con un constructor – esto podría o no ser útil en su caso en particular. En segundo lugar, si
tiene muchas formas de crear un objeto, tendrá la oportunidad de proporcionar nombres de función
más significativos. El primer ejemplo que me viene a la mente es cuando quiere crear una instancia de
una clase Usuario, a usted le agradaría tener Usuario.CrearPorCredenciales(string nombredeusuario,
string clave), Usuario.CrearPorId(int id) y Usuario.ObtenerUsuariosPorRol (string rol). Puede realizar la
misma funcionalidad con la sobrecarga del constructor, pero rara vez con la misma claridad. A decir
verdad, siempre me tomo un rato en la difícil tarea de elegir cual utilizar, por lo que realmente es una
cuestión de gusto y de sentimiento interior.
Modificadores de acceso
En cuanto usted se dé a la tarea de escribir clases que encapsulen el comportamiento del negocio, una
rica API emergerá para ser consumida por su interfaz de usuario. Es una buena idea mantener esta API
limpia y comprensible. El método más sencillo para ello es mantener la API pequeña y ocultar todo a
excepción de los métodos más necesarios. Algunos métodos obviamente requieren ser públicos y otros
privados, pero si aún no está seguro, seleccione el modificador de acceso más restrictivo y cámbielo sólo
cuando sea necesario. Yo hago buen uso del modificador interno en muchos de mis métodos y
propiedades. Los miembros internos sólo son visibles para otros miembros en el mismo ensamblado –
por lo que si usted separa físicamente sus capas a través de varios ensamblados (que generalmente es
una buena idea), usted podrá minimizar considerablemente su API.
Interfaces
Las interfaces jugarán un papel muy importante para crear código fácil de mantener. Las emplearemos
tanto para desacoplar nuestro código como para crear clases simuladas en nuestras pruebas unitarias.
Una interfaz es un contrato al cual cualquier clase implementada debe adherirse. Digamos que
queremos encapsular toda la comunicación con la base de datos dentro de una clase llamada
AccesoADatosSqlServer así:
Puede ver que el código de ejemplo tiene una referencia directa a AccesoADatosSqlServer – como
podría hacerlo con muchos otros métodos para comunicarse con la base de datos. Este código
altamente acoplado causa problemas para realizar cambios o pruebas (no podríamos realizar una
prueba a UnMétodoSimple sin tener el método ObtenerActualizaciones totalmente funcional. El
acoplamiento puede ser solucionado haciendo uso de una interfaz:
{
return new AccesoADatosSqlServer ();
}
}
Hemos introducido la interfaz junto con una clase auxiliar para devolver una instancia de dicha interfaz.
Si queremos cambiar nuestra implementación, por decir un AccesoADatosOracle, simplemente creamos
la nueva clase de Oracle, nos aseguramos de implementar la interfaz y de cambiar la clase de ayuda para
devolverla en su lugar. Y así, en vez de tener que hacer múltiples cambios (posiblemente cientos),
simplemente tendremos que hacer uno.
Esto es sólo un ejemplo sencillo de cómo podemos utilizar las interfaces para que ayuden a nuestra
causa. Podemos fortalecer el código al crear una instancia de nuestra clase a través de la configuración
dinámica de datos o introduciendo un framework especialmente diseñado para el trabajo (que es
exactamente lo que vamos a hacer). A menudo podremos ser favorecidos al programar con interfaces
en lugar de hacerlo con clases reales, por lo que, si no está familiarizado con ellas, le sugiero hacer
alguna lectura adicional.
En este capítulo
La razón por la que existe el desarrollo empresarial es que no existe un solo producto estándar que
pueda resolver con éxito todas las necesidades de un sistema complejo. Simplemente hay demasiados
requisitos extraños o entrelazadas y demasiadas reglas de negocio. Hasta la fecha, no hay paradigma
más adecuado para esta tarea que la programación orientada a objetos. De hecho, la OOP fue diseñada
con el objetivo específico de permitir a los desarrolladores modelar las cosas de la vida real. Todavía
puede ser difícil ver el valor a largo plazo del diseño dirigido por dominio. Compartir un lenguaje común
con el cliente y para los usuarios, además de tener una mayor capacidad para realizar pruebas podrían
no parecer necesarios. Esperemos que a medida de que avance a través de los capítulos restantes y que
experimente por su cuenta, empiece a adoptar algunos de los conceptos que se ajusten a sus
necesidades y a las de sus clientes.
Persistencia
3
LA INTENCIÓN DE CODD ERA LIBERAR A LOS PROGRAMADORES DE LA OBLIGACIÓN
DE SABER LA ESTRUCTURA FÍSICA DE LOS DATOS. NUESTRA INTENCIÓN ES
LIBERARLOS AÚN MÁS EVITANDO QUE TENGAN QUE SABER LA ESTRUCTURA LÓGICA.
– LAZY SOFTWARE
E n capítulos anteriores pudimos platicar ampliamente de DDD sin tener que hablar mucho de base
de datos. Si estás acostumbrado a programar con Datasets, entonces has de tener varias dudas
de como funcionaria esto. Los Datasets son grandiosos y te han resuelto demasiados problemas.
En este capítulo discutiremos sobre cómo lidiar con ‘persistencia’ usando DDD.
Escribiremos código para unir la brecha entre nuestros Objetos C# y nuestras tablas SQL. En secciones
posteriores veremos alternativas más avanzadas (dos diferentes formas de mapeo O/R) que, como los
Datasets, hace el trabajo difícil por nosotros. La intención de este capítulo es traer solución a las
discusiones previas tratando sobre patrones de persistencia más avanzados.
La brecha
Como sabes, tu programa trabaja en la memoria y requiere un lugar para guardar (o persistir)
información. Hoy en día la solución favorita es una base de datos relacional. Actualmente la persistencia
es un tema de suma importancia en el campo de desarrollo de software porque, sin la ayuda de
patrones y herramientas, es una cosa muy difícil de llevar a cabo. Con respecto a Programación
Orientada a Objetos, al reto se le ha asignado un término: Disparidad de Impedimento Relación-Objeto.
Básicamente esto significa que los datos de relación no se distribuyen perfectamente en objetos y los
objetos no se distribuyen perfectamente cuando se archivan datos de relación. Microsoft básicamente
trata de ignorar este problema y simplemente hizo una representación de relación dentro del código de
Objetos-orientados – una estrategia inteligente pero con sus pros y contras como el pobre
desenvolvimiento, descontrol de abstracciones, dificultad para someterlo a prueba, y problemas para
darle mantenimiento. (Por otro lado son bases de datos de orientadas a objetos que, hasta donde yo sé,
no han despegado tampoco).
Antes que tratar de ignorar el problema, podemos, y debemos afrontarlo. Debemos afrontarlo para que
podamos nivelar lo mejor de ambos mundos – reglas complejas de negocios implementadas en OOP y
archivado & recuperación de datos vía base de datos relacional. Claro, eso es calculando que podemos
cerrar la brecha. Pero ¿qué brecha exactamente? ¿Qué es esta disparidad de impedimento?
Probablemente estás pensando que no puede ser tan difícil invocar datos relacionales en objetos y de
vuelta a las tablas. Si lo pensaste, entonces ¡tienes toda la razón! (bueno, generalmente... por ahora
asumamos que siempre será un proceso simple)
DataMapper
Para proyectos pequeños con un puñado de clases de dominio y tablas de base de datos, mi sugerencia
siempre ha sido escribir manualmente un código que conecte estos dos mundos. Miremos un ejemplo
simple: La primera cosa que hacemos es expandir nuestra Clase de Actualización (nos estamos
enfocando solamente en las porciones de datos de nuestra Clase (los campos) desde que es donde
reside la Persistencia):
public int Id
{
get { return _id; }
internal set { _id = value; }
}
public string Name
{
get { return _name; }
set { _name = value; }
}
public string Descripción
{
get { return _descripción; }
set { _descripción = value; }
}
public decimal Precio
{
get { return _precio; }
set { _precio = value; }
}
Hemos incluido los campos básicos que esperabas ver en la Clase. A continuación crearemos la tabla que
almacenará, o persistirá, la información de Actualización.
Sin sorpresas todavía. Ahora viene la parte interesante (relativamente hablando), comenzamos a
construir nuestra capa de acceso de datos, que se sitúa entre el dominio y el modelo relacional (se
excluyeron las interfaces por propósitos de brevedad)
ExecuteReader es un método de ayuda que ligeramente reduce el código redundante que tenemos
que escribir.
RetrieveAllUpgrades es mas interesante ya que selecciona todas las actualizaciones y las carga en una lista
vía la función DataMapper.CreateUpgrade.
Si lo necesitamos, podemos re-utilizar CrearActualización tantas veces como sea necesario. Por ejemplo,
nos gustaría tener la habilidad de recuperar Actualizaciones por ID o por Precio – ambos serán nuevos
métodos en la Clase SqlServerDataAccess –
Obviamente, podemos aplicar la misma lógica cuando queramos archivar objetos Actualización de regreso
en la tabla, aquí te damos una posible solución:
Tenemos un problema
A pesar de que hemos tomado un ejemplo muy común y simple, todavía caemos en la temida
Disparidad de Impedimento. Note que nuestra capa de acceso de datos ( SqlServerDataAccess o DataMapper)
no maneja la tan necesaria colección ActualizacionesRequeridas. Esto es porque una de las cosas más
difíciles de manejar son relaciones. En el mundo de los dominios estas son referencias (o una colección
de referencias) hacia otros objetos; donde el mundo relacional usa claves externas. Esta diferencia es
una constante que se inclina del lado de los desarrolladores.
La solución no es tan difícil. Primero agregaremos una tabla de unificación que asociará las
actualizaciones con otras actualizaciones que sean requeridas (puede ser 0, 1 o más).
dataReader.NextResult();
while (dataReader.Read())
{
int IdActualización = dataReader.GetInt32(0);
int ActualizaciónRequeridaId = dataReader.GetInt32(1);
Actualización actualización;
Actualización requerida;
if (!localCache.TryGetValue(actualizaciónId, out actualización)
|| !localCache.TryGetValue(actualizaciónRequeridaId, out
requerida))
{
//sería buena idea lanzar una excepción
continue;
}
actualización.ActualizacionesRequeridas.Add(actualizaciónRequerida);
}
return actualizaciones;
}
}
Llamamos la tabla de unificación extra junto con nuestra solicitud inicial y creamos un diccionario local
de búsqueda para acceso rápido a nuestras actualizaciones por su ID. Lo siguiente es que repetimos a
través de la tabla de unificación, obtenemos las actualizaciones apropiadas de la búsqueda en el
diccionario y las adherimos a la colección.
No es la solución más elegante, pero trabaja muy bien! Tendremos la oportunidad de re-factorizar las
funciones un poco más para hacerla un poco más entendible, pero por ahora y para este simple caso,
hará el trabajo.
Limitaciones
Aunque solo estamos haciendo una observación inicial al bosquejo. Vale la pena observar las
limitaciones a las que nos hemos sometido. Una vez que continúe el camino de escribir manualmente
esta clase de código fácilmente se puede escapar de las manos. Si queremos agregar métodos de
Filtro/Orden nos conduciría a escribir SQL dinámica o tendríamos que escribir demasiados métodos.
Terminaríamos escribiendo un número exagerado de métodos RecuperarActualizacionesPorX que luciría
tediosamente similar uno del otro.
A menudo deseará tener relaciones de carga-fácil. Esto es, en vez de cargar todas las actualizaciones
requeridas al inicio, quizá solo queremos cargarlas cuando sea necesario. En este caso no es un
problema mayor desde que es solo una referencia de 32bit. Un mejor ejemplo sería el Modelo
Relacional de Actualización. Es relativamente sencillo implementar las cargas fácilmente, pero como los
mencionamos anteriormente, es demasiada repetición de código.
El asunto más importante tiene que ver con la identidad. Si llamamos RecuperarTodaslasActualizaciones
dos veces, debemos distinguir entre las instancias de Actualización, esto puede desembocar en
inconsistencias, dado que:
actualización1b.Precio = 2000;
actualización1b.Guardar();
Existen posiblemente más limitaciones, pero la última de la que hablaremos tiene que ver con unidades
de trabajo (infórmate más usando Google para buscar el patrón ‘Unidades de Trabajo’)
Esencialmente cuando creas tu código manualmente para tu capa de acceso de datos, tienes que
asegurarte que cuando se persiste un objeto tú también persistes, si es necesario, actualiza objetos con
referencia. Si se está trabajando en la sección administrativa de nuestro sistema de auto ventas, es
deberá por ejemplo crear una nuevo Modelo y agregar una nueva Actualización. Si se llama a Guardar
en tu Modelo, se necesita asegurar que tu Actualización sea también grabada. La solución más simple
es utilizar Guardar seguido por cada acción individual – pero esto puede ser ambas, difícil (relaciones
pueden tener varios niveles de profundidad) e ineficiente. Igualmente deberás cambiar solamente
algunas propiedades y entonces tener que decidir entre volver a grabar todos los campos, o de alguna
manera llevar registro de los cambios de propiedades y actualizarlas.
Cabe recordar que para sistemas pequeños, esto no representa gran problema, pero para proyectos
grandes, es casi imposible codificar manualmente (además es preferible invertir en la funcionalidad que
el cliente solicitó, que perder tu tiempo tratando de implementar tu propia Unidad de Trabajo).
En este capítulo
Al final de cuentas, no dependeremos de bosquejar manualmente – no es lo suficientemente flexible y
terminaríamos desperdiciando demasiado tiempo escribiendo código que es inútil para el cliente. Sin
embargo, es importante ver la función de bosquejar en acción – y aun así que decidimos practicar con
un ejemplo simple, nos encontramos con varios inconvenientes. Desde que bosquejar de esta manera es
totalmente directo, lo más importante es que entiendas las limitaciones que tiene este método.
Imagínate que podría pasar si dos instancias distintas de la misma información están presentes flotando
en tu código, o que rápido se expandiría tu capa de acceso de datos cuando se incrementen los
requisitos.
No revisaremos la persistencia por algunos capítulos – pero cuando sea necesario re-examinaremos
todo el potencial que contiene este método.
Inyección de dependencias.
E s común escuchar a desarrolladores promover el uso de capas como un método para proveer
extensibilidad. El ejemplo más común, y uno que usé en el Capítulo 2 cuando estábamos
revisando las interfaces, es la habilidad de cambiar la capa de acceso a datos para conectarte a
una base de datos distinta. Si tus proyectos no se parecen a los míos, de seguro tú ya sabes qué base de
datos vas a utilizar y no vas a tener que cambiarla. De seguro podrías darle esa flexibilidad por
adelantado - Por si acaso – pero que me dices de “mantén las cosas simples” y “No vas a necesitarlo”
(YAGNI por sus siglas en inglés)
Yo estaba acostumbrado a escribir sobre la importancia de las capas de dominio para tener reusabilidad
a través de diferentes capas de presentación: Sitios web, aplicaciones Windows y Servicios Web.
Irónicamente, rara vez he tenido que escribir múltiples capas de presentación para una capa de
dominio. Yo sigo creyendo que el desarrollo en capas es importante, pero mi razonamiento ha
cambiado. Hoy veo el desarrollo basado en capas como algo natural por producto con código de alta
cohesión con al menos algunas ideas alrededor de acoplamiento. Es decir, si haces las cosas
correctamente, debería de salir automáticamente tu desarrollo en capas.
La verdadera razón por la cual estamos invirtiendo un capítulo completo a desacoplamiento (el
desarrollo en capas es una implementación de alto nivel de desacoplamiento) se debe a que es un
ingrediente clave para poder escribir código que se puede probar. No fue hasta que comencé con
pruebas unitarias que me di cuenta lo enredado y frágil que era mi código. Rápidamente me quedé
frustrado porqué el método X dependía de una función en la clase Y que necesitaba una Base de datos
activa y funcional. Para prevenir los dolores de cabeza que sufrí, vamos a cubrir acoplamiento y después
hablaremos sobre pruebas unitarias en el siguiente capítulo.
(Algo más sobre “no vas a necesitarlo” YAGNI. Mientras varios desarrolladores consideran que es una
regla dura, yo normalmente pienso en ella como una guía. Hay muy buenas razones por las cuales
quieras ignorar YAGNI, la más obvia es tu propia experiencia. Si tú sabes que algo va a ser difícil de
implementar después, sería una buena idea implementarlo ahora, o al menos poner las bases. Esto es
algo que yo hago frecuentemente con el almacenamiento en caché, creando una implementación de
Para probar efectivamente el método Guardar, hay 3 cosas que debemos hacer:
Lo que no queremos hacer (que es tan importante como lo que queremos hacer), es probar la
funcionalidad de EsValido o de las funciones de Accesoadatos Guardar y Actualizar (otras pruebas
se encargarán de validarlas). El último punto es importante – todo lo que tenemos que hacer es
asegurarnos que estas funciones son llamadas con los parámetros adecuados y que los valores de
retorno (en caso de tenerlos) son manejados de manera adecuada. Es difícil encapsular tu cabeza con los
conceptos de clases simuladas sin un ejemplo concreto, pero los marcos de referencia de simulación de
clases nos van a permitir interceptar las llamadas a Guardar y Actualizar, asegurar que los
argumentos adecuados son enviados, y forzar que se regrese el valor que queremos los marcos de
referencia de simulación son efectivos y divertidos… a menos que tu código este altamente acoplado.
Ya que siempre es una buena idea desacoplar tu base de datos del dominio, usaremos este ejemplo
durante este capítulo.
Inyecció n de dependencias
En el capítulo 2 vimos como las interfaces pueden ayudar nuestra causa – sin embargo, este código no
nos permite dinámicamente proveer una implementación simulada de IAccesoaDatos que sea
regresada por la fábrica de clases AccesoaDatos. Para poder lograrlo, vamos a utilizar un patrón
llamado Inyección de Dependencias (ID). ID es diseñado específicamente para este tipo de situaciones,
ya que como su nombre lo indica, es un patrón que cambia una dependencia escrita en código
compilado en algo que puede ser inyectado en tiempo de ejecución. Vamos a revisar dos formas de ID,
una creada manualmente, y otra que mejora una librería de terceros.
Constructor de inyección
La forma más simple de ID es el constructor de inyección – lo que hace es inyectar las dependencias vía
un constructor de clases. Primero, veamos nuestra interface de AccesoaDatos otra vez y creemos una
implementación (simulada) falsa (no te preocupes, no tienes que crear implementaciones simuladas de
cada componente, pero por ahora nos va a ayudar a mantener las cosas simples):
Aunque nuestra función simulada de Actualizar, podría ser mejorada, por el momento nos funciona
bien. Ya que tenemos esta clase falsa, solo necesitamos hacer una pequeña modificación a la clase
Auto:
Vamos a revisar el código y sigámoslo paso a paso. Date cuenta en el uso de la sobrecarga del
constructor la cual al introducir ID no tiene ningún impacto en el código existente – si decides no
inyectar una instancia IAccesoaDatos, la implementación inicial es utilizada. Por otro lado, si
queremos inyectar una implementación específica, como una instancia de AccesoaDatosSimulado
podemos hacerlo así:
Marcos de referencia
Hacer ID manualmente funciona perfecto en algunos casos, pero sería desastroso en situaciones más
complejas. Un proyecto en el que trabajé recientemente tenía muchos componentes fundamentales
que tenían que ser inyectados – uno para el manejo del cache, uno para registro, uno para la base de
datos y otro para un servicio web. Las clases se contaminaron con múltiples constructores
sobrecargados y demasiados tenían que ver con configurar la clases para pruebas unitarias. Ya que ID es
crítico para las pruebas unitarias, y la mayoría de las personas que usan pruebas unitarias aman las
herramientas de código abierto, no es sorpresa que exista un buen número de marcos de referencia
para ayudar a automatizar la ID. El resto de este capítulo nos vamos a enfocar en StructureMap, un
marco de referencia para Inyección de Dependencias creado por mi compañero en CodeBetter Jeremy
Miller (http://structuremap.sourceforge.net/)
<StructureMap>
<DefaultInstance
PluginType="CodeBetter.Foundations.IDataAccess, CodeBetter.Foundations"
PluggedType="CodeBetter.Foundations.SqlDataAccess, CodeBetter.Foundations"/>
</StructureMap>
Ya que no quiero gastar mucho tiempo hablando de configuración, es importante que sepas que el
archivo XML se debe encontrar en la carpeta /bin dentro de tu aplicación. Puedes automatizarlo en
VS.NET seleccionando los archivos, y después ve a Propiedades y configurar el atributo Copiar al
directorio destino para que quede como Copiar Siempre. (Hay una gran variedad de opciones
de configuración disponibles. Si te interesa aprender más sobre esto, te recomiendo que visites el sitio
de StructureMap ).
Ya que lo tenemos configurado, podemos deshacer todos los cambios que hicimos a nuestra clase Auto
para permitir la inyección del constructor (quita el campo _ ProveedordeDatos, y los constructores).
Para obtener la implementación correcta de IAccesoaDatos, Solamente necesitamos pedírsela a
StructureMap, el método Guardar ahora se ve así:
if (!EsValido())
{
//Hacer: crear un mejor manejador de Excepciones
throw new InvalidOperationException("El auto debe estar en un
estado válido");
}
Para usar una simulación en lugar de la implementación original, solamente tenemos que inyectar la
simulación dentro de StructureMap:
Usamos InjectStub para que las siguientes llamadas a GetInstance regresen nuestra simulación, y
nos aseguramos que todo regrese a su estado original usando ResetDefaults.
Los marcos de referencia como StructureMap son fácil de utilizar y además de mucha utilidad. Con un
par de líneas de configuración y unos cambios menores en tu código, disminuyes de manera
considerable el acoplamiento lo que incrementa la facilidad para ejecutar pruebas. He llevado
StructureMap en proyectos muy grandes y lo he implementado en cuestión de minutos – el impacto es
menor.
una última dependencia que me gustaría ocultar – nuestros objetos de negocio estarían mejor que no
supieran de nuestra implementación especifica de ID. En lugar de llamar ObjectFactory de
StructureMap directamente, le vamos a agregar un nivel más de indireccionamiento:
De nuevo, gracias a un cambio menor, tenemos la posibilidad de hacer cambios masivos (seleccionando
diferentes marcos de referencia de ID) de manera adecuada en el desarrollo de nuestra aplicación
fácilmente.
En este capítulo
Reducir el acoplamiento es una de esas cosas que es fácil de implementar y que genera grandes
resultados enfocados a nuestra búsqueda por la facilidad de mantenimiento. Todo lo que se requiere es
un poco de conocimiento y disciplina – por supuesto las herramientas no hacen daño. Debería de ser
obvio por qué querer disminuir las dependencias entre componentes de nuestro código – especialmente
entre los componentes que son responsables de diferentes aspectos del sistema (interfaz de usuario,
dominio y datos son los 3 más obvios). En el capítulo siguiente veremos lo que son las pruebas unitarias
que van a incrementar los beneficios de la inyección de dependencias. Si estas teniendo problemas en
entender lo que son la inyección de dependencias, te recomiendo que cheques el artículo de
DotNetSlackers al respecto.
Pruebas de Unidad
5
ESCOGEMOS NO HACER PRUEBAS DE UNIDAD PORQUE NOS DA MÁS GANANCIAS EL
NO HACERLO! - (COMPAÑÍA DE CONSULTARÍA ALEATORIA)
A lo largo de este libro hemos hablado de la importancia de la habilidad de probar nuestro código
y hemos visto algunas técnicas que nos facilitan el probar nuestro sistema. Uno de los grandes
beneficios de escribir pruebas para nuestro sistema es la habilidad de entregarle al cliente un
producto de mejor calidad. Si bien esto es cierto también para las pruebas de unidad, la principal razón
por la que yo escribo pruebas de unidad es porque, la mejor forma de facilidad el mantenimiento y
evolución de nuestro sistema es teniendo un set pruebas de unidad bien escritas. Constantemente
escuchamos a los promotores de las pruebas de unidad hablar de qué tanta confianza les ha dado tener
este tipo de pruebas, y eso es exactamente de lo que se trata. En un proyecto en el que estoy
trabajando, estamos constantemente haciendo arreglos y mejoras al sistema (mejoras funcionales, de
velocidad, refactorizar, etc.); siendo un sistema relativamente grande, deberían existir cambios que nos
aterroricen. ¿Es posible hacerlo? ¿Tendrá efectos secundarios extraños? ¿Qué errores serán
introducidos? Sin nuestro set de pruebas de unidad, tal vez nos rehusaríamos a hacerlos. Pero sabemos,
al igual que nuestros clientes, que los cambios riesgosos son los que tienen mayor potencial de éxito. El
tener más de 700 pruebas de unidad que corren en un par de minutos, nos permite romper los
componentes, reorganizar el código y construir funcionalidades en las que no pensamos hace un año sin
tener que preocuparnos demasiado. Tenemos confianza en que nuestras pruebas de unidad están
completas (pruebas completamente nuestro sistema) y sabemos que es poco probable que
introduzcamos errores en nuestro ambiente de producción; si, nuestros cambios pueden introducir
errores, pero los detectaremos inmediatamente.
Las pruebas de unidad no sólo son para mitigar el riesgo de cambios peligrosos. En mi vida de
programador, también he sido responsable de errores mayores causados por cambios que parecían de
poco riesgo. El punto es que puedo hacer una cambio menor o realmente fundamental en el sistema,
correr el set de pruebas de unidad desde el IDE y en menos de 2 minutos saber en dónde estoy parado.
No puedo enfatizar demasiado en la importancia de las pruebas de unidad. Sí, ayudan a encontrar
errores y a validar que mi código hace que lo debe hacer, pero mucho más importante es su habilidad
mágica de revelar defectos fatales o partes invaluables en el diseño del sistema. Me emociono siempre
que encuentro un método o funcionalidad que es increíblemente difícil de probar ya que quiere decir
que probablemente he encontrado un defecto en alguna parte fundamental del sistema que pudo haber
sido ignorada hasta que nos pidieran un cambio inesperado. De igual forma, cuando escribo una prueba
en un par de segundos para algo que creía que iba a ser difícil de probar, sé que alguien en nuestro
equipo escribió código que es reusable para otros proyectos.
calláramos. Por muchos años he leído blogs y platicado con colegas que realmente estaban involucrados
en hacer pruebas de unidad, pero yo no lo hacía. En retrospectiva, estas son las razones por lo que me
tomó tanto tiempo subirme al tren:
1. Tenía un malentendido acerca del objetivo de las pruebas de unidad. Como ya lo dije, las
pruebas de unidad mejoran la calidad de un sistema, pero realmente es acerca de facilitar el
hacer cambios y mantener el sistema. Además, si sigues en ese camino y adoptas el siguiente
paso lógico utilizando Desarrollo Guiado por las Pruebas (Test Driven Development o TDD) las
pruebas de unidad realmente se convierten en diseño. Parafraseando a Scott Bellware, TDD no
es acerca de probar el sistema porque no estamos pensando como probadores del sistema
cuando hacemos TDD, estamos pensando como diseñadores de software.
2. Como muchos, solía pensar que los desarrolladores no deben escribir las pruebas. No sé la
historia detrás de esta creencia, pero ahora creo que es sólo una excusa que utilizan los malos
programadores. Probar el sistema es el proceso tanto de encontrar errores como de validar que
el sistema hace lo que debe hacer. Tal vez los desarrolladores no son buenos en encontrar
errores en su propio código, pero son los que mejor pueden asegurarse que el sistema hace lo
que creemos que hace y los clientes son los que mejor se pueden asegurarse de que trabaja
como debería. Si estás interesado en saber más sobre el tema, te sugiero que investigues sobre
pruebas de aceptación (acceptance testing y FitNesse). Aunque las pruebas de unidad no sean
exclusivamente acerca de probar el sistema, los programadores que no creen que deban probar
su propio código, simplemente están siendo irresponsables.
3. Probar un sistema no es divertido. Estar sentado frente al monitor, capturando datos y
asegurándose que todos esté bien, realmente no es nuestra idea de diversión. Pero hacer
pruebas de unidad es programar, lo que quiere decir que hay muchas métricas y parámetros
para medir tu éxito. A veces, como programar, es un poco mundano, pero a fin de cuentas no es
diferente al tipo de programación que haces cada día.
4. Toma tiempo. Los promotores te dirán que hacer pruebas de unidad no toma tiempo, AHORRA
tiempo. Esto es cierto en el sentido que el tiempo que pasas escribiendo las pruebas de unidad,
es poco comparado con el tiempo que te ahorrarás en modificaciones y corrección de errores.
Eso me parece un poco descabellado. Honestamente hacer pruebas de unidad SÍ toma bastante
tiempo (especialmente cuando empiezas). Es posible que no tengas suficiente tiempo para
hacer las pruebas de unidad o que el cliente no sienta que el costo inicial se justifique. En estas
situaciones, sugiero que identifiques las partes más críticas de tu código y las pruebes lo más
posible; un par de horas escribiendo las pruebas de unidad pueden tener un gran impacto.
Al final de todo, hacer pruebas de unidad parecía algo complicado y misterioso que era usado
únicamente en proyectos más avanzados, el beneficio parecía inalcanzable y los tiempos no parecían
permitirlo. Resulta que tomó mucha práctica (me costó trabajo aprender qué probar y cómo hacerlo),
pero los beneficios se notaron casi inmediatamente.
Las Herramientas
Con StructureMap ya configurado del capítulo anterior, sólo necesitamos añadir dos frameworks y una
herramienta más para tener nuestra todo lo necesario para hacer pruebas de unidad: nUnit,
RhinoMocks y TestDriven.NET.
TestDriven.NET es una extensión (add-in) de ejecución de pruebas para Visual Studio que añade la
opción “Run Test” en el menú de contexto (botón derecho), pero no perderemos tiempo hablando de
eso. La licencia personal de TestDriven.NET es válida únicamente para proyecto de código abierto (open
source) y para uso de evaluación. Pero no te preocupes si el licenciamiento no te parece, nUnit tiene su
propio ejecutor de pruebas, sólo que no está integrado en VS.NET (Usuarios de Resharper también
pueden utilizar la funcionalidad incluida).
nUnit es el framework de pruebas que estaremos usando. Existen otras alternativas como mbUnit, pero
yo no sé tanto de estas como debería.
RhinoMocks es mocking framework que estaremos usando para crear objetos falsos. En el capítulo
anterior creamos nuestros mocks manualmente, lo que era limitado y nos tomó tiempo, RhinoMocks
nos generará automáticamente las clases mock basadas en una interfaz y nos permitirá verificar y
controlar la interacción entre el objeto que estamos probando y estos objetos mock.
nUnit
Lo primero que debemos hacer es añadir la referencia a nunit.framework.dll y a
Rhino.Mocks.dll. Mi preferencia personal es poner las pruebas de unidad en su propio proyecto.
Por ejemplo si mi capa de dominio está localizada en CodeBetter.Foundations, Me gusta crear un
nuevo proyecto llamado CodeBetter.Foundations.Tests. El inconveniente de esto es que no
podremos probar métodos privados (más de esto en un momento). En .NET 2.0+ podemos usar el
atributo InternalsVisibleToAttribute para permitir al proyecto de pruebas acceder a los
métodos con visibilidad interna (abre el Properties/AssemblyInfo.cs y añade[assembly:
InternalsVisibleTo(“CodeBetter.Foundations.Tests”)], es algo que hago típicamente)
Hay dos cosas que debes saber de nUnit. Primero, se configuran las pruebas utilizando atributos. El
atributo TestFixtureAttribute es aplicado a la clase que contiene las pruebas y métodos de
inicialización y finalización. El atributo SetupAttribute es aplicado al método que quieres que se
ejecute para cada prueba (no siempre necesitarás esto), el atributo TearDownAttribute es aplicado al
método que quieras que se ejecute después de cada prueba; y finalmente el atributo TestAttribute
es aplicado a las pruebas en sí. Existen otros atributos, pero estos 4 son los más importantes. Esto es
como se vería una clase de prueba:
using NUnit.Framework;
[TestFixture]
public class CarTests
{
[SetUp]
public void SetUp() { //todo }
[TearDown]
public void TearDown(){ //todo }
[Test]
public void SaveThrowsExceptionWhenInvalid(){ //todo }
[Test]
public void SaveCallsDataAccessAndSetsId(){ //todo }
//more tests
}
Como puedes ver, cada prueba tiene un nombre muy explícito, es importante expresar exactamente lo
que la prueba va a hacer, y ya que las prueba nunca deben hacer demasiado, rara vez tendrás nombres
excesivamente largos.
La segunda cosa que hay que saber de nUnit, es que para confirmar que tu prueba se ejecutó como se
esperaba utilizas la clase Assert y sus métodos. Ya sé que estos no es correcto, pero si tuviéramos un
método que tomara como parámetro un param int[] numbers y regresara la suma de los números,
nuestra prueba de unidad de vería así:
[Test]
public void MathUtilityReturnsZeroWhenNoParameters()
{
Assert.AreEqual(0, MathUtility.Add());
}
[Test]
public void MathUtilityReturnsValueWhenPassedOneValue()
{
Assert.AreEqual(10, MathUtility.Add(10));
}
[Test]
public void MathUtilityReturnsValueWhenPassedMultipleValues()
{
Assert.AreEqual(29, MathUtility.Add(10,2,17));
}
[Test]
public void MathUtilityWrapsOnOverflow()
{
Assert.AreEqual(-2, MathUtility.Add(int.MaxValue, int.MaxValue));
}
No se ve en el ejemplo anterior, pero la clase Assert tiene más de una función, como por ejemplo
Assert.IsFalse, Assert.IsTrue, Assert.IsNull, Assert.IsNotNull, Assert.AreSame,
Assert.AreNotEqual, Assert.Greater, Assert.IsInstanceOfType y otros más.
Estoy seguro que algunos pensarán que 4 pruebas para cubrir el método MathUtility.Add es algo
excesivo. Podrán pensar que estas 4 pruebas podrían ser agrupadas en una sola (y para este caso podría
decir que es como ustedes prefieran), pero, cuando empecé a hacer pruebas de unidad, caí en el mal
hábito de dejar que el alcance de mis pruebas creciera demasiado. Empezaba con pruebas que creaban
un objeto, ejecutaban alguno de sus métodos y se aseguraban que funcionara como quería. Pero
siempre decía, “ya que estoy aquí, por qué no añadir unos cuantos asserts más para asegurarme que
estos campos tengan los valores que deben tener”. Esto es muy peligroso, porque un cambio en el
código podía romper varias pruebas que no tenían relación con el cambio – definitivamente esto es una
señal de que no has enfocado tus pruebas lo suficiente.
Esto nos lleva al tema de probar métodos privados. Si googleas esto encontrarás varias discusiones al
respecto, pero el consenso general parece ser que no se deben probar los métodos privados
directamente. Creo que la razón más importante para no probar métodos privados es que nuestro
objetivo no es probar métodos o líneas de código, sino el comportamiento de una parte del código. Esto
es algo que siempre debes recordar. Si pruebas exhaustivamente la interfaz pública de tu código, los
métodos privados deberían también estar probándose automáticamente. Otro argumento en contra de
probar métodos privados es que rompe el encapsulamiento de tu clase. Ya hablamos de la importancia
de esconder la información; y los métodos privados contienen detalles de implementación que
queremos poder cambiar sin romper el código que utiliza nuestra clase, si probamos métodos privados
directamente, es posible que algunos cambios en la implementación de la clase romperán nuestras
pruebas, lo que no ayuda a tener código que sea fácil de mantener.
Mocking
Para empezar a utilizar pruebas de unidad, debes empezar a probar funcionalidades simples; pero
rápidamente querrás probar métodos más complejos que tienen dependencias con otros componentes
(como con la base de datos), por ejemplo, querrás añadir pruebas para completar la cobertura del
método Save de nuestra clase Car. Ya que queremos mantener nuestras pruebas lo más granular y
ligeras posible (las pruebas deben poder ejecutarse rápidamente y muy seguido para que tengamos
retroalimentación instantánea), realmente no queremos tener que inicializar una base de datos con
datos falsos y asegurarnos que se mantenga en un estado predecible entre prueba y prueba. En realidad
lo que queremos asegurarnos es que el método Save interactúe de forma correcta con nuestra capa de
datos; después podremos añadir otras pruebas para nuestra capa de datos. Si nuestro método Save
funciona correctamente, nuestra capa de datos funciona correctamente y los dos interactúan
correctamente, tendremos una buena parte del camino avanzado para hacer pruebas más tradicionales.
En el capítulo anterior, vimos el principio de probar con mocks. Estábamos usando una clase falsa o
mock creada manualmente que tenía algunas limitaciones importantes. La más significativa era nuestra
inhabilidad para confirmar que las llamadas a nuestro objeto mock ocurrieran como esperábamos. Esto,
junto con la facilidad de uso, es exactamente el problema que resuelve RhinoMocks. Usar esta
herramienta es muy sencillo, le dices que es lo que quieres representar (una interfaz o clase,
preferentemente una interfaz), le dices que métodos y con qué parámetros esperas que sean llamados,
y le pides que verifique que estas expectativas se cumplieron.
Antes de empezar, necesitamos darle acceso a RhinoMocks a nuestros tipos internos. Esto se hace
añadiendo [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] a nuestro
archivo Properties/AssemblyInfo.cs.
Ahora podemos empezar a programar nuestra prueba que cubre el camino de actualización de nuestro
método Save cuando ya existe el Car en la base de datos:
[TestFixture]
public class CarTest
{
[Test]
public void SaveCarCallsUpdateWhenAlreadyExistingCar()
{
car.Id = 32;
car.Save();
mocks.VerifyAll();
ObjectFactory.ResetDefaults();
}
}
Una vez que el objeto mock ha sido creado, lo que tomó una línea, lo inyectamos en nuestro framework
de inyección de dependencias (StructureMap en este caso). Cuando se crea un mock utilizando
RhinoMocks, empieza en modo de registro, lo que quiere decir que todas la operaciones que se le
Podemos probar todo esto forzando a nuestra prueba a que falle (noten la llamada extra a
dataAccess.Update):
[Test]
public void SaveCarCallsUpdateWhenAlreadyExistingCar()
{
MockRepository mocks = new MockRepository();
IDataAccess dataAccess = mocks.CreateMock<IDataAccess>();
ObjectFactory.InjectStub(typeof(IDataAccess), dataAccess);
car.Id = 32;
car.Save();
mocks.VerifyAll();
ObjectFactory.ResetDefaults();
}
Nuestra prueba falla con un mensaje de RhinoMocks que nos dice que esperábamos dos llamadas al
método Update, pero sólo una ocurrió.
Para el caso cuando el Car no existe en la base de datos, la interacción con el método Save de la capa de
datos es un poco más compleja, tenemos que asegurarnos que el valor que regresa el método Save del
objeto dataAccess, es manejado correctamente. Esta es la prueba:
[Test]
public void SaveCarCallsSaveWhenNew()
{
MockRepository mocks = new MockRepository();
IDataAccess dataAccess = mocks.CreateMock<IDataAccess>();
ObjectFactory.InjectStub(typeof(IDataAccess), dataAccess);
Expect.Call(dataAccess.Save(car)).Return(389);
mocks.ReplayAll();
car.Save();
mocks.VerifyAll();
Assert.AreEqual(389, car.Id);
ObjectFactory.ResetDefaults();
}
Utilizando el método Expect.Call nos permite especificar el valor que queremos regresar cuando
llamen a este método. También añadimos una llamada a Assert.Equal para verificar que lo que
regresamos como Id desde la capa de datos es lo que se asignó al objeto Car y así validar que la
interacción entre los dos objetos es correcta. Espero que las posibilidades de controlar el valor que
regresa el método (así como los valores output/ref) te muestren lo fácil que es probar los casos
extremos.
Si cambiáramos nuestro método Save para que genere una excepción si el Id que se regresa del
dataAccess es inválido, nuestra prueba se vería así:
[TestFixture]
public class CarTest
{
private MockRepository _mocks;
private IDataAccess _dataAccess;
[SetUp]
public void SetUp()
{
_mocks = new MockRepository();
_dataAccess = _mocks.CreateMock<IDataAccess>();
ObjectFactory.InjectStub(typeof(IDataAccess), _dataAccess);
}
[TearDown]
public void TearDown()
{
_mocks.VerifyAll();
}
[Test, ExpectedException("CodeBetter.Foundations.PersistenceException")]
public void SaveCarCallsSaveWhenNew()
{
Car car = new Car();
Expect.Call(_dataAccess.Save(car)).Return(0);
_mocks.ReplayAll();
car.Save();
}
}
Combinado con una utilería como NCover, también puedes obtener reportes de la cobertura de tus
pruebas. Básicamente, la métrica de cobertura de dice qué porcentaje del código de un
assembly/namespace/clase/método fue ejecutado con tus pruebas. NCover tiene un visor de código que
resalta en rojo las líneas que no fueron ejecutadas por las pruebas. Generalmente, no me gusta utilizar
la métrica de cobertura para medir que tan completo es mi set de pruebas. Después de todo, el ejecutar
una línea de código no quiere decir que realmente la probaste. Más bien, para lo que me gusta usar
NCover es para identificar el código que no he probado. Es decir, el que una línea o método haya sido
ejecutada no quiere decir que la prueba es correcta, pero si una línea de código o método no ha sido
ejecutado, entonces necesitas pensar en añadir más pruebas para cubrirlo.
Hemos mencionado el Desarrollo Guiado por las Pruebas (Test Driven Development o TDD) brevemente
en este libro. Como mencioné antes, TDD se trata de diseño y no de pruebas. TDD quiere decir que
escribes la prueba primero y después escribes el código para hacer que la prueba pase. En TDD
habríamos escrito la prueba para el método Save antes de tener cualquier funcionalidad en el método
en sí. Claro que la prueba fallaría; entonces escribiríamos la funcionalidad específica para hacer que la
prueba pase. El principio para desarrolladores que utilizan TDD es rojo → verde → refactorizar. Lo que
quiere decir que el primer paso es tener una prueba que falle, después hacerla pasar y después
modificar el código como sea requerido.
En mi experiencia, TDD va muy bien con Diseño Guiado por el Dominio (Domain Driven Design), ya que
nos hace concentrarnos en las reglas de negocio del sistema. Si nuestro cliente tienen muchos
problemas para mantener las dependencias entre las actualizaciones del sistema, nos enfocamos en
escribir pruebas que definan el comportamiento y el API de la funcionalidad específica. Les recomiendo
que primero se familiaricen con pruebas de unidad en general antes de entrar en TDD.
Por otro lado, probar la capa de datos es posible y lo recomendaría. Puede ser que haya mejores
métodos, pero mi forma de hacerlos es mantener todos mis CREATE Table / CREATE Sprocs en archivos
de texto con mi proyecto, crear una base de datos de prueba y utilizar los métodos SetUp y TearDown
de mi clase de prueba para crear y mantener la base de datos en un estado conocido en cada prueba.
Este tema quizá sea para un artículo futuro en mi blog, por lo que por ahora se los dejo a su creatividad.
En este capítulo
Hacer pruebas de unidad no fue tan difícil como me imaginé en un principio. Seguro que mis primeras
pruebas no fueron las mejores (a veces escribía pruebas que no tenían ningún sentido, como probar que
una propiedad de una clase funcionaba como debía) y otras veces eran demasiado complejas y se salían
del alcance definido; pero después de mi primer proyecto, aprendí mucho de qué funcionaba y qué no.
Una cosa que me quedó clara inmediatamente, fue qué tan limpio y claro quedaba mi código. También
me di cuenta que si algo era difícil de probar, al reescribirlo para que fuera posible probarlo, quedaba
mucho más legible, más desacoplado y en general más fácil de trabajar con él. El mejor consejo que les
puedo dar es empezar con algo pequeño, experimentar con diferentes técnicas, no tener miedo a fallar
y aprender de tus errores; y por supuesto, no esperes a terminar el proyecto para hacer pruebas de
unidad, ¡escríbelas conforme escribas el código!
E n el capítulo 3 hicimos nuestro primer esfuerzo para unir el mundo de los objetos con los datos
mediante la escritura a mano de nuestra propia capa de acceso a datos y su definición de
conversión. Este enfoque resultó ser más bien limitado y requirió una cantidad significativa de
código repetitivo (aunque fue útil para demostrar las bases). Agregar más objetos y funcionalidad
sobrecargaría nuestro Capa de Acceso a Datos (DAL) en una enorme violación inmanejable del principio
que dicta ‘No te repitas a ti mismo’ (DRY, por sus siglas en inglés). En este capítulo veremos un marco de
trabajo real para la definición de conversiones entre Objetos y Entidades Relacionales (O/R Mapping)
que haga todo el trabajo pesado por nosotros. Específicamente veremos el popular marco de trabajo de
código abierto llamado NHibernate.
La única y más grande barrera que impide a la gente adoptar el diseño guiado por el dominio (DDD por
sus siglas en inglés), es el problema de la persistencia. Mi propia adopción de las definiciones de
conversión entre estructuras relacionales y los objetos (O/R Mappers) inicio con gran confusión y duda.
Básicamente se te pedirá que cambies tu conocimiento de un método probado por algo que parece de
un poco mágico. Puede ser requerida algo de fe ciega.
La primer cosa con la que hay que llegar a un acuerdo es con que las definiciones de conversión generan
tu SQL por ti, lo sé, suena como que será algo lento, inseguro e inflexible, especialmente debido a que
probablemente imaginaste que se tendría que usar SQL en línea. Pero sí puedes quitarte esos miedos de
tu mente por un segundo, tienes que admitir que podría ahorrarte mucho tiempo y tener como
resultado un número mucho menor de defectos. Recuerda, queremos enfocarnos en construir el
comportamiento, no preocuparnos con cuestiones de interconexión (y si te hace sentir mejor, una
buena definición de conversiones entre estructuras relacionales y objetos te proveerá formas sencillas
de desactivar la generación automatizada de código y ejecutar tu propio SQL o tus procedimientos
almacenados).
Por supuesto que formulado de esta manera, el SQL en línea realmente apesta. Sin embargo, si te detienes y lo
piensas y realmente comparas manzanas con manzanas, la verdad es que ninguna de las 2 es particularmente
mejor que la otra, Examinemos algunos puntos de interés .
El SQL en línea debe ser escrito usando consultas parametrizadas de la misma forma en que lo haces con
los procedimientos almacenados. Por ejemplo, la forma correcta de escribir el código de arriba para
eliminar un posible ataque de inyección de SQL es:
En algún lugar, de alguna forma, la gente se metió en la cabeza que las compilaciones de código deben
evitarse a toda costa (tal vez esto venga de los días cuando los proyectos podrían tardar días en
compilar). Si cambias un procedimiento almacenado, aun tendrás que ejecutar nuevamente tus pruebas
¿A quién le importa? En la mayoría de los casos tu base de datos esta soportada por una conexión GigE
con tus servidores y tú no pagas ese ancho de banda. Estás hablando de fracciones de nanosegundos.
Más relevante aún, una definición de conversión bien configurada puede ahorrarte viajes de ida y vuelta
gracias a la identificación de las implementaciones de los mapas, el cache y la carga diferida.
Esta es la excusa más frecuentemente utilizada, Escribe una sentencia común y razonable de SQL en
línea y después escribe lo mismo con un procedimiento almacenado y cronometra el tiempo de
ejecución. Adelante hazlo. En la mayoría de los casos no habrá diferencia o esta será muy poca. En
algunos casos los procedimientos almacenados serán más lentos ya que el plan de ejecución no será
eficiente con algunos parámetros. Jeff Atwood catalogó el uso de procedimientos almacenados en busca
de una mayor velocidad de ejecución como un caso extremo de optimización prematura. Tiene razón. El
enfoque adecuado es tomar la estrategia más simple (permite que una herramienta genere el SQL por
ti), y optimiza consultas especificas en caso de que identifiques cuellos de botella.
Me tomo un tiempo, pero después de un par de años, me di cuenta que el debate entre el SQL en línea y
los procedimientos almacenados era tan trivial como el que se tiene con C# y VB.NET. Si tan solo se
trataba de elegir uno u otro, entonces selecciona el que prefieras y continua con tu próximo reto. Si no
hay nada más que decir sobre el tema, yo escogería procedimientos almacenados. Sin embargo, cuando
añades una definición de conversión Objeto/Estructura relacional a la mezcla, de forma repentina
obtendrás ventajas significativas. Dejas de participar en discusiones bizantinas para simplemente decir
“quiero eso”.
Específicamente, hay tres grandes beneficios con las definiciones de conversión entre estructuras
relacionales y objetos:
1.- Terminarás escribiendo mucho menos código – lo que obviamente resulta en un sistema más
mantenible,
2.- Obtendrás un nivel real de abstracción del origen de datos subyacente – por una parte porque
estarás consultando la definición de conversión para obtener los datos directamente (y esta a su vez
convertirá eso en el SQL apropiado), por otra parte porque estarás proveyendo información de la
definición de conversión entre los esquemas de tablas y los objetos de dominio,
3.- Tu código se vuelve más simple – si tu nivel de discrepancia entre los objetos y las estructuras
relacionales es bajo, escribirás mucho menos código repetitivo. Si tu nivel de discrepancia entre los
objetos y las estructuras es alto no tendrás que comprometer el diseño de la base de datos y el diseño
del dominio – puedes construir ambos de una manera optimizada, y dejar que la definición de
conversión maneje la discrepancia.
Al final, todo se traduce en la construcción de la solución más sencilla desde el inicio. La optimizaciones
deberían dejarse para después de que el código ha sido perfilado y se han identificado los cuellos de
botella reales. Como la mayoría de las cosas, podría no sonar tan sencillo por la complejidad en el
aprendizaje de hacerlo por adelantado, pero esa es la realidad de nuestra profesión.
NHibernate
De los marcos de trabajo y herramientas que hemos visto hasta el momento, NHibernate es la más compleja. Esta
complejidad ciertamente es algo que deberías tomar en cuanta cuando te decidas por alguna solución de
persistencia, pero una vez que encuentres un proyecto que te permita algún tiempo para investigación y
desarrollo, el beneficio valdrá la pena en proyectos futuros. Lo mejor acerca de NHibernate, y una de las
principales metas del diseño del marco de trabajo, es que es completamente transparente - tus objetos de
dominio no son forzados a heredar ninguna clase base específica
y no tienes que usar un montón de atributos de decoración. Esto
Recuerda que nuestro objetivo es hace posible que tu capa de dominio pueda ser sometida a
ampliar nuestra base de pruebas unitarias – si estas usando un mecanismo diferente de
conocimientos viendo diferentes persistencia, digamos datasets con tipos definidos, el nivel de
formas de construir sistemas con el acoplamiento tan estrecho entre el dominio y los datos hace muy
difícil sino es que imposible el efectuar apropiadamente las
fin de proveer un mayor valor a
pruebas unitarias.
nuestros clientes. Mientras que
podríamos hacerlo hablando A muy groso modo, configuras Nhibernate diciéndole como tu
específicamente de NHibernate, el base de datos (tablas y columnas) hacen referencia con tus
objetos de dominio, usa la API de Nhibernate y el lenguaje de
objetivo es introducir el concepto
consulta (HQL – Hibernate Query Language) para comunicarte
de Definiciones de conversión con tu base de datos, y deja que el haga el trabajo de bajo nivel
Entidad relacional / Objeto, y tratar con ADO.NET y SQL. Esto no solo provee separación entre la
de corregir la fe ciega que tienen estructura de tu tabla y los objetos de dominio, sino que también
puesta los desarrolladores de .NET desacopla tu código de la implementación para una base de
datos específica.
en los procedimientos almacenados
y ADO.NET En capítulos previos nos enfocamos en un sistema para un
concesionario de automóviles – específicamente centrándonos
en automóviles y actualizaciones. En este capítulo cambiaremos
la perspectiva un poco y veremos la venta de automóviles (ventas, modelos y personal de ventas). El modelo de
dominio es simple – un vendedor (SalesPerson) tiene 0 o más Ventas (Sales) las cuales hacen referencia a un
Modelo (Model) específico.2
También se incluye una solución de VS.NET que contiene código de ejemplo y anotaciones - puedes encontrar un
vínculo al final de este capítulo. Todo lo que necesitas para ponerlo en marcha es crear una nueva base de datos,
ejecutar la secuencia de comandos de SQL provista (un mecanismo completo para la creación de tablas), y
configura la cadena de conexión. El ejemplo, y el resto de este capítulo, fueron diseñados con la intención de
ayudarte a comenzar a trabajar con NHibernate – un tema que frecuentemente se pasa por alto.
2
Nota de traducción: para guardar compatibilidad con la aplicación de ejemplo, no se traducirán los nombres de clases, variables y demás.
Finalmente, encontrarás que el manual de referencia de NHibernate es de excepcional calidad, el cual es tanto una
herramienta útil para empezar, como también una referencia para buscar información de temas específicos.
También hay un libro que está siendo publicado por Manning, ‘NHibernate en acción’, el cual estará disponible en
junio, mientras tanto puedes comprar una versión en formato electrónico previa al lanzamiento del libro.
Configuración
El secreto para la sorprendente flexibilidad de NHibernate radica en su configurabilidad. Inicialmente el proceso de
configurarlo puede ser más bien desalentador, pero después de un proyecto de prueba se vuelve natural. El primer
paso es configurar el propio NHibernate. La configuración más simple, que debe ser agregada a tu app.config o
web.config se ve así:
<configuration>
<configSections>
<section name="hibernate-configuration"
type="NHibernate.Cfg.ConfigurationSectionHandler, NHibernate" />
</configSections>
<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
<session-factory>
<property name="hibernate.dialect">
NHibernate.Dialect.MsSql2005Dialect
</property>
<property name="hibernate.connection.provider">
NHibernate.Connection.DriverConnectionProvider
</property>
<property name="hibernate.connection.connection_string">
Server=SERVER;Initial Catalog=DB;User Id=USER;Password=PASSWORD;
</property>
<mapping assembly="CodeBetter.Foundations" />
</session-factory>
</hibernate-configuration>
</configuration>
De los 4 valores, dialect es el más interesante, este le dice a NHibernate que lenguaje específico habla
nuestra base de datos. Sí, en nuestro código, le pedimos a NHibernate que regrese un resultado
paginado de Cars y nuestro dialecto está configurado para SQL Server 2005, NHibernate emitirá una
sentencia de SELECT utilizando la función de ranking ROW_NUMBER (). Sin embargo, si el dialecto está
configurado a MySql, NHibernate emitirá el SELECT con LIMIT. En la mayoría de los casos, configurarás
esto una sola vez y te olvidarás del tema. Pero esto proporciona algunos conocimientos sobre las
capacidades provistas por la capa que genera todo tu código de acceso a datos.
cada objeto de dominio, y que estos se localicen dentro de la carpeta Mappings. El archivo de definición
para nuestro objeto Model (Modelo), llamado Model.hbm.xml, se ve de esta forma:
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
assembly="CodeBetter.Foundations"
namespace="CodeBetter.Foundations">
<class name="Model" table="Models" lazy="true" proxy="Model">
<id name="Id" column="Id" type="int" access="field.lowercase-underscore">
<generator class="native" />
</id>
<property name="Name" column="Name"
type="string" not-null="true" length="64" />
<property name="Description" column="Description"
type="string" not-null="true" />
<property name="Price" column="Price"
type="double" not-null="true" />
</class>
</hibernate-mapping>
(Es importante asegurarse de que el parámetro Build Action para todos los archivos de definición de
conversiones se configuren como Embedded Resources)
Este archivo le dice a NHibernate que la clase Model se refiere a registros en la tabla Models, y que las 4
propiedades Id, Name, Description y Price se refieren a las columnas Id, Name, Description, y Price. La
información extra alrededor de la propiedad Id especifica que el valor es generado por la base de datos
(en contraposición a NHibernate (para soluciones residentes en clústeres o grupos de servidores), o
nuestro propio algoritmo) y que no hay configurador, así que deberá ingresar a través del campo con la
convención de nombres especificada (proveemos Id como el nombre y la estrategia de nomenclatura
con minúsculas y guión bajo (lowercase-underscore), para que use el campo llamado _id.
Con el archivo de definición de conversiones configurado, podemos comenzar a interactuar con la base
de datos:
model.Price -= 5000;
ISession session = _sessionFactory.OpenSession();
session.Update(model);
}
El ejemplo de arriba muestra lo fácil que es persistir nuevos objetos a la base de datos, extraerlos y actualizarlos –
todo esto sin el uso directo de ADO.NET o SQL.
Tal vez te estés preguntando de donde viene el objeto _sessionFactory, y que es exactamente un ISession.
_sessionFactory (que implementa la interfaz ISessionFactory) es un objeto global seguro para el uso en
hilos de ejecución el cual es muy probable que crees en el inicio de la aplicación. Típicamente necesitaras uno por
cada base de datos que tu aplicación este usando (lo que significa que típicamente necesitaras solo uno), y su
trabajo, como con la mayoría de las fabricas de objetos, es crear un objeto pre configurado: un objeto ISession
no tiene equivalente en ADO.NET, pero si se relaciona con un bajo nivel de cohesión a una conexión de base de
datos. Sin embargo, la creación de un ISession no necesariamente abre una conexión, en lugar de eso, el objeto
ISession administra de forma inteligente los objetos de tipo conexión y comando por ti. A diferencia de las
conexiones que deben ser abiertas tarde y cerradas de manera temprana, no tienes que preocuparte por tener
objetos ISession alrededor por un rato (aún cuando estas no sean seguras para su procesamiento en hilos de
ejecución). Si estas construyendo una aplicación ASP.NET, puedes abrir de forma segura un objeto que implemente
ISession en el método BeginRequest y cerrarlo en el método EndRequest (o mejor aún, cargar de forma
diferida en caso de que la solicitud específica no requiera un ISession).
La interfaz ITransaction es otra pieza del rompecabezas que es creada llamando el método
BeginTransaction en un ISession. Es común para los desarrolladores de .NET ignorar la necesidad de usar
transacciones dentro de sus aplicaciones. Esto es desafortunado ya que puede llevarnos a estados inestables e
incluso irrecuperables de los datos. Un ITransaction es usado para mantener el rastro de la unidad de trabajo -
rastrear que cambio, o que ha sido agregado o borrado, averiguar qué y cómo aplicarlo a la base de datos, y
proveer la capacidad de deshacer en caso de que un paso individual falle.
Relaciones
En nuestro sistema, es importante que rastreemos las ventas – específicamente con relación a la fuerza de ventas,
de tal forma que podamos proveer algunos reportes básicos. Se nos ha dicho que una venta solo puede pertenecer
a un vendedor, y así establecer una relación uno-a-muchos esto es, un agente de ventas puede tener múltiples
ventas, y una venta solo puede pertenecer a un único vendedor. En nuestra base de datos, la relación está
representada como una columna SalesPersonId en la tabla Sales (llave foránea). En nuestro dominio, la clase
SalesPerson tiene una colección de Sales y la clase Sales tiene una propiedad SalesPerson (Referencia).
Ambos extremos de la relación necesitan ser configurados en el archivo de definición de conversiones apropiado,
En el extremo de Sales, que se relaciona con una propiedad única, usamos un elemento property glorificado
llamado many-to-one:
...
<many-to-one name="SalesPerson"
class="SalesPerson"
column="SalesPersonId"
not-null="true"/>
...
Estamos especificando el nombre de la propiedad, el tipo/clase, y el nombre de la columna que es la llave foránea.
También estamos especificando una limitante extra, que es, que cuando agregamos un nuevo objeto Sales, la
propiedad SalesPerson no puede ser nula.
El otro lado de la relación, la colección de ventas (Sales) que Con la liberación de .NET 3.5
tiene un vendedor (SalesPerson), es un poco más
finalmente se ha agregado una
complicada – básicamente porque la terminología de
NHibernate no pertenece a la jerga estándar usada en .NET. colección HashSet al marco de
Para configurar una colección usamos un elemento de tipo set, trabajo. Idealmente, versiones
list, map, bag o array. Tu primera inclinación puede ser futuras agregaran otro tipo de
usar una lista, pero NHibernate requiere que tengas una conjuntos con un OrderedSet. Los
columna que especifique el índice. En otras palabras, el equipo
conjuntos son colecciones muy
de NHibernate ve una lista como una colección donde el índice
es importante, y por lo tanto debe ser especificado. Lo que la útiles y eficientes, ¡así que
mayoría de los desarrolladores de .NET entienden como un considera agregarlos a tu arsenal
list, NHibernate lo llama una bolsa (Bag). Siendo algo de herramientas! Puedes aprender
confuso tal vez, tanto si utilizas un elemento list o bag, tu más leyendo el artículo en el que
tipo de dominio debe ser un IList (o su equivalente genérico
Jason Smith describe los conjuntos.
IList<T>). Esto es debido a que .NET no cuenta con un objeto
IBag, En resumen, para las colecciones que uses comúnmente,
utiliza el elemento bag y haz tu propiedad del tipo IList.
La otra opción interesante de colección es el set (conjunto). Un conjunto es una colección que no puede contener
duplicados- un escenario común para una aplicación empresarial (aunque rara vez se afirma explícitamente).
Extrañamente, .NET no tiene una colección de tipo conjunto, así que NHibernate usa la interfaz
Iesi.Collection.ISet. Existen 4 implementaciones específicas, ListSet que es realmente rápida para
colecciones muy pequeñas (10 elementos o menos), SortedSet que puede ser ordenada, HashSet la cual es
rápida para colecciones más grandes y HybridSet la cual usa inicialmente un ListSet y automáticamente se
cambia así misma a un HashSet conforme crece tu colección.
Para nuestro sistema usaremos un objeto bag (aún cuando no podemos tener ventas duplicadas, es mucho más
sencillo por el momento), así que declaramos nuestra colección de Sales como un IList:
De nuevo, si observas a cada elemento/atributo, no es tan complicado como podría verse al principio.
Identificamos el nombre de nuestra propiedad, especifica la estrategia de acceso (no tenemos un configurador, así
que hay que indicarle que use el campo con nuestra convención de nombres), la tabla y la columna que contienen
la llave foránea, y el tipo/clase de los elementos en la colección.
También hemos configurado el atributo cascade a all lo que significa que cuando nosotros invoquemos la
actualización (update) en un objeto del tipo SalesPerson, cualquier cambio que sea hecho a su colección de
ventas (Sales) (adiciones, remociones, cambios a las ventas existentes) será automáticamente persistido. La
actualización en cascada puede ser un buen ahorrador de tiempo conforme tu sistema crece en complejidad.
Consultas
NHibernate soporta dos diferentes tipos de esquemas para realizar consultas: Hibernate Query Language (HQL) y
Consultas de Criterios (también puedes realizar consultas en SQL convencional, pero perderá portabilidad al
hacerlo). HQL es la forma más sencilla de las 2 ya que se parece mucho a SQL – usas From, where,
aggregates, order by, group by, etc. Sin embargo en vez de consultar directamente contra tus tablas,
escribes consultas contra tu dominio - lo que significa que HQL Soporta los principios de la orientación a objetos
tales como la herencia y el polimorfismo. Ambos métodos de consulta son abstracciones arriba de SQL, lo que
significa que obtienes portabilidad total – lo único que necesitas hacer para dirigirse a una base de datos diferente
es cambiar la configuración de su dialecto.
HQL funciona a partir de la interfaz IQuery, que se crea con la invocación del método CreateQuery en tu sesión.
Con IQuery puedes regresar entidades individuales, colecciones, parámetros substitutos y más. Aquí hay algunos
ejemplos:
//Lo mismo que lo anterior, pero en una línea y el apellido como variable
SalesPerson p = session.CreateQuery("from SalesPerson p where p.LastName
= ?").SetString(0, lastName).UniqueResult<SalesPerson>();
Esta es solo una muestra de lo que se puede hacer con HQL (el ejemplo que puede descargarse tiene algunos
ejemplos ligeramente más complicados).
Carga diferida
Cuando cargamos un vendedor, haciendo algo como esto: SalesPerson person =
session.Get<SalesPerson>(1); la colección Sales no será cargada. Esto es porque, por defecto, las
colecciones son cargadas de manera diferida. Esto es, que no tocaremos la base de datos hasta que la información
sea específicamente solicitada (ej. Podemos acceder a la propiedad Sales). Podemos invalidar este
comportamiento cambiando la configuración lazy=”false” en el elemento bag.
La otra, más interesante, estrategia de carga implementada por NHibernate está en las entidades en sí mismas.
Frecuentemente querrás agregar una referencia a un objeto sin tener que cargar el objeto real desde la base de
datos. Por ejemplo, cuando agregamos una venta (Sales) a un vendedor (SalesPerson), necesitamos
especificar el modelo (Model), pero no queremos cargar cada propiedad / lo único que queremos es obtener el Id
para poder guardarlo en la columna ModelId de la tabla Sales. Cuando usas session.Load<T> (id)
NHibernate cargara un proxy del objeto actual (a menos de que especifiques lazy=”false” en el elemento
clase). Hasta donde puede importarte, el proxy se comporta exactamente igual que el objeto real, pero ningún
dato será extraído de la base de datos hasta la primera vez que lo solicitas. Esto hace posible escribir el siguiente
código:
Sin tener que tocar siquiera la base de datos para cargar el objeto Model.
Descarga
Puedes descargar un proyecto con más ejemplos del uso de NHibernate en:
En este capítulo
Solo hemos tocado un poco de lo que puedes hacer con NHibernate. No hemos visto las consultas por criterio (que
es una API de consulta más íntimamente ligada con tu dominio) sus capacidades de cache, filtrado de colecciones,
optimización del rendimiento, registro de bitácoras, o capacidades nativas de SQL. Más allá de la herramienta de
NHibernate, afortunadamente es muy probable que hayas aprendido más acerca de cómo definir relaciones entre
objetos y estructuras relacionales, y soluciones alternativas de la limitada cesta incluida dentro de .NET. Es difícil
abandonar el SQL escrito a mano, ir más allá de lo que es cómodo, es imposible ignorar los beneficios de las
definiciones de conversión entre objetos y estructuras relacionales.
¡Estas mas allá de la mitad del camino! Espero que estés disfrutando y aprendiendo mucho. Este puede
ser un buen momento para tomar un descanso de la lectura y poner manos a la obra con la aplicación
gratuita de aprendizaje Canvas Learning Application.
Mucha de la confusión sobre la memoria nace del hecho de que tanto C# y VB.NET son lenguajes
administrados y que el CLR provee la recolección automática de basura. Esto ha causado que muchos
desarrolladores asuman erróneamente que no necesitan preocuparse por la memoria.
Asignación de Memoria
En .NET, como en muchos otros lenguajes, cada variable que se defina está almacenada en el stack3 o en
el heap4. Estos son dos espacios separados asignados en la memoria de sistema que sirven un propósito
distinto, aunque complementario. Lo que va donde está predeterminado: tipos de valor van en el stack,
mientras que los tipos de referencia va en el heap. En otras palabras, todos los tipos de sistema, como
char, int, long, byte, enum y cualquier estructura (ya sean definidas por.NET o por usted) van en el
stack. La única excepción a esta regla son los tipos de valor que pertenecen a tipos de referencia – por
ejemplo la propiedad Id de una clase User va en el heap junto con la instancia de la clase User misma.
El Stack
Aunque estamos acostumbrados al mágico colector de basura, los valores en el stack son
automáticamente administrados aún en un mundo sin colector de basura (como en C). Esto es porque
cuando sea que entramos a un nuevo alcance (como un método o una sentencia If) los valores son
empujados al stack y cuando salen del stack los valores son liberados. Esta es la razón por la que un
stack es sinónimo a LIFO - last-in first-out (último en entrar primero en salir). Puede pensarlo en este
modo: cuando se crea un nuevo alcance, por ejemplo un método, un marcador es puesto en el stack y
los valores son añadidos como se necesiten. Cuando se deja ese alcance, todos los valores son liberados
incluyendo el marcador del método. Esto funciona en cualquier nivel de anidado.
3
Pila (trad.)
4
Cúmulo, montón (trad.)
Hasta que veamos la interacción entre el heap y el stack, la única manera real de meterse en problemas
con el stack es con StackOverflowException. Esto significa que ha usado todo el espacio disponible
del stack. 99.9% del tiempo, esto indica una llamada recursiva interminable (una función que se llama a
sí misma ad infinitum). En teoría esto puede ser causado por un muy muy mal diseño de sistema,
aunque nunca he visto una llamada no recursiva usando todo el espacio del stack.
El Heap
La asignación de memoria en el heap no es tan simple como el stack. La mayoría de la asignación de
memoria basada en el heap ocurre cuando creamos un objeto new. El compilador averigua cuanta
memoria necesitaremos (lo cual no es tan difícil, aún con objetos con referencias anidadas), toma un
apropiado montón de memoria y regresa el apuntador a la memoria asignada (más acerca de esto en un
momento). El ejemplo más sencillo es una cadena, si cada carácter en una cadena toma 2 bytes, y
creamos una nueva cadena con el valor de “Hola Mundo”, entonces el CLR necesitará asignar 22 bytes
(11x2) más cualquier adicional necesitado.
Hablando de cadenas, seguramente ha oído que las cadenas son inmutables – esto es, una vez que ha
sido declarada una cadena y asignado un valor, si se modifica esa cadena (cambiando el valor o
concatenando otra cadena a ella), entonces una nueva cadena se crea. Esto realmente puede tener
implicaciones de rendimiento negativas, y por ello la recomendación general es usar un
StringBuilder para cualquier manipulación de cadenas significativa. La verdad es que cualquier
objeto almacenado en el heap es inmutable con respecto a la asignación de tamaño, y cualquier cambio
en el tamaño subyacente requerirá una nueva asignación. El StringBuilder, junto con algunas
colecciones, parcialmente pueden sacar la vuelta a esto usando buffers internos. Una vez que el buffer
se llena, la misma reasignación ocurre y algún tipo de algoritmo de crecimiento es usado para
determinar el nuevo tamaño (el más simple siendo antiguoTamaño * 2). Siempre que sea posible es
buena idea especificar la capacidad inicial de dichos objetos para evitar este tipo de reasignación (el
constructor para tanto el StringBuilder y el ArrayList (entre muchas otras colecciones) le
permiten especificar capacidad inicial).
Recolectar basura del heap es una tarea no trivial. A diferencia del stack donde el último alcance puede
simplemente liberarlo, los objetos del heap no son locales a un determinado alcance. En lugar de ello, la
mayoría son referencias profundamente anidadas de otros objetos referenciados. En lenguajes como en
C, cuando un programador causa que la memoria sea asignada al heap, debe asegurarse también de
remover del heap cuando ha terminado con él. En lenguajes administrados, el motor en tiempo de
ejecución se encarga de limpiar los recursos (.NET usa un Recolector de Basura Generacional que está
brevemente descrito en la Wikipedia).
Hay muchos incidentes horribles que pueden molestar a los desarrolladores mientras trabajan con el
heap. Fugas de memoria no solo son posibles sino muy comunes, la fragmentación de memoria puede
causar todo tipo de caos, y varios problemas de rendimiento pueden generarse gracias a
comportamiento de asignación extraño o interacción con código sin administrar (lo cual .NET hace
mucho debajo del agua).
Apuntadores
Para muchos desarrolladores, aprender sobre apuntadores en la escuela fue una experiencia dolorosa.
Representan la verdaderamente real indirección que existe entre código y hardware. Muchos más
desarrolladores nunca tuvieron la experiencia de aprender sobre ellos - saltaron directamente a
programar en un lenguaje que no los expone directamente. La verdad sin embargo es que cualquiera
que diga que C# o Java son lenguajes sin apuntadores es simplemente un error. Como los apuntadores
son el mecanismo con el cual todos los lenguajes almacenan valores en el heap, es más bien tonto no
entender como son usados.
Los apuntadores representan el nexus del modelo de memoria de un sistema – esto es, los apuntadores
son el mecanismo donde el stack y el heap trabajan juntos para proveer el subsistema de memoria
requerido por su programa. Como discutimos anteriormente, cuando instancia un objeto new, .NET
asigna un bloque de memoria al heap y regresa un apuntador al inicio de este bloque de memoria. Esto
es todo lo que un apuntador es: la dirección de inicio para el bloque de memoria que contiene un
objeto. La dirección no es nada más que un número único, generalmente representado en formato
hexadecimal. Por lo tanto, un apuntador no es nada más que un número único que le dice a .NET donde
está el objeto mismo en memoria. Esta indirección es transparente en Java o .NET, pero no en C o C++
donde se puede manipular la dirección de memoria directamente con un apuntador aritmético. En C o
C++ se puede tomar un apuntador y agregar 1 a él, y así arbitrariamente cambiar a donde está
apuntando (y seguramente hacer tronar el programa debido a esto).
Donde se pone interesante es donde el apuntador está realmente almacenado. Ellos en realidad siguen
las mismas reglas descritas arriba: como enteros son almacenados en el stack – al menos, claro, que
ellos formen parte de una referencia a un objeto y entonces estarán en el heap con el resto del objeto.
Puede no ser claro aún, pero esto significa que ultimadamente, todos los objetos heap están enraizados
al stack (posiblemente a través de numerosos niveles de referencias). Veamos primero este ejemplo
simple:
Del código de arriba, terminaremos con 2 valores en el stack, el entero 5 y el apuntador a nuestra
cadena, así como también precisamente el valor en el heap. Aquí una representación gráfica:
Cuando salimos de nuestra function main (olvidémonos del hecho de que el programa se parará),
nuestro stack liberará todos los valores locales, lo que significa que tanto el valor de x como de y se
perderán. Esto es significativo porque la memoria asignada en el heap todavía contiene nuestra cadena,
pero hemos perdido toda referencia a ella (no hay algún apuntador apuntándola). En C o C++ esto
resulta en una fuga de memoria – sin una referencia a nuestra dirección en el heap no podemos liberarla
de la memoria). En C# o Java, nuestro confiable recolector de basura detectará el objeto sin referencia y
lo liberará.
Veremos ejemplos más complejos, que aparte de tener más flechas apuntando, es básicamente el
mismo.
void HacerAlgo()
{
Empleado jefe = new Empleado(1);
_subordinado = new Empleado(2);
_subordinado.Gerente = _jefe;
}
}
Interesantemente, cuando salimos de nuestro método, la variable jefe se liberará del stack, pero el
subordinado, que está definido por el alcance padre, no. Esto significa que el recolector de basura no
tendrá nada que limpiar porque los dos valores del heap seguirán siendo referenciados (uno
directamente del stack, y el otro indirectamente del stack a través del objeto referenciado.
Como puede ver, los apuntadores definitivamente juegan una parte importante tanto en C# como en
VB.NET. Como el apuntador aritmético no está disponible en ninguno de estos lenguajes, los
apuntadores son grandemente simplificados y con suerte fácilmente entendidos.
Boxing
El Boxing ocurre cuando un tipo de valor (almacenado en el stack) es coaccionado en el heap. El
Unboxing ocurre cuando estos tipos de valor son puestos de vuelta al stack. La manera más simple de
coaccionar un tipo de valor, como un entero, en el heap es haciendo un cast 5con él:
int x = 5;
object y = x;
Un escenario más común donde el boxing ocurre es cuando se provee un tipo de valor a un método que
acepta un objeto. Esto es común con colecciones en .NET 1.x antes de la introducción de genéricas
(generics). Las clases de colecciones no genéricas mayormente trabajan con el tipo de objeto, así que el
código siguiente resulta en un boxing y unboxing:
ByRef
Sin un buen entendimiento de apuntadores, es virtualmente imposible entender pasar un valor por
referencia o por valor. Los desarrolladores generalmente entienden la implicación de pasar un tipo de
5
Conversión implícita
valor, como un entero, por referencia, pero pocos comprenden porque pasar una referencia por
referencia. ByRef y ByVal afectan la referencia y el tipo de valor por igual – entendiendo que siempre
trabajan contra el valor subyacente (que en el caso de un tipo de referencia significa que trabajan contra
el apuntador y no el valor). Usando ByRef es la única situación común donde .NET no resolverá
automáticamente la indirección del apuntador (pasando por referencia o como un parámetro de salida
no está permitido en Java).
Primero veremos como ByVal/ByRef afecta los tipos de valor. Dado el siguiente código:
int contador2 = 0;
SiembraContador(ref contador2);
Console.WriteLine(contador2);
}
Podemos esperar una salida de 0 precedida de 1. La primer llamada no pasa contador1 por referencia,
lo que significa que una copia de contador1 es pasado a SiembraContador y los cambios hechos
dentro son locales a la función. En otras palabras, estamos tomando el valor en el stack y duplicándolo a
otra localidad del stack.
En el Segundo caso estamos realmente pasando el valor por referencia lo que significa que ninguna
copia está siendo creada y los cambios no son localizados a la función SiembraContador.
El comportamiento de los tipos de referencia es exactamente el mismo, sin embargo no puede ser
aparente al principio. Veremos dos ejemplos. El primero usa una clase AdministracionPagos para
cambiar las propiedades de una Empleado. En el código de abajo vemos que tenemos dos empleados y
en ambos casos estamos otorgando un aumento de $2000. La única diferencia es que uno pasa el
empleado por referencia mientras que el otro lo pasa por valor. ¿Puede adivinar la salida?
En ambos casos, la salida es 12000. A primera vista, esto parece diferente a lo que vimos con los tipos de
valor. Lo que sucede es que pasar por referencia tipos de referencia por valor en verdad pasa una copia
del valor, pero no del valor en heap. En lugar de ello, pasamos una copia de nuestro apuntador. Y como
un apuntador y una copia del apuntador apuntan a la misma memoria en el heap, un cambio hecho en
uno se ve reflejado en el otro.
Cuando se pasa un tipo de referencia por referencia, se está pasando el apuntador mismo en lugar de
una copia del apuntador. Esto hace preguntarnos, ¿cuándo realmente se pasa un tipo de referencia por
referencia? La única razón para pasar por referencia es cuando se requiere modificar al apuntador
mismo – es decir, a donde apunta. Esto puede resultar en desagradables efectos secundarios – por lo
cual es una buena práctica que las funciones que así lo quieran deben especificar que necesitan el
parámetro pasado por referencia. Veamos nuestro segundo ejemplo.
{
private int _salario;
public int Salario
{
get {return _salario;}
set {_salario = value;}
}
public Empleado(int salarioInicial)
{
_salario = salarioInicial;
}
}
Intente averiguar qué pasará y porque. Una pista: una excepción será lanzada. Si dedujo que la llamada
a empleado1.Salario resultará en 10000 mientras que la segunda provocará una
NullReferenceException entonces está en lo cierto. En el primer caso estamos asignando a una
copia del apuntador original a null – no tiene ningún impacto en lo que está apuntando empleado1. En
el segundo caso, estamos pasando una copia pero con el mismo valor de stack usado por empleado2.
Entonces asignar al empleado a null es lo mismo que escribir empleado2 = null;.
No es muy común que se quiera cambiar la dirección apuntada por una variable desde un método
separado – por lo que la única vez que tal vez vea un tipo de referencia pasado por referencia es cuando
quiera regresar múltiples valores desde un llamado a función (en cuyo caso sería mejor usar un
parámetro out, o usar un acercamiento OO más puro). El ejemplo de arriba verdaderamente señala los
peligros de jugar en un ambiente donde las reglas no son comprendidas completamente.
Nuestro valor en stack (un apuntador) será liberado, y con él se irá la única manera que tenemos
referencia a la memoria creada para almacenar nuestra cadena. Dejándonos con ningún método de
liberarla. Este no es un problema en .NET porque tiene un recolector de basura que se fija en memoria
sin referencia y la libera. Sin embargo, un tipo de fuga de memoria es todavía posible si se mantienen las
referencias indefinidamente. Esto es común en aplicaciones grandes con referencias profundamente
anidadas. Estos pueden ser difícil de identificar pues la fuga puede ser muy pequeña y la aplicación
puede que no sea ejecutada por largo tiempo.
Ultimadamente cuando su programa termina el sistema operativo reclamará toda la memoria, fugada o
no. Sin embargo, si empieza a ver OutOfMemoryException y no está usando datos inusualmente
grandes, hay una buena posibilidad de tener una fuga de memoria. .NET tiene herramientas para
ayudarlo, pero seguramente querrá aprovechar un verificador de memoria comercial como dotTrace o
ANTS Profiler. Al estar cazando fugas de memoria estará viendo por objetos fugados (lo que es muy fácil
de hacer tomando dos muestras de su memoria y compararlas), rastreando a través de todos los objetos
que aún mantienen una referencia a ellos y corregir el problema.
Hay una situación en específico que es conveniente mencionar como una causa común de fugas de
memoria: eventos. Si, en una clase, se registra un evento, una referencia es creada a su clase. A menos
que se deregistre del evento el ciclo de vida de sus objetos serán finalmente determinados por la fuente
del evento. En otras palabras, si ClassA (el escucha) registra un evento de ClassB (la fuente del
evento) una referencia es creada de ClassB a ClassA. Hay dos soluciones: deregistrar de los eventos
cuando hayamos terminado (el patrón IDisposable es la solución idónea), o usar el Patrón WeakEvent o
una versión simplificada.
Fragmentación
Otra causa del OutOfMemoryException tiene que ver con la fragmentación de memoria. Cuando la
memoria es asignada en el heap siempre es un bloque continuo. Esto significa que la memoria
disponible debe ser localizada para un bloque suficientemente grande. Al ejecutarse su programa, el
heap se vuelve cada vez más fragmentado (como en su disco duro) y puede terminar con mucho
espacio, pero repartido en una manera que lo hace inusable. Bajo circunstancias normales, el recolector
de basura compactará el heap conforme vaya liberando memoria. Al compactar memoria, las
direcciones de los objetos cambian y .NET se asegura de actualizar todas las referencias
apropiadamente. Sin embargo, algunas veces .NET no puede mover un objeto: sobre todo cuando el
objeto está fijado a una dirección de memoria específica.
Fijamiento
El fijamiento (pinning) ocurre cuando un objeto está anclado a una dirección específica en el heap. La
memoria fija no puede ser compactada por el recolector de basura resultando en fragmentación. ¿Por
qué se fijan los valores? La causa más común es porque su código está interactuando con código no
administrado. Cuando el recolector de basura de .NET compacta el heap, actualiza todas las referencias
en el código administrado, pero no tiene manera de entrar al código no administrado y hacer lo mismo.
Por lo tanto, antes de interoperar primero debe fijar objetos a la memoria. Como muchos métodos en el
.NET Framework dependen de código no administrado, el fijamiento puede ocurrir sin que se sepa de
ello (el escenario con el que estoy familiarizado son las clases Socket .NET que dependen de
implementaciones no administradas y buffers de pins).
Una manera común de sacar la vuelta a este tipo de fijamiento es declarar objetos grandes que no
causan mucha fragmentación como los más pequeños (esto es incluso más verdadero considerando que
objetos grandes son puestos en un heap especial (llamado Large Object Heap (LOH) que no es
compactado). Por ejemplo, en lugar de crear cientos de buffers de 4KB, puede crear un buffer grande y
asignar pedazos de él usted mismo. Para un ejemplo y obtener más información de fijamiento, sugiero
ver el apost avanzado de Greg Young acerca de fijamiento y sockets asíncronos.
Hay una segunda razón por la cual un objeto puede ser fijado – cuando usted explícitamente lo hace. En
C# (no en VB.NET) si usted compila su ensamblado con la opción unsafe , puede fijar un objeto con la
sentencia fixed. Mientras que fijamiento extensivo presurisa el sistema, el uso justo de la sentencia
fixed puede grandemente mejorar el rendimiento. ¿Por qué? Porque un objeto fijado puede ser
manipulado directamente con aritmética de apuntador – esto no es posible si el objeto no está fijado
pues el recolector de basura puede reasignar su objeto en algún otro sitio de la memoria.
Tome como ejemplo este eficiente conversión de una cadena ASCII a entera que se ejecuta 6 veces más
rápido que con int.Parse.
A menos que haga algo anormal, no puede haber razón alguna para marcar su ensamblado como unsafe
y tomar ventaja de la sentencia fixed. El código de arriba fácilmente colgarse (pase null como la
cadena y vea que pasa), no es tan rico en características como int.Parse, y en la escala de cosas a
considerar es extremadamente riesgoso mientras que no provee beneficios.
Asignar a null
Así que, ¿debería asignar sus tipos de referencia a null cuando ha terminado con ellos? Por supuesto
que no. Una vez que la variable sale de su alcance, se libera del stack y la referencia es removida. Si no
puede esperar a que se salga del alcance, tal vez necesite refactorizar su código.
Finalización Determinística
A pesar de la presencia del recolector de basura, los desarrolladores deben tomar cuidado de manejar
algunas de sus referencias. Esto porque algunos objetos dependen de recursos vitales o limitados, como
los manejadores de archivos o conexiones de bases de datos que deben ser liberadas tan pronto como
sea posible. Esto es problemático pues no sabemos cuando el recolector de basura va a ejecutarse – por
naturaleza el recolector de basura solo se ejecuta cuando la memoria escasea. Para compensar, las
clases que dependen de estos recursos pueden hacer uso del patrón Disposable. Todos los
desarrolladores .NET puede que sean familiares con este patrón, así como con su implementación ) la
interfaz IDisposable), así que no ahondaremos en lo que ya sabe. Con respecto a este capítulo, es
simplemente importante que entienda el rol determinístico de la finalización. No libera memoria usada
por el objeto. Libera recursos. En el caso de las conexiones con bases de datos por ejemplo, libera la
conexión de regreso al pool para que pueda ser reusada.
Finalmente, si está construyendo una clase que pueda ser beneficiada de una finalización determinística
encontrará que implementar el patrón IDisposable es simple. Una guía sencilla está disponible en
MSDN.
En este capítulo
Stacks, heaps y apuntadores pueden ser abrumadores al principio. Dentro del contexto de los lenguajes
administrados, no hay mucho de ello realmente. Los beneficios de entender estos conceptos son
tangibles en la programación del día a día, e invaluables cuando algún comportamiento inesperado
ocurre. Puede ser el programador que causa raros NullReferenceExceptions y
OutOfMemoryExceptions, o el que tiene que corregirlos.
L as excepciones son construcciones tan poderosas que los desarrolladores se pueden sentir
agobiados y un tanto a la defensiva al lidiar con ellas. Esto es desafortunado porque las
excepciones realmente representan una oportunidad clave para que desarrolladores hagan sus
sistemas considerablemente más robustos. En este capítulo veremos tres distintos aspectos de las
excepciones: manejo, creación y lanzamiento. Considerando que las excepciones son inamovibles usted
no puede ni correr, ni esconderse de ellas, así que mejor contrólelas.
Manejando Excepciones
Su estrategia para manejar excepciones debería contemplar dos reglas de oro:
1. Sólo atrape aquellas excepciones por las cuales pueda hacer algo, y
2. Usted no puede hacer nada ante la gran mayoría de las excepciones
La mayoría de los desarrolladores novatos hace exactamente lo contrario a la primera regla, y luchan
desesperadamente contra la segunda. Cuando su aplicación realice algo de manera realmente
excepcional y fuera de la operación normal, lo mejorar que se puede hacer es fallar en ese lugar y en ese
momento. Si no lo hace, no sólo perderá información acerca de su misterioso problema, sino que se
arriesga a poner su aplicación en un estado desconocido, el cual puede llevarlo a resultados de
consecuencias mucho peores.
En cualquier momento usted puede encontrarse escribiendo una sentencia try/catch, y preguntarse si
realmente puede hacer algo si sucede una excepción. Si su base de datos se cae, ¿realmente puede
escribir código para recuperarla? ¿No sería mejor desplegar un amigable mensaje de error al usuario y
notificar el problema? Es duro aceptarlo en un principio, pero algunas veces es mejor colapsar, registrar
el error y terminar. Aún para sistemas de misión crítica, si se emplea una base de datos de forma
normal, ¿qué puede hacer si esta se cae? Este orden de ideas no es exclusivo para bases de datos o para
fallas ambientales, sino también para errores comunes. Si al convertir un valor de configuración a
entero se recibe una excepción de formato, ¿tendrá sentido continuar como si todo estuviera bien?
Seguramente no.
Por supuesto, si puede controlar una excepción definitivamente debe hacerlo - pero asegúrese de
capturar únicamente el tipo de excepción que pueda controlar. Capturar excepciones y no controlarlas
realmente se llama indigestión de excepción (prefiero llamarlo ilusiones) y es codificar mal. Un ejemplo
que frecuentemente veo tiene que ver con la validación de las entradas. Por ejemplo, veamos cómo no
deberíamos de tratar el IdCategoría que se pasa por una consulta en una página ASP.NET.
int IdCategoría;
try
{
IdCategría = int.Parse(Request.QueryString["IdCategoría"]);
}
catch(Exception)
{
IdCategoría = 1;
}
El problema con el código anterior es que independientemente del tipo de excepción que se produzca,
se tratará del mismo modo. ¿Podría el valor 1 manejar una excepción de desbordamiento de memoria?
En lugar del código anterior debería capturar una excepción específica:
int IdCategoría;
try
{
IdCategoría = int.Parse(Request.QueryString["IdCategoría"])
}
catch(FormatException)
{
IdCategoría = -1;
}
(un mejor acercamiento a este problema sería utilizar la función de int.TryParse introducida en .NET 2.0
- sobre todo teniendo en cuenta que int.Parse puede
producir otros dos tipos de excepciones que nos Palabras de advertencia basadas en
gustaría controlar de la misma manera, pero esa es otra una mala experiencia personal:
historia). algunos tipos de excepciones
tienden a multiplicarse. Si opta por
Registros
A pesar de que no se controlen la mayoría de la
enviar correos siempre que ocurre
excepciones, usted debería registrar todas y cada una una excepción podrá fácilmente
de ellas. Idealmente, podría centralizar su registro – un saturar su servidor de correo. Una
HttpModule de OnError es su mejor opción para una solución más inteligente
aplicación ASP.NET o servicio web. He visto a menudo a implementaría algún tipo de buffer
los desarrolladores capturar excepciones donde estas o agregación.
producen sólo por registrarlas y volverlas a lanzar. Esto
provoca una gran cantidad de código innecesario y repetitivo – es mejor dejar que las excepciones
emerjan hasta el código y registrar todas las excepciones en la frontera de su sistema. Para precisar qué
implementación de registro de excepciones utilizará deberá saber que tan críticas son. Tal vez querrá
notificar por correo electrónico tan pronto como se produzca una excepción, o tal vez pueda
simplemente registrarla en un archivo o base de datos que revise diariamente o tal vez tenga otro
proceso para enviar un resumen diario. Muchos desarrolladores aprovechan frameworks para generar
registros como log4net o Microsoft´s Logging Application Block.
Limpieza
En el capítulo anterior hablamos de la finalización determinista con respecto a la naturaleza perezosa
del recolector de basura. Las excepciones añaden complejidad a esta situación ya que su naturaleza
abrupta puede causar que el método Dispose no sea llamado. Un error al llamar a la base de datos es un
ejemplo clásico:
SqlConnection conexión;
SqlCommand comando;
try
{
conexión = new SqlConnection(FROM_CONFIGURATION)
comando = new SqlCommand("AlgúnSQL", conexión);
conexión.Open();
comando.ExecuteNonQuery();
}
finally
{
if (comando != null) { comando.Dispose(); }
if (conexión != null) { conexión.Dispose(); }
}
El punto es que incluso si usted no puede controlar una excepción, y debe centralizar todos su registro,
es necesario ser conscientes de dónde pueden surgir las excepciones - especialmente en lo que se
refiere a las clases que implementan IDiposable.
Lanzar Excepciones
No hay una regla mágica para generar excepciones ni la hay para capturarlas (de nuevo, la regla es “no
capture excepciones a menos que realmente las pueda controlar”). Sin embargo, generar excepciones,
sean o no las suyas (lo que trataremos a continuación), aún es bastante sencillo. Primero analizaremos la
mecánica real de generar excepciones, que se basa en la instrucción throw. A continuación
examinaremos cuándo y por qué es realmente deseable producir excepciones.
//o
Agregué el segundo ejemplo porque algunos desarrolladores piensan que las excepciones son algo
especial y único - pero la verdad es que son igual que cualquier otro objeto (excepto que heredan de
System.Exception, que a su vez hereda de System.Object). De hecho, el crea una nueva excepción no
significa que se tenga que lanzarla - aunque probablemente siempre lo hará.
En ocasiones necesitará redirigir una excepción porque, aunque no pueda controlar la excepción,
necesita ejecutar código en cuanto se produzca la excepción. El ejemplo más común es tener que
deshacer una transacción cuando algo falla:
Si usted encuentra en una situación donde cree que desea redirigir una excepción con el controlador
como la fuente, un mejor enfoque consiste en utilizar una excepción anidada:
De esta forma la traza de la pila original es accesible mediante la propiedad InnerException expuesta por
todas las excepciones.
Existen realmente dos niveles de reflexión sobre cómo se deben utilizar excepciones. El primer nivel,
que es universalmente aceptado, es que usted no debería dudar en plantear una excepción cuando se
produce una situación realmente excepcional. Mi ejemplo favorito es el análisis de archivos de
configuración. Muchos desarrolladores utilizan generosamente valores predeterminados para cualquier
entrada no válida. Esto está bien en algunos casos, pero en otros puede poner el sistema en un estado
poco fiable o inesperado. Otro ejemplo podría ser una aplicación de Facebook que obtiene un resultado
inesperado de una llamada a la API. Usted puede pasar por alto el error, o podría generar una
excepción, registrarla (de modo que pueda corregir, ya que podría haber cambiado la API) y presentar
un mensaje útil para los usuarios.
La otra creencia es que las excepciones no deberían reservarse para situaciones excepcionales, sino que
deberían utilizarse en cualquier situación en la que no se puede ejecutar el comportamiento esperado.
Este enfoque está relacionado con el diseño por contrato - una metodología que estoy adoptando más y
más cada día. Esencialmente, si el método de GuardarUsuario no es capaz de Guardar el usuario, debe
producir una excepción.
En lenguajes como C#, VB.NET y Java, que no son compatibles con los mecanismo de diseño por
contrato, este enfoque puede tener resultados diversos. Una tabla Hash devuelve null cuando no se
encuentra una clave, pero un diccionario produce una excepción - el comportamiento impredecible no
ayuda (si siente curiosidad de saber por qué ellos funcionan de forma diferente revise la publicación de
Brad Abrams en su blog). Existe también una línea que divide lo que constituye el flujo de control y lo
que se considera una excepción. Las excepciones no deberían utilizarse para controlar una lógica como
if/else, pero si es tan grande el papel que desempeñan en una biblioteca, probablemente los
programadores deberían utilizarlas como tal (el método de int.Parse es un buen ejemplo de esto).
En general, me resulta fácil decidir qué debería y no debería generar una excepción. Generalmente me
hago preguntas como:
• Es esto excepcional,
• Es esto lo esperado,
• Puedo continuar haciendo algo significativo en este momento y
• Esto es algo que debería realizarse consciente de modo que yo puedo arreglarlo, o al menos
darle una revisión.
Quizás lo más importante por hacer cuando se generan excepciones, o al trabajar con excepciones en
general, es pensar en el usuario. La gran mayoría de los usuarios son ingenuos en comparación con los
programadores y pueden fácilmente caer en pánico cuando se presentan mensajes de error. Jeff
Atwood recientemente publicó en su blog la importancia de estrellarse responsablemente.
Los usuarios no deberían estar expuestos a la pantalla azul de Windows (no se piense que dado que la
vara está en una posición baja está bien ser perezoso).
Muchas de las excepciones que creo son nada más excepciones de marcado - es decir, amplían la clase
base de System.Exception y no proporcionan nada adicional. Comparo estas interfaces de marcador (o
atributos de marcador), tales como la interfaz INamingContainer. Son particularmente útiles porque
permiten evitar la indigestión de excepciones. Tomemos el siguiente código como ejemplo. Si el método
Guardar() no inicia una excepción personalizada cuando se produce un error en la validación, no
tendremos más remedio que tragarnos todas las excepciones:
try
{
usuario.Guardar();
}
catch
{
Error.Text = usuario.ObtenerErrores();
Error.Visible = true;
}
//versus
try
{
usuario.Guardar();
}
catch(ValidationException ex)
{
Error.Text = ex.GetValidationMessage();
Error.Visible = true;
}
En el ejemplo anterior también se muestra cómo podemos extender las excepciones para proporcionar
comportamientos más personalizado y específicamente relacionados con nuestras excepciones. Esto
puede ser tan simple como un ErrorCode, para información más compleja y como un
PermissionException que expone el permiso del usuario y el permiso necesario que falta.
Por supuesto, no todas las excepciones están vinculadas al dominio. Es común ver más excepciones
orientadas a la operación. Si confía en un servicio web que devuelve un código de error, es muy posible
que deba hacer ajustes en sus excepciones personalizada para detener la ejecución (Recuerde, falle
rápido) y aproveche su infraestructura de registro.
Crear una excepción personalizada es realmente un proceso de dos pasos. Primero (y técnicamente esto
es todo lo que se necesita) crear una clase, con un nombre significativo, que hereda de
System.Exception.
El paso extra que puede aplicar es marcar su clase con SerializeAttribute y siempre ofrecer
al menos cuatro constructores:
public LaExcepción()
public LaExcepción(string mensaje)
public LaExcepción(string mensaje, Exception ExcepciónReferida)
protected LaExcepción(SerializationInfo información, StreamingContext contexto)
Los tres primeros permiten que la excepción se utilice en una forma esperada. El cuarto se utiliza para
admitir la serialización ya que .NET requiere serializar las excepciones - lo que significa que también
debería implementar el método GetObjectData. El propósito de la serialización es que en caso de tener
propiedades personalizadas, que desee sobrevivan a la ejecución las pueda serializar y deserializar. A
continuación se muestra el ejemplo completo:
[Serializable]
public class ExcepciónMejorada : Exception
{
private int _IdMejora;
{
if (info != null)
{
_IdMejora = info.GetInt32("IdMejora");
}
}
public override void GetObjectData(SerializationInfo i, StreamingContext c)
{
if (i != null)
{
i.AddValue("IdMejora", _IdMejora);
}
base.GetObjectData(i, c)
}
}
En este Capítulo
Puede tardar bastante un cambio fundamental en la perspectiva para apreciar todo lo que las
excepciones pueden ofrecer. Las excepciones no son algo que deba ser temido o de lo que debamos
protegernos, sino más bien deben ser tratadas como información vital sobre la salud del sistema. Se
debe evitar la indigestión por excepciones. No se deben atrapar una excepción a menos que realmente
la pueda controlar. Es igualmente importante hacer uso de las excepciones incorporadas y el de las
excepciones propias cuando algo inesperado ocurre dentro del código. Incluso se puede expandir este
patrón para cualquier método que no hace lo que dice que hará. Por último, las excepciones son una
parte del modelado del negocio. Como tal, las excepciones no sólo son útiles para fines operacionales
sino también deberían formar parte del modelado de su dominio general.
P ocas palabras clave son tan simples pero tan sorprendentemente poderosas como virtual en
C# (overridable en VB.NET). Cuando se marca un método como virtual se está permitiendo
que una clase que hereda de otra pueda cambiar su comportamiento. Sin esta funcionalidad, la
herencia y el polimorfismo no serían de mucha utilidad. Un ejemplo simple, ligeramente modificado y
extraído de Programming Ruby (ISBN: 978-0-9745140-5-5), cambia el comportamiento al método to_s
de la clase Song (ToString) que a su vez hereda de la clase KaraokeSong,se muestra a continuación
class Song
def to_s
return sprintf("Song: %s, %s (%d)", @name, @artist, @duration)
end
end
Quizás te hayas dado cuenta en ese mismo código, que el método base to_s no está marcado como
virtual. Esto es porque en muchos lenguajes, incluyendo Java, los métodos son virtuales por defecto.
Esto representa una diferencia de opinión fundamental entre los diseñadores de lenguaje Java y los
diseñadores de C#/VB.NET. En C# los métodos son finales por defecto y los desarrolladores
explícitamente deben permitir el cambio de comportamiento (override mediante la palabra clave
virtual). En Java los métodos son virtuales por defecto y los desarrolladores explícitamente deben de
prohibir el cambio de comportamiento u override mediante la palabra clave final)
Típicamente los métodos virtuales son discutidos en el contexto de la herencia de los modelos de
dominio, por ejemplo: Una CanciónKaraoke hereda de Canción o un Perro hereda de Mascota.
Estos conceptos son muy importantes pero ya están bien documentados por lo tanto examinaremos los
métodos virtuales mediante un acercamiento más técnico: Los proxies
Gracias al polimorfismo FindById puede regresar una Task o una TaskProxy. El cliente no
tiene que saber cual fue regresado, de hecho tampoco necesita conocer que el objeto TaskProxy existe.
Solo se programa a través de la API pública.
Debido a que el proxy es solamente una subclase que implementa comportamiento adicional quizás te
podías preguntar si un Perro es un proxy de Mascota. Los proxies tienden a implementar funciones más
técnicas como el monitoreo, el caching, la autorización etc. de una manera transparente. En otras
palabras no se debería declarar una variable como TaskProxy, si no que más bien se declararía una
variable del tipo Perro. Como consecuencia un proxy no añadiría más miembros toda vez sé que tú no
estás programando en contra de su API, de la misma manera que si una clase Perro implementara un
método Ladrar
Interception
La razón por la cual estamos explorando los temas de la herencia de una manera más técnica es porque
las dos herramientas que se han tratado en el contexto del libro RhinoMocks e NHibernate hacen uso
extensivo de los proxies aunque no sea notorio a primera vista.
RhinoMocks usa un proxy es para soportar su funcionalidad base mientras que NHibernate utiliza
proxies para explotar sus capacidades de carga perezosa (lazy loading). En este capítulo veremos cómo
funciona NHibernate toda vez que es más fácil entender qué es lo que sucede tras bambalinas con esta
tecnología sin embargo el mismo nivel de abstracción se aplica con RhinoMocks
(Nota sobre NHibernate: Es un sistema de mapeo entidad relación transparente o sin consecuencias
debido a que no es necesario modificar las clases del dominio para que funcione. Sin embargo para
poder habilitar el mecanismo de carga perezosa todos los miembros deben de ser virtuales. Esto todavía
se puede considerar sin consecuencias o transparente debido a que el programador no añade
elementos específicos de NHibernate a tus clases, como heredar de una clase base o agregar atributos
por todas partes)
Cuando se utiliza NHibernate existen dos maneras diferentes para proveer el mecanismo de carga
perezosa. El primero y el más obvio es cuando utilizamos colecciones hijas por ejemplo: Es probable que
en algunas ocasiones no se quiera cargar Model's Upgrades hasta que son necesitadas realmente. El
archivo de mapeo se vería como el siguiente:
Al asignar el atributo lazy a true en nuestro elemento bag, le estamos especificando a NHibrernate a que
cargue perezosamente la colección Upgrades. NHibernate puede hacerlo sencillamente debido a que
regresa sus propios tipos de Colección (Todos de los cuales implementan interfaces por defecto como
IList, por lo que para el programador resulta transparente)
El segundo, y por mucho, más interesante, se utiliza la carga perezosa para los objetos individuales de
dominio. La idea en concreto es que algunas veces será necesario que los objetos enteros sean cargados
diferidamente. ¿Por qué? Bueno, supongamos entonces que se ha realizado una venta (Sale). Las ventas
están asociadas con una persona de ventas (SalesPerson) y un modelo de carro.
Desafortunadamente tuvimos que hacer una conexión a la base de datos dos veces para los objetos
SalesPerson y Model aunque realmente no son usados. Realmente lo único que necesitamos es su ID
(toda vez que es lo que se inserta en la base de datos), que ya tenemos.
Al momento de crear un proxy NHibernate nos permite cargar a un objeto de una manera perezosa
solamente para este tipo de circunstancias. Lo primero que se debe hacer es cambiar nuestro mapeo y
permitir la carga perezosa para los modelos Models y SalesPeoples
El atributo proxy le dice a NHibernate qué tipo debe de ser utilizado con el proxy. Este tipo puede ser la
clase misma a la cual se están mapeando, o una interface implementada por la clase
Dado que estamos usando la misma clase que nuestra interface de proxy, necesitamos asegurarnos de
que todos los miembros están marcados como virtuales. Si alguno no lo está NHibernate una excepción
con una lista de métodos no virtuales. Una vez dicho esto, estamos listos:
En el código anterior podemos ver que estamos utilizando Load en lugar de Get. La diferencia entre
ambos es que al utilizar una clase que soportar carga perezosa el método Load obtendrá el proxy
mientras que el método Get obtendrá el objeto real. con este código ya no estamos yendo hacia la base
de datos para cargas los IDs. En su lugar el llamar Session.Load<Model>(2)regresa un proxy
dinámicamente generado por NHibernate.
El proxy tendrá un ID de 2 debido a que ya se le proporcionó el valor y todas las demás propiedades
están sin y inicializar
Cualquier llamada a otro miembro de nuestro proxy como sale.Model.Name será transparentemente
interceptado y el objeto será cargado en tiempo de ejecución desde la base de datos
Una nota adicional es que la capacidad de carga perezosa NHibernate puede ser difícil de monitorear al
momento de hacer una depuración dentro de Visual Studio, esto es porque su funcionalidad de
realmente inspecciona los valores de los objetos al ser inspeccionados. La mejor manera de examinar
qué es lo que está sucediendo es añadir un par de puntos de interrupción al momento de ejecutar el
código fuente y revisar qué es lo que está sucediendo dentro de la base de datos a través de la
herramienta SQL Profiler o el log de NHibernate.
Esperamos también que te puedas imaginar cómo funcionan los proxies utilizados por RhinoMocks para
grabar datos y para generar interacciones al momento de que se crea un objeto realmente está creando
un Proxy. Un objeto real es del proxy intercepta todas las llamadas y dependiendo en qué estado se
encuentre as de su propia funcionalidad realiza su tarea. Desde luego para que esto funcione se debe de
generar una interface prototipo y miembros virtuales de las clases
En éste capítulo
En el capítulo 6 cubrimos brevemente las capacidades de carga perezosa NHibernate. En este capítulo
expandimos la discusión para llegar a más a las implementaciones a reales. El uso de proxies es tan
común que no solamente te encontrarás con ellos sino que es muy probable que tengas una buena
razón para implementarlos tú mismo. Aun me encuentro impresionado con la funcionalidad que se
proveen en RhinoMock e NHibernate gracias al patrón de diseño del proxy.
Desde luego que todo se basa en permitirles cambiar o inyectar su comportamiento en las clases de tu
dominio.
Esperamos que este capítulo te haga ahondar en la discusión de que métodos deberían de ser virtuales y
cuáles no.
Se recomienda fuertemente que visites las ligas que se han provisto en la primera página de este
capítulo para que puedas entender los pros y los contras de los métodos virtuales y finales.
Resumiendo
P ara muchos, programar es un trabajo de retos y disfrutable que paga los recibos. Sin embargo,
dado que leído a lo largo de todo esto, hay una posibilidad de que, como yo, programar es algo
más importante para usted. Es una labor artesanal, y lo que crea significa más para usted que lo
que cualquiera no programador pueda entender. Tomo con mucho orgullo y placer que construir algo
que sobresale a mi nivel de calidad y aprender de las partes que necesitan ser mejoradas.
No es fácil construir software de calidad. Nuevas características aquí o un malentendido allá, y nuestro
trabajo duro empieza, aunque levemente, a mostrar debilidades. Es por eso que es importante tener un
entendimiento sólido de los fundamentos de buen diseño de software. Logramos cubrir muchos de los
detalles reales de implementación, pero a un nivel alto, aquí están mis principios esenciales de una
buena ingeniería de software:
La solución más flexible es la más simple. He trabajado en proyectos cuya flexibilidad está
construida desde el principio en el sistema. Siempre ha sido un desastre. Para muestra de este
entendimiento está en YAGNI, DRY, KISS y claridad.
El acoplamiento no es evitable, pero debe ser minimizado. Lo más dependiente que una clase
sea de otra clase, o una capa de otra capa, será más difícil que su código cambie. Creo
fuertemente que la ruta para dominar bajo acoplamiento es por las pruebas unitarias. Código
mal acoplado será imposible de probar y será llanamente obvio.
La noción de que los desarrolladores no deberían probar ha sido la anti bala de plata de nuestro
tiempo. Usted es responsable (y de ser posible el que rinda cuentas) del código que escribe, y
pensar que un método o clase hace lo que se supone no es suficiente. La búsqueda de la
perfección debe ser suficiente razón para escribir pruebas, pero, hay mejores razones para
hacerlo. Probar ayuda a identificar malos acoplamientos. Probar ayuda a encontrar cosas raras
en su API. Probar ayuda a documentar comportamiento y expectativas. Probar permite hacer
cambios pequeños pero radicales con mayor confianza y mucho menos riesgo.
Es posible construir software exitoso sin ser Ágil – pero no será con tanta certeza y mucho
menos divertido. Mis gozos serían de poco tiempo sin una colaboración del cliente constante.
Renunciaría a esta carrera sin desarrollos iterativos. Viviría en un sueño si requiriera
especificaciones firmadas antes de empezar a desarrollar.
Cuestione el status quo y siempre esté al acecho de alternativas. Tome buena parte de su
tiempo aprendiendo. Aprenda diferentes lenguajes y diferentes marcos de trabajo. Aprendiendo
Ruby y Rails me ha hecho mucho mejor programador. Puedo identificar el principio de mi
camino en ser mejor programador a hace unos años atrás cuando estaba muy envuelto en
código fuente para un proyecto open source, tratando de hallar sentido en él.
Mi último consejo es no se dé por vencido. Soy muy lento para aprender, y mientras más aprendo,
más me doy cuenta de lo poco que sé. Y todavía no entiendo la mitad de lo que mis pares bloggean
en CodeBetter.com (en serio). Si está dispuesto a tomar su tiempo y tratarlo, verá el progreso.
Escoja un simple proyecto para invertirle un fin de semana construyéndolo usando las nuevas
herramientas y principios (sólo elija uno o dos a la vez). Más importantemente, si no está
divirtiéndose, no lo haga. Y si usted tiene la oportunidad de aprender de un mentor o de un
proyecto, tómela – aunque usted aprenda más de equivocaciones que de éxitos.
Sinceramente espero que encuentre algo valioso aquí. Si lo desea, puede agradecerme haciendo
algo bueno para alguien especial, algún gesto amable a un extraño, o algo significativo por el medio
ambiente.
Recuerde descargar gratuitamente la Canvas Learning Application para una mirada más a
detalle de las ideas y herramientas representadas en este libro.