Sie sind auf Seite 1von 324

Andrs Marzal

Buenas prcticas en desarrollo de software con .NET

Buenas practicas en desarrollo de software


con .NET
Tratamos de aprender buenas prcticas en desarrollo de software. Algunas se derivan del ms puro sentido
comn, y no hay problema en adoptarlas tan pronto se llama la atencin sobre ellas: la misma exposicin a
esas buenas prcticas es suficiente para hacerlas propias y aplicarlas exitosamente. Otras requieren haber
padecido en carne propia una serie de dificultades que se plantean en ciertos contextos del desarrollo de
software. Y, muy importante, la percepcin de esas buenas prcticas como una ayuda efectiva depende de
encontrarse en un estadio de conocimiento como desarrollador de software que capacite para estudiarlas e
interiorizarlas. Planteemos para empezar, pues, una breve digresin acerca de los niveles en el aprendizaje
de una habilidad.

1 Aprenda a programar en cuntas horas?


1.1 El modelo Dreyfus de adquisicin de habilidades
Hay una clasificacin en varios niveles de las etapas del aprendizaje de habilidades. Se conoce por modelo
Dreyfus al tomar el nombre de sus autores, los hermanos Stuart y Hubert Dreyfus. El modelo Dreyfus se
present a principios de los 80 en el trabajo A Five-Stage Model of the Mental Activities Involved in Directed
Skill Acquisition, un informe que se redact para la Air Force Office of Scientific Research con objeto de
mejorar el adiestramiento de pilotos de avin.

La clasificacin de los Dreyfus refleja los cambios que se experimentan en el dominio de habilidades durante
su aprendizaje. En estos cambios, el aprendiz pasa

del apoyo en principios abstractos al uso de experiencias pasadas especficas como paradigmas;
del pensamiento analtico basado en normas a la intuicin;
de una percepcin en la que todo parece una agregacin dispersa de partes con igual relevancia a
otra en la que se ve un todo en el que slo ciertas partes son relevantes;
de un estado de observador ajeno a la situacin a otro de participante totalmente implicado en la
situacin.

Andrs Marzal

Buenas prcticas en desarrollo de software con .NET

Segn el modelo, los cinco niveles en la adquisicin de una habilidad son1


1.
2.
3.
4.
5.

Principiante
Principiante avanzado
Competente
Eficiente
Experto

Evidentemente, y aunque no se mencione en la lista de niveles, hay un nivel an menos avanzado que los
citados: ignorante. De ah partimos todos.
1.1.1 Principiante
Se empieza a tener consciencia del rea de aprendizaje, pero slo con ideas y conceptos abstractos. El
principiante tiene poca o ninguna habilidad para poner las ideas en prctica de modo fiable. Aplica lo
aprendido siguiendo reglas sin considerar el contexto.

Descomposicin de la situacin: necesita que los elementos se definan clara y objetivamente.


Comportamiento en la toma de decisiones: sigue reglas sin considerar el contexto.
Cmo se ejercita el juicio: .
Habilidades y herramientas: expuesto a ellas y capaz de aplicarlas si se le dirige.
Tiempo de entrenamiento formal: 2 a 5 das.
Entrenamiento con prctica: 1 a 2 meses.

1.1.2 Principiante avanzado


Las experiencias con casos reales producen un aprendizaje marginal que le permite alcanzar un nivel de
prestaciones aceptable. Empieza a entender el alcance del rea de aprendizaje y reconoce la falta de
conocimiento sobre la disciplina. Puede aplicar herramientas, procesos y principios en contextos similares a
los casos bien definidos que han estudiado.

Descomposicin de la situacin: percibe similitudes con ejemplos ya vistos.


Comportamiento en la toma de decisiones: encuentra correspondencias con el conjunto de reglas
apropiado.
Cmo se ejercita el juicio: .
Habilidades y herramientas: las reconoce y aplica en entornos estructurados.
Tiempo de entrenamiento formal: 5 a 10 das.
Entrenamiento con prctica: 3 a 6 meses.

1.1.3 Competente
Cuenta con trabajo prctico en varias reas que forman el campo de aprendizaje. Internaliza nuevas
habilidades con la capacidad de ir ms all de los procedimientos ligados a reglas en un entorno muy
estructurado. Adapta el aprendizaje a diferentes situaciones analizando circunstancias cambiantes y
seleccionando entre alternativas viables.

Descomposicin de la situacin: considera varias alternativas.

Los niveles que se muestran son los que se usan al citar el modelo de Dreyfus, aunque en su trabajo original usaban
una nomenclatura distinta: principianta (novice), competencia (competence), eficiencia (proficiency), expertitud
(expertise) y maestra (maestry).

Andrs Marzal

Buenas prcticas en desarrollo de software con .NET

Comportamiento en la toma de decisiones: determina analticamente la mejor alternativa.


Cmo se ejercita el juicio: considera conscientemente el valor de las consecuencias de cada
alternativa.
Habilidades y herramientas: internalizadas y aplicadas a ms entornos desestructurados.
Tiempo de entrenamiento formal: 20 a 30 das.
Entrenamiento con prctica: 12 a 18 meses.

1.1.4 Eficiente
Dispone de experiencia en varias situaciones. Ha internalizado herramientas y conceptos que puede aplicar a
una variedad de situaciones sin gran esfuerzo. Tiene una comprensin holstica e intuitiva de las situaciones,
sin necesidad de descomponer el problema antes de encontrar una solucin.

Descomposicin de la situacin: internalizada, intuitiva.


Comportamiento en la toma de decisiones: centrada en la eleccin que mejor permite conseguir el
plan intuitivo.
Cmo se ejercita el juicio: se mueve rpidamente a partir de la experiencia anterior.
Habilidades y herramientas: intuitivas y se aplican conscientemente en todos los escenarios.
Tiempo de entrenamiento formal: cuanto haga falta para abordar los asuntos especficos que surjan.
Entrenamiento con prctica: 1 a 3 aos.

1.1.5 Experto
La percepcin y la accin estn completamente internalizadas en procesos de trabajo normales. Cuando las
cosas se desarrollan normalmente, el trabajo parece rutina. Alcanzar este nivel requiere una relacin
cercana con otro experto del que se obtiene ms aprendizaje va exposicin, observacin, conversacin y
otras interacciones continuadas.

Descomposicin de la situacin: internalizada, intuitiva.


Comportamiento en la toma de decisiones: acta inconscientemente, de modo automtico.
Cmo se ejercita el juicio: inconscientemente hace lo que generalmente funciona.
Habilidades y herramientas: intuitivas y se aplican conscientemente en todos los escenarios.
Tiempo de entrenamiento formal: informal, va interaccin con otros expertos.
Entrenamiento con prctica: 5 a 10 aos.

1.1.6 El nivel esperado de un curso de formacin


Conocidos los niveles y sus caractersticas, hemos de saber que un curso de formacin como el que nos
ocupa no podr llevarnos mucho ms all del nivel de principiante avanzado o quiz rozar el de
competente. Y eso con mucha dedicacin.
Vale la pena observar que en los dos niveles superiores no hay entrenamiento formal: slo aprendizaje
basado en la prctica y el tiempo. Ese es un factor en el que hemos de centrarnos inevitablemente: el
tiempo.

1.2 La regla de las 10.000 horas


Ahora que conocemos los cinco niveles, puede asustar el tiempo que requiere alcanzar el nivel de experto.
No hay atajos? No hay gente con un talento nato capaz de llegar a ese estadio ms rpidamente?

Andrs Marzal

Buenas prcticas en desarrollo de software con .NET

El estatus de genio est mitificado. Detrs del genio suele haber una gran dedicacin al estudio y mucha,
mucha prctica. En su libro Outliers, Malcolm Gladwell seala en un artculo que el nivel de experto requiere
unas 10.000 horas de trabajo en no importa qu mbito.

El artculo divulga resultados de un estudio de K. Anders Ericsson en la Academia de Msica de Berln.


Dividieron a los violinistas en tres grupos:

en el primero, las estrellas, quienes podan llegar a ser solistas de nivel;


en el segundo, los considerados buenos;
en el tercero, los que pareca improbable que llegaran a profesionales.

A todos ellos se les pregunt por el nmero de horas que haban dedicado a practicar desde el primer da
que tocaron un violn. Todos haban empezado sobre los cinco aos de edad practicando de dos a tres horas
semanales. Las diferencias empezaron a manifestarse a la edad de ocho aos. Los del mejor grupo dedicaban
unas seis horas semanales con nueve aos, ocho horas a los doce, diecisis horas a los catorce, y as hasta las
ms de treinta horas a los veinte aos. A esa edad sumaban unas 10.000 horas de prctica. Cuando
estudiaron a grupos de pianistas emergi el mismo patrn. Lo que no encontraron fue genios natos, es
decir, gente que tuviera un nivel elevado sin practicar con esa dedicacin. Tampoco encontraron intiles
natos, es decir, gente que dedicando ese tiempo, no llegara a un nivel alto.
En palabras de Daniel Levitin, neurlogo:
El cuadro emergente de estos estudios es que se necesitan diez mil horas de prctica para alcanzar
el nivel de maestra asociado a un experto de nivel mundial. Estudio tras estudio, de compositores,
jugadores de bisbol, escritores de ficcin, patinadores sobre hielo, pianistas de concierto, jugadores
de ajedrez, grandes delincuentes ese nmero surge una y otra vez. Por supuesto, esto no resuelve la
cuestin de por qu algunas personas obtienen ms provecho de sus sesiones de prcticas que otras.
Pero nadie ha encontrado an un caso en el que el nivel de experto a escala mundial se alcanzara en
menos tiempo. Parece que el cerebro necesita este tiempo para asimilar todo lo necesario para
alcanzar la maestra.
Gladwell cita Numerosos ejemplos en los que aparece la regla de las 10.000 horas y que en el imaginario
popular se representan como casos de genio natural: Wolfgang Amadeus Mozart, Bobby Fisher, Bill Joy, The
Beatles, Bill Gates El artculo tambin trata otras cuestiones, como la existencia de ventanas de
oportunidad que parecen favorecer a personas de ciertas generaciones cuando un cambio tecnolgico
radical aparece. Pero esa es otra historia.

Andrs Marzal

Buenas prcticas en desarrollo de software con .NET

Quedmonos con que un ao laboral supone unas 1.750 horas de trabajo real (suponiendo que la jornada se
aprovecha bien). Eso hace que se requiera un mnimo de cinco aos y ocho meses para alcanzar el nivel de
experto. La buena noticia es que resulta posible alcanzar el nivel de experto: slo se necesita suficiente
dedicacin.
1.2.1 Aprenda a programar en 10 aos
Peter Norvig, director de investigacin en Google, colg en su web (http://norvig.com/) un ensayo titulado
Teach yourself to program in 10 years, donde abunda en esta misma idea y critica las decenas de libros
tipo Aprenda [ponga aqu su lenguaje de programacin] en [ponga aqu 24 horas/3 das/7 das].

En el ensayo se citan trabajos de Benjamin Bloom, John R. Hayes o William G. Chase y Herbert S. Simon (y el
artculo de Malcolm Gladwell) que coinciden en la idea de que se necesita un perodo de unos 10 aos (o
10.000 horas) para desarrollar un nivel de experto en cualquier campo.
La clave, segn remarca Norvig, es la prctica deliberada: no slo hacer algo una y otra vez, sino desafiarse
con tareas que estn ms all de lo que uno sabe en un momento dado, tratar de abordarlas, analizar la
propia capacidad cuando se realizan y corregir los errores cometidos.
Algunos de los consejos que da Norvig son:

Consiga que le interese la programacin y practique porque es divertida. Y asegrese de que sigue
sindolo lo bastante como para que desee dedicarle diez aos.
Hable con otros programadores, lea programas de otros. Esto ms importante que cualquier libro o
curso de entrenamiento.
Programe. El mejor modo de aprender es aprender haciendo.
Si quiere, dedique cuatro aos a la formacin universitaria de grado (o ms con el postgrado). Esto le
dar acceso a trabajos que requieren credenciales y le proporcionar una comprensin ms
profunda del campo. Pero si no le gusta la escuela, puede (con cierta dedicacin) conseguir una
experiencia similar en el trabajo. En cualquier caso, el aprendizaje con libros no ser suficiente.
Computer science education cannot make anybody an expert programmer any more tan
studying brushes and pigment can make somebody an expert painter.
Eric Raymond.
Trabaje en proyectos con otros programadores. Sea el mejor programador en algunos proyectos y el
peor en otros.

Andrs Marzal

Buenas prcticas en desarrollo de software con .NET

Trabaje en proyectos despus de que lo hayan hecho otros programadores. Implquese en


comprender un programa escrito por otro, en repararlo cuando los programadores originales no
estn accesibles. Piense en cmo disear programas para que sean ms fciles de mantener por
quienes tendrn que hacerlo despus de usted.
Aprenda al menos una docena de lenguajes de programacin. Incluya uno que soporte abstracciones
de clases (como C++ o Java), uno que soporte abstraccin funcional (como Lisp o ML), uno que
soporte abstraccin sintctica (como Lisp), uno que soporte especificaciones declarativas (como
Prolog o plantillas de C++), uno que soporte corrutinas (como Icon o Scheme) y uno que soporte
paralelismo (como Sisal).
Recuerde que hay computadores en la ciencia de computadores. Ha de saber cunto cuesta
ejecutar una instruccin, acceder a una palabra de memoria (con o sin falta de cach), leer palabras
consecutivas de disco y desplazarse a un nuevo lugar del disco.

Una nota respecto del aprendizaje de varios lenguajes de programacin: el acento no se pone tanto en los
diversos lenguajes por sus diferencias sintcticas (que tambin son interesantes) como por sus diferentes
paradigmas a la hora de abordar la programacin (orientacin a objetos, programacin funcional,
programacin declarativa, programacin concurrente). Si quiere un estudio de algunos lenguajes bajo este
prisma, puede consultar el libro Seven Languages in Seven Weeks: A Pragmatic Guide to Learning
Programming Languages, de Bruce A. Tate.

Se exploran lenguajes de programacin no tanto para cambiar de entorno de trabajo gratuitamente como
para ver qu cosas son particularmente fciles de expresar en otros lenguajes y cmo podramos obtener
algo similar en los que usamos habitualmente. Si no estudia algunos lenguajes funcionales, el programador
de Java, por poner un ejemplo, se perder una coleccin de tcnicas que, ms tarde o ms temprano
irrumpirn en su lenguaje de programacin (y que ya irrumpieron hace algunos aos en C#). No estar
preparado ahora supone llegar tarde a un cambio inminente. Pero no slo eso: si conoce los fundamentos de
la programacin funcional, por ejemplo, podr aplicar tcnicas que permiten implementan o emulan en su
lenguaje habitual algunos de sus aspectos ms interesantes. Y si no lo hace, tendr problemas para entender
algunas libreras que ya anticipan estas posibilidades o el cdigo de otros programadores que estn tratando
de seguir ese camino.
1.2.2 Coding Dojo
Un buen programador debe ejercitar su oficio y plantearse retos. La rutina del trabajo puede encasillar al
programador, que raramente saldr de un lenguaje de programacin o dos y que acabar escribiendo cdigo

Andrs Marzal

Buenas prcticas en desarrollo de software con .NET

clonado de un limitado repertorio de programas tipo. Los mejores programadores exploran constantemente
otros lenguajes de programacin y tcnicas empleadas por otros programadores.
Un modo de ejercitarse es acudir a Coding Dojos, esto es, encuentros de programadores en el que se
practican katas. Las katas toman su nombre de los ejercicios coreografiados de las artes marciales. Es
recomendable la lectura de http://www.codinghorror.com/blog/2008/06/the-ultimate-code-kata.html para
entender qu es una kata en programacin. Por otra parte, la pgina http://www.codekata.com/ es una
fuente de recursos para katas que mantiene Dave Thomas (un nombre que aparecer ms adelante).
Tambin es recomendable consultar los ejemplos de Coding Dojo del wiki http://www.codingdojo.org/.

Es bueno entrar en contacto con algn grupo local que organice Coding Dojos o montar uno propio. Un
punto de entrada en Castelln es la gente que organiza http://decharlas.com . Decharlas es una serie
peridica de conferencias y charlas sobre desarrollo de software que se celebran usualmente en la
Universitat Jaume I. Viene gente de toda Espaa y en torno a Decharlas se est generando comunidad. Est
atento.

2 Metodologas y tecnologas
Puestas las cosas en su contexto, centrmonos en los objetivos del curso, que no son otros que exponerle a
un conjunto de buenas prcticas en desarrollo de software. Distingamos entre:

Metodologas.
Formas de organizar y abordar el desarrollo de software que son independientes del lenguaje o
plataforma de software.
Tecnologas.
Conjuntos de herramientas y tcnicas que permiten implementar ciertas estrategias o principios de
desarrollo de software con lenguajes de programacin o plataformas concretos.

Andrs Marzal

Buenas prcticas en desarrollo de software con .NET

2.1 Metodologas pesadas y metodologas ligeras


A finales de los 60 se acu el trmino crisis del software para referirse al problema no resuelto de cmo
escribir programas correctos, comprensibles y verificables. La crisis se manifest en2:

Proyectos que desbordaban el presupuesto.


Proyectos que no cumplan los plazos.
Programas ineficientes.
Programas de mala calidad.
Programas que no cumplan con los requerimientos.
Programas inmanejables y cdigo difcil de mantener.
Programas que jams se entregaban.

La primera respuesta fue sistematizar el proceso de construccin de software estableciendo analogas con la
ingeniera clsica. El conjunto de metodologas resultante estaba orientado a proyectos, como lo estn las
prcticas propias de la ingeniera:

Se estudian los requerimientos del proyecto que se detallan en un documento.


Se disea un proyecto detallado, con planos, donde todo puede ser cuantificado y estimado para
ser ejecutado con un presupuesto y en un plazo determinado. El resultado de esta fase es un bloque
de documentacin.
Se ejecuta el proyecto de acuerdo con el plan. El resultado de esta fase es un producto software
completo y funcional.
Se verifica que todo se ha desarrollado conforme a lo proyectado y que el cdigo funciona
correctamente.
Se pasa a una fase de mantenimiento en la que se detectan y corrigen errores, se aade
funcionalidad, etc.

Esta metodologa se conoce por modelo en cascada o Big Design Up Front (BDUF). En la prctica,
raramente conduce al xito si se sigue escrupulosamente. Lo habitual es dedicar mucho tiempo a las dos
primeras fases para descubrir, tras empezar la ejecucin del proyecto, que no se haban tenido en cuenta
Numerosos detalles, lo que obliga a reconsiderar el proyecto en s.

De http://en.wikipedia.org/wiki/Software_crisis.

Andrs Marzal

Buenas prcticas en desarrollo de software con .NET

El proceso en cascada es poco realista y se suele acabar entrando en una dinmica de bucle para corregir los
errores de diseo inevitables: al proyecto sigue un conato de implementacin que obliga a retocar el
proyecto para descubrir, al tratar de ejecutar nuevamente, que es necesario hacer ms ajustes El
resultado: muchos proyectos fracasados, fuera de plazo o fuera de presupuesto, equipos de desarrolladores
frustrados Crisis.
La bsqueda de una analoga en el proceso de desarrollo de software con el propio de diseo y ejecucin de
proyectos en las ingenieras convencionales parece abocada al fracaso. Copiar lo que funciona en las
ingenieras clsicas no es garanta de xito; ms bien al contrario. La ingeniera trata, generalmente, con
procesos de fabricacin predecibles. El desarrollo de software guarda ms relacin con el desarrollo de
productos nuevos y es un proceso emprico. Esta tabla, adaptada de la que aparece en Becoming Agile in
an imperfect world, de Greg Smith y Ahmed Sisky, recoge las diferencias entre ambos tipos de proceso:
Fabricacin predecible (procesos definidos)

Desarrollo de nuevos productos (procesos empricos)

Es posible completar primero las especificaciones y


construir despus.

Raramente se pueden crear especificaciones


detalladas e inalterables en una primera fase.

Al principio se puede estimar fiablemente tiempo y


coste.

Al principio resulta imposible estimar fiablemente


tiempo y esfuerzo. A medida que se dispone de
datos empricos, se hace ms y ms posible
planificar y estimar.

Es posible identificar, definir, planificar y ordenar


todas las actividades detalladas al principio del
proyecto.

Al principio es imposible identificar, definir,


planificar y ordenar actividades. Se requieren pasos
adaptativos guiados por ciclos de construccin con
realimentacin.

La adaptacin al cambio impredecible no es la


norma, y la tasa de cambios es relativamente baja.

La adaptacin creativa al cambio impredecible es la


norma. La tasa de cambios es alta.

Andrs Marzal

Buenas prcticas en desarrollo de software con .NET

Si se busca una analoga ms eficaz que la consabida entre desarrolladores e ingenieros, es recomendable
leer el trabajo Hackers and Painters, de Paul Graham, autor de la primera aplicacin web y fundador de Y
Combinator (http://ycombinator.com/).

En los 90 surgi un movimiento de contestacin a estas metodologas. El nuevo movimiento denomin a las
metodologas basadas en proyectos metodologas pesadas (heavyweight), por contraposicin a la etiqueta
de las metodologas que se proponan entonces y que se denominaron metodologas ligeras (lightweight)
o metodologas giles.

2.2 Metodologas giles


En el curso abordaremos algunas metodologas o cuestiones metodolgicas bajo la influencia de las
denominadas metodologas giles y algunas de las tecnologas que les dan soporte. El manifiesto gil es
obra de 17 programadores que se reunieron en Snowbird, Utah, en 2001 para hablar sobre una tendencia en
desarrollo de software que se empezaba a conocer por procesos ligeros. La ingeniera del software,
especialmente en su visin academicista centrada en los proyectos, haba llevado a la industria a un impasse
que pareca superarse con esa nueva corriente de metodologas.
El manifiesto gil, que es el documento fundacional del movimiento, nace de la experiencia real de
programadores expertos, lejos de la Academia. No hacen ms que recoger las prcticas que les funcionan
demostradamente y adoptan una postura poco dogmtica respecto a su propia metodologa: cada equipo
debe adoptar los principios y prcticas que les funcionen, y no ms ni menos.

10

Andrs Marzal

Buenas prcticas en desarrollo de software con .NET

2.3 El manifiesto gil


El manifiesto gil reza as:
The Manifesto for Agile Software Development
We are uncovering better ways of developing software by doing it and helping others do it.
Through this work we have come to value:

Individuals and interactions over processes and tools


Working software over comprehensive documentation
Customer collaboration over contract negotiation
Responding to change over following a plan

That is, while there is value in the items on the right, we value the items on the left more.
http://agilemanifesto.org/
Los 17 autores son expertos en desarrollo de software y muchos de ellos son referencia, con millares de
seguidores de sus libros, ensayos, blogs, etc.:

Kent Beck, creador de las metodologas Extreme Programming (XP) y Test Driven Development
(TDD). Cre la librera JUnit en colaboracin con Erich Gamma (lder en el diseo de Eclipse).
Populariz las tarjetas Class Responsibility Collaboration (CRC) con Ward Cunningham. Es autor de
Extreme Programming Explained. Blog en http://www.threeriversinstitute.org/blog/.
Alistair Cockburn, inventor de la escala Cockburn para la categorizacin de proyectos de software e
impulsor de la Declaracin de Independencia PM o Declaracin de Independencia para la Gestin
Moderna, que propugna la aplicabilidad de las metodologas giles en otros entornos de gestin.
Blog en http://alistair.cockburn.us/Blog.
Ward Cunningham, inventor y desarrollador del primer wiki (en 1994 ide WikiWikiWeb) y pionero
en el uso de patrones de diseo y Extreme Programming. Trabaj con Kent Beck en las tarjetas CRC.
Creador e impulsor de los test de integracin con Fit, el Framework for Integrated Test. Blog en
http://dorkbotpdx.org/blog/wardcunningham.
Martin Fowler, autor de varios libros influyentes sobre desarrollo de software y responsable
cientfico en ThoughtWorks. Mantiene un blog de referencia. Populariz el trmino Inyeccin de
Dependencias como un modo de Inversin de Control. Bliki (blog+wiki) en
http://martinfowler.com/bliki/index.html.
Andrew Hunt, escritor de libros de desarrollo de software, en particular de The Pragmatic
Programmer. Con Dave Thomas cre la serie de libros Pragmatic Bookshelf para desarrolladores
de software. Blog en http://blog.toolshed.com/.
Ron Jeffries, fundador de la metodologa Extreme Programming, con Kent Beck y Ward Cunningham.
Es autor del segundo libro sobre Extreme Programming: Extreme Programming Installed. Blog en
http://www.xprogramming.com/blog/.
Mike Beedle, uno de los primeros adoptantes de Scrum. Coautor de Scrum, Agile Software
Development.
Robert C. Martin, conocido tambin como Uncle Bob, fundador de Object Mentor y editor jefe de
The C++ Report entre 1996 y 1999. Autor de Agile Software Development: Principles, Patterns, and

11

Andrs Marzal

Buenas prcticas en desarrollo de software con .NET

Practices y de Clean Code: A Handbook of Agile Software Craftsmanship. Blog en


http://blog.objectmentor.com/ y Twitter en http://twitter.com/unclebobmartin.
Arie van Bennekum, implicado en la metodologa DSDM y miembro de DSDM Consortium desde
1997. Twitter http://twitter.com/arievanbennekum.
Dave Thomas, coautor de The Pragmatic Programmer. Blog en http://pragdave.pragprog.com/.
Jim Highsmith, principal desarrollador de la metodologa gil Adaptive Software Development. Blog
en http://blog.cutter.com/author/jimhighsmith/.
Jon Kern, impulsor de la metodologa gil Feature-Driven Development. Blog en
http://technicaldebt.com/.
Brian Marick, impulsor y representante de la comunidad de software testing. Blog en
http://www.exampler.com/blog/.
James Grenning, fundador de Renaissance Software y usuario de metodologas giles avant-la-lettre.
Autor del artculo Planning Poker or How to avoid analysis paralysis while release planning. Blog en
http://www.renaissancesoftware.net/blog/.
Steve Mellor, coinventor del mtodo Shlaer-Mellor para el desarrollo de sistemas orientados a
objetos.
Ken Schwaber, presidente de Advanced Development Methods. Trabaj con Jeff Sutherland en la
definicin de Scrum y es uno de sus formalizadores. Coautor de Scrum, Agile Software
Development. Blog en http://kenschwaber.wordpress.com/.
Jeff Sutherland, CIO de una startup del MIT, PatientKeeper. Uno de los inventores de Scrum. Blog en
http://scrum.jeffsutherland.com/.

Kent Beck

Steve Mellor

Alistair Cockburn

Ward Cunningham

Martin Fowler

12

Andrs Marzal

Buenas prcticas en desarrollo de software con .NET

Andy Hunt

Ron Jeffries

Mike Beedle

Robert C. Martin (Uncle Bob)

Arie van Bennekum

Dave Thomas

Jim Highsmith

Jon Kern

Brian Marick

James Grenning

Ken Schwaber

Jeff Sutherland

2.4 Los principios de la agilidad


Segn Venkat Subramanian y Andy Hunt (en Practices of an agile developer working in the real world),
agile development uses feedback to make constant adjustments in a highly collaborative environment.

La agilidad pone el acento en la entrega de valor. Propone la realizacin y entrega incremental de software
con la participacin directa del usuario. Como dice James O. Coplien en la introduccin de Clean Code. A
Handbook of Agile Software Craftmanship, el libro de Robert C. Martin:
In these days of Scrum and Agile, the focus is on quickly bringing product to market. We want
the factory running at top speed to produce software. These are human factories: thinking, feeling
coders who are working from a product backlog or user story to create product.

13

Andrs Marzal

Buenas prcticas en desarrollo de software con .NET

Los principios tras el manifiesto gil son:

Nuestra mayor prioridad es dar satisfaccin al cliente mediante la entrega temprana y continua de
software con valor.
Damos la bienvenida a los requerimientos cambiantes, incluso tardos en el desarrollo. Los procesos
giles abrazan el cambio en favor de dar una ventaja competitiva al cliente.
Entregue software que funciona frecuentemente, en plazos de un par de semanas a un par de
meses, con preferencia por la escala temporal ms corta.
Los responsables de negocio y los desarrolladores deben trabajar a diario en el proyecto.
Construya proyectos con individuos motivados. Deles el entorno y apoyo que necesitan, y confe en
que conseguirn hacer el trabajo.
El mtodo ms eficiente y efectivo para comunicar informacin hacia los desarrolladores y entre los
desarrolladores de un equipo es la conversacin cara a cara.
El software que funciona es la medida principal de progreso.
Los procesos giles promocionan el desarrollo sostenible. Los patrocinadores, desarrolladores y
usuarios deben ser capaces de mantener un ritmo constante indefinidamente.
La atencin continua a la excelencia tcnica y el buen diseo mejoran la agilidad.
La simplicidad, el arte de maximizar la cantidad de trabajo que no se hace, es esencial.
Las mejores arquitecturas, requerimientos y diseos emergen de equipos que se auto-organizan.
A intervalos regulares, el equipo reflexiona sobre cmo llegar a ser ms efectivo; entonces ajusta su
comportamiento adecuadamente.

2.5 Conjuntos de prcticas con nombre propio


Los dos conjuntos de prcticas con nombre propio y que se identifican con las metodologas giles son
Programacin Extrema y Scrum/Kanban. Hay otras: Lean, FDD, AUP, Crystal y DSDM.
La programacin extrema (que probablemente se traduzca mejor por programacin de riesgo) data de
1996 y se basa en un conjunto de prcticas combinadas.

14

Andrs Marzal

Buenas prcticas en desarrollo de software con .NET

Entre las ms llamativas se encuentra la programacin en pareja o el desarrollo guiado por las pruebas.

Scrum pone el acento en la gestin del proyecto y propone un proceso de recogida de historias de usuario,
cuantificacin del esfuerzo de cada tarea, planificacin de sprints (tandas de trabajo orientadas a completar
una seleccin de historias de usuario), ejecucin de sprints con supervisin diaria, entrega de producto y
retrospectiva para analizar lo hecho y mejorar continuamente.

15

Andrs Marzal

Buenas prcticas en desarrollo de software con .NET

Kanban, que es una tcnica/metodologa independiente que puede enriquecer a Scrum, plantea un sistema
de visualizacin de la carga de trabajo y control del flujo para evitar cuellos de botella y trabajadores ociosos
en una etapa del proceso a la espera de que el cuello de botella genere nueva carga de trabajo.

Lo esencial es que la agilidad se basa en los ciclos cortos, la realimentacin continua y la adaptacin al
equipo de desarrolladores. En su ncleo hay un conjunto de herramientas que facilita el trabajo con las
metodologas giles:

16

Andrs Marzal

Buenas prcticas en desarrollo de software con .NET

3 Principios de diseo de software


Hay una serie de principios de diseo que son muy citados en la comunidad del desarrollo gil. Son principios
alineados con una tcnica de produccin del software que aboga por la simplicidad, la legibilidad y la
mantenibilidad del cdigo.
El trabajo de Robert C. Martin (Uncle Bob) titulado Design Principles and Design Patterns merece una
lectura para tener un cuadro general de los principios de diseo de software que pretendemos seguir al
construir software.

En este texto slo vamos a enunciar algunos de los principales principios de diseo (muchos de ellos
recogidos en el trabajo citado):

3.1 Open Closed Principle (OCP)


Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.

Es decir, deberamos poder modificar el comportamiento de las entidades sin modificar su cdigo fuente. En
principio se asumi que la herencia era un buen modo de redefinir comportamientos (popularizado por

17

Andrs Marzal

Buenas prcticas en desarrollo de software con .NET

Bertrand Meyer en su libro de 1988 Object Oriented Software Construction). Hoy se prefiere el diseo
orientado a interfaces.

3.2 Liskov Substitution Principle (LSP)


Subclasses should be substitutable for their base classes.
Si un objeto es de tipo S y S es un subtipo de T, los objetos de tipo T deberan poder sustituirse por objetos
de tipo S sin que se alteren las propiedades deseables en un programa (correccin, tarea realizada, etc.). El
principio de Liskov tambin se conoce por subtipado comportamental (fuerte) ((strong) behavioral
subtyping) y fue introducido por Barbara Liskov en una ponencia de 1987 titulada Data abstraction and
hierarchy.

3.3 Dependency Inversion Principle (DIP)


Depend upon Abstractions. Do not depend upon concretion.

Este principio preconiza el desacoplamiento all donde se encuentren relaciones de dependencia de mdulos
de alto nivel con respecto de mdulos de bajo nivel. El principio prescribe:

Que los mdulos de alto nivel no dependan de los de bajo nivel, lo que puede hacerse consiguiendo
que ambos dependan de abstracciones.
Que las abstracciones no dependan de concreciones, sino que las concreciones dependan de
abstracciones.

18

Andrs Marzal

Buenas prcticas en desarrollo de software con .NET

El principio fue postulado por Robert C. Martin (Uncle Bob) y aparece en el trabajo OO Design Quality
Metrics. An Analysis of Dependencies, de 1995.

3.4 Interface Segregation Principle (ISP)


Many client specific interfaces are better than one general purpose interface.

Si una interfaz es demasiado grande, conviene fraccionarla en varias interfaces ms pequeas y especficas
para que los clientes slo dependan de aquello que realmente usan. El principio fue formulado por Robert C.
Martin.

3.5 Single Responsibility Principle (SRP)


An object should have only one responsibility.

El principio afirma que toda clase debe tener una sola responsabilidad y que sta debera estar encapsulada
en la clase. Todos los servicios que ofrece la clase deben estar estrechamente alineados con esa
responsabilidad. El principio fue introducido por Robert C. Martin como parte del principio de cohesin en
un artculo que formaba parte de Principles of Object Oriented Design y se populariz en su libro Agile
Software Development, Principles, Patterns, and Practices.

19

Andrs Marzal

Buenas prcticas en desarrollo de software con .NET

3.6 Release/Reuse Equivalency Principle (REP)


The granule of reuse is the granule of release. This granule is the package. Only components that are released through a
tracking system can be effectively reused.

El cdigo no se debe reutilizar por copia-y-pega, pues no se obtiene beneficio alguno si se modifica el cdigo
original. El cdigo se debe reutilizar haciendo uso de libreras publicadas (released). El autor de la librera es
el responsable de modificarla e, idealmente, el usuario no debe ver el cdigo fuente. La publicacin de
libreras obliga a identificar cada versin con nmeros o nombres, lo que obliga a usar algn sistema de
control de versiones. En principio es posible usar una clase como unidad de publicacin, pero una aplicacin
tiene tantas que sera difcil controlar el elevado nmero de versiones de estas unidades de grano fino. Se
requiere una entidad de mayor tamao, como el paquete (grupo de clases). Este problema se conoce por
problema de la granuralidad.

3.7 Common Closure Principle (CCP)


Classes that change together, belong together.

Entendemos por clausura el conjunto de elementos que se ven afectados y necesitan algn cambio cuando
hay un cambio en otra clase. Deberamos considerar paquete a todas las clases que presentan esta
sensibilidad al cambio de uno de sus elementos. No siempre es posible disear paquetes que respecten este
principio.

3.8 Common Reuse Principle (CRP)


Classes that arent reused together should not be grouped together.

Es un principio que ayuda a disear paquetes, esto es, unidades de publicacin. El principio exige que slo
agrupemos clases muy cohesivas. Una derivada del principio es que todas las clases que ofrecen o cooperan
en proporcionar una determinada funcionalidad deben agruparse en un paquete.

3.9 Acyclic Dependencies Principle (ADP)


The dependencies between packages must not form cycles.

El grafo de dependencias entre paquetes debe formar un Grado Dirigido Acclico. Si no es as, el despliegue
de paquetes es un infierno. Este diagrama ilustra un grafo de dependencias con ciclos, muy habitual cuando
se usan libreras GUI (extrado de http://www.objectmentor.com/resources/articles/granularity.pdf):

20

Andrs Marzal

Buenas prcticas en desarrollo de software con .NET

Las dependencias cclicas se pueden romper, por ejemplo, con DIP. Este diagrama, del mismo artculo, ilustra
cmo romper la dependencia cclica invirtiendo la dependencia:

3.10 Stable Dependencies Principle (SDP)


Depend in the direction of stability. A package should only depend upon packages that are more stable than it is.

Hace falta presentar unas cuantas definiciones:

La fragilidad es la tendencia de un programa a romperse en varios lugares cuando se introduce un


solo cambio. El diseo de dependencias es crtico: las interdependencias contribuyen a la fragilidad.
Slo debe haber dependencias con el cdigo no-voltil, es decir, que es imposible (o muy
improbable) que vaya a sufrir cambios.
La volatilidad depende de varios factores, pero uno de los que influyen ms directamente y es fcil
de medir es la estabilidad, que es una medida de lo difcil que es cambiar un mdulo. Cuanto ms
difcil de cambiar, menos voltil.
Una clase es independiente cuando no depende de nada y la independencia es un indicador de
estabilidad: los cambios en alguien que depende de una clase independiente no pueden afectarle.
Una clase de la que dependen muchas otras es una clase responsable. Hay una gran resistencia a
que una clase responsable cambie, pues un cambio comportara nuevos cambios que se propagan a
muchas otras clases. Las clases ms estables son las que son independientes y responsables.

21

Andrs Marzal

Buenas prcticas en desarrollo de software con .NET

Hay una mtrica de estabilidad posicional que depende del nmero de dependencias que entran y
salen de un paquete:

, Acoplamientos aferentes (afferent couplings): nmero de clases de fuera del paquete


que depende de las clases de dentro del paquete.

, Acoplamiento eferentes (efferent couplings): nmero de clases dentro del paquete que
dependen de clases de fuera del paquete.

, Inestabilidad (instability):
. Es un valor entre 0 y 1. El valor 0 indica la
mxima estabilidad (paquete independiente y responsable) y el valor 1, la mxima
inestabilidad (paquete dependiente e irresponsable).
Este ejemplo, extrado de http://www.objectmentor.com/resources/articles/stability.pdf, ilustra un
clculo de estabilidad posicional:

El principio SDP dice que un paquete slo debe depender de paquetes con I menor que el suyo. El valor de la
inestabilidad debe decrecer en la direccin de las dependencias (o, dicho del revs: el valor de la estabilidad
debe aumentar en la direccin de las dependencias).

3.11 Stable Abstractions Principle (SAP)


Stable packages should be abstract packages.

Una mtrica adicional es la abstraccin de un paquete:

, abstraccin (abstractness), se define como en nmero de clases abstractas partido por el del
total de clases. Un valor de 0 indica que un paquete no tienen ninguna clase abstracta y un valor
de 1 indica que slo tiene clases abstractas.

Los buenos paquetes se alojan en esta diagonal de una grfica de abstraccin contra (in)estabilidad, en la
que hay unas zonas de exclusin:

22

Andrs Marzal

Buenas prcticas en desarrollo de software con .NET

Una ltima mtrica permite conocer la distancia de un paquete a la zona sana:

, Distancia. Se define como


para estar entre 0 y 1:

. En un valor entre 0 y 0.707, que se puede normalizar

La herramienta NDepend (http://www.ndepend.com/) permite obtener estas y otras mtricas


automticamente.

3.12 Law of Demeter for Functions/Methods (LoD-F/M) o Least Knowledge


Principle (LKP)
Each unit should have only limited knowledge about other units: only units closely related to the current unit. Each
unit should only talk to its friends; dont talk to strangers.

En la programacin orientada a objetos, se entiende que unidad es cada mtodo individual y que las
unidades relacionadas estrechamente son

los mtodos del propio objeto,


los argumentos pasados al mtodo,
los objetos que instancia directamente en el propio mtodo,
los mtodos de partes de la clase que son accesibles directamente o variables globales visibles en el
mbito de M.

Un objeto A puede solicitar un servicio de un objeto B, pero A no debe acceder a un objeto C a travs de B
para solicitar sus servicios. Si lo hiciera, sera porque conoce demasiado sobre la estructura interna de B. O
bien B aumenta sus servicios para ofrecer directamente los que ofrece B, o bien A accede directamente a C.

3.13 Dont Repeat Yourself (DRY) o Duplication is Evil (DE)


Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.

Fue formulado por Andy Hunt y Dave Thomas en el libro The Pragmatic Programmer. Es un principio
elemental y que ya ha aparecido sugerido en el principio Release/Reuse Equivalency Principle (REP).

23

Andrs Marzal

Buenas prcticas en desarrollo de software con .NET

3.14 You Aint Gonna Need It (YAGNI)


Always implement things when you actually need them, never when you just foresee that you need them.
Un problema corriente en el cdigo es la sobreingeniera, es decir, la construccin de (mucha) ms funcionalidad de
la estrictamente necesaria. Esto dificulta la lectura del cdigo y su mantenimiento sin aportar valor.

3.15 Keep It Simple, Stupid! (KISS)


La complejidad innecesaria debe evitarse.

3.16 Los principios SOLID


Oir hablar frecuentemente de los principios SOLID, que no son ms que una seleccin de algunos de los que
hemos citado. Los cinco principios SOLID son:

Single Responsibility Principle

Open Closed Principle

Liskov Substitution Principle

Interface Segregation Principle

Dependency Inversion Principle

24

Andrs Marzal

Buenas prcticas en desarrollo de software con .NET

3.17 Crditos y recursos de esta seccin

El trabajo de los hermanos Dreyfus sobre los cinco niveles en el aprendizaje de habilidades se puede
descargar de http://www.dtic.mil/cgibin/GetTRDoc?Location=U2&doc=GetTRDoc.pdf&AD=ADA084551.
El libro Outliers, de Malcolm Gladwell, est a la venta en http://www.amazon.com/Outliers-StorySuccess-Malcolm-Gladwell/dp/0316017922.
El ensayo Teach yourself to program in 10 years, de Peter Norvig, est disponible en
http://norvig.com/21-days.html.
El ensayo Hackers and Painters, de Paul Graham, est disponible en
http://www.paulgraham.com/hp.html y forma parte del libro del mismo ttulo, que se puede
adquirir en http://oreilly.com/catalog/9780596006624.
El libro Seven Languages in Seven Weeks, de Bruce A. Tate, est a la venta en
http://pragprog.com/titles/btlang/seven-languages-in-seven-weeks.
El libro Becoming Agile est a la venta en http://www.manning.com/smith/.
El libro Practices of an Agile Developer, de Venkat Subramanian y Andy Hunt, est a la venta en
http://pragprog.com/titles/pad/practices-of-an-agile-developer.
El libro Clean Code, de Uncle Bob, est a la venta en http://www.amazon.com/Clean-CodeHandbook-Software-Craftsmanship/dp/0132350882.
El libro Object Oriented Software Construction, de Bertrand Meyer, est a la venta en
http://www.amazon.com/Object-Oriented-Software-Construction-Book-CD-ROM/dp/0136291554.
El paper OO Design Quality Metrics. An Analysis of Dependencies de Uncle Bob est disponible en
http://www.objectmentor.com/resources/articles/oodmetrc.pdf.
El libro Agile Software Development, Principles, Patterns, and Practices, de Uncle Bob, est a la
venta en http://www.amazon.com/Software-Development-Principles-PatternsPractices/dp/0135974445.
El libro The Pragmatic Programmer, de Andy Hunt y Dave Thomas, est a la venta en
http://pragprog.com/the-pragmatic-programmer.
El trabajo Poker Planning, de James W. Grenning, est disponible en
http://www.renaissancesoftware.net/files/articles/PlanningPoker-v1.1.pdf.
Imagen de XP Practices extrada de http://xprogramming.com/images/circles.jpg.
Fotografa de panel Kanban extrada de http://leansoftwareengineering.com/2007/10/27/kanbanbootstrap/.
Imagen Import God; extrada de http://www.facebook.com/group.php?gid=7530335849.
Imagen de Agile Development extrada de Wikipedia:
http://en.wikipedia.org/wiki/File:Agile_Software_Development_methodology.jpg.
El trabajo Design Principles and Design Patterns de Uncle Bob est disponible en
http://www.objectmentor.com/resources/articles/Principles_and_Patterns.pdf.
Los carteles motivacionales que ilustran los principios de desarrollo de software se han extrado de
http://www.doolwind.com/blog/solid-principles-for-game-developers/.

25

Andrs Marzal

Buenas prcticas en desarrollo de software con .NET

4 Patrones de diseo
Christopher Alexander, arquitecto, deseaba cambiar el modo en el que se diseaban los espacios de trabajo
y vivienda. Pensaba que lo ideal es que se diseen y construyan por los propios ocupantes, pues son ellos
quienes mejor conocen los requerimientos.

Dada la complejidad que supone el diseo de una construccin, sera necesario dotarse de un conjunto de
soluciones arquetpicas para los problemas ms frecuentes y usar un lenguaje que facilite la expresin de
los diseos.
La observacin ms relevante es, quiz, que hay ciertos problemas recurrentes en el mundo de la
arquitectura para los que se han descubierto y redescubierto ciertas soluciones que, convenientemente
abstradas de sus detalles ms concretos, podemos denominar patrones o patrones de diseo. Su libro
A Pattern Language: Towns, Buildings, Construction presentaba estos patrones as:
The elements of this language are entities called patterns. Each pattern describes a problem which
occurs over and over again in our environment, and then describes the core of the solution to that
problem, in such a way that you can use this solution a million times over, without ever doing it the
same way twice.
For convenience and clarity, each pattern has the same format. First, there is a picture, which shown
an archetypical example of that pattern. Second, after the picture, each pattern has an introductory
paragraph, which sets the context for the pattern, by explaining how it helps to complete certain
larger patterns. Then there are three diamonds to mark the beginning of the problem. After the
diamonds there is a headline, in bold type. This headline gives the essence of the problem in one or
two sentences. After the headline comes the body of the problem. This is the longest section. It
describes the empirical background of the pattern, the evidence for its validity, the range of different
ways the pattern can be manifested in a building, and so on. Then, again in bold type, like the
headline, is the solutionthe heart of the patternwhich describes the field of physical and social
relationships which are required to solve the stated problem, in the stated context. This solution is
always stated in the form of an instructionso that you know exactly what you need to do, to build
the pattern. Then, after the solution, there is a diagram, which shows the solution in the form of a
diagram, with labels to indicate its main components.
After the diagram, another three diamonds, to show that the main body of the pattern is finished.
And finally, after the diamonds there is a paragraph which ties the pattern to all those smaller
patterns in the language, which are needed to complete this pattern, to embellish it, to fill it out.

26

Andrs Marzal

Buenas prcticas en desarrollo de software con .NET

There are two essential purposes behind this format. First, to present each pattern connected to
other patterns, so that you grasp the collection of all 253 patterns as a whole, as a language, within
which you can create an in finite variety of combinations. Second, to present the problem and
solution of each pattern in such a way that you can judge it for yourself, and modify it, without losing
the essence that is central to it.

Los patrones son esquemas de soluciones para problemas genricos frecuentes que han de adaptarse a los
detalles concretos de cada instancia de dichos problemas. A la hora de presentar cada elemento de su
coleccin de patrones, Alexander segua disciplinadamente una estructura muy homognea.
Una ventaja de esta aproximacin a la presentacin de soluciones para problemas recurrentes es que, en
tanto se define un nombre para cada patrn, se crea una nomenclatura que simplifica la comunicacin entre
los especialistas que la adoptan. No es poca cosa.
El lenguaje de patrones no ofrece un marco conceptual vlido nicamente para la arquitectura: cualquier
ingeniera puede encontrar inspiracin en el trabajo de Alexander para crear un conjunto propio de
soluciones arquetpicas para problemas que se plantean frecuentemente en un campo.

4.1 Patrones de diseo en ingeniera del software


En 1987, Kent Beck y Ward Cunningham presentaron en OOPSLA el trabajo Using Pattern Languages for
Object-Oriented Programs, donde reconocan el trabajo de Alexander y anunciaban el inicio de la escritura
de un lenguaje de patrones para el diseo de software orientado a interfaces de usuario.

En 1994, Ward Cunningham cre un wiki para albergar una coleccin de patrones de diseo editable
fcilmente por la comunidad: el Portland Pattern Repository3 (http://c2.com/ppr/).
En el campo del diseo de software, los programadores encuentran constantemente problemas
esencialmente idnticos y acaban descubriendo soluciones similares para estos. Disponer de un catlogo de
patrones resulta de indudable ayuda. Por una parte, ofrece soluciones independientes de los lenguajes de
programacin especficos para problemas que encontramos en cualquier sistema de software mnimamente
complejo. Por otra, nos dota de un lenguaje comn capaz de eliminar muchas dificultades en la
comunicacin entre equipos de desarrollo. Resulta mucho ms sencillo, por ejemplo, hablar de un Singleton
que de un objeto del que slo hay una instancia y al que se accede a travs de un mtodo o propiedad de
3

Portland Pattern Repository es el primer Wiki (o WikiWikiWeb, como fue bautizado por Cunningham).

27

Andrs Marzal

Buenas prcticas en desarrollo de software con .NET

una clase. Y an hay una ventaja adicional si atendemos a las necesidades de los programadores
principiantes: se les expone a una coleccin de tcnicas basadas en buenos anlisis de los problemas y a los
que se sola llegar casi exclusivamente por el tortuoso camino de la experiencia propia. Finalmente, el diseo
del software se beneficia de que aquellas partes de la estructura del software que corresponden a patrones
sean fcilmente identificables y, por tanto, no se entrometan en la comprensin de arquitecturas complejas.
Si uno detecta, por ejemplo, un Singleton, no tiene que detenerse a entender trabajosamente todos los
elementos que conforman su mecnica, pues estar comprendida de antemano.
Cualquier tratado moderno de software, tanto centrado en el diseo como en las herramientas que ayudan
a su desarrollo, har uso de patrones de diseo. No tener un conocimiento razonable de ellos dificultar
inevitablemente su comprensin.
El trabajo que ayud a divulgar el concepto de patrones de diseo en la comunidad de desarrolladores es
Design Patterns: Elements of Reusable Object-Oriented Software, de E. Gamma, R. Helm, R. Johnson y J.M.
Vlissides, publicado en 1994. El libro se conoce popularmente como el de la banda de los cuatro o GoF,
por Gang of Four, y es uno de los libros con mayor impacto en la comunidad de desarrolladores.

El libro ofrece una reflexin acerca de la programacin orientada a objetos y presenta dos docenas de
patrones de diseo agrupados en tres tipos de patrones:
Creational Patterns

Structural Patterns

Behavioral Patterns

Abstract Factory,

Adapter,

Chain of responsibility,

Builder,

Bridge,

Command,

Factory Method,

Composite,

Interpreter,

Prototype,

Decorator,

Iterator,

Singleton,

Facade,

Mediator,

Multiton.

Flyweight,

Memento,

Proxy.

Observer,
State,

28

Andrs Marzal

Buenas prcticas en desarrollo de software con .NET

Strategy,
Template method,
Visitor.

El libro de la banda de los cuatro sigue una estructuracin de los contenidos de cada patrn inspirada en la
que us Alexander para los patrones en la construccin:

Pattern Name and Classification


Intent
Also Known As
Motivation
Applicability
Structure
Participants
Collaborations
Consequences
Implementation
Sample Code
Known Uses
Related Patterns

El libro de la banda de los cuatro es un tanto rido. En los ltimos 15 aos han aparecido muchos tratados
sobre patrones de diseo y el nmero de patrones de uso comn ha crecido sustancialmente. Podemos
destacar Code Complete (segunda edicin), Holub on Patterns o Head First Design Patterns (este
ltimo es especialmente didctico).

Entre los patrones de diseo que no aparecen en el libro de la banda de los cuatro encontramos:
Creational Patterns

Behavioral Patterns

Concurrency Patterns

29

Andrs Marzal

Buenas prcticas en desarrollo de software con .NET

Lazy Initialization,

Blackboard,

Active Object,

Object pool,

Null object,

Balking,

Resource acquisition is initialization.

Servant,

Messaging,

Specification.

Double-checked locking,
Event-based asynchronous,
Guarded suspension,
Lock,
Monitor object,
Read-write lock.
Scheduler,
Thread pool,
Thread-specific storage.

Nosotros estudiaremos unos pocos patrones. Nuestro objetivo es ofrecer una introduccin al mundo del
diseo basado en patrones y dar a conocer los que aparecern ms tarde cuando estudiemos algunas de las
tcnicas que conforman el objeto del curso: pruebas unitarias, inyeccin de dependencias, etctera.
Hay un buen libro para aprender patrones de diseo con C# 3.0: C#3.0 Design Patterns, de Judith Bishop.

4.2 El patrn de diseo Decorator


Decorator es un patrn de diseo estructural que ofrece una alternativa a la herencia como forma de
extender la funcionalidad de una clase. Y por qu implementar extensiones de funcionalidad sin recurrir a
la herencia, que parece la tcnica propia de la orientacin a objetos? Porque, como veremos, la herencia
conduce a una situacin indeseable cuando queremos formar objetos seleccionando con precisin la
funcionalidad que esperamos de un cierto objeto.

30

Andrs Marzal

Buenas prcticas en desarrollo de software con .NET

El decorador es un patrn de uso comn en interfaces grficas de usuario. Un componente de una librera de
interfaces grficas puede consistir en un lienzo para dibujo o en una caja para editar texto. Si estos
componentes slo muestran una parte del contenido de una superficie grande (una zona del dibujo o unos
apenas unos prrafos de un texto largo), podemos movernos por esta con barras de desplazamiento. Un
componente enriquecido con barras de desplazamiento sigue siendo un componente de la misma
naturaleza, slo que con una funcionalidad aadida. Decimos que las barras de desplazamiento decoran al
componente y que el componente con las barras es un nuevo componente, solo que decorado. Imaginemos
ahora que a un componente simple o a uno con barras de desplazamiento le aadimos un borde negro con
sombra para destacarlo: el resultado seguir siendo un componente, pero al que hemos aadido una nueva
decoracin.
Un componente.

Un componente
decorado con un borde,
que tambin es un
componente.

Un componente
decorado con barras de
desplazamiento, que
tambin es un
componente.

Un componente
(decorado con barras de
desplazamiento)
decorado con un borde,
que tambin es un
componente.

Al disear la librera de componentes podemos entender que un objeto decorado ha de ser miembro de una
clase que especialice a la clase del objeto sin decoracin, es decir, que la herencia es la herramienta con la
que hemos de modelar esta relacin de decoracin, pero veremos que hay una solucin ms elegante.
Aunque el concepto de decorador se visualiza fcilmente en el campo de las GUI, no slo vale para este
campo. Es frecuente que se usen decoradores en libreras de entrada salida y al final veremos cmo la
librera de flujos de entrada/salida (streams) de .NET se ha diseado con este patrn. Antes de entrar a
estudiar un caso concreto como simples espectadores, es mejor que veamos una aplicacin propia del
concepto de decorador en un caso sencillo respondiendo a unas decisiones de diseo que podemos hacer
propias.
4.2.1 Un ejemplo: procesadores de cadenas
Nuestro ejemplo consistir en un conjunto de implementaciones para una interfaz IStringProcessor que
ofrecer, a partir de un mtodo, la capacidad de procesar una cadena y modificar su contenido de acuerdo
con algn propsito particular (es una forma de halar, porque en .NET las cadenas son inmutables). Una
implementacin de IStringProcessor podra, por ejemplo, pasar el texto a maysculas, otra podra
transcribir a texto los nmeros, otra podra sustituir ciertas palabras por sus abreviaturas, otra podra
normalizar el texto en aspectos como asegurar que despus de cada signo de puntuacin hay un espacio,
pero no antes, y an otra podra asegurar que no apareciesen secuencias de ms de un espacio en blanco.
De hecho, implementaremos estos procesadores de texto y veremos cmo podemos combinarlos
flexiblemente gracias al uso del patrn Decorator.
Empezamos por presentar la interfaz IStringProcessor, que se define en el fichero IStringProcessor.cs:

31

Andrs Marzal

Buenas prcticas en desarrollo de software con .NET

namespace StringProcessor
{
public interface IStringProcessor
{
string Process(string input);
}
}

Nuestra primera implementacin es una clase que proporciona una versin todo maysculas del texto:
namespace StringProcessor
{
namespace WithSubclasses
{
public class UpperCaser : IStringProcessor
{
public string Process(string input)
{
return input.ToUpper();
}
}
}
}

Vamos a por la clase que reemplaza las secuencias de dos o ms espacios en blanco por un solo espacio:
using System.Text.RegularExpressions;
namespace StringProcessor
{
namespace WithSubclasses
{
public class WhiteSequenceRemover : IStringProcessor
{
public string Process(string input)
{
return Regex.Replace(input, " +", " ");
}
}
}
}

Y si ahora quisisemos una clase que combinase las dos funcionalidades? Muy fcil: la herencia viene en
nuestra ayuda. Bueno: no tan fcil. En .NET no hay herencia mltiple, as que no podremos combinar las dos
clases para crear una nueva. Aprovechemos, al menos, una de ellas. Eso nos obliga a volver sobre nuestros
pasos y declarar como virtual el mtodo de la clase base. Cmo no habamos previsto esta futura
necesidad?
class UpperCaser : IStringProcessor
{
public virtual string Process(string input)
{
return input.ToUpper();
}
}

Nuestra nueva clase es


namespace StringProcessor
{
namespace WithSubclasses
{

32

Andrs Marzal

Buenas prcticas en desarrollo de software con .NET

public class UpperCaserAndWhiteSpaceRemover : UpperCaser


{
public override string Process(string input)
{
var upperCaseInput = base.Process(input);
return Regex.Replace(upperCaseInput, " +", " ");
}
}
}
}

Y ya empiezan los problemas: hemos duplicado cdigo, lo que va contra el principio DIY (Dont Repeat
Yourself). Si ms adelante modificsemos el mtodo de sustitucin de espacios en blanco porque
descubrisemos, por ejemplo, que la librera Regex presenta problemas de eficiencia, tendramos que hacer
cambios en dos puntos de nuestro programa. Pero, bien, asumamos que no nos queda ms remedio. Hay
algunos problemas adicionales: hemos escondido que UpperCaserAndWhiteSpaceRemover implementa la
interfaz IStringProcessor. Este segundo problema slo afecta a la legibilidad del cdigo y es fcilmente
subsanable:
class UpperCaserAndWhiteSpaceRemover : UpperCaser, IStringProcessor
...

Nos interesa ahora implementar el procesador que sustituye cada digito por su texto.
namespace StringProcessor
{
namespace WithSubclasses
{
public class DigitRemover: IStringProcessor
{
public virtual string Process(string input)
{
return input.Replace("0", "cero ")
.Replace("1", "uno ")
.Replace("2", "dos ")
.Replace("3", "tres ")
.Replace("4", "cuatro ")
.Replace("5", "cinco ")
.Replace("6", "seis ")
.Replace("7", "siete ")
.Replace("8", "ocho ")
.Replace("9", "nueve ");
}
}
}
}

Y ahora empieza la complicacin. Cmo combinamos ahora este nuevo procesador con cada uno de los
anteriores. Ya tenemos implementados tres procesadores distintos (UpperCaser, WhiteSequenceRemover y
UpperCaserAndWhiteSequenceRemover) que podemos desear combinar con el nuevo DigitRemover. Esto
dar lugar a tres nuevas clases y nuestra librera estar formada por un total de siete. Si aadimos un nuevo
procesador de cadenas, la librera deber enriquecerse hasta tener quince clases. Encima, muchas de ellas
repetirn cdigo, con los problemas que ya hemos apuntado. Un infierno.
Veamos cmo resolver el problema con una aproximacin distinta. El cdigo de UpperCaser y
WhiteSequenceRemover ser este:
public class UpperCaser : IStringProcessor

33

Andrs Marzal

Buenas prcticas en desarrollo de software con .NET

{
private readonly IStringProcessor _stringProcessor;
public UpperCaser(IStringProcessor stringProcessor = null)
{
_stringProcessor = stringProcessor;
}
public string Process(string input)
{
if (_stringProcessor != null)
input = _stringProcessor.Process(input);
return input.ToUpper();
}
}
public class WhiteSequenceRemover : IStringProcessor
{
private readonly IStringProcessor _stringProcessor;
public WhiteSequenceRemover(IStringProcessor stringProcessor = null)
{
_stringProcessor = stringProcessor;
}
public string Process(string input)
{
if (_stringProcessor != null)
input = _stringProcessor.Process(input);
return Regex.Replace(input, " +", " ");
}
}

Las dos clases se han diseado siguiendo un mismo esquema. Las dos implementan la misma interfaz y
ambas almacenan un IStringProcessor como campo privado (_stringProcessor) al que slo se puede
asignar un valor a travs del constructor. El valor de _stringProcessor puede ser nulo, pues el constructor
admite ste valor como valor por defecto.
Esta cuestin de que _stringProcessor pueda tomar valor nulo no es muy elegante: obliga a poner una
guarda en cada definicin de Process. Si creamos un procesador idempotente, el resultado ser ms
elegante:
class IdentityStringProcessor : IStringProcessor
{
public string Process(string input)
{
return input;
}
}
public class UpperCaser : IStringProcessor
{
private readonly IStringProcessor _stringProcessor;
public UpperCaser(IStringProcessor stringProcessor = null)
{
_stringProcessor = stringProcessor ?? new IdentityStringProcessor();
}
public string Process(string input)
{
return _stringProcessor.Process(input).ToUpper();

34

Andrs Marzal

Buenas prcticas en desarrollo de software con .NET

}
}
public class WhiteSequenceRemover : IStringProcessor
{
private readonly IStringProcessor _stringProcessor;
public WhiteSequenceRemover(IStringProcessor stringProcessor = null)
{
_stringProcessor = stringProcessor ?? new IdentityStringProcessor();
}
public string Process(string input)
{
return Regex.Replace(_stringProcessor.Process(input), " +", " ");
}
}

Veamos cmo construir un procesador de cadenas que pase cadenas a todo maysculas y cmo usarlo:
class Demo
{
static void Main()
{
IStringProcessor upperCaser = new UpperCaser();
var entrada = "un ejemplo de
cadena";
var salida = upperCaser.Process(entrada);
Console.WriteLine("{0} -> {1}", entrada, salida);
}
}

Nada especial. Resulta evidente cmo crear un procesador de cadenas que elimine los blancos de ms, pero
cmo crear un procesador que pase a maysculas y elimine los espacios en blanco?:
class Demo
{
static void Main()
{
IStringProcessor myProcessor = new WhiteSequenceRemover(new UpperCaser());
var entrada = "un ejemplo de
cadena";
var salida = myProcessor.Process(entrada);
Console.WriteLine("{0} -> {1}", entrada, salida);
}
}

El constructor de un IStringProcessor acepta como parmetro (potencialmente) otro IStringProcessor


con el que forma un proceso en cadena.
WhiteSequencerRemover
UpperCaser
IdentityStringProcessor

Cada procesador aade funcionalidad a otro: decimos que un procesador decora al otro. Donde tena dos
procesadores simples puedo obtener dos complejos con gran sencillez, sin complicar la coleccin de clases
que ofrezco en mi librera. Bienvenidos a nuestro primer patrn de diseo: el Decorator.
4.2.2 El patrn Decorator
Veamos ahora una presentacin tpica del patrn en el propio libro de la banda de los cuatro.

35

Andrs Marzal

Decorator
Propsito
Aadir dinmicamente responsabilidades adicionales a un
objeto. Los decoradores proporcionan una alternativa flexible
a la especializacin por herencia para extender funcionalidad.
Tambin conocido como
Wrapper (envoltorio).
Motivacin
A veces queremos aadir responsabilidades a objetos
individuales, no a una clase entera. Una librera para
interfaces grficas de usuario, por ejemplo, debe permitir
agregar propiedades como bordes o desplazamiento a
cualquier componente de la interfaz de usuario.
Una forma de aadir funcionalidad es con herencia. Heredar
el borde de una clase puede lograr que cada instancia de sus
subclases presente un borde alrededor. Esta prctica es, sin
embargo, inflexible porque la eleccin del borde se hace
estticamente. Un cliente no puede controlar cmo y cundo
decorar un componente con un borde.
Un enfoque ms flexible consiste en incluir el componente en
otro objeto que aada el borde. El objeto que encierra al otro
se llama decorador. El decorador se ajusta a la interfaz del
componente que decora, de manera que su presencia es
transparente para los clientes del componente. El decorador
reenva la llamada al componente y realiza acciones
adicionales (como el dibujo de un borde) antes o despus del
reenvo. La transparencia permite decoradores anidados
recursivamente, permitiendo as aadir un nmero ilimitado
de responsabilidades.

Buenas prcticas en desarrollo de software con .NET

ScrollDecorator para producir una vista limita, texto


desplazable:

Las clases ScrollDecorator y BorderDecorator son subclases


de Decorator, una clase abstracta para los componentes
visuales que decoran otros componentes visuales.

VisualComponent es la clase abstracta de objetos visuales.


Define el dibujado y la interfaz de gestin de eventos. Ntese
que la clase Decorator simplemente reenva las peticiones de
dibujado a su componente y cmo las subclases de Decorator
puede extender la operacin.
Las subclases de Decorator son libres de aadir operaciones
para funcionalidades especficas. Por ejemplo, el
funcionamiento del mtodo ScrollTo de ScrollDecorator
permite a otros objetos efectuar desplazamientos en la
interfaz si saben que hay un objeto ScrollDecorator en la
interfaz. El aspecto relevante de este modelo es que permite
que los decoradores aparecezcan en cualquier lugar donde
puede aparecer un VisualComponent.
De ese modo, los clientes normalmente no pueden
diferenciar entre un componente decorado y otro sin
decorar, y as no se crea una dependencia con respecto a la
decoracin.

Por ejemplo, supongamos que tenemos un objeto de


TextView que muestra el texto en una ventana. TextView no
tiene barras de desplazamiento por defecto porque no
siempre se necesitan. Cuando lo hacemos, podemos utilizar
un ScrollDecorator para agregarlos. Supongamos tambin
que queremos aadir un borde grueso negro alrededor de la
TextView. Podemos utilizar un BorderDecorator para
agregarlo. Simplemente componemos los decoradores con la
Vista de Texto para producir el resultado deseado.
El siguiente diagrama de objetos muestra cmo componer un
objeto TextView con BorderDecorator y objetos

Aplicabilidad
Use Decorator:

para aadir responsabilidades a objetos


individuales dinmica y transparentemente, es
decir, sin afectar a otros objetos.
para responsabilidades que se pueden retirar.
cuando la extensin por especializacin con
herencia no es practicable. A veces, un nmero
elevado de extensiones independientes produciran
una explosin de subclases para soportar a
cualquier combinacin. O una definicin de clase
podra estar oculta o, en general, no disponible
para definir una subclase.

36

Andrs Marzal

Buenas prcticas en desarrollo de software con .NET

2.
Estructura

Participantes

Componente (VisualComponent): define la interfaz


para objetos que pueden tener responsabilidades
aadidas dinmicamente.

ConcreteComponent (TextView): define un objeto


al que se pueden aadir responsabilidades.

Decorator: mantiene una referencia a un objeto


Component y define una interfaz que se atiene a la
interfaz del Component.

ConcreteDecorator (BorderDecorator,
ScrollDecorator): aade responsabilidades al
componente.

3.

4.

Colaboraciones

El Decorator reenva las peticiones a sus objetos


Component. Opcionalmente puede realizar
operaciones adicionales antes y despus de
reenviar la peticin.
Consecuencias
El patrn decorador tiene al menos dos beneficios clave y
pasivos dos:
1.

Ms flexibilidad que la herencia esttica. El patrn


Decorator proporciona una manera ms flexible de
aadir responsabilidades a objetos que la que
podemos tener con la herencia (mltiple) esttica.
Con decoradores las responsabilidades se pueden
agregar y quitar en tiempo de ejecucin con slo
conectarlas y desconectarlas. Por el contrario, la
herencia requiere la creacin de una nueva clase
para cada responsabilidad adicional (por ejemplo,
BorderedScrollableTextView, BorderedTextView).
Esto da lugar a muchas clases y aumenta la
complejidad de un sistema. Por otra parte,
proporciona diferentes clases Decorator para una
clase de componentes especficos permite mezclar
y combinar responsabilidades. Los decoradores
tambin facilitan agregar una propiedad dos veces.
Por ejemplo, para dar una TextView un borde doble
basta con conectar dos BorderDecorators. Heredar
dos veces de una clase que aada el borde es
propenso a la comisin de errores, en el mejor de
los casos.

Evita clases cargadas de funcionalidad en lo alto de


la jerarqua. Decorator ofrece un enfoque pague
sobre la marcha a la adicin de responsabilidades.
En lugar de tratar de soportar todas las
caractersticas previsibles en una clase compleja y
personalizable, define una clase simple y agrega la
funcionalidad incrementalmente mediante
decoraciones. La funcionalidad puede componerse
de piezas sencillas. Como resultado, una aplicacin
no tiene por qu pagar por funcionalidad que no
utiliza. Tambin facilita definir nuevos tipos de
decoradores con independencia de las clases de
objetos extendidas, permitiendo extensiones
imprevisibles. Ampliar una clase compleja suele
exponer detalles relacionados con las
responsabilidades que se aaden.
Un decorador y su componente no son idnticos. Un
decorador acta como una envoltura transparente.
Pero desde el punto de vista de la identidad del
objeto, un componente decorado no es idntico al
propio componente. Por lo tanto no se debe
depender de la identidad del objeto al usar
decoradores.
Montones de pequeos objetos. Un diseo que
utiliza decoradores suele dar lugar a sistemas
compuestos por gran cantidad de pequeos
objetos parecidos. Los objetos se diferencian slo
en la forma de interconectarse, no en su clase o en
el valor de sus variables. Aunque estos sistemas son
fciles de personalizar por aquellos que los
entienden, puede dificultar el aprendizaje y la
depuracin.

Implementacin
Deben tenerse en cuenta varias cuestiones al aplicar el
patrn Decorator:
1.

2.

3.

Conformidad con la interfaz. La interfaz de un objeto


decorador debe ajustarse a la interfaz del componente
que se decora. por tanto, la clase ConcreteDecorator
debe heredar de una clase comn (al menos en C++).
Omisin de la clase abstracta Decorator. No hay
necesidad de definir una clase abstracta Decorator
cuando slo hay necesidad de aadir una
responsabilidad. Este suele ser el caso cuando se trata
con una jerarqua de clases existente en lugar del diseo
de una nueva. En ese caso, se puede combinar la
responsabilidad del Decorator de reenviar peticiones a
la componente en el ConcreteDecorator.
Mantenimiento de levedad en las clases Component.
Para asegurar un ajuste de la interfaz, componentes y
decoradores deben descender de una clase Component
comn. Es importante que esa clase comn sea ligera, es
decir, sta debe centrarse en la definicin de una
interfaz, no en el almacenamiento de datos. La
definicin de la representacin de datos se desplazara a

37

Andrs Marzal

4.

las subclases; de lo contrario la complejidad de la clase


Component puede hacer que los decoradores sean
demasiado pesados para ser usados frecuentemente.
Juntar una gran cantidad de funcionalidad en un
Component aumenta la probabilidad de que las
subclases especficas paguen por funciones que no
necesitan.
Cambio de la piel de un objeto frente a cambio de sus
entraas. Podemos pensar en un decorador como la piel
de un objeto que cambia su comportamiento. Una
alternativa es cambiar las entraas del objeto. La
Estrategia (315) es un buen ejemplo de patrn para el
cambio de las entraas.
Las estrategias son una opcin mejor en situaciones en
que la clase Component es intrnsecamente pesada,
haciendo que el patrn Decorator sea demasiado
costoso de aplicar. En el patrn Estrategia, el
componente desplaza su comportamiento a un objeto
Estrategia distinto. El patrn Estrategia nos permite
modificar o ampliar la funcionalidad del componente
mediante la sustitucin del objeto estrategia.
Por ejemplo, podemos soportar diferentes estilos de
borde haciendo que el componente difiera el dibujado
del borde a objeto Border separado. El objeto Border es
un objeto Strategy que encapsula una estrategia de
dibujado de bordes. Al extender el nmero de
estrategias de una a una lista potencialmente ilimitada
se consigue el mismo efecto que si se anidaran los
decoradores recursivamente.
En MacApp 3.0 [App89] y Bedrock [Sym93a], por
ejemplo, los componentes grficos (denominados
vistas) mantienen una lista de objetos de adorno que
pueden ligar adornadores adicionales, como los bordes,
a un componente de vista. Si una vista tiene adornos
vinculados, le da la oportunidad de dibujar los
ornamentos adicionales. MacApp y BedRock deben usar
esta aproximacin por la clase View es pesada. Sera
demasiado caro usar una View completamente
equipada para limitarse a aadir un borde.
Como el patrn Decorator solo cambia un componente
desde el exterior, el componente no ha de saber nada
sobre sus decoradores; esto es, los decoradores son
transparentes al componente:

Con estrategias, los mismos componentes saben de las


posibles extensiones. As pues, ha de referenciar y
mantener las correspondientes estrategias:

La aproximacin basada en Strategy puede requerir la

Buenas prcticas en desarrollo de software con .NET

modificacin del componente para acomodar nuevas


extensiones. Por otra parte, una estrategia puede tener
su propia interfaz especializada, mientras que una
interfaz de decoracin debe cumplir con la del
componente (DrawBorder, GetWidth, etc.), lo que
significa que la estrategia puede ser ligera incluso si la
clase Component es pesada.
MacApp y Bedrock usan esta aproximacin para ms
cosas que adornar vistas. Tambin la usan para
aumentar el comportamiento de objetos en el
tratamiento de eventos. En ambos sistemas, la vista
mantiene una lista de objetos comportamiento que
pueden modificar e interceptar eventos. La vista da, a
cada objeto de comportamiento registrado, una
oportunidad de gestionar el evento antes que a los
comportamientos no registrados, redefinindolos
efectivamente. Podemos decorar una vista con soporte
especial de gestin del teclado, por ejemplo, registrando
un objeto de comportamiento que intercepta y gestiona
los eventos de tecla.
Cdigo de ejemplo
[Lo omitimos por extenso y por estar presentado en C++]
Usos conocidos
Muchas librera de interface de usuario orientadas a objetos
usas decoradores para aadir ornamentaciones a los
componentes. Entre los ejemplos encontramos InterViews
[LVC98, LCI+092], ET++ [WGM88] y la librera de clases
ObjectWorks\Smalltalk [Par90]. Aplicaciones ms exticas de
Decorator son: DebuggingGlyph de InterViews y
PassivityWrapper de ParcPlace SmallTallk. Un
DebuggingGlyph imprime informacin de depuracin antes y
despus de reenviar una peticin de maquetacin a su
componente. La informacin de traza puede usarse para
analizar y depurar el comportamiento de maquetacin de los
objetos en una composicin compleja. El PassivityWrapper
puede habilitar o deshabilitar las interacciones de los
usuarios con el componente.
Pero el parn Decorator no limita su uso a interfaces grficas
de usuario, como el siguiente ejemplo (basado en las clases
de flujos de datos de ET++) ilustra.
Los flujos de datos son abstracciones fundamentales de la
mayora de sistemas I/O. Un flujo puede proporcionar una
interfaz para convertir objetos en secuencias de bytes o
caracteres. Esto permite transcribir un objeto a un fichero o a
una cadena en memoria para su posterior recuperacin. Un
modo directo de hacerlo es definir una clase abstracta
Stream con subclases MemoryStream y FileStream. Pero
supongamos que tambin queremos hacer lo siguiente:

Comprimir los datos del flujo usando diferentes


algoritmos de compresin (codificacin por
longitud de recorrido, Lempel-Ziv,etc.).

38

Andrs Marzal

Reducir los datos del flujo a caracteres ASCII de 7


bits de modo que se puedan transmitir por un canal
de comunicacin ASCII.

El patrn Decorator proporciona un modo elegante de aadir


esas responsabilidades a los flujos. El siguiente diagrama
muestra una solucin al problema.

La clase abstracta Stream mantiene un buffer interno y


proporciona operaciones para almacenar datos en el flujo
(PutInt, PutString). Cuando el buffer se ella, Stream llama a la
operacin abstracta HandleBufferFull, que efecta la
verdadera transferencia de datos. La versin FileStream de
esta operacin redefine esta operacin ara transferir el
buffer a un fichero.
La clase clave aqu es StreamDecorator, que mantiene una
referencia a un componente de flujo y reenva las peticiones
hacia l. Las subclases de StreamDecorator redefinen
HandleBufferFull y realizan acciones adicionales antes de
invocar la operacin HandleBufferFull del StreamDecorator.

Buenas prcticas en desarrollo de software con .NET

Por ejemplo, la subclase CompressingStream comprime los


datos, y ASCII7Stream convierte los datos en ASCII de 7 bits.
Ahora, para crear un FileStream que comprime sus datos y
convierte los datos binarios comprimidos a ASCII de 7 bits,
decoramos un FleStream con un CompressingStream y un
ASCII7Stream:
Stream* aStream = new CompressingStream(
new ASCII7Stream(
new FileStream("aFileName")
)
);
aStream->PutInt(12);
aStream->PutString("aString");

Patrones relacionados
Adapter (139): Un decorador es diferente de un adaptador
en que un decorador slo cambia las responsabilidad de un
objeto, no su interfaz; un adaptador dar a un objeto una
interfaz completamente nueva.
Composite (163): Un decorador puede verse como un
compuesto degenerado con un nico componente. Sin
embargo, un decorador aade responsabilidades adicionales
y su cometido no es la agregacin de objetos.
Strategy (315): Un decorador permite cambiar la piel de un
objeto; una estrategia permite cambiarle las entraas. Son
dos formas alternativas de cambiar un objeto.

39

Buenas prcticas en desarrollo de software con .NET

Nosotros no recurriremos a explicaciones tan detalladas de los patrones de diseo que presentaremos (y
que sern, adems, unos pocos), pero s presentaremos una descripcin de su finalidad y, posiblemente, un
grfico UML que describa los elementos que forman parte de una implementacin genrica del patrn y sus
interrelaciones. En el caso del patrn Decorator, este es el diagrama:

Ntese que Component y Decorator son clases que implementan la misma interfaz IComponent (El tringulo
indica herencia cuando va seguido de una lnea continua) o implementacin (si la lnea es discontinua, como
en la figura). La clase Decorator contiene un atributo privado (los atributos van en la primera zona del
cuadro de clase y si son privados, llevan un -) con el objeto decorado y define la operacin a la que obliga
la interfaz (los mtodos se marcan con parntesis y, si son pblicos, van precedidos por un +). La nota
(rectngulo con esquina doblada) aclara que esta operacin llama a la operacin homnima del objeto
decorado. El objeto Client es un objeto que puede tener (el rombo indica posesin) instancias de
Component o de Decorator. En cualquier caso, las percibe como instancias de clases que implementan
IComponent.
4.2.3 Un ejemplo de Decorator en el mundo real: la librera de flujos de entrada/salida
Ya hemos visto en la ficha del libro de los cuatro que el patrn se usa en el diseo de libreras de flujos de
datos para entrada/salida. Tambin se usa en la librera estndar .NET. Vamos este cuadro de la arquitectura
de flujos de datos en .NET (adaptada del libro C# 4.0 in a Nutshell, de Joseph Albahari y Ben Albahari):
Stream adapters

Decorator streams

Backing store streams

StreamReader
Text

Int,
float,
string...

FileStream
StreamWriter

DeflateSream

BinaryReader

GZipStream

BinaryWriter

CryptoStream

XmlReader

BufferedStream

XML
data

MemoryStream

NetworkStream
XmlWriter

Raw
bytes

IsolatedStorageStream

40

Buenas prcticas en desarrollo de software con .NET

Al leer o escribir datos elegimos:

Un flujo (y slo uno) que indica la fuente fsica de almacenamiento:


o FileStream: fichero,
o IsolatedStorageStream: zona de almacenamiento aislado,
o MemoryStream: memoria,
o NetworkStream: red,
Uno o ms decoradores que aaden funcionalidad:
o Compresin con protocolo especial en cabecera y cola: GzipStream,
o Compresin: DeflateStream,
o Con encriptacin: CryptoStream,
o Con buffer: BufferedStream.
Un adaptador (y slo uno) que ofrece un perfil particular segn el tipo de datos que deseamos
leer/escribir:
o Text: StreamReader o StreamWriter.
o Datos binarios: BinaryReader o BinaryWriter.
o Datos en formato XML: XmlReader o XmlWriter.

(Por cierto hay un patrn de diseo denominado Adapter (adaptador) que veremos ms adelante. Los
adaptadores se ajustan a este diseo.)
Supongamos que deseamos leer la primera lnea de un fichero de texto, sin ms. El cdigo presentar este
aspecto:
using(Stream s = new FileStream("texto.txt", FileMode.Open))
using (TextReader tr = new StreamReader(s))
{
string line = tr.ReadLine();
Console.WriteLine(line);
}

El StreamReader recibe un Stream con los datos en binario y se encarga de interpretarlos como texto.
Supongamos ahora que los datos estn comprimidos en el fichero. Basta con decorar apropiadamente el
FileStream:
using(Stream s = new FileStream("texto.txt", FileMode.Open))
using(Stream sc = new GZipStream(s, CompressionMode.Decompress))
using (TextReader tr = new StreamReader(sc))
{
string line = tr.ReadLine();
Console.WriteLine(line);
}

Y si, adems, queremos que la lectura de datos sea eficiente y haga uso de un buffer?:
using(Stream s = new FileStream("texto.txt", FileMode.Open))
using(Stream sb = new BufferedStream(s, 8192))
using(Stream sc = new GZipStream(sb, CompressionMode.Decompress))
using (TextReader tr = new StreamReader(sc))
{
string line = tr.ReadLine();
Console.WriteLine(line);
}

41

Buenas prcticas en desarrollo de software con .NET

Ntese que los decoradores reciben un Stream en el constructor y ellos mismos son objetos de la clase
Stream. Eso es lo que permite anidarlos con tanta sencillez.
La librera de entrada/salida es relativamente compleja. Saber que ciertos objetos son decoradores ayuda a
entender su papel en el sistema y el modo en que deben usarse.
4.2.4 Ejercicio
Queremos hacer diseos de ASCII Art y tenemos una coleccin de clases extensibles. Esas clases siempre
tienen un mtodo Dibuja, sin parmetros, que proporciona una cadena con un dibujo ASCII. Los objetos de
la clase Murcilago, por ejemplo, devuelve esta cadena cuando se invoca el mtodo Dibuja:
)\/(
)_\_V_/_(
)____(
`-'

Nota: Los blancos los hemos representado con para que se vean y la cadena tiene algo de formato y color
que no forma parte de la salida.
Y la clase Rana devuelve esto al llamar a Dibuja:
/\/\
|@)|@)|
/\
\\______//
\_||/
___/||\___
\\|()|//
\||/
\\\///
//\/\\
UUUUUUUUUUUU

Podemos embellecer cualquier coleccin de formas bsicas con una nueva clase que se llama Marco y que
tambin dispone de un mtodo Dibuja. Su usamos la clase Marco con un Murcilago, el resultado es ste
+++++++++++++++
+)\/(+
+)_\_V_/_(+
+)____(+
+`-'+
+++++++++++++++

Como se puede ver, el resultado es un marco formado por el carcter + sobre el dibujo original. Lo curioso
es que podemos aadir un marco a un objeto Marco. En el caso del murcilago con marco, el resultado es
este:
+++++++++++++++++
+++++++++++++++++
++)\/(++
++)_\_V_/_(++
++)____(++
++`-'++
+++++++++++++++++
+++++++++++++++++

Implementa el sistema haciendo uso del patrn Decorator.

42

Buenas prcticas en desarrollo de software con .NET

4.3 El patrn Adapter


Ya que hemos visto que la librera de entrada/salida hace uso de otro patrn de diseo, el adaptador o
Adapter, vamos a estudiarlo brevemente.
Un Adapter permite usar objetos de una clase cuya interfaz no se ajusta a los requerimientos de uso de
nuestra aplicacin o librera. Se usa frecuentemente cuando se nos proporciona una librera con clases ya
definidas y que no podemos modificar, pero que hemos de usar invocando mtodos que no implementan
directamente o que presentan variaciones con respecto a los que s implementan (en nmero de
parmetros, en comportamiento, etc.).
Este diagrama UML presenta los elementos que conforman un Adapter y nos permite completar su
presentacin:
Client

interface
ITarget

Adaptee

+Request()

+SpecificRequest()

Adapter
+Request()

Invoca a
SpecificRequest

La interfaz ITarget describe las operaciones que nos gustara encontrar en la clase que no podemos
modificar. En este diagrama se ejemplifican con un solo mtodo: Request(). La clase que debe ser adaptada
es la que aparece como Adaptee. Efectivamente, no implementa un mtodo Request, sino uno propio
llamado SpecificRequest. La clase Adapter implementa la interfaz ITarget y posee una instancia de
Adaptee. Su objetivo es ofrecer una implementacin de Request que gestione del modo apropiado la
correspondiente llamada a Adapter.
Se podra pensar que es un patrn muy similar al Decorator, pues hay una composicin de objetos y un
reenvo de llamadas. Pero se trata de un patrn realmente diferente: el objeto adaptado no implementa la
interfaz ITarget y no es posible anidar objetos adaptados como s hacamos con los decorados.
El patrn encuentra una aplicacin evidente en la adaptacin de cdigo legacy, pero no solo. Hemos visto
que la librera de entrada/salida ofrece adaptadores para que los Stream (decorados o no) presenten
conjuntos de operaciones especializadas que hagan ms cmodo su uso en funcin del tipo de datos que
gestionan.
Por un principio de parsimonia, cuando slo hay una clase que implementa la interfaz ITarget, esta interfaz
no tiene por qu existir como tal.
Un programador acostumbrado al trabajo con patrones de diseo captar inmediatamente las diferencias
entre uno y otro cuando se aproxime a la solucin de un problema y sabr qu patrn es el indicado. Y
cuando haya de comunicarlo al resto del equipo, podr ser muy conciso.

43

Buenas prcticas en desarrollo de software con .NET

Nota: el Adapter que hemos presentado es el denominado adaptador de objetos. En la literatura se


presenta otro denominado adaptador de clases. No lo presentamos aqu.

4.4 Abstract Factory


Los objetos se crean a partir de la definicin de clase mediante sus constructores. Un constructor es un
mtodo especial, aunque solo sea en el sentido de que no contiene una sentencia return, pero siempre
devuelve algo (o arroja una excepcin si hay un problema durante la ejecucin): una instancia de esa clase.
Imaginemos una interfaz IStack que ofrece un perfil para implementaciones de una pila. Como sabr, una
pila es una estructura de datos a la que podemos aadir elementos y extraerlos en orden inverso al de
ingreso, adems de consultar el ltimo elemento aadido. Esta interfaz recoge las funciones Push (para
aadir) y Pop (para extraer) y la propiedad de slo lectura Top (para consultar el ltimo elemento metido en
la pila). Aparece, adems, una propiedad IsEmpty que permite saber si la pila est vaca.
public interface IStack<T>
{
void Push(T item);
T Pop();
T Top { get; }
bool IsEmpty { get; }
}

Hay varias implementaciones posibles de una pila. Una usa un simple vector:
public class ArrayStack<T> : IStack<T>
{
private T[] _data;
private int _count;
public ArrayStack(int capacity)
{
_data = new T[capacity];
_count = 0;
}
public void Push(T item)
{
if (_count == _data.Length)
throw new Exception("The stack has exceeded its capacity");
_data[_count++] = item;
}
public T Pop()
{
if (_count == 0)
throw new Exception("The stack is empty");
T result = _data[_count];
_count--;
_data[_count] = default(T);
return result;
}
public T Top
{
get
{
if (_count == 0)
throw new Exception("The stack is empty");
return _data[_count - 1];
}

44

Buenas prcticas en desarrollo de software con .NET

}
public bool IsEmpty
{
get { return _count == 0; }
}
}

Esta pila presenta dos problemas:

Si se trata de insertar ms elementos que celdas tiene el vector _data, se lanza una excepcin.
Si sobredimensionamos la capacidad de la pila para evitar el problema que acabamos de apuntar,
podemos desperdiciar una cantidad de memoria considerable.

Una implementacin alternativa se apoya en listas enlazadas:


public class LinkedStack<T> : IStack<T>
{
private class Node
{
public T Item;
public Node Next;
public Node(T item, Node next)
{
Item = item;
Next = next;
}
}
private Node _list;
public LinkedStack()
{
_list = null;
}
public void Push(T item)
{
_list = new Node(item, _list);
}
public T Pop()
{
if (_list == null)
throw new Exception("The stack is empty");
var result = _list;
_list = _list.Next;
return result.Item;
}
public T Top
{
get
{
if (_list == null)
throw new Exception("The stack is empty");
return _list.Item;
}
}
public bool IsEmpty
{
get { return _list != null; }

45

Buenas prcticas en desarrollo de software con .NET

}
}

Esta implementacin no presenta los problemas de la pila basada en un vector, pero consume ms memoria
por cada elemento insertado en la pila, pues crea un objeto de la clase Node y este objeto almacena, adems
del elemento que se aade a la pila, un puntero a otro Node (o a null).
Habr, pues, contextos en los que es ms apropiado recurrir a una pila basada en un vector y contexto en los
que convendr usar la pila basada en la lista enlazada (por no hablar de contextos en los que puede convenir
hacer uso de pilas implementadas de formas diferentes de las dos que hemos presentado).
Imaginemos ahora un mtodo que necesita una pila de enteros para efectuar una actividad: un mtodo que
invierte el contenido de un IEnumerable<T>. La pila en si puede ser una variable declarada como de tipo
IStack<T>. Si la pila va a albergar un nmero mximo de elementos inferior o igual a cierta cantidad,
digamos que 100, convendr montar la pila como una instancia de ArrayStack<T>. Si, por el contrario, el
nmero mximo de elementos excede de 100 o somos incapaces de determinar ese nmero mximo,
convendr usar una instancia de LinkedStack<T>. Este es el cdigo del mtodo:
public static class Reverser
{
public static IEnumerable<T> Reverse<T>(IEnumerable<T> sequence)
{
IStack<T> stack;
long count = long.MaxValue;
var collection = sequence as ICollection<T>;
if (collection != null)
count = Math.Min(count, collection.Count);
if (count <= 100)
{
stack = new ArrayStack<T>((int)count);
}
else
{
stack = new LinkedStack<T>();
}
foreach (var item in sequence)
{
stack.Push(item);
}
while (!stack.IsEmpty)
{
yield return stack.Pop();
}
}
}

Fijmonos en el fragmento destacado en amarillo: es la seleccin de la clase que queremos instanciar para
implementar la pila. Imaginemos ahora que no disponemos de dos implementaciones posibles, sino de tres o
cuatro. Tendremos que modificar ese fragmento de cdigo si deseamos que se tengan en cuenta las
alternativas. El problema es an ms serio si consideramos que esa misma toma de decisin puede
reproducirse en muchos lugares de nuestro cdigo: en todos aquellos puntos en los que necesitemos
escoger la mejor implementacin posible para la pila.

46

Buenas prcticas en desarrollo de software con .NET

La solucin pasa por crear un mtodo especial que encierre la lgica de la toma de decisin y devuelva una
instancia de la clase ms apropiada:
public static class Stacks
{
public static IStack<T> Create<T>(int capacity)
{
IStack<T> result;
if (capacity <= 100)
{
result = new ArrayStack<T>((int)capacity);
}
else
{
result = new LinkedStack<T>();
}
return result;
}
}

El mtodo Stacks.Create<T> es fbrica de instancias de objetos que implementan la interfaz IStack<T>:


es un mtodo factora (Factory Method).
El modo de uso es sencillo:
public static class Reverser
{
public static IEnumerable<T> Reverse<T>(IEnumerable<T> sequence)
{
long count = long.MaxValue;
var collection = sequence as ICollection<T>;
if (collection != null)
count = Math.Min(count, collection.Count);
IStack<T> stack = Stacks.Create<T>((int)count);
foreach (var item in sequence)
{
stack.Push(item);
}
while (!stack.IsEmpty)
{
yield return stack.Pop();
}
}
}

Nuestro cdigo presenta una menor dependencia con respecto a las clases concretas que implementan
IStack<T>, lo que sigue el principio de diseo DIP.

4.5 Singleton
En matemticas, un singleton es un conjunto con un solo elemento. El trmino tiene difcil traduccin al
espaol con un solo vocablo, as que usaremos el trmino ingls. Un Singleton, en programacin orientada a
objetos, es una clase de la que slo es posible instanciar un objeto. Ese es su aspecto fundamental, aunque
es frecuente que, adems, el Singleton se instancie perezosamente, es decir, slo cuando es estrictamente
necesario hacer uso de l.

47

Buenas prcticas en desarrollo de software con .NET

Imaginemos un sistema de registro de eventos que nos ayude a detectar la aparicin de fallos en ejecucin o
a mostrar o almacenar informacin sobre acontecimientos relevantes de un programa en marcha. El Logger
es un objeto ms o menos complejo que se puede configurar indicando qu tipo de eventos registra o
muestra y el dispositivo o dispositivos en el que se muestra/registra la informacin relativa a ellos.
Lgicamente, desde cualquier punto de un programa querremos usar la misma instancia del Logger, as que
el Logger puede/debe disearse como Singleton.
Hagamos una primera versin de Logger que muestra los mensajes por consola y que no aplica filtro alguno
a dichos mensajes (y que no es un Singleton):
public enum LogLevel { Info, Warn, Error } ;
public class SimpleLogger
{
public void Log(LogLevel level, string message)
{
Console.WriteLine("{0}: {1}", level.ToString().ToUpper(), message);
}
}

Hemos definido tres niveles de eventos y una funcin Log que muestra por consola el nivel y un mensaje.
Podemos, por disciplina, instanciar una nica instancia de SimpleLogger, pero nadie nos impide construir
ms. Por otra parte, y aunque construyamos una sola, cmo la hacemos llegar a los puntos del cdigo que
hacen uso de ella? Una posibilidad es recurriendo a una variable global. Otra, pasando la instancia de unos
mtodos a otros en todos aquellos que puedan construir un objeto que necesite un Logger directa o
indirectamente. (Y ms tarde veremos un tercer modo mediante la inyeccin de dependencias.)
Esta nueva versin resuelve dos problemas: bloque la posibilidad de crear ms de un instancia y crea un
punto nico de acceso a la nica instancia de SimpleLogger:
public class SimpleLogger
{
private static readonly SimpleLogger instance = new SimpleLogger();
private SimpleLogger()
{
}
public static SimpleLogger Instance { get { return instance; }

public void Log(LogLevel level, string message)


{
Console.WriteLine("{0}: {1}", level.ToString().ToUpper(), message);
}
}

El nico constructor se ha declarado privado para asegurarnos de que nadie puede invocarlo desde fuera de
la clase. El campo esttico instance mantiene la nica instancia de SimpleLogger y quien quiera usarla
debe hacerlo a travs de la propiedad SimpleLogger.Instance.
La instancia de construye en algn momento previo a su uso, pero no tenemos mucho control acerca de
cundo. En este ejemplo no es algo muy preocupante, pues el objeto SimpleLogger es muy ligero. No
siempre ser el caso. Qu estrategia seguimos si queremos que slo se instancie en el momento en el que
va a ser usado por primera vez? Es muy sencillo si complicamos ligeramente la propiedad Instance:

48

Buenas prcticas en desarrollo de software con .NET

public class SimpleLogger


{
private static SimpleLogger instance;
private public SimpleLogger()
{
}
public static SimpleLogger Instance
{
get
{
if (instance == null)
instance = new SimpleLogger();
return instance;
}
}
public void Log(LogLevel level, string message)
{
Console.WriteLine("{0}: {1}", level.ToString().ToUpper(), message);
}
}

Si nuestro cdigo ha de ejecutarse en una aplicacin multihilo, hemos de tener precaucin a la hora de crear
la instancia:
public class SimpleLogger
{
private static volatile SimpleLogger instance;
private static object syncRoot = new Object();
private public SimpleLogger()
{
}
public static SimpleLogger Instance
{
get
{
if (instance == null)
lock(syncRoot)
{
if (instance == null)
instance = new SimpleLogger();
}
return instance;
}
}
public void Log(LogLevel level, string message)
{
Console.WriteLine("{0}: {1}", level.ToString().ToUpper(), message);
}
}

C# 4.0 nos permite simplificar el cdigo de creacin de objetos construidos perezosamente. Para ello
introdujo el tipo genrico Lazy<T>. La clase anterior se reescribira como sigue y mantendra el mismo nivel
de seguridad:
public class SimpleLogger
{
private static Lazy<SimpleLogger> instance = new Lazy<SimpleLogger>();

49

Buenas prcticas en desarrollo de software con .NET

private public SimpleLogger()


{
}
public static SimpleLogger Instance
{
get { return instance.Value; }
}
public void Log(LogLevel level, string message)
{
Console.WriteLine("{0}: {1}", level.ToString().ToUpper(), message);
}
}

Referimos al lector a la documentacin estndar para conocer con ms detalle el modo de uso del tipo
Lazy<T>.
Con cualquiera de las tres ltimas clases que hemos definido como posibles implementaciones, cmo
hacemos para que otros objetos accedan a nuestra nica instancia de SimpleLogger? Podemos definir en
ellos un campo SimpleLogger al que asignamos SimpleLogger.Instance en el momento de la
construccin. Esto presenta varios inconvenientes:

Creamos una dependencia con respecto de clases concretas, no de abstracciones. Si ms adelante


cambiamos de clase para registro de eventos, tendremos que tocar muchos puntos del cdigo.
La construccin del objeto dependiente de SimpleLogger comportar la creacin inmediata de la
instancia, con lo que no habremos ganado gran cosa haciendo
Estamos accediendo a un contexto con una especie de variable global, lo que crea una dependencia
que deberamos evitar en la medida de lo posible.

De todos estos problemas nos encargaremos ms adelante, aunque ya podemos indicar por dnde irn las
soluciones:

Por usar una interfaz que separe la abstraccin (qu es un Logger?) de su implementacin
(SimpleLogger es un Logger concreto).
Usar libreras que faciliten la inyeccin de dependencias, es decir, que permitan que los objetos
dependientes reciban aquello que necesitan sin solicitarlo explcitamente y controlar el ciclo de vida
delos objetos desde esas libreras.

4.6 Observer
Los objetos se comunican entre s de formas diferentes. Ciertos objetos deben informar a otros de que han
ocurrido ciertos eventos y otros necesitan informarse de cundo ocurren esos eventos para reaccionar del
modo que consideren oportuno. Los objetos del primer tipo se conocen como observables y los del
segundo tipo como observadores. Si deseamos preservar el mayor nivel de desacoplamiento entre unos y
otros, tendremos que idear un sistema que permita a unos aceptar suscripciones a la notificacin de
eventos y a otros suscribirse a dicha notificacin. Es ah donde entra en juego el patrn
Observable/Observer o, simplemente, Observer.
El patrn de diseo se puede implementar de diferentes modos, pero veremos que C# ofrece un soporte
nativo que simplifica la labor. Empecemos siguiendo una filosofa que no hace uso de las posibilidades
caractersticas de C#. Este diagrama UML ayudar a entender una implementacin clsica del patrn:

50

Buenas prcticas en desarrollo de software con .NET

interface
IObserver

Observable
-Notify()
+Suscribe(observer)
+Unsuscribe(observer)

+Update(observable)

Observer
+Update(observable)

Los IObserver pueden suscribirse a un Observable a travs del mtodo Subscribe (o anular la suscripcin
con Unsuscribe). El Observable mantiene una lista de objetos que se han suscrito. Cuando ocurre el evento
del que debe avisarse a todos observadores, Observable invoca al mtodo Notify(). Este mtodo realiza
una llamada a Update sobre cada uno de los observadores suscritos.
El patrn de uso frecuentsimo en los sistemas modernos. Raro es el GUI que no recurre a este patrn de
diseo. Los creadores de C#, conscientes de la importancia del patrn dieron soporte nativo al mismo con
una estructura del lenguaje: los delegados (delegate).
Creemos un ejemplo. Una clase que recibe una secuencia de enteros y produce un aviso por cada nmero
par que encuentra. El aviso consistir en una llamada a cierto mtodo de todos los objetos que deseen ser
notificados. Nuestra clase observable podra definirse as:
public delegate void EvenNumberDetectedHandler(int number);
public class EvenDetector
{
public EvenNumberDetectedHandler EvenNumberDetected;
public void Detect(IEnumerable<int> numbers)
{
foreach (var number in numbers)
{
if (number % 2 == 0)
{
if (EvenNumberDetected != null)
{
EvenNumberDetected(number);
}
}
}
}
}

Lo primero que hemos definido es un tipo: EvenNumberDetectedHandler. El tipo define un perfil de funcin
o mtodo. Las clases que deseen ser notificadas del evento tendrn que proporcionar un mtodo con este
perfil. La clase EvenDetector contiene un campo de este tipo. El mtodo Detect es sencillo: recibe la
secuencia de enteros y la recorre; cuando detecta un nmero par, llama a EvenNumberDetected con el
nmero como parmetros (slo si EvenNumberDetected tiene valor no nulo). Para entender del todo el
mecanismo, necesitamos crear un observador y conectarlo al observable. Definamos dos clases de
observadores diferentes:
public class Observer1
{
public void AvisoPorConsola(int number)
{

51

Buenas prcticas en desarrollo de software con .NET

Console.WriteLine("He visto {0}", number);


}
}
public class Observer2
{
public void MuestroUnoMas(int number)
{
Console.WriteLine("El siguiente de {0} es {1}", number, number+1);
}
}

Y ahora establezcamos las suscripciones:


EvenDetector ed = new EvenDetector();
Observer1 o1 = new Observer1();
Observer2 o2 = new Observer2();
ed.EvenNumberDetected += new EvenNumberDetectedHandler(o1.AvisoPorConsola);
ed.EvenNumberDetected += new EvenNumberDetectedHandler(o2.MuestroUnoMas);

Hay una versin ms sencilla de la suscripcin, en la que el compilador nos echa una mano envolviendo el
mtodo con la construccin de un EvenNumberDetectedHandler automticamente:
ed.EvenNumberDetected += o1.AvisoPorConsola;
ed.EvenNumberDetected += o2.MuestroUnoMas;

El operador += indica que aadimos a la lista de suscriptores el mtodo que aparece a mano derecha. Dicho
mtodo tiene un perfil compatible con el tipo delegate que hemos definido antes. Cuando se active el
mtodo Detect de ed, cada aparicin de un nmero par disparar una llamada a los mtodos suscritos. Es
decir, esta lnea:
ed.Detect(new [] {1,2,3,4});

provocar la aparicin en pantalla de estos mensajes:


He
El
He
El

visto 2
siguiente de 2 es 3
visto 4
siguiente de 4 es 5

Ntese que un mismo evento ha sido notificado a varios elementos. Es lo que denominamos multicasting.
Podemos eliminar una suscripcin con el operador -=. Si el objeto o1 no desea recibir ms notificaciones,
bastar con la sentencia:
ed.EvenNumberDetected -= new EvenNumberDetectedHandler(o1.AvisoPorConsola);

o, en su versin simplificada:
ed.EvenNumberDetected -= o1.AvisoPorConsola;

Y aqu hay un problema: es posible que cualquier objeto elimine la suscripcin de cualquier otro, lo que
puede genera problemas de permisos y seguridad en nuestro cdigo. En la plataforma .NET se trata de
corregir este problema distinguiendo entre delegados y eventos. Los eventos son delegados que
controlan el proceso de borrado de suscripciones: slo la clase que gestiona el evento y la aade un mtodo

52

Buenas prcticas en desarrollo de software con .NET

a la lista de suscriptores pueden eliminarlo. Para que nuestro notificador sea un evento basta con usar la
palabra reservada event en la declaracin del campo correspondiente:
public delegate void EvenNumberDetectedHandler(int number);
public class EvenDetector
{
public event EvenNumberDetectedHandler EvenNumberDetected;

4.7 Un estilo: Fluent Interface


La interfaz fluida no es exactamente un patrn de diseo, sino ms bien un estilo de diseo basado en el
encadenamiento de mtodos (method chaining).
Imaginemos una clase que implementa una coleccin, esto es, un estructura de datos a la que podemos
aadir varios elementos con un mtodo Add. Valga esta implementacin para ilustrar una tcnica
convencional:
public class Convencional<T>
{
private IList<T> _store;
public Convencional()
{
_store = new List<T>();
}
public void Add(T item)
{
_store.Add(item);
}
}

Al usar la clase tendremos aadiendo varios elementos tendremos que usar varias sentencias4:
Convencional<int> convencional = new Convencional<int>();
convencional.Add(1);
convencional.Add(2);
convencional.Add(5);
convencional.Add(3);
convencional.Add(2);

Una interfaz fluida se basa en que el mtodo devuelve siempre un objeto que nos permite encadenar
llamadas a mtodos:
public class Fluida<T>
{
private IList<T> _store;
public Fluida()
{
_store = new List<T>();
}
public Fluida<T> Add(T item)
4

Hagamos como si no supisemos que se puede inicializar una estructura que disponga del mtodo Add en el propio
proc eso de construccin con una sola sentencia.

53

Buenas prcticas en desarrollo de software con .NET

{
_store.Add(item);
return this;
}
}

Las llamadas ahora se pueden encadenar as:


Fluida<int> fluida = new Fluida<int>();
fluida.Add(1).Add(2).Add(5).Add(3).Add(2);

Las interfaces fluidas se han puesto de moda porque permiten implementar DSL (Domain Specific
Languages). Los DSL son lenguajes de propsito (muy) especfico que facilitan la codificacin de informacin
por parte de usuarios avanzados, pero no necesariamente programadores.
Podemos imaginar cmo se puede codificar fluidamente una sentencia en una aplicacin de pedidos de
pizza con un DSL:
cliente.nuevoPedido().con("pepperoni").con("doble de queso").sin("tomate").para(2);

La semntica de la sentencia es evidente. Codificar las clases para el cliente y el pedido no resulta
particularmente difcil.

4.8 Builder
El patrn de diseo Builder (constructor como traduccin se presta a la confusin con el trmino usado
para el mtodo especial que permite instanciar una clase) separa la construccin de un objeto complejo de
su representacin, de modo que el mismo proceso constructivo pueda crear diferentes representaciones. Es
similar al patrn Factory, pero crea un objeto complejo siguiendo una serie de pasos. Es muy habitual que el
Builder se desarrolle con un estilo de interfaz fluida.
Veamos un ejemplo de construccin de un objeto complejo. Una casa puede tener varias habitaciones, una
cocina y cero o ms plazas de garaje. Esta definicin de casa, que contiene definiciones internas de los
componentes citados, contiene tambin la definicin de un Builder:
public class Casa
{
private IList<Habitacin> _habitaciones;
private Cocina _cocina;
private Garaje _garaje;
public class Builder
{
private readonly IList<Habitacin> _habitaciones = new List<Habitacin>();
private Cocina _cocina;
private Garaje _garaje = new Garaje(0);
public Builder ConHabitacin(Habitacin.Tipo tipo, decimal metros)
{
_habitaciones.Add(new Habitacin(tipo, metros));
return this;
}
public Builder ConCocina(Cocina.Tipo tipo, decimal metros)
{
if (_cocina != null)
{

54

Buenas prcticas en desarrollo de software con .NET

throw new ArgumentException("No puede tener ms de una cocina");


}
_cocina = new Cocina(tipo, metros);
return this;
}
public Builder ConPlazasDeGaraje(int cantidad)
{
_garaje.Plazas += cantidad;
return this;
}
public Casa Construye()
{
if (_cocina == null)
{
throw new ArgumentException("Es imposible que no haya cocina");
}
return new Casa {_habitaciones = _habitaciones, _cocina = _cocina,
_garaje = _garaje };
}
}
public class Habitacin
{
public enum Tipo
{
Dormitorio, Saln, Comedor, SalaDeEstar, Otro
} ;
private Tipo _tipo;
private decimal _metros;
public Habitacin(Tipo tipo, decimal metros)
{
_tipo = tipo;
_metros = metros;
}
public override string ToString()
{
return string.Format("habitacin {0} ({1} m2)", _tipo, _metros);
}
}
public class Cocina
{
public enum Tipo
{
Completa, Office, ConIslaCentral
} ;
private Tipo _tipo;
private decimal _metros;
public Cocina(Tipo tipo, decimal metros)
{
_tipo = tipo;
_metros = metros;
}
public override string ToString()
{
return string.Format("cocina {0} ({1} m2)", _tipo, _metros);
}
}

55

Buenas prcticas en desarrollo de software con .NET

public class Garaje


{
public int Plazas { get; internal set; }
public Garaje(int plazas)
{
Plazas = plazas;
}
public override string ToString()
{
if (Plazas > 0)
{
return string.Format("{0} plazas de garaje", Plazas);
}
else
{
return string.Empty;
}
}
}
public override string ToString()
{
var s = new StringBuilder("Casa con ");
foreach (var habitacin in _habitaciones)
{
s.Append(habitacin.ToString() + ", ");
}
s.Append(_cocina.ToString());
if (_garaje.Plazas > 0)
{
s.Append("y " + _garaje.ToString());
}
return s.ToString();
}
}

Podemos construir una casa as:


Casa c = new Casa.Builder().ConHabitacin(Casa.Habitacin.Tipo.Dormitorio, 12M)
.ConHabitacin(Casa.Habitacin.Tipo.Comedor, 20M)
.ConCocina(Casa.Cocina.Tipo.ConIslaCentral, 12)
.ConPlazasDeGaraje(2)
.Construye();

Por cierto: si analizamos el mtodo ToString de Casa veremos que se apoya en un Builder de cadenas:
StringBuilder. Un StringBuilder es un objeto que viene predefinido en la librera estndar y sigue el
patrn Builder (y es, adems, un ejemplo de interfaz fluida, pues se pueden encadenar llamadas as:
s.Append("Un").Append(" ").Append("ejemplo");

4.9 Unas reflexiones finales


Acabamos, pues con unas reflexiones:

Los patrones de diseo proponen tcnicas para resolver problemas frecuentes, pero estas tcnicas
no son corss rgidos o construcciones tan especficas que puedan suministrarse como componentes
de una librera: el desarrollador debe interpretar la tcnica y aplicarla a su problema con un diseo
particularizado a partir de las directrices fijadas por el patrn.

56

Buenas prcticas en desarrollo de software con .NET

Si bien los patrones de diseo no dependen de lenguajes de programacin concretos, ciertos


patrones de diseo se soportan mejor en ciertos lenguajes de programacin (por ejemplo, el patrn
Observer es particularmente sencillo de implementar en C#). Por ejemplo, los Decorator se apoyan
en interfaces y encontramos interfaces en C# (y Java), pero en lenguajes como C++ hemos de
recurrir a clases abstractas y en Python no hay interfaces ni es frecuente trabajar con clases
abstractas puras (pese a lo cual sigue siendo posible implementar el patrn Decorator). Esto es as
porque el diseo de esos lenguajes de programacin ofrecen constructos ideados para dar ese
soporte o porque las caractersticas dinmicas de algunos lenguajes simplifican su implementacin.
Los desarrolladores primerizos en el uso de los patrones de diseo tienden a un sobreutilizarlos y
plagan el cdigo de diferentes patrones. Se debe ser consciente de que un abuso de los patrones no
conduce a un cdigo mejor. Su uso debe limitarse a aquello en lo que mejoran claramente el cdigo:
no son una panacea. Pero no conviene preocuparse en exceso por esta cuestin: el tiempo pone las
cosas en su sitio.
Sin una experiencia razonable en programacin, los patrones de diseo parecen invenciones ms o
menos extravagantes. Slo se perciben como soluciones efectivas cuando uno se ha planteado ya el
problema que resuelven.
Un mismo patrn de diseo puede resolver uno o varios problemas que resultan difciles de abordar
con las herramientas convencionales.
Los patrones pueden aparecer en la literatura con diferentes nombres (el Decorator tambin se
conoce por Wrapper), aunque un objetivo de las recopilaciones de patrones es, precisamente,
establecer una terminologa estandarizada que facilite la comunicacin.
Muchos patrones estn interrelacionados y la explicacin de uno puede comportar referencias a
muchos otros. Esto dificulta un aprendizaje puramente secuencial de los patrones de diseo. Cuanto
ms se sabe de los patrones de diseo, ms fcil es saber ms de ellos.
Conocer los patrones de diseo ayuda a entender el cdigo escrito por otros. Si vemos que se hace
uso de un Decorator para modelar algo, podemos centrarnos en ese algo y no en la infraestructura
que corresponde al patrn en s.

4.10 Crditos y recursos

El libro A Pattern Language. Towns, Buildings, Construction se puede adquirir a traves de la


pgina web de Christopher Alexander: http://www.patternlanguage.com/leveltwo/books.htm.
El artculo Using Pattern Languages for Object-Oriented Programs de Kent Beck y Ward
Cunningham est disponible en http://c2.com/doc/oopsla87.html.
El Portland Pattern Repository se encuentra en http://c2.com/ppr/.
El libro de la banda de los cuatro se puede adquirir en http://www.amazon.com/Design-PatternsElements-Reusable-Object-Oriented/dp/0201633612.
La segunda edicin de Code Complete, de Steve McConnel se puede comprar en
http://www.amazon.com/Code-Complete-Second-Steve-McConnell/dp/0735619670/sr=11/qid=1169499581?ie=UTF8&s=books.
El libro Holub on Patterns, de Allen Holub, est a la venta en
http://www.holub.com/payment/holub.on.patterns.html.
Head First Design Patterns se puede adquirir en http://oreilly.com/catalog/9780596007126.
C# 3.0 Design Patterns, de Judith Bishop, se vende en http://oreilly.com/catalog/9780596527730.

57

Buenas prcticas en desarrollo de software con .NET

El ASCII art del ejercicio del patrn de diseo Decorator se ha extrado de


http://www.chris.com/ascii.

5 Reflexin
Lenguajes como Java, C#, Ruby o Python tienen un rasgo en el que conviene detenerse: son lenguajes
dotados de introspeccin, esto es, la capacidad de examinar las caractersticas de los objetos en tiempo de
ejecucin. Podemos, por ejemplo, conocer de qu clase es instancia un objeto inquiriendo apropiadamente
al mismo objeto, o saber si dispone de un mtodo determinado, o conocer la lista de sus mtodos,
propiedades, etc. En C# se usa el trmino reflexin para referirse a la introspeccin. Los datos que
obtenemos por reflexin se conocen por metadatos, pues son datos que describen a otros datos.
Muchas herramientas basan su magia en un uso apropiado de la introspeccin, as que conviene conocer
lo bsico de esta tcnica para entender cmo resuelven ciertos problemas.
En .NET podemos ejecutar acciones de reflexin sobre ensamblados, mdulos y tipos. Estas acciones pueden
consistir en simples consultas o ir ms all y crear instancias de una clase dinmicamente. Las utilidades a las
que recurriremos se encuentran en el espacio de nombres System.Reflection.
Las aplicaciones .NET se ejecutan en la CLR (Common Language Runtime), un sistema que gestiona dominios
de aplicacin (application domains). Un dominio de aplicacin es un sistema aislado en el que se cargan
ensamblados (assemblies). El dominio de aplicacin facilita la gestin de la seguridad en el sistema
imponiendo limitaciones a lo que puede hacer el cdigo que contiene (acceso a recursos, comunicacin con
otras aplicaciones, etc.). Los ensamblados son los bloques bsicos de las aplicaciones .NET. Son colecciones
de tipos y recursos que forman una unidad funcional a efectos de versionado, distribucin, reutilizacin, etc.
Hay un nivel de agrupacin de elementos: el mdulo (module), que se corresponde con una unidad de
compilacin. Los mdulos contienen las definiciones de tipos (clases o tipos valor) y estos, a su vez,
contienen cdigo MSIL. El compilador no se limita a almacenar el cdigo MSIL de esos elementos: a cada
tem le asocia los metadatos que podremos consultar en tiempo de ejecucin.
5.1.1 GetType y typeof
Todos los objetos .NET ofrecen un mtodo GetType() que devuelve un dato de tipo System.Type
identificando el tipo al que pertenece el objeto. Los tipos en s son valores de la clase System.Type.
Bsicamente hace lo mismo que el operador typeof sobre el identificador de un tipo, pero con instancias
suyas. Veamos un ejemplo de uso:
namespace Reflexion1
{
class UnaClase
{
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine(3.GetType());
Console.WriteLine((3.1).GetType());
var uc = new UnaClase();
Console.WriteLine(uc.GetType());
Console.ReadKey();
}

58

Buenas prcticas en desarrollo de software con .NET

}
}

Al ejecutar, tenemos esto en pantalla:


System.Int32
System.Double
Reflexion1.UnaClase

Para obtener el tipo de una clase hemos de recurrir al operador typeof. Es decir, no podemos ejecutar una
sentencia como sta (que da error al compilar):
System.Type t = UnaClase; // Mal

En lugar de eso, hemos de escribir:


System.Type t = typeof(Casa);

5.1.2 GetMethods, GetMembers


La clase System.Type dispone de mtodos introspectivos, como GetMethods o GetMembers. El primero
proporciona un vector con todos los mtodos de una clase y el segundo, una lista con todos los miembros.
Un ejemplo ayuda a entender lo que hacen bastante ms que empezar a entrar en una explicacin con un
excesivo nivel de detalle:
using System;
using System.Reflection;
namespace Reflexion2
{
class UnaClase
{
private string _saludo = "Hi";
public string Saludo
{
set { _saludo = value.Trim(); }
get { return _saludo; }
}
public void Saluda(string nombre)
{
Console.WriteLine("{0}, {1}", Saludo, nombre);
}
public void Despide()
{
Console.WriteLine("Bye");
}
}
class Program
{
static void Main(string[] args)
{
MethodInfo[] methodInfoArray = typeof(UnaClase).GetMethods();
foreach (var methodInfo in methodInfoArray)
{
Console.WriteLine(methodInfo.Name);
foreach (var parameterInfo in methodInfo.GetParameters())
{

59

Buenas prcticas en desarrollo de software con .NET

Console.WriteLine(" {0}: {1}",


parameterInfo.Name, parameterInfo.ParameterType);
}
}
Console.WriteLine("====");
MemberInfo[] memberInfoArray = typeof(UnaClase).GetMembers();
foreach (var memberInfo in memberInfoArray)
{
Console.WriteLine("{0}: {1}",
memberInfo.Name, memberInfo.DeclaringType);
}
Console.ReadKey();
}
}
}

La salida por pantalla es esta:


set_Saludo
value: System.String
get_Saludo
Saluda
nombre: System.String
Despide
ToString
Equals
obj: System.Object
GetHashCode
GetType
====
set_Saludo: Reflexion2.UnaClase
get_Saludo: Reflexion2.UnaClase
Saluda: Reflexion2.UnaClase
Despide: Reflexion2.UnaClase
ToString: System.Object
Equals: System.Object
GetHashCode: System.Object
GetType: System.Object
.ctor: Reflexion2.UnaClase
Saludo: Reflexion2.UnaClase

Los objetos que devuelve GetMethods() o GetMembers() son de ciertos tipos definidos en
System.Reflection. No es necesario que entremos en el detalle de todos los (meta)datos que contienen,
pero el ejemplo presentado permite captar razonablemente bien de qu va esto de la introspeccin y la
potencia que podemos llegar a tener si somos capaces de averiguar tanta informacin sobre objetos en
tiempo de ejecucin.
Hay muchos otros mtodos capaces de extraer informacin sobre un tipo, como GetProperties,
GetInterfaces, GetEvents, etc.
5.1.3 InvokeMember
Otros mtodos permiten obtener el valor de un campo o invocar un mtodo a partir de la cadena que
corresponde a su identificador:
class Program
{
static void Main(string[] args)
{
UnaClase uc = new UnaClase();
Type t = uc.GetType();
t.InvokeMember("Saluda", BindingFlags.InvokeMethod, null, uc, new object[] {"t"});

60

Buenas prcticas en desarrollo de software con .NET

t.InvokeMember("Despide", BindingFlags.InvokeMethod, null, uc, null);


t.InvokeMember("Saludo", BindingFlags.SetProperty, null, uc, new object[1] {"Hola"});
Console.WriteLine(t.InvokeMember("Saludo", BindingFlags.GetProperty, null, uc, null));
Console.ReadKey();
}
}

La salida del programa es este texto por consola:


Hi, t
Bye
Hola

5.2 Creacin dinmica de objetos


En el espacio de nombres System encontramos una factora de objetos con la que podemos instanciar
objetos de cualquier clase, tanto local como remotamente. Basta con conocer el tipo de la clase para poder
construir una instancia con su constructor por defecto.
using System;
namespace Activador
{
class MiClase
{
public string Id { get; private set; }
public MiClase()
{
Id = "Ninguno";
}
public MiClase(string nombre, string apellido)
{
Id = nombre + " " + apellido;
}
public MiClase(int cdigo)
{
Id = cdigo.ToString();
}
}
class Program
{
static void Main(string[] args)
{
var mi1 = (MiClase) Activator.CreateInstance<MiClase>();
var mi2 = (MiClase) Activator.CreateInstance(typeof(MiClase));
var mi3 = (MiClase) Activator.CreateInstance(mi2.GetType());
var mi4 = (MiClase) Activator.CreateInstance(Type.GetType("Activador.MiClase"));
Console.WriteLine("{0}, {1}, {2}, {3}.", mi1.Id, mi2.Id, mi3.Id, mi4.Id);
Console.ReadKey();
}
}
}

Ejecutar el programa produce este resultado en pantalla:


Ninguno, Ninguno, Ninguno, Ninguno.

Las cuatro veces hemos invocado el constructor por defecto, de ah que siempre se asigne la cadena
"Ninguno" a la propiedad Id de cada objeto.

61

Buenas prcticas en desarrollo de software con .NET

Tambin podemos usar constructores con parmetros aunque, naturalmente, hemos de conocer el tipo de
cada uno de estos o provocaremos una excepcin.
class Program
{
static void Main(string[] args)
{
var mi1 = (MiClase) Activator.CreateInstance(typeof(MiClase),
new object[] {"Pepe", "Prez"});
var mi2 = (MiClase) Activator.CreateInstance(typeof(MiClase), new object[] { 1024 });
Console.WriteLine("{0}, {1}.", mi1.Id, mi2.Id);
Console.ReadKey();
}
}

Al ejecutar, leemos en pantalla:


Pepe Prez, 1024.

Algunas de las utilidades que manejaremos en el curso usan esta forma de instanciacin, que puede crear
objetos a partir de una simple referencia a su tipo.

6 Atributos
Como hemos visto, todo objeto presenta cierta informacin que podemos calificar de estndar o de
serie y sobre la que podemos demandar detalle: mtodos, campos, atributos, etc. La plataforma .NET
permite que aadamos metadatos propios y que averigemos despus, por reflexin, si una clase u objeto
lleva asociados esos metadatos. La informacin que aadimos se especificar con ciertas marcas (entre
corchetes) que precedern al elemento enriquecido (por ejemplo, a la lnea con la que empieza la definicin
de una clase). Los metadatos y, por extensin las marcas con las que los expresamos, reciben el nombre de
atributos. Los atributos facilitan el uso de estilos declarativos al programar, pues marcan las clases,
mtodos, campos, etc. con informacin que pueden explotar diferentes herramientas, en tiempo de
compilacin o de ejecucin.
No slo .NET dispone de la posibilidad de marcar unidades con atributos. En Java, por ejemplo, se conoce
por anotaciones a los atributos. En lenguajes como Python se dispone de decoradores (lo cual crea cierta
confusin con el patrn de diseo que ya hemos estudiado), aunque van ms all de los atributos dada la
naturaleza mucho ms dinmica de ese lenguaje.
Aprenderemos a crear nuestros propios atributos no tanto porque vayamos a hacer uso directo de esta
posibilidad (aunque es una tcnica a considerar para ciertas aplicaciones), como porque las herramientas
que usaremos s hacen un uso extensivo de ellas.

6.1 Uso de atributos


El siguiente cdigo contiene algo especial: un mtodo marcado con un atributo de la clase Conditional. El
atributo se expresa justo antes de la definicin del objeto al que marca, en este caso, un mtodo:
using System;
using System.Diagnostics;
namespace Atributos1
{
class UnaClase

62

Buenas prcticas en desarrollo de software con .NET

{
[Conditional("DEBUG")]
public static void Avisa() {
Console.WriteLine("El aviso");
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Voy");
UnaClase.Avisa();
Console.ReadKey();
}
}
}

El mtodo esttico Avisa de UnaClase est marcado con Condicional(cadena). Es un atributo


predefinido, es decir, viene de serie como utilidad para el programador. Este atributo se interpreta como
que el mtodo en cuestin slo debe compilarse (de hecho, existir) si est definido el pragma que
expresamos con la cadena; en el ejemplo, slo si estamos compilando en modo DEBUG. Ejecutemos el
programa con la configuracin DEBUG y tendremos este resultado en pantalla:
Voy
El aviso

Pasemos a RELEASE y veremos cmo la salida pasa a ser


Voy

En este segundo caso, el mtodo Avisa no se invoca. El compilador ha tenido en cuenta el atributo del
mtodo a la hora de generar o no cdigo para la llamada al mtodo.
Hay muchos ms atributos predefinidos y presentan los usos ms variopintos. Veamos unos pocos:

El atributo DllImport permite importar cdigo de una DLL:


[DllImport("cards.dll")]
public static extern bool cdtInit (ref int width,ref int height);

El atributo Flags indica que una enumeracin debe tratarse como un campo de bits.
[Flags]
public enum FontProperties
{
Bold = 1,
Italic = 2,
Underlined = 4,
None = 8
}

Con Flags estamos indicando que los diferentes valores pueden mezclarse, como aqu:
FontProperties f = FontProperties.Bold | FontProperties.Italic;

El atributo Obsolete permite marcar cdigo como obsoleto y, por tanto, declararlo no utilizable. Al
compilar, aparece un aviso (warning) si se hace uso de una clase obsoleta (y el IDE puede marcar el
uso de algn modo que permita visualizar esos usos durante la edicin del cdigo).

63

Buenas prcticas en desarrollo de software con .NET

[Obsolete]
class UnaClase
{
public static void Avisa() {
Console.WriteLine("El aviso");
}
}

El atributo Obsolete puede invocarse opcionalmente con parmetros. Si, por ejemplo, deseamos
que el aviso incluya un texto nuestro, podemos suministrarlo con un parmetro de tipo string:
[Obsolete("No uses este mtodo.")]. Un segundo parmetro de tipo bool permite indicar si
queremos que el uso de la clase sea considerado un error de compilador, y no un simple aviso. As,
[Obsolete("Prohibido", true)] impide el uso de la clase.

6.2 Definicin de atributos de usuario


Ya hemos visto cmo usar atributos en diferentes elementos (clases, tipos enumerados, mtodos), etc.
Definamos nosotros un atributo propio y hagamos uso de l con reflexin. Nuestro objetivo es perderle el
miedo a los atributos y entender qu es y qu no es un atributo.
Vamos a crear un atributo que permita marcar nuestras clases con informacin de ayuda que una aplicacin
podra usar a conveniencia. Nuestro atributo se llamar Ayuda y llevar un parmetro consistente en una
cadena con el texto de ayuda correspondiente. Empezamos por la definicin de una clase AyudaAttribute.
public class AyudaAttribute : Attribute
{
public AyudaAttribute(string texto)
{
TextoDeAyuda = texto;
}
public String TextoDeAyuda { get; private set; }
}

Heredamos de la clase System.Attribute, que es la clase base de todos los atributos, y definimos una clase
que mantiene una propiedad con el texto que suministramos en el constructor. El sufijo Attribute se usa en
la definicin de los atributos, pero no formar parte del nombre que usaremos al marcar clases. Podemos
usar ya el atributo en nuestras propias clases:
[Ayuda("Una clase que no hace gran cosa.")]
class MiClase
{
}

Al compilar el cdigo de MiClase, se aadir una instancia de AyudaAttribute al cdigo de la clase. Esa
instancia contendr el texto que hemos suministrado como argumento. Veamos ahora como, por reflexin,
podemos recuperar ese texto a partir de una instancia:
class Program
{
static void Main(string[] args)
{
MiClase mc = new MiClase();
Type mct = mc.GetType();
foreach (var a in mct.GetCustomAttributes(false))
{
AyudaAttribute ayuda = a as AyudaAttribute;
if (ayuda != null)

64

Buenas prcticas en desarrollo de software con .NET

{
Console.WriteLine("{0}: {1}", mct, ayuda.TextoDeAyuda);
}
}
Console.ReadKey();
}
}

La reflexin nos ha permitido obtener un listado de atributos de usuario con GetCustomAtributes y, a


travs de l, recuperar el texto de ayuda.
Se puede hablar mucho ms acerca de los atributos, pero con esto tenemos suficiente para lo que vamos a
necesitar en el resto del curso.

7 Testeo unitario con NUnit y TDD


Muchos proyectos de software fracasan sin llegar a ofrecer un producto que pueda pasar a explotacin.
Otros fracasan de un modo an ms estrepitoso: entregan algo que pasa a produccin y provocan una
sucesin de errores en explotacin. Estos errores se manifiestan de formas desastrosas y parecen
propagarse por el cdigo: cada vez que se cree haber dado con la clave de un error y se corrige, saltan
nuevos errores en otros mdulos del sistema. El parcheo rpido del cdigo no hace ms que empeorar la
calidad del resultado final y, finalmente, el cdigo parece pedir a gritos un rediseo completo. El horror.
Hay varios caminos que llevan a ese punto del desastre, pero el principal es el excesivo acoplamiento de los
mdulos (las interdependencias entre mdulos) y la ausencia de una sistemtica en las pruebas de que el
software sigue funcionando tras efectuar alguna modificacin en el cdigo. Los dos aspectos estn ms
relacionados de los que pueda parecer:

Si los mdulos son muy interdependientes, es imposible hacer pruebas sencillas que comprueben
que cada mdulo hace una cosa y la hace bien.
Si hubisemos diseado el cdigo con el objetivo de que fuera comprobable, hubisemos eliminado
las indeseables interdependencias desde el mismo principio.

Nadie discutir la afirmacin de que el software debe probarse antes de pasar a explotacin. Atendiendo a
cierta categorizacin, hay dos tipos de pruebas:

Pruebas de programador, orientadas a demostrar que el cdigo hace lo que el programador espera
que haga. Suelen orientarse a la verificacin de que ciertos mtodos, aisladamente, tienen el
comportamiento esperado. Se disean muchas veces para poner a prueba el cdigo desde el
conocimiento de sus entraas, as que las hacen programadores y pueden servir de documentacin
para otros programadores.
Pruebas de usuario o cliente, tambin denominadas pruebas de aceptacin, orientadas a demostrar
que el software hace lo que le cliente espera de l. Suelen orientarse a comprobar que clases o
interfaces completas hacen lo que se espera de ellas. No se centran en cmo se consigue un
resultado, sino en qu resultado se consigue. No tienen porqu escribirlas un programador y pueden
usarse por cualquiera en la cadena de desarrollo.

Hay varios tipos de pruebas a las que podemos someter el cdigo o la aplicacin para que vaya aumentando
nuestra confianza en su correccin:

Pruebas unitarias: cada mtodo (o unidad bsica) se prueba aisladamente.

65

Buenas prcticas en desarrollo de software con .NET

Pruebas de sistema o funcionales: se prueba el sistema completo para ver si satisface los
requerimientos. Dentro de esta categora encontramos:
o Pruebas exploratorias: buscan nuevos errores. Se imaginan escenarios que pudieran
provocar un fallo en el programa. Conducen al diseo de pruebas automatizables para evitar
que un bug corregido reaparezca: lo que denominamos pruebas de regresin.
o Pruebas de aceptacin (acceptance testing): verifican que el programa satisface los
requerimientos del cliente. Se escriben conjuntamente con el cliente, que suministra el
conocimiento propio del dominio.
o Pruebas de integracin: verifica que los componentes del sistema interaccionan
apropiadamente entre s.
o Pruebas de prestaciones (performance testing): comprueba el uso de recursos del programa
completo y mira cmo interacciona con los recursos desplegados en un entorno tan similar
como sea posible al entorno de explotacin. Hay diferentes tipos de prueba de prestaciones:
Pruebas de prestaciones propiamente dichas, que comprueba el uso de recursos
como la memoria, el tiempo, etctera, cuando la aplicacin se usa en condiciones
normales.
Pruebas de carga (load testing, o volume testing o endurance testing): lleva el
sistema a sus lmites, imponiendo cargas extremas pero posibles en un escenario de
explotacin.
Pruebas de estrs: lleva el sistema ms all de los lmites esperables con objeto de
estudiar la recuperabilidad o robustez del sistema. Se puede imponer la escritura de
un fichero de tamao superior a la capacidad de almacenamiento, o la atencin a un
nmero brutalmente alto de conexiones.

Aunque todos los tipos de prueba son importantes en un sistema real, nos vamos a centrar en el que ms
concierne a los programadores durante el desarrollo de unidades bsicas: las pruebas unitarias.
El testeo unitario es una de las prcticas que ayudan a desacoplar cdigo y, si se adopta en las fases de
diseo del sistema, conduce a diseos ms fciles de mantener, es decir, ms resistentes al cambio. La idea
de probar unidades elementales de cdigo no es en absoluto reciente. S lo es su adopcin generalizada por
parte de la comunidad de desarrolladores, con herramientas de uso comn como las que usaremos en el
curso y que automatizan el proceso de la ejecucin de las pruebas.
Las pruebas unitarias se componen de cdigo. Parece una obviedad, pero es extremadamente importante
que las pruebas estn bien diseadas. Entre las caractersticas de unas buenas pruebas unitarias y del
entorno de pruebas (adaptado de The Art of Unit Testing, de Roy Osherove) tenemos:

El proceso de ejecucin debe ser automatizable y repetible.


Deben ser fciles de implementar.
Una vez escritas, las pruebas deben acompaar al cdigo que ponen a prueba.
Deberan ser ejecutables por cualquiera.
Deberan ejecutarse por un procedimiento tan simple como pulsar un botn.
Deberan ejecutarse rpidamente.

66

Buenas prcticas en desarrollo de software con .NET

No vamos a engaar a nadie. Crear pruebas unitarias supone escribir ms cdigo; de hecho, mucho ms
cdigo. Pero ha de tenerse en cuenta que a la larga, compensa ese cdigo extra. Nos permite estar
razonablemente seguros de que el cdigo hace lo que se espera de l. Si un bug pasa inadvertido y se
detecta ms tarde, no slo se ha corregir: se ha disear una prueba unitaria que controle que ese bug no se
reproduce ms tarde. Con el tiempo, nuestro cdigo se va blindando y evita la reaparicin de errores ya
corregidos, pues el conjunto creciente de pruebas es un chivato automtico. Y si ms tarde (o durante el
propio desarrollo) es necesario refactorizar el cdigo, las pruebas ya escritas nos ayudarn a estar seguros de
que la funcionalidad alcanzada se mantiene en la nueva versin del cdigo.

7.1 xUnit
El testeo unitario es uno de los pilares de las metodologas giles y encontramos entornos de testeo unitario
en la prctica totalidad de los lenguajes de programacin de uso comn. Kent Beck, uno de los firmantes del
manifiesto gil, dise un entorno de testeo unitario para Smalltalk: SUnit. Hoy, el entorno de referencia es
jUnit, la versin para Java. La mayor parte de los lenguajes cuentan con alguna adaptacin de este entorno.
Estas adaptaciones se conocen globalmente por xUnit. La propia de .NET es NUnit (http://www.nunit.org/).
Microsoft dispone de un entorno propio para el testeo unitario, pero nosotros usaremos NUnit por varias
razones.

Es un estndar de facto.
Se puede usar con herramientas de desarrollo de cdigo abierto, como MonoDevelop.
Es un entorno xUnit y, por tanto, su aprendizaje nos abre la puerta a entornos similares en otros
lenguajes de programacin.

7.2 Instalacin de NUnit


NUnit va por la versin 2.5.9 para uso en produccin. En http://www.nunit.org/index.php?p=download hay
un paquete de instalacin para MS Windows. Lo primero es, obviamente, descargarlo e instalarlo. Al
ejecutar el fichero msi, bastar con aceptar la licencia y escoger la instalacin tpica para que se instalen el
ejecutable que permite controlar la ejecucin de pruebas desde fuera del entorno Visual Studio (el test
runner) y los ensamblados pertinentes en el GAC. Tpicamente, los ficheros del paquete se instalarn en
C:\Program Files (x86)\NUnit 2.5.9.
Alternativamente se puede descargar el fichero comprimido, que obliga a una instalacin manual de los
ejecutables.

67

Buenas prcticas en desarrollo de software con .NET

7.3 Primeros pasos


Vamos a empezar usando NUnit para construir una batera de pruebas unitarias sobre un cdigo ya
existente.
Creamos un proyecto de tipo Class Library con el ttulo Banca. Esta librera contendr un fichero
CuentaCorriente.cs con este cdigo:
using System;
namespace Banca
{
public class CuentaCorriente
{
public Decimal Saldo { get; private set; }
public CuentaCorriente(Decimal saldo = 0M)
{
Saldo = saldo;
}
public void Abono(Decimal cantidad)
{
Saldo += cantidad;
}
public void Adeudo(Decimal cantidad)
{
Saldo -= cantidad;
}

public void Transferencia(CuentaCorriente destino, Decimal cantidad)


{
}
}
}

El mtodo Transferencia no contiene cdigo alguno, as que no realiza ninguna tarea. La clase, tal cual est
definida, contiene un error.
7.3.1 Un accesorio de pruebas
Vamos a definir las pruebas. Aunque lo normal es crear un proyecto independiente con las pruebas unitarias,
crearemos las pruebas en el mismo ensamblado. Hemos de incorporar las referencias a las libreras propias
de NUnit. En el Solution Explorer abrimos la carpeta Banca y, dentro de ella, la carpeta References. Con el
botn derecho sobre la carpeta, elegimos Add Reference en el men contextual. Se abre un cuadro de
dilogo con varias pestaas y elegimos la pestaa .NET (si hicimos la instalacin automtica; si no, hemos de
buscar la librera con Browse):

68

Buenas prcticas en desarrollo de software con .NET

Seleccionamos nunit.framework de la lista de componentes y pulsamos el botn Ok. Aadimos ahora un


fichero TestCuentaCorriente.cs al proyecto Banca y tecleamos en l este contenido.
using NUnit.Framework;
namespace Banca
{
[TestFixture]
class TestCuentaCorriente
{
[Test]
public void AbonoDeDinero_AumentaSaldo()
{
CuentaCorriente cuentaCorriente = new CuentaCorriente();
cuentaCorriente.Abono(100M);
Assert.AreEqual(100M, cuentaCorriente.Saldo);
}
}
}

Hemos usado dos atributos, uno para marcar la clase TestCuentaCorriente y otro para marcar su nico
mtodo. Estos atributos estn definidos en nunit.framework. TestFixture se puede traducir por
Accesorio para Pruebas. El atributo la declara como clase con cdigo de pruebas. El atributo Test marca al
mtodo AbonoDeDinero_AumentaSaldo como una prueba unitaria. El mtodo crea una cuenta corriente
(cuyo saldo inicial debe ser 0); efecta un abono de la 100 euros y pasa a comprobar que, despus, el saldo
es de 100 euros. La comprobacin se hace con un mtodo auxiliar:
Assert.AreEqual(100M, cuentaCorriente.Saldo);

Assert es una clase con mtodos estticos que permiten hacer diferentes comprobaciones. En este caso,

que dos cantidades son iguales. La primera cantidad es el valor esperado y la segunda, el obtenido
realmente.
Una prueba unitaria, siguiendo una definicin de libro, es una pieza de cdigo automatizado que invoca un
mtodo de la clase bajo prueba y comprueba que se observan ciertas asunciones sobre el comportamiento
lgico de ese mtodo o de la clase.
Pues ya est. Hemos definido nuestra primera prueba unitaria.

69

Buenas prcticas en desarrollo de software con .NET

7.3.2 Ejecucin de las pruebas


Ejecutamos ahora NUnit.exe, la herramienta de ejecucin de pruebas. Para hacerlo, vamos al men de inicio
del sistema, buscamos en Todos los programas y, en la carpeta NUnit 2.5.9 encontraremos el programa
NUnit, que hemos de iniciar (de nuevo, esto es as si hemos hecho la instalacin automtica; si no, hemos de
buscar manualmente el ejecutable en el resultado de descomprimir el fichero zip). La interfaz grfica
presenta este aspecto:

Seleccionamos File Open Project y vamos a la carpeta del proyecto Banca. Dentro encontraremos la
carpeta bin, que a su vez contiene la carpeta Debug. Ah est Banca.dll, el ensamblado que contiene el
cdigo de la clase que vamos a poner a prueba y las pruebas. Por cierto, aquello que sometemos a prueba
recibe el nombre de SUT, por System Under Test. Tras la carga, la interfaz pasa a tener este aspecto:

La herramienta ha explorado el ensamblado y ha encontrado la clase TestCuentaCorrienta y, dentro de


ella, el mtodo AbonoDeDinero_AumentaSaldo. Pulsamos el botn Run y la interfaz pasa a este otro estado:

70

Buenas prcticas en desarrollo de software con .NET

NUnit nos informa de que ha ejecutado todos los test (bueno, el test, que slo hay uno) y que todos se han
superado. Perfecto.
Aadamos una prueba para el mtodo Transferencia:
using NUnit.Framework;
namespace Banca
{
[TestFixture]
class TestCuentaCorriente
{
[Test]
public void AbonoDeDinero_AumentaSaldo()
{
CuentaCorriente cuentaCorriente = new CuentaCorriente();
cuentaCorriente.Abono(100M);
Assert.AreEqual(cuentaCorriente.Saldo, 100M);
}
[Test]
public void Transferencia_DisminuyeSaldoOrigenAumentaSaldoDestino()
{
CuentaCorriente origen = new CuentaCorriente(500M);
CuentaCorriente destino = new CuentaCorriente();
origen.Transferencia(destino, 100M);
Assert.AreEqual(400M, origen.Saldo);
Assert.AreEqual(100M, destino.Saldo);
}
}
}

Tan pronto reconstruimos (compilamos) el proyecto, NUnit recarga el ensamblado automticamente:

71

Buenas prcticas en desarrollo de software con .NET

Ejecutemos las pruebas pulsando el botn Run. Este es el resultado:

La barra roja nos indica que no todas las pruebas se han superado. No slo sabemos que hay un fallo; NUnit
nos da detalles acerca de lo ocurrido:
Banca.TestCuentaCorriente.Transferencia_DisminuyeSaldoOrigenAumentaSaldoDestino:
Expected: 400m
But was: 500m

Nos indica que se esperaba un valor de 400M donde ha encontrado el valor 500M. El fallo nos lo
esperbamos. Vamos a corregirlo escribiendo el cdigo que hubisemos podido escribir inicialmente:
public void Transferencia(CuentaCorriente destino, Decimal cantidad)
{
this.Adeudo(cantidad);
destino.Abono(cantidad);
}

Ejecutamos nuevamente las pruebas:

72

Buenas prcticas en desarrollo de software con .NET

Perfecto. Todas las pruebas superadas con xito. Las pruebas que hemos diseado son incompletas: no
repasan todos los mtodos de la clase CuentaCorriente. Pensemos qu pruebas podemos disear para
fiscalizar an ms el cdigo:

La cuenta corriente debe tener saldo cero si invocamos el constructor sin parmetros.
La cuenta corriente debe tener un saldo igual a la cantidad que suministramos al invocar el
constructor.
El mtodo Adeudo debe sustraer del saldo la cantidad que se suministra como argumento.
using NUnit.Framework;
namespace Banca
{
[TestFixture]
class TestCuentaCorriente
{
[Test]
public void ConstructorSinParmetros_SaldoCero()
{
CuentaCorriente cuentaCorriente = new CuentaCorriente();
Assert.AreEqual(0M, cuentaCorriente.Saldo);
}
[Test]
public void ConstructorConCantidad_SaldoIgualACantidad()
{
CuentaCorriente cuentaCorriente = new CuentaCorriente(100M);
Assert.AreEqual(100M, cuentaCorriente.Saldo);
}
[Test]
public void AbonoDeDinero_AumentaSaldo()
{
CuentaCorriente cuentaCorriente = new CuentaCorriente();
cuentaCorriente.Abono(100M);
Assert.AreEqual(100M, cuentaCorriente.Saldo);
}
[Test]
public void AdeudoDeDinero_DisminuyeSaldo()
{
CuentaCorriente cuentaCorriente = new CuentaCorriente(300M);
cuentaCorriente.Adeudo(100M);
Assert.AreEqual(200M, cuentaCorriente.Saldo);

73

Buenas prcticas en desarrollo de software con .NET

}
[Test]
public void Transferencia_DisminuyeSaldoOrigenAumentaSaldoDestino()
{
CuentaCorriente origen = new CuentaCorriente(500M);
CuentaCorriente destino = new CuentaCorriente();
origen.Transferencia(destino, 100M);
Assert.AreEqual(400M, origen.Saldo);
Assert.AreEqual(100M, destino.Saldo);
}
}
}

Y ya sabemos qu hacer: compilar y ejecutar las pruebas. Todo correcto:

Hay una ltima cuestin sobre la que conviene que nos detengamos brevemente. El mtodo que pone a
prueba la Transferencia hace dos comprobaciones: que el saldo origen aumenta y que el saldo destino
disminuye. Ya hemos visto que, cuando no haba cdigo en el mtodo, la prueba fallaba. Pero slo fallaba la
primera de las aserciones: la segunda no se llegaba a ejecutar. Es preferible que cada prueba ejecute un solo
aserto (aunque ojo con los dogmatismos). Es decir, el mtodo de prueba
Transferencia_DisminuyeSaldoOrigenAumentaSaldoDestino debera dividirse en estos otros:
[Test]
public void Transferencia_DisminuyeSaldoOrigen()
{
CuentaCorriente origen = new CuentaCorriente(500M);
CuentaCorriente destino = new CuentaCorriente();
origen.Transferencia(destino, 100M);
Assert.AreEqual(400M, origen.Saldo);
}
[Test]
public void Transferencia_AumentaSaldoDestino()
{
CuentaCorriente origen = new CuentaCorriente(500M);
CuentaCorriente destino = new CuentaCorriente();
origen.Transferencia(destino, 100M);
Assert.AreEqual(100M, destino.Saldo);
}

Es pronto, de todos modos, para que entremos en estas cuestiones de estilo.

74

Buenas prcticas en desarrollo de software con .NET

7.4 La clase Assert


Las pruebas unitarias tienen tres pasos:

Preparar un contexto o escenario en el que se ejecuta la prueba.


Ejecucin del clculo que ejercita la unidad que deseamos probar.
Comprobacin de que el resultado es el esperado.

El tercer paso consiste, tpicamente, en la ejecucin de un mtodo de la clase esttica Assert. Hay muchos
mtodos que permiten expresar diferentes comprobaciones. Es importante escoger el mtodo apropiado
para que el cdigo sea legible y para que el resultado de las comprobaciones sea expresivo.
La mayor parte de los mtodos que mostramos ahora admiten parmetros opcionales:

Una cadena con mensaje que se mostrar cuando la asercin falle.


Una cadena de formato y un nmero arbitrario de objetos. La cadena se interpolar y mostrar
cuando falle la asercin.

7.4.1

IsTrue/IsFalse

7.4.2

AreEqual/AreNotEqual/AreSame/AreNotSame
AreEqual(T exp1, T exp2), donde T es int, uint, decimal, float, double u object: comprueba
si las dos expresiones se evalan al mismo valor.
AreNotEqual(T exp1, T exp2), donde T es int, uint, decimal, float, double u object:
comprueba si las dos expresiones se evalan con valores diferentes.
AreSame(object o1, object o2): comrpueba si o1 y o2 son referencias al mismo objeto.
AreNotSame(object o1, object o2): comrpueba si o1 y o2 son referencias objetos distintos.

7.4.3

Assert.IsTrue(boolean expression) : comprueba que la expresin se evala a true.


Assert.IsFalse(boolean expression): comprueba que la expresin se evala a false.

Greater/Less
Greater(T exp1, T exp2), donde T es int, uint, decimal, float, double o IComparable:

comprueba si exp1 > exp2.


Less(T exp1, T exp2), donde T es int, uint, decimal, float, double o IComparable:
comprueba si exp1 < exp2.

7.4.4

Contains

7.4.5

IsInstanceOfType/IsNotInstanceOfType
IsInstanceOfType(Type t, object o): comprueba que o es una instancia de t.
IsNotInstanceOfType(Type t, object o): comprueba que o no es una instancia de t.

7.4.6

IsAsssignableFrom/IsNotAssignableFrom
IsAsssignableFrom(Type t, object o): comprueba que o se puede asignar a una variable de
tipo t.
IsNotAssignableFrom(Type t, object o): lo contrario.

7.4.7

Contains(object o, IList col): comprueba si el objeto o forma parte de la lista (o vector) col.

IsNull/IsNotNull
IsNull(object o): comprueba que o es null.

75

Buenas prcticas en desarrollo de software con .NET

IsNotNull(object o): comprueba que o no es null.

7.4.8

IsNaN

7.4.9

Para cadenas

IsNan(double exp): comprueba exp proporciona el valor NaN.

IsEmpty(string s): comprueba que s es la cadena vaca.


IsNotEmpty(string s): comprueba que s no es la cadena vaca.
Contains(string expected, string s): comprueba si expected forma parte de s.
StartsWith(string expected, string s): comprueba si expected es prefijo de s.
EndsWith(string expected, string s): comprueba si expected es sufijo de s.
AreEqualIgnoringCase(string expected, string s): comprueba que expected y s son iguales

si no tenemos en cuenta la caja.


7.4.10 Utilidades
Hay un par de utilidades que permiten controlar directamente el resultado de una prueba. Suelen usarse en
depuracin o al construir funciones con comprobaciones complejas que deben usarse en ms de un lugar.

Fail(): provoca un fallo.


Ignore(): hace que se ignore la prueba.

7.5 Atributos
Hemos vistos que las clases y mtodos que conforman nuestras pruebas unitarias se marcan con atributos.
Relacionamos aqu los atributos definidos en NUnit.
7.5.1 [TestFixture] clase
Marca la clase que contiene pruebas unitarias. La clase marcada debe ser pblica y tener un constructor por
defecto que no tenga efectos secundarios, pues la clase puede instanciarse varias veces al ejecutar las
pruebas.
7.5.2 [Test] mtodo
Marca un mtodo como mtodo de prueba. El mtodo no debe devolver valor alguno ni tener parmetros.
7.5.3 [Setup] mtodo
Marca un mtodo como preparatorio del escenario en el que se deben ejecutar todos los mtodos marcados
con [Test] de una clase marcada con [TestFixture]. El mtodo se invoca antes de cada llamada a un
mtodo marcado con [Test]. La clase marcada con [TestFixture] slo puede tener un mtodo marcado
con [Setup].
Si se definen clases con herencia, el atributo [Setup] se hereda.
7.5.4 [Teardown] mtodo
Marca un mtodo como encargado de liberar recursos reservados por un mtodo marcado por [Setup]. Se
invoca al final de la ejecucin de cada mtodo marcado con [Test]. Slo puede haber uno por clase.
Si se definen clases con herencia, el atributo [Teardown] se hereda.
7.5.5 [TestFixtureSetup] mtodo
Mtodo que se ejecuta una vez antes de ejecutar todos los mtodos marcados con [Test].

76

Buenas prcticas en desarrollo de software con .NET

7.5.6 [TestFixtureTeardown] mtodo


Mtodo que se ejecuta una vez despus de haber ejecutado todos los mtodos marcados con [Test].
7.5.7 [ExpectedException(Type type)] mtodo
Marca que se puede aadir a un mtodo marcado con [Test]. Hace que se considere vlido el lanzamiento
de una excepcin de tipo type. Si la excepcin no se lanza, se considera que la prueba ha fracasado.
Alternativamente, el tipo de excepcin esperado se puede expresar con una cadena y el identificador
(completo) del tipo de la excepcin.
7.5.8 [Platform(string s)] clase o mtodo
Permite condicionar la clase o mtodo a la ejecucin en una determinada plataforma. Se reconocen (al
menos) estas plataformas:
Win, Win32, Win32S, Win32Windows, Win32NT, WinCE, Win95, Win98, WinMe, NT3, NT4, NT5, Win2K,
WinXP, Win2003Server, Unix, Linux, Net, Net-1.0, Net-1.1, Net-2.0, NetCF, SSCLI, Rotor, Mono.
Ejemplos de plataforma:

"NET-2.0": .NET 2.0


"WinME": Windows ME
"Win98,WinME": Windows 98 o WindowsME

7.5.9 [Category(string s)] clase o mtodo


Permite marcar una clase o mtodo con una cadena para ejecutar pruebas unitarias selectivamente. Si un
mtodo de prueba es extremadamente lento, se puede marcar, por ejemplo, con lento. Los entornos de
ejecucin de pruebas permiten indicar si los mtodos marcados con una cadena han de ejecutarse o no.
7.5.10 [Explicit] clase o mtodo
Marca una clase o mtodo para que no se ejecute si no se selecciona explcitamente en el entorno de
ejecucin de pruebas.
7.5.11 [Ignore] clase o mtodo
La prueba se ignora. Se usa para deshabilitar temporalmente una clase o mtodo.
7.5.12 [Combinatorial] mtodo y [Values] o [Range] parmetro
Permite generar automticamente un test para cada combinacin de valores. Lo mejor es ver un ejemplo.
Este marcado:
[Test, Combinatorial]
public void MyTest(
[Values(1, 2, 3)] int x,
[Values("A", "B")] string s)
{
...
}

genera seis pruebas unitarias, una por cada combinacin de los valores posibles para x (1, 2 y 3) con los
valores posibles para s ("A" y "B").
En lugar de valores puntuales se puede especificar un rango de valores con [Range]. Este marcado

77

Buenas prcticas en desarrollo de software con .NET

[Test, Combinatorial]
public void MyTest(
[Range(1, 7, 2)] int x,
[Values("A", "B")] string s)
{
...
}

genera una prueba por cada combinacin de (1, 3, 5, 7) con ("A" y "B").
7.5.13 [Sequential] mtodo y [Values] o [Range] parmetro
Permite generar automticamente un test para cada par de valores. Este marcado:
[Test, Sequential]
public void MyTest(
[Values(1,2,3)] int x,
[Values("A","B")] string s)
{
...
}

genera tres pruebas unitarias, una con el par de valores (1, "A"), otra con (2, "B") y otra con (3, null).
7.5.14 [Timeout(int ms)] mtodo
Fija un tiempo mximo de ejecucin para una prueba (en milisegundos). Si se excede ese tiempo, la prueba
se cancela y se considera fallida.
7.5.15 [Maxtime(int ms)] mtodo
Fija un tiempo mximo de ejecucin para una prueba (en milisegundos). Si se excede ese tiempo, la prueba
se considera fallida (pero la prueba no se cancela antes de terminar).

7.6 Desarrollo guiado por pruebas (Test Driven Development)


En el ejemplo que hemos desarrollado partamos de cdigo ya existente y hemos diseado unas pruebas
unitarias. Hemos seguido un proceso que podemos representar grficamente as:

Escribir cdigo

Escribir
pruebas

Ejecutar
pruebas

Corregir
cdigo

Hay un problema con seguir esta aproximacin: muchas veces el desarrollador no tiene tiempo para los
pasos segundo y tercero. Al tener cdigo que parece funcionar, descarta estas etapas y se concentra en
escribir ms cdigo funcional. El resultado es que nunca se tiene tiempo para escribir las pruebas o las
pruebas cubren insuficientemente el cdigo creado.
Paremos un momento para reparar en que, en nuestro ejemplo, tambin hemos diseado unas pruebas
unitarias para cdigo que an no exista: el mtodo Transferencia fue probado antes de escribir su cuerpo.
Esta prctica de escribir primero las pruebas y despus el cdigo encaja en una metodologa de desarrollo
conocida por Desarrollo guiado por pruebas o, mejor, TDD, que son las siglas de Test Driven Development.
Una de las ideas bsicas del TDD es que un conjunto de pruebas bien planteado constituye una buena
especificacin de requisitos para una clase o mtodo. Otra, que al disear las pruebas antes que el cdigo
proponemos patrones de uso del cdigo que ayudan a disear el propio cdigo.
El proceso de desarrollo TDD es algo ms complejo:

78

Buenas prcticas en desarrollo de software con .NET

Escribir una
prueba

Lograr que las


pruebas se
superen
escribiendo
cdigo de
produccin

Ejecutar todas
las pruebas

Ejecutar todas
las pruebas

Corregir
errores

No

Se superan
todas las
pruebas?

Refactorizar el
cdigo
Si

Si
No

Refactorizar?

Ntese que cada ciclo pasa por el diseo de una prueba para cdigo de produccin inexistente. Si las
pruebas no se superan, nuestra misin es corregir el cdigo para llegar a verde, esto es, para superarlas.
Puede llamar la atencin la pregunta que nos hacemos cuando las pruebas se superan: Refactorizar?.
Refactorizar es modificar el cdigo con objeto de mejorarlo. La refactorizacin parte de cdigo que funciona
correctamente (hasta donde podemos tener una garanta porque supera las pruebas) y se hace con red:
las propias pruebas, que deben seguir superndose tras las refactorizacin.
En cualquier caso, debe tenerse en cuenta que TDD no es la bala de plata para el diseo de software. Es
una metodologa que, usada sensatamente, es de ayuda. Como siempre, los planteamientos dogmticos son
no conducen a nada bueno en el desarrollo de software.

7.7 Ejecucin de pruebas


Ya hemos visto cmo ejecutar pruebas con una interfaz grfica de usuario. En los sistemas con integracin
continua es importante que la ejecucin de pruebas sea automatizable. NUnit ofrece un entorno de
ejecucin de pruebas como programa de consola. El programa es nunit-console.exe y acepta como
parmetro la DLL con las pruebas (o un csproj con el proyecto Visual Studio o un nunit con un proyecto
NUnit). La ejecucin de las pruebas muestra el resultado por la salida estndar y deja un fichero XML que
puede analizarse con las herramientas apropiadas para saber si el sistema supera las pruebas en un instante
dado.
El programa nunit-console presenta varias opciones. Pueden consultarse en la pgina del manual de NUnit:
http://www.nunit.org/index.php?p=consoleCommandLine&r=2.2.8.

7.8 Una buena prctica: nombrado de los mtodos de prueba


La escritura de buenas pruebas unitarias es clave para que se justifique el tiempo invertido en ellas. Una de
las cuestiones que deben preocuparnos es la legibilidad de los mensajes cuando una prueba falla. Hemos
visto que los mtodos de Assert admiten un parmetro con un mensaje. Este mensaje se muestra cuando
falla la asercin. La escritura de un mensaje en cada asercin es aburrida, por lo que es fcil que contengan
texto pobre o, directamente, que el programador pase de escribirlo. Hay una alternativa mucho mejor: ser
sistemtico en el nombrado de los mtodos de prueba procurando que su lectura aclare qu unidad ha

79

Buenas prcticas en desarrollo de software con .NET

fallado y por qu ha fallado. Dado que al fallar una prueba se muestra el nombre del mtodo de dicha
prueba, un nombre bien diseado hace que el mensaje de aviso estndar sea muy aclaratorio de lo sucedido.
Siguiendo los consejos de The Art of Unit Testing, los nombres de las pruebas unitarias deberan:

Ser un frase compuesta por tres elementos (separados por el carcter de subrayado):
o El nombre del mtodo puesto a prueba.
o El contexto en el que se pone a prueba el mtodo.
o El resultado esperado de la prueba.

Por ejemplo: IsValidFileName_ConFicheroVlido_DevuelveTrue nos indica que estamos poniendo a


prueba un mtodo llamado IsValidFileName proporcionando un fichero vlido y esperando que el mtodo
devuelva el valor true.

7.9 Un ejemplo de TDD con NUnit


Vamos a desarrollar un pequeo ejemplo de desarrollo de una librera con TDD: una clase que nos facilite un
mtodo para transcriba nmeros escritos con cifras con su expresin escrita con letras. Si el mtodo recibe,
por ejemplo, la cadena 17, devolver diecisiete y si recibe la cadena 32, devolver treinta y dos.
Aceptaremos cualquier nmero de hasta seis cifras.
Cree un proyecto llamado TranscriptorDeNmeros con la plantilla de Class Library. No modifiques nada en
ese proyecto y crea otro proyect: TestTranscriptorDeNmeros. Este segundo proyecto tambin es una Class
Library. Aadimos a sus referencias la librera nunit.framework.
Sigamos la ortodoxia en TDD. Empecemos por proponer un caso de transcripcin definiendo una entrada y
su salida. Ante la entrada 0, el transcriptor devolver cero. Empezamos creando la prueba unitaria. En el
proyecto TestTranscriptorDeNmeros editamos el fichero Class1.cs (que se ha creado por defecto al
aadir el proyecto) para que quede as:
using NUnit.Framework;
using TranscriptorDeNmeros;
namespace TestTranscriptorDeNmeros
{
[TestFixture]
public class TestTranscriptorDeNmeros
{
[Test]
public void Transcribe_0_ReturnsCero()
{
Transcriptor transcriptor = new Transcriptor ();
string result = transcriptor.Transcribe(0);
string expected = "cero";
Assert.AreEqual(expected, result);
}
}
}

(Aunque no es estrictamente necesario, renombre tambin el fichero Class1.cs para que se llame
TestTranscriptor.cs.)
Hemos usado una clase Transcriptor que ni siquiera hemos definido. Evidentemente, no podemos ni
compilar el proyecto. Definamos ahora la clase con lo mnimo necesario para que supere esta prueba. En el
proyecto TranscriptorDeNmeros definimos la clase Transcriptor y definimos el mtodo Transcribe as:

80

Buenas prcticas en desarrollo de software con .NET

namespace TranscriptorDeNmeros
{
public class Transcriptor
{
public string Transcribe(int nmero)
{
return "cero";
}
}
}

El mtodo se limita a devolver la cadena cero sin tener en cuenta el valor del parmetro entero que se le
suministra. Es la lgica mnima que nos permite superar el test.
Ahora hemos de aadir a TestTranscriptorDeNmeros una referencia al proyecto TranscriptorDeNmeros y
compilamos. Iniciamos la interfaz grfica de NUnit y abrimos la DLL TestTranscriptorDeNmeros.dll.
Pulsamos en Run y verde!

Hemos superado nuestra primera ejecucin de pruebas haciendo lo mnimo necesario. Aadamos una nueva
prueba:
using NUnit.Framework;
using TranscriptorDeNmeros;
namespace TestTranscriptorDeNmeros
{
[TestFixture]
public class TestTranscriptorDeNmeros
{
[Test]
public void Transcribe_0_ReturnsCero()
{
Transcriptor transcriptor = new Transcriptor();
string result = transcriptor.Transcribe(0);
string expected = "cero";
Assert.AreEqual(expected, result);
}
[Test]
public void Transcribe_1_ReturnsUno()
{
Transcriptor transcriptor = new Transcriptor();
string result = transcriptor.Transcribe(1);
string expected = "uno";

81

Buenas prcticas en desarrollo de software con .NET

Assert.AreEqual(expected, result);
}
}
}

Y al ejecutar las pruebas pasamos a rojo:

Nuestro objetivo es pasar a verde haciendo el cambio mnimo necesario para Transcribe, que en nuestra
opinin es ste:
namespace TranscriptorDeNmeros
{
public class Transcriptor
{
public string Transcribe(int nmero)
{
if (nmero == 0)
{
return "cero";
}
else
{
return "uno";
}
}
}
}

Obviamente, el transcriptor no hace lo que har finalmente. Hace lo mnimo para superar las pruebas
unitarias existentes en este instante. Nuestro objetivo es que el desarrollo se gue por las pruebas para estar
seguros de que nuestra clase y sus pruebas estn sincronizadas. Ejecutemos las pruebas y comprobaremos
que estamos en verde:

82

Buenas prcticas en desarrollo de software con .NET

Esto significa que estamos en condiciones de aadir funcionalidad. Pero antes, refactoricemos nuestra clase
pruebas: los dos mtodos de prueba (y cualquiera que hagamos a partir de ahora) necesita un transcriptor,
as que podemos crearlo en un mtodo de Setup:
namespace TestTranscriptorDeNmeros
{
[TestFixture]
public class TestTranscriptorDeNmeros
{
Transcriptor transcriptor;
[SetUp]
public void CreateTranscriptor()
{
transcriptor = new Transcriptor();
}
[Test]
public void Transcribe_0_ReturnsCero()
{
string result = transcriptor.Transcribe(0);
string expected = "cero";
Assert.AreEqual(expected, result);
}
[Test]
public void Transcribe_1_ReturnsUno()
{
string result = transcriptor.Transcribe(1);
string expected = "uno";
Assert.AreEqual(expected, result);
}
}
}

Aadamos pruebas unitarias para todos los nmeros entre 0 y 9 y hagamos que Transcribe las supere. Las
pruebas tienen este aspecto:
using NUnit.Framework;
using TranscriptorDeNmeros;
namespace TestTranscriptorDeNmeros
{
[TestFixture]

83

Buenas prcticas en desarrollo de software con .NET

public class TestTranscriptorDeNmeros


{
Transcriptor transcriptor;
[SetUp]
public void CreateTranscriptor()
{
transcriptor = new Transcriptor();
}
[Test]
public void Transcribe_0_ReturnsCero()
{
string result = transcriptor.Transcribe(0);
string expected = "cero";
Assert.AreEqual(expected, result);
}

[Test]
public void Transcribe_9_ReturnsNueve()
{
string result = transcriptor.Transcribe(9);
string expected = "nueve";
Assert.AreEqual(expected, result);
}
}
}

Y el cdigo que las permite supera, ste otro:


namespace TranscriptorDeNmeros
{
public class Transcriptor
{
public string Transcribe(int nmero)
{
switch (nmero)
{
case 0:
{
return "cero";
}
case 1:
{
return "uno";
}
case 2:
{
return "dos";
}
case 3:
{
return "tres";
}
case 4:
{
return "cuatro";
}
case 5:
{
return "cinco";
}
case 6:

84

Buenas prcticas en desarrollo de software con .NET

{
return "seis";
}
case 7:
{
return "siete";
}
case 8:
{
return "ocho";
}
case 9:
{
return "nueve";
}
default:
{
return "";
}
}
}
}
}

Ejecutemos las pruebas y verde!


Procede una crtica: estamos escribiendo mucho cdigo para algo que, en principio, no es muy complicado.
Por una parte, estamos tratando de ser muy didcticos e ir paso a paso. Lo sustancial es que cada cosa que
hemos hecho ha venido determinada por un cdigo que pona a prueba una funcionalidad an antes de que
sta existiera. Al implementarla, hemos ido sobre seguro: podamos comprobar que todo funcionaba porque
la prueba ya se haba escrito. Con todo, alguien podra ver excesivo que hay diez funciones de prueba, una
para nmero de una cifra, y podra proponer este otro mtodo de prueba que, en principio, hace lo mismo
que los otros diez:
[Test]
public void Trancribe_0To9_ReturnsCeroToNueve()
{
var expected = new [] {"cero", "uno", "dos", "tres", "cuatro",
"cinco", "seis", "siete", "ocho", "nueve"};
for (int i = 0; i < 10; i++)
{
string result = transcriptor.Transcribe(i);
Assert.AreEqual(expected[i], result);
}
}

Es mucho menos cdigo, s, pero no hace exactamente lo mismo que el conjunto de las diez pruebas
anteriores: si el transcriptor fallara, pongamos por caso, al transcribir el nmero 5, la nueva rutina de prueba
se detendra con un mensaje similar a ste:
TestTranscriptorDeNmeros.TestTranscriptorDeNmeros.Trancribe_0To9_ReturnsCeroToNueve:
Expected string length 5 but was 8. Strings differ at index 0.
Expected: "cinco"
But was: "loquesea"
--------------^

Dnde est el problema, pues? En que al detenerse ante este fallo, no se pone a prueba la correccin para
los nmeros 6 a 9. Cada ejecucin de la batera de pruebas debera reportar todo lo que falla, no detenerse
ante un fallo cualquiera.

85

Buenas prcticas en desarrollo de software con .NET

Pensemos en los siguientes diez nmeros que debemos aprender a transcribir: los siete primeros son
especiales porque no siguen ninguna norma. Diez, once, doce, trece, catorce, quince y diecisis deberan
traducirse directamente. Aadimos las siete nuevas pruebas (y omitimos aqu el cdigo correspondiente).
Hemos de aadir el cdigo que permita superar las pruebas, pero empezamos a estar cansados de aadir
elementos al switch de Transcribe y decidimos cambiar la implementacin del cdigo por esta otra:
namespace TranscriptorDeNmeros
{
public class Transcriptor
{
readonly string[] literals =
new[] {"cero", "uno", "dos", "tres", "cuatro",
"cinco", "seis", "siete", "ocho", "nueve",
"diez", "once", "doce", "trece", "catorce",
"quince", "diecisis"};
public string Transcribe(int nmero)
{
if (nmero <= 16)
{
return literals[nmero];
}
else
{
return "";
}
}
}
}

Ejecutamos las pruebas y verde!

Ya empezamos a poder apreciar las ventajas de disponer de una batera de pruebas: hemos cambiado
radicalmente la implementacin, pero hemos hecho un salto con red. Las pruebas nos permite saber que le
cambio no ha afectado a la funcionalidad que ya habamos alcanzado.
Hasta aqu hemos ido paso a paso, aadiendo una prueba por nmero. Esta estrategia no puede seguirse
indefinidamente. No haremos mil pruebas para probar las transcripciones del 0 al 999.
A partir del nmero 17 (inclusive), las cosas empiezan a presentar regularidades y algunas excepciones:

86

Buenas prcticas en desarrollo de software con .NET

1. Los nmeros del 16 al 19 se construyen con dieci seguido (como palabra nica) de la transcripcin
de la cifra de unidades (diecisiete, dieciocho y diecinueve).
2. El nmero 20 se transcribe como veinte.
3. Los nmeros del 21 lal 29, excepto el 22, el 23 y el 26, se forman con veinte seguido de la
transcripcin de la cifra de unidades.
4. El nmero 22 se transcribe como veintids, el 23 como veintitrs y el 26 como veintisis.
5. Los nmeros del 30 al 100 divisibles por 10 se transcriben como treinta, cuarenta, cincuenta,
sesenta, setenta, ochenta, noventa y cien.
6. Los nmeros del 31 al 99, ambos inclusive, que no son mltiplos de 10 se transcriben con una
cadena de la forma X y Y, donde X es

, siendo

el nmero, y donde Y es

Por ejemplo 39 se transcribe como treinta y nueve.


7. Los nmeros del 101 al 199 se transcriben como ciento X, donde X es la transcripcin de
.
8. Los nmeros del 200 al 900 mltiplos de 100 se transcriben con doscientos, trescientos,
cuatrocientos, quinientos, seiscientos, setecientos, ochocientos y novecientos.
9. Los nmeros del 200 al 999 que no son mltiplos de 100 se transcriben con una cadena de la forma
X Y, donde X es la transcripcin de

e Y es la transcripcin de

10. El nmero 1.000 se transcribe como mil.


11. Los nmeros de 2.000 a 999.999 se transcriben con una cadena de la forma X mil Y, donde X es la
transcripcin de

e Y es la transcripcin de

Y nos detendremos aqu (el resto lo dejaremos como ejercicio).


Vamos a por la primera regla. Son tres casos. Vale la pena que los cubramos con tres pruebas concretas.
Aadimos estas pruebas:
[Test]
public void Transcribe_17_ReturnsDiecisiete()
{
string result = transcriptor.Transcribe(17);
string expected = "diecisiete";
Assert.AreEqual(expected, result);
}
[Test]
public void Transcribe_18_ReturnsDieciocho()
{
string result = transcriptor.Transcribe(18);
string expected = "dieciocho";
Assert.AreEqual(expected, result);
}
[Test]
public void Transcribe_19_ReturnsDiecinueve()
{
string result = transcriptor.Transcribe(19);
string expected = "diecinueve";
Assert.AreEqual(expected, result);
}

Ejecutamos y rojo!

87

Buenas prcticas en desarrollo de software con .NET

La verdad es que da pereza crear lgica ms o menos compleja para tres casos particulares. Los
abordaremos como todos los anteriores:
namespace TranscriptorDeNmeros
{
public class Transcriptor
{
readonly string[] literals =
new[] {"cero", "uno", "dos", "tres", "cuatro",
"cinco", "seis", "siete", "ocho", "nueve",
"diez", "once", "doce", "trece", "catorce",
"quince", "diecisis", "diecisiete", "dieciocho", "diecinueve"};
public string Transcribe(int nmero)
{
if (nmero <= 19)
{
return literals[nmero];
}
else
{
return "";
}
}
}
}

Ya pasamos las pruebas. El nmero 20 es tan especial como los anteriores: escribimos su prueba, falla al
ejecucin y escribimos el cdigo que permite superar la prueba.
Por fin vamos a enfrentarnos a cdigo interesante: el de los nmeros 21 a 29. No haremos una prueba para
cada uno: haremos slo cuatro. Probaremos los nmeros 21, 22, 23, 26 y 28. Hemos escogido los nmeros
21 y 28 al azar, pero 22, 23 y 26 se han escogido deliberadamente por tratarse de nmeros especiales en lo
tocante a su transcripcin (por la tilde). No reproducimos el cdigo de las cuatro pruebas, que una vez
escritas conducen a una ejecucin de pruebas en rojo.
Nuestro cdigo SUT se modifica ahora para leerse as:
namespace TranscriptorDeNmeros
{
public class Transcriptor
{

88

Buenas prcticas en desarrollo de software con .NET

readonly string[] literals =


new[] {"cero", "uno", "dos", "tres", "cuatro",
"cinco", "seis", "siete", "ocho", "nueve",
"diez", "once", "doce", "trece", "catorce",
"quince", "diecisis", "diecisiete", "dieciocho", "diecinueve",
"veinte"};
public string Transcribe(int nmero)
{
if (nmero <= 20)
{
return literals[nmero];
}
else
{
if (nmero == 22)
{
return "veintids";
}
if (nmero == 23)
{
return "veintitrs";
}
else if (nmero == 26)
{
return "veintisis";
}
else
{
return "veinti" + Transcribe(nmero%10);
}
}
}
}
}

Y verde, de nuevo.
Detengmonos un momento para hacer una observacin. Hemos escogido un par de nmeros al azar entre
21 y 29 (adems de 23 y 26, por su condicin de casos especiales). Podramos hacer en la tentacin de elegir
nmeros realmente al azar con cada ejecucin de las pruebas, es decir, disear una prueba como sta:
[Test]
public void Transcribe_RandomNumber_ReturnsProperTranscription()
{
int n = 21 + (new Random().Next(9));
string[] r = new string[]
{
"veintiuno", "veintids", "veintitrs",
"veiniticuatro", "veinticinco", "veinitisis",
"veintisiete", "veinitiocho", "veintiocho",
"veinitinueve"
};
string result = transcriptor.Transcribe(n);
string expected = r[n-21];
Assert.AreEqual(expected, result);
}

Estaramos cayendo en un error: con cada prueba estaramos ejecutando la comprobacin para un valor
distinto. Las pruebas unitarias han de ir formando un corpus de pruebas que va creciendo por acumulacin.
No deben modificarse alegremente, y cambiar aleatoriamente el valor para el que se hace la comprobacin

89

Buenas prcticas en desarrollo de software con .NET

podra dar como resultado que una ejecucin fallase y no pudisemos reproducir las condiciones que la
hicieron fallar. No es una buena prctica.
Pasamos a una parte ms interesante de nuestro TDD: desarrollemos pruebas y cdigo para los nmeros
divisibles por 10 entre 30 y 100. Pondremos a prueba la correccin de la transcripcin con algunos casos
concretos, como 30, 50 y 90. Una vez escritas las pruebas y comprobado que estamos en rojo, pasamos a
escribir cdigo:
namespace TranscriptorDeNmeros
{
public class Transcriptor
{
readonly string[] first_numbers =
new[] {"cero", "uno", "dos", "tres", "cuatro",
"cinco", "seis", "siete", "ocho", "nueve",
"diez", "once", "doce", "trece", "catorce",
"quince", "diecisis", "diecisiete", "dieciocho", "diecinueve",
"veinte"};
readonly string[] divisibleBy10Between30And100 =
new[]
{
"treinta", "cuarenta", "cincuenta", "sesenta",
"setenta", "ochenta", "noventa", "cien"
};
public string Transcribe(int nmero)
{
if (nmero <= 20)
{
return first_numbers[nmero];
}
else if (nmero < 30)
{
if (nmero == 22)
{
return "veintids";
}
if (nmero == 23)
{
return "veintitrs";
}
else if (nmero == 26)
{
return "veintisis";
}
else
{
return "veinti" + Transcribe(nmero%10);
}
}
else
{
if (nmero % 10 == 0)
{
return divisibleBy10Between30And100[nmero / 10 - 3];
}
}
return "";
}
}
}

90

Buenas prcticas en desarrollo de software con .NET

Y estamos en verde:

Pasamos a los nmeros menores que 100. Probaremos los nmeros 31, 43, 89 y 99. Escribimos las pruebas,
ejecutamos en rojo y escribimos el cdigo:
namespace TranscriptorDeNmeros
{
public class Transcriptor
{
readonly string[] first_numbers =
new[] {"cero", "uno", "dos", "tres", "cuatro",
"cinco", "seis", "siete", "ocho", "nueve",
"diez", "once", "doce", "trece", "catorce",
"quince", "diecisis", "diecisiete", "dieciocho", "diecinueve",
"veinte"};
readonly string[] divisibleBy10Between30And100 =
new[]
{
"treinta", "cuarenta", "cincuenta", "sesenta",
"setenta", "ochenta", "noventa", "cien"
};
string Transcribe(int nmero)
{
if (nmero <= 20)
{
return first_numbers[nmero];
}
else if (nmero < 30)
{
if (nmero == 22)
{
return "veintids";
}
if (nmero == 23)
{
return "veintitrs";
}
else if (nmero == 26)
{
return "veintisis";
}
else
{
return "veinti" + Transcribe(nmero%10);

91

Buenas prcticas en desarrollo de software con .NET

}
}
else
{
if (nmero % 10 == 0)
{
return divisibleBy10Between30And100[nmero / 10 - 3];
}
else
{
return divisibleBy10Between30And100[nmero/10 - 3] +
" y " + first_numbers[nmero%10];
}
}
return "";
}
}
}

Volvemos a estar en verde. A por los nmeros mayores que 100 y menores que mil. Empezamos por los
mltiplos de 100. Probaremos con 300, 700, 800 y 900. Tras comprobar que estamos en rojo, codificamos
una posible solucin:
namespace TranscriptorDeNmeros
{
public class Transcriptor
{
readonly string[] first_numbers =
new[] {"cero", "uno", "dos", "tres", "cuatro",
"cinco", "seis", "siete", "ocho", "nueve",
"diez", "once", "doce", "trece", "catorce",
"quince", "diecisis", "diecisiete", "dieciocho", "diecinueve",
"veinte"};
readonly string[] divisibleBy10Between30And100 =
new[]
{
"treinta", "cuarenta", "cincuenta", "sesenta",
"setenta", "ochenta", "noventa", "cien"
};
readonly string[] divisibleBy100Between200And900 =
new[]
{
"doscientos", "trescientos", "cuatrocientos", "quinientos",
"seiscientos", "setecientos", "ochocientos", "novecientos",
};
public string Transcribe(int nmero)
{
if (nmero <= 20)
{
return first_numbers[nmero];
}
else if (nmero < 30)
{
if (nmero == 22)
{
return "veintids";
}
if (nmero == 23)
{
return "veintitrs";
}

92

Buenas prcticas en desarrollo de software con .NET

else if (nmero == 26)


{
return "veintisis";
}
else
{
return "veinti" + Transcribe(nmero%10);
}
}
else if (nmero <= 100)
{
if (nmero % 10 == 0)
{
return divisibleBy10Between30And100[nmero / 10 - 3];
}
else
{
return divisibleBy10Between30And100[nmero/10 - 3] +
" y " + first_numbers[nmero%10];
}
}
else
{
if (nmero % 100 == 0)
{
return divisibleBy100Between200And900[nmero/100-2];
}
}
return "";
}
}
}

Volvemos a verde. Seleccionemos unos pocos nmeros entre 100 y 999 para hacer pruebas con los que no
son mltiplos de 100: los nmeros, no s 156, 615 y 823.Mostramos nicamente el ltimo else, que es lo
que modificamos ahora:
else
{
if (nmero % 100 == 0)
{
return divisibleBy100Between200And900[nmero/100-2];
}
else
{
return divisibleBy100Between200And900[nmero/100 - 2] +
" " + Transcribe(nmero%100);
}
}

Ejecutamos y, rojo! Hemos cometido un error: no hemos tenido en cuenta los nmeros que empiezan con
ciento. Modifiquemos el cdigo, tanto en la definicin de la variable con los prefijos apropiados como en el
else:
readonly string[] divisibleBy100Between200And900 =
new[]
{
"ciento", "doscientos", "trescientos", "cuatrocientos",
"quinientos", "seiscientos", "setecientos", "ochocientos",
"novecientos",
};

93

Buenas prcticas en desarrollo de software con .NET

else
{
if (nmero % 100 == 0)
{
return divisibleBy100Between200And900[nmero/100-1];
}
else
{
return divisibleBy100Between200And900[nmero/100 - 1] +
" " + Transcribe(nmero%100);
}
}

Ahora s: compilamos, ejecutamos las pruebas y nuevamente en verde. Para ir acabando con el ejemplo,
preparamos pruebas para los nmeros 1000, 1015, 3457, 7865 y 9999.
Mostramos aqu el ltimo else, que hemos modificado, y el nuevo fragmento de cdigo en el SUT:
else if (nmero < 1000)
{
if (nmero % 100 == 0)
{
return divisibleBy100Between200And900[nmero/100-1];
}
else
{
return divisibleBy100Between200And900[nmero/100 - 1] +
" " + Transcribe(nmero%100);
}
}
else
{
if (nmero == 1000)
{
return "mil";
}
else if (nmero < 2000)
{
return "mil " + Transcribe(nmero%1000);
}
else
{
return first_numbers[nmero / 1000] +
" mil " + Transcribe(nmero % 1000);
}

Ejecutamos con todo en verde.


Dejamos como ejercicio al lector ampliar el rango de valores que puede tratar nuestro transcriptor. Nosotros
vamos a cerrar el rango de valores admisibles haciendo que los intentos de transcribir nmero negativos o
superiores a 9999 resulten en el lanzamiento de una excepcin del tipo. Nuestras nuevas pruebas tendrn
este aspecto:
[Test]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void Transcribe_NegativeNumber_ThrowsArgumentOutOfRangeException()
{
string result = transcriptor.Transcribe(-1);
}
[Test]

94

Buenas prcticas en desarrollo de software con .NET

[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void Transcribe_GreaterThan9999_ThrowsArgumentOutOfRangeException()
{
string result = transcriptor.Transcribe(10000);
}

Si ejecutamos ahora las pruebas, habr dos fallos:

El mensaje que se muestra en pantalla reza as:


TestTranscriptorDeNmeros.TestTranscriptorDeNmeros.Transcribe_GreaterThan9999_ThrowsArgumentOutOf
RangeException:
System.ArgumentOutOfRangeException was expected
TestTranscriptorDeNmeros.TestTranscriptorDeNmeros.Transcribe_NegativeNumber_ThrowsArgumentOutOfR
angeException:
An unexpected exception type was thrown
Expected: System.ArgumentOutOfRangeException
but was: System.IndexOutOfRangeException : Index was outside the bounds of the array.

En el primer caso se esperaba una excepcin y no se observ. En el segundo, se esperaba una excepcin de
cierto tipo, pero se observ una de otro tipo. En cualquier caso, no ocurri lo esperado.
Modificamos el cdigo del SUT para que el mtodo Transcribe empiece as:
public string Transcribe(int nmero)
{
if (nmero < 0 || nmero > 9999)
{
throw new ArgumentOutOfRangeException(nmero.ToString());
}
if (nmero <= 20)
{

Ejecutamos las pruebas y, verde!

95

Buenas prcticas en desarrollo de software con .NET

Un total de 47 mtodos que ocupan 392 lneas de cdigo para poner a prueba una clase con un mtodo y
100 lneas de cdigo. No es tan desproporcionado como pudiera parecer a quien no est acostumbrado al
desarrollo basado en pruebas unitarias.
Es importante que el tiempo de ejecucin de una batera de pruebas unitarias sea de, a lo sumo, unos pocos
segundos. Si no es as, el programador estar tentado de ejecutarlas pocas veces, eliminando as la ayuda
que suponen cuando se desarrolla software. Nuestras 47 pruebas se han ejecutado en menos de medio
segundo5, por lo que no hay ningn freno psicolgico a un uso recurrente de las pruebas.
La ventaja es que no slo disponemos de cdigo funcional, sino de una batera de pruebas que hace que
cualquier cambio futuro del SUT que resulte en la introduccin de errores ser ms probablemente
detectado.

7.10 Ejercicio: orden natural


Nos gustara disear un comparador de cadenas (IComparer<string>) que tuviera en cuenta la comparacin
de nmeros tal cul la entendemos naturalmente para ordenar nombres de fichero. Al ordenar por nombre
los ficheros a1.txt, a100.txt, a2.txt, el usuario espera encontrar la lista as: a1.txt, a2.txt, a100.txt.
El problema est planteado en http://www.codinghorror.com/blog/2007/12/sorting-for-humans-naturalsort-order.html (que, por cierto, es un artculo en uno de los blog que vale la pena seguir: Coding Horror, de
Jeff Atwood).

Medio segundo teniendo en cuenta que hay sobrecostes por el mero hecho de cargar el mdulo y controlar la
ejecucin de las priuebas, lo que hace que duplicar el cdigo no suponga duplicar el tiempo de ejecucin.

96

Buenas prcticas en desarrollo de software con .NET

7.11 Ejercicio: el juego de la vida


El juego de la vida es un autmata celular en un universo consistente en un tablero con celdas vivas y
muertas. Parte de una configuracin de celdas vivas y muertas y con cada pulso se decide si cada celda est
viva o muerta con unas reglas sencillas que atienden al estado de las celdas vecinas y al suyo propio en el
instante inmediatamente anterior:

Toda celda viva con menos de dos celdas vecinas muere por despoblacin.

Toda celda viva con ms de tres celdas vecinas muere por sobrepoblacin.

Toda celda viva con dos o tres celdas vecinas vivas sobrevive.

Toda celda muerta con exactamente tres vecinas vivas se convierte en una celda viva.

97

Buenas prcticas en desarrollo de software con .NET

7.12 Una herramienta integrable en Visual Studio


NUnit es una herramienta gratuita cuya interfaz de usuario es independiente de Visual Studio 2010. Si
tenemos una versin de Visual Studio en la que se pueden instalar extensiones (cualquiera excepto las series
Express de Visual Studio) conviene que instalemos alguna de las extensiones que permiten la ejecucin de
las pruebas sin abandonar Visual Studio.
Hay una herramienta de pago llamada ReSharper que ofrece esta integracin y muchas ms funcionalidades
(anlisis de cdigo, navegacin y bsqueda, refactorizaciones, internacionalizacin, generacin de cdigo,
automatizacin de la construccin, asistencia en la escritura de cdigo, limpieza de cdigo, plantillas de
cdigo, edicin de XAML, ). Se puede adquirir en http://www.jetbrains.com/resharper/.
Con ReSharper, los accesorios y mtodos de prueba unitaria se marcan automticamente en el margen del
editor de texto.

En la figura se pueden ver las marcas: son crculos de color verde y amarillo. Si se pincha en la marca se
accede a un men contextual desde el que se puede ejecutar cada prueba individualmente o todas las
pruebas de un accesorio. La ejecucin se puede llevar a cabo directamente o invocando al depurador.
Otro modo de ejecutar las pruebas con ReSharper es va el men contextual que aparece al pulsar el botn
derecho del ratn en la carpeta del proyecto de pruebas que se muestra en el Solution Explorer. Al ejecutar
las pruebas se muestra un panel con el resultado como ste:

98

Buenas prcticas en desarrollo de software con .NET

Otra herramienta centrada en la ejecucin de pruebas es TestDriven.Net. Es un producto comercial, pero


con una licencia para particulares y proyectos de software libre. Se puede descargar de
http://www.testdriven.net.

Al instalarla, Visual Studio enriquece algunos mens con opciones para ejecutar pruebas.

La salida no es muy agradable: es un simple volcado de texto en la consola de salida.

99

Buenas prcticas en desarrollo de software con .NET

En cualquier caso puede resultar ms cmodo que andar entrando y saliendo de Visual Studio cada vez que
se ejecutan las pruebas.

7.13 Crditos y recursos

La pgina web de NUnit es http://www.nunit.org/.


El libro The Art of Unit Testing with examples in C#, de Roy Osherove, est a la venta en
http://www.manning.com/osherove/. El autor mantiene una pgina web relacionada con el libro en
http://artofunittesting.com/ y un blog http://osherove.com/blog/.
Hay una lista de problemas para ejercitar TDD en http://sites.google.com/site/tddproblems.
Esta entrada de un blog advierte de anti-patrones en TDD: http://blog.jamescarr.org/2006/11/03/tdd-anti-patterns/.

8 Dobles de prueba
Ya hemos visto qu es el testeo unitario. Un principio fundamental del testeo unitario es que el SUT es nico,
es decir, cada prueba se disea para comprobar el correcto funcionamiento de un aspecto de un nico
elemento de nuestro sistema software. Pero es corriente que aislar un elemento de modo que slo se le
ponga a prueba a l resulte imposible, pues este elemento no tiene sentido sin la participacin de otro u
otros elementos porque ha de interactuar necesariamente con ellos. Decimos entonces que nuestra unidad
presenta dependencias externas.
La prctica habitual consiste en crear un objeto que se haga pasar por el necesario durante la prueba, lo que
denominamos un doble de prueba. Si ese objeto debe implementarse con un gran esfuerzo de
programacin, no valdr la pena. Por eso, el doble de prueba contendr la lgica mnima para conseguir
hacerse pasar por el objeto real. La lgica mnima permitir una escritura rpida y una ejecucin tambin
rpida.
En principio, el objetivo es doble:

no depender del comportamiento de otro objeto (que podra contener errores) cuando
comprobamos que nuestro SUT funciona correctamente,
mantener el tiempo de ejecucin de las pruebas tan bajo como sea posible, evitando el coste que
supone acceder a recursos potencialmente lentos (sistema de ficheros, bases de datos, etc).

Pero si estamos haciendo TDD, hay un objetivo adicional:

construir nuestro software del modo ms desacoplado posible, haciendo tan sencillo como sea
posible el sustituir una clase por otra.

100

Buenas prcticas en desarrollo de software con .NET

Este ltimo objetivo cuesta de entender hasta que se ve en la prctica, pero es de los ms importantes: tiene
un impacto directo en el diseo de nuestro software.
Hay varios tipos de dobles de prueba. Si seguimos la nomenclatura presentada en el artculo
http://martinfowler.com/articles/mocksArentStubs.html tenemos:

Dummy: Objeto que se suministra para llenar un hueco. Por ejemplo, si un mtodo requiere un
parmetro de un cierto tipo pero no hace uso de l, se puede crear una instancia cualquiera (un
dummy) de ese tipo y suministrarla como argumento.
Fake: Objeto con una implementacin funcional, pero que toma algn atajo que no lo hace til en
produccin. Un ejemplo es una base de datos en memoria.
Stub: Objeto que proporciona respuestas enlatadas a las llamadas que tienen lugar durante una
prueba, pero generalmente incapaces de dar respuestas a nada que no est en la prueba. Tambin
puede registrar informacin acerca de las llamadas. Por ejemplo, el stub de una pasarela de correo
podra memorizar el los mensajes que envi, o quiz slo su nmero.
Mock: Objeto preprogramado con expectativas que forman una especificacin de las llamadas que
se espera que reciba.

Hay muchas libreras que ofrecen la posibilidad de crear Mocks. Entre ellas tenemos:

NUnit.Mocking: no hay buena documentacin y no es la mejor librera que podemos encontrar.


Durante algn tiempo no soportaba los modos de trabajo que de uso creciente en la comunidad. Por
otra parte, usa cadena en lugar de identificadores de mtodos, con lo que el compilador no avisa de
errores comunes al construir o usar los mocks.
TypeMock: producto comercial.
Rhino.Mocks: es una de las ms completas, pero con una curva de aprendizaje mayor que la de
otras.
Moq: es una librera sencilla, con una interfaz fluida.

Usaremos Moq por presentar una curva de aprendizaje muy suave y presentar una coleccin de conceptos
limitada pero suficiente para aprender a usar dobles de prueba.

8.1 Nuestra aplicacin de ejemplo: un lector de RSS


Empezamos con un ejemplo adaptado de un libro de buenas prcticas que usa el lenguaje Python.
Recurrimos a ese material por su inters propio, pero tambin porque demuestra que las tcnicas que
aprendemos en el curso son universales o al menos no dependen del lenguaje de programacin
empleado.
Nuestro ejemplo consiste en un sistema de lectura de fuentes RSS (Really Simple Syndication), el sistema de
presentacin de contenidos que usan infinidad de sistemas web para publicar a la informacin en un
formato legible por herramientas y fcilmente manipulable. Las fuentes RSS son direcciones URL a las que se
lanza una peticin GET de HTTP y proporcionan como resultado un contenido XML que sigue un
determinado esquema. Las fuentes RSS se suelen mostrar incrustadas en pginas web con este logo:

101

Buenas prcticas en desarrollo de software con .NET

El peridico El Pas, por ejemplo, publica sus noticias ms recientes en RSS. Basta con acceder a la URL
http://www.elpais.com/rss/feed.html?feedId=17046 para obtener un documento XML con un contenido
similar a ste (el sangrado se ha alterado para facilitar la lectura):
<?xml version="1.0" encoding="iso-8859-1"?>
<rss version="2.0" xmlns:media="http://search.yahoo.com/mrss/">
<channel>
<title><![CDATA[ELPAIS.com - Lo ltimo]]></title>
<link><![CDATA[http://www.elpais.com/loultimo/]]></link>
<description><![CDATA[ELPAIS.com - Lo ltimo]]></description>
<lastBuildDate>Sun, 27 Mar 2011 18:58:06 +0200</lastBuildDate>
<language>es-es</language>
<copyright><![CDATA[Copyright Prisa Digital S.L.]]></copyright>
<ttl>15</ttl>
<image>
<url>http://www.elpais.com/im/tit_logo.gif</url>
<title>ELPAIS.com - Lo ltimo</title>
<link>http://www.elpais.com</link>
</image>
<item>
<title><![CDATA[El Palacio de Cibeles abre al pblico ]]></title>
<link><![CDATA[http://www.elpais.com/articulo/espana/Palacio/Cibeles/abre/publico/zonas/restringidas/e
lpepuesp/20110327elpepunac_8/Tes]]></link>
<description><![CDATA[<a
href="http://www.elpais.com/articulo/madrid/palacio/Cibeles/dentro/elpepiespmad/20100923elpmad_3/Tes"
target="_blank">El Palacio de Cibeles</a> abre al pblico las puertas de su rea cultural. Desde hoy y
hasta el 27 de julio, los ciudadanos podrn visitar el interior del edificio, que ser tambin <a
href="http://www.elpais.com/articulo/madrid/Gallardon/trasladara/alcaldia/Cibeles/principios/2004/elpe
piautmad/20030731elpmad_8/Tes" target="_blank">la sede del Ayuntamiento de Madrid</a> , por primera
vez en sus ms de 100 aos de historia.]]></description>
<guid isPermaLink="true">
<![CDATA[http://www.elpais.com/articulo/espana/Palacio/Cibeles/abre/publico/zonas/restringidas
/elpepuesp/20110327elpepunac_8/Tes]]>
</guid>
<author><![CDATA[]]></author>
<pubDate><![CDATA[Sun, 27 Mar 2011 18:41:00 +0200]]></pubDate>
</item>
<item>
<title><![CDATA[Un eurodiputado 'pillado' por una cmara oculta ]]></title>
<link><![CDATA[http://www.elpais.com/articulo/espana/eurodiputado/PP/pillado/camara/oculta/lobby/falso
/elpepuesp/20110327elpepunac_9/Tes]]></link>
<description><![CDATA[El eurodiputado popular <a
href="http://www.europarl.europa.eu/members/public/geoSearch/view.do?language=ES&id=96763"
target="_blank">Pablo Zalba Bidegain </a> acord retocar una directiva comunitaria destinada a
proteger a los consumidores europeos siguiendo las peticiones de un grupo falso de presin, segn
revela hoy el dominical <i>The Sunday Times</i>, que afirma que esta conversacin fue grabada en
secreto. Zalba, que ha denunciado haber sido vctima de una "trampa", ha negado haber cobrado por
modificar el texto y ha sealado que enmend la ley porque as la "mejoraba
sustancialmente".]]></description>
<guid isPermaLink="true">
<![CDATA[http://www.elpais.com/articulo/espana/eurodiputado/PP/pillado/camara/oculta/lobby/fal
so/elpepuesp/20110327elpepunac_9/Tes]]>
</guid>
<author><![CDATA[EFE]]></author>
<pubDate><![CDATA[Sun, 27 Mar 2011 18:44:00 +0200]]></pubDate>
</item>
</channel>
</rss>

La especificacin de RSS 2.0, que es el formato de este documento, se puede consultar en la pgina
http://feed2.w3.org/docs/rss2.html. No hay un solo formato RSS en uso.
La aplicacin que vamos a construir est orientada a la lnea de rdenes. Recibir una serie de URLs y
mostrar un resumen de la informacin RSS. Si slo le suministrsemos la URL anterior, mostrara esa misma
informacin en el siguiente formato:
*** ELPAIS.com - Lo ltimo
Sun, 27 Mar 2011 18:41:00 +0200: El Palacio de Cibeles abre al pblico

102

Buenas prcticas en desarrollo de software con .NET

Sun, 27 Mar 2011 18:44:00 +0200: Un eurodiputado 'pillado' por una cmara oculta

La salida sigue un formato sencillo: En el resumen, el nombre de la fuente RSS ocupa la primera lnea y cada
tem del RSS ocupa una lnea adicional. En cada una de esas lneas se muestra la fecha de publicacin del
tem, dos puntos y el ttulo del tem.
Si se proporcionasen ms URLs, la aplicacin mostrara un resumen como ese para cada una.
Desarrollaremos la aplicacin siguiendo la metodologa TDD, pero un tanto minorada por razones
prcticas. Para cada unidad disearemos una o dos pruebas, cuando convendra hacer muchas ms y
atender as a todos los contextos imaginables. El objetivo de ver qu es TDD lo hemos cubierto en el tema
anterior. Iremos ms rpido ahora para poder cubrir otros objetivos en un tiempo razonable. En particular,
veremos cmo el diseo TDD (incluso en esta versin simplificada) ayuda a producir software
incrementalmente, con elevado grado de desacomplamiento, y cmo es posible eliminar dependencias
cuando stas se detectan. Un buen desacoplamiento permitir introducir dobles de prueba y podremos
estudiar stubs y mocks. Veremos que los mocks ofrecen ms funcionalidad que los stubs y que se codifican
ms fcilmente.

8.2 Aplicacin RssReaderApp y librera Rss


Empezamos creando el proyecto RssReaderApp, del tipo Console Application. En el fichero Program.cs
editaremos el texto para dejarlo as:
namespace RssReaderApp
{
class Program
{
static void Main(string[] args)
{
Rss.Reader reader = new Rss.Reader();
reader.Display(args);
}
}
}

Naturalmente, la clase Rss.Reader no existe y el programa no puede compilarse. Creamos aparte un nuevo
proyecto con nombre Rss y de tipo Class Library. Su fichero Class1.cs se renombra para ser Reader.cs y su
contenido se edita para que se lea as:
using System.Collections.Generic;
namespace Rss
{
public class Reader
{
public void Display(IEnumerable<string> urls)
{
foreach (var url in urls)
{
FormatFeed(url);
}
}
public void FormatFeed(string url)
{
}
}
}

103

Buenas prcticas en desarrollo de software con .NET

El mtodo Display debe recibir una relacin de URLs y mostrar un resumen del contenido RSS de cada una
de ellas. Para ello invoca al mtodo FormatFeed, que an no hemos definido.
Al proyecto RssReaderApp le aadimos una referencia a la librera Rss. Ya podemos compilar, aunque el
programa no hace absolutamente nada.

8.3 TDD
Desarrollaremos con la metodologa TDD. Para ello creamos un proyecto TestRss de tipo Class Library al que
aadimos la referencia al ensamblado nunit.framework.dll y una referencia a la librera Rss.
Hemos de dotar de lgica al mtodo FormatFeed. Seguro que viene bien disponer de un mtodo que
formatee un tem individual de una fuente RSS. As pues, nuestro primer objetivo es disear un mtodo que
muestre un tem del contenido RSS en una sola lnea, con la fecha y el titular del tem. En el ejemplo
anterior, el mtodo es responsable de producir una lnea como sta:
Sun, 27 Mar 2011 18:41:00 +0200: El Palacio de Cibeles abre al pblico

Si no estamos acostumbrados, analizar un documento XML puede resultar complejo. Por otra parte, un
mtodo que reciba el documento XML, lo analice, extraiga el contenido relevante y lo muestre es un mtodo
que parece, a priori, excesivamente ambicioso (propio de un objetos Dios). Es mejor que cada mtodo
haga una sola cosa y la haga bien. Dejaremos, pues, la cuestin de analizar XML para ms adelante.
Supondremos de momento que la informacin del documento XML se nos pasa ya en algn formato
apropiado, con objetos .NET, y que apenas hemos de juntar trozos de texto para producir el resultado. Lo
ms sencillo es suponer que nos pasan dos argumentos de tipo cadena y que nos limitamos a juntar las
cadenas apropiadamente. No ha de ser complicado disear un mtodo que haga eso y slo eso.
Empezamos por definir la prueba unitaria:
using NUnit.Framework;
using System.Collections.Generic;
namespace TestRss
{
[TestFixture]
public class TestReader
{
private Rss.Reader _reader;
[SetUp]
public void PreparaReader()
{
_reader = new Rss.Reader();
}
[Test]
public string FormatItem_ConDateYTitle_DevuelveLineaFormateada()
{
string date = "Sun, 27 Mar 2011 18:41:00 +0200";
string title = "El Palacio de Cibeles abre al pblico ";
string expected =
"Sun, 27 Mar 2011 18:41:00 +0200: El Palacio de Cibeles abre al pblico";
var result = _reader.FormatItem(date, title);
Assert.AreEqual(expected, result);
}
}
}

104

Buenas prcticas en desarrollo de software con .NET

El mtodo FormatItem no est definido, pero ofrecer una implementacin a partir de este caso de uso
resulta trivial:
using System.Collections.Generic;
namespace Rss
{
public class Reader
{
public void Display(IEnumerable<string> urls)
{
foreach (var url in urls)
{
FormatFeed(url);
}
}
public void FormatFeed(string url)
{
}
public string FormatItem(string date, string title)
{
return string.Format("{0}: {1}", feed, title);
}
}
}

En el GUI de NUnit cargamos TestRss.dll, ejecutamos y verde!


No sabemos an cmo analizaremos el documento XML, pero si podemos saber qu informacin nos
interesa y pensar en algn modo de representarla con estructuras de datos propias de .NET. Creemos
nuestras propias clases Feed e Item:
using System.Collections.Generic;
namespace Rss
{
public class Feed
{
public string Title { get; private set; }
public IEnumerable<Item> Items { get; private set; }
public Feed(string title, IEnumerable<Item> items)
{
Title = title;
Items = new List<Item>(items);
}
}
public class Item
{
public string Date { get; private set; }
public string Title { get; private set; }
public Item(string date, string title)
{
Date = date;
Title = title;
}
}
public class Reader

105

Buenas prcticas en desarrollo de software con .NET

{
public void Display(IEnumerable<string> urls)
{
foreach (var url in urls)
{
FormatFeed(url);
}
}
public void FormatFeed(string url)
{
}
public string FormatItem(string date, string title)
{
return string.Format("{0}: {1}", date, title);
}
}
}

Reescribamos el test (y cambiemos su identificador) para que recoja nuestro rediseo:


[Test]
public void FormatItem_ConItem_DevuelveLineaFormateada()
{
var feed = new Rss.Feed(
title: "ELPAIS.com - Lo ltimo",
items: new List<Rss.Item>
{
new Rss.Item(
date: "Sun, 27 Mar 2011 18:41:00 +0200",
title: "El Palacio de Cibeles abre al pblico"
)
});
var expected = "Sun, 27 Mar 2011 18:41:00 +0200: El Palacio de Cibeles abre al pblico";
var result = reader.FormatItem(feed.Items.First());
Assert.AreEqual(expected, result);
}

Y reescribamos tambin el mtodo:


public class Reader
{
public void Display(IEnumerable<string> urls)
{
foreach (var url in urls)
{
FormatFeed(url);
}
}
public void FormatFeed(string url)
{
}
public string FormatItem(Item item)
{
return string.Format("{0}: {1}", item.Date, item.Title);
}
}

Ntese que estamos tomando decisiones de diseo sobre la marcha, corrigiendo decisiones previas cuando
es menester. El diseo nos lo va proporcionando el uso que hacemos de aquello que vamos escribiendo. Ya

106

Buenas prcticas en desarrollo de software con .NET

sabemos que esa es una de las ventajas del TDD. Ah! Y el diseo actual no tiene por qu ser el definitivo.
Hemos de estar abiertos al cambio.
Vamos ahora a por un mtodo que proporcione el resumen de una fuente completa. Escribimos primero el
test, suponiendo que el mtodo funciona correctamente y tiene el perfil que nos conviene:
[Test]
public void FormatItem_ConFeed_DevuelveInformeCompleto()
{
var feed = new Rss.Feed(
title: "ELPAIS.com - Lo ltimo",
items: new List<Rss.Item>
{
new Rss.Item(
date: "Sun, 27 Mar 2011 18:41:00 +0200",
title: "El Palacio de Cibeles abre al pblico "
),
new Rss.Item(
date: "Sun, 27 Mar 2011 18:44:00 +0200",
title: "Un eurodiputado 'pillado' por una cmara oculta "
)
});
var expected =
@"*** ELPAIS.com - Lo ltimo
Sun, 27 Mar 2011 18:41:00 +0200: El Palacio de Cibeles abre al pblico
Mar 2011 18:44:00 +0200, ELPAIS.com - Lo ltimo: Un eurodiputado 'pillado' por una cmara oculta
";
var result = reader.FormatFeed(feed);
Assert.AreEqual(expected, result);
}

Aunque definimos en su momento FormatFeed como un mtodo sin valor de retorno y con un parmetro de
tipo cadena, vemos ahora que nos conviene un perfil distinto. El mtodo no hace nada de momento, as que
hemos de definirlo ahora y cambiar su perfil:
public string FormatFeed(Feed feed)
{
var sb = new StringBuilder("*** ");
sb.Append(feed.Title);
sb.Append(Environment.NewLine);
foreach (var item in feed.Items)
{
sb.Append(this.FormatItem(feed, item));
sb.Append(Environment.NewLine);
}
return sb.ToString();
}

Compilamos, ejecutamos las pruebas, y estamos en verde.


Hemos de refactorizar las pruebas. La estructura de datos con el Feed y los Item de pruebas se pueden crear
en el mtodo de SetUp:
using System.Linq;
using NUnit.Framework;
using System.Collections.Generic;
namespace TestRss
{
[TestFixture]
public class TestReader

107

Buenas prcticas en desarrollo de software con .NET

{
private Rss.Reader _reader;
private Rss.Feed _feed;
[SetUp]
public void PreparaReader()
{
_reader = new Rss.Reader();
_feed = new Rss.Feed(
title: "ELPAIS.com - Lo ltimo",
items: new List<Rss.Item>
{
new Rss.Item(
date: "Sun, 27 Mar 2011 18:41:00 +0200",
title: "El Palacio de Cibeles abre al pblico"
),
new Rss.Item(
date: "Sun, 27 Mar 2011 18:44:00 +0200",
title: "Un eurodiputado 'pillado' por una cmara oculta"
)
});
}
[Test]
public void FormatItem_ConItem_DevuelveLineaFormateada()
{
var expected =
"Sun, 27 Mar 2011 18:41:00 +0200: El Palacio de Cibeles abre al pblico ";
var result = _reader.FormatItem(_feed.Items.First());
Assert.AreEqual(expected, result);
}
[Test]
public void FormatFeed_ConFeed_DevuelveInformeCompleto()
{
var expected =
@"Sun, 27 Mar 2011 18:41:00 +0200: El Palacio de Cibeles abre al pblico
Sun, 27 Mar 2011 18:44:00 +0200: Un eurodiputado 'pillado' por una cmara oculta
";
var result = _reader.FormatFeed(_feed);
Assert.AreEqual(expected, result);
}
}
}

Vamos a construir el mtodo que convierte el XML de un feed en el correspondiente objeto Rss.Feed.
Empezamos por una prueba unitaria que defina el comportamiento esperado:
[Test]
public void Parse_ConWellFormedXml_DevuelveFeed()
{
var xml = @"<?xml version='1.0' encoding='iso-8859-1'?>

</rss>

";
var expected = feed;
var result = reader.Parse(xml);
Assert.AreEqual(expected, result);
}

(No mostramos la cadena XML completa. Su contenido se corresponde a una copia del ejemplo XML,
sustituyendo las dobles comillas por comillas simples en su interior para evitar problemas de codificacin de
cadenas en C#.)

108

Buenas prcticas en desarrollo de software con .NET

El programa no compila. Podemos hacer que compile con una implementacin que no hace nada relevante:
public class Feed
{

public Feed Parse(string xml)


{
return null;
}

Naturalmente, no pasamos una de las tres pruebas unitarias. Mejoremos la implementacin y hagamos uso
de una librera de tratamiento XML.
public Feed Parse(string xml)
{
XDocument xdoc = XDocument.Load(new StringReader(xml));
var feed= new Feed(
title: xdoc.Element("rss").Element("channel").Element("title").Value,
items: from elt in xdoc.Element("rss").Element("channel").Elements("item")
select new Item(
date: elt.Element("pubDate").Value,
title: elt.Element("title").Value
));
return feed;
}

Parece que todo est listo. Ejecutamos las pruebas y rojo! El mensaje del fallo es ste:
TestRss.TestReader.Parse_ConWellFormedXml_DevuelveFeed:
Expected: <Rss.Feed>
But was: <Rss.Feed>

8.4 Depuracin con NUnit


Nos interesara ver con detalle que ocurre. Cmo podemos iniciar una sesin de depuracin? En primer
lugar, pongamos un breakpoint en la orden Assert.AreEqual de TestReader y compilemos. En el men
Debug de Visual Studio seleccionamos la opcin Attach to Process y seleccionamos el proceso nunitagent.exe. Al pusar Run en el GUI de NUnit, la ejecucin se detendr en el breakpoint. Podemos examinar las
variables expected y result:

109

Buenas prcticas en desarrollo de software con .NET

Podemos comprobar que los dos contienen bsicamente. Naturalmente, lo que nos est ocurriendo es que
no se ha definido la funcin Equals en Feed, as que la comparacin se limita a comprobar si los dos objetos
son el mismo (no si los contenidos de ambos objetos son iguales, que es distinto).
Hemos de definir un mtodo de igualdad. Por elegancia, haremos que Feed e Item implementen
IEquatable<Feed> e IEquatable<Item>, respectivamente. Y eso hace que debamos redefinir el mtodo
Equals de Object y que convenga redefinir tambin GetHashcode:
public class Feed : IEquatable<Feed>
{
public string Title { get; private set; }
public IEnumerable<Item> Items { get; private set; }
public Feed(string title = "", IEnumerable<Item> items = null)
{
Title = title;
Items = new List<Item>(items ?? Enumerable.Empty<Item>());
}
public override bool Equals(object obj)
{
var other = obj as Feed;
if (other == null)
{
return false;
}
return Equals(other);
}
public override int GetHashCode()
{
var hash = Title.GetHashCode();
foreach (var item in Items)
{
hash = hash*23 + item.GetHashCode();
}
return hash;
}
#region IEquatable<Feed> Members
public bool Equals(Feed other)
{
if (Title != other.Title)
{
return false;
}
var otherEnum = other.Items.GetEnumerator();
foreach (var item in Items)
{
if (!otherEnum.MoveNext())
{
return false;
}
if (!item.Equals(otherEnum.Current))
{
return false;
}
}
if (otherEnum.MoveNext())
{
return false;
}

110

Buenas prcticas en desarrollo de software con .NET

return true;
}
#endregion
}
public class Item : IEquatable<Item>
{
public string Date { get; private set; }
public string Title { get; private set; }
public Item(string date, string title)
{
Date = date;
Title = title;
}
public override bool Equals(object obj)
{
var other = obj as Item;
if (other == null)
{
return false;
}
return Equals(other);
}
public override int GetHashCode()
{
return Title.GetHashCode() * 23 + Date.GetHashCode();
}
#region IEquatable<Item> Members
public bool Equals(Item other)
{
return Date == other.Date && Title == other.Title;
}
#endregion
}

(Para saber de dnde sale la frmula para calcular cdigos de dispersin y por qu ese factor 23, en
StackOverflow hay una pregunta con su consiguiente respuesta que refieren a un libro interesante:
http://stackoverflow.com/questions/263400/what-is-the-best-algorithm-for-an-overridden-system-objectgethashcode).
Ejecutamos ahora nuestra coleccin de pruebas y verde! Naturalmente, ahora deberamos detenernos a
escribir pruebas unitarias para los tres mtodos que hemos definido en Feed y los otros tres que hemos
definido en Item. Las dejamos como ejercicio para el lector.

8.5 Hemos violado el principio de diseo SRP


Nuestra clase Rss.Reader empieza a ser compleja: sabe presentar informacin y tambin sabe analizar
ficheros XML. Algo huele mal en el cdigo, se percibe un code smell, esto es, un sntoma de que hay un
problema latente.
Lo cierto es que hemos violado uno de los principios de diseo: el SRP (Single Responsibility Principle), que
dice que cada clase debera tener una sola responsabilidad. Un analizador de XML y un presentador de
informacin son elementos muy diferentes entre s y no es natural que el cdigo de ambos elementos

111

Buenas prcticas en desarrollo de software con .NET

conviva en una sola clase. Hemos de crear clases separadas para cada responsabilidad. Rss.Reader acabar
siendo la cola que unir ambas clases.
8.5.1 Refactorizando
Como el cdigo ya est escrito, refactorizarlo es sencillo. Aprovechamos para arreglar un asunto pendiente:
Feed e Item deberan declararse en ficheros propios, Feed.cs e Item.cs. Siguiendo esa misma lgica, las
clases Rss.Parser y Rss.Formatter se definirn en ficheros propios:
Nuestros dos nuevos ficheros quedan, por el momento, as:
Parser.cs
using System.IO;
using System.Linq;
using System.Xml.Linq;
namespace Rss
{
public class Parser
{
public Feed Parse(string xml)
{
XDocument xdoc = XDocument.Load(new StringReader(xml));
var feed = new Feed(
title: xdoc.Element("rss").Element("channel").Element("title").Value,
items: from elt in xdoc.Element("rss").Element("channel").Elements("item")
select new Item(
date: elt.Element("pubDate").Value,
title: elt.Element("title").Value
));
return feed;
}
}
}

Formatter.cs
using System;
using System.Text;
namespace Rss
{
public class Formatter
{
public string FormatItem(Item item)
{
return string.Format("{0}: {1}", item.Date, item.Title);
}
public string FormatFeed(Feed feed)
{
var sb = new StringBuilder("*** ");
sb.Append(feed.Title);
sb.Append(Environment.NewLine);
foreach (var item in feed.Items)
{
sb.Append(this.FormatItem(item));
sb.Append(Environment.NewLine);
}
return sb.ToString();
}

112

Buenas prcticas en desarrollo de software con .NET

}
}

Y ahora nos queda por redefinir la clase Reader para que haga su trabajo, que es hacer colaborar a un
Parser con un Formatter. El contenido de Reader.cs podra quedar as:
namespace Rss
{
public class Reader
{
private Parser _parser;
private Formatter _formatter;
public Reader()
{
_parser = new Parser();
_formatter = new Formatter();
}
public string FeedReport(string xml)
{
var feed = _parser.Parse(xml);
var formatted = _formatter.FormatFeed(feed);
return formatted;
}
}
}

El resultado an necesitar retoques, pero ya puede comprobar que el diseo es evolutivo y va guiado por
las pruebas y refactorizaciones.
Llevamos tiempo sin compilar ni ejecutar pruebas. Con tanto rediseo, las pruebas ya no compilan.
Tendremos que retocarlas. De hecho, conviene separar ahora las pruebas en nuevas clases, una por cada
una de las clases que consideramos un SUT. As, el nuevo fichero TestFormatter.cs contendr:
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
namespace TestRss
{
[TestFixture]
public class TestFormatter
{
private Rss.Formatter _formatter;
private Rss.Feed _feed;
[SetUp]
public void PreparaTestFormatter()
{
_formatter = new Rss.Formatter();
_feed = new Rss.Feed(
title: "ELPAIS.com - Lo ltimo",
items: new List<Rss.Item>
{
new Rss.Item(
date: "Sun, 27 Mar 2011 18:41:00 +0200",
title: "El Palacio de Cibeles abre al pblico"
),
new Rss.Item(
date: "Sun, 27 Mar 2011 18:44:00 +0200",
title: "Un eurodiputado 'pillado' por una cmara oculta"

113

Buenas prcticas en desarrollo de software con .NET

)
});
}
[Test]
public void FormatItem_ConFeedEItem_DevuelveLineaFormateada()
{
var expected =
"Sun, 27 Mar 2011 18:41:00 +0200: El Palacio de Cibeles abre al pblico";
var result = _formatter.FormatItem(_feed, _feed.Items.First());
Assert.AreEqual(expected, result);
}
[Test]
public void FormatFeed_ConFeed_DevuelveLineasFormateadas()
{
var expected = @"*** ELPAIS.com - Lo ltimo
Sun, 27 Mar 2011 18:41:00 +0200: El Palacio de Cibeles abre al pblico
Sun, 27 Mar 2011 18:44:00 +0200: Un eurodiputado 'pillado' por una cmara oculta
";
var result = _formatter.FormatFeed(_feed);
Assert.AreEqual(expected, result);
}
}
}

Ahora hemos de poner a prueba a nuestra redefinida clase Reader. En principio no hay gran cosa que hacer
(omitimos en el listado parte de la cadena XML en aras de la brevedad):
using System.Linq;
using NUnit.Framework;
using System.Collections.Generic;
namespace TestRss
{
[TestFixture]
public class TestReader
{
private Rss.Reader _reader;
[SetUp]
public void PreparaReader()
{
_reader = new Rss.Reader();
}
[Test]
public void FeedReport_ConXmlBienFormado_DevuelveLneasFormateadas()
{
var xml =
@"<?xml version='1.0' encoding='iso-8859-1'?>

</rss>
";
var expected = @"*** ELPAIS.com - Lo ltimo
Sun, 27 Mar 2011 18:41:00 +0200: El Palacio de Cibeles abre al pblico
Sun, 27 Mar 2011 18:44:00 +0200: Un eurodiputado 'pillado' por una cmara oculta
";
var result = _reader.FeedReport(xml);
Assert.AreEqual(expected, result);
}
}
}

114

Buenas prcticas en desarrollo de software con .NET

8.5.2 Eliminando dependencias


Analicemos nuestra clase Reader. Hay un nuevo code smell: hay una fuerte dependencia de dos clases. La
clase Reader depende de las clases concretas Parser y Formatter. Nuestro diseo es excesivamente rgido.
Estamos violando el principio DIP (Dependency Inversion Principle), que dice que hemos de depender de
abstracciones, no de implementaciones.
El modo en el que hemos de desacoplar el cdigo pasa por la definicin de interfaces y su uso al expresar
dependencias. Definimos una interfaz IParser que define el perfil que debe tener todo analizador para ser
utilizable por parte de Reader. En el fichero IParser.cs del proyecto Rss escribimos:
namespace Rss
{
public interface IParser
{
Feed Parse(string xml);
}
}

Y en IFormatter.cs escribimos:
namespace Rss
{
public interface IFormatter
{
string FormatFeed(Feed feed);
}
}

Naturalmente, las clases Parser y Formatter deben retocarse para explicitar que implementan las
interfaces IParser e IFormatter, respectivamente. Nuestra clase Reader tambin se ha de retocar. De
momento queda as:
namespace Rss
{
public class Reader
{
private IParser _parser;
private IFormatter _formatter;
public Reader()
{
_parser = new Parser();
_formatter = new Formatter();
}
public string FeedReport(string xml)
{
var feed = _parser.Parse(xml);
var formatted = _formatter.FormatFeed(feed);
return formatted;
}
}
}

Mmmm. No hemos ganado gran cosa. El cdigo sigue presentando una fuerte dependencia de las mismas
clases. Aunque los campos _parser y _formatter se han definido como instancias de IParser e
IFormatter, el constructor sigue recurriendo a las clases particulares Parser y Formatter para construir los
objetos, as que la dependencia sigue estando ah, en el cdigo de la clase.

115

Buenas prcticas en desarrollo de software con .NET

Una tcnica que permite desacoplar el cdigo es la que se conoce por inyeccin de dependencias, y a ella
dedicaremos mucha atencin ms adelante. Un modo de inyectar dependencias es mediante parmetros en
el constructor:
namespace Rss
{
public class Reader
{
private IParser _parser;
private IFormatter _formatter;
public Reader(IParser parser, IFormatter formatter)
{
_parser = parser;
_formatter = formatter;
}
public string FeedReport(string xml)
{
var feed = _parser.Parse(xml);
var formatted = _formatter.FormatFeed(feed);
return formatted;
}
}
}

Naturalmente, quien haya de construir un Reader deber suministrar sendas instancias de un Parser y un
Formatter. No habremos complicado demasiado nuestro cdigo? Veremos cmo evitar este problema con
una tcnica muy potente: el uso de contenedores e inyectores de dependencias.
Ahora nos queda retocar la prueba unitaria para que funcione con el nuevo cdigo:
using NUnit.Framework;
namespace TestRss
{
[TestFixture]
public class TestReader
{
private Rss.Reader _reader;
[SetUp]
public void PreparaReader()
{
var parser = new Rss.Parser();
var formatter = new Rss.Formatter();
_reader = new Rss.Reader(parser, formatter);
}

8.6 Qu queremos probar realmente de Rss.Reader? Introduciendo los dobles


de prueba
Y ahora llegamos, por fin, al meollo de este captulo: Qu queremos probar realmente de Rss.Reader?
Deseamos demostrar empricamente que si proporcionamos una implementacin de IParser y una
implementacin de IFormatter que son correctas, Reader produce el resultado deseado.
Qu ocurrir si suministramos una implementacin de IParser o IFormatter incorrectas al ejecutar las
pruebas unitarias? Que la prueba unitaria de Reader fallar, hacindonos creer que Reader es una clase

116

Buenas prcticas en desarrollo de software con .NET

defectuosa. Pero no ser el caso: Reader no necesitar que se toque una sola lnea suya, sino que se repare
la clase o clases defectuosas de las que depende.
Hemos de repetirlo para que quede meridianamente claro: la prueba unitaria centrada en Reader slo debe
comprobar que Reader hace lo correcto si sus dependencias funcionan correctamente. Cmo hacemos para
comprobar que tal cosa ocurre exactamente as, si esas dependencias an no han sido sometidas a las
pruebas unitarias que les corresponden para asegurarnos razonablemente de que funcionan correctamente?
Una solucin es disear una clase para IParser y una clase para IFormatter que proporcione exactamente
lo que se necesita, pero absolutamente nada ms. Es lo que denominamos un stub. Lo mejor ser
considerar un ejemplo:
using NUnit.Framework;
using System.Collections.Generic;
namespace TestRss
{
static class SomeConstants
{
internal static string input = @"<?xml version='1.0' encoding='iso-8859-1'?>

</rss>
";
internal static Rss.Feed intermediate = new Rss.Feed(
title: "ELPAIS.com - Lo ltimo",
items: new List<Rss.Item>
{
new Rss.Item(
date: "Sun, 27 Mar 2011 18:41:00 +0200",
title: "El Palacio de Cibeles abre al pblico "
),
new Rss.Item(
date: "Sun, 27 Mar 2011 18:44:00 +0200",
title: "Un eurodiputado 'pillado' por una cmara oculta "
)
});
internal static string output = @"*** ELPAIS.com - Lo ltimo
Sun, 27 Mar 2011 18:41:00 +0200: El Palacio de Cibeles abre al pblico
Sun, 27 Mar 2011 18:44:00 +0200: Un eurodiputado 'pillado' por una cmara oculta
";
}
class ParserMock
{

: Rss.IParser

#region IParser Members


public Rss.Feed Parse(string xml)
{
return SomeConstants.intermediate;
}
#endregion
}
class FormatterMock : Rss.IFormatter
{
#region IFormatter Members
public string FormatFeed(Rss.Feed feed)
{
return SomeConstants.output;

117

Buenas prcticas en desarrollo de software con .NET

}
#endregion
}
[TestFixture]
public class TestReader
{
[Test]
public void FeedReport_RecibeXmlBienFormado_DevuelveLneasFormateadas()
{
var parser = new ParserMock ();
var formatter = new FormatterMock ();
var reader = new Rss.Reader(parser, formatter);
var result = reader.FeedReport(SomeConstants.input);
Assert.AreEqual(SomeConstants.output, result);
}
}
}

Ciertamente estamos ante un caso un tanto forzado, pero la brevedad de una introduccin obliga a este tipo
de truculencia. Hemos de tener en cuenta que esta tcnica no slo es til una vez hemos desacoplado el
cdigo de los elementos con lo que guardaba una dependencia originalmente. Si uno de los objeto fuese
lento en ejecucin, podra arruinar la idea misma de las pruebas unitarias, que han de ser rpidas si se
pretende que sean tiles. Un objeto lento puede ser, por ejemplo, el que accede a un base de datos, a una
pgina web, al sistema de ficheros de un modo extensivo, etctera. Todos esos objetos deben impostarse
con stubs si se desea mantener los tiempos de ejecucin en un orden razonable.

8.7 Mocks con Moq


Lo que es cierto es que hemos tenido que hacer un gran esfuerzo para crear los stubs. Ah es donde
podemos contar con la ayuda de libreras que nos ofrece utilidades para crear stubs al vuelo. Bueno, lo
habitual es que la librera permita crear mocks. Pero no nos anticipemos. Confundamos deliberadamente los
dos trminos por el momento y ya veremos en qu se diferencian.
8.7.1 Instalacin de Moq
Lo primero es instalar la librera Moq en nuestro proyecto TestRss. La librera se puede encontrar en
http://code.google.com/p/moq. En el momento en el que se escribi este texto, la ltima versin de Moq se
distribua en un paquete comprimido con nombre Moq.4.0.10827.zip.
Al abrirlo encontramos varias carpetas. Accedemos a la carpeta NET40 y extraemos el fichero moq.dll. En el
proyecto TestRss aadimos una referencia a moq.dll. Listos.
8.7.2 Primeros pasos con Moq
Empezamos por suprimir completamente nuestras definiciones de las clases ParserMock y FormatterMock.
No son necesarias. Reescribimos el contenido de TestReader.cs para que quede as (sabiendo que la cadena
XML no se muestra aqu completa):
using NUnit.Framework;
using System.Collections.Generic;
using Moq;
namespace TestRss
{
static class SomeConstants

118

Buenas prcticas en desarrollo de software con .NET

{
internal static string input = @"<?xml version='1.0' encoding='iso-8859-1'?>

</rss>
";
internal static Rss.Feed intermediate = new Rss.Feed(
title: "ELPAIS.com - Lo ltimo",
items: new List<Rss.Item>
{
new Rss.Item(
date: "Sun, 27 Mar 2011 18:41:00 +0200",
title: "El Palacio de Cibeles abre al pblico "
),
new Rss.Item(
date: "Sun, 27 Mar 2011 18:44:00 +0200",
title: "Un eurodiputado 'pillado' por una cmara oculta "
)
});
internal static string output = @"*** ELPAIS.com - Lo ltimo
Sun, 27 Mar 2011 18:41:00 +0200: El Palacio de Cibeles abre al pblico
Sun, 27 Mar 2011 18:44:00 +0200: Un eurodiputado 'pillado' por una cmara oculta
";
}
[TestFixture]
public class TestReader
{
[Test]
public void FeedReport_RecibeXmlBienFormado_DevuelveLneasFormateadas()
{
var parserMock = new Mock<Rss.IParser>();
parserMock.Setup(p => p.Parse(SomeConstants.input))
.Returns(SomeConstants.intermediate);
var formatterMock = new Mock<Rss.IFormatter>();
formatterMock.Setup(f => f.FormatFeed(SomeConstants.intermediate))
.Returns(SomeConstants.output);
var reader = new Rss.Reader(parserMock.Object, formatterMock.Object);
var result = reader.FeedReport(SomeConstants.input);
Assert.AreEqual(SomeConstants.output, result);
}
}
}

Mucha tela! Vamos paso a paso para entender bien este fragmento de cdigo.
En primer lugar, incorporamos el espacio de nombres Moq:
using Moq;

Ya en la prueba unitaria, creamos un objeto de tipo Mock<Rss.IParser>, esto es, un impostor para la
interfaz IParser:
var parserMock

= new Mock<Rss.IParser>();

El objeto parserMock es de la clase Mock<Rss.IParser> y en su campo parserMock.Object mantiene una


referencia a objeto que es de la clase IParser. Ese objeto es, precisamente, un stub de Rss.IParser, solo
que forma parte del mock. Ah vemos la diferencia entre el stub y el mock: el stub forma parte constituyente
del mock, que es un objeto con funcionalidad referida a la definicin de expectativas y su verificacin, como
veremos ahora.

119

Buenas prcticas en desarrollo de software con .NET

La siguiente sentencia es un tanto compleja:


parserMock.Setup(p => p.Parse(SomeConstants.input)).Returns(SomeConstants.intermediate);

Vamos por partes. La sentencia contiene dos llamadas a mtodo: una al mtodo Setup de parserMock , que
devolver algo sobre lo que invocamos el mtodo Returns. Centrmonos en la primera de las llamadas:
parserMock.Setup(p => p.Parse(SomeConstants.input)).Returns(SomeConstants.intermediate);

Con esta sentencia indicamos que esperamos que se produzca una llamada al mtodo Setup de
parserMock.Object con el argumento SomeConstants.input (que era la cadena XML). La sintaxis del
argumento de Setup puede resultar extraa hasta que uno se habita. Ese argumento es una lambdafuncin o funcin annima. Es una funcin que recibe un parmetro, al que llamamos p, y devuelve lo que
devuelva la llamada a p.Parse(SomeConstants.input). Parece que podramos haber expresado lo mismo
con
parserMock.Setup(parserMock.Parse(SomeConstants.input)) [mal]

Pero no es as. Esta ltima sentencia no puede ser siquiera compilada: parserMock no tiene un mtodo
Parse, as que el compilador sealar un error. Quiz piense que esta otra llamada s tendra xito:
parserMock.Setup(parserMock.Object.Parse(SomeConstants.input)) [mal]

Y, es cierto, a efectos de compilacin no tendramos el problema anterior. Pero el resultado de la


compilacin provocara que Setup recibiese como argumento el resultado de la llamada a
parserStup.Object.Parse, lo que es errneo porque Setup espera un argumento de otro tipo: una
lambda-funcin.
Al pasar una lambda-funcin, el compilador construye una estructura de datos que representa el clculo que
la funcin hace y suministra esa estructura a Setup. La lambda-funcin est definida como una
Action<IParser>, as que el parmetro p es de tipo IParser y admite una llamada a Parse.
Sigamos. La segunda llamada de la sentencia es una invocacin al mtodo Returns con el argumento
SomeConstants.intermediate.
parserMock.Setup(p => p.Parse(SomeConstants.input)).Returns(SomeConstants.intermediate);

Con esta llamada se declara que la llamada a parserMock.Object.Parse(SomeConstants.input)


devolver, como resultado, el objeto SomeConstants.intermediate.
Ntese que no escribimos una clase convencional: de eso ya se ocupa (probablemente usando reflexin) la
librera Moq.
Estas otras sentencias resultan fciles de entender ahora que sabemos interpretar las anteriores:
var formatterMock = new Mock<Rss.IFormatter>();
formatterMock.Setup(f => f.FormatFeed(SomeConstants.intermediate)).Returns(SomeConstants.output);

Y nos queda esta lnea, que usa por fin los objetos Mock sobre los que hemos definido un cierto
comportamiento:
var reader = new Rss.Reader(parserMock.Object, formatterMock.Object);

120

Buenas prcticas en desarrollo de software con .NET

Ntese que suministramos el IParser y el IFormatter mediante el campo Object de cada mock, no las
propias instancias de Mock<IParser> y Mock<IFormatter>.
Qu ocurrir cuando se ejecute esta otra sentencia?:
var result = reader.FeedReport(SomeConstants.input);

La llamada a FeedReport ejecutar el cdigo que mostramos ahora paso a paso y que corresponde al cuerpo
de dicho mtodo (sustituyendo los campos y parmetros por sus respectivos valores y argumentos). En
primer lugar:
var feed = parserMock.Object.Parse(SomeConstants.input);

El objeto parserMock.Object recibe la llamada a Parse con el argumento SomeConstants.input y


devuelve (porque as lo hemos declarado antes) el valor SomeConstants.intermediate. As pues, la
siguiente sentencia es equivalente a esta:
var formatted = formatterMock.Object.FormatFeed(SomeConstants.intermediate);

Y, de nuevo gracias a la declaracin de comportamiento esperado que hicimos, se devuelve como resultado
SomeConstants.output. La siguiente sentencia se ejecuta y finaliza la ejecucin del mtodo:
return formatted;

Ya est. Hemos conseguido que este test se supere con xito:


Assert.AreEqual(SomeConstants.output, result);

8.8 Aadiendo un cargador de contenido por HTTP


Ya tenemos casi nuestro lector. Falta que obtenga los datos que necesita de una fuente RSS real, accesible
por HTTP. Est claro que cargar un contenido por HTTP es una nueva responsabilidad que no debemos
asignar a Rss.Reader, ni a Rss.Parser, ni a Rss.Formatter. Tendremos que definir una nueva interfaz y
una clase que la implemente. Llamemos ILoader a esa clase:
ILoader.cs
namespace Rss
{
public interface ILoader
{
string Load(string url);
}
}

Loader.cs
using System.Net;
using System.IO;
namespace Rss
{
public class Loader : ILoader
{
#region ILoader Members
public string Load(string url)

121

Buenas prcticas en desarrollo de software con .NET

{
var client = new WebClient();
using (var s = client.OpenRead(url))
using(var r = new StreamReader(s))
{
return r.ReadToEnd();
}
}
#endregion
}
}

Tendremos que modificar nuestra clase Rss.Reader:


namespace Rss
{
public class Reader
{
private ILoader _loader;
private IParser _parser;
private IFormatter _formatter;
public Reader(ILoader loader, IParser parser, IFormatter formatter)
{
_loader = loader;
_parser = parser;
_formatter = formatter;
}
public string FeedReport(string xml)
{
var feed = _parser.Parse(xml);
var formatted = _formatter.FormatFeed(feed);
return formatted;
}
public void Display(IEnumerable<string> urls)
{
foreach (var url in urls)
{
var xml = _loader.Load(url);
var result = FeedReport(xml);
System.Console.Write(result);
}
}
}
}

Hemos definido el mtodo Display como la rutina principal de los objetos de tipo Rss.Reader. Los
argumentos que recibe son URLs que el mtodo enumera para pedir a cada el contenido XML
correspondiente, generar el informe a partir de dicho XML y mostrar por pantalla el XML.
Modificamos tambin el cdigo de pruebas unitarias para esta clase:
using NUnit.Framework;
using System.Collections.Generic;
using Moq;
namespace TestRss
{
static class SomeConstants
{

122

Buenas prcticas en desarrollo de software con .NET

internal static string url = "http://www.elpais.com/rss/feed.html?feedId=17046";


internal static string input = @"<?xml version='1.0' encoding='iso-8859-1'?>

</rss>
";
internal static Rss.Feed intermediate = new Rss.Feed(
title: "ELPAIS.com - Lo ltimo",
items: new List<Rss.Item>
{
new Rss.Item(
date: "Sun, 27 Mar 2011 18:41:00 +0200",
title: "El Palacio de Cibeles abre al pblico "
),
new Rss.Item(
date: "Sun, 27 Mar 2011 18:44:00 +0200",
title: "Un eurodiputado 'pillado' por una cmara oculta "
)
});
internal static string output = @"*** ELPAIS.com - Lo ltimo
Sun, 27 Mar 2011 18:41:00 +0200: El Palacio de Cibeles abre al pblico
Sun, 27 Mar 2011 18:44:00 +0200: Un eurodiputado 'pillado' por una cmara oculta
";
}
[TestFixture]
public class TestReader
{
[Test]
public void FeedReport_RecibeXmlBienFormado_DevuelveLneasFormateadas()
{
var loaderMock= new Mock<Rss.ILoader>();
loaderMock.Setup(l => l.Load(SomeConstants.input)).Returns(SomeConstants.url);
var parserMock = new Mock<Rss.IParser>();
parserMock.Setup(p => p.Parse(SomeConstants.input))
.Returns(SomeConstants.intermediate);
var formatterMock = new Mock<Rss.IFormatter>();
formatterMock.Setup(f => f.FormatFeed(SomeConstants.intermediate))
.Returns(SomeConstants.output);
var reader = new Rss.Reader(loaderMock.Object, parserMock.Object,
formatterMock.Object);
var result = reader.FeedReport(SomeConstants.input);
Assert.AreEqual(SomeConstants.output, result);
}
}
}

Si ejecutamos las pruebas, todo va bien. Pero nos hemos dejado algo importante en el tintero: poner a
prueba a la clase Loader.

8.9 Pruebas con clculos lentos


Diseamos una prueba unitaria en una nueva clase TestLoader. La prueba cargar los datos de una pgina
RSS. Usaremos la de siempre. Hay un problema: la informacin que se genera en una fuente RSS es, por su
propia naturaleza, variable. Un acceso a la misma URL proporciona resultados distintos en momentos
distintos. Podramos montar infraestructura especial: un sitio RSS propio en el que nosotros generamos el
contenido. Otra posibilidad es relajar lo que comprobamos del acceso. Podemos conformarnos con

123

Buenas prcticas en desarrollo de software con .NET

comprobar que se trata de un documento XML y que contiene algunos elementos que deben estar
presentes. Haremos eso.
using
using
using
using

NUnit.Framework;
Rss;
System.Xml.Linq;
System.IO;

namespace TestRss
{
[TestFixture]
class TestLoader
{
[Test]
public void Load_ConUrl_ObtieneXmlVlido()
{
Loader loader = new Loader();
var xml = loader.Load("http://www.elpais.com/rss/feed.html?feedId=17046");
XDocument xdoc = XDocument.Load(new StringReader(xml));
Assert.IsNotNull(xdoc);
Assert.IsNotNull(xdoc.Element("rss"));
}
}
}

Bueno. Ejecutemos las pruebas unitarias:

Hay un serio problema. Las pruebas no tardaban en ejecutarse, hasta el momento, ms de un segundo (de
hecho, apenas un dcima de segundo y pico). Hemos pasado a ms de cinco segundos. Un tiempo excesivo.
Y eso que estamos siendo muy tacaos en el nmero de pruebas unitarias que estamos creando: si
cresemos el nmero de pruebas propio del TDD, el problema se agravara. En cualquier caso, esta prueba
consume excesivo tiempo y no debera ejecutarse frecuentemente.
NUnit permite marcar una prueba unitaria con una categora (una cadena arbitraria) para tratarla
especficamente cuando convenga:
using NUnit.Framework;
using Rss;
using System.Xml.Linq;

124

Buenas prcticas en desarrollo de software con .NET

using System.IO;
namespace TestRss
{
[TestFixture]
class TestLoader
{
[Test]
[Category("Lenta")]
public void Load_ConUrl_ObtieneXmlVlido()
{
Loader loader = new Loader();
var xml = loader.Load("http://www.elpais.com/rss/feed.html?feedId=17046");
XDocument xdoc = XDocument.Load(new StringReader(xml));
Assert.IsNotNull(xdoc);
Assert.IsNotNull(xdoc.Element("rss"));
}
}
}

La interfaz GUI de NUnit dispone de una pestaa (a mano izquierda) que da acceso a las categoras:

Podemos seleccionar categoras, que se mostrarn en la caja Selected Categories, y excluir de ejecucin las
pruebas unitarias correspondientes:

125

Buenas prcticas en desarrollo de software con .NET

Si ejecutamos ahora, se ejecuta todo excepto la prueba marcada:

Cada cierto tiempo podemos incluir esa prueba unitaria en la ejecucin. Pero slo cada cierto tiempo.

8.10 Verificacin de expectativas


Ya vemos que con Moq podemos definir el comportamiento de un objeto que implementa una interfaz sin
necesidad de construir una clase del modo habitual. En ocasiones querremos tener ms control del que
ofrece este procedimiento consistente en definir una llamada y el valor de retorno asociado.
8.10.1 Verificacin del nmero de llamadas
Podemos tener inters en comprobar que se han efectuado las llamadas que hemos definido y que lo hacen
en el orden en el que se han definido. Este mtodo de prueba unitaria incluye tres nuevas sentencias:
[Test]
public void FeedReport_RecibeXmlBienFormado_DevuelveLneasFormateadas()
{
var loaderMock= new Mock<Rss.ILoader>(MockBehavior.Strict);
loaderMock.Setup(l => l.Load(SomeConstants.url)).Returns(SomeConstants.input);

126

Buenas prcticas en desarrollo de software con .NET

var parserMock = new Mock<Rss.IParser>( MockBehavior.Strict);


parserMock.Setup(p => p.Parse(SomeConstants.input))
.Returns(SomeConstants.intermediate);
var formatterMock = new Mock<Rss.IFormatter>(MockBehavior.Strict);
formatterMock.Setup(f => f.FormatFeed(SomeConstants.intermediate))
.Returns(SomeConstants.output);
var reader = new Rss.Reader(loaderMock.Object, parserMock.Object,
formatterMock.Object);
var result = reader.FeedReport(SomeConstants.input);
loaderMock.Verify(l => l.Load(SomeConstants.url), Times.Never());
parserMock.Verify(p => p.Parse(SomeConstants.input), Times.Exactly(1));
formatterMock.Verify(f => f.FormatFeed(SomeConstants.intermediate), Times.Exactly(1));
Assert.AreEqual(SomeConstants.output, result);
}

Al crear los mocks hemos indicado que vamos a exigir un comportamiento estricto de acuerdo con ciertas
reglas (valor MockBehavior.Strict). El mtodo Verifiy se encarga de verificar que esas reglas se cumplen:
loaderMock.Verify(l => l.Load(SomeConstants.url), Times.Never());
parserMock.Verify(p => p.Parse(SomeConstants.input), Times.Exactly(1));
formatterMock.Verify(f => f.FormatFeed(SomeConstants.intermediate), Times.Exactly(1));

La primera lnea dice que la llamada indicada con la lambda-funcin no debe producirse nunca y las dos
lneas siguientes dicen que las respectivas llamadas deben producirse exactamente una vez cada una. Para
hacer una prueba, modifiquemos la primera verificacin y pongamos una condicin que no se da:
loaderMock.Verify(l => l.Load(SomeConstants.input), Times.AtLeast(2));

Exigimos que la llamada tenga lugar al menos dos veces. Al ejecutar las pruebas unitarias tenemos:

El mensaje de error es ste:


TestRss.TestReader.FeedReport_RecibeXmlBienFormado_DevuelveInformeCompleto:

127

Buenas prcticas en desarrollo de software con .NET

Moq.MockException :
Expected invocation on the mock at least 2 times, but was 0 times: l => l.Load(SomeConstants.url)
Configured setups:
l => l.Load(SomeConstants.url), Times.Never
No invocations performed.

Moq nos indica que se esperaba al menos dos invocaciones sobre un mtodo, pero que no se observ
ninguna.
Podemos controlar el nmero de invocaciones con:

Times.AtLeast(int n): al menos n veces.


Times.AtLeastOnce(): al menos una vez.
Times.AtMost(int n): a lo sumo n veces.
Times.AtMostOnce(): a lo sumo una vez.
Times.Between(int a, int b, Range range): entre a y b, donde range puede ser
Range.Inclusive o Range.Exclusive, para indicar si b est comprendida o no en el rango.

Times.Never(): nunca.
Times.Once(): una sola vez.

Una ltima observacin: nuestro objeto _loader.Object es un objeto dummy: no hace nada, pero se
necesita para poder suministrarlo como argumento a un mtodo.
8.10.2 Verificacin con control relajado de parmetros
Al llamar a los mtodos, Moq comprueba que los parmetros son aquellos que se indican. Imaginemos que
no nos importara con qu datos se invoca a parserMock. Podemos indicarlo a Moq as:
[Test]
public void FeedReport_RecibeXmlBienFormado_DevuelveInformeCompleto()
{
var loaderMock = new Mock<Rss.ILoader>(MockBehavior.Strict);
loaderMock.Setup(l => l.Load(SomeConstants.url)).Returns(SomeConstants.input);
var parserMock = new Mock<Rss.IParser>(MockBehavior.Strict);
parserMock.Setup(p => p.Parse(It.IsAny<string>()))
.Returns(SomeConstants.intermediate);
var formatterMock = new Mock<Rss.IFormatter>(MockBehavior.Strict);
formatterMock.Setup(f => f.FormatFeed(SomeConstants.intermediate))
.Returns(SomeConstants.output);
var reader = new Rss.Reader(loaderMock.Object, parserMock.Object,
formatterMock.Object);
var result = reader.FeedReport(SomeConstants.input);
loaderMock.Verify(l => l.Load(SomeConstants.url), Times.Never());
parserMock.Verify(p => p.Parse(It.IsAny<string>()), Times.Once());
formatterMock.Verify(f => f.FormatFeed(SomeConstants.intermediate), Times.Once());
Assert.AreEqual(SomeConstants.output, result);
}

El objeto It permite especificar algunas expectativas para los parmetros:

It.Is<T>: especifica el valor exacto esperado.

128

Buenas prcticas en desarrollo de software con .NET

It.IsAny<T>: especifica que se espera cualquier valor del tipo T.


It.IsInRange<T>(from, to, Range): especifica que el valor est en el rango indicado.
It.IsRegex(s): se espera una cadena que concuerde con la expresin regular s.
It.IsRegex(s, RegexOptions): se espera una cadena que concuerde con la expresin regular s,

modulada por las opciones que se indican en el segundo parmetro.


8.10.3 Una dependencia ms que eliminar y control de propiedades
Ahora deseamos disear una prueba para la rutina principal: Display. Y nos encontramos con un problema
serio. La rutina muestra su salida por la salida estndar. Tendremos que capturar la salida estndar para
asegurarnos de que todo est bien:
[Test]
public void Display_RecibeUrls_MuestraPorPantallaInformeCompleto()
{
var loaderMock = new Mock<Rss.ILoader>(MockBehavior.Strict);
loaderMock.Setup(l => l.Load(SomeConstants.url)).Returns(SomeConstants.input);
var parserMock = new Mock<Rss.IParser>(MockBehavior.Strict);
parserMock.Setup(p => p.Parse(SomeConstants.input))
.Returns(SomeConstants.intermediate);
var formatterMock = new Mock<Rss.IFormatter>(MockBehavior.Strict);
formatterMock.Setup(f => f.FormatFeed(SomeConstants.intermediate))
.Returns(SomeConstants.output);
var reader = new Rss.Reader(loaderMock.Object, parserMock.Object,
formatterMock.Object);
var oldOut = System.Console.Out;
string result = "";
using(TextWriter tw = new StringWriter())
{
System.Console.SetOut(tw);
reader.Display(new[] {SomeConstants.url});
result = tw.ToString();
}
System.Console.SetOut(oldOut);
loaderMock.Verify(l => l.Load(SomeConstants.url), Times.Once());
parserMock.Verify(p => p.Parse(SomeConstants.input), Times.Once());
formatterMock.Verify(f => f.FormatFeed(SomeConstants.intermediate), Times.Once());
Assert.AreEqual(SomeConstants.output, result);
}

Capturar la salida estndar no ha resultado trivial, pero hemos dado con un modo de hacerlo. En cualquier
caso, estamos ante un nuevo code smell. Hay una dependencia demasiado fuerte entre nuestra clase y un
objeto concreto del sistema: System.Console. Deberamos desacoplar el cdigo. Cmo lo hacemos?
Vamos a probar una idea nueva. Hagamos que el mtodo Write (que es todo lo que necesitamos de la
consola) sea un delegado cuyo valor por defecto es System.Console.Write, pero al que podremos dar otro
valor o valores, pues un delegado admite ms de un valor. El delegado ser una propiedad:
Reader.cs
using System.Collections.Generic;

129

Buenas prcticas en desarrollo de software con .NET

using System;
namespace Rss
{
public class Reader
{
private ILoader _loader;
private IParser _parser;
private IFormatter _formatter;
private IWriter _writer;
public Reader(ILoader loader, IParser parser, IFormatter formatter, IWriter writer)
{
_loader = loader;
_parser = parser;
_formatter = formatter;
_writer = writer;
}
public string FeedReport(string xml)
{
var feed = _parser.Parse(xml);
var formatted = _formatter.FormatFeed(feed);
return formatted;
}
public void Display(IEnumerable<string> urls)
{
foreach (var url in urls)
{
var xml = _loader.Load(url);
var result = FeedReport(xml);
_writer.Write(result);
}
}
}
}

IWriter.cs
using System;
namespace Rss
{
public interface IWriter
{
Action<string> Write { get; }
}
}

Writer.cs
using System;
namespace Rss
{
public class Writer : IWriter
{
public Writer()
{
Write += System.Console.Write;
}
#region IWriter Members

130

Buenas prcticas en desarrollo de software con .NET

public Action<string> Write { get; set; }


#endregion
}
}

Program.cs
namespace RssReaderApp
{
class Program
{
static void Main(string[] args)
{
var loader = new Rss.Loader();
var parser = new Rss.Parser();
var formatter = new Rss.Formatter();
var writer = new Rss.Writer();
var reader = new Rss.Reader(loader, parser, formatter, writer);
reader.Display(args);
}
}
}

TestReader.cs

[Test]
public void Display_RecibeUrls_MuestraPorPantallaInformeCompleto()
{
var loaderMock = new Mock<Rss.ILoader>(MockBehavior.Strict);
loaderMock.Setup(l => l.Load(SomeConstants.url)).Returns(SomeConstants.input);
var parserMock = new Mock<Rss.IParser>(MockBehavior.Strict);
parserMock.Setup(p => p.Parse(SomeConstants.input))
.Returns(SomeConstants.intermediate);
var formatterMock = new Mock<Rss.IFormatter>(MockBehavior.Strict);
formatterMock.Setup(f => f.FormatFeed(SomeConstants.intermediate))
.Returns(SomeConstants.output);
var sw = new StringWriter();
var writerMock = new Mock<Rss.IWriter>(MockBehavior.Strict);
writerMock.SetupGet(w => w.Write).Returns(sw.Write);
var reader = new Rss.Reader(loaderMock.Object, parserMock.Object,
formatterMock.Object, writerMock.Object);
reader.Display(new[] {SomeConstants.url});
var result = sw.ToString();
loaderMock.Verify(l => l.Load(SomeConstants.url), Times.Once());
parserMock.Verify(p => p.Parse(SomeConstants.input), Times.Once());
formatterMock.Verify(f => f.FormatFeed(SomeConstants.intermediate), Times.Once());
Assert.AreEqual(SomeConstants.output, result);
}

131

Buenas prcticas en desarrollo de software con .NET

Podemos mejorar ahora nuestro control de mocks con utilidades para asegurarnos de que se accede a la
propiedad, del mismo que nos aseguramos de que se acceda a los mtodos:
[Test]
public void Display_RecibeUrls_MuestraPorPantallaInformeCompleto()
{
var loaderMock = new Mock<Rss.ILoader>(MockBehavior.Strict);
loaderMock.Setup(l => l.Load(SomeConstants.url)).Returns(SomeConstants.input);
var parserMock = new Mock<Rss.IParser>(MockBehavior.Strict);
parserMock.Setup(p => p.Parse(SomeConstants.input))
.Returns(SomeConstants.intermediate);
var formatterMock = new Mock<Rss.IFormatter>(MockBehavior.Strict);
formatterMock.Setup(f => f.FormatFeed(SomeConstants.intermediate))
.Returns(SomeConstants.output);
var sw = new StringWriter();
var writerMock = new Mock<Rss.IWriter>(MockBehavior.Strict);
writerMock.SetupGet(w => w.Write).Returns(sw.Write);
var reader = new Rss.Reader(loaderMock.Object, parserMock.Object,
formatterMock.Object, writerMock.Object);
reader.Display(new[] {SomeConstants.url});
var result = sw.ToString();
loaderMock.Verify(l => l.Load(SomeConstants.url), Times.Once());
parserMock.Verify(p => p.Parse(SomeConstants.input), Times.Once());
formatterMock.Verify(f => f.FormatFeed(SomeConstants.intermediate), Times.Once());
writerMock.VerifyGet(w => w.Write, Times.Once());
Assert.AreEqual(SomeConstants.output, result);
}

8.11 Revisando la privacidad de las clases


Hemos diseado las clases con mucha alegra: casi todo es pblico. Hay ciertos mtodos y ciertas clases que
son instrumentales en el diseo de la librera, pero que el usuario no debera manejar directamente. Si
abrimos todo a todo el mundo, la librera aumenta notablemente en dificultad de uso.
Pero hay un conflicto latente: queremos que no todo sea pblico, pero en principio necesitamos que todo
sea pblico para que una librera independiente (la de pruebas unitarias) pueda acceder libremente a los
mtodos.
Vamos a ir cerrando lo que debamos cerrar y ver qu problemas se generan al usar las cosas no pblicas. Ha
de ser consciente, en cualquier caso, de que hay debate acerca de la idea de que un mtodo privado haya de
ser objeto de pruebas unitarias. Pero supongamos que deseamos hacerlo y que cerramos un mtodo:
using System;
using System.Text;
namespace Rss
{
public class Formatter : IFormatter
{
private string FormatItem(Item item)
{
return string.Format("{0}: {1}", item.Date, item.Title);
}

132

Buenas prcticas en desarrollo de software con .NET

public string FormatFeed(Feed feed)


{
var sb = new StringBuilder("*** ");
sb.Append(feed.Title);
sb.Append(Environment.NewLine);
foreach (var item in feed.Items)
{
sb.Append(this.FormatItem(item));
sb.Append(Environment.NewLine);
}
return sb.ToString();
}
}
}

No podemos compilar porque TestFormatter no tiene acceso al mtodo FormatItem. Una posibilidad de
superar el problema es usar reflexin:
[Test]
public void FormatItem_ConItem_DevuelveLineaFormateada()
{
var expected =
"Sun, 27 Mar 2011 18:41:00 +0200: El Palacio de Cibeles abre al pblico";
// var result = _formatter.FormatItem(_feed.Items.First());
BindingFlags eFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
MethodInfo m = typeof(Rss.Formatter).GetMethod("FormatItem", eFlags);
var result = (string) m.Invoke(_formatter, new object[] { _feed.Items.First() });
Assert.AreEqual(expected, result);
}

Hay una alternativa. En lugar de declarar el mtodo como privado, podemos declararlo como internal, esto
es, visible nicamente para las clases del mismo ensamblado. Un usuario de la librera no ver los mtodos
(o clases) marcados con internal a menos que le demos permiso:
using System;
using System.Text;
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("TestRss")]
namespace Rss
{
public class Formatter : IFormatter
{
internal string FormatItem(Item item)
{
return string.Format("{0}: {1}", item.Date, item.Title);
}
public string FormatFeed(Feed feed)
{
var sb = new StringBuilder("*** ");
sb.Append(feed.Title);
sb.Append(Environment.NewLine);
foreach (var item in feed.Items)
{
sb.Append(this.FormatItem(item));
sb.Append(Environment.NewLine);
}
return sb.ToString();
}
}
}

133

Buenas prcticas en desarrollo de software con .NET

Gracias al atributo InternalsVisibleTo, que marca todo el ensamblado, TestRss puede ver los mtodos
declarados internal. La versin de TestRss que no hace uso de la reflexin vuelve a ser vlida y es mucho
ms sencilla de programar.
La marca internal puede usarse tambin sobre clases.

8.12 Una refactorizacin


La forma en que hemos diseado Rss.Reader es mejorable. En su estado actual, el diseo est bajo la
influencia de la nica aplicacin que hace uso de la clase Rss.Reader. En esta, las URL se leen como
argumentos de la lnea de rdenes y el vector de cadenas args se suministra directamente a
Rss.Reader.Display como la enumeracin de cadenas que espera.
Estara bien que la coleccin de URLs se mantuviera en una propiedad de Rss.Reader y que pudisemos
editar esta coleccin evitando posible URL duplicadas. El mtodo Display dejara de tener argumentos y
recurrira al valor actual de ese parmetro.
Reader.cs
using System.Collections.Generic;
using System;
namespace Rss
{
public class Reader
{
private ILoader _loader;
private IParser _parser;
private IFormatter _formatter;
private IWriter _writer;
public ISet<string> Urls { get; private set; }
public Reader(ILoader loader, IParser parser, IFormatter formatter, IWriter writer)
{
_loader = loader;
_parser = parser;
_formatter = formatter;
_writer = writer;
Urls = new HashSet<string>();
}
public string FeedReport(string xml)
{
var feed = _parser.Parse(xml);
var formatted = _formatter.FormatFeed(feed);
return formatted;
}
public void Display()
{
foreach (var url in Urls)
{
var xml = _loader.Load(url);
var result = FeedReport(xml);
_writer.Write(result);
}
}
}
}

134

Buenas prcticas en desarrollo de software con .NET

(Naturalmente, hemos de modificar las pruebas unitarias.)


Es una solucin razonable. Pero no la mejor. La lgica de la gestin de las URL nos ha parecido tan sencilla
que una simple propiedad bastaba para dar solucin a la demanda. Y si esta lgica ha de complicarse ms
adelante? Y si, por ejemplo, hemos de cargar una serie de URL predefinidas de fichero, o de un servicio web
o de quin sabe qu otro origen? Y si, por poner otro ejemplo, hay una poltica de prohibicin de acceso a
ciertas URL? Nuestra clase no sabe de esas gestiones ms complejas o polticas de lista negra, cuando un
buen diseo permitira inyectarlas.
Ese diseo mejorado pasa, nuevamente, por crear una clase con la responsabilidad de gestionar el registro
de URLs.
IUrlManager.cs
using System.Collections.Generic;
namespace Rss
{
public interface IUrlManager : IEnumerable<string>
{
void Add(string url);
}
}

UrlManager.cs
using System.Collections.Generic;
namespace Rss
{
public class UrlManager : IUrlManager
{
private readonly ICollection<string> _urls;
public UrlManager()
{
_urls = new HashSet<string>();
}
#region IUrlManager Members
public void Add(string url)
{
_urls.Add(url);
}
#endregion
#region IEnumerable<string> Members
public IEnumerator<string> GetEnumerator()
{
return _urls.GetEnumerator();
}
#endregion
#region IEnumerable Members
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{

135

Buenas prcticas en desarrollo de software con .NET

return GetEnumerator();
}
#endregion
}
}

Reader.cs
using System.Collections.Generic;
using System;
namespace Rss
{
public class Reader
{
private ILoader _loader;
private IParser _parser;
private IFormatter _formatter;
private IWriter _writer;
private IUrlManager _urlManager;
public Reader(ILoader loader, IParser parser, IFormatter formatter,
IWriter writer, IUrlManager urlManager)
{
_loader = loader;
_parser = parser;
_formatter = formatter;
_writer = writer;
_urlManager = urlManager;
}
public string FeedReport(string xml)
{
var feed = _parser.Parse(xml);
var formatted = _formatter.FormatFeed(feed);
return formatted;
}
public void Display()
{
foreach (var url in _urlManager)
{
var xml = _loader.Load(url);
var result = FeedReport(xml);
_writer.Write(result);
}
}
}
}

AppRssReader.cs
namespace RssReaderApp
{
class Program
{
static void Main(string[] args)
{
var loader = new Rss.Loader();
var parser = new Rss. Parser();
var formatter = new Rss.Formatter();
var writer = new Rss.Writer();
var urlManager = new Rss.UrlManager();
foreach (var arg in args)

136

Buenas prcticas en desarrollo de software con .NET

{
urlManager.Add(arg);
}
var reader = new Rss.Reader(loader, parser, formatter, writer, urlManager);
reader.Display();
System.Console.ReadKey();
}
}
}

Tambin los test se ven afectados, y ya contienen una zona comn que podemos refactorizar definiendo una
rutina SetUp:
TestReader.cs

[TestFixture]
public class TestReader
{
private Mock<Rss.ILoader> _loaderMock;
private Mock<Rss.IParser> _parserMock;
private Mock<Rss.IFormatter> _formatterMock;
private Mock<Rss.IWriter> _writerMock;
private Mock<Rss.IUrlManager> _urlManagerMock;
private Rss.Reader _reader;
[SetUp]
public void Prepara()
{
_loaderMock = new Mock<Rss.ILoader>(MockBehavior.Strict);
_loaderMock.Setup(l => l.Load(SomeConstants.url))
.Returns(SomeConstants.input);
_parserMock = new Mock<Rss.IParser>(MockBehavior.Strict);
_parserMock.Setup(p => p.Parse(It.IsAny<string>()))
.Returns(SomeConstants.intermediate);

_formatterMock = new Mock<Rss.IFormatter>(MockBehavior.Strict);


_formatterMock.Setup(f => f.FormatFeed(SomeConstants.intermediate))
.Returns(SomeConstants.output);
_writerMock = new Mock<Rss.IWriter>(MockBehavior.Strict);
_writerMock.SetupGet(w => w.Write);
_urlManagerMock = new Mock<Rss.IUrlManager>(MockBehavior.Strict);
_urlManagerMock.Setup(u => u.Add(SomeConstants.url));
_urlManagerMock.Setup(u => u.GetEnumerator())
.Returns(Enumerable.Repeat(SomeConstants.url, 1).GetEnumerator());
_reader = new Rss.Reader(_loaderMock.Object, _parserMock.Object,
_formatterMock.Object, _writerMock.Object,
_urlManagerMock.Object);
}
[Test]
public void FeedReport_RecibeXmlBienFormado_DevuelveInformeCompleto()
{
var result = _reader.FeedReport(SomeConstants.input);
_loaderMock.Verify(l => l.Load(SomeConstants.url), Times.Never());
_parserMock.Verify(p => p.Parse(It.IsAny<string>()), Times.Once());
_formatterMock.Verify(f => f.FormatFeed(SomeConstants.intermediate), Times.Once());
_urlManagerMock.Verify(u => u.GetEnumerator(), Times.Never());

137

Buenas prcticas en desarrollo de software con .NET

Assert.AreEqual(SomeConstants.output, result);
}
[Test]
public void Display_RecibeUrls_MuestraPorPantallaInformeCompleto()
{
var sw = new StringWriter();
_writerMock.SetupGet(w => w.Write).Returns(sw.Write);
_reader.Display();
var result = sw.ToString();
_loaderMock.Verify(l => l.Load(SomeConstants.url), Times.Once());
_parserMock.Verify(p => p.Parse(SomeConstants.input), Times.Once());
_formatterMock.Verify(f => f.FormatFeed(SomeConstants.intermediate), Times.Once());
_writerMock.VerifyGet(w => w.Write, Times.Once());
_urlManagerMock.Verify(u => u.GetEnumerator(), Times.Once());
Assert.AreEqual(SomeConstants.output, result);
}
}

8.13 Ms sobre Moq


No podemos poner en prctica todo lo que Moq ofrece con este ejemplo sin forzar excesivamente la
mquina. No obstante, conviene que mostremos algunas de las posibilidades que ofrece ms all de las que
hemos plasmado en el ejemplo. Las que mostramos y comentamos con cierto detalle se han seleccionado de
entra las que se recogen en el documento http://code.google.com/p/moq/wiki/QuickStart.
8.13.1 Acceso a argumentos en el valor de retorno
Podemos acceder a los propios argumentos del mtodo cuando preparamos el resultado de una llamada a
mtodo impostada:
mock.Setup(x => x.DoSomething(It.IsAny<string>())).Returns((string s) => s.ToLower());

El valor de s es el del parmetro del mtodo DoSomething. Especificamos que no importa la cadena que nos
suministren, devolvemos la misma cadena en su versin con minsculas.
8.13.2 Lanzamiento de excepciones cuando se llama a la funcin
Es posible hacer que se lance una funcin cuando se llama a un mtodo. Hay dos formas de especificar este
comportamiento:
mock.Setup(foo => foo.DoSomething("reset")).Throws<InvalidOperationException>();
mock.Setup(foo => foo.DoSomething("")).Throws(new ArgumentException("command");

8.13.3 Evaluacin perezosa del valor de retorno


Hay una diferencia importante entre estas dos especificaciones de comportamiento:
mock.Setup(foo => foo.GetCount()).Returns(count);
mock.Setup(foo => foo.GetCount()).Returns(() => count);

En la primera, la llamada a GetCount devolver el valor que tena la variable count en el momento en el que
se defini el comportamiento. En la segunda, se devolver el valor que tenga en el momento de la ejecucin
de la llamada a GetCount sobre el stub. En la segunda llamada se usa una lambda-funcin que crea una
clausura que incorpora a la variable count, por lo que se puede acceder a su valor en cualquier momento.

138

Buenas prcticas en desarrollo de software con .NET

8.13.4 Retrollamadas (callbacks)


Con Callback podemos invocar una funcin antes o despus de una llamada a mtodo. En este ejemplo, se
llamar a una funcin que incremente el valor de una variable calls (atrapada en la clausura) tras impostar
la llamada a Execute:
var mock = new Mock<IFoo>();
mock.Setup(foo => foo.Execute("ping"))
.Returns(true)
.Callback(() => calls++);

La retrollamada puede usar como argumentos los propios de la funcin tras la que se invoca. En este
ejemplo, s es la cadena que se suministra a Execute:
mock.Setup(foo => foo.Execute(It.IsAny<string>()))
.Returns(true)
.Callback((string s) => calls.Add(s));

Si la funcin tuviera ms de un parmetro, se podra invocar as:


mock.Setup(foo => foo.Execute(It.IsAny<int>(), It.IsAny<string>()))
.Returns(true)
.Callback<int, string>((i, s) => calls.Add(s));

Se puede poner una retrollamada antes de la invocacin y otra despus:


mock.Setup(foo => foo.Execute("ping"))
.Callback(() => Console.WriteLine("Before returns"))
.Returns(true)
.Callback(() => Console.WriteLine("After returns"));

8.13.5 Devolucin de valores diferentes para diferentes invocaciones a un mtodo con los
mismos argumentos
Si hemos definido un valor de retorno con evaluacin perezosa, esto es, con una clausura, la llamada a la
funcin puede cambiar el valor de las variables atrapadas en la clausura, proporcionando as valores
distintos con cada llamada:
var mock = new Mock<IFoo>();
var calls = 0;
mock.Setup(foo => foo.GetCountThing())
.Returns(() => calls)
.Callback(() => calls++);
Console.WriteLine(mock.Object.GetCountThing());

En este ejemplo, la primera llamada devuelve el valor 0 y la segunda devuelve el valor 1.


8.13.6 Acceso y asignacin de propiedades
Se puede controlar el acceso a una propiedad e indicar el valor de retorno:
mock.Setup(foo => foo.Name).Returns("bar");

Y se puede declarar la expectativa de que se asigne un cierto valor a una propiedad:


mock.SetupSet(foo => foo.Name = "foo");

139

Buenas prcticas en desarrollo de software con .NET

8.13.7 Indicar que una propiedad se comporte como un stub


Las propiedades del stub de un mock no funcionan como tales propiedades. Al definir comportamientos
decimos que esperamos que se acceda y definimos el valor que debe proporcionar el acceso, o definimos la
expectativa de una asignacin (como hemos visto en el apartado anterior), pero las propiedades en s no
memorizan nada. Si queremos que la propiedad funcione como tal, hemos de indicarlo explcitamente:
mock.SetupProperty(f => f.Name);
mock.SetupProperty(f => f.Name, "foo");
IFoo foo = mock.Object;
Assert.Equal("foo", foo.Name);
foo.Name = "bar";
Assert.Equal("bar", foo.Name);

La primera lnea indica que mock.Object.Name debe comportarse como una propiedad de verdad. La
segunda lnea asigna, adems, un valor por defecto a la propiedad. El primer aserto funciona precisamente
porque la propiedad tiene el valor por defecto. El segundo aserto funciona porque la asignacin de la
penltima lnea ha sido efectiva.
8.13.8 Indicar que todas las propiedades deben comportase como un stub
mock.SetupAllProperties();

8.13.9 Lanzamiento de eventos


Si una interfaz define un evento:
public delegate void MyEventHandler(int i, bool b);
public interface IFoo
{
event MyEventHandler MyEvent;
}

Es posible invocar el evento a voluntad con una sintaxis un tanto especial.


var mock = new Mock<IFoo>();
mock.Raise(foo => foo.MyEvent += null, 25, true);

8.13.10

Verificacin de que se ha accedido a una propiedad

mock.VerifyGet(foo => foo.Name);

8.13.11 Verificacin de que se ha asignado valor a una propiedad


Si no importa el valor asignado:
mock.VerifySet(foo => foo.Name);

Si deseamos asegurarnos de que el valor es uno determinado:


mock.VerifySet(foo => foo.Name = "foo");

Si deseamos asegurarnos de que el valor cumple cierta condicin:


mock.VerifySet(foo => foo.Value = It.IsInRange(1, 5, Range.Inclusive));

140

Buenas prcticas en desarrollo de software con .NET

8.14 Antes de acabar


No hemos acabado an nuestra aplicacin. Trataremos algunas cuestiones de extensin de la funcionalidad
y las aplicaremos a nuestra aplicacin, pero lo haremos en el marco de una explicacin de la inyeccin de
dependencias.
Ye hemos alcanzado un diseo flexible, que hace utilizable nuestra clase Rss.Reader en diferentes
escenarios. Y esa flexibilidad empezar a compensar ahora mismo.

8.15 Un hecho (no tan) inesperado


Nos enfrentamos a un descubrimiento de ltima hora. No hay un solo formato de uso comn en la
publicacin de contenidos RSS. Hay dos estndares de uso comn:

RSS 2.0, del que es ejemplo el texto XML que hemos venido usando en los ejemplos.
Atom 1.0, que tambin es texto XML, pero que sigue una especificacin distinta que se puede
consultar en http://www.atomenabled.org/developers/syndication/atom-format-spec.php.

Para saber ms, conviene acudir a la Wikipedia: http://en.wikipedia.org/wiki/Atom_%28standard%29.


Cmo nos enfrentamos a este problema? Desde luego, mucho mejor con una librera flexible y desacoplada
como la nuestra que con la clase Dios que habamos empezado a disear al principio.
Empezaremos renombrando nuestro Parser y haciendo que se llame Rss2Parser. Las herramientas de
refactorizacin hacen que esto resulte trivial. Crearemos a continuacin una clase Atom1Parser:
using System.IO;
using System.Linq;
using System.Xml.Linq;
namespace Rss
{
public class Atom1Parser : IParser
{
public Feed Parse(string xml)
{
XDocument xdoc = XDocument.Load(new StringReader(xml));
XNamespace ns = "http://www.w3.org/2005/Atom";
var feed = new Feed(
title: xdoc.Element(ns + "feed").Element(ns + "title").Value,
items: from elt in xdoc.Element(ns + "feed").Elements(ns + "entry")
select new Item(
date: elt.Element(ns + "updated").Value,
title: elt.Element(ns + "title").Value
));
return feed;
}
}
}

No olvidemos las pruebas unitarias para esta clase (usando como cdigo de ejemplo de datos Atom 1.0 el
que aparece en la pgina de la Wikipedia):
using
using
using
using
using
using

System;
System.Collections.Generic;
System.Linq;
System.Text;
NUnit.Framework;
Rss;

141

Buenas prcticas en desarrollo de software con .NET

namespace TestRss
{
[TestFixture]
public class TestAtom1Parser
{
private Rss.Atom1Parser _parser;
[SetUp]
public void PreparaTestParser()
{
_parser = new Atom1Parser();
}

[Test]
public void Parse_ConWellFormedXml_ReturnsFeed()
{
var xml =
@"<?xml version='1.0' encoding='utf-8'?>
<feed xmlns='http://www.w3.org/2005/Atom'>
<title>Example Feed</title>
<subtitle>A subtitle.</subtitle>
<link href='http://example.org/feed/' rel='self' />
<link href='http://example.org/' />
<id>urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6</id>
<updated>2003-12-13T18:30:02Z</updated>
<author>
<name>John Doe</name>
<email>johndoe@example.com</email>
</author>
<entry>
<title>Atom-Powered Robots Run Amok</title>
<link href='http://example.org/2003/12/13/atom03' />
<link rel='alternate' type='text/html'
href='http://example.org/2003/12/13/atom03.html'/>
<link rel='edit' href='http://example.org/2003/12/13/atom03/edit'/>
<id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
<updated>2003-12-13T18:30:02Z</updated>
<summary>Some text.</summary>
</entry>
</feed>
";
var expected = new Rss.Feed(
title: "Example Feed",
items: new List<Rss.Item>
{
new Rss.Item(
date: "2003-12-13T18:30:02Z",
title: "Atom-Powered Robots Run Amok"
),
});
var result = _parser.Parse(xml);
Assert.AreEqual(expected, result);
}
}
}

Ejecutamos las pruebas y verde.


Bueno. Ahora tenemos un problema. Qu analizador escogemos en nuestro lector? Tenemos un analizador
de Rss 2.0 y otro de Atom 1.0. Puedo seleccionar uno e inyectarlo en el lector cuando se construye, pero
entonces nuestro lector slo sabr tratar con uno de los dos formatos.

142

Buenas prcticas en desarrollo de software con .NET

Una solucin es hacer un analizador que pueda trabajar con los dos formatos:
Rss2AndAtom1Parser.cs
using System.Xml.Linq;
using System.IO;
namespace Rss
{
public class Rss2AndAtom1Parser : IParser
{
private Atom1Parser _atom1Parser;
private Rss2Parser _rss2Parser;
public Rss2AndAtom1Parser()
{
_atom1Parser = new Atom1Parser();
_rss2Parser = new Rss2Parser();
}
#region IParser Members
public Feed Parse(string xml)
{
XDocument xdoc = XDocument.Load(new StringReader(xml));
XNamespace ns = "http://www.w3.org/2005/Atom";
if (xdoc.Element(ns + "feed") != null)
{
return _atom1Parser.Parse(xml);
}
else
{
return _rss2Parser.Parse(xml);
}
}
#endregion
}
}

Nuestra aplicacin dispone ahora de una herramienta de anlisis ms potente. Hagamos que la use
inyectndola al lector:
namespace RssReaderApp
{
class Program
{
static void Main(string[] args)
{
var loader = new Rss.Loader();
var parser = new Rss.Rss2AndAtom1Parser();
var formatter = new Rss.Formatter();
var writer = new Rss.Writer();
var urlManager = new Rss.UrlManager();
foreach (var arg in args)
{
urlManager.Add(arg);
}
var reader = new Rss.Reader(loader, parser, formatter, writer, urlManager);
reader.Display();
System.Console.ReadKey();
}
}
}

143

Buenas prcticas en desarrollo de software con .NET

El esfuerzo en el desarrollo de la aplicacin ha sido considerable, pero es ahora cuando empieza a


compensar:

La inyeccin de dependencias ha facilitado ahora la sustitucin de unos componentes por otros, lo


que aumenta la capacidad de adaptacin de la librera a nuevos escenarios.
Los cambios son ms seguros si disponemos de una batera de pruebas unitarias que nos aseguran
que no hemos roto nada que ya estuviera funcionando (y eso que el desarrollo presentado no ha
creado todas las pruebas unitarias que debisemos haber creado, pues queramos centrarnos en
otros aspectos del desarrollo).

9 Configuracin de aplicacin va fichero de configuracin


Uno de los problemas de codificar todo lo relativo a la aplicacin en el cdigo que compilamos es que
introducimos rigideces en nuestro diseo. Si un lector de RSS cablea en el cdigo las URL de las fuentes RSS
(o algunas URL de fuentes RSS) a las que accede, el cdigo depende de ellas. Si ms adelante hubiese que
cambiar alguna de las fuentes RSS, tendramos que volver a editar el cdigo fuente, compilar la aplicacin y
desplegarla nuevamente. Hay ms datos de una aplicacin que pueden cambiar y plantean el mismo tipo de
problemas: la conexin a una base de datos, el nivel de traza en el registro de actividad con el que deseamos
ejecutar una aplicacin, etc.
Hay una solucin obvia a este problema: registrar los datos de configuracin que pueden cambiar en algn
fichero que podamos editar una vez se ha desplegado la aplicacin. Esto introduce dos problemas de
estandarizacin:

El convenio que hemos de adoptar para que se sepa dnde se registran esos datos, que debera
basarse en algn estndar.
El formato del fichero, que acabar siendo algn lenguaje especfico de dominio que habr que
disear y documentar y para el que habr que disear un analizador.

En .NET el problema se proporciona con una solucin de serie:

Hay un lugar estndar para almacenar los datos: el fichero de configuracin de la aplicacin
app.config. (Hace tiempo Windows usaba ficheros con extensin .ini o el registro del sistema.
Acceder al registro planteaba problemas de seguridad y en algunos sistemas el administrador
impeda el acceso a este recurso, por lo que no es una prctica recomendable.)
No hemos de definir un lenguaje propio, pues el fichero usa XML y un repertorio de marcas
predefinidas.

En este bloque aprenderemos a usar ficheros de configuracin.

9.1 Creacin de un fichero App.Config


Cuando creamos una aplicacin, no se crea automticamente un fichero App.config. Hemos de ir al proyecto
y crearlo manualmente. Vamos al proyecto RssReaderApp y aadamos un fichero de configuracin propio.
En el men contextual del proyecto RssReaderApp seleccionamos Add New ItemApplication
Configuration File y damos al fichero el nombre App.config:

144

Buenas prcticas en desarrollo de software con .NET

El fichero presenta este aspecto inicial:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
</configuration>

Vamos a usar el fichero de configuracin para dos cosas:

Precargar una (o ms) URL aparte de las que se suministren por la lnea de rdenes.
Seleccionar uno de los tres lectores RSS de que disponemos.

Accedemos a App.config a travs de la clase esttica System.Configuration.ConfigurationManager .


En la primera versin de .NET el fichero de configuracin se usaba como poco ms que un gran almacn de
pares clave-valor, donde el valor era una simple cadena. Desde .NET 2.0, es posible crear secciones y tipar la
informacin.

9.2 Secciones definidas por el usuario


9.2.1 Definicin de una seccin propia con atributos de usuario
Vamos a definir una seccin propia para el fichero de configuracin. Nuestro objetivo es poder definir en el
fichero app.config el valor de la URL por defecto para el lector de fuentes RSS as:

<rssReader defaultUrl="http://www.elpais.com/rss/feed.html?feedId=17046"/>

Una marca <rssReader> sealar una seccin del fichero de configuracin app.config con la configuracin
de usuario que nos interesa. En ella, el atributo defaultUrl permitir indicar una URL.
Empezamos creando una clase RssReaderAppConfigurationSection que hereda de la clase
System.Configuration.ConfigurationSection, con lo que estaremos definiendo una seccin en nuestro
fichero de configuracin:

145

Buenas prcticas en desarrollo de software con .NET

using System.Configuration;
namespace RssReaderApp
{
public class RssReaderAppConfigurationSection : ConfigurationSection
{
private static ConfigurationProperty defaultUrl;
private static ConfigurationPropertyCollection properties;
public string DefaultUrl
{
get { return (string)base[defaultUrl]; }
}
protected override ConfigurationPropertyCollection Properties
{
get { return properties; }
}
public RssReaderAppConfigurationSection()
{
defaultUrl = new ConfigurationProperty("defaultUrl", typeof(string),
null, ConfigurationPropertyOptions.IsRequired);
properties = new ConfigurationPropertyCollection();
properties.Add(defaultUrl);
}
}
}

En la clase definimos una propiedad DefaultUrl que no es ms que una pasarela de acceso a una
ConfigurationProperty. Cada atributo XML que definamos (en nuestro caso slo uno: defaultUrl) ser
una ConfigurationProperty.
Es obligatorio definir una coleccin con todas las ConfigurationProperty que hemos definido. Para ello
recurrimos a una propiedad de tipo ConfigurationPropertyCollection , que no hace ms que recoger en
una estructura de datos (mediante invocaciones al mtodo Add) todas la ConfigurationProperty que
hemos creado (en este caso, y por el momento, slo una).
Veamos un fichero de configuracin de la aplicacin que hace uso de esta seccin:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="rssReader"
type="RssReaderApp.RssReaderAppConfigurationSection, RssReaderApp" />
</configSections>
<rssReader defaultUrl="http://www.elpais.com/rss/feed.html?feedId=17046" />
</configuration>

Ntese que hay un prlogo encerrado en la marca <configSections> en el que declaramos que la marca
<rssReader> debe interpretarse a partir del tipo RssReaderApp.RssReaderAppConfigurationSection
que hemos definido en el ensamblado RssReaderApp (que es el de nuestra aplicacin).
Por defecto, las propiedades de configuracin que hemos definido son atributos del elemento de la seccin
que indicaremos con un elemento rssReader. Slo hemos definido por el momento un atributo:
defaultUrl.

146

Buenas prcticas en desarrollo de software con .NET

Y ahora, veamos un ejemplo de aplicacin que carga el contenido del fichero app.config:
using System.Configuration;
namespace RssReaderApp
{
class Program
{
static void Main(string[] args)
{
var config = (RssReaderAppConfigurationSection)
ConfigurationManager.GetSection("rssReader");
var
var
var
var
var

loader = new Rss.Loader();


parser = new Rss.Rss2AndAtom1Parser();
formatter = new Rss.Formatter();
writer = new Rss.Writer();
urlManager = new Rss.UrlManager();

string defaultUrl = config.DefaultUrl;


urlManager.Add(defaultUrl);
foreach (var arg in args)
{
urlManager.Add(arg);
}
var reader = new Rss.Reader(loader, parser, formatter, writer, urlManager);
reader.Display();
System.Console.ReadKey();
}
}
}

La primera lnea de la rutina instancia un objeto de la clase que hemos definido:


RssReaderAppConfigurationSection. El objeto ya viene cargado con los datos del fichero de
configuracin, as que podemos consultar directamente su atributo DefaultUrl.
9.2.2 Un atributo con tipo definido por el usuario
El atributo XML defaultUrl contiene un valor de un tipo estndar y la propiedad DefaultUrl de la nuestra
clase de seccin de configuracin coincide en ser de ese mismo tipo: una simple cadena que se interpreta
como una URL. En ocasiones necesitaremos especificar valores de usuario ms complejos, y eso pasar por
un proceso de interpretacin de la cadena ms sofisticado. Puede que el valor que desee leer el usuario
deba expresarse con un tipo de datos ms elaborado que una cadena, o incluso con un tipo de datos
definido por el usuario.
Vamos a aadir un segundo atributo para definir el tipo de IParser que queremos conectar a nuestro
lector. Ntese que queremos especificar un tipo, pero en un fichero XML como app.config, que no es ms
que un fichero de texto, no hay ms remedio que codificarlo con una cadena. En el fichero se escribir algo
as como:
<rssReader defaultUrl="http://www.elpais.com/rss/feed.html?feedId=17046"
parser="Rss.Rss2Parser, Rss"/>

Pero deseamos que nuestra aplicacin encuentre el trabajo de interpretacin de la cadena "Rss.Rss2Parser,
Rss" ya hecho y lea el valor del atributo como lo que representa: un tipo.
using System.Configuration;

147

Buenas prcticas en desarrollo de software con .NET

namespace RssReaderApp
{
public class RssReaderAppConfigurationSection : ConfigurationSection
{
private static ConfigurationPropertyCollection properties;
private static ConfigurationProperty defaultUrl;
private static ConfigurationProperty parser;
public string DefaultUrl
{
get { return (string)base[defaultUrl]; }
}
public System.Type Parser
{
get
{
System.Type type = System.Type.GetType((string)base[parser]);
if (type.GetInterface("Rss.IParser") != null)
{
return type;
}
else
{
throw new System.ArgumentException(string.Format("{0} is not an Rss.IParser",
base[parser]));
}
}
}
public RssReaderAppConfigurationSection()
{
defaultUrl = new ConfigurationProperty("defaultUrl", typeof(string),
null, ConfigurationPropertyOptions.IsRequired);
parser = new ConfigurationProperty("parser", typeof(string),
null, ConfigurationPropertyOptions.IsRequired);
properties = new ConfigurationPropertyCollection();
properties.Add(defaultUrl);
properties.Add(parser);
}
protected override ConfigurationPropertyCollection Properties
{
get { return properties; }
}
}
}

El fichero de configuracin pasa a tener este aspecto:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="rssReader"
type="RssReaderApp.RssReaderAppConfigurationSection, RssReaderApp" />
</configSections>
<rssReader defaultUrl="http://www.elpais.com/rss/feed.html?feedId=17046"
parser="Rss.Rss2Parser, Rss"/>
</configuration>

148

Buenas prcticas en desarrollo de software con .NET

El nuevo atributo XML, parser, tiene como valor una cadena con el nombre completo de un tipo y, separado
por una coma, el nombre del ensamblado en el que reside el tipo. Es el formato que necesita el mtodo
System.Type.GetType para generar el tipo a partir de una cadena. La propiedad Parser, en su mtodo get,
hace la transformacin.
Veamos cmo usar el valor en nuestra aplicacin:
using System.Configuration;
using System;
namespace RssReaderApp
{
class Program
{
static void Main(string[] args)
{
var config = (RssReaderAppConfigurationSection)
ConfigurationManager.GetSection("rssReader");
var
var
var
var
var

loader = new Rss.Loader();


parser = (Rss.IParser)Activator.CreateInstance(config.Parser);
formatter = new Rss.Formatter();
writer = new Rss.Writer();
urlManager = new Rss.UrlManager();

string defaultUrl = config.DefaultUrl;


urlManager.Add(defaultUrl);
foreach (var arg in args)
{
urlManager.Add(arg);
}
var reader = new Rss.Reader(loader, parser, formatter, writer, urlManager);
reader.Display();
System.Console.ReadKey();
}
}
}

Hemos usado la factora Activator.CreateInstance para construir una instancia del tipo. El valor de
config.Parse es de tipo System.Type. Tenemos la seguridad de que el tipo implementa la interfaz
Rss.IParser porque nuestro intrprete del fichero de configuracin se ha asegurado de ello.
9.2.3 Secciones con elementos anidados
Los atributos de una marca XML son muy limitados para expresar valores complejos y de momento slo
sabemos crear este tipo de componentes en nuestros ficheros de configuracin. Vamos a empezar ahora
convirtiendo el atributo defaultUrl en un elemento anidado <url> con un atributo value. Es decir,
podremos especificar una URL as:
<rssReader parser="Rss.Rss2Parser, Rss">
<url value="http://www.elpais.com/rss/feed.html?feedId=17046" />
</rssReader>

Ms tarde haremos que ese nuevo elemento XML permita la especificacin de una coleccin de valores, en
lugar de permitir expresar un solo valor.
using System.Configuration;
namespace RssReaderApp

149

Buenas prcticas en desarrollo de software con .NET

{
public class UrlElement : ConfigurationElement
{
private static ConfigurationPropertyCollection properties;
private static ConfigurationProperty value;
public string Value
{
get { return (string)base[value]; }
}
public UrlElement()
{
value = new ConfigurationProperty("value", typeof(string),
null, ConfigurationPropertyOptions.IsRequired);
properties = new ConfigurationPropertyCollection { value };
}
protected override ConfigurationPropertyCollection Properties
{
get { return properties; }
}
}
public class RssReaderAppConfigurationSection : ConfigurationSection
{
private static ConfigurationPropertyCollection properties;
private static ConfigurationProperty parser;
private static ConfigurationProperty url;
public System.Type Parser
{
get
{
System.Type type = System.Type.GetType((string)base[parser]);
if (type.GetInterface("Rss.IParser") != null)
{
return type;
}
else
{
throw new System.ArgumentException(string.Format("{0} is not an Rss.IParser",
base[parser]));
}
}
}
public UrlElement Url
{
get { return (UrlElement) base[url]; }
}
public RssReaderAppConfigurationSection()
{
parser = new ConfigurationProperty("parser", typeof(string),
null, ConfigurationPropertyOptions.IsRequired);
url = new ConfigurationProperty("url", typeof(UrlElement),
null, ConfigurationPropertyOptions.IsRequired);
properties = new ConfigurationPropertyCollection();
properties.Add(url);
properties.Add(parser);
}
protected override ConfigurationPropertyCollection Properties
{
get { return properties; }

150

Buenas prcticas en desarrollo de software con .NET

}
}
}

Un fichero de configuracin vlido quedara ahora as:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="rssReader"
type="RssReaderApp.RssReaderAppConfigurationSection, RssReaderApp" />
</configSections>
<rssReader parser="Rss.Rss2Parser, Rss">
<url value="http://www.elpais.com/rss/feed.html?feedId=17046" />
</rssReader>
</configuration>

Vale la pena advertir que en RssReaderAppConfigurationSection no hay demasiada diferencia entre la


definicin de un atributo XML y un elemento XML. La diferencia es que la propiedad Url devuelve un objeto
de tipo UrlElement, que es un objeto de tipo ConfigurationElement.
La forma de acceder al valor de la URL tiene ahora dos niveles:
using System.Configuration;
using System;
namespace RssReaderApp
{
class Program
{
static void Main(string[] args)
{
var config = (RssReaderAppConfigurationSection)
ConfigurationManager.GetSection("rssReader");
var loader = new Rss.Loader();
var parser = (Rss.IParser)Activator.CreateInstance(config.Parser);
var formatter = new Rss.Formatter();
var writer = new Rss.Writer();
var urlManager = new Rss.UrlManager();
string defaultUrl = config.Url.Value;
urlManager.Add(defaultUrl);
foreach (var arg in args)
{
urlManager.Add(arg);
}
var reader = new Rss.Reader(loader, parser, formatter, writer, urlManager);
reader.Display();
System.Console.ReadKey();
}
}
}

9.2.4 Secciones con elementos de tipo coleccin


No tenemos por qu limitarnos a una sola URL definida en el fichero de configuracin. Para poner varias
podramos utilizar algn truco, como disear un formato para el valor que se proporciona en el atributo del
elemento <url> (separando, por ejemplo, diferentes URLs con comas). Es decir, podramos usr un
separador, como el punto y coma, para expresar as dos URL:

151

Buenas prcticas en desarrollo de software con .NET

<rssReader parser="Rss.Rss2Parser, Rss">


<url value="http://fuente1.com/rss;http://fuente2.com/rss" />
</rssReader>

Pero hay una forma ms elegante: definir el elemento como una coleccin de valores y usar un elemento
XML para cada URL. As:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="rssReader"
type="RssReaderApp.RssReaderAppConfigurationSection, RssReaderApp" />
</configSections>
<rssReader parser="Rss.Rss2Parser, Rss">
<url>
<add value="http://www.elpais.com/rss/feed.html?feedId=17046" />
<add value="http://www.meneame.net/rss2.php" />
</url>
</rssReader>
</configuration>

Existe un tipo de coleccin de valores predefinido que se puede comportar como una lista de elementos o
como un diccionario en el que uno de los atributos de cada elemento se comporta como clave. En el cdigo
que mostramos ilustramos los dos posibles comportamientos, aunque de un modo forzado, pues usamos el
propio valor como clave. Aunque forzado, el ejemplo permite una extensin inmediata al modelo de
diccionario.
using System.Configuration;
namespace RssReaderApp
{
public class UrlElement : ConfigurationElement
{
private static ConfigurationPropertyCollection properties;
private static ConfigurationProperty value;
public string Value
{
get { return (string)base[value]; }
}
public UrlElement()
{
value = new ConfigurationProperty("value", typeof(string),
null, ConfigurationPropertyOptions.IsRequired);
properties = new ConfigurationPropertyCollection { value };
}
protected override ConfigurationPropertyCollection Properties
{
get { return properties; }
}
}
[ConfigurationCollection(typeof(UrlElement),
CollectionType=ConfigurationElementCollectionType.AddRemoveClearMap)]
public class UrlElementCollection : ConfigurationElementCollection
{
private static ConfigurationPropertyCollection properties;
static UrlElementCollection()

152

Buenas prcticas en desarrollo de software con .NET

{
properties = new ConfigurationPropertyCollection();
}
public UrlElementCollection()
{
}
protected override ConfigurationPropertyCollection Properties
{
get { return properties; }
}
public override ConfigurationElementCollectionType CollectionType
{
get { return ConfigurationElementCollectionType.AddRemoveClearMap; }
}
public UrlElement this[int index]
{
get { return (UrlElement)base.BaseGet(index); }
set
{
if (base.BaseGet(index) != null)
{
base.BaseRemoveAt(index);
}
base.BaseAdd(index, value);
}
}
public UrlElement this[string name]
{
get { return (UrlElement)base.BaseGet(name); }
}
protected override ConfigurationElement CreateNewElement()
{
return new UrlElement();
}
protected override object GetElementKey(ConfigurationElement element)
{
return (element as UrlElement).Value;
}
}
public class RssReaderAppConfigurationSection : ConfigurationSection
{
private static ConfigurationPropertyCollection properties;
private static ConfigurationProperty parser;
private static ConfigurationProperty url;
public System.Type Parser
{
get
{
System.Type type = System.Type.GetType((string)base[parser]);
if (type.GetInterface("Rss.IParser") != null)
{
return type;
}
else
{
throw new System.ArgumentException(string.Format("{0} is not an Rss.IParser",
base[parser]));

153

Buenas prcticas en desarrollo de software con .NET

}
}
}
public UrlElementCollection Url
{
get { return (UrlElementCollection)base[url]; }
}
public RssReaderAppConfigurationSection()
{
parser = new ConfigurationProperty("parser", typeof(string),
null, ConfigurationPropertyOptions.IsRequired);
url = new ConfigurationProperty("url", typeof(UrlElementCollection),
null, ConfigurationPropertyOptions.IsRequired);
properties = new ConfigurationPropertyCollection();
properties.Add(url);
properties.Add(parser);
}
protected override ConfigurationPropertyCollection Properties
{
get { return properties; }
}
}
}

Y el cdigo que hace uso de la configuracin:


using System.Configuration;
using System;
namespace RssReaderApp
{
class Program
{
static void Main(string[] args)
{
var config = (RssReaderAppConfigurationSection)
ConfigurationManager.GetSection("rssReader");
var
var
var
var

loader = new Rss.Loader();


parser = (Rss.IParser)Activator.CreateInstance(config.Parser);
formatter = new Rss.Formatter();
writer = new Rss.Writer();

var urlManager = new Rss.UrlManager();


foreach (UrlElement url in config.Url)
{
urlManager.Add(url.Value);
}
foreach (var arg in args)
{
urlManager.Add(arg);
}
var reader = new Rss.Reader(loader, parser, formatter, writer, urlManager);
reader.Display();
System.Console.ReadKey();
}
}
}

154

Buenas prcticas en desarrollo de software con .NET

Es posible ir ms all en la definicin de ficheros de configuracin, definiendo grupos de secciones o


colecciones ms potentes. Pero lo estudiado permite crear modelos razonablemente potentes y resulta
suficiente para la mayora de las aplicaciones.
Por otra parte, hemos desvelado parte de la magia que veremos en uso en temas posteriores, y ese es uno
de los objetivos: que entendamos las bases sobre las que se construyen las herramientas que nos han de
acompaar en el desarrollo de software.

10 Registro de actividad
Durante el proceso de diseo e implementacin del software tenemos gran control sobre la ejecucin del
cdigo. Podemos someterlo a pruebas y, ante un fallo, podemos ejecutar la aplicacin con un depurador
para trazar el origen de los problemas y corregirlos. Al depurar, una prctica habitual consiste en imprimir
por consola mensajes con, posiblemente, el valor de ciertas variables. En explotacin contamos con menos
posibilidades, a menos que no nos importe desplegar software que lanza constantemente mensajes por
consola.
Una de las prcticas que deben considerarse es el uso de sistema de registro de actividad o, como se dice en
ingls, un logger. Los sistemas de registro de actividades (o de logging) guardan informacin sobre los
puntos relevantes por los que pasa un programa y, posiblemente, tambin informacin de estado al pasar
por ellos. Idealmente debera ser posible saber en qu punto (aproximado) de ejecucin se encuentra un
programa examinando los volcados del registro de actividad.
El logging no slo vale para sistemas en explotacin. Tambin pueden ser de gran ayuda en desarrollo:
ayuda a localizar puntos problemticos y orientar rpidamente el esfuerzo de depuracin.
Existen varios mdulos de logging (como Microsoft Logging Application Block o LucidLog.Net) y, ciertamente,
construir uno propio no es excesivamente complejo si se desea una funcionalidad bsica. El estndar de
facto es Log4Net, un desarrollo inspirado en una librera de logging para Java (Log4J). Log4Net es cdigo
abierto mantenido por Apache Software Foundation.
Antes de hablar de Log4Net, veamos por encima una forma ms primitiva de mostrar mensajes slo en
depuracin o traza de nuestro software.

10.1 System.Diagnostics
El espacio de nombres System.Diagnostics, estndar en .NET e integrado en las libreras bsicas, ofrece
dos clases tiles para mostrar mensajes al ejecutar un programa en modo DEBUG o TRACE. La clase esttica
Debug (que bsicamente es igual que Trace) ofrece unos mtodos tiles:

Assert(expresin booleana): detiene la ejecucin si el valor booleano es falso.


Write(mensaje) y WriteLine(mensaje): muestra por pantalla un mensaje, sin o con un salto de

lnea al final.
WriteIf y WriteIf(expresin booleana, mensaje) : muestra por pantalla el mensaje si y slo si
el valor del primer argumento es cierto.
Indent()/Unindent(): aade/elimina sangrado a la salida.

155

Buenas prcticas en desarrollo de software con .NET

La gracia est en que estas funciones slo son efectivas si el programa se ejecuta con la variable pragma
DEBUG definida, es decir, cuando estamos desarrollando. Si compilamos para Release, las llamadas a esos
mtodos no generan cdigo.
La clase Debug (y Trace) permite aadir escuchadores, es decir, delegados que se invocan con cada
evento. A travs suyo podemos registrar los eventos en una base de datos, en un fichero, etc.
He aqu un ejemplo de uso:
System.Diagnostics.Debug.Assert(true, "Un mensaje",
"Este mensaje se muestra en la ventana de depuracin.");
System.Diagnostics.Debug.Indent();
System.Diagnostics.Debug.WriteLine("Un nmero: {0}", 3);
System.Diagnostics.Debug.Unindent();
System.Diagnostics.Debug.WriteLineIf(10 % 2 == 0, "Nmero par.");

Veremos cmo los sistemas de logging son ms potentes, igual de flexibles o ms, y vienen con las pilas
puestas.

10.2 Instalacin de Log4Net


Log4Net se distribuye como un paquete zip que podemos descargar del sitio de Apache Software
Foundation, concretamente de http://logging.apache.org/log4net/download.html. Log4Net est an en fase
de incubacin (aunque ya lleva aos en desarrollo), as que el paquete que bajamos empieza por
incubating. A fecha de hoy, la ltima versin es incubating-log4net-1.2.10.zip. El paquete contiene los
fuentes, la documentacin, pruebas unitarias, algunos ejemplos y binarios para diferentes plataformas
(Mono, .NET, .NET Compact Framework, etc.). Una vez hayamos descomprimido el paquete tendremos
acceso a la librera en formato DLL log4net-1.2.10\bin\net\2.0\release\log4net.dll. Ese es el
ensamblado que hemos de aadir a las referencias de los proyectos que usen Log4Net.6

10.3 Lo bsico
Hay cinco niveles de importancia en las actividades que podemos registrar y, aunque no hay ninguna
obligacin de cada una represente algo especfico, si suele asociarse por convenio cierta semntica a cada
nivel:

FATAL

Se ha detectado un acontecimiento que debera detener la ejecucin del software.

Pero si hacemos esto, sin ms, es probable que tengamos problemas. Ante una aplicacin de consola, por
ejemplo, al compilar obtendremos un mensaje (Warning) como ste:
The referenced assembly "log4net" could not be resolved because it has a dependency on
"System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" which is not in
the currently targeted framework ".NETFramework,Version=v4.0,Profile=Client". Please remove
references to assemblies not in the targeted framework or consider retargeting your project.

Para eliminar el problema hemos de ir a Properties en el men contextual de la aplicacin y cambiar el


Target, que pasar de .NET Framework 4 Client Profile a .NET Framework 4. Es un problema frecuente al usar
libreras que se han compilado con ese Target.

156

Buenas prcticas en desarrollo de software con .NET

ERROR

Se ha detectado un acontecimiento que no debera ocurrir, por lo que el software debera entrar en
un proceso de recuperacin.

WARN

Se ha detectado un acontecimiento anmalo, pero que no tiene impacto negativo en el


funcionamiento de la aplicacin, y se deja constancia.

INFO

Se ha detectado un acontecimiento del que se desea dejar constancia (un usuario se registr en el
sistema, se complet con xito una transaccin, etc.).

DEBUG

Se ha detectado un acontecimiento que interesa controlar nicamente mientras se desarrolla el


sistema y, posiblemente, se est en fase de depuracin del cdigo.
Al llamar al logger, lo haremos indicando el nivel de importancia. La configuracin de la aplicacin puede
determinar de qu nivel en adelante (de menos importante a ms) vale la pena registrar acontecimientos. Si
fijamos el nivel en WARN, por ejemplo, se registrarn nicamente los eventos de nivel WARN, ERROR y FATAL,
aunque en nuestro cdigo tambin haya llamadas con niveles INFO y DEBUG. La configuracin admite estos
otros dos valores.

OFF: No registrar nada.


ALL: Registrar todo.

10.4 Un primer ejemplo


Creemos una aplicacin de consola para probar los cinco niveles. El cdigo del programa es este:
using System;
namespace ParaLogging
{
class Program
{
static void Main(string[] args)
{
log4net.Config.BasicConfigurator.Configure();
log4net.ILog log = log4net.LogManager.GetLogger(typeof(Program));
log.Debug("Esto es DEBUG");
log.Info("Esto es INFO");
log.Warn("Esto es WARN");
log.Error("Esto es ERROR");
log.Fatal("Esto es FATAL");
Console.ReadKey();
}
}
}

No olvidemos incluir la DLL en las referencias del proyecto.7


Lo primero que hace el programa es configurar el sistema de logging. Hemos recurrido a una configuracin
por defecto, pero pronto veremos las posibilidades que ofrece la configuracin. La segunda accin es
obtener un logger asociado a nuestro programa. Un patrn alternativo al invocar GetLogger es ste:
log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase
7

Ni tampoco cambiar el Target de la aplicacin para evitar el error apuntado en la anterior nota al pie de pgina.

157

Buenas prcticas en desarrollo de software con .NET

.GetCurrentMethod().DeclaringType);

Cada una de las siguientes cinco lneas pide un registro de actividad con uno de los cinco posibles niveles. Al
ejecutar, obtenemos esto por pantalla:
41 [10] DEBUG ParaLogging.Program (null) - Esto es DEBUG
164 [10] INFO ParaLogging.Program (null) - Esto es INFO
165 [10] WARN ParaLogging.Program (null) - Esto es WARN
165 [10] ERROR ParaLogging.Program (null) - Esto es ERROR
165 [10] FATAL ParaLogging.Program (null) - Esto es FATAL

Se muestra cierta informacin propia y, al final, el texto que hemos suministrado como argumento a cada
una de las llamadas. El aspecto de cada lnea est determinado por el layout que adoptemos y que se
especifica en la configuracin.

10.5 Configuracin XML


Antes de usar el logger hemos de configurarlo. En el ejemplo hemos usado una configuracin bsica desde el
propio programa. Tpicamente se configura el logger con el fichero app.config. El programa debe cargar la
configuracin de ese fichero, por lo que hemos de cambiar algo:
using System;
using log4net;
using log4net.Config;
namespace ParaLogging
{
class Program
{
static void Main(string[] args)
{
ILog log = log4net.LogManager.GetLogger(typeof(Program));
XmlConfigurator.Configure();
log.Debug("Esto es DEBUG");
log.Info("Esto es INFO");
log.Warn("Esto es WARN");
log.Error("Esto es ERROR");
log.Fatal("Esto es FATAL");
Console.ReadKey();
}
}
}

Abrimos el fichero app.config (que se muestra en el explorador de soluciones como un fichero ms de


nuestro proyecto) y encontramos un texto XML como ste:
<?xml version="1.0"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/>
</startup>
</configuration>

Lo editamos ahora para dejarlo as:


<?xml version="1.0"?>
<configuration>
<configSections>
<section name="log4net"
type="log4net.Config.Log4NetConfigurationSectionHandler, log4net"/>
</configSections>

158

Buenas prcticas en desarrollo de software con .NET

<log4net>
<appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender">
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date [%thread] %-5level %logger [%ndc] - %message%newline"/>
</layout>
</appender>
<root>
<level value="ALL" />
<appender-ref ref="ConsoleAppender" />
</root>
</log4net>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/>
</startup>
</configuration>

Hemos configurado el logger declarando una seccin <log4net></log4net> en la relacin de secciones


que conforman app.config. El significado del sub-lenguaje XML que encontraremos en el interior de las
marcas log4net se define en la clase log4net.Config.Log4NetConfigurationSectionHandler del
ensamblado log4net.dll.
Dentro del elemento <log4net>, y en primer lugar, hemos definido un appender. Un appender es un
punto al que se aade la informacin que ir registrando el logger. En nuestro ejemplo, hemos definido un
appender llamado ConsoleAppender cuya semntica se define en la clase
log4net.Appender.ConsoleAppender. Esta clase define la salida por consola de los mensajes. La marca
<layout> permite controlar el aspecto de cada uno de los registros. Estamos usando un layout definido en la
clase log4net.Layout.PatternLayout. La definicin del layout en s se describe en la marca
<conversionPattern>. En nuestro caso estamos solicitando que se muestre primero la fecha en la que
ocurre el evento, seguida del nmero de hilo de ejecucin entre corchetes, seguida del nivel del evento (con
espacio para 5 caracteres), seguida del nombre del logger (en nuestro caso, el nombre del tipo que se
suministr al obtener el logger), seguida de un NDC (nested diagnostic context) entre corchetes y,
finalmente, seguida de un guin y el mensaje que nos suministraron como argumento.
2011-02-20
2011-02-20
2011-02-20
2011-02-20
2011-02-20

13:44:27,265
13:44:27,289
13:44:27,290
13:44:27,290
13:44:27,290

[9]
[9]
[9]
[9]
[9]

DEBUG
INFO
WARN
ERROR
FATAL

ParaLogging.Program
ParaLogging.Program
ParaLogging.Program
ParaLogging.Program
ParaLogging.Program

[(null)]
[(null)]
[(null)]
[(null)]
[(null)]

Esto
Esto
Esto
Esto
Esto

es
es
es
es
es

DEBUG
INFO
WARN
ERROR
FATAL

En el fichero de configuracin hemos fijado el nivel a ALL. Si en lugar de ALL ponemos WARN, slo se
muestran mensajes de nivel WARN o superior:
<root>
<level value="WARN" />
<appender-ref ref="ConsoleAppender" />
</root>

Al ejecutar el mismo programa obtenemos:


2011-02-20 14:03:25,892 [10] WARN ParaLogging.Program [(null)] - Esto es WARN
2011-02-20 14:03:25,918 [10] ERROR ParaLogging.Program [(null)] - Esto es ERROR
2011-02-20 14:03:25,918 [10] FATAL ParaLogging.Program [(null)] - Esto es FATAL

159

Buenas prcticas en desarrollo de software con .NET

Ya podemos entender una de las comodidades que ofrece un sistema de logging: podemos plagar el cdigo
con mensajes de ayuda a la depuracin y eliminarlos al pasar a explotacin fijando un nivel superior a DEBUG.
Pero si en explotacin deseamos, al detectar un mal funcionamiento, un logging ms exhaustivo, basta con
fijar nuevamente el nivel DEBUG.

10.6 Appenders
Hemos visto cmo mostrar informacin por pantalla. Pero los registros suelen almacenarse en algn sistema
que permite su anlisis posterior. Log4Net ofrece una amplia variedad de appenders. Estos son algunos de
ellos:

log4net.Appender.AdoNetAppender

Graba en una base de datos.

log4net.Appender.ColoredConsoleAppender

Aade los eventos a una consola de color.

log4net.Appender.ConsoleAppender

Aade a la consola estndar.

log4net.Appender.DebugAppender

Los aade a la salida de depuracin.

log4net.Appender.EventLogAppender

Aade los eventos al log de eventos del sistema.

log4net.Appender.ForwardingAppender

Reenva los eventos a los appenders vinculados.

log4net.Appender.FileAppender

Aade a un fichero.

log4net.Appender.MemoryAppender

Almacena los eventos en un vector de memoria.

log4net.Appender.RemoteSyslogAppender

Registra los eventos en un daemon syslog remote.

log4net.Appender.RemotingAppender

Enva los eventos a un sumidero de logging remote.

log4net.Appender.RollingFileAppender

Aade los eventos a un sistema que hace rotacin de ficheros de log por fecha, tamao o ambos
criterios.

log4net.Appender.SmtpAppender

Enva e-mail cuando ocurre un evento de logging determinado (tpicamente ERROR o FATAL).

log4net.Appender.TraceAppender

Aade los eventos al sistema de traza.


La comunidad disea e implementa otros appenders. Hay, por ejemplo, un appender para MongoDB (una de
las base NoSQL de moda en aplicaciones para la nube) y otro para Twitter.
10.6.1 ConsoleAppender/ColorConsoleAppender
El appender ms bsico es el ConsoleAppender, til para dar informacin inmediata por pantalla durante la
ejecucin del programa. La versin en color, ColorConsoleAppender, muestra los mensajes coloreados por
nivel.

160

Buenas prcticas en desarrollo de software con .NET

Se puede configurar qu salida usar (estndar o de error) con el elemento <target> (dentro de
<appender>). Podemos usar Console.Error o Console.Out. Por defecto se usa la salida estndar. As
podemos usar la salida de error:
<appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender">
<layout type="log4net.Layout.PatternLayout">
<conversionPattern
value="%date [%thread] %-5level %logger [%ndc] - %message%newline"/>
</layout>
<target value="Console.Error" />
</appender>

En la versin en color se puede configurar el color asociado a cada nivel:


<log4net>
<appender name="ColoredConsoleAppender"
type="log4net.Appender.ColoredConsoleAppender">
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date [%thread] %-5level %logger [%ndc] - %message%newline"/>
</layout>
<target value="Console.Error" />
<mapping>
<level value="FATAL" />
<foreColor value="Red" />
<backColor value="White" />
</mapping>
<mapping>
<level value="ERROR" />
<foreColor value="Red, HighIntensity" />
</mapping>
<mapping>
<level value="WARN" />
<foreColor value="Yellow" />
</mapping>
<mapping>
<level value="INFO" />
<foreColor value="Cyan" />
</mapping>
<mapping>
<level value="DEBUG" />
<foreColor value="Green" />
</mapping>
</appender>
<root>
<level value="WARN" />
<appender-ref ref="ColoredConsoleAppender" />
</root>
</log4net>

El color de fondo o de texto puede ser Blue, Green, Red, Yellow, Purple, Cyan o White. Con
HighIntensity se usa un color ms intenso.
10.6.2 DebugAppender/TraceAppender
Es un adaptador para System.Diagnostics.Debug (y hay otro para Trace). Permite usar los escuchadores
propios de System.Diagnostics.Debug. En Visual Studio, la salida se muestra por la ventana de
depuracin.
Se le puede controlar el buffering con la marca <immediateFlush>:
<appender name="DebugAppender" type="log4net.Appender.DebugAppender">

161

Buenas prcticas en desarrollo de software con .NET

<immediateFlush value="true" />


</appender>

10.6.3 FileAppender
Este appender permite aadir texto a un fichero y hacer as que la informacin de los eventos persista. Al
configurarlo hemos de proporcionar cierta informacin con las marcas apropiadas:

<file value="ruta a fichero" />


<appendToFile value= "true o false" />
<encoding value= "utf-8" />
<immediateFlush value= "true o false" />
<lockingModel value= "modo" />: fija el modo de bloque en el fichero.
Puede ser log4net.Appender.FileAppender+MinimalLock o
log4net.Appender.FileAppender+ExclusiveLock.

He aqu un ejemplo de configuracin que hace uso de algunas de esas marcas:


<appender name="FileAppender" type="log4net.Appender.FileAppender">
<file value="fichero.txt" />
<appendToFile value="true" />
<encoding value="utf-8" />
<layout type="log4net.Layout.SimpleLayout" />
</appender>

Un consejo: conviene no usar FileAppender, sino RollingFileAppender, pues FileAppender no limita el


crecimiento del fichero y podemos encontrar, con el tiempo, ficheros de registro de actividad que ocupan
gigas y consumen todo el espacio en disco. Veamos qu es RollingFileAppender.
10.6.4 RollingFileAppender
Es similar a FileAppender, pero permite efectuar rotacin sobre varios ficheros en funcin de la fecha o el
tamao alcanzado por el fichero. Los ficheros de rotacin tienen un nmero como sufijo: fichero1.txt,
fichero2.txt, etc.
Estas marcas adicionales controlan el comportamiento de un RollingFileAppender:

<rollingStyle value= "Once|Size|Date|Composite" />: con Once, se inicializa cada vez que se

inicializa Log4Net; con Size, se inicializa al alcanzar un tamao; con Date, al alcanzar una fecha; con
Composite, se atiende tamao y fecha.
<maximumFileSize value= "#(KB|MB|GB) " />: se inicializa al alcanzar el tamao que se
especifica.
<maxSizeRollBackups value= "#" />: nmero mximo de ficheros en rotacin si estamos en
estilo Size. En estilo Composite limita el nmero de ficheros por da. No hace nada con Once o Date.
<datePattern value= "pattern" />:
<staticLogFileName value= "true|false" />: si vale true, escribe en el fichero cuya ruta se dio
en <file>. Si vale false, escribe en el ltimo fichero de rotacin: fichero1, fichero2, fichero3
<countDirection value= "#" />: si es mayor que cero, el fichero ms reciente tiene el nmero
ms grande. Si es menor que cero, el ms reciente es fichero1. Por defecto es -1.

10.6.5 Mltiples appenders


Podemos usar ms de un appender simultneamente. Basta con especificarlos todos en la configuracin,
cada uno con su elemento XML:

162

Buenas prcticas en desarrollo de software con .NET

<root>
<level value="ALL" />
<appender-ref ref="DebugAppender" />
<appender-ref ref="ColoredConsoleAppender" />
</root>

10.7 Layouts
Con los layouts controlamos qu informacin se muestra y cmo. La configuracin ms sencilla es la que
adopta valores por defecto. Para usarla basta con usar log4net.Layout.SimpleLayout:
<appender name="ColoredConsoleAppender"
type="log4net.Appender.ColoredConsoleAppender">
<layout type="log4net.Layout.SimpleLayout" />
</appender>

Hemos visto antes cmo especificar un layout con patrones. Usamos entonces log4net.Layout.PatternLayout
y la marca <conversionPattern value= "patrn" />. El lenguaje de patrones que entiende
log4net.Layout.PatternLayout es muy rico. Apuntamos algunos de los elementos bsicos:

%date

Instante en el que se produce el evento. Por defecto, el instante se anota hasta las milsimas de
segundo. Se puede configurar indicando el formato entre llaves. He aqu un ejemplo:
%date{dd MMM yyyy HH:mm:ss,fff} .

%file

Nombre del fichero .cs en el que se produjo la llamada al logger. (Lento.)

%level

Nivel de logging con el que se ha producido la llamada.

%line

Lnea del fichero desde la que se produjo la llamada. (Lento.)

%message

Mensaje proporcionado en la llamada.

%method

Mtodo desde el que se produce la llamada. (Lento.)

%newline

Salto de lnea.

%property{id}

Muestra el valor de la propiedad con clave id. Las propiedades se aaden en los loggers y los
appenders. He aqu un ejemplo de propiedad aadida:
log4net.GlobalContext.Properties["miPropiedad"] = "Esto es mi texto";
Hay un contexto global (el que hemos usado), un TheadContext y un LogicalThreadContext. Ms

adelante hablamos de contextos.

%timestamp

Milisegundos transcurridos desde el inicio de la aplicacin.

%thread

Nmero de hilo de ejecucin.

%%

Smbolo de %.

%username

Nombre del usuario en la sesin Windows. (Lento.)

163

Buenas prcticas en desarrollo de software con .NET

%utcdate

Instante, pero en tiempo universal.


El formato de la informacin asociada a las marcas se puede controlar con nmeros entre el % y el
identificador de la marca. Un nmero positivo indica un nmero de caracteres mnimo, con relleno de
blancos por la izquierda; un nmero negativo hace lo mismo, pero rellenando por la derecha; un nmero
precedido de un punto indica el nmero mximo de caracteres, con truncamiento de los primeros caracteres
si es menester. La marca %10.20message significa que si el mensaje no llega a 10 caracteres, se rellenar con
blancos por la izquierda, y que si tiene ms de 20 caracteres, slo se mostrarn los ltimos 20.
Otro motor de layout interesante es log4net.Layout.XMLLayout, que permite producir informacin
estructurada con XML. Veamos un ejemplo:
<appender name="ColoredConsoleAppender" type="log4net.Appender.ColoredConsoleAppender">
<layout type="log4net.Layout.XMLLayout" />
</appender>

Al ejecutar obtenemos esta salida XML:


<log4net:event logger="ParaLogging.Program" timestamp="2011-02-20T19:07:25.157+01:00"
level="DEBUG" thread="10" domain="ParaLogging.vshost.exe"
username="pro\amarzal">
<log4net:message>Esto es DEBUG</log4net:message>
<log4net:properties>
<log4net:data="" name="log4net:HostName" value="pro" />
</log4net:properties>
</log4net:event>
<log4net:event logger="ParaLogging.Program" timestamp="2011-02-20T19:07:25.191+01:00"
level="INFO" thread="10" domain="ParaLogging.vshost.exe"
username="pro\amarzal">
<log4net:message>Esto es INFO</log4net:message>
<log4net:properties>
<log4net:data="" name="log4net:HostName" value="pro" />
</log4net:properties>
</log4net:event>
<log4net:event logger="ParaLogging.Program" timestamp="2011-02-20T19:07:25.195+01:00"
level="WARN" thread="10" domain="ParaLogging.vshost.exe"
username="pro\amarzal">
<log4net:message>Esto es WARN</log4net:message>
<log4net:properties>
<log4net:data="" name="log4net:HostName" value="pro" />
</log4net:properties>
</log4net:event>
<log4net:event logger="ParaLogging.Program" timestamp="2011-02-20T19:07:25.196+01:00"
level="ERROR" thread="10" domain="ParaLogging.vshost.exe"
username="pro\amarzal">
<log4net:message>Esto es ERROR</log4net:message>
<log4net:properties>
<log4net:data="" name="log4net:HostName" value="pro" />
</log4net:properties>
</log4net:event>
<log4net:event logger="ParaLogging.Program" timestamp="2011-02-20T19:07:25.196+01:00"
level="FATAL" thread="10" domain="ParaLogging.vshost.exe"
username="pro\amarzal">
<log4net:message>Esto es FATAL</log4net:message>
<log4net:properties>
<log4net:data="" name="log4net:HostName" value="pro" />
</log4net:properties>
</log4net:event>

164

Buenas prcticas en desarrollo de software con .NET

10.8 Configuracin jerrquica de Loggers


En una aplicacin podemos crear varios loggers. Podemos crear uno por clase y otro para el programa
principal, por ejemplo. De ese modo el texto contendr informacin sobre la clase que hizo la llamada
correspondiente. Podemos, adems, configurar cada logger y asociar a cada uno un appender diferente.
Veamos un ejemplo:
<log4net>
<appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender">
<layout type="log4net.Layout.SimpleLayout"/>
</appender>
<appender name="FileAppender" type="log4net.Appender.FileAppender">
<file value="fichero-log.txt" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date %logger [%level]- %message%newline" />
</layout>
</appender>
<root>
<level value="ALL" />
</root>
<logger name="ParaLogging">
<level value="ALL" />
<appender-ref ref="FileAppender" />
</logger>
<logger name="ParaLogging.Program">
<level value="ALL" />
<appender-ref ref="ConsoleAppender" />
</logger>
<logger name="ParaLogging.MiClase">
<level value="OFF" />
</logger>
</log4net>

Estamos asignando un logger a todos los objetos del espacio de nombres ParaLogging, con salida en
fichero. El programa principal sacar, adems, la salida por consola. La clase ParaLogging.MiClase
desconecta el registro de mensajes.
Hablamos de configuracin jerrquica porque cada logger puede ocuparse de un nivel y se anidan estos en
funcin de la jerarqua definida por espacios de nombres y clases.

10.9 Contextos
Cuando una aplicacin es ejecutada por varios clientes concurrentes o por varios componentes, conviene
conocer el contexto en el que se produce el evento.
Ya hemos indicado antes que hay tres contextos:

global (GlobalContext),
por hilo (ThreadContext) y
por hilo lgico (ThreadLogicalContext).

Podemos asociar propiedades a los contextos:


log4net.ThreadContext.Properties["miContexto"] = "Llamado desde Main";

Y conocer el valor asociado a la propiedad con un patrn de layout:

165

Buenas prcticas en desarrollo de software con .NET

"%property{miContexto}"

Las propiedades no slo pueden ser cadenas. Se puede calcular valores con clases que definen ToString.
Una clase CounterProperty podra, por ejemplo, devolver el valor de un contador.
log4net.ThreadContext.Properties["contador"] = new CounterProperty();

Cuando se accede al valor, se toma lo que devuelve ToString().


Los contextos definen pilas que pueden ayudar a mostrar contextos complejos, en los que se entra y sale de
un estado como se entra y sale de los mtodos.
Esta llamada:
log4net.ThreadContext.Stacks["miContexto"].Push("externo")

apila un contexto. Para salir del contexto hacemos:


log4net.ThreadContext.Stacks["miContexto"].Close()

Los contextos anidados se muestran con el patrn %ndc.

10.10 Filtros
Los appenders pueden llevar asociados filtros. Los filtros ayudan a decidir si un evento debe ser considerado
por un appender o no. Con los filtros podemos seleccionar los niveles que deseamos registrar, uno a uno
(log4net.Filter.LevelMatchFilter ) o por rango (log4net.Filter.LevelRangeFilter), o dejar pasar
slo aquellos que concuerdan con una cadena (log4net.Filter.StringMatchFilter ), o en funcin de si
hay concordancia con el valor o expresin regular para una propiedad contextual
(log4net.Filter.PropertyFilter),o si hay concordancia con el nombre del logger
(log4net.Filter.LoggerMatchFilter ), o no dejar pasar a ninguno (log4net.Filter.DenyAllFilter).
Esta configuracin, por ejemplo, selecciona los eventos de nivel comprendido entre DEBUG y WARN:
<appender name="LogFileAppender" type="log4net.Appender.FileAppender">
<file value="fichero.txt" />
<filter type="log4net.Filter.LevelRangeFilter">
<levelMin value="DEBUG" />
<levelMax value="WARN" />
</filter>
<layout type="log4net.Layout.SimpleLayout" />
</appender>

10.10.1 LoggerMatchFilter
Filtra contra el nombre del logger que emite el mensaje. Se configura con:

loggerToMatch: cadena con el valor esperado. La concordancia se hace con String.StartsWith.


acceptOnMatch: se acepta el mensaje si y slo si vale true.

10.10.2 LevelMatchFilter
Filtra por nivel:

levelToMatch: Debug, Info, Warn, Error o Fatal.


acceptOnMatch.

166

Buenas prcticas en desarrollo de software con .NET

10.10.3 LevelRangeFilter
Filtra por valor del nivel en un rango

levelMin: nivel mnimo.


levelMax: nivel mximo.
acceptOnMatch.

10.10.4 StringMatchFilter
Filtra por el contenido de texto del mensaje, con una expresin regular o en funcin de si la cadena contiene
una subcadena determinada:

regexToMatch: expresin regular que se compara con el mensaje.


stringToMatch: cadena que se compara con el mensaje usando String.IndexOf.
acceptOnMatch.

10.10.5 PropertyFilter
Filtra en funcin del contenido de texto del valor de una propiedad:

key: nombre de la propiedad.


regexToMatch: expresin regular que comparar con el valor de la propiedad.
stringToMatch: cadena que comparar va String.IndexOf.
acceptOnMatch.

10.10.6 DenyAllFilter
Poco que decir de este, que lo filtra todo.

10.11 Un ejemplo de uso


Enriquezcamos nuestra aplicacin de lectura de Rss con la inclusin de un logger que registre cada acceso a
una fuente de noticias.
Empezamos aadiendo la referencia a log4net en el proyecto Rss. Nuestra clase Rss.Reader necesitar una
referencia al logger, y sta referencia, que proporciona valor a una nueva dependencia, se inyectar en el
constructor.
using log4net;
namespace Rss
{
public class Reader
{
private ILog _log;
private ILoader _loader;
private IParser _parser;
private IFormatter _formatter;
private IWriter _writer;
private IUrlManager _urlManager;
public Reader(ILog log, ILoader loader, IParser parser, IFormatter formatter,
IWriter writer, IUrlManager urlManager)
{
_log = log;
_loader = loader;
_parser = parser;
_formatter = formatter;

167

Buenas prcticas en desarrollo de software con .NET

_writer = writer;
_urlManager = urlManager;
}

Naturalmente, compilar ahora conducir a la obtencin de errores. Uno de ellos se dar en la aplicacin
RssReaderApp, que necesitar incluir una referencia a log4net.dll, cambiar su Target framework a .NET
Framework 4 y modificar el cdigo:
using System.Configuration;
using System;
namespace RssReaderApp
{
class Program
{
static void Main(string[] args)
{
var config = (RssReaderAppConfigurationSection)
ConfigurationManager.GetSection("rssReader");
log4net.Config.BasicConfigurator.Configure();
var log = log4net.LogManager.GetLogger(typeof(Program));
var loader = new Rss.Loader();
var parser = (Rss.IParser)Activator.CreateInstance(config.Parser);
var formatter = new Rss.Formatter();
var writer = new Rss.Writer();
var urlManager = new Rss.UrlManager();
foreach (UrlElement url in config.Url)
{
urlManager.Add(url.Value);
}
foreach (var arg in args)
{
urlManager.Add(arg);
}
var reader = new Rss.Reader(log, loader, parser, formatter, writer, urlManager);
reader.Display();
System.Console.ReadKey();
}
}
}

El otro cdigo afectado es el de la clase TestReader, en el proyecto TestRss. Tambin hemos de aadir la
referencia a la librera y modificar el cdigo de TestReader.cs:
[TestFixture]
public class TestReader
{
private Mock<log4net.ILog> _logMock;
private Mock<Rss.ILoader> _loaderMock;
private Mock<Rss.IParser> _parserMock;
private Mock<Rss.IFormatter> _formatterMock;
private Mock<Rss.IWriter> _writerMock;
private Mock<Rss.IUrlManager> _urlManagerMock;
private Rss.Reader _reader;
[SetUp]
public void Prepara()
{
_logMock = new Mock<log4net.ILog>(MockBehavior.Loose);
_loaderMock = new Mock<Rss.ILoader>(MockBehavior.Strict);
_loaderMock.Setup(l => l.Load(SomeConstants.url))

168

Buenas prcticas en desarrollo de software con .NET

.Returns(SomeConstants.input);
_parserMock = new Mock<Rss.IParser>(MockBehavior.Strict);
_parserMock.Setup(p => p.Parse(It.IsAny<string>()))
.Returns(SomeConstants.intermediate);

_formatterMock = new Mock<Rss.IFormatter>(MockBehavior.Strict);


_formatterMock.Setup(f => f.FormatFeed(SomeConstants.intermediate))
.Returns(SomeConstants.output);
_writerMock = new Mock<Rss.IWriter>(MockBehavior.Strict);
_writerMock.SetupGet(w => w.Write);
_urlManagerMock = new Mock<Rss.IUrlManager>(MockBehavior.Strict);
_urlManagerMock.Setup(u => u.Add(SomeConstants.url));
_urlManagerMock.Setup(u => u.GetEnumerator())
.Returns(Enumerable.Repeat(SomeConstants.url, 1).GetEnumerator());
_reader = new Rss.Reader(_logMock.Object, _loaderMock.Object, _parserMock.Object,
_formatterMock.Object, _writerMock.Object,
_urlManagerMock.Object);
}

Modificamos el mtodo Display de Reader:


public void Display()
{
foreach (var url in _urlManager)
{
_log.Info(string.Format("Intentando acceder a {0}", url));
var xml = _loader.Load(url);
var result = FeedReport(xml);
_writer.Write(result);
_log.Info(string.Format("Fin del acceso a {0}", url));
}
}

Ejecutamos y obtenemos algo similar a esto por pantalla:


62 [9] INFO RssReaderApp.Program (null) - Intentando acceder a http://www.elpais.com/rss/feed.html?feedId=17046
*** ELPAIS.com - Lo ltimo
Sun, 24 Apr 2011 17:00:00 +0200: Fallece la actriz francesa Marie-France Pisier, musa de Truffaut
Sun, 24 Apr 2011 15:18:00 +0200: Miles de personas celebran el Aberri Eguna en Gernika reclamando la independencia
Sun, 24 Apr 2011 14:28:00 +0200: Los combates contin?an en Misrata pese al anuncio de retirada de Gadafi

Sat, 23 Apr 2011 21:27:00 +0200: Giovani encuentra su sitio


3634 [9] INFO RssReaderApp.Program (null) - Fin del acceso a http://www.elpais.com/rss/feed.html?feedId=17046
3634 [9] INFO RssReaderApp.Program (null) - Intentando acceder a http://www.meneame.net/rss2.php
*** Mename: publicadas
Sun, 24 Apr 2011 15:10:02 +0000: Motn a bordo de un avin de Vueling rumbo a Ciudad Real
Sun, 24 Apr 2011 14:25:02 +0000: El CSIC lanza un catlogo de ebooks que incluye 28 libros gratis
Sun, 24 Apr 2011 13:35:02 +0000: Bienvenidos a la nuclear de Hamaoka, en la zona ms ssmica del mundo

Fri, 22 Apr 2011 22:25:02 +0000: Cientos de gitanos huyen de una localidad de Hungra
4882 [9] INFO RssReaderApp.Program (null) - Fin del acceso a http://www.meneame.net/rss2.php

La salida incluye el registro de los eventos, cuando sera preferible que los eventos se registrasen en fichero.
Aprovechamos para configurar el logger en app.config, que ha de presentar este aspecto:
<?xml version="1.0"?>
<configuration>
<configSections>
<section name="rssReader"

169

Buenas prcticas en desarrollo de software con .NET

type="RssReaderApp.RssReaderAppConfigurationSection, RssReaderApp"/>
<section name="log4net"
type="log4net.Config.Log4NetConfigurationSectionHandler, log4net"/>
</configSections>
<rssReader parser="Rss.Rss2Parser, Rss">
<url>
<add value="http://www.elpais.com/rss/feed.html?feedId=17046"/>
<add value="http://www.meneame.net/rss2.php"/>
</url>
</rssReader>
<log4net>
<appender name="FileAppender" type="log4net.Appender.FileAppender">
<file value="rss.log" />
<appendToFile value="true" />
<encoding value="utf-8" />
<layout type="log4net.Layout.SimpleLayout" />
</appender>
<root>
<level value="ALL" />
<appender-ref ref="FileAppender" />
</root>
</log4net>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/>
</startup>
</configuration>

Es necesario indicar a la aplicacin que debe configurar el logger a partir del fichero app.config:
using System.Configuration;
using System;
namespace RssReaderApp
{
class Program
{
static void Main(string[] args)
{
var config = (RssReaderAppConfigurationSection)
ConfigurationManager.GetSection("rssReader");
log4net.Config.XmlConfigurator.Configure();
var log = log4net.LogManager.GetLogger(typeof(Program));
var loader = new Rss.Loader();
var parser = (Rss.IParser)Activator.CreateInstance(config.Parser);
var formatter = new Rss.Formatter();
var writer = new Rss.Writer();
var urlManager = new Rss.UrlManager();
foreach (UrlElement url in config.Url)
{
urlManager.Add(url.Value);
}
foreach (var arg in args)
{
urlManager.Add(arg);
}
var reader = new Rss.Reader(log, loader, parser,
formatter, writer, urlManager);
reader.Display();

170

Buenas prcticas en desarrollo de software con .NET

System.Console.ReadKey();
}
}
}

Si ejecutamos ahora, tenemos esta salida:


*** ELPAIS.com - Lo ltimo
Sun, 24 Apr 2011 17:00:00 +0200:
Sun, 24 Apr 2011 15:18:00 +0200:
Sun, 24 Apr 2011 14:28:00 +0200:

Sat, 23 Apr 2011 21:27:00 +0200:


*** Mename: publicadas
Sun, 24 Apr 2011 15:10:02 +0000:
Sun, 24 Apr 2011 14:25:02 +0000:
Sun, 24 Apr 2011 13:35:02 +0000:

Fri, 22 Apr 2011 22:25:02 +0000:

Fallece la actriz francesa Marie-France Pisier, musa de Truffaut


Miles de personas celebran el Aberri Eguna en Gernika reclamando la independencia
Los combates contin?an en Misrata pese al anuncio de retirada de Gadafi
Giovani encuentra su sitio
Motn a bordo de un avin de Vueling rumbo a Ciudad Real
El CSIC lanza un catlogo de ebooks que incluye 28 libros gratis
Bienvenidos a la nuclear de Hamaoka, en la zona ms ssmica del mundo
Cientos de gitanos huyen de una localidad de Hungra

Y si ahora buscamos en el directorio en el que se encuentra la aplicacin (en el caso de quien esto escribe, en
el directorio C:\Users\amarzal\Documents\ Curso Buenas Prcticas\BuenasPracticas\RssReader\bin\Debug),
encontraremos un fichero rss.log. Su contenido es ste:
INFO
INFO
INFO
INFO

Intentando acceder a http://www.elpais.com/rss/feed.html?feedId=17046


Fin del acceso a http://www.elpais.com/rss/feed.html?feedId=17046
Intentando acceder a http://www.meneame.net/rss2.php
Fin del acceso a http://www.meneame.net/rss2.php

10.12 Buenas prcticas


Acabamos con unos consejos:

Es recomendable que los logger tengan como nombre el de la clase desde la que se les llama va
LogManager.GetLogger(typeof(nombre de la clase)).
Una aplicacin debera registrar cada excepcin detectada y tratada, pues las excepciones deben
seguir manteniendo ese carcter de comportamiento inesperado y, en consecuencia, suponen un
hecho notable que debe registrarse.
Ms vale poner un log de ms que uno de menos. El impacto sobre la eficiencia se puede controlar.
Conviene controlar el espacio que consumen los fichero de logging. Para ello es recomendable usar
RollingFileAppender en lugar de FileAppender.

11 Serializacin
Al crear registros de actividad puede convenir mostrar el contenido de un objeto. Podemos redefinir
ToString para que la cadena que proporciona describa el objeto. Esto supone cierto esfuerzo y, por otra
parte, puede que deseemos reservar ToString para otro propsito.
La necesidad de volcar el contenido de un objeto a algn formato textual (o binario) es frecuente. Tambin
lo es la necesidad de reconstruir el objeto a partir del volcado. Si disponemos de esta capacidad
bidireccional, podemos facilitar enormemente el intercambio de datos. La codificacin de los valores puede
ser un problema, pero XML ofrece aqu un apoyo que vale la pena tener en cuenta.
Se denomina serializar a la accin de convertir un objeto en una descripcin que permite su reconstruccin
(deserializacin) posterior. La descripcin se puede almacenar en un fichero o transmitir entre nodos de una
red. Se denomina persistencia a la capacidad de un objeto de serializarse/deserializarse.

171

Buenas prcticas en desarrollo de software con .NET

La serializacin no slo es til para almacenar y recuperar informacin: tambin es til para obtener clones
profundos de objetos.
Hay tres motores para serializacin con .NET:

Serializador binario.
Serializador XML.
Serializador basado en contrato de datos (Data contract).

Serializar un objeto simple no parece plantear demasiados problemas. En nuestras aplicaciones y libreras los
objetos pueden estar interrelacionados y mantener referencias entre s. As pues, hemos de entender que un
objeto es un nodo en un grafo de objetos. Ciertos serializadores son capaces de almacenar todo el subgrafo
alcanzable desde un objeto y otros no. Hay un peligro potencial: que el grafo de objetos contenga ciclos. Si el
serializador detecta un ciclo y es incapaz de tratar con l, advertir del problema lanzando una excepcin.
Otro problema posible es la existencia de objetos para los que se mantienen varias referencias. El
serializador XML generar una descripcin por cada referencia, aunque se trate del mismo objeto. Si se
desea un comportamiento ms sofisticado se ha de recurrir a otro serializadores, como el binario o el
denominado DataContractSerializer.
Nosotros presentamos primero la serializacin XML, que es la ms sencilla y resulta suficiente para el uso
que deseamos darle en este texto. .NET facilita enormemente la serializacin de objetos con XML. Por
defecto, todo tipo es serializable. Estudiaremos luego la serializacin basada en contratos, que ms verstil.
El principal problema del serializador binario es su dependencia de la versin del ensamblado. Si
modificamos una clase, los datos serializados son irrecuperables. En segn qu aplicaciones esto no es un
problema, pues ciertos datos tienen una estructura muy estable, pero en otras es una fuente de problemas.
Para saber ms del serializador binario (y para ampliar lo explicado aqu de los otros dos serializadores) es
recomendable leer el captulo 16 del libro C# 4.0 in a Nutshell. Baste decir que

11.1 Serializador XML


11.1.1 Un ejemplo sencillo
Veamos un ejemplo. Definimos una clase con propiedades de diferentes tipos:
namespace Serializar
{
public enum Rol
{
Estudiante,
Profesor,
Administrativo
};
public class Persona
{
public string Nombre { get; set; }
public string Apellido { get; set; }
public int Edad { get; set; }
public Rol[] Roles { get; set; }
}
}

172

Buenas prcticas en desarrollo de software con .NET

Y ahora, desde nuestro programa principal, vamos a instanciar la clase, asignar valores a los atributos,
guardar una serializacin en disco y recuperarla:
using System;
using System.Xml.Serialization;
using System.IO;
namespace Serializar
{
class Program
{
static void Main(string[] args)
{
var p = new Persona
{
Nombre = "Pepe",
Apellido = "Prez",
Edad = 28,
Roles = new [] {Rol.Estudiante, Rol.Administrativo}
};
var personaSerializer = new XmlSerializer(typeof(Persona));
using (var f = new FileStream("datos.xml", FileMode.Create))
{
personaSerializer.Serialize(f, p);
}
using (var f = new FileStream("datos.xml", FileMode.Open))
{
var q = (Persona) personaSerializer.Deserialize(f);
}
Console.WriteLine();
}
}
}

Hemos usado un serializador XML (XmlSerializer) para que los datos se almacenen en un formato legible
por personas. Ntese que los datos se han almacenado en un fichero denominado datos.xml. Si examinamos
su contenido veremos esto:
<?xml version="1.0"?>
<Persona xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Nombre>Pepe</Nombre>
<Apellido>Prez</Apellido>
<Edad>28</Edad>
<Roles>
<Rol>Estudiante</Rol>
<Rol>Administrativo</Rol>
</Roles>
</Persona>

El formato es legible y no lo hemos tenido que definir nosotros trabajosamente. Es posible definir procesos
de serializacin personalizados, pero el comportamiento por defecto es utilizable en la mayor parte de
escenarios habituales.
En nuestro ejemplo hemos serializado en XML, que es legible, pero muy verboso. En situaciones en las que
importe ms la eficiencia que la legibilidad podemos recurrir a serializadores que almacenan la informacin
en binario.

173

Buenas prcticas en desarrollo de software con .NET

11.1.2 Proteccin de propiedades y campos


El serializador XML almacena la descripcin de todas las propiedades y campos pblicos.
Una ltima cuestin. Los objetos pueden tener informacin que no deseemos almacenar. Por ejemplo, un
objeto puede almacenar el nmero de veces que ha sido accedido en una sesin determinada y ese dato no
tiene por qu persistir. Podemos marcar una propiedad con el atributo [XmlIgnore] para que no se
serialice.
Extendamos el ejemplo anterior:
using System.Xml.Serialization;
namespace Serializar
{
public enum Rol
{
Estudiante,
Profesor,
Administrativo
} ;
public class Persona
{
public string Nombre { get; set; }
public string Apellido { get; set; }
public int Edad { get; set; }
public Rol[] Roles { get; set; }
[XmlIgnore] public int Contador { get; set; }
}
}

El programa principal pasa a ser este:


using System;
using System.Xml.Serialization;
using System.IO;
namespace Serializar
{
class Program
{
static void Main(string[] args)
{
var p = new Persona
{
Nombre = "Pepe",
Apellido = "Prez",
Edad = 28,
Roles = new[] { Rol.Administrativo },
Contador = 100
};
var personaSerializer = new XmlSerializer(typeof(Persona));
using (var f = new FileStream("datos.xml", FileMode.Create))
{
personaSerializer.Serialize(f, p);
}
using (var f = new FileStream("datos.xml", FileMode.Open))
{
var q = (Persona) personaSerializer.Deserialize(f);
Console.WriteLine("{0} {1}", q.Nombre, q.Contador);

174

Buenas prcticas en desarrollo de software con .NET

}
Console.ReadKey();
}
}
}

La salida por pantalla al ejecutar es sta:


Pepe 0

Se puede comprobar que la propiedad no almacen su valor al serializar el objeto.

11.2 Serializador basado en contratos


El serializador basado en contratos es ms reciente en .NET y ofrece mayor versatilidad. Antes de presentar
este serializador hemos de hablar brevemente de formateadores.8
11.2.1 Formateadores
Al serializar, los datos pasan por un formateador, un objeto que genera la salida con la descripcin del objeto
en el formato que decidamos. Hay dos grupos de formateadores:

Formateadores XML.
Formateadores binarios.

El serializador XML est vinculado a un formateador XML, pero el basado en contratos nos permite elegir. El
formateador binario es til si se desea eficiencia temporal (al serializar/deserializar) y espacial. El
formateador XML interesa si prima la legibilidad.
11.2.2 Uso del serializador
Para trabajar con contrato de datos hemos de elegir primero si usaremos la clase DataContractSerializer
(crea un acoplamiento dbil entre tipos .NET y tipos de contrato de datos) o NetDataContractSerializer
(establece un vnculo fuerte entre tipos .NET y tipos de contrato de datos). Nosotros escogeremos siempre el
primero.
A continuacin, hemos de marcar los tipos y miembros que deseamos serializar con [DataContract] y
[DataMember], respectivamente. A continuacin invocaremos el mtodo WriteObject o ReadObject, segn
queramos serializar o deserializar, respectivamente.
Repitamos un ejemplo como el anterior con el nuevo serializador. Lo primero es marcar la clase Persona y
los miembros que deseamos serializar:
using System.Runtime.Serialization;
namespace Serializar
{
public enum Rol
{
Estudiante,
Profesor,
Administrativo
} ;

Hay que incluir una referencia a la librera System.Runtime.Serialization para poder usar atributos como
DataContractAttribute o DataMemberAttribute.

175

Buenas prcticas en desarrollo de software con .NET

[DataContract]
public class Persona
{
[DataMember] public
[DataMember] public
[DataMember] public
[DataMember] public
}

string Nombre { get; set; }


string Apellido { get; set; }
int Edad { get; set; }
Rol[] Roles { get; set; }

El programa principal queda as:


using System;
using System.IO;
using System.Runtime.Serialization;
namespace Serializar
{
class Program
{
static void Main(string[] args)
{
var p = new Persona
{
Nombre = "Pepe",
Apellido = "Prez",
Edad = 28,
Roles = new[] {Rol.Estudiante, Rol.Administrativo},
};
var personaSerializer = new DataContractSerializer(typeof(Persona));
using (var f = new FileStream("datos.xml", FileMode.Create))
{
personaSerializer.WriteObject(f, p);
}
using (var f = new FileStream("datos.xml", FileMode.Open))
{
var q = (Persona) personaSerializer.ReadObject(f);
Console.WriteLine("{0} {1}", q.Nombre, q.Apellido);
}
Console.ReadKey();
}
}
}

Si examinamos el contenido de datos.xml, veremos que contiene informacin en XML (aunque no es un


documento XML por faltar la lnea inicial <?xml ?>) y que no est formateada para lectura humana.
<Persona xmlns="http://schemas.datacontract.org/2004/07/Serializar" xmlns:i="http://www.w3.org/200
1/XMLSchema-instance"><Apellido>Prez</Apellido><Edad>28</Edad><Nombre>Pepe</Nombre><Roles><Rol>Ad
ministrativo</Rol></Roles></Persona>

Vemoslo formateado manualmente:


<Persona xmlns="http://schemas.datacontract.org/2004/07/Serializar"
xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<Apellido>Prez</Apellido>
<Edad>28</Edad>
<Nombre>Pepe</Nombre>
<Roles>
<Rol>Estudiante</Rol>

176

Buenas prcticas en desarrollo de software con .NET

<Rol>Administrativo</Rol>
</Roles>
</Persona>

El contenido del fichero XML refleja con sus marcas los nombres de clase y miembros que hemos usado. No
necesariamente ha de ser as. Podemos definir nuestras propias marcas:
using System.Runtime.Serialization;
namespace Serializar
{
public enum Rol
{
Estudiante,
Profesor,
Administrativo
} ;
[DataContract(Name="Person")]
public class Persona
{
[DataMember(Name="FirstName")] public string Nombre { get; set; }
[DataMember(Name="LastName")] public string Apellido { get; set; }
[DataMember(Name="Age")] public int Edad { get; set; }
[DataMember] public Rol[] Roles { get; set; }
}
}

El contenido de datos.xml ahora pasa a ser:


<Person xmlns="http://schemas.datacontract.org/2004/07/Serializar" xmlns:i="http://www.w3.org/2001
/XMLSchema-instance"><Age>28</Age><FirstName>Pepe</FirstName><LastName>Prez</LastName><Roles><Rol
>Estudiante</Rol><Rol>Administrativo</Rol></Roles></Person>

11.2.3 Un asunto avanzado: declaracin de espacios de nombres


Tambin podemos definir espacios de nombre XML:
using System.Runtime.Serialization;
namespace Serializar
{
public enum Rol
{
Estudiante,
Profesor,
Administrativo
} ;
[DataContract(Name="Person", Namespace="http://www.uji.es/people")]
public class Persona
{
[DataMember(Name="FirstName")] public string Nombre { get; set; }
[DataMember(Name="LastName")] public string Apellido { get; set; }
[DataMember(Name="Age")] public int Edad { get; set; }
[DataMember] public Rol[] Roles { get; set; }
}
}

El fichero contiene ahora:


<Person xmlns="http://www.uji.es/people" xmlns:i="http://www.w3.org/2001/XMLSchema-instance"><Age>
28</Age><FirstName>Pepe</FirstName><LastName>Prez</LastName><Roles xmlns:a="http://schemas.dataco

177

Buenas prcticas en desarrollo de software con .NET

ntract.org/2004/07/Serializar"><a:Rol>Estudiante</a:Rol><a:Rol>Administrativo</a:Rol></Roles></Per
son>

Ntese que al fijar un espacio de nombres propio se ha eliminado el que asignaba el serializador
automticamente. De este modo hemos eliminado cualquier referencia al nombre fsico de la clase en el
fichero XML, con lo que el formato es ms robusto frente a cambios del cdigo.
Tambin cabe advertir que se ha creado un nuevo espacio de nombres para los objetos cuya serializacin no
se ha explicitado (el tipo enumerado).
11.2.4 Serializacin de referencias
No hay problema en que un objeto apunte a otro complejo: si lo apuntado es serializable, se almacena el
grafo de objetos alcanzable desde el que serializamos.
using System.Runtime.Serialization;
namespace Serializar
{
public enum Rol
{
Estudiante,
Profesor,
Administrativo
} ;
[DataContract(Name="Person", Namespace="http://www.uji.es/people")]
public class Persona
{
[DataMember(Name="FirstName")] public string Nombre { get; set; }
[DataMember(Name="LastName")] public string Apellido { get; set; }
[DataMember(Name="Age")] public int Edad { get; set; }
[DataMember] public Rol[] Roles { get; set; }
[DataMember] public Persona MejorAmigo { get; set; }
}
}

El programa principal que ilustra el nuevo concepto es ste:


using System;
using System.IO;
using System.Runtime.Serialization;
namespace Serializar
{
class Program
{
static void Main(string[] args)
{
var p1 = new Persona
{
Nombre = "Pepe",
Apellido = "Prez",
Edad = 28,
Roles = new[] {Rol.Estudiante, Rol.Administrativo},
};
var p2 = new Persona
{
Nombre = "Toni",
Apellido = "Garca",
Edad = 30,
Roles = new[] { Rol.Estudiante },

178

Buenas prcticas en desarrollo de software con .NET

};
p1.MejorAmigo = p2;
p2.MejorAmigo = null;
var personaSerializer = new DataContractSerializer(typeof(Persona));
using (var f = new FileStream("datos.xml", FileMode.Create))
{
personaSerializer.WriteObject(f, p1);
}
using (var f = new FileStream("datos.xml", FileMode.Open))
{
var q = (Persona) personaSerializer.ReadObject(f);
Console.WriteLine("{0} {1} -> {2} {3}",
q.Nombre, q.Apellido,
q.MejorAmigo.Nombre, q.MejorAmigo.Apellido);
}
Console.ReadKey();
}
}
}

La salida por pantalla es sta:


Pepe Prez -> Toni Garca

Y si examinamos el fichero XML encontramos esto (sin formatear):


<Person xmlns="http://schemas.datacontract.org/2004/07/Serializar"
xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<Age>28</Age>
<FirstName>Pepe</FirstName>
<LastName>Prez</LastName>
<MejorAmigo>
<Age>30</Age>
<FirstName>Toni</FirstName>
<LastName>Garca</LastName>
<MejorAmigo i:nil="true"/>
<Roles>
<Rol>Estudiante</Rol>
</Roles>
</MejorAmigo>
<Roles>
<Rol>Estudiante</Rol>
<Rol>Administrativo</Rol>
</Roles>
</Person>

Hemos destacado las marcas que contienen la descripcin del mejor amigo de cada persona. Una contiene la
descripcin directamente. La otra representa el valor null de un modo especial. El espacio de nombres
asociado al prefijo i permite codificar informacin especial de los tipos .NET.
Hay un problema con las subclases. Veamos este ejemplo:
using System.IO;
using System.Runtime.Serialization;
namespace Serializar
{
public enum Rol
{

179

Buenas prcticas en desarrollo de software con .NET

Estudiante,
Profesor,
Administrativo
} ;
[DataContract(Name = "Person")]
public class Persona
{
[DataMember(Name = "FirstName")] public string Nombre { get; set; }
[DataMember(Name = "LastName")] public string Apellido { get; set; }
[DataMember(Name = "Age")] public int Edad { get; set; }
[DataMember] public Rol[] Roles { get; set; }
[DataMember] public Persona MejorAmigo { get; set; }
public static Persona DeepClone(Persona p)
{
var ds = new DataContractSerializer(typeof(Persona));
MemoryStream stream = new MemoryStream();
ds.WriteObject(stream, p);
stream.Position = 0;
return (Persona)ds.ReadObject(stream);
}
}
[DataContract]
public class Erasmus : Persona
{
[DataMember] public string PasDeOrigen { get; set; }
}
}

El programa principal intenta gestionar personas convencionales y personas Erasmus:


using System;
using System.IO;
using System.Runtime.Serialization;
namespace Serializar
{
class Program
{
static void Main(string[] args)
{
var p1 = new Persona
{
Nombre = "Pepe",
Apellido = "Prez",
Edad = 28,
Roles = new[] {Rol.Estudiante, Rol.Administrativo},
};
var p2 = new Erasmus
{
Nombre = "Toni",
Apellido = "Garca",
Edad = 30,
Roles = new[] { Rol.Estudiante },
PasDeOrigen = "Italia"
};
p1.MejorAmigo = p2;
p2.MejorAmigo = null;
var personaSerializer = new DataContractSerializer(typeof(Persona));
var ds = new DataContractSerializer(typeof (Persona));

180

Buenas prcticas en desarrollo de software con .NET

using (var f = new FileStream("datos.xml", FileMode.Create))


{
personaSerializer.WriteObject(f, p1);
}
using (var f = new FileStream("datos.xml", FileMode.Open))
{
var q1 = (Persona) personaSerializer.ReadObject(f);
Console.WriteLine("{0} {1} {2}",
q1.MejorAmigo.Nombre, q1.MejorAmigo.Apellido,
((Erasmus)q1.MejorAmigo).PasDeOrigen);
}
Console.ReadKey();
}
}
}

Sin embargo, al ejecutar salta una excepcin de tipo SerializationException en la lnea que hemos
destacado. El mensaje de la excepcin es Type 'Serializar.Erasmus' with data contract name
'Erasmus:http://schemas.datacontract.org/2004/07/Serializar' is not expected. Consider using a
DataContractResolver or add any types not known statically to the list of known types - for example, by using
the KnownTypeAttribute attribute or by adding them to the list of known types passed to
DataContractSerializer.. Nos indica que no sabe serializar la subclase de un modo que luego pueda
deserializarse.
Hemos de informar al serializador de que ha de poder deserializar los subtipos que esperamos tener que
tratar:
using System.IO;
using System.Runtime.Serialization;
namespace Serializar
{
public enum Rol
{
Estudiante,
Profesor,
Administrativo
} ;
[DataContract(Name = "Person"), KnownType(typeof(Erasmus))]
public class Persona
{
[DataMember(Name = "FirstName")] public string Nombre { get; set; }
[DataMember(Name = "LastName")] public string Apellido { get; set; }
[DataMember(Name = "Age")] public int Edad { get; set; }
[DataMember] public Rol[] Roles { get; set; }
[DataMember] public Persona MejorAmigo { get; set; }
public static Persona DeepClone(Persona p)
{
var ds = new DataContractSerializer(typeof(Persona));
MemoryStream stream = new MemoryStream();
ds.WriteObject(stream, p);
stream.Position = 0;
return (Persona)ds.ReadObject(stream);
}
}

181

Buenas prcticas en desarrollo de software con .NET

[DataContract]
public class Erasmus : Persona
{
[DataMember] public string PasDeOrigen { get; set; }
}
}

Con este cambio, todo funciona correctamente. El contenido del fichero datos.xml es ste:
<Person xmlns="http://schemas.datacontract.org/2004/07/Serializar"
xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<Age>28</Age>
<FirstName>Pepe</FirstName>
<LastName>Prez</LastName>
<MejorAmigo i:type="Erasmus">
<Age>30</Age>
<FirstName>Toni</FirstName>
<LastName>Garca</LastName>
<MejorAmigo i:nil="true"/>
<Roles>
<Rol>Estudiante</Rol>
</Roles>
<PasDeOrigen>Italia</PasDeOrigen>
</MejorAmigo>
<Roles>
<Rol>Estudiante</Rol>
<Rol>Administrativo</Rol>
</Roles>
</Person>

11.2.5 Clonacin profunda


Ahora que vemos que se puede reconstruir informacin compleja, es fcil usar el serializador para
implementar un mtodo de clonacin profunda.
using System.IO;
using System.Runtime.Serialization;
namespace Serializar
{
public enum Rol
{
Estudiante,
Profesor,
Administrativo
} ;
[DataContract(Name="Person")]
public class Persona
{
[DataMember(Name="FirstName")] public string Nombre { get; set; }
[DataMember(Name="LastName")] public string Apellido { get; set; }
[DataMember(Name="Age")] public int Edad { get; set; }
[DataMember] public Rol[] Roles { get; set; }
[DataMember] public Persona MejorAmigo { get; set; }
public static Persona DeepClone(Persona p)
{
var ds = new DataContractSerializer(typeof(Persona));
MemoryStream stream = new MemoryStream();
ds.WriteObject(stream, p);
stream.Position = 0;
return (Persona)ds.ReadObject(stream);
}
}

182

Buenas prcticas en desarrollo de software con .NET

Ntese que se usa un stream de memoria para evitar accesos al sistema de ficheros.
Para obtener una copia de un objeto utilizaramos el mtodo as:
var copia = Persona.DeepClone(p1);

12 Inyeccin de dependencias
Hemos aprendido a disear aplicaciones guindonos por las pruebas unitarias, y hemos aprendido tambin a
eliminar dependencias con un diseo que refuerza el principio de la responsabilidad nica (SRP) y que nos
hace depender de abstracciones, de acuerdo con el principio de inversin de dependencias (DIP). Este
segundo enfoque produce cdigo ms fcil de mantener, en el que los objetos actan como servicios.
Nuestro lector de RSS ha usado:

Un sistema de registro de actividad, que implementa la interfaz log4net.ILog (y cuya


implementacin se especifica en el fichero de configuracin).
Un servicio para la carga de fuentes RSS a partir de una URL, que implementa una interfaz ILoader.
Un servicio para el anlisis del texto que codifica la informacin como XML, que implementa una
interfaz IParser. Hemos diseado tres versiones del analizador: una par RSS 2.0, otra para Atom 1.0
y otro capaz de analizar los dos formatos.
Un servicio para formatear el texto como una sucesin de lneas, que implementa la interfaz
IFormatter.
Un servicio para mostrar las lneas en un dispositivo de salida, que implementa la interfaz IWritter.
Un servicio para la gestin de URLs, que implementa la interfaz IUrlManager.

Sustituir un servicio por otra que desempee el mismo papel es relativamente sencillo si seguimos este
enfoque. Si apareciera un nuevo formato de RSS, o un nuevo formato de salida, o un nuevo dispositivo de
salida, adaptar nuestro software al cambio resultara casi inmediato.
Nuestro lector no es ms que un cliente de los servicios. La relacin entre cliente y servicio implica la
existencia de un contrato entre ambos objetos. Cliente y servicio reciben tambin los nombres,
respectivamente, de:

Dependiente (dependent): el cliente que necesita a otros objetos para llevar a cabo su cometido.
Dependencia (dependency): el servicio que es necesitado por un cliente para llevar a cabo su
cometido.

En nuestro cdigo hemos gestionado las dependencias manualmente. Hemos aprendido que las interfaces
permiten evitar depender de una implementacin concreta. Los servicios se definen como
implementaciones de una interfaz y el cliente slo expresa su dependencia con respecto de la interfaz. As
pues, el cliente no construye instancias de los servicios y espera a que alguien inyecte las dependencias
desde el exterior. Un modo de hacerlo (no es el nico) consiste en definir un constructor que espera una
argumento para cada dependencia. Quien usa a nuestro cliente lo construir suministrando instancias
concretas de los servicios que usa.

183

Buenas prcticas en desarrollo de software con .NET

Las relaciones entre dependientes y dependencias generan un grafo de objetos. La Inyeccin de


Dependencias (DI, por Dependency Injection) se ocupa de construir fiablemente los grafos de objetos y de las
estrategias, patrones y buenas prcticas para este fin.
Vamos a abundar en el principio de Inversin de Dependencias y vamos a estudiar herramientas que nos
ayuden en el diseo.
Todo el sistema se apoya en que escribimos cdigo:

Orientado al comportamiento: Nuestro objetivo no es construir ms y ms objetos, sino programar


un comportamiento para un sistema software.
Modular: El cdigo agrupado en unidades modulares es ms mantenible, reutilizables y fcil de
empaquetar (y desplegar). El cdigo modular puede concentrarse en pequeos subconjuntos de
funcionalidad y facilitar el desarrollo de cada uno de ellos.
Testable: Los mdulos facilitan la escritura de pruebas que verifican el comportamiento de las
unidades sin depender del sistema completo. Un buen diseo modular facilita, por ejemplo, la
sustitucin de piezas por impostores durante las pruebas.

12.1 Inyeccin (manual) de dependencias


Empezaremos por estudiar algunas formas de inyectar dependencias (o cablear (wiring) dependencias,
que tambin se denomina as al acto de inyectarlas). Recordemos que deseamos evitar cdigo como ste:
namespace Ejemplo
{
public class Servicio
{
public void PideAlgo()
{
System.Console.WriteLine("Algo!");
}
}
public class Cliente
{
private Servicio _servicio; // Dependencia!
public Cliente()
{
_servicio = new Servicio(); // Dependencia!
}
}
public class Usuario
{
public void Usa()
{
Cliente c = new Cliente();
}
}
}

La dependencia de nuestro cliente con una implementacin particular del servicio crea una rigidez en el
cdigo que acabar pasando factura. La primera idea es independizarse de la implementacin concreta
usando una interfaz para el servicio. Pese a hacer eso, este cdigo sigue manteniendo una dependencia al
construir una instancia de Servicio y al construir una instancia de Cliente:

184

Buenas prcticas en desarrollo de software con .NET

namespace Ejemplo
{
public interface IServicio
{
void PideAlgo();
}
public class Servicio : IServicio
{
public void PideAlgo()
{
System.Console.WriteLine("Algo!");
}
}
public class Cliente
{
private IServicio _servicio; // Dependencia suprimida.
public Cliente()
{
_servicio = new Servicio(); // Dependencia!
}
}
public class Usuario
{
public void Usa()
{
Cliente c = new Cliente();
}
}
}

12.1.1 Inyeccin de dependencias por constructor


Esta ya la conocemos y consiste en que el constructor espere que se le suministre la implementacin
mediante un argumento del constructor:
namespace Ejemplo
{
public interface IServicio
{
void PideAlgo();
}
public class Servicio : IServicio
{
public void PideAlgo()
{
System.Console.WriteLine("Algo!");
}
}
public class Cliente
{
private IServicio _servicio; // Dependencia suprimida.
public Cliente(IServicio servicio) // Dependencia suprimida por inyeccin.
{
_servicio = servicio;
}
}

185

Buenas prcticas en desarrollo de software con .NET

public class Usuario


{
public void Usa()
{
Cliente c = new Cliente(new Servicio());
}
}
}

La primera ventaja que podemos apreciar es la posibilidad de inyectar mocks en las pruebas unitarias de la
clase Cliente, pues basta con suministrar uno al constructor del SUT.
12.1.2 Inyeccin por propiedades
Definir constructores complejos puede llegar a ser un problema: una lista larga de parmetros es fuente
probable de errores. El usuario puede no recordar el orden en el que debe suministrar los argumentos
cuando dos o ms presentan el mismo tipo y, e cualquier caso, es fatigoso andar suministrando argumento
tras argumento.
Podemos usar propiedades como puntos de enganche para las dependencias y mantener el constructor tan
sencillo como sea posible:
public class Cliente
{
public IServicio Servicio { get; set; }
public Cliente()
{
}
}
public class Usuario
{
public void Usa()
{
Cliente c1 = new Cliente();
c1.Servicio = new Servicio1();
Cliente c2 = new Cliente { Servicio = new Servicio1() };

}
}

Esta tcnica debe usarse con cuidado, pues el usuario podra olvidar asignar un valor a la propiedad y
provocar, ms tarde, un fallo.
12.1.3 Inyeccin con un Builder
Recordemos que un Builder es un patrn de diseo para efectuar construcciones complejas con ayuda de un
objeto auxiliar (el Builder), que con uno o ms mtodos va acumulando los datos necesarios para que la
construccin sea exitosa. En este ejemplo presentamos un Builder un tanto forzado, ya que la clase no es
demasiado compleja, pero ayuda a ilustrar la tcnica a un nivel bsico:
public class Cliente
{
public class Builder
{
public class Config
{
public IServicio Servicio;

186

Buenas prcticas en desarrollo de software con .NET

}
private Config _config;
public Builder()
{
_config = new Config();
}
public Builder ConServicio(IServicio servicio)
{
_config.Servicio = servicio;
return this;
}
public Config Build()
{
if (_config.Servicio == null)
{
throw new InvalidOperationException();
}
else
{
return _config;
}
}
}
private IServicio _servicio;
public Cliente(Builder.Config config)
{
_servicio = config.Servicio;
}
}
public class Usuario
{
public void Usa()
{
Cliente c = new Cliente((new Cliente.Builder()).ConServicio(new Servicio()).Build());
}
}

12.1.4 Inyeccin mediante factoras abstractas


Imaginemos que hay dos implementaciones del servicio: Servicio1 y Servicio2. Una factora puede
producir objetos de una u otra implementacin. Cuando el cliente necesita una instancia, pide a la factora
que se la cree. Naturalmente, producir una instancia de un tipo determinado, as que parece que no hemos
eliminado la dependencia:
namespace Ejemplo
{
public interface IServicio
{
void PideAlgo();
}
public class Servicio1 : IServicio
{
public void PideAlgo()
{
System.Console.WriteLine("Algo!");
}

187

Buenas prcticas en desarrollo de software con .NET

}
public class Servicio2 : IServicio
{
public void PideAlgo()
{
System.Console.WriteLine("Something!");
}
}
public class FactoraDeServicios
{
public IServicio CreaServicio1()
{
return new Servicio1();
}
public IServicio CreaServicio2()
{
return new Servicio2();
}
public IServicio CreaServicio()
{
return CreaServicio1();
}
}
public class Cliente
{
private IServicio _servicio;
public Cliente()
{
_servicio = new FactoraDeServicios().CreaServicio();
}
}

public class Usuario


{
public void Usa()
{
Cliente c = new Cliente();
}
}
}

Pero s lo hemos hecho, pues no hemos de tocar el cliente para cambiar su servicio: hemos de cambiar la
factora. Si queremos inyectar un stub al cliente, basta con que la factora produzca un stub.
12.1.5 Inyeccin mediante localizador de servicios
Un localizador de servicios es un tipo de factora. Puede verse como un diccionario que permite acceder al
servicio a travs de una clave.
public class LocalizadorDeServicios
{
Dictionary<string, object> _table = new Dictionary<string, object>();
public void DeclaraServicio(string clave, object servicio)
{
_table[clave] = servicio;
}

188

Buenas prcticas en desarrollo de software con .NET

public object Obtn(string clave)


{
return _table[clave];
}
}
public class Cliente
{
private IServicio _servicio;
public Cliente(LocalizadorDeServicios localizador)
{
_servicio = localizador.Obtn("IServicio");
}
}
public class Usuario
{
public void Usa()
{
LocalizadorDeServicios localizador = new LocalizadorDeServicios();
localizador.DeclaraServicio("IServicio", new Servicio1());
Cliente c = new Cliente(localizador);
}
}

El localizador puede inyectarse en el constructor o ser una variable global que permita resolver las
demandas de servicio de todo el sistema o parte de l.

12.2 El principio de Hollywood


Todas las tcnicas que hemos considerado parten de un papel activo por parte del cliente. Esta actitud viola
el denominado principio de Hollywood, que se resume en
No nos llame. Nosotros le llamaremos.
El principio de Hollywood (que toma nombre de la expresin que usan los directores de casting para
despedir a los candidatos a un papel) traspasa el papel activo a un nuevo objeto: el inyector de
dependencias, que toma el control. Por ello, el principio de Hollywood tambin se conoce como IoC, siglas
de Inversin del Control en ingls (Inversion of Control).
Aplicando el principio de Hollywood, los clientes se limitan a declarar de algn modo qu dependencias
necesitan y el inyector construye el grafo de objetos a partir de esta declaracin y de algn contenedor de
dependencias (un objeto similar al localizador de dependencias que hemos visto antes).

12.3 Libreras para Inyeccin de Dependencias


Hay muchas libreras para inyeccin de dependencias en .NET. Podemos citar:

Castle Windsor (http://www.castleproject.org/container/index.html). Cdigo abierto (licencia


Apache 2), bien documentado y muy utilizado.
StructureMap (http://structuremap.net/structuremap/index.html). Cdigo abierto (Apache 2), con
comunidad de desarrollo muy activa, interfaz fluida.
Spring.NET (http://www.springframework.net/). Cdigo abierto (Apache 2), basado en la librera
Spring de Java, que es el estndar de facto en esa plataforma.
Autofaq (http://code.google.com/p/autofac/). Cdigo abierto (licencia MIT).

189

Buenas prcticas en desarrollo de software con .NET

Unity (http://unity.codeplex.com/). Cdigo (semi)abierto (licencia MS-PL), forma parte de las


herramientas enmarcadas en Patterns and Practices de Microsoft.
Ninject (http://ninject.org/). Cdigo abierto (Apache 2).
S2Container.NET (http://s2container.net.seasar.org/en/index.html). Port de la herramienta Seasar2
de Java.
PicoContainer.NET (http://docs.codehaus.org/display/PICO/Home). Port de la herramienta
PicoContainer de Java.

Nosotros usaremos Castle Windsor. Aunque Unity es muy utilizado y cuenta con el apoyo de Microsoft,
Castele Windsor es tambin muy popular y otras herramientas se apoyan en Castle Windsor. Por otra parte,
resulta ms sencillo a efectos didcticos, y nuestro objetivo es aprender una serie de conceptos con
herramientas que los pongan en prctica. Una vez aprendidos con una herramienta, su uso con otra resulta
generalmente trivial. Castle Windsor nos permite alcanzar nuestro objetivo ms rpidamente.

13 Castle Windsor
Castle Project es un framework de cdigo abierto para proveer a la comunidad .NET de herramientas que
ayuden a construir aplicaciones basadas en buenos principios arquitectnicos. Entre esas herramientas se
encuentra un sistema de inversin de control: Castle Windsor.
Ya sabemos lo que es la Inversin de Control (IdC): es un principio de diseo que permite inyectar
dependencias. La IdC plantea el problema prctico de tener que preparar todas las dependencias de un
objeto antes de ponerlo en funcionamiento o, incluso, de crearlo. Esto puede complicar la lgica de nuestra
aplicacin y dificultar cambios en el diseo, pues cualquier modificacin de un punto de inyeccin obliga a
retocar nuestro cdigo.
Los Contenedores de Inversin de Control (Inversion of Control Container) tratan de paliar este problema.
Son objetos que memorizan asociaciones entre interfaces e implementaciones y que son capaces de inyectar
las dependencias all donde son necesarias.
Ntese que la metodologa de diseo que hemos seguido hasta el momento ha descompuesto las
aplicaciones en conjunto de datos y servicios. En el ejemplo RSS, los datos se representaban con clases como
Feed e Item. Los servicios eran implementaciones de diferentes interfaces: IFormatter, ILoader
Tpicamente, en el Contenedor de IdC slo se dan de alta los servicios, no los datos. Este dar de alta
consiste en establecer una asociacin entre la interfaz (o clase) y la implementacin que deseamos usar. El
trmino usado para dar de alta es registrar, las implementaciones se denominan componentes y la
creacin de instancias all donde se necesitan se denomina resolucin (del componente).
Cuando se resuelve un componente, el componente debe crearse de algn modo o estar ya disponible.
Castle Windsor permite definir diferentes estilos de vida (lifestyle) para los componentes. Por defecto, el
estilo de vida es Singleton, que consiste en crear una nica instancia de un componente que comparten
todos los que la necesitan (recuerda el patrn de diseo del mismo nombre?). Pero es posible crear
instancias diferentes con cada resolucin o dependientes de cierta informacin de contexto.

190

Buenas prcticas en desarrollo de software con .NET

13.1 Instalacin
En la pgina http://www.castleproject.org/castle/download.html encontraremos un apartado titulado
Windsor 2.5.3 con el enlace Download release (cuya URL es
https://sourceforge.net/projects/castleproject/files/Windsor/2.5/Castle.Windsor.2.5.3.zip/download).
El paquete contiene libreras compiladas para diferentes Target framework. Tendremos que usar el propio
de nuestra aplicacin. En la carpeta dotnet40, por ejemplo, encontramos las DLL Castle.Core.dll y
Castle.Windsor.dll. Basta con hacer referencia a estas libreras en nuestro proyecto para que podamos usar
el sistema Castle Windsor.

13.2 Patrn de uso de un contenedor Castle Windsor


Se sigue un proceso que se conoce como de las tres llamadas o RRR, por Register-Resolve-Release.
13.2.1 Register
En el punto de entrada de nuestra aplicacin hemos de preparar el contenedor. El proceso de inicializacin o
bootstrapping suele consistir en un solo mtodo como ste:
public IWindsorContainer BootstrapContainer()
{
return new WindsorContainer().Install(instalador1, instalador2);
}

El mtodo Install de WindsorContainer inicializa el contenedor y declara sus instaladores. Los


instaladores son objetos que implementan la interfaz IWindsorInstaller, con un solo mtodo denominado
Install.
Veamos un ejemplo de instalador:
public class RepositoriesInstaller : IWindsorInstaller
{
public void Install(IWindsorContainer container, IConfigurationStore store)
{
container.Register(AllTypes.FromAssemblyNamed("Acme.Crm.Data")
.Where(type => type.Name.EndsWith("Repository"))
.WithService.DefaultInterface()
.Configure(c => c.LifeStyle.PerWebRequest));
}
}

Ms adelante veremos qu significa cada elemento del cuerpo de Install. Lo lgico en una aplicacin es
que creemos varios instaladores, uno por cada paquete de funcionalidad. En una aplicacin MVC, por
ejemplo, podramos tener un instalador de vistas, otro de controladores y otro de modelos. Supongamos
que tenemos una aplicacin con controladores y repositorios. El mtodo de bootstrapping podra ser ste:
public IWindsorContainer BootstrapContainer()
{
return new WindsorContainer().Install(new ControllersInstaller(), new RepositoriesInstaller());
}

El mtodo Install acepta un nmero variable de parmetros.


Para evitar el tedio de dar de alta todos los instaladores, Castle Windsor ofrece dos utilidades:

La clase esttica FromAssembly, que permite dar de alta todos los instaladores de un ensamblado.

191

Buenas prcticas en desarrollo de software con .NET

La clase esttica Configuration, que permite usar una configuracin externa.

Este mtodo de bootstrapping se apoya en ambas utilidades:


public IWindsorContainer BootstrapContainer()
{
return new WindsorContainer().Install(Configuration.FromAppConfig(),
FromAssembly.This());
}

13.2.1.1 FromAssembly
Estudiemos con un poco ms de calma las posibilidades que ofrece FromAssembly:

Con FromAssembly.This() se accede a todos los instaladores (clases que implementan


IWindsorInstaller) en el ensamblado desde el que se efecta la llamada.
Con FromAssembly.Named("nombre") se carga el ensamblado que tiene el nombre que se
suministra (se busca en los lugares estndar). Tambin se puede suministrar la ruta a un DLL o a un
.exe.
Con FromAssembly.Containing<ServicesInstaller>() lo instala desde el ensamblado que
contiene el tipo entre < y >.
Con FromAssembly.InDirectory(new AssemblyFilter("Extensions") se instala de los
ensamblados que se encuentran en el directorio que se indique. El objeto de la clase
AssemblyFilter ayuda a reducir el nmero de ensamblados en los que buscar.
Con FromAssembly.Instance(this.GetPluginAssembly()) se instala desde un ensamblado
arbitrario que se suministra directamente.

13.2.1.2 Configuration
La clase Configuration ayuda cuando la configuracin se suministra con un fichero externo. He aqu
algunos casos de uso:
container.Install(
Configuration.FromAppConfig(),
Configuration.FromXmlFile("settings.xml"),
Configuration.FromXmlFile(
new AssemblyResource("assembly://Acme.Crm.Data/Configuration/services.xml")));

Evidentemente, FromAppConfig carga la configuracin de app.config y los otros de un fichero XML.


Estudiaremos ms adelante la sintaxis de los ficheros XML.
13.2.2 Resolve
En la fase de resolucin, pedimos al contenedor que inyecte dependencias all donde hagan falta:
var container = new BootstrapContainer();
var shell = container.Resolve<IShell>();
shell.Display();

Lo normal es que slo invoquemos a Resolve una sola vez, en el objeto raz de nuestra aplicacin. El
contenedor se encarga de resolver las dependencias anidadas. Es parte de la gracia.
13.2.3 Release
Al final de la aplicacin hemos de invocar necesariamente al mtodo Dispose del contenedor.
container.Dispose()

192

Buenas prcticas en desarrollo de software con .NET

Tngase en cuenta que el contenedor mantiene referencias a objetos y que, si no se liberan


apropiadamente, podemos tener problemas.

13.3 Una API con interfaz fluida


Castle Windsor ofrece una interfaz fluida para registrar componentes. Aunque tambin se puede configurar
con XML, generalmente se prefiere la interfaz fluida. Para usar la API hemos de incluir las referencias
Castle.Core.dll y Castle.Windsor.dll y usar el espacio de nombres Castle.MicroKernel.Registration .
Partiremos de la base de que se ha creado una instancia de WindsorContainer:
IWindsorContainer container = new WindsorContainer();

Para registrar un par interfaz-componente o clase-componente usaremos el mtodo Register. Podemos


usarlo libremente, pero recuerde que la prctica recomendada es definir un instalador en el que se hacer las
llamadas a Register.
13.3.1 Registro elemento a elemento
13.3.1.1 Registro de un tipo
Si deseamos que el contendor gestione la vida de las instancias de una clase, sin necesidad de definir una
interfaz, podemos hacerlo as:
container.Register(Component.For<MyServiceImpl>());

Con ello se crear una instancia nica de MyServiceImpl que se inyectar donde se quiera usar un
myServiceImpl. Ntese que la instancia es nica: el estilo de vida por defecto es Singleton.
13.3.1.2 Registro de un tipo para una interfaz
Con esta llamada asociamos la clase MyServiceImpl a la interfaz IMyService:
container.Register(Component.For<IMyService>().ImplementedBy<MyServiceImpl>());

Alternativamente se puede expresar lo mismo con:


container.Register(Component.For(typeof(IMyService).ImplementedBy(typeof(MyServiceImpl));

13.3.1.3 Registro de un tipo generic


Castle Windsor permite trabajar con tipos genricos detallando cada caso:
container.Register(Component.For<IRepository<Customer>>().ImplementedBy<NHRepository<Customer>>(),
Component.For<IRepository<Order>>().ImplementedBy<NHRepository<Order>>());

Pero se puede crear una asociacin independiente del tipo entre < y >. La sintaxis debe ser la que hace uso
de typeof:
container.Register(Component.For(typeof(IRepository<>).ImplementedBy(typeof(NHRepository<>));

13.3.1.4 Declaracin del estilo de vida


Se puede declarar un estilo de vida con la propiedad Lifestyle:
container.Register(Component.For<IMyService>()
.ImplementedBy<MyServiceImpl>()
.LifeStyle.Transient);

193

Buenas prcticas en desarrollo de software con .NET

Los valores que se pueden usar son:

Singleton: una instancia nica para el contenedor. Es el estilo de vida por defecto.
Transient: una instancia cada vez que se necesita un componente. (Deben liberarse manualmente

para no tener problemas de fuga de memoria.)


PerThread: una instancia en el mbito de cada hilo de ejecucin.
PerWebRequest: una instancia por cada peticin web.
Pooled: se crea un conjunto de instancias y se devuelve una de ellas para atender la peticin. El
usuario debe liberar los elementos para que el conjunto disponga de instancias con las que atender
peticiones. Hay dos parmetros que definen su comportamiento:
o initialSize: nmero de elementos instanciados inicialmente.
o maxSize: nmero mximo de elementos simultneamente vivos.
Custom: definido por el usuario.

13.3.1.5 Registro de ms de un componente para un mismo servicio


Se puede dar de alta ms de un componente para un mismo servicio. Por defecto se sirve el primero dado de
alta:
container.Register(Component.For<IMyService>().ImplementedBy<MyServiceImpl>(),
Component.For<IMyService>().ImplementedBy<OtherServiceImpl>());

13.3.1.6 Registro de una instancia ya existente


Se pueda dar de alta una instancia ya existente, que se servir como un Singleton (sin alternativa posible):
var customer = new CustomerImpl();
container.Register(Component.For<ICustomer>().Instance(customer));

13.3.1.7 Registro de una factora mediante delegado


Se puede dar de alta un delegado con el que construir instancias:
container.AddFacility<FactorySupportFacility>()
.Register(Component.For<IMyService>()
.UsingFactoryMethod(() => MyLegacyServiceFactory.CreateMyService()));

Es posible tratar a la propia factora como una dependencia:


container.AddFacility<FactorySupportFacility>()
.Register(Component.For<IMyFactory>().ImplementedBy<MyFactory>(),
Component.For<IMyService>()
.UsingFactoryMethod(kernel => kernel.Resolve<IMyFactory>().Create()));

13.3.1.8 Postproceso del componente creado


Puede resultar necesario efectuar un postproceso al componente recin creado antes de su primer uso (si no
se crean con una factora propia, que ya puede/debe incluir esas posibles acciones). Para ello tenemos el
mtodo OnCreate, que recibe una lambda-funcin con dos parmetros (un IKernel y la instancia recin
creada):
container.Register(Component.For<IService>()
.ImplementedBy<MyService>()
.OnCreate((kernel, instance) => instance.Name += "a"));

194

Buenas prcticas en desarrollo de software con .NET

13.3.1.9 Asignar un nombre a un componente


No slo se puede acceder por tipo a una dependencia: tambin con un nombre. El nombre se resuelve con el
identificador del parmetro con el que se inyecta la dependencia.
container.Register(Component.For<IMyService>()
.ImplementedBy<MyServiceImpl>()
.Named("myservice.default"),
Component.For<IMyService>()
.ImplementedBy<OtherServiceImpl>()
.Named("myservice.alternative"),
Component.For<ProductController>()
.ServiceOverrides(ServiceOverride.ForKey("myService")
.Eq("myservice.alternative")));
public class ProductController
{
public ProductController(IMyService myService)
{
}
}

13.3.1.10
Registro de componentes con mltiples interfaces
Si un mismo objeto presta varios servicios, se puede registrar para todos ellos:
container.Register(Component.For<IUserRepository, IRepository>().ImplementedBy<MyRepository>());

13.3.2 Dependencias en lnea


Al registrar componentes puede resultar necesario proporcionar dependencias que no son servicios. Por
ejemplo, podemos tener que proporcionar valores de parmetros.
13.3.2.1 Paso de valores estticos
Si hay una dependencia con respecto al valor de una propiedad, podemos proporcionar el valor as:
var twitterApiKey = @"the key goes here";
container.Register(Component.For<ITwitterCaller>().ImplementedBy<MyTwitterCaller>()
.DependsOn(Property.ForKey("APIKey").Eq(twitterApiKey)));

El nombre APIKey concordar sin atender a la caja (minsculas/maysculas).


Como esa sintaxis es farragosa, hay una alternativa basada en objetos annimos:
var twitterApiKey = @"the key goes here";
container.Register(Component.For<ITwitterCaller>().ImplementedBy<MyTwitterCaller>()
.DependsOn(new { apiKey = twitterApiKey } ));

Y an otra basada en diccionarios:


var twitterApiKey = @"the key goes here";
container.Register(Component.For<ITwitterCaller>().ImplementedBy<MyTwitterCaller>()
.DependsOn(new Dictionary<string,string>{{"APIKey", twitterApiKey}})));

13.3.2.2 Seleccin de implementacin


Podemos seleccionar una implementacin por nombre cuando se haya de resolver la dependencia de un
componente:
container.Register(Component.For<ITransactionProcessingEngine>()
.ImplementedBy<TransactionProcessingEngine>()
.DependsOn(Property.ForKey("Logger").Is("secureLogger"))

195

Buenas prcticas en desarrollo de software con .NET

);

En el ejemplo se dice que lo que se denomine Logger debe resolverse con una instancia de secureLogger,
que es un nombre con el que se ha registrado un logger determinado en el contenedor.
Alternativamente se puede usar el mtodo ServiceOverrides:
container.Register(Component.For<ITransactionProcessingEngine>()
.ImplementedBy<TransactionProcessingEngine>()
.DependsOn(ServiceOverride.ForKey("Logger").Eq("secureLogger")));

Este mtodo hace posible usar una expresin ms elegante:


container.Register(Component.For<ITransactionProcessingEngine>()
.ImplementedBy<TransactionProcessingEngine>()
.ServiceOverrides(new { logger = "secureLogger" }));

13.3.2.3 Parmetros dinmicos


Puede que el valor que deseamos suministrar no se conozca hasta el tiempo de ejecucin. El mtodo
DynamicParameters recibe un delegado con dos parmetros: un IKernel y un diccionario con los
parmetros suministrados.
container.Register(Component.For<ClassWithArguments>()
.LifeStyle.Transient
.DynamicParameters((k, d) => d["createdTimestamp"] = DateTime.Now));

Opcionalmente se puede devolver un mtodo (un delegado) que se invocar cuando el objeto se destruya:
container.Register(Component.For<ClassWithArguments>()
.LifeStyle.Transient
.DynamicParameters((k, d) =>
{
d["arg1"] = "foo";
return kk => ++releaseCalled;
}));

13.3.3 Registro por convenio


Podemos evitar el registro elemento a elemento usando algunos convenios. Con la clase AllTypes se puede
acceder a grupos de tipos que cumplen ciertas propiedades. La interfaz fluida que hace uso de AllTypes
recurre a tres pasos:

Seleccin del ensamblado.


AllTypes.FromThisAssembly()

Seleccin del tipo base o la condicin.


o Fijando un tipo base o una interfaz.
AllTypes.FromThisAssembly().BasedOn<IMessage>()

Con una condicin.


AllTypes.FromAssemblyContaining<MyController>()
.Where(t=>Attribute.IsDefined(t, typeof(CacheAttribute)))

Sin restricciones.
AllTypes.FromAssemblyNamed("Acme.Crm.Services").Pick()

Filtrado adicional y configuracin.

Veamos algunos ejemplos.

196

Buenas prcticas en desarrollo de software con .NET

Podemos, por ejemplo, registrar todos los descendientes de un tipo dado:


container.Register(AllTypes.FromThisAssembly()
.BasedOn<SmartDispatcherController>()
.Configure(c => c.Lifestyle.Transient));

O registrar los que son descendientes de una interfaz u otra:


container.Register(AllTypes.FromAssemblyContaining<DeleteCustomerCommand>()
.BasedOn<IEntity>()
.BasedOn<IHandler>());

En los dos casos anteriores no est claro a que se asocia el tipo. Se puede especificar con WithService. En
ese ejemplo se asocia ICommand<T> al primer componente que implementa esa interfaz:
container.Register(AllTypes.FromThisAssembly()
.BasedOn(typeof(ICommand<>)).WithService.Base()
.BasedOn(typeof(IValidator<>)).WithService.Base());

Este otro selecciona el tipo atendiendo al nombre de la interfaz y de la clase: IServicio se asociar a
Servicio:
container.Register(AllTypes.FromThisAssembly()
.Where(Component.IsInNamespace("Acme.Crm.Services"))
.WithService.DefaultInterface());

Con este otro se registran todas las clases que implementen directa o indirectamente una interfaz:
container.Register(AllTypes.FromThisAssembly()
.BasedOn<IService>().WithService.FromInterface());

Con WithService.AllInterfaces() se registra un componente en todas las interfaces que implementa.


Con WithService.Self() se registra como un servicio asociado a su propio tipo. Y con
WithService.Select se proporciona lgica con un delegado para indicar a qu se asocia una clase.
Es posible acceder a los tipos no pblicos de una interfaz:
container.Register(AllTypes.FromThisAssembly()
.IncludeNonPublicTypes()
.BasedOn<NonPublicComponent>());

13.3.4 Registro condicional


Es posible registrar componentes imponiendo condiciones.
13.3.4.1 Slo si no se haba registrado antes
container.Register(Component.For(typeof(IRepository<>))
.ImplementedBy(typeof(Repository<>))
.Unless(Component.ServiceAlreadyRegistered));

13.3.4.2 Filtrando componentes en registro mltiple


container.Register(AllTypes.Of<ICustomer>()
.FromAssembly(Assembly.GetExecutingAssembly())
.Unless(t => typeof(SpecificCustomer).IsAssignableFrom(t)));
container.Register(AllTypes.Of<ICustomer>()
.FromAssembly(Assembly.GetExecutingAssembly())
.If(t => t.FullName.Contains("Chain")));

197

Buenas prcticas en desarrollo de software con .NET

container.Register(AllTypes.FromAssembly(Assembly.GetExecutingAssembly())
.Where(Component.IsInSameNamespaceAs<FooRepository>())
.WithService.FirstInterface());
container.Register(AllTypes.Of<CustomerChain1>()
.Pick(from type in Assembly.GetExecutingAssembly().GetExportedTypes()
where type.IsDefined(typeof(SerializableAttribute), true)
select type));

13.4 Proceso interno de resolucin de dependencias


Supongamos que hemos dado de alta una asociacin interfaz-implementacin en el contenedor. Qu
ocurre cuando se resuelve la dependencia, es decir, cuando alguien declara necesitar una instancia de una
clase que implemente la interfaz? Castle Windsor acta en una serie de pasos:
1. En primer lugar, comprueba que hay una asociacin dada de alta para esa interfaz. Si no hay un
componente registrado, el contenedor trata de registrarlo perezosamente. Si no tiene xito, lanza
una excepcin ComponentNotFoundException. Si tiene xito, el contenedor obtendr un manejador
del componente (handler) y le pedir que resuelva la instancia del componente.
2. El manejador invoca los parmetros dinmicos que se declararon en su momento.
3. Si no se dan argumentos en lnea, el contenedor trata de resolver todas las dependencias que tenga
el componente. Para ello intenta resolver primero las dependencias por nombre y despus por tipo.
Si no lo consigue, lanza una excepcin HandlerException.
4. Si todo fue bien, se pide a un gestor de estilo de vida (lifestyle manager) que resuelva el
componente. El gestor de estilo de vida funciona, en principio, as:
a. Si ya hay una instancia de ese componente, se entrega una referencia a la instancia.
b. Si no, se invoca a un activador de componentes para crear una instancia nueva. Esto supone
invocar el constructor de una clase. Cuando se ha creado el componente, se invoca a todos
los comision concerns del componente, se dispara un evento ComponentCreated y se
devuelve la instancia al gestor de ciclo de vida para que devuelva, a su vez, una referencia a
ella.

13.5 Configuracin con app.config (o, en general, con fichero XML)


Ya sabemos trabajar con ficheros app.config. Castle Windsor permite definir una seccin propia en la que
definir componentes. He aqu un ejemplo:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="castle"
type="Castle.Windsor.Configuration.AppDomain.CastleSectionHandler, Castle.Windsor"/>
</configSections>
<castle>
<components>
<component id="userProviderService"
service="Example.Windsor.IUserProvider, Windsor_Example"
type="Example.Windsor.UserProvider, Windsor_Example"/>
<component id="userManagerService"
service="Example.Windsor.IUserManager, Windsor_Example"
type="Example.Windsor.UserManager, Windsor_Example"/>
<component id="companyManagerService"
type="Example.Windsor.CompanyManager, Windsor_Example"/>
</components>
</castle>
</configuration>

198

Buenas prcticas en desarrollo de software con .NET

En el elemento <components> podemos crear elementos <component>. Cada elemento declara un


componente que se da de alta en el contenedor.
El XML no tiene por qu ser el app.config. Vale cualquier otro fichero XML:
IWindsorContainer _container = new WindsorContainer(new XmlInterpreter("CastleConfig.xml"));

Como hemos dado de alta los componentes de dos modos, esto es, con un un identificador y como un
servicio, podemos resolver los componentes de cualquiera de los dos modos:
userManager = (IUserManager)_container.Resolve("userManagerService");
userManager = _container.Resolve<IUserManager>();

14 Aspect Oriented Programming


En el software es comn encontrar lo que se conoce por intereses ortogonales (cross-cutting concerns). Un
inters (concern) es un rea cohesiva de funcionalidad. Por ejemplo, el registro de actividades es un
inters transversal, o el uso de cachs para reducir el tiempo de respuesta a costa de memoria o aadir
control de acceso a un mtodo en funcin de los roles de un usuario. La funcionalidad suele implementarse
con clases, mtodos, etc., pero los intereses ortogonales no aceptan cmodamente este tipo de divisin. El
registro de actividades, por ejemplo, puede hacerse al inicio y final de cada mtodo invocado. Escribir el
cdigo correspondiente al inicio y final de cada mtodo no es muy apropiado. No slo porque resulte
incmodo, sino tambin porque el mtodo debera hacer aquello para lo que se ha diseado y nada ms.
Registrar la actividad puede resultar de inters para la aplicacin, pero su lgica escapa al inters ms
inmediato del mtodo.
Dicho esto, ya podemos definir la programacin orientada a aspectos (AOP, de Aspect Oriented
Programming) como un paradigma de programacin que aumenta la modularidad permitiendo la separacin
de los intereses ortogonales.

14.1 Una solucin basada en el patrn Proxy


Si queremos respetar que cada mtodo hace lo que debe hacer y nada ms, podemos definir un decorador
que enriquezca nuestra implementacin con la funcionalidad deseada. La clase decorada mantendra la
lgica que le es propia y la decoradora podra gestionar el registro de actividad antes y/o despus de cada
llamada a un mtodo de la decorada.
El modelo de composicin propio de los decoradores facilita el enriquecer la funcionalidad de una clase con
ms de un inters ortogonal.
Por cierto, los decoradores tienen por objeto enriquecer la funcionalidad de una clase dinmicamente.
Cuando el objetivo se centra ms en controlar el acceso al objeto del que se apropia, el patrn se conoce por
Proxy (apoderado). El Proxy pretende esconder los detalles del cliente y es corriente que el Proxy cree
directamente al objeto del que se apropia (a diferencia del Decorator, que siempre lo recibe como
parmetro en la construccin). Es ms, la relacin entre apoderado y objeto del que se toma posesin suele
cerrarse en tiempo de compilacin, cuando el decorador permite que la relacin se establezca en tiempo de
ejecucin.
La solucin parece apropiada, pero obliga a definir ms y ms clases. Hay herramientas que simplifican esta
labor.

199

Buenas prcticas en desarrollo de software con .NET

14.1.1 Castle DynamicProxy


Castle Project ofrece una librera que facilita la creacin dinmica de Proxies, esto es, que permite obtener al
vuelo clases que extienden la funcionalidad de otras hacindose cargo de su construccin (recuerde: un
Proxy se diferencia de un Decorator en el proceso de construccin). La extensin de funcionalidad se
proporciona mediante interceptores (Interceptors) que se pueden encadenar.
Los interceptores son objetos que implementan la interfaz IInterceptor. Esa interfaz consiste en un nico
mtodo: Intercept, que recibe un objeto de tipo IInvocation y no devuelve nada. Este ejemplo sencillo
ayuda a entender un interceptor.
using System;
using Castle.DynamicProxy;
namespace Interceptando
{
public class MiInterceptor : IInterceptor
{
#region IInterceptor Members
public void Intercept(IInvocation invocation)
{
Console.WriteLine("Antes de la llamada a {0}", invocation.Method.Name);
invocation.Proceed();
Console.WriteLine("Despus de la llamada a {0}", invocation.Method.Name);
}
#endregion
}
}

Este interceptor imprime por consola un mensaje antes de que se produzca la llamada al mtodo
interceptado y otro cuando la llamada ha concluido. El mensaje incluye el nombre del mtodo invocado.
Cmo asociamos el interceptor a los objetos de una clase? Empecemos definiendo una clase muy sencilla:
using System;
namespace Interceptando
{
public class MiClase
{
public virtual void Saludo(string nombre)
{
Console.WriteLine("Hola, {0}!", nombre);
}
public virtual void Despedida(string nombre)
{
Console.WriteLine("Adios, {0}!", nombre);
}
}
}

Ntese que hemos definido los mtodos como virtuales. En el comportamiento por defecto, el
apoderamiento se basa en herencia y slo podemos interceptar mtodos virtuales.
Y ahora definamos el programa principal:
using System;
using Castle.DynamicProxy;

200

Buenas prcticas en desarrollo de software con .NET

namespace Interceptando
{
public static class Program
{
public static void Main(string[] args)
{
var miObjeto = new MiClase();
var persona = "Pepe";
miObjeto.Saludo(persona);
miObjeto.Despedida(persona);
var proxyGen = new ProxyGenerator();
var proxy = proxyGen.CreateClassProxy<MiClase>(new MiInterceptor());
proxy.Saludo(persona);
proxy.Despedida(persona);
Console.ReadKey();
}
}
}

La variable proxyGen contiene una factora de proxies. Con CreateClassProxy solicitamos una instancia de
un Proxy para MiClase que use el interceptor MiInterceptor. Al ejecutar el programa obtenemos esta
salida:
Hola, Pepe!
Adios, Pepe!
Antes de la llamada a
Hola, Pepe!
Despus de la llamada
Antes de la llamada a
Adios, Pepe!
Despus de la llamada

Saludo
a Saludo
Despedida
a Despedida

Las dos primeras llamadas se han hecho sobre un objeto directo y no hay intercepcin de llamadas. Las
dos siguientes se hacen sobre el Proxy y antes y despus de la llamada hemos conseguido introducir salida
por pantalla.
Y si deseamos interceptar slo la llamada a Saludo y no la llamada a Despedida? Hemos de crear un objeto
que permite determinar los mtodos interceptados:
public class MiInterceptorProxyGenerationHook : IProxyGenerationHook
{
#region IProxyGenerationHook Members
public void MethodsInspected()
{
}
public void NonProxyableMemberNotification(Type type,
System.Reflection.MemberInfo memberInfo)
{
}
public bool ShouldInterceptMethod(Type type, System.Reflection.MethodInfo methodInfo)
{
return (methodInfo.Name == "Saludo");
}
#endregion

201

Buenas prcticas en desarrollo de software con .NET

Nos interesa el mtodo ShouldInterceptMethod, que recibe el tipo de la clase del mtodo que podemos
interceptar y un objeto con informacin del mtodo (en el que podemos acceder, entre otros, a datos como
su nombre). Si el mtodo devuelve true, hemos de interceptarlo9.
Ahora hemos de pasar este gancho al creador del proxy:
using System;
using Castle.DynamicProxy;
namespace Interceptando
{
public static class Program
{
public static void Main(string[] args)
{
var miObjeto = new MiClase();
var persona = "Pepe";
miObjeto.Saludo(persona);
miObjeto.Despedida(persona);
var proxyGen = new ProxyGenerator();
var options = new ProxyGenerationOptions(new MiInterceptorProxyGenerationHook());
var proxy = proxyGen.CreateClassProxy<MiClase>(options, new MiInterceptor());
proxy.Saludo(persona);
proxy.Despedida(persona);
Console.ReadKey();
}
}
}

La librera DynamicProxy puede ser til para construir stubs. Es posible crear un Proxy dinmico para una
interfaz de modo que la implementacin la proporcionen los interceptores. Pero esto supone alejarnos de
nuestro objetivo, que es presentar AOP con Castle Windsor.

14.2 AOP con Castle Windsor


Castle Windor cuenta entre sus bloques bsicos con una herramienta que simplifica la creacin de Proxies:
Castle DynamicProxy. La implementacin de AOP de Castle Windsor se apoya en el uso y extensin de esa
herramienta.
Castle Windsor permite interceptar las llamadas a mtodos de un objeto y ejecutar cdigo de usuario antes
y/o despus de la llamada. Para ello utiliza la librera DynamicProxy, pero con una cara algo ms amable.
Hay tres modos de declarar interceptores:

Con la interfaz fluida.


Con atributos.
Con un fichero XML.

De los tres mtodos slo hemos proporcionado lgica para uno de ellos. Los otros dos son auxiliares y tiles durante el
desarrollo. El mtodo MethodsInspected se invoca cuando todos los mtodos de la clase subordinada se han
inspeccionado y el mtodo NonProxyableMemberNotification se invoca sobre los mtodos que no se pueden
interceptar. Los mtodos no virtuales, por ejemplo, no se pueden interceptar.

202

Buenas prcticas en desarrollo de software con .NET

En cualquier caso, lo primero que hemos de hacer es declarar los interceptores como componentes.
Mostramos primero los ficheros implicados en la definicin e instalacin del interceptor:
MiInterceptor.cs
using System;
using Castle.DynamicProxy;
namespace InterceptandoMas
{
public class MiInterceptor : IInterceptor
{
#region IInterceptor Members
public void Intercept(IInvocation invocation)
{
Console.WriteLine("Antes de la llamada a {0}", invocation.Method.Name);
invocation.Proceed();
Console.WriteLine("Despus de la llamada a {0}", invocation.Method.Name);
}
#endregion
}
}

InterceptorsInstaller.cs
using Castle.MicroKernel.Registration;
using Castle.MicroKernel.SubSystems.Configuration;
using Castle.Windsor;
namespace InterceptandoMas
{
public class InterceptorsInstaller : IWindsorInstaller
{
#region IWindsorInstaller Members
public void Install(IWindsorContainer container, IConfigurationStore store)
{
container.Register(Component.For<MiInterceptor>());
}
#endregion
}
}

Ahora definimos nuestro componente y su instalador:


MiClase.cs
using System;
namespace InterceptandoMas
{
public class MiClase
{
public virtual void Saludo(string nombre)
{
Console.WriteLine("Hola, {0}!", nombre);
}
public virtual void Despedida(string nombre)
{

203

Buenas prcticas en desarrollo de software con .NET

Console.WriteLine("Adios, {0}!", nombre);


}
}
}

MisClasesInstaller.cs
using
using
using
using

Castle.Core;
Castle.MicroKernel.Registration;
Castle.MicroKernel.SubSystems.Configuration;
Castle.Windsor;

namespace InterceptandoMas
{
public class MisClasesInstaller : IWindsorInstaller
{
#region IWindsorInstaller Members
public void Install(IWindsorContainer container, IConfigurationStore store)
{
container.Register(Component.For<MiClase>()
.Interceptors(InterceptorReference
.ForType<MiInterceptor>())
.First);
}
#endregion
}
}

Este ltimo fichero requiere un examen ms detallado. Estamos registrando nuestro componente e
indicando que hay un interceptor que afectar a su comportamiento. Ntese que acabamos la secuencia de
llamadas con First. Con ello indicamos que este interceptor debe ejecutarse en primer lugar en el caso de
que haya varios interceptores asociados al componente. Adems de First, podemos indicar orden con
Last, AnyWhere o sealando el orden exacto con AtIndex(int).
El programa principal queda as:
using System;
using Castle.Windsor;
namespace InterceptandoMas
{
public static class Program
{
public static void Main(string[] args)
{
var container = new WindsorContainer();
container.Install(new InterceptorsInstaller(), new MisClasesInstaller());
var miObjeto = new MiClase();
var persona = "Pepe";
miObjeto.Saludo(persona);
miObjeto.Despedida(persona);
var mio = container.Resolve<MiClase>();
mio.Saludo("Toni");
mio.Despedida("Toni");
Console.ReadKey();
}
}

204

Buenas prcticas en desarrollo de software con .NET

Hemos creado un contenedor y dado de alta sus instaladores. Al crear un componente de MiClase, el
sistema nos devuelve un objeto con sus mtodos interceptados. Si ejecutamos el programa tenemos:
Hola, Pepe!
Adios, Pepe!
Antes de la llamada a
Hola, Toni!
Despus de la llamada
Antes de la llamada a
Adios, Toni!
Despus de la llamada

Saludo
a Saludo
Despedida
a Despedida

Cmo podemos controlar qu mtodos se interceptan y cules no? Una solucin chapucera sera definir
cdigo en el propio mtodo Intercept del interceptor para que examine cada vez el tipo y mtodo
interceptados y decida si debe hacer algo o, sencillamente, llamar a Proceed() sobre el IInvocation que se
le suministra. Pero esto es ineficiente, as que conviene encontrar otra solucin.
Para seleccionar los selectores que se aplican a cada tipo/mtodo podemos crear un objeto que implemente
la interfaz IInterceptorSelector. Esta interfaz slo tiene un mtodo. Su perfil es este:
public IInterceptor[] SelectInterceptors(Type type,
System.Reflection.MethodInfo method,
IInterceptor[] interceptors)

La idea es que incluyamos lgica que, dado un tipo, un mtodo y un vector de interceptores, deje pasar
nicamente los interceptores aplicables a ese tipo/mtodo. Supongamos que slo deseamos aplicar el
interceptor al mtodo Saludo de no importa qu tipo. Escribiremos cdigo como ste:
MiSelectorDeInterceptores.cs
using System;
using Castle.DynamicProxy;
namespace InterceptandoMas
{
class MiSelectorDeInterceptores : IInterceptorSelector
{
#region IInterceptorSelector Members
public IInterceptor[] SelectInterceptors(Type type,
System.Reflection.MethodInfo method,
IInterceptor[] interceptors)
{
if (method.Name == "Saludo")
{
return interceptors;
}
else
{
return new IInterceptor[0];
}
}
#endregion
}
}

205

Buenas prcticas en desarrollo de software con .NET

Al dar de alta el componente con sus interceptores podemos indicar que queremos que haya un proceso de
seleccin:
using
using
using
using

Castle.Core;
Castle.MicroKernel.Registration;
Castle.MicroKernel.SubSystems.Configuration;
Castle.Windsor;

namespace InterceptandoMas
{
public class MisClasesInstaller : IWindsorInstaller
{
#region IWindsorInstaller Members
public void Install(IWindsorContainer container, IConfigurationStore store)
{
container.Register(Component.For<MiClase>()
.Interceptors(
InterceptorReference.ForType<MiInterceptor>())
.SelectedWith(new MiSelectorDeInterceptores())
.First);
}
#endregion
}
}

Ejecutemos ahora el programa:


Hola, Pepe!
Adios, Pepe!
Antes de la llamada a Saludo
Hola, Toni!
Despus de la llamada a Saludo
Adios, Toni!

Slo el mtodo Saludo ha sido interceptado.


14.2.1 Un ejemplo til: cach de llamadas
Ciertas llamadas a mtodos pueden resultar muy costosas y nuestra aplicacin puede efectuar numerosas
llamadas con los mismos parmetros para obtener un mismo resultado. Una estrategia para agilizar las
respuestas es memorizar las que ya se han calculado previamente para convertir futuros clculos repetidos
en meras consultas a una tabla.
Empezamos por los componentes: dos clases para calcular ciertas funciones.
Calculos.cs
namespace Caching
{
public class Fibonacci
{
public virtual int Compute(int n)
{
if (n <= 1)
{
return 1;
}
else
{
return Compute(n-1) + Compute(n-2);

206

Buenas prcticas en desarrollo de software con .NET

}
}
}
public class Factorial
{
public virtual int Compute(int n)
{
if (n <= 1)
{
return 1;
}
else
{
return n * Compute(n-1);
}
}
}
}

Nuestro interceptor memorizar en un diccionario de diccionarios los resultados de cada entrada para cada
llamada a un mtodo:
CachingInterceptor.cs
using
using
using
using
using

System;
System.Collections.Generic;
System.Linq;
System.Text;
Castle.DynamicProxy;

namespace Caching
{
class CachingInterceptor : IInterceptor
{
private Dictionary<string, Dictionary<int, int>> _cache;
public CachingInterceptor()
{
_cache = new Dictionary<string, Dictionary<int, int>>();
}
#region IInterceptor Members
public void Intercept(IInvocation invocation)
{
var key = invocation.TargetType.Name + ":" + invocation.Method.Name;
if (!_cache.ContainsKey(key))
{
_cache[key] = new Dictionary<int, int>();
}
var subKey = (int) invocation.GetArgumentValue(0);
if (!_cache[key].ContainsKey(subKey))
{
invocation.Proceed();
var result = (int) invocation.ReturnValue;
_cache[key][subKey] = result;
Console.WriteLine("Clculo para {0}({1}): {2}", key, subKey, result);
}
else
{
invocation.ReturnValue = _cache[key][subKey];
}
}

207

Buenas prcticas en desarrollo de software con .NET

#endregion
}
}

(La lnea que destacamos con fondo amarillo se ha puesto a efecto de que podamos ver qu hace el
mecanismo de cache cuando ejecutemos. Debe eliminarse en la versin definitiva.)
WindsorInstallers.cs
using
using
using
using

Castle.Core;
Castle.MicroKernel.Registration;
Castle.MicroKernel.SubSystems.Configuration;
Castle.Windsor;

namespace Caching
{
class ComponentsInstallers : IWindsorInstaller
{
#region IWindsorInstaller Members
public void Install(IWindsorContainer container, IConfigurationStore store)
{
container.Register(Component.For<Fibonacci>()
.Interceptors(
InterceptorReference.ForType<CachingInterceptor>())
.Anywhere,
Component.For<Factorial>()
.Interceptors(
InterceptorReference.ForType<CachingInterceptor>())
.Anywhere);
}
#endregion
}
class InterceptorsInstallers : IWindsorInstaller
{
#region IWindsorInstaller Members
public void Install(IWindsorContainer container, IConfigurationStore store)
{
container.Register(Component.For<CachingInterceptor>());
}
#endregion
}
}

Program.cs
using System;
using Castle.Windsor;
using Castle.Windsor.Installer;
namespace Caching
{
public static class Program
{
public static void Main(string[] args)
{
var container = new WindsorContainer().Install(
new ComponentsInstallers(),

208

Buenas prcticas en desarrollo de software con .NET

new InterceptorsInstallers());
var fi = container.Resolve<Fibonacci>();
var fa = container.Resolve<Factorial>();
Console.WriteLine(fi.Compute(8));
Console.WriteLine(fi.Compute(12));
Console.WriteLine(fi.Compute(3));
Console.WriteLine(fa.Compute(12));
Console.WriteLine(fa.Compute(15));
Console.ReadKey();
}
}
}

Al ejecutar tenemos esto:


Clculo para
Clculo para
Clculo para
Clculo para
Clculo para
Clculo para
Clculo para
Clculo para
Clculo para
34
Clculo para
Clculo para
Clculo para
Clculo para
233
3
Clculo para
Clculo para
Clculo para
Clculo para
Clculo para
Clculo para
Clculo para
Clculo para
Clculo para
Clculo para
Clculo para
Clculo para
479001600
Clculo para
Clculo para
Clculo para
2004310016

Fibonacci:Compute(1):
Fibonacci:Compute(0):
Fibonacci:Compute(2):
Fibonacci:Compute(3):
Fibonacci:Compute(4):
Fibonacci:Compute(5):
Fibonacci:Compute(6):
Fibonacci:Compute(7):
Fibonacci:Compute(8):

1
1
2
3
5
8
13
21
34

Fibonacci:Compute(9): 55
Fibonacci:Compute(10): 89
Fibonacci:Compute(11): 144
Fibonacci:Compute(12): 233

Factorial:Compute(1): 1
Factorial:Compute(2): 2
Factorial:Compute(3): 6
Factorial:Compute(4): 24
Factorial:Compute(5): 120
Factorial:Compute(6): 720
Factorial:Compute(7): 5040
Factorial:Compute(8): 40320
Factorial:Compute(9): 362880
Factorial:Compute(10): 3628800
Factorial:Compute(11): 39916800
Factorial:Compute(12): 479001600
Factorial:Compute(13): 1932053504
Factorial:Compute(14): 1278945280
Factorial:Compute(15): 2004310016

Se puede comprobar cmo el clculo slo se efecta cuando no se ha efectuado una llamada con el mismo
parmetro.
Obviamente, este tipo de mecanismos siguen siendo ineficientes en aplicaciones de clculo cientfico, pero
el sobrecoste del mecanismo de cache es despreciable en aplicaciones web o de acceso a una base de datos.
14.2.2 Otro ejemplo: Logging con inters ortogonal
Uno de los intereses ortogonales ms comunes el registro de actividad. Castle Windsor puede usar como
infraestructura a log4net (o NLog, otra implementacin de sistemas de logging).

209

Buenas prcticas en desarrollo de software con .NET

Tomemos un ejemplo de http://ayende.com/Blog/archive/2008/07/31/Logging--the-AOP-way.aspx. Vamos a


crear un interceptor que haga logging con Castle Windsor. La clase que deseamos interceptar implementa un
servicio de envo de SMS:
public interface ISmsSender
{
int Send(string to, string msg);
}
public class SmsSender : ISmsSender
{
public int Send(string to, string msg)
{
if(msg.Length>160)
throw new ArgumentException("too long","msg");
return to.Length;
}
}

El interceptor se define as:


public class LoggingInterceptor : IInterceptor
{
public void Intercept(IInvocation invocation)
{
var logger = LogManager.GetLogger(invocation.TargetType);
try
{
StringBuilder sb = null;
if (logger.IsDebugEnabled)
{
sb = new StringBuilder(invocation.TargetType.FullName)
.Append(".")
.Append(invocation.Method)
.Append("(");
for (int i = 0; i < invocation.Arguments.Length; i++)
{
if (i > 0)
sb.Append(", ");
sb.Append(invocation.Arguments[i]);
}
sb.Append(")");
logger.Debug(sb);
}
invocation.Proceed();
if(logger.IsDebugEnabled)
{
logger.Debug("Result of " + sb + " is: " + invocation.ReturnValue);
}
}
catch (Exception e)
{
logger.Error(e);
throw;
}
}
}

El contenedor Castle Windsor se define as:


var container = new WindsorContainer().Register(

210

Buenas prcticas en desarrollo de software con .NET

Component.For<LoggingInterceptor>(),
Component.For<ISmsSender>().ImplementedBy<SmsSender>()
.Interceptors(new InterceptorReference(typeof(LoggingInterceptor))).First
);
BasicConfigurator.Configure(); // configure log4net
var sender = container.Resolve<ISmsSender>();
try
{
sender.Send("ayende", "short");
sender.Send("rahien", new string('q', 161));
}
catch (Exception)
{
}

15 MSBuild
Cuando usamos de vista un IDE como Visual Studio perdemos de vista el proceso que debe seguirse para
pasar de nuestro cdigo fuente al ensamblado con la librera o aplicacin que hemos creado. En entornos
orientados a la lnea de rdenes este proceso se explicita bien porque el programador compila manualmente
cada unidad de compilacin y ensambla el conjunto de binarios en una librera o ejecutable, bien porque se
usa una herramienta que permite especificar las dependencias entre unidades de compilacin y libreras y
los pasos que deben seguirse para generar nuestro producto final. En el mundo C, la herramienta usada
normalmente es make. En .NET, la herramienta es MSBuild. De hecho, Visual Studio usa internamente esta
herramienta.

15.1 Un fichero de definicin de proyecto


Un proyecto C# con Visual Studio tiene una estructura en el sistema de ficheros que vale la pena examinar
con detenimiento. Creemos un proyecto trivial: el clsico programa que muestra por consola el saludo
Hola, mundo!. Creamos el proyecto HolaMundo de tipo Console Application, que se representa as en el
Solution Explorer:

Si desplegamos el rbol del proyecto encontraremos esto:

211

Buenas prcticas en desarrollo de software con .NET

Veamos qu hay en el sistema de ficheros:

Nos interesa el fichero HolaMundo.csproj. Es el fichero que describe el proyecto. Si hacemos doble clic en
ese fichero, Visual Studio abrir el proyecto completo. El contenido del fichero es ste:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">x86</Platform>
<ProductVersion>8.0.30703</ProductVersion>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>{DBFD7912-161A-415F-9427-4A6E2CA43DDD}</ProjectGuid>
<OutputType>Exe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>HolaMundo</RootNamespace>
<AssemblyName>HolaMundo</AssemblyName>
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
<TargetFrameworkProfile>Client</TargetFrameworkProfile>
<FileAlignment>512</FileAlignment>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x86' ">
<PlatformTarget>x86</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' ">
<PlatformTarget>x86</PlatformTarget>

212

Buenas prcticas en desarrollo de software con .NET

<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and
uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

Es un fichero XML. Analicemos su estructura.

La raz es un elemento {http://schemas.microsoft.com/developer/msbuild/2003}Project.


Todos los elementos en su interior pertenecen al mismo espacio de nombres.
Hay cuatro zonas en el fichero:
o Una zona con elementos PropertyGroup.
o Una zona con elementos ItemGroup.
o Una zona con un elemento Import.
o Una zona comentada para aadir tareas propias.

El primer PropertyGroup es ste:


<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">x86</Platform>
<ProductVersion>8.0.30703</ProductVersion>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>{DBFD7912-161A-415F-9427-4A6E2CA43DDD}</ProjectGuid>
<OutputType>Exe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>HolaMundo</RootNamespace>
<AssemblyName>HolaMundo</AssemblyName>
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
<TargetFrameworkProfile>Client</TargetFrameworkProfile>
<FileAlignment>512</FileAlignment>
</PropertyGroup>

213

Buenas prcticas en desarrollo de software con .NET

Est definiendo una configuracin con la etiqueta Debug y fija la plataforma a x86. Define la versin del
producto y la versin del esquema, as como un identificador nico para el proyecto. Fija el producto final
(OutputType) como un ejecutable (Exe). Declara la carpeta Properties. Fija el espacio de nombres raz como
HolaMundo y el nombre del ensamblado como HolaMundo. Determina la versin del Framework .NET a la 4.0
y el perfil del Framework objetivo a Client. Finalmente marca un alineamiento de fichero a 512.
Parte de esta informacin se puede fijar desde dentro de Visual Studio. Si seleccionamos Properties del
proyecto en Visual Studio llegamos a esta pantalla:

Est claro que algunos valores del fichero XML se pueden fijar ah.
Vamos a por otro elemento XML del fichero:
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x86' ">
<PlatformTarget>x86</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>

Este grupo de propiedades define aspectos de la compilacin en modo Debug para x86. Se indica que debe
incluirse informacin completa para depuracin (DebugType vale full), que no se optimice el cdigo
(Optimize a false), que se deje el resultado de la compilacin en un directorio determinado (OutputPath

214

Buenas prcticas en desarrollo de software con .NET

vale bin\debug), que las constantes DEBUG y TRACE estn definidas al compilar, que los errores aparezcan
en pantalla y que se generen avisos de compilacin de nivel 4. Hay una pantalla para definir algunos de estos
valores (y algunos otros):

El siguiente grupo define parmetros para el modo Relase de x86. Se pueden entender haciendo la
comparacin con el anterior elemento, as que no nos extendemos.
Aparece luego este elemento XML:
<ItemGroup>
<Reference
<Reference
<Reference
<Reference
<Reference
<Reference
<Reference
</ItemGroup>

Include="System" />
Include="System.Core" />
Include="System.Xml.Linq" />
Include="System.Data.DataSetExtensions" />
Include="Microsoft.CSharp" />
Include="System.Data" />
Include="System.Xml" />

Est claro que se estn definiendo las referencias del proyecto.

215

Buenas prcticas en desarrollo de software con .NET

El siguiente elemento XML es:


<ItemGroup>
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>

Se indica qu ficheros fuente forman parte de este programa.


El elemento Import tiene, de momento, un inters relativo:
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />

Indica a MSBuild dnde encontrar definiciones de las tareas que sabe hacer (como compilar con el
compilador de C# 4.0, que es una tarea predefinida).
Finalmente, la zona comentada es un punto para poder aadir tareas propias que se ejecutarn antes y
despus de la construccin del objetivo:
<!-- To modify your build process, add your task inside one of the targets below and
uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->

El fichero que acabamos de estudiar es un fichero de proyecto que puede consumir la herramienta MSBuild.
Esta herramienta est integrada en Visual Studio, pero tambin se puede invocar desde la consola.
En general, diremos que este tipo de ficheros son ficheros MSBuild porque estn diseados para su uso con
esta herramienta.

15.2 Estructura de los ficheros de proyecto


El proyecto ms sencillo que se puede definir es este:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
</Project>

Es un proyecto vaco.
Podemos definir propiedades en un proyecto. Las propiedades van encerradas en un elemento
PropertyGroup y no son ms que colecciones de pares clave-valor. Las claves son los nombres de marca de
elementos XML y los valores son el contenido del elemento. He aqu un ejemplo:
<PropertyGroup>
<RootNamespace>HolaMundo</RootNamespace>
<AssemblyName>HolaMundo</AssemblyName>
<DefineConstants>DEBUG;TRACE</DefineConstants>
</PropertyGroup>

Se definen los pares

("RootNameSpace", "HolaMundo")

216

Buenas prcticas en desarrollo de software con .NET

("AssembyName", "HolaMundo")
("DefineConstants", "DEBUG;TRACE")

Como puede convenir, por legibilidad, agrupar las propiedades por familias lgicas desde el punto de vista
del programador, es posible usar varios PropertyGroup:
<PropertyGroup>
<RootNamespace>HolaMundo</RootNamespace>
<AssemblyName>HolaMundo</AssemblyName>
<PropertyGroup>
</PropertyGroup>
<DefineConstants>DEBUG;TRACE</DefineConstants>
</PropertyGroup>

Los grupos de propiedades pueden cargarse condicionalmente. Para ello podemos expresar una condicin
en un atributo:
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' ">
<PlatformTarget>x86</PlatformTarget>
<DebugSymbols>true</DebugSymbols>

</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x86' ">
<PlatformTarget>x86</PlatformTarget>
<DebugType>pdbonly</DebugType>

</PropertyGroup>

Con las propiedades fijamos parmetros. Otro componente de estos ficheros son las acciones ejecutables.
Las hay de dos tipos:

Tareas (tasks): son la unidad mnima de trabajo en un fichero MSBuild. Cada tarea se especifica con
un elemento XML propio.
Objetivos (targets): agrupaciones de tareas que permiten alcanzar un objetivo. Los objetivos se
expresan con un elemento XML Target y han de llevar un nombre que se declara en un atributo
Name.

Las tareas siempre forman parte de un objetivo.


Este ejemplo declara un objetivo con una tarea consistente en mostrar un mensaje por pantalla:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="Saluda">
<Message Text="Un saludo." />
</Target>
</Project>

La tarea Message muestra el valor del atributo Text por la salida estndar.

15.3 Invocacin directa de msbuild


Si ese texto se encuentra en un fichero llamado proyecto.proj (o .csproj), podemos ejecutar las tareas del
objetivo Saluda desde la lnea de rdenes. Hemos de iniciar antes un intrprete de lneas de rdenes
configurado apropiadamente. Encontraremos uno en el men Inicio: Start Todos los programas

217

Buenas prcticas en desarrollo de software con .NET

Microsoft Visual Studio 2010 Visual Studio Tools Visual Studio Command Prompt (2010). En la consola
que aparece cambiamos el directorio activo al que contiene el proyecto y escribimos esta orden:
msbuild proyecto.proj /t:Saluda

La opcin /t permite especificar el objetivo que deseamos alcanzar. Al ejecutar la orden obtenemos esta
salida:
Microsoft (R) Build Engine Version 4.0.30319.1
[Microsoft .NET Framework, Version 4.0.30319.225]
Copyright (C) Microsoft Corporation 2007. All rights reserved.
Build started 27/04/2011 11:20:39.
Project "C:\Users\amarzal\Desktop\proyecto.xml" on node 1 (Saluda target(s)).
Saluda:
Un saludo.
Done Building Project "C:\Users\amarzal\Desktop\proyecto.xml" (Saluda target(s)
).

Build succeeded.
0 Warning(s)
0 Error(s)
Time Elapsed 00:00:00.07

15.4 Propiedades
Como hemos dicho, las propiedades permiten crear pares clave-valor. Podemos acceder al valor con la
notacin $(clave) en las cadenas:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Nombre>Pepe</Nombre>
</PropertyGroup>
<Target Name="Saluda">
<Message Text="Un saludo, $(Nombre)." />
</Target>
</Project>

Recordemos que las propiedades siempre aparecen dentro de un elemento PropertyGroup.


Hay algunas propiedades que estn predefinidas y sus identificadores estn reservados:

MSBuildProjectDirectory: ruta al directorio en el que se encuentra el proyecto.


MSBuildProjectDirectoryNoRoot: dem, pero excluyendo la raz.
MSBuildProjectFile: nombre del fichero de proyecto (con la extensin).
MSBuildProjectExtension: extensin del fichero de proyecto.
MSBuildProjectFullPath: ruta del fichero de proyecto.
MSBuildProjectName: nombre del fichero de proyecto (sin la extensin).
MSBuildToolsPath: ruta al directorio que contiene las herramientas.
MSBuildProjectDefaultTargets: lista de objetivos por defecto.
MSBuildExtensionsPath: ruta al directorio que contiene extensiones.
MSBuildExtensionsPath: dem para extensiones de 32 bits.
MSBuildNodeCount: nmero de nodos (procesos) que se usan para construir el proyecto.
MSBuildStartupDirectory: ruta a la carpeta desde la que se invoca msbuild.

218

Buenas prcticas en desarrollo de software con .NET

MSBuildToolsPath: ruta a las herramientas msbuild usadas para construir el proyecto.


MSBuildToolsVersion: versin de las herramientas.

Las variables de entorno son accesibles como propiedades predefinidas:


<Target Name="VariableEntorno">
<Message Text="Windir: $(windir)." />
</Target>

15.5 tems
Los tems son, por lo general, referencias a ficheros que se ven implicados de algn modo en el proceso de
construccin. Los tems aparecen dentro de un elemento ItemGroup.
Hay varios tems con identificadores predefinidos. Uno es SolutionFile, que especifica la solucin de la que
forma parte un proyecto. En este ejemplo se define un SolutionFile y luego se referencia:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<SolutionFile Include="..\MSBuildExamples.sln" />
</ItemGroup>
<Target Name="PrintSolutionInfo">
<Message Text="SolutionFile: @(SolutionFile)" />
</Target>
</Project>

Ntese que se usa @(identificador) para resolver la referencia al fichero.


Otro tem muy usado es Compile, que especifica ficheros fuente:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<SolutionFile Include="..\MSBuildExamples.sln" />
</ItemGroup>
<Target Name="PrintSolutionInfo">
<Message Text="SolutionFile: @(SolutionFile)" />
</Target>
<ItemGroup>
<Compile Include="Form1.cs;Form1.Designer.cs;Program.cs;Properties\AssemblyInfo.cs" />
</ItemGroup>
<Target Name="PrintCompileInfo">
<Message Text="Compile: @(Compile)" />
</Target>
</Project>

En el ejemplo se han puesto todos los fuentes en un solo Compile. Se puede usar ms de un Compile en un
proyecto:
<ItemGroup>
<Compile Include="Form1.cs" />
<Compile Include="Form1.Designer.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>

Otra marca de tem de uso comn es Content, que suele especificar recursos que forman parte del proyecto,
como pginas HTML en una aplicacin web.
<ItemGroup>

219

Buenas prcticas en desarrollo de software con .NET

<Content Include="script.js"/>
</ItemGroup>

Como los tems se refieren usualmente a ficheros ya existentes, podemos usar expresiones glob10 para
expresar conjuntos de ficheros. La expresin *.cs representa a todos los ficheros con extensin cs.
Hay un elemento especial en este tipo de expresiones: **. Con el doble asterisco se indica a MSBuild que
explore recursivamente en busca del patrn. Con src\**\*.cs se buscan todos los ficheros con extensin cs
en el directorio src.
Podemos definir nuestros propios tems:
<ItemGroup>
<src Include="src\fichero.txt" />
</ItemGroup>

15.5.1 Metadatos
Ya que los tems representan ficheros, puede convenir acceder a sus metadatos. La sintaxis que se usa para
acceder a ellos es un tanto especial. Con esta expresin:
$(identificador->%(CreatedTime))

accedemos a la fecha de creacin del tem Fichero. Podemos acceder a estos metadatos:

Identity: valor que se le dio al especificar el tem.


FullPath: ruta completa al fichero.
Rootdir: directorio raz al que pertenece el fichero.
Filename: nombre del fichero sin incluir la extensin.
Extension: extension.
RelativeDir: directorio relativo al fichero actual.
Directory: directorio del tem sin el directorio raz.
RecursiveDir: parte de la ruta que se reemplaza por el **.
ModifiedTime: ltimo instante en el que modific el fichero.
CreatedTime: instante de creacin
AccessedTime: ltimo instante de acceso.

15.5.2 Metadatos de usuario


Se pueden usar metadatos de usuario, pues no son ms que pares clave-valor. Un elemento puede contener
otros elementos y estos se interpretan como metadatos:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Server Include="Server1">
<Type>2008</Type>
<Name>SVR01</Name>
<AdminContact>Sayed Ibrahim Hashimi</AdminContact>
</Server>
<Server Include="Server2">
<Type>2003</Type>
10

Los patrones glob se usan para describir grupos de ficheros con un lenguaje reminiscente de las expresiones
regulares. Por ejemplo, la expresin a*.txt se resuelve como la lista de ficheros que empiezan por a y tienen extensin
txt; y hola.* es la lista de ficheros que tiene por nombre hola y no importa qu extensin.

220

Buenas prcticas en desarrollo de software con .NET

<Name>SVR02</Name>
<AdminContact>Sayed Y. Hashimi</AdminContact>
</Server>
<Server Include="Server3">
<Type>2008</Type>
<Name>SVR03</Name>
<AdminContact>Nicole Woodsmall</AdminContact>
</Server>
<Server Include="Server4">
<Type>2003</Type>
<Name>SVR04</Name>
<AdminContact>Keith Tingle</AdminContact>
</Server>
</ItemGroup>
<Target Name="PrintInfo" Outputs="%(Server.Identity)">
<Message Text="Server: @(Server)" />
<Message Text="Admin: @(Server->'%(AdminContact)')" />
</Target>
</Project>

15.6 Condiciones
Es posible cargar elementos del proyecto condicionalmente. Las condiciones se indican con el atributo
Condition. El valor del atributo es una expresin que permite especificar comparaciones y preguntas pro la
existencia de un fichero. Los operadores/funciones que podemos usar son:

==: igualdad.
!=: desigualdad.
Exists: existencia.
!Exists: no existencia.

Un par de ejemplos:
<ItemGroup>
<Content Include="script.js"/>
<Content Include="script.debug.js" Condition="$(Configuration)=='Debug'" />
</ItemGroup>

15.7 Objetivos por defecto


Al crear un proyecto se pueden definir objetivos por defecto, que se tratan de alcanzar cuando se ejecuta
msbuild sin especificar un objetivo explcito:
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
...
</Project>

15.8 La orden msbuild


Se puede invocar msbuild con diferentes opciones:

/help (/?): ayuda.


/nologo: no producir informacin de presentacin.
/version (/ver): mostrar la versin.
@file: para escoger ficheros de respuesta la parmetros.
/noautoresponse (/noautoresp): suprime la inclusin de msbuild.rsp como fichero de respuesta.
/target (/t): fija un objetivo.

221

Buenas prcticas en desarrollo de software con .NET

/property:<n>=<v> (/p): especifica una propiedad o redefine el valor de un a propiedad ya definica

en el fichero de proyecto.
/verbosity (/v): nivel de locuacidad. Los valores son quiet (q), minimal (m), normal (n), detailed (d),
diagnostic (diag).
/validate (/val): se asegura de que el proyecto es correcto antes de ejecutar.
/logger (/l): vincula un logger a la construccin.
/consoleloggerparameters (/clp): pasa parmetros al logger de la consola.
/noconsolelogger (/noconlog): suprime el uso del logger de la consola.
/filelogger (/fl): vincula un logger de fichero.
/fileloggerparameters (/flp): pasa parmetros al logger.
/distributedFileLogger (/dl): vincula un logger distribuido.
/maxcpucount (/m): fija el mximo nmero de procesos a usar en la construccin.
/ignoreprojectextensions (/ignore): ignora las extensin que se suministran.
/toolsversion (/tv): especifica la versin de las herramientas .NET que deben usarse en la
construccin.
/nodeReuse (/nr): especifica si los nodos deben reutilizarse o no.

15.9 Propiedades dinmicas


Las propiedades que hemos considerado hasta el momento son estticas, es decir, se definen antes del
tiempo de ejecucin. MSBuild puede construir propiedades durante la ejecucin y aprovechar as el
resultado de ejecutar ciertas tareas.
La propiedad creada se indica en un elemento CreateProperty. En este ejemplo se crea una propiedad
llamada NewFile cuyo valor, Module1.vb, se forma a partir de dos propiedades estticas:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<SourceFilename>Module1</SourceFilename>
<SourceFileExtension>vb</SourceFileExtension>
</PropertyGroup>
<Target Name="CreateProperties">
<CreateProperty Value="$(SourceFilename).$(SourceFileExtension)">
<Output TaskParameter="Value" PropertyName="NewFile" />
</CreateProperty>
</Target>
</Project>

El elemento Output indica que el parmetro Value de la tarea CreateProperty se almacene en la


propiedad llamada NewFile.

15.10 Tareas
Hay un repertorio de tareas predefinidas que son, en principio, suficientes para la mayor parte de proyectos.
Las tareas son objetos .NET que implementan la interfaz Microsoft.Build.Framework.ITask . El usuario
puede definir sus propias tareas y extender la funcionalidad de MSBuild implementando esta interfaz en
clases propias.

222

Buenas prcticas en desarrollo de software con .NET

Algunas de las tareas predefinidas son:

AL (Assembly Linker): crea un ensamblado con manifiesto a partir de uno o ms ficheros que son

mdulos o recursos.
AssignCulture: asigna identificador de cultura a los tems.
AssignProjectConfiguration: asigna una lista de cadenas de configuracin y las asigna a los
proyectos especificados.
AssignTargetPath: aade el atributo TargetPah, si no est ya especificado, a una lista de ficheros.
CallTarget: invoca un objetivo en un fichero de proyecto.
CombinePath: combina varias rutas en una sola.
ConvertToAbsolutePath: convierte una ruta relative en una absoluta.
Copy: copia ficheros a un nuevo destino.
CreateItem: llena una coleccin de items a partir de tems de entrada, permitiendo la copia de
tems de una lista a otra.
CreateProperty: crea propiedades con valores de entrada, permitiendo que los valores de un
propiedad o cadena se copien a otra.
Csc: invoca el compilador de C# para producir ejecutables, libreras de enlace dinmico o mdulos
de cdigo.
Delete: borra ficheros.
Error: detiene la construccin y muestra un error en funcin del resultado de una sentencia
condicional.
Exec: ejecuta un programa u orden con los argumentos que se indiquen.
FindAppConfigFile: encuentra el fichero app.config en las listas que se suministran.
FindInList: encuentra un item en una lista de ficheros que concuerda con la especificacin que se
proporcione.
FindUnderPath: determina qu items de la coleccin de tems que se especifique existen en la
carpeta indicada y cualquier subcarpeta suya.
FormatUrl: convierte una URL al format de URL correcto.
FormatVersion: aade el nmero de revisin al de versin.
GenerateResource: convierte ficheros .txt o .resx a ficheros binarios CLR .resources.
GetFrameworkPath: obtiene la ruta a los ensamblados estndar .NET.
MakeDir: crea un directorio y, si es necesario, sus directorios padre.
Message: muestra un mensaje.
Move: mueve ficheros un lugar.
MSBuild: construye proyectos MSBuild a partir de otro proyecto MSBuild.
ReadLinesFromFile: lee una lista de items de un fichero de texto.
RemoveDir: elimina directories y todos sus ficheros y subdirectorios.
RemoveDuplicates: elimina duplicados de una coleccin de tems.
ResolveAssemblyReference: determina los ensamblados que dependen de los que se especifican.
Touch: fija el valor del instante de acceso y modificacin de unos ficheros.
Warning: genera un aviso en funcin de una condicin.
WriteCodeFragment: genera un fichero de cdigo temporal a partir de un fragmento de cdifo.
WriteLinesToFile: escribe los items que se indique en un fichero de texto especificado.
XslTransformation: transforma entrada XML usando XSLT.

223

Buenas prcticas en desarrollo de software con .NET

Podramos entrar ahora describir todas (o muchas de) esas tareas, pero sera un ejercicio farragoso cuando
siempre se puede consultar la referencia de cada una de ellas. Tan slo comentamos una con cierto detalle
para entender el manejo relativamente sofisticado de una de ellas: la tarea Copy.
15.10.1 Copy
Esta tarea permite copiar ficheros. Tiene una serie de parmetros:

SourceFiles: ficheros que deben ser copiados.


DestinationFolder: carpeta de destino.
DestinationFiles: ficheros de sentido (en correspondencia uno a uno con los ficheros fuente). Se

debe escoger este parmetro o el anterior; no los dos.


CopiedFiles: parmetro de salida con los ficheros que se han copiado exitosamente.
SkipUnchangedFiles: si vale true, slo se copian los ficheros que han cambiado (atendiendo a la
fecha).
OverwriteReadOnlyFiles: si vale true, los ficheros de slo lectura se machacan.

Un ejemplo de uso:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<SrcFiles Include="src\*" />
</ItemGroup>
<PropertyGroup>
<Dest>dest\</Dest>
</PropertyGroup>
<Target Name="PrintFiles">
<Message Text="SrcFiles: @(SrcFiles)" />
</Target>
<Target Name="CopyFiles">
<Copy SourceFiles="@(SrcFiles)" DestinationFolder="$(Dest)" />
</Target>
</Project>

15.11 MSBuild Extension Pack


Muchas tareas de uso comn no forman parte del paquete estndar de tareas de MSBuild. Hay un proyecto
de cdigo abierto en http://msbuildextensionpack.codeplex.com/ que define muchas tareas adicionales
(unas 400), algunas muy interesantes en el marco de lo estudiado hasta el momento.
15.11.1 Instalacin
Hemos de descargar el paquete enlazado en MSBuild Extension Pack April 2011 (All Files) de
http://msbuildextensionpack.codeplex.com/releases/view/57599. Es un fichero zip que contiene el
instalador, los ficheros binarios y la ayuda para .NET 3.5 y .NET 4.0.
La instalacin es sencilla: basta con ejecutar el instalador MSBuild Extension Pack 4.0.msi11 aceptar la licencia
y seguir los pasos del asistente.
15.11.2 Documentacin
La instalacin incluye un fichero de ayuda en formato chm12. El manual documenta todas las tareas,
agrupadas por familias:
11

He experimentado problemas con la versin de 64 bits, por lo que las demostraciones usan la de 32 bits.

224

Buenas prcticas en desarrollo de software con .NET

Presentamos ahora algunas de las ms interesantes.


15.11.3 NUnit
Esta tarea permite ejecutar pruebas unitarias para NUnit. Este fichero permite ejecutar automticamente las
pruebas sobre la DLL del ejemplo que desarrollamos antes:
<?xml version="1.0" encoding="UTF-8" ?>
<Project ToolsVersion="4.0" DefaultTargets="Default"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<TPath>$(ProgramFiles)\MSBuild\ExtensionPack\4.0\MSBuild.ExtensionPack.tasks</TPath>
</PropertyGroup>
<Import Project="$(TPath)"/>
<PropertyGroup>
<ToolPath>$(ProgramFIles)\NUnit 2.5.9\bin\net-2.0</ToolPath>
</PropertyGroup>
<Target Name="Default">
<ItemGroup>
<Assemblies Include="C:\Users\amarzal\Documents\My Dropbox\Curso Buenas
Prcticas\BuenasPracticas\TestTranscriptorDeNumeros\bin\Debug\*.dll"/>
</ItemGroup>
<MSBuild.ExtensionPack.CodeQuality.NUnit Assemblies="@(Assemblies)"
ToolPath="$(ToolPath)"
OutputXmlFile="NunitResults.xml">
<Output TaskParameter="Total" PropertyName="ResultTotal"/>
<Output TaskParameter="NotRun" PropertyName="ResultNotRun"/>
<Output TaskParameter="Failures" PropertyName="ResultFailures"/>
<Output TaskParameter="Errors" PropertyName="ResultErrors"/>
<Output TaskParameter="Inconclusive" PropertyName="ResultInconclusive"/>
<Output TaskParameter="Ignored" PropertyName="ResultIgnored"/>
<Output TaskParameter="Skipped" PropertyName="ResultSkipped"/>
<Output TaskParameter="Invalid" PropertyName="ResultInvalid"/>
</MSBuild.ExtensionPack.CodeQuality.NUnit>
12

Si la lectura da un fichero CHM descargado de Internet problemas, hay que acceder a Propiedades del fichero (con el
men contextual que parece al pulsar el botn derecho del ratn sobre el icono del fichero) y pulsar en el botn
Desbloquear que hay abajo a la derecha. El bloque con el que viene es una medida de seguridad que sea aplica a los
contenidos descargados de la red.

225

Buenas prcticas en desarrollo de software con .NET

<Message
<Message
<Message
<Message
<Message
<Message
<Message
<Message
</Target>
</Project>

Text="ResultTotal: $(ResultTotal)"/>
Text="ResultNotRun: $(ResultNotRun)"/>
Text="ResultFailures: $(ResultFailures)"/>
Text="ResultErrors: $(ResultErrors)"/>
Text="ResultInconclusive: $(ResultInconclusive)"/>
Text="ResultIgnored: $(ResultIgnored)"/>
Text="ResultSkipped: $(ResultSkipped)"/>
Text="ResultInvalid: $(ResultInvalid)"/>

Si ejecutamos MSBuild sobre ese proyecto, obtenemos esta salida por consola:
Microsoft (R) Build Engine Version 4.0.30319.1
[Microsoft .NET Framework, Version 4.0.30319.225]
Copyright (C) Microsoft Corporation 2007. All rights reserved.
Build started 27/04/2011 17:48:41.
Project "C:\Users\amarzal\Desktop\proyecto.xml" on node 1 (default targets).
Default:
C:\Program Files (x86)\NUnit 2.5.9\bin\net-2.0\nunit-console.exe /nologo "C:\
Users\amarzal\Documents\My Dropbox\Curso Buenas Prcticas\BuenasPracticas\Tes
tTranscriptorDeNumeros\bin\Debug\nunit.framework.dll" "C:\Users\amarzal\Docum
ents\My Dropbox\Curso Buenas Prcticas\BuenasPracticas\TestTranscriptorDeNume
ros\bin\Debug\TestTranscriptorDeNumeros.dll" "C:\Users\amarzal\Documents\My D
ropbox\Curso Buenas Prcticas\BuenasPracticas\TestTranscriptorDeNumeros\bin\D
ebug\TranscriptorDeNumeros.dll" /xml=NunitResults.xml
ProcessModel: Default
DomainUsage: Multiple
Execution Runtime: Default
...............................................
Tests run: 47, Errors: 0, Failures: 0, Inconclusive: 0, Time: 0,062 seconds
Not run: 0, Invalid: 0, Ignored: 0, Skipped: 0
ResultTotal: 47
ResultNotRun: 0
ResultFailures: 0
ResultErrors: 0
ResultInconclusive: 0
ResultIgnored: 0
ResultSkipped: 0
ResultInvalid: 0
Done Building Project "C:\Users\amarzal\Desktop\proyecto.xml" (default targets)
.

Build succeeded.
0 Warning(s)
0 Error(s)
Time Elapsed 00:00:05.38

15.11.4 Email
Esta es una tarea que enva correo electrnico usando algn servidor SMTP.
<Project ToolsVersion="4.0" DefaultTargets="Default"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<TPath>$(MSBuildProjectDirectory)\..\MSBuild.ExtensionPack.tasks</TPath>
<TPath
Condition="Exists('$(MSBuildProjectDirectory)\..\..\Common\MSBuild.ExtensionPack.tasks')">$(MSBuil
dProjectDirectory)\..\..\Common\MSBuild.ExtensionPack.tasks</TPath>
</PropertyGroup>
<Import Project="$(TPath)"/>
<Target Name="Default">

226

Buenas prcticas en desarrollo de software con .NET

<ItemGroup>
<!-- Specify some attachments -->
<Attachment Include="C:\demo.txt"/>
<Attachment Include="C:\demo2.txt"/>
<!-- Specify some recipients -->
<Recipient Include="nospam@freet2odev.com"/>
<Recipient Include="nospam2@freet2odev.com"/>
</ItemGroup>
<MSBuild.ExtensionPack.Communication.Email
TaskAction="Send"
Subject="Test Email"
SmtpServer="yoursmtpserver"
MailFrom="nospam@freet2odev.com"
MailTo="@(Recipient)"
Body="body text"
Attachments="@(Attachment)"/>
</Target>
</Project>

15.11.5 Twitter
Y esta tarea enva un tweet a Twitter!
<Project ToolsVersion="4.0" DefaultTargets="Default"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<TPath>$(MSBuildProjectDirectory)\..\MSBuild.ExtensionPack.tasks</TPath>
<TPath
Condition="Exists('$(MSBuildProjectDirectory)\..\..\Common\MSBuild.ExtensionPack.tasks')">$(MSBuil
dProjectDirectory)\..\..\Common\MSBuild.ExtensionPack.tasks</TPath>
</PropertyGroup>
<Import Project="$(TPath)"/>
<Target Name="Default">
<!-- Send a Twitter message-->
<MSBuild.ExtensionPack.Communication.Twitter TaskAction="Tweet" Message="Hello Sir, this
is your build server letting you know that all is ok." UserName="yourtwitterusername"
UserPassword="yourtwitterpassword"/>
</Target>
</Project>

15.11.6 Zip
Con esta tarea se puede comprimir un conjunto de ficheros. Puede ser de ayuda para generar un artefacto
de salida, como un instalador y algunos recursos, en un paquete comprimido.
<Project ToolsVersion="4.0" DefaultTargets="Default"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<TPath>$(MSBuildProjectDirectory)\..\MSBuild.ExtensionPack.tasks</TPath>
<TPath
Condition="Exists('$(MSBuildProjectDirectory)\..\..\Common\MSBuild.ExtensionPack.tasks')">$(MSBuil
dProjectDirectory)\..\..\Common\MSBuild.ExtensionPack.tasks</TPath>
</PropertyGroup>
<Import Project="$(TPath)"/>
<Target Name="Default" DependsOnTargets="Sample1;Sample2"/>
<Target Name="Sample1">
<ItemGroup>
<!-- Set the collection of files to Zip-->
<FilesToZip Include="C:\hotfixes\**\*"/>
</ItemGroup>
<!-- Create a zip file based on the FilesToZip collection -->
<MSBuild.ExtensionPack.Compression.Zip TaskAction="Create" CompressFiles="@(FilesToZip)"
RemoveRoot="C:\hotfixes\" ZipFileName="C:\newZipByFile.zip"/>
<!-- Create a zip file based on a Path -->

227

Buenas prcticas en desarrollo de software con .NET

<MSBuild.ExtensionPack.Compression.Zip TaskAction="Create" CompressPath="C:\hotfixes"


RemoveRoot="C:\hotfixes\" ZipFileName="C:\newZipByPath.zip"/>
<!-- Extract a zip file-->
<MSBuild.ExtensionPack.Compression.Zip TaskAction="Extract" ExtractPath="C:\aaa11"
ZipFileName="C:\newZipByPath.zip"/>
</Target>
<Target Name="Sample2">
<PropertyGroup>
<SourceDirectory>MotorData\</SourceDirectory>
</PropertyGroup>
<ItemGroup>
<Files Include="$(SourceDirectory)*" Exclude="$(SourceDirectory).XYZ\**\*">
<Group>Common</Group>
</Files>
<Files Include="$(SourceDirectory)Cars\*" Exclude="$(SourceDirectory)Cars\.XYZ\**\*">
<Group>Cars</Group>
</Files>
<Files Include="$(SourceDirectory)Trucks\*"
Exclude="$(SourceDirectory)Trucks\.XYZ\**\*">
<Group>Trucks</Group>
</Files>
</ItemGroup>
<!-- Create the output folder -->
<ItemGroup>
<OutputDirectory Include="output\"/>
</ItemGroup>
<MakeDir Directories="@(OutputDirectory)"/>
<PropertyGroup>
<WorkingDir>%(OutputDirectory.Fullpath)</WorkingDir>
</PropertyGroup>
<!-- Zip files based on the group they belong to -->
<MSBuild.ExtensionPack.Compression.Zip TaskAction="Create" CompressFiles="@(Files)"
ZipFileName="$(WorkingDir)%(Files.Group).zip"/>
</Target>
</Project>

15.11.7 Sound
Esta tarea permite reproducir sonidos del sistema. Puede ayudarnos en procesos de compilacin largos para
obtener un aviso sonoro de complecin del proceso.
<Project ToolsVersion="4.0" DefaultTargets="Default"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<TPath>$(MSBuildProjectDirectory)\..\MSBuild.ExtensionPack.tasks</TPath>
<TPath
Condition="Exists('$(MSBuildProjectDirectory)\..\..\Common\MSBuild.ExtensionPack.tasks')">$(MSBuil
dProjectDirectory)\..\..\Common\MSBuild.ExtensionPack.tasks</TPath>
</PropertyGroup>
<Import Project="$(TPath)"/>
<Target Name="Default">
<!-- Play a bunch of sounds with various tones, repeats and durations-->
<MSBuild.ExtensionPack.Multimedia.Sound TaskAction="Play"
SoundFile="C:\Windows\Media\notify.wav" Repeat="10"/>
<MSBuild.ExtensionPack.Framework.Thread TaskAction="Sleep" Timeout="500"/>
<MSBuild.ExtensionPack.Multimedia.Sound TaskAction="Play" SystemSound="Asterisk"/>
<MSBuild.ExtensionPack.Framework.Thread TaskAction="Sleep" Timeout="500"/>
<MSBuild.ExtensionPack.Multimedia.Sound TaskAction="Play" SystemSound="Beep"/>
<MSBuild.ExtensionPack.Framework.Thread TaskAction="Sleep" Timeout="500"/>
<MSBuild.ExtensionPack.Multimedia.Sound TaskAction="Play" SystemSound="Exclamation"/>
<MSBuild.ExtensionPack.Framework.Thread TaskAction="Sleep" Timeout="500"/>
<MSBuild.ExtensionPack.Multimedia.Sound TaskAction="Play" SystemSound="Hand"/>
<MSBuild.ExtensionPack.Framework.Thread TaskAction="Sleep" Timeout="500"/>
<MSBuild.ExtensionPack.Multimedia.Sound TaskAction="Play" SystemSound="Question"/>
</Target>

228

Buenas prcticas en desarrollo de software con .NET

</Project>

15.12 MSBuild Explorer


En http://www.msbuildexplorer.com/ hay una herramienta GUI para facilitar el trabajo con MSBuild. La
versin actual trabaja con .NET 3.5, pero se ha anunciado una versin para .NET 4.0 para julio de este ao
(vase la entra de 1 de abril del blog http://www.freetodev.com/).
Esta herramienta es til para leer ficheros MSBuild y para generarlos.

15.13 Referencias
El material de este apartado se ha obtenido principalmente estas fuentes:

El libro Inside the Microsoft Build Engine: Using MSBuild and Team Foundation Build, de Sayed
Ibrahim Hashimi y William Bartholomew, de la editorial Microsoft Press.
La documentacin official de MSBuild (http://msdn.microsoft.com/en-us/library/dd393574.aspx).

16 Instaladores
Al usar una herramienta como Visual Studio nuestra aplicacin (o conjunto de aplicaciones) consiste en uno
o ms paquetes binarios con cdigo (ejecutable o de librera) y varios recursos. La distribucin de un
producto .NET es sencilla si se est dispuesto a ejecutar muchas acciones manualmente. Lo ideal es crear un
instalador que simplifique este trabajo y permita que los usuarios finales puedan desplegar el software sin
entrar en detalles tcnicos o de un conocimiento excesivo de los artefactos que componen la aplicacin. Los
usuarios esperan poder instalar el producto con un instalador, esto es, un fichero Setup.exe o
AplicacinInstaller.msi.
Visual Studio permite crear proyectos para el despliegue de aplicaciones.

229

Buenas prcticas en desarrollo de software con .NET

16.1 Un instalador de ejemplo


Vamos crear un instalador para nuestra aplicacin RssReaderApp, usa app.config y depende de nuestra
librera Rss y de log4net.
Empezamos aadiendo a nuestra solucin un proyecto de tipo Setup Project, que se encuentra en Other
Project Types Setup and Deployment Visual Studio Installer, del cuadro de dilogo que aparece al
solicitar la adicin del nuevo proyecto:

Llamaremos a nuestro programa RssReaderInstaller. Visual Studio presentar un aspecto similar a ste:

230

Buenas prcticas en desarrollo de software con .NET

Tenemos un proyecto de instalacin vaco. Empezamos por aadir los ficheros y carpetas que queremos
instalar en el ordenador del usuario. Para ello vamos a la carpeta Application Folder de la izquierda (bajo el
icono con texto File System on Target Machine), accedemos al men contextual con el botn derecho y
seleccionamos Add Project Output Aparecer un cuadro de dilogo como ste:

Ntese que Project hemos seleccionado la aplicacin RssReaderApp. Pulsamos en OK y en Visual Studio
veremos algo similar a esto:

Se ha seleccionado automticamente la salida del proyecto.


Conviene crear ahora un enlace directo a Primary output from RssReaderApp (Active), cosa que hacemos
con el men contextual sobre ese elemento y seleccionando la entrada Create Shortcut to Primary output
from RssReaderApp (Active). Aparecer algo similar a esto:

231

Buenas prcticas en desarrollo de software con .NET

Como el destino del enlace es el escritorio, hemos de arrastrar el icono a Users Desktop. Al construir
aparecer en pantalla algo similar a esto:

16.2 Un parntesis
Si al construir el proyecto aparece un error que guarda relacin con el Target Framework. The target
version of the .NET Framework in the project does not match the .NET Framework launch condition version
'.NET Framework 4 Client Profile'. Update the version of the .NET Framework launch condition to match the
target version of the.NET Framework in the Advanced Compile Options Dialog Box (VB) or the Application
Page (C#, F#).
Para corregirlo basta con sacar el men contextual del icono que seala el error y escoger Properties. Bajo el
Solution Explorer aparece un cuadro con las propiedades:

Hemos de cambiar el valor de Version:

232

Buenas prcticas en desarrollo de software con .NET

Si el error persiste, hemos de ir al men contextual de la aplicacin, en el Solution Explorer, y seleccionar


Properties. Aparecer una ventana como esta:

Pulsamos Prerrequisites y aparece este cuadro:

233

Buenas prcticas en desarrollo de software con .NET

Nos aseguramos de que slo est seleccionado el framework necesario para nuestra aplicacin y pulsamos
Ok/Aceptar en los dos cuadros abiertos.

16.3 Seguimos con el ejemplo


Si ahora vamos al sistema de ficheros, encontraremos en la carpeta Debug (o Release) de nuestro proyecto
de instalacin estos dos ejecutables:

Ya tenemos nuestro instalador.


Probemos el funcionamiento. Al ejecutar RssReaderInstaller.msi aparecer un asistente:

Solicitar una ruta de instalacin:

234

Buenas prcticas en desarrollo de software con .NET

Y esperar confirmacin:

Tras pulsar en Next tratar de iniciar el proceso de instalacin:

235

Buenas prcticas en desarrollo de software con .NET

Pero en este instante el sistema solicitar permisos de administracin para proceder con la instalacin. Si
confirmamos la escalada de permisos, la aplicacin e instalar la aplicacin:

Finalmente solicitar que se pulse Close para salir:

236

Buenas prcticas en desarrollo de software con .NET

Hemos creado un sistema de instalacin idntico al de las aplicaciones de uso comn en Windows.

17 Sistemas de control de versiones distribuidos


El cdigo de nuestros proyectos est necesariamente en constante evolucin. Hay dos problemas que hemos
de abordar:

Cmo tener un histrico de cambios que nos permita retroceder en el tiempo y recuperar el estado
del proyecto en un instante anterior.
Cmo permitir que dos o ms programadores editen simultneamente ficheros del proyecto y la
gestin de conflictos entre las ediciones en un mismo fichero se resuelvan cmodamente.

Hay otra cuestin importante:

Cmo podemos mantener varias ramas del cdigo para trabajar simultneamente en versiones
sensiblemente diferentes de un producto. Y cmo fundir ramas cuando se desea incorporar en un
solo producto funcionalidades que se han desarrollado en ramas distintas.

Un mtodo de controlar las versiones, que afortunadamente va cayendo en el olvido, consiste en ir


empaquetando versiones del cdigo en ficheros comprimidos que datamos de algn modo. Estos ficheros se
pueden almacenar en un repositorio accesible por todo el equipo. Aunque el problema de recuperar
versiones puede solucionarse as, ninguno de los otros tiene fcil arreglo con chapuzas como esa.
Los entornos de desarrollo modernos utilizan sistemas de control de versiones. Este tipo de repositorios se
conoce con el nombre genrico cd CVS, por Control Version Systems. Los hay de dos tipos:

Centralizados: hay un repositorio central de cdigo que mantiene la versin oficial. Los
desarrolladores siempre actualizan su cdigo con ese repositorio central y todos los cambios en los
ficheros fuente se registran en ese repositorio, con lo que estn accesibles instantneamente para
todo el equipo de desarrolladores.

237

Buenas prcticas en desarrollo de software con .NET

Distribuidos: cada copia del cdigo fuente es un repositorio completo, con toda la historia de
cambios sufridos por cada fichero. No hay un repositorio central, salvo que el equipo decida que un
repositorio determinado tiene ese papel de oficial. Los desarrolladores registran muchos de los
cambios nicamente en su copia local, y slo actualizan el repositorio central cuando consideran que
es conveniente a efectos de compartir con otros los cambios. Este tipo particular de repositorios se
conoce con el nombre genrico cd DCVS, por Distributed Control Version Systems.

Durante un tiempo slo haba sistemas centralizados. El primer sistema (si no tenemos en cuenta el
antecesor de todos los sistema de control: RCS) fue CVS (no confundir con el trmino general que engloba a
todos los sistemas de control de versiones). Planteaba algunos problemas que se solucionaron en un
producto directaente inspirado en CVS, pero ms verstil: Subversion, tambin conocido por SVN.
Aunque ya haba varios sistemas distribuidos veteranos, como Arch o Bazaar, este tipo de sistemas extendi
su uso enormemente con la aparicin de Git. Git fue desarrollado por Linus Torvalds, creador de Linux, para
mantener el cdigo fuente del sistema operativo libre. Otro sistema muy popular es Mercurial y nosotros
estudiaremos ste. La razn de escoger Mercurial es que presenta menos problemas de portabilidad a
Windows (Git es un herramienta concebida para sistems Posix) y resulta algo ms sencilla de entender. Salvo
un pequeo ncleo de cdigo escrito en C (por razones de eficiencia), Mercurial usa Python como lenguaje
de implementacin; de ah su mayor portabilidad.
Aunque nuestro objetivo es presentar Mercurial para mantener cdigo con control de versiones, es una
herramienta que permite gestionar versiones en documentos de cualquier tipo (y especialmente cmoda si
son de texto). Es recomendable su uso en contextos distintos del desarrollo de software.

17.1 Mercurial
17.1.1 Interfaces de usuario
Mercurial presenta tres interfaces distintas:

La interfaz de lnea de rdenes, en las que se interacta con el repositorio mediante la orden hg (de
Hg, el smbolo qumico del mercurio).
La interfaz integrada con el escritorio de Windows, TortoiseHg, que permite gestionar la mayor parte
de acciones con el men contextual sobre archivos y carpetas. Asimismo, los iconos se marcan con el
estado (actualizado, con cambios, con conflictos, etctera). TortoiseHg presenta una especie de
interfaz doble: las acciones son accesibles con mens contextuales sobre carpetas y archivos en
Windows Explorer, pero tambin desde una aplicacin GUI denominada Hg Workbench que se
invoca desde ese mismo men contextual.
La interfaz integrada con Visual Studio 2010, VisualHg, que enriquece los mens de la aplicacin y la
barra de herramientas. Tambin muestra visualmente el estado de cada fichero en el Solution
Explorer.

17.1.2 Instalacin
La pgina oficial de Mercurial es http://mercurial.selenic.com/. All podemos descargar la ltima versin, que
a fecha de hoy es la 1.8.2. Para Windows, el instalador incluye TortoiseHg, que va por la versin 2.0.4.
Bajamos de la pgina web el paquete tortoisehg-2.0.3-hg-1.8.2-x64.msi (si el operativo es de 64 bits) que
contiene las dos primeras interfaces: la de consola y la integrada con el escritorio. La instalacin es trivial.
Podemos asegurarnos de que todo fue bien yendo al escritorio y pulsando el botn derecho del ratn:

238

Buenas prcticas en desarrollo de software con .NET

aparecer un men contextual con una entrada Hg Workbench y un submen TortoiseHg. Si arrancamos una
consola y escribimos la orden hg, obtendremos una pantalla de ayuda:
Microsoft Windows [Versin 6.1.7601]
Copyright (c) 2009 Microsoft Corporation. Reservados todos los derechos.
C:\Users\amarzal>hg
Mercurial Distributed SCM
basic commands:
add
annotate
clone
commit
diff
export
forget
init
log
merge
pull
push
remove
serve
status
summary
update

add the specified files on the next commit


show changeset information by line for each file
make a copy of an existing repository
commit the specified files or all outstanding changes
diff repository (or selected files)
dump the header and diffs for one or more changesets
forget the specified files on the next commit
create a new repository in the given directory
show revision history of entire repository or files
merge working directory with another revision
pull changes from the specified source
push changes to the specified destination
remove the specified files on the next commit
start stand-alone webserver
show changed files in the working directory
summarize working directory state
update working directory (or switch revisions)

use "hg help" for the full list of commands or "hg -v" for details
C:\Users\amarzal>

La orden hg help proporciona una ayuda ms extensa y hg help add muestra la pgina de ayuda de la
orden add.
17.1.3 Configuracin
Conviene dar de alta en un fichero informacin que, de otro modo, tendramos que proporcionar en
numerosas ocasiones. Por ejemplo, el nombre del autor de los cambios se puede indicar en un fichero de
configuracin. El fichero .hgrc en el directorio principal del usuario (%HOME%\.hgrc, es decir,
C:\Users\usuario\.hgrc) es un posible lugar para indicar esa informacin. Pero hay varios ficheros que
Mercurial mira para encontrar la configuracin:

%USERPROFILE%\.hgrc
%USERPROFILE%\Mercurial.ini
%HOME%\.hgrc
%HOME%\Mercurial.ini

Mercurial busca la configuracin en el primero de esos ficheros que exista.


17.1.4 Conceptos previos
El usuario trabaja en una copia de trabajo del directorio que est sujeto a control de versiones. Ese mismo
directorio contiene el repositorio en un subdirectorio que no debe modificarse manualmente nunca. El
repositorio contiene una coleccin de conjuntos de cambios (changesets) relacionados entre s con una
estructura de grafo. Un conjunto de cambio es una representacin de las diferencias entre dos versiones de
cada uno de los ficheros controlados. El conjunto de cambios se conoce tambin por revisin. Estos
conjuntos se identifican con un nmero y un identificador. El identificador es un nmero hexadecimal de 40

239

Buenas prcticas en desarrollo de software con .NET

dgitos (un hash generado con MD5, que es nico por cada revisin). Esta imagen muestra el grafo que
relaciona los changesets de un repositorio (extrada del wiki de Mercurial
http://mercurial.selenic.com/wiki/UnderstandingMercurial):

Se puede ver que, excepto la primera, cada revisin se construye sobre los cambios que introdujeron una o
dos revisiones anteriores. Cuando slo hay una revisin antecesora, est claro que se sigue un proceso
incremental de cambios, sin ms. Cuando hay dos predecesores, el cambio es fruto de una fusin de dos
versiones anteriores. Esta situacin se puede dar porque el repositorio puede ser atacado por diferentes
usuarios y cada uno puede modificar independientemente una misma revisin. Es lo que ocurri en las
revisiones 2 y 3: se obtuvieron a partir de la revisin 1, probablemente por dos programadores
independientes. Para que los cambios hechos por uno y otro convivieran en el repositorio hubo que fundir
las versiones 2 y 3 para dar lugar a la 4. Alguien modific la versin 4 para generar la 5 y esa rama an no se
ha fundido con la revisin 6, que tambin es una evolucin de la 4. El ltimo nodo, llamado working copy
representa a nuestra copia de trabajo, posiblemente con cambios que an no se han registrado en el
repositorio. Ntese que la revisin 6 est marcada con tip. Es la revisin sobre la que la copia de trabajo
puede estar introduciendo modificaciones.
17.1.5 Una sesin de ejemplo
Antes de presentar la rdenes Mercurial para cada accin, veamos alguna situacin de trabajo tpica
(adaptado del mismo lugar del que se obtuvo la imagen anterior).
1) Alice tiene un repositorio como este:

2) Bob clona el repositorio de Alice, as que tiene una copia propia idntica:

3) Bob hace cambios en su copia de trabajo, sin que Alice tenga noticia de ello. Bob consigna los cambios
en su repositorio local y Alice sigue sin percibir ningn cambio:

240

Buenas prcticas en desarrollo de software con .NET

4) Alice, por su parte, acta de modo similar:

5) Bob estira (pull) los cambios del repositorio de Alice hacia el suyo. La copia de trabajo de Bob no se ve
afectada por el estirn, pero s el repositorio. La cima (tip) del repositorio es la versin g, de Alice,
porque es conjunto de cambios ms reciente:

6) Bob funde (merge) las versiones f y g. Su copia de trabajo tiene ahora dos padres.

7) Bob examina el resultado de la fusin y se asegura de que no hay conflictos. Entonces consigna los
cambios y cera as una nueva versin con dos padres:

8) Si Alice estira del repositorio de Bob, obtendr los cambios que introdujo ste, pero su copia de trabajo
seguir apuntando a la versin g:

9) Alice tendr que hacer una actualizacin (update) para conseguir la ltima versin. No necesita hacer
una fusin (merge) porque los cambios de g ya estn integrados en h.

17.2 Tutorial
Seguiremos un tutorial inspirado en el que se encuentra en http://hginit.com. All slo se introducen los
conceptos con la interfaz de lnea. Seguiremos una aproximacin distinta: primaremos el uso de la interfaz
grfica, pero indicaremos tambin las acciones equivalentes de lnea de rdenes.

241

Buenas prcticas en desarrollo de software con .NET

17.2.1 Configuracin
Editamos C:\Users\usuario\Mercurial.ini para dar de alta nuestro identificador de usuario y el editor
que deseamos usar cuando Mercurial desee solicitar la introduccin de texto:
# Generated by TortoiseHg settings dialog
[ui]
username = amarzal@pro
editor = notepad

(El formato habitual para username es Nombre Apellido <direccin@de.correo>.)


17.2.2 Creacin de un proyecto y su repositorio
Creemos una carpeta con nombre miproyecto en el escritorio. En su interior crearemos un fichero de texto
llamado mitexto.txt con este contenido:
Este fichero de texto forma parte del tutorial de Mercurial.
Est en una carpeta de nombre miproyecto.
Ahora mismo no hay control de versiones.

Vamos a crear un repositorio Mercurial. Podemos hacerlo de dos modos. Con la lnea de rdenes tendramos
que ir al directorio miproyecto y, una vez dentro, ejecutar la orden hg init. Con la interfaz TortoiseHg
vamos al men contextual de la carpeta y elegimos TortoiseHg Create Repository Here. Aparecer un
cuadro de dilogo como ste:

Pulsamos el botn Crear y saldr una pantalla indicando que todo fue bien.

Si vemos el contenido de la carpeta, encontraremos algo ms que el fichero mitexto.txt:

242

Buenas prcticas en desarrollo de software con .NET

El fichero .hgignore es un fichero de texto vaco. Servir para indicar qu ficheros no deben gestionarse con
control de versiones. Tpicamente slo queremos controlar nuestros ficheros fuente, pero no los que se
producen automticamente a partir de ellos, como los ensamblados que resultan de un proceso de
compilacin. El fichero .hgignore permite expresar patrones de nombres de fichero que no deben formar
parte del repositorio.
El directorio .hg es el repositorio. Es un directorio nico en el que Mercurial lleva el control de cambios. No
debe editarse manualmente su contenido nunca.
Recordemos que los ficheros del proyecto que vemos en cada instante y que recogen el estado actual del
proyecto forman nuestra copia de trabajo. Nuestro objetivo es que el proyecto progrese editando la copia
de trabajo y registrando los cambios que vayamos efectuando en el repositorio.
17.2.3 Adicin de un fichero
Actualmente el repositorio no est gestionando ningn fichero. Tampoco mitexto.txt. Para indicar a
Mercurial que debe controlar ese fichero tenemos dos posibilidades. Con la lnea de rdenes ejecutaramos
hg add mitexto.txt. Con Tortoise seleccionamos TortoiseHgAdd Files y aparece este cuadro:

Pulsamos en Agregar y ya est. Sabemos que el fichero est siendo gestionado por Mercurial porque su
icono aparece marcado en el explorador de Windows:

243

Buenas prcticas en desarrollo de software con .NET

La cruz azul indica que ha sido aadido.


17.2.4 Consigna de cambios
El hecho de que el fichero haya sido aadido no significa que el repositorio ya almacene la ltima versin. Es
necesario ejecutar una accin de consigna del cambio (commit). Con la lnea de rdenes basta con hacer hg
commit. Con Tortoise seleccionamos HgCommit en el men sobre cualquier elemento de la carpeta.
Aparecer este cuadro:

Damos a Consignar y veremos que nos solicita un mensaje para la consignacin.

Nos pide que demos una descripcin de los cambios que introduce la consigna. Escribimos algo como
Primera versin.:

244

Buenas prcticas en desarrollo de software con .NET

Y pulsamos en Consignar nuevamente. Si quisisemos hacer esto mismo con la lnea de rdenes,
ejecutaramos hg commit m Primera versin..
El cuadro insiste en que hay un fichero que no estamos controlando con Mercurial:

Tiene sentido controlar un fichero que forma parte de la infraestructura misma? La verdad es que s: si
otros desarrolladores obtiene una copia del repositorio, nos conviene compartir la relacin de ficheros que
deben ser ignorado. Podramos aadirlo del mismo modo que hicimos con mitexto.txt, pero lo haremos
ahora desde esta interfaz. Con el men contextual sobre el fichero .hgignore seleccionamos Add. Adems,
marcaremos el checkbox del fichero y escribiremos un texto para la consigna, con lo que quedar as:

245

Buenas prcticas en desarrollo de software con .NET

Tras pulsar Consignar estar todo en orden. Podemos pulsar a continuacin Cancel para que desaparezca el
cuadro de dilogo. Tenemos dos ficheros en el repositorio. Sabemos que nuestra copia est sincronizada con
el repositorio por los iconos que se muestran en la carpeta:

17.2.5 Registro de cambios


Pidamos que nos muestre el registro de cambios en el repositorio. Podemos hacerlo desde la lnea de
rdenes con hg log. Por pantalla aparecer esto:
changeset:
tag:
user:
date:
summary:

1:e0009eeaea8d
tip
amarzal@pro
Thu Apr 28 15:08:20 2011 +0200
Aadido .hgignore.

changeset:
user:
date:
summary:

0:5dcd74736d88
amarzal@pro
Thu Apr 28 09:40:24 2011 +0200
Primera versin.

Se nos informa de que hay dos cambios y se muestran en orden inverso. De cada conjunto de cambios se
muestra un nmero de revisin y, separada con dos puntos, la etiqueta que lo identifica (changeset), el
usuario que dio de alta los cambios (user), la fecha del cambio (date) y la informacin que dimos al consignar
el cambio (summary). Uno de los cambios (el ltimo) presenta adems una etiqueta (tag): tip. En ingls
tip significa punta o cima. Se nos indica as que ese el ltimo cambio realizado, la punta o extremo del
proceso de cambios.

246

Buenas prcticas en desarrollo de software con .NET

Podemos obtener esta misma informacin (y mucha ms) si solicitamos a TortoiseHg que nos muestre el Hg
Workbench. Aparecer una ventana como sta:

La historia de cambios se muestra con un grafo justo debajo de la barra de herramientas. Pinchando en cada
cambio se pueden obtener detalles de las acciones que comport ese cambio.
17.2.6 Cambios
Trabajemos un poco en nuestro proyecto. Creemos un nuevo fichero de texto, nuevo.txt, con este texto:
Este es el contenido del nuevo fichero.
No olvide aadirlo al repositorio con hg add.
Y no olvide, despus, consignar el cambio con hg commit.

Ahora, modifiquemos mitexto.txt para que pase a leerse as (se destacan los cambios con fondo amarillo):
Este fichero de texto forma parte del tutorial de Mercurial.
Est en una carpeta de nombre miproyecto.
Ahora mismo s hay control de versiones.
Y esta es una lnea nueva.

Leamos el estado de nuestra copia de trabajo:

247

Buenas prcticas en desarrollo de software con .NET

Se puede ver que el fichero mitexto.txt est marcado como no sincronizado. El fichero nuevo.txt no tiene
ninguna marca porque an no se ha aadido. La carpeta miproyecto (a la izquierda), tambin est marcada
como no sincronizada.
Aadamos ahora el fichero nuevo.txt (por el mtodo que se prefiera: lnea de rdenes o TortoiseHg):

Tenemos cada icono con una marca distinta. Consignemos los cambios (commit) con este mensaje:
Aadido nuevo.txt y modificado mitexto.txt. :

17.2.7 Vuelta atrs


Vamos a meter la pata y a recuperarnos del error. Cambiemos el contenido de mitexto.txt por este texto:
Nuevo contenido, pero errneo.
Nos gustara poder volver atrs.

Adems, eliminemos nuevo.txt tirndolo a la papelera. Y ahora volvamos atrs. Para volver a la versin
anterior podemos ejecutar la orden hg revert all o, dese TortoiseHg, seleccionar la entrada de men
Revert Aparecer esto:

248

Buenas prcticas en desarrollo de software con .NET

Nos indica que mitexto.txt est modificado y nuevo.txt ha desparecido. Esta misma informacin se puede
mostrar con la orden hg status:
M mitexto.txt
! nuevo.txt

Seleccionamos los dos ficheros y pulsamos Deshacer. As nos queda la carpeta:

Iconos que indican que no hay sincrona? La integracin con el escritorio no es perfecta. Si queremos estar
seguros, seleccionamos TortoiseHg Update Icons.

249

Buenas prcticas en desarrollo de software con .NET

17.2.8 Analizando los cambios


Hagamos algn cambio en nuevo.txt. Arranquemos un editor y hagamos que el contenido del fichero pase a
ser este:
Este es el contenido del fichero, ya no tan nuevo.
Acabamos de editarlo para cambiar el contenido.
Y no olvide, despus, consignar el cambio con hg commit.

Podemos ver las diferencias entre el contenido actual del fichero y lo que tenemos en el repositorio. Con la
lnea de rdenes escribimos hg diff nuevo.txt:
diff -r 902c6b89bcb1 nuevo.txt
--- a/nuevo.txt Thu Apr 28 16:28:59 2011 +0200
+++ b/nuevo.txt Thu Apr 28 17:47:05 2011 +0200
@@ -1,3 +1,3 @@
-Este es el contenido del nuevo fichero.
-No olvide aadirlo al repositorio con hg add.
+Este es el contenido del fichero, ya no tan nuevo.
+Acabamos de editarlo para cambiar el contenido.
Y no olvide, despus, consignar el cambio con hg commit.
\ No newline at end of file

La salida de la orden sigue un formato estndar de herramientas para mostrar diferencias entre ficheros. La
primera lnea muestra la revisin usada en la comparacin (con el identificador 902c6b89bcb1) y el fichero.
Las dos lneas siguientes indican que las lneas de la versin con la fecha que se indica aparecern con
guiones y las de la otra, con cruces. Luego aparece una lnea flanqueada con @@ que dice que se muestran a
continuacin las lneas 1 a 3 de uno y otro fichero. Las dos primeras estaban en el original, pero no en el
actual y las dos siguientes estn en el ms reciente, pero no en el antiguo. Finalmente se muestra una lnea
comn a ambos y un mensaje indicando que la ltima lnea no acaba con un salto de lnea.
Podemos acceder a esta misma informacin con TortoiseHg Visual Diff sobre el fichero nuevo.txt:

250

Buenas prcticas en desarrollo de software con .NET

17.2.9 Borrado de un fichero


Vamos a eliminar ahora el fichero nuevo.txt. La primera idea consiste en tirar a la papelera el fichero de
nuestra copia de trabajo. Pero eso slo lo elimina de la copia de trabajo: Mercurial seguir manteniendo
informacin del fichero como si nunca se hubiera eliminado.
Primero hemos de eliminar el fichero del repositorio. Podemos hacerlo con hg remove nuevo.txt. O
eliminador con TortoiseHgRemove Files:

Si pulsamos Eliminar para suprimirlo del repositorio, llegamos a un error:

251

Buenas prcticas en desarrollo de software con .NET

Mercurial nos advierte de que podemos perder cambios: la versin de la copia de trabajo no est
sincronizada, as que si ms tarde queremos recuperar el fichero con el contenido actual, no podremos. Nos
sugiere que, si estamos seguros, usemos una opcin (-f) para forzar la eliminacin. Haremos algo mejor:
primero consignaremos los cambios del fichero y luego lo eliminaremos tranquilamente. As pues,
ejecutamos Hg Commit y repetimos el proceso de eliminacin:

Eliminar ha suprimido todo rastro del fichero en la copia de trabajo. Se han dado de alta esos cambios en el
repositorio? Pidamos nuevamente el Hg Workbench:

252

Buenas prcticas en desarrollo de software con .NET

No! Ocurre como con Add, que no basta aadir (eliminar) un fichero: hemos de consignar el cambio. Lo
podemos hacer desde dentro del mismo Workbench. Escribimos el texto explicativo y pulsamos Consignar:

Ahora s.
17.2.10 Viajando en el tiempo
Podemos ir atrs en el tiempo con la orden hg update r id, donde id es la versin a la que deseamos ir (la
r por revisin). Es ms cmodo hacerlo desde TortoiseHg. Seleccionamos TortoiseHgUpdate:

253

Buenas prcticas en desarrollo de software con .NET

En el combobox Actualiza a: podemos seleccionar la revisin a la que deseamos ir. El desplegable del
combobox slo permite elegir versiones con etiqueta (default, tip). La caja es editable y en ella podemos
escribir el nmero de versin. Escribamos un 2:

Y pulsemos Update. Nuestra copia de trabajo habr recuperado los ficheros como estaban en aquel
momento. Volvamos a la versin actual seleccionando Update con la versin etiquetada como tip.
17.2.11 Publicacin del repositorio con un servidor
Ahora queremos compartir el repositorio con otro programador. Mercurial permite crear un servidor sencillo
para facilitar un intercambio rpido. Ojo! Ese servidor no est pensado para servir continuamente el
repositorio a cualquier punto de Internet. Para ello hay que montar un servidor con algunas medidas de
seguridad y ese asunto queda fuera de nuestro objetivo docente.
Arranquemos un servidor con la lnea de rdenes: hg serve. Aparecer un mensaje indicando el puerto de
escucha:
listening at http://pro:8000/ (bound to *:8000)

Iniciemos un navegador y vayamos a la URL http://localhost:8000. Saldr una pgina web con algo parecido
a esto:

254

Buenas prcticas en desarrollo de software con .NET

Una interfaz web para acceder al repositorio! A mano izquierda encontramos un men de acciones.
Podemos arrancar el servidor desde la interfaz grfica con TortoiseHgWeb Server.
Vamos a obtener una copia del repositorio. Aunque trabajemos con un solo usuario y un solo ordenador,
podemos acceder desde otra mquina y con otro usuario. Si lo hacemos con la lnea de rdenes
ejecutaramos hg clone http://localhost:8000 destino, donde destino es el nombre del directorio en
el que deseo albergar mi copia.
Alternativamente podemos ejecutar TortoiseHgClone y dejar el cuadro de dilogo as:

Ya est. En el directorio miproyectoclonado tenemos una copia del repositorio y una copia de trabajo que
coincide con la ltima versin del repositorio.
17.2.12 Mantener la sincrona
Ahora tenemos dos copias del repositorio. Ninguna de ellas es ms importante que la otra. Llamemos A al
usuario que editaba la primera copia y B al que ha obtenido un clon. Supongamos que A cambia el contenido
de su copia de trabajo. Por ejemplo, crea una carpeta llamada Sub y dentro crea un fichero subtexto.txt con
este contenido:
Un subtexto.

Tendremos que aadir este fichero como hemos hecho con los otros. No es necesario incluir la carpeta: al
incluir un fichero, Mercurial sabe llevar cuenta de la carpeta o carpetas en las que se encuentra. Recordemos
que no basta con aadir: adems hemos de consignar los cambios.

255

Buenas prcticas en desarrollo de software con .NET

El repositorio y copia de trabajo de B no tiene noticia del cambio que ha introducido A. Puede averiguarlo (y
conseguir una sincronizacin) con una herramienta especial: TortoiseHgSynchronize (o dentro del
Workbench, que integra sta y otras herramientas).

Pulsamos en el primer botn de los 4 que tienen flechas verdes, en la zona superior. Con ello comparamos
nuestro repositorio con el que se est sirviendo en la web:

256

Buenas prcticas en desarrollo de software con .NET

Nos indica que podemos actualizar nuestra copia. Lo podemos hacer con el segundo de los botones. Tras
pulsarlo:

Si volvemos a pedir que se contrasten los cambios que no hemos sincronizado, veremos que ya no hay:

257

Buenas prcticas en desarrollo de software con .NET

Pero, ojo!: los repositorios estn sincronizados, no as la copia de trabajo de B. Ahora tendr que ejecutar
un Update en miproyectoclonado. Ahora s. Hemos sincronizado repositorio y copia de trabajo.
Las acciones que hemos ejecutado tienen nombre:

Averiguar los cambios entrantes (incoming):


Actualizar (update): traer cambios del repositorio a la copia de trabajo.
Estirar (pull): llevar cambios de un repositorio remoto a mi repositorio.

No slo podemos sincronizar copiando del repositorio de la web: tambin podemos copiar hacia el
repositorio de la web. Para eso tenemos los otros dos botones. Sus acciones tienen nombre:

Consignar (commit): llevar cambios de la copia de trabajo al repositorio.


Empujar (push): llevar cambios de mi repositorio a un repositorio remoto.

Hemos dicho que no hay ningn repositorio maestro, que cualquier copia es tan central como cualquier
otra copia. Es as, pero nada impide que el grupo de desarrolladores decida que una de las copias es la copia
central. En esa copia se registrarn los cambios validados y se entender que la ltima versin del software
reside ah. Al inicio de cada da de trabajo, los desarrolladores actualizarn su copia con los cambios del
repositorio (si no hay conflictos, algo de lo que hablamos ms tarde) y cuando ven que tienen cambios
relevantes que se pueden compartir, los publicar en ese repositorio central para que estn accesibles para
todos los miembros del equipo.
17.2.13 Un resbaln
Ahora tenemos dos repositorios sincronizados. Supongamos que A edita mitexto.txt para que contenga esto:
Este fichero de texto forma parte del tutorial de Mercurial.
Est en una carpeta de nombre miproyecto.
Ahora mismo s hay control de versiones.
Y esta es una lnea nueva.
La ltima edicin la hizo A.

A consigna los cambios con el mensaje Edicin de mitexto.txt.

258

Buenas prcticas en desarrollo de software con .NET

Y ahora B edita Sub\subtexto.txt para que quede as:


Un subtexto que ha editado por ltima vez B.

B consigna los cambios con el mensaje Edicin de Sub\subtexto.txt


Cada usuario ha modificado un fichero distinto. Para volver a sincronizar los ficheros parece lgico que
hayamos de seguir estos pasos:

A invoca a la herramienta de sincronizacin estirando (pull) los cambios de B hacia el repositorio


de A. Para ello B ha de levantar antes un servidor web que publique su repositorio (si seguimos el
tutorial en la misma mquina hay que elegir un puerto distinto del usado por A). Una vez hecho, A
puede hacer pull. Con eso A consigue que su repositorio incorpore los cambios del repositorio de B.
A hace update para que su copia de trabajo refleje los cambios.
B hace un pull del repositorio remoto.
B hace un update para que su copia de trabajo refleje los cambios.

Hemos vuelto a sincronizar las dos copias? Tiene inters que en una y otra veamos el Workbench. En A
tenemos este grafo de revisiones:

Y en B tenemos:

259

Buenas prcticas en desarrollo de software con .NET

Podemos ver que hemos experimentado problemas si analizamos el grfico con la historia de las versiones.
La revisin sexta queda en un callejn sin salida. Hemos tenido un problema serio: no ha funcionado el
mtodo de sincronizacin. Por qu? Cuando A hizo un pull, recibi los cambios que haba efectuado B en
Sub\subtexto.txt, pero perdi los cambios que haba introducido l mismo, A, en mitexto.txt! Lo mismo ha
ocurrido con B: ha conseguido los cambios de A, pero ha perdido los suyos. Un desastre.
Pero no est todo perdido. Lo que queremos hacer es fundir (merge) las versiones 6 y ltima del repositorio.
Vamos a la versin 6 en la copia de B y en el men contextual escogemos Merge with local El workbench
mostrar este grfico de versiones:

260

Buenas prcticas en desarrollo de software con .NET

Ya est. B tiene los dos ficheros modificados: ha fusionado los cambios de las dos ramas. Tendremos que
hacer lo mismo con A.
Hemos conseguido sincronizar los dos repositorios y copias de trabajo. Pero no ha sido trivial.
Acostumbrarse a esta forma de trabajar cuesta un poco.
Para que no ocurran problemas como este hemos de recordar hacer fusiones cuando traigamos una copia
remota. Un examen del grafo es necesario para saber qu estamos haciendo y si hemos omitido algn paso.
17.2.14 Repositorio central y flujo de trabajo
En un equipo con varios desarrolladores no es frecuente hacen sincronizaciones dos a dos, sino pasando por
un repositorio central. El flujo de trabajo se parece a esto:
1.
2.
3.
4.
5.

Si ha pasado un tiempo sin acceder al repositorio central, se hace un pull y un update.


Se editan los ficheros de la copia de trabajo.
Se hace un commit para que nuestro repositorio local registre los cambios.
Se repiten los pasos 2 y 3 hasta que creamos tener cambios que ya se pueden exportar a los dems.
Para compartir los cambios, se siguen estos pasos:
a. Se hace un pull para obtener los cambios que puedan haber hecho los dems
desarrolladores.
b. Se hace un merge para fundirlos con los propios.
c. Nos aseguramos de que la mezcla no ha generado problemas (si estamos trabajando en un
proyecto, tendramos que compilar ahora y ejecutar todas las pruebas unitarias para
comprobar que seguimos en verde).
d. Hacemos un commit para que nuestro repositorio recoja los cambios de la mezcla.
e. Ejecutamos un push para que el repositorio central reciba los cambios que hemos hecho.

261

Buenas prcticas en desarrollo de software con .NET

17.2.15 Conflictos
Aunque pareca que generbamos conflictos antes, la herramienta ha sido capaz de encontrar una solucin
satisfactoria. Era fcil porque las modificaciones ocurran en ficheros diferentes.
Si las modificaciones tienen lugar en un mismo fichero, puede que tengamos problemas. Y slo decimos
puede porque cambios en zonas diferenciadas de un mismo fichero pueden resolverse sin intervencin
nuestra.
Generemos una situacin en la que se precisa intervencin humana. Hagamos que A modifique su copia de
mitexto.txt para que quede as:
Este fichero de texto forma parte del tutorial de Mercurial.
Est en una carpeta de nombre miproyecto.
Ahora mismo s hay control de versiones.
Y esta es una lnea nueva que ha tocado A.

Y que B modifique el mismo fichero para que quede as:


Este fichero de texto forma parte del tutorial de Mercurial.
Est en una carpeta de nombre miproyecto.
Ahora mismo s hay control de versiones.
Y esta es una lnea nueva que ha tocado B.

Hagamos que B haga commit. El repositorio de B ya contiene los cambios de B.


Ahora A quiere sincronizar su copia con la de B, pero no sabe que habr un conflicto por la edicin que ha
hecho B. Sigamos el protocolo que hemos apuntado antes desde el paso 3. A hace commit. A continuacin, A
hace un pull para obtener una copia del repositorio de B y, acto seguido, hace un merge entre la versin del
repositorio y la copia de trabajo. Mercurial detecta un conflicto:

El mensaje There were merge conflicts that must be resolved contiene un hiperenlace: si
pulsamos en resolved aparecer un nuevo cuadro de dilogo:

262

Buenas prcticas en desarrollo de software con .NET

El conflicto slo puede resolverse escogiendo una de las dos versiones, pues afecta a una misma lnea del
mismo fichero. Si tenemos claro qu versin escoger, pulsaremos Take Local (la nuestra) o Take Other (la
que hemos trado del repositorio ajeno). Si pulsamos Tool resolve, se activar una herramienta que nos
permite comparar tres versiones del fichero:

La versin anterior a nuestro ltimo commit en nuestro repositorio.


La versin de la copia de trabajo.
La versin del repositorio ajeno.

263

Buenas prcticas en desarrollo de software con .NET

En la zona de abajo se muestra el contenido que deseamos. Ah podemos elegir lneas de las otras tres
versiones y formar la versin que damos por buena. Cuando lo hayamos hecho, guardaremos el resultado y
saldremos de la herramienta.
El cuadro de dilogo mostrar ahora esto:

Hemos resuelto los conflictos. Ya podemos cerrar este cuadro y volvemos al anterior:

264

Buenas prcticas en desarrollo de software con .NET

Si pulsamos en Commit, habremos completado la accin de fusin. Veamos el aspecto del Workbench:

Un grfico complicado!
Si estamos trabajando con un repositorio central, no hemos acabado. Ahora que nuestro repositorio no
tiene conflictos, podemos consignar los cambios en nuestro repositorio y de ah, empujarlos al repositorio
central.
17.2.16 El da a da
El da a da con Mercurial es ms sencillo que este caso enrevesado que hemos planteado. Usualmente hay
un repositorio central que los programadores usan para intercambiar informacin.
En el wiki de Mercurial (http://mercurial.selenic.com/wiki/UnderstandingMercurial) hay un tutorial que
reproducimos aqu porque contiene una narracin de una sesin tpica de trabajo con Mercurial que da
detalles sobre el estado del repositorio en cada instante.
Mercurial ofrece ms posibilidades que las que hemos presentado aqu, pero con esto es casi suficiente para
empezar. Slo queremos comentar una ms: la posibilidad de usar colas de parches (patch queues).
17.2.17 Colas de parches
Hay un problema con el uso diario del sistema. Si un desarrollador hace consignas de cambios muy
frecuentemente, cada consigna crea una revisin en el repositorio y esta revisin nos acompaar siempre.
Es fcil que acabemos teniendo centenares o millares de revisiones que tienen poca entidad. En principio no
hay problema: siempre podemos marcar con una etiqueta los cambios que dieron lugar a una versin
importante. Pero si con esto no nos basta, hay una solucin: no consignar cambios en los ficheros, sino
parches con las diferencias entre versiones de ficheros. Es como entrar en una zona temporal de cambios en
los que se consigna menos informacin para no saturar el repositorio.
Los detalles de esta posibilidad se pueden consultar en http://mercurial.selenic.com/wiki/MqExtension.

265

Buenas prcticas en desarrollo de software con .NET

17.3 Integracin con Visual Studio


VisualHG es un plugin para Visual Studio 2010 que permite invocar las principales operaciones de Mercurial
desde el entorno de desarrollo, adems de mostrar grficamente el estado de cada fichero en el Solution
Explorer.
17.3.1 Configuracin de Visual Studio 2010
Empezamos yendo a ToolsOptions y, en el cuadro de dilogo, activamos el tem Source ControlPlug-in
Selection y nos aseguramos de que est seleccionado VisualHG:

17.3.2 Alta del sistema de control de versiones para una solucin


Si la solucin ya est bajo control de Mercurial (cosa que podemos hacer desde fuera de Visual Studio), se
detectar automticamente. Si no es as, tendremos que crear el repositorio desde la lnea de rdenes o
TortoiseHg.
Vamos a la carpeta de nuestra solucin en Windows Explorer y seleccionamos TortoiseHgCreate
Repository Here Este es un buen fichero .hgignore para trabajar en proyectos con Visual Studio:
syntax: glob
*.csproj.user
*.ReSharper.user
/obj/*
/bin/*
*.ncb
*.suo
*.obj
*.exe
*.pdb
*.user
*.aps
*.pch
*.vspscc
*_i.c
*_p.c
*.ncb
*.suo
*.tlb
*.tlh

266

Buenas prcticas en desarrollo de software con .NET

*.bak
*.cache
*.ilk
*.log
*.lib
*.sbr
*.scc
[Bb]in
[Dd]ebug*/
obj/
[Rr]elease*/
_ReSharper*/
[Tt]est[Rr]esult*
[Bb]uild[Ll]og.*
*.[Pp]ublish.xml

Recordemos que crear el repositorio no supone que los ficheros estn dados de alta. Con TortoiseHg vamos
al directorio que contiene la solucin y seleccionamos TortoiseHgAdd Files Si hemos editado .hgignore
como en el ejemplo, aparecer un cuadro de seleccin de fichero que ya marca algunos con la I de ignore:

Seleccionamos todos los ficheros que aparecen con un interrogante en la columna de estado y pulsamos en
Agregar. Vamos ahora Visual Studio y veremos que Solution Explorer detecta el estado de los ficheros:

267

Buenas prcticas en desarrollo de software con .NET

Hace falta consignar los cambios, operacin que ya podemos hacer desde el entorno. Con el men
contextual de la solucin escogemos VisualHGCommit. Aparece un cuadro para que demos el mensaje
asociado a la revisin y confirmemos la operacin:

Escribimos un mensaje como, por ejemplo, Alta del proyecto y pulsamos en Consignar. Si falta algn dato,
como el nombre del usuario, nos los solicitar ahora:

268

Buenas prcticas en desarrollo de software con .NET

Una vez acabado el proceso, Visual Studio reflejar el nuevo estado de los ficheros con respecto al
repositorio:

Con VisualHG podremos gestionar el 99% de las acciones tpicas del control de versiones de un proyecto sin
necesidad de salir de Visual Studio.
17.3.3 Un ejemplo
En https://hg01.codeplex.com/netalgoritnia hay una librera de algoritmos en C# creada a partir de la librera
algoritmia de Python. La librera est siendo desarrollada por dos personas (Jorge Garca y Sergio Pertiez) y
aqu se puede ver un grafo tpico de edicin:

269

Buenas prcticas en desarrollo de software con .NET

Si se desea trabajar con una copia basta con clonar el repositorio.

17.4 Referencias y recursos

El tutorial de Mercurial es una adaptacin del excelente texto HG Init, de Joel Spolsky, disponible
en http://hginit.com/.
El sitio oficial de Mercurial incluye otro tutorial en http://mercurial.selenic.com/quickstart/.
En http://tortoisehg.bitbucket.org/manual/2.0/ hay un manual (que incluye un tutorial) de
TortoiseHg.
Hay un libro disponible a travs de la pgina de Mercurial: Mercurial The Definitive Guide, de Bryan
OSullivan. Est accesible en http://hgbook.red-bean.com/.
Hay forjas pblicas basadas en Mercurial que permiten albergar proyectos de software libre.
Codeplex (http://www.codeplex.com/), por ejemplo, es una forja gratuita mantenida por Microsoft
en la que hay muchos proyectos abiertos para la plataforma .NET.
Si no se desea trabajar con cdigo abierto, una alternativa es Bitbucket (https://bitbucket.org/) de la
empresa Atlassian, que es gratuito para equipos de hasta 5 programadores y tiene precios
asequibles que van escalando con el tamao del equipo. Bitbucket permite, adems gestionar el
proyecto con herramientas que veremos ms adelante.

270

Buenas prcticas en desarrollo de software con .NET

18 Seguimiento de errores
Coordinar un equipo de desarrollo que trabaja en uno o varios proyectos es complejo. Hay herramientas que
ayudan en esta labor. Atlassian es una empresa que proporciona el producto estrella en este campo: Jira. Es
un producto comercial y puede instalarse en un servidor propio u hospedarse en los servidores de Atlassian.
No es una solucin barata. Aunque recomendamos su uso para grupos de trabajo ya consolidados o
soportados por una empresa, estudiaremos ahora una herramienta gratuita: Bitbucket.
Bitbucket slo presenta dos mdulos: uno para gestionar documentacin con un wiki y otro de seguimiento
de errores (issue tracking). Al trabajar en equipo, este ltimo mdulo es imprescindible para documentar los
bugs, asignar su resolucin a programadores y seguir el proceso de correccin.

18.1 Bitbucket
Bitbucket es un repositorio Mercurial para proyectos de cdigo abierto o privativo. Si el nmero de
desarrolladores es de 5 o menos, no hay coste por usarlo. La ventaja de usar un repositorio hospedado es
obvia: no hay costes asociados al mantenimiento de un servidor propio y las copias de seguridad se
gestionan por quien presta el servicio. Hay algn riesgo con la disponibilidad del servicio, pero es mnimo.
Vamos a dar de alta un proyecto: un repositorio con el proyecto Algoritmia, presentado hace poco. Si ya
disponemos de un clon del repositorio, estamos listos.
18.1.1 Creacin de una cuenta y un repositorio
Creemos una cuenta en Bitbucket. La pantalla principal de nuestra cuenta tiene este aspecto:

Vamos al men Repositories y escogemos Create New Repository. Aparece un formulario:

271

Buenas prcticas en desarrollo de software con .NET

Tras llenar los campos, pulsamos en Create Repository. Asegurmonos de haber marcado los campos Wiki e
Issue Tracking.
La pantalla principal del repositorio tiene este aspecto:

18.1.2 Alta de contenido


Empujemos el contenido de nuestro repositorio a Bitbucket con TortoiseHg:

272

Buenas prcticas en desarrollo de software con .NET

Ya podemos consultar los ficheros en nuestra interfaz en la pestaa Source:

18.1.3 Algunos elementos de la interfaz


Con la barra de herramientas podemos ejecutar ciertas acciones:

Podemos gestionar invitaciones a otros desarrolladores y otorgar permisos de lectura, escritura o


administracin desde el men Invite. Los cambios se publican con una fuente RSS a la que podemos
suscribirnos. Se puede enviar un aviso por correo a los desarrolladores para advertirles de algn cambio
importante que deberan incorporar con urgencia. Tambin podemos hacer un fork del proyecto, esto es,

273

Buenas prcticas en desarrollo de software con .NET

arrancar una versin completamente diferenciada de la que albergamos en el repositorio. Se puede crear
una cola de parches con patch queue. Asimismo, podemos marcar un repositorio como following para
seguir su desarrollo. Finalmente, es posible descargar copias comprimidas con zip o tar del cdigo fuente.
18.1.4 Issues
La pestaa Issues nos lleva a la interfaz de gestin del seguimiento de errores. Inicialmente est vaca.
Creemos una entrada nueva con IssuesCreate New Issue. Aparecer este formulario:

Hemos de rellenar estos campos:

Title: una descripcin breve de la entrada.


Content: una descripcin extensa de la entrada. Si se trata de un error, la descripcin debe incluir un
receta para reproducir el error. Se pueden aadir enlaces a otras entradas, a un conjunto de cambios
o a un usuario. El editor permite incluir cdigo.
Attach a file: fichero que podemos, opcionalmente, aadir para ampliar la informacin de la entrada.
Assign to: desarrollador al que se asigna la entrada. Puede usarse el identificador de cualquier
desarrollados o nobody cuando no se hace la asignacin.
Type: tipo de entrada. Puede ser:
o Bug: error.
o Enhancement: mejora que se desea implementar.
o Proposal: propuesta de mejora que an debe ser aceptada o rechazada.

Una vez dada de alta, la entrada se muestra en la pestaa Issues de este modo:

274

Buenas prcticas en desarrollo de software con .NET

La entrada muestra los datos que hemos aadido y permite su edicin o borrado (o marcado como Spam!,
que tambin estos sitios web son objeto de Spam). Se pueden aadir comentarios por parte de otros
desarrolladores y cambiar el estado. Los estados son:

New: recin creado.


Open: en proceso de resolucin.
Resolved: resuelto.
Invalid: entrada no vlida (quiz porque no es posible reproducir el error tal cul se indica en la
entrada).
Duplicate: entrada que ya se dio de alta previamente (en cuyo caso se puede indicar de qu entrada
es un duplicado).
Wontfix: no se desea resolver porque, quiz, lo que parece un error es una funcionalidad de la
aplicacin.

Tambin se puede poner en espera (on hold). Desde esta interfaz se puede reasignar la entrada o cambiar
su clasificacin en bug/enhancement/proposal. Y es posible, finalmente, marcar la entrada con un
following para indicar que se desea estar especialmente atento a su seguimiento.
La interfaz pemrite ver un listado abreviado de entradas:

Todas (All)
Slo las que estn en estado abierto (Open)

275

Buenas prcticas en desarrollo de software con .NET

Slo las que dio de alta el desarrollador (Mine)

O se puede tambin usar una expresin de consulta para ver exactamente las que cumplen ciertas
condiciones.

Todo proyecto de software debe gestionarse con una herramienta similar a esta, especialmente si se trabaja
en equipo. La informacin que contiene el gestor de incidencias es til tanto para el equipo como para los
usuarios, que pueden informarse sobre los errores detectados y el estado en que se encuentra su correccin
por parte del equipo de desarrollo.

19 Integracin Continua
Hemos aprendido un montn de metodologas y herramientas que nos ayudan a desarrollar software. El
conjunto de prcticas pasa por una disciplina del equipo de desarrolladores que deben asegurarse de que el
cdigo est en verde antes de subirlo al repositorio. Pero, qu pasa cuando no es as?
Un escenario ms frecuente de lo deseable es el que se produce cuando se inicia la jornada y el
desarrollador actualiza su copia de trabajo para descubrir que el repositorio central mantiene una versin
con errores. Toca entonces volver atrs. El ltimo desarrollador subi una copia con errores y el hecho paso
inadvertido hasta que acaba por afectar al flujo de trabajo del grupo. Es ms, ese da hay que desplegar una
copia del software a los usuarios finales y no se est muy seguro de cul es la ltima versin funcional del
cdigo, con lo que no se sabe bien qu compilar y desplegar.
Conviene disponer de una herramienta que automatice el proceso de construccin de software de modo
continuado. Entre sus misiones tenemos:

Ejecutar las pruebas tan pronto se dan de alta cambios y avisar de cualquier problema detectado.
Mantener instaladores de la aplicacin funcionales y actualizados, tpicamente con menos de 24
horas de desfase con respecto a la base de cdigo.

Hay varias herramientas que permiten montar la infraestructura necesaria para efectuar este proceso de
integracin continua: .NET CruiseControl, TeamCity

276

Buenas prcticas en desarrollo de software con .NET

Usaremos TeamCity, que es un producto comercial, pero gratuito para equipos de desarrollo pequeos. La
empresa JetBrains, creadora de ReSharper, desarrolla TeamCity. La pgina principal es
http://www.jetbrains.com/teamcity/.

19.1 Instalacin
Hemos de descargar TeamCity de la pgina principal. Ejecutamos el instalador

Seleccionamos los componentes:

Nos pide que indiquemos un directorio para la configuracin, que por defecto es
C:\Users\amarzal\.BuildServer. Nos solicitar a continuacin que indiquemos un puerto HTTP. Por defecto
sugiere el puerto 80, pero dado que ese el puerto estndar de los servidores web, usaremos el 8080. Un
nuevo formulario permite editar la configuracin:

277

Buenas prcticas en desarrollo de software con .NET

Pulsamos el botn Save:

A continuacin nos solicita la cuenta con la que ejecutar el entorno.

278

Buenas prcticas en desarrollo de software con .NET

Nos solicita iniciar el servicio del agente de construccin (Build Agent) y el servicio del Servidor TeamCity. Y
acabamos el proceso con la posibilidad de iniciar automticamente un navegador que conecte con TeamCity.

19.2 Gestin de un proyecto


Al iniciarse el navegador, aparecer una pantalla que obliga a aceptar la licencia de uso de TeamCity. Una vez
aceptada, tendremos que registrar el administrador. Esta es la pantalla principal a la que llegamos tras dar
de alta esa cuenta y acceder a http://localhost:8080:

Como TeamCity se ha instalado como servicio Windows, podemos arrancar/detener el servicio de dos
modos:

Manualmente, con el script C:\TeamCity\bin\runAll.bat: con runAll.bat start se inicia y con runAll.bat
stop se detiene.
Con la interfaz grfica de servicios de Windows.

Vamos a la pgina de creacin de proyecto:

279

Buenas prcticas en desarrollo de software con .NET

Hemos introducido el ttulo del proyecto y una breve descripcin del mismo. Tras pulsar Create, el
formulario aade funcionalidad:

Vamos a crear una configuracin para construccin (build configuration). Tras pulsar en el enlace, pasamos a
un nuevo formulario. Vamos a denominar a esta configuracin Compila y corre pruebas, aunque
inicialmente slo compilaremos (ms adelante enriqueceremos la configuracin con la ejecucin de pruebas
unitarias).

280

Buenas prcticas en desarrollo de software con .NET

Hay varios elementos en el formulario. Build number format solicita que demos una plantilla para crear un
identificador de cada vez que se ejecute la construccin automtica. La marca {0} se sustituir por el valor de
un contado de construcciones. El Build counter permite fijar el valor inicial del contador de construcciones.
Pulsamos en el botn VCS Settings y pasamos a un formulario en el que declaramos el sistema de control de
versiones que mantienen nuestro cdigo. La pantalla aparece originalmente as:

281

Buenas prcticas en desarrollo de software con .NET

Ahora seleccionamos Create and attach new VCS root y pasamos a este otro formulario:

En el desplegable que permite fijar el tipo de VCS seleccionamos Mercurial. Se abre entonces un formulario
ms extenso:

La ruta por defecto a la orden hg es vlida por la instalacin que hemos hecho de Mercurial, as que no hay
que tocar nada. Como nuestro repositorio se mantiene en Bitbucket, damos su URL en el campo Pull
changes from:. El valor de Clone repository to: puede mantener el valor por defecto que proporciona
TeamCity. Hemos de suministrar el usuario y password de Bitbucket. El Checking Interval permite fijar cada

282

Buenas prcticas en desarrollo de software con .NET

cuantos segundos se debe consultar el repositorio para ver si ha habido algn cambio que desate un proceso
de construccin.
Podemos asegurarnos de que todo est bien con Test connection.

Cerramos el aviso y pulsamos Save en el formulario New VCS Root. Volvemos entonces al formulario Create
Build Configuration, que presentar este aspecto:

Pasamos a la siguiente pantalla con el botn Add Build Step >>. Se trata de un formulario para dar de alta
el fichero que automatiza la construccin del proyecto. En nuestro caso ser un fichero MSBuild, pero
TeamCity permite trabajar con alternativas como NAnt.

283

Buenas prcticas en desarrollo de software con .NET

En esta pantalla vamos a definir el proceso de compilacin. Seleccionamos como Runner Type la etiqueta
Visual Studio .sln. Los fichero .sln, es decir, los que describen una Solucin de Visual Studio (una agregacin
de proyectos) son ficheros MSBuild.
Al seleccionar el Runner Type que nos interesa, el formulario cambiar para contemplar los campos que son
propios de la compilacin con este tipo de ficheros. Pinchamos en Solution file path para seleccionar el
fichero de solucin y seleccionamos la configuracin de configuracin Debug (podra ser Debug o Release).

284

Buenas prcticas en desarrollo de software con .NET

Salvamos y el navegador vuelve a mostrar la pantalla de la configuracin de construccin:

Podemos programar ahora la accin que disparar la ejecucin. Seleccionamos el paso 4: Build Triggering.
Aparecer esta pantalla:

285

Buenas prcticas en desarrollo de software con .NET

Aadimos un trigger con Add new trigger y seleccionamos el tipo VCS trigger, esto es, disparador por
sistema de control de versiones:

No hay que modificar nada, as que salvamos.


Y ya podemos probar nuestra tarea de construccin, aunque no hemos completado la tarea, que de
momento slo compila y no ejecuta pruebas. Vamos a la pestaa Projects y pulsamos el botn Run

286

Buenas prcticas en desarrollo de software con .NET

Aparecer un testigo de que se est ejecutando el proceso de construccin:

Al acabar la compilacin, aparecer un mensaje indicando el xito del proceso:

El sistema ir ejecutando un proceso de este estilo cada vez que detecte un cambio en el repositorio.
Produzcamos uno artificialmente. Aadamos un fichero y empujemos el cambio al repositorio.
Tras una pausa (el tiempo necesario para que TeamCity consulte el repositorio en busca de cambios),
aparecer el resultado de la nueva compilacin.

Ya tenemos un sistema de integracin continua. Con cada cambio en el repositorio se ejecuta una
compilacin del sistema. Cualquier desarrollador sabr en qu estado est el repositorio antes de descargar
la ltima versin.

287

Buenas prcticas en desarrollo de software con .NET

Introduzcamos un error en uno de los ficheros para ver qu ocurre en ese caso. Tras darlo de alta y esperar
un tiempo (o pulsar inmediatamente Run), aparece un aviso de que hubo un error:

Podemos obtener un informe completo pinchando en el mensaje Compilation failed.

El mensaje es el de error producido por el compilador. Corrijamos el error y nuestra cuarta compilacin ser
exitosa.
Vamos a aadir un paso de construccin que tenemos pendiente: la ejecucin de las pruebas unitarias
Pasamos a la pestaa Administration (arriba a la derecha) y pulsamos en el enlace Edit de la configuracin de
construccin titulada Compila y corre pruebas. En el tercer paso, que est etiquetado como 3 Build Step
Visual Studio (sln) pulsamos y seleccionaos Add Build Step para aadir el nuevo paso de construccin.
Este nuevo paso tendr como Runner Type NUnit. En el desplegable se nos permitir escoger la versin
NUnit (en este caso es la 2.5.9), la versin del runtime de .NET (que fijamos a 4.0) y seleccionamos los
ensamblados con pruebas unitarias en Run tests from. En este proyecto escogemos el ensamblado
Algoritmia\Tests\bin\Debug\Tests.dll.

288

Buenas prcticas en desarrollo de software con .NET

Pulsamos en Save y, en la siguiente pantalla, solicitamos la ejecucin pulsando en el botn Run.

El resultado es un mensaje que nos indica que se superaron las 783 pruebas (Tests passed: 783). Perfecto.
Podemos acceder al detalle si pinchamos en el mensaje Tests passed 783:

289

Buenas prcticas en desarrollo de software con .NET

Y si ahora pinchamos en la pestaa Tests, el listado llega al mximo nivel de detalle:

En My Settings & Tools podemos configurar notificadores:

290

Buenas prcticas en desarrollo de software con .NET

Cuando hay un problema los desarrolladores pueden ser avisados por correo electrnico. Tambin es posible
instalar un notificador de bandeja en Windows que nos avise del estado de cada construccin:

Los entornos actuales de desarrollo siguen procesos de integracin continua con diferentes configuraciones.
Una frecuente es la que hemos presentado, pero otras tienen por objeto generar un paquete de instalacin
para el software. Si las pruebas unitarias se superan con xito, cada noche se genera un paquete con la
ltima construccin nocturna (nightly-build)

20 Scrum y Kanban
Hemos visto muchas herramientas que nos ayudan a desarrollar, pero nos hemos detenido poco a estudiar
metodologas de trabajo eficaces que materialicen la agilidad. Acabamos el curso hablando brevemente de
Scrum y Kanban. La primera es una metodologa para organizar el trabajo en ciclos de 2 a 4 semanas de
modo que se produzca valor en periodos cortos. La segunda, es una tcnica que evita los cuellos de botellas
en procesos de trabajo en cadena. Ambas se alinean con la Agile Project Management Declaration of
Interdependence, un manifiesto que realiz un grupo de expertos en 2005 (accesible en http://pmdoi.org).
Los expertos son David Anderson, Sanjiv Augustine, Christopher Avery, Alistair Cockburn, Mike Cohn, Doug

291

Buenas prcticas en desarrollo de software con .NET

DeCarlo, Donna Fitzgerald, Jim Highsmith, Ole Jepsen, Lowell Lindstrom, Todd Little, Kent McDonald,
Pollyanna Pixton, Preston Smith yRobert Wysocki, y la declaracin reza as:
Somos una comunidad de lderes de proyecto con gran xito en la entrega de resultados. Para
alcanzar esos resultados:
o
o
o
o
o
o

Incrementamos el retorno de inversin centrndonos en el flujo continuo de valor.


Entregamos resultados fiables comprometiendo a los clientes en interacciones frecuentes y
pertenencia compartida.
Esperamos la incertidumbre y las gestionamos con iteraciones, anticipacin y adaptacin.
Liberamos la creatividad y la innovacin reconociendo que los individuos son la fuente de
valor definitiva, y creando un entorno en el que puedan marcar la diferencia.
Disparamos el rendimiento con la rendicin de cuentas ante los resultados y la
responsabilidad compartida para la eficacia del equipo.
Mejoramos la efectividad y la fiabilidad a travs de estrategias, procesos y prcticas
situacionalmente especficos.

20.1 Scrum
Scrum es el trmino que se usa en el mundo anglosajn para referirse a la mel del rugby. Su aplicacin al
desarrollo de productos proviene del artculo The New New Product Development Game (con el subttulo
Stop running the relay race and take up rugby, de Hirotaka Takeuchi e Ikujiro Nonaka.

La idea del Scrum es centrarse en aportar valor rpida y continuadamente. Para ello define una serie de
roles, un mecnica para la especificacin de funcionalidades y ciclos cortos con retroalimentacin.

292

Buenas prcticas en desarrollo de software con .NET

En este texto usaremos las palabras tcnicas en su versin inglesa para no inducir a confusin con
traducciones al castellano que an no cuentan con la tradicin suficiente para que haya un consenso.
20.1.1 Un vistazo rpido
En Scrum hay tres roles y cada persona ejerce uno de ellos:

ScrumMaster: es la persona encargada de que el proceso se aplique y de que el entorno disponga de


las herramientas necesarias.
El Product Owner (propietario del producto): es quien representa a quienes necesitan el producto
software. No es un desarrollador, sino un cliente. Su papel es de interfaz entre los usuarios finales y
Stakeholders (partes implicadas en la necesidad de disponer del producto) y el equipo de trabajo.
Desarrolllador: son todos los que forman el Equipo de Desarrollo (Development Team). El
ScrumMaster suele ser un desarrollador ms.

El proceso empieza con la elaboracin de un Product Backlog, que es una lista de requerimientos priorizada.
Esta lista puede contener todo tipo de elementos (de negocio, tcnicos) pero es preferible limitarla a
requerimientos de negocio. Suele adoptar la forma de User Stories (historias de usuario) y se suele
confeccionar en una o dos sesiones de trabajo (es decir, en uno o dos das).
Entonces se pasa a una reunin de planificacin de un Sprint o Sprint Planning. Un Sprint es un periodo de
entre dos y cuatro semanas (aunque puede ser ms corto o ms largo) en el que el equipo se concentra en
implementar funcionalidad (seleccionando las User Stories apropiadas) que permitirn ofrecer un producto
con valor. A esta reunin debera acudir el Product Owner para que entienda qu va a obtenerse como
resultado y ayude a priorizar las User Stories de modo que el valor entregado sea el ms alto posible. Una
reunin de planificacin suele durar cuatro horas para un Sprint de dos semanas y ocho para un Sprint de
cuatro semanas. La reunin se estructura en dos partes:

Qu: el Product Owner ayuda al equipo a seleccionar las User Stories con realimentacin del equipo.
Estas historias conforma el Sprint Backlog.
Cmo: el equipos de desarrollo descompone la User Stories seleccionadas en Tasks y deducen la
cantidad de tiempo que costar implementar cada Task. Las Tasks se escriben en tarjetas y se
colocan en el Task Board. Hablamos de un panel fsico en el que pegar Post-Its o de software, como
Jira, que gestiona estas tarjetas grficamente. Un objetivo es disponer de una representacin visual
del trabajo que se ha de realizar.

De aqu pasamos al Sprint en s, que es una iteracin de desarrollo que tiene por objeto confeccionar un
producto. El Sprint implica nicamente al ScrumMaster y al Equipo de Desarrollo. Cada da del Sprint es un
Daily Scrum y empieza con un Standup Meeting (reunin en pie). El Standup Meeting es un encuentro
breve en la sala en que se puede visualizar el estado del proyecto. La duracin normal es de unos 15 minutos
y el nombre de reunin en pie refuerza la idea de que es un encuentro rpido. Hay dos informaciones
visuales importantes para la reunin:

El Task Board: es el panel de trabajo en el que se puede visualizar el conjunto de Tasks objeto del
Sprint y su estado: cules estn en el Sprint Backlog (en espera de ser abordadas), cules estn
siendo acometidas en ese instante y por quin, y qu tareas se han finalizado ya.
El Burndown Chart: un grfico que muestra el trabajo realizado contra el tiempo transcurrido.
Permite saber si el ritmo del Sprint es el apropiado.

293

Buenas prcticas en desarrollo de software con .NET

La reunin permite al equipo compartir cualquier problema experimentado durante el Sprint y adoptar
medidas correctivas inmediatamente.

Justo antes de finalizar un Sprint, hay una reunin en la que participan todos, incluido el Product Owner: La
Sprint Review. En la reunin, de duracin similar al Sprint Planning, se pretende:

Que el equipo de desarrollo y el Product Owner discutan qu se hizo y qu no.


Que el equipo de desarrollo demuestre el producto confeccionado y obtenga realimentacin del
Product Owner.
Actualizar el Product Backlog con la experiencia adquirida por el Product Owner con el nuevo
incremento del producto.

Otra reunin importante y que slo concierne al Equipo de Desarrollo y al ScrumMaster es el Sprint
Retrospective, que tiene por objeto reflexionar sobre el funcionamiento del equipo en el Sprint. El objetivo
es obtener propuestas de mejora que ayuden a que el siguiente Sprint sea an mejor.
Y el ciclo empieza de nuevo.

(Imagen de http://www.scrumbrowser.com/#el=SmallScrum/0/HEAD/folder/scr.01)

294

Buenas prcticas en desarrollo de software con .NET

20.1.2 Roles
Ahora que hemos visto el proceso, definamos con ms precisin las responsabilidades de cada rol:

Product Owner:
o Trabajar con los Stakeholders para definir el Product Backlog y ser responsable de los
resultados de negocio.
o Recoger requerimientos para el Product Backlog.
o Colaborar con el ScrumMaster y el Equipo para planificar los Sprints y definir el producto
esperado con cada iteracin.
o Colaborar con el ScrumMaster para proteger al Equipo de molestias externas.
o Guiar al equipo en la consecucin de los objetivos del Sprint.
o Estar informado del progreso del proyecto.
o Estar disponible para dar realimentacin al Equipo.
ScrumMaster:
o Servir de guardin del proceso Scrum.
o Guiar al equipo hacia la consecucin de los objetivos del Sprint.
o Trabajar con el Product Owner para proteger al equipo de molestias externas.
o Ayudar al Esquipo a mantener el ritmo de progreso.
o Organizar la Sprint Retrospective para ayuda al Equipo a mejorar en el proceso y en su
rendimiento.
Equipo de Desarrollo:
o Auto-gestionarse y auto-organizarse.
o Responsabilizarse de las estimaciones de los tems del Sprint Backlog y de las User Stories.
o Convertir User Stories en Tasks que definan actividades que se pueden llevar a cabo.
o Seguir el progreso del proyecto.
o Responsabilidades de la demostracin de los resultados del Sprint al Product Owner y los
Stakeholders al final de cada Sprint.

20.1.3 El Product Backlog


El Product Backlog recoge los requerimientos del producto y los plasma en una serie de objetivos. Es
responsabilidad de los Stakeholders y su representante, el Product Owner, pero su confeccin requiere de la
ayuda del ScrumMaster para que siga un formato digerible por el Equipo.
Antes de empezar se ha de haber identificado a todos los Stakeholders, es decir, personas implicadas en el
producto de un modo u otro.
20.1.3.1 Objetivos
Todo empieza con un establecimiento de objetivos y medidas de xito definidas por los StakeHolders. Los
Stakeholders han de responder a estas preguntas:

Qu objetivos de negocio o metas tiene?


Para qu necesita el nuevo producto de software?
Cmo medira el grado de consecucin de los objetivos?

Los objetivos deben ser SMART:

Specific: todo el mundo ha de entender del mismo modo los objetivos.

295

Buenas prcticas en desarrollo de software con .NET

Measurable: se ha de poder determinar qu objetivos se han alcanzado en cada instante.


Achievable: los Stakeholders estn de acuerdo qu son los objetivos.
Realistic: han de ser alcanzables con los recursos disponibles.
Time-Based: el tiempo disponible ser suficiente para alcanzarlos.

He aqu un ejemplo extrado de Scrum in Action.

20.1.3.2 Product Backlog


El Product Backlog se compone de User Stories priorizadas. En Scrum in Action proponen un proceso de
refinamiento de los objetivos por niveles, pasando del Bosque (el producto), a los rboles (los mdulos que
componen el producto), a las ramas (la funcionalidad estructurada en unidades) y las hojas (las historias de
usuario). Este ejemplo ayuda a entender el refinamiento:

No hay un formato nico para las historias de usuario (las hojas del grfico de pirmide). Un formato posible
es ste:

Ntese el format estructurado del estilo: As a [user/power user/], I can [] *which helps *with goals++.

296

Buenas prcticas en desarrollo de software con .NET

En http://www.mountaingoatsoftware.com/topics/user-stories encontramos algunos ejemplos de User


Stories. Ciertas historias se consideran inapropiadas por demasiado ambiciosas para ser resueltas en un solo
Sprint:

As a user, I can backup my entire hard drive.

Es major que las User Story correspondan a funcionalidades menos y detalladas con ms precisin:

As a power user, I can specify files or folders to backup based on file size, date created, and date
modified.
As a user, I can indicate folders not to backup so that my backup drive isnt filled up with things I
dont need saved.

20.1.3.3 Estimacin y velocidad


El siguiente paso es la estimacin de coste para las historias de usuario. Se usa una medida propia: los Story
Points (puntos de historia). Un Story Point no es nada predefinido y universal: es una medida relativa de
complejidad. La definicin de Story Point depende del equipo de desarrollo, pero es una unidad que todo el
mundo puede entender. Lo habitual es tomar una User Story de referencia, fijarle una puntuacin, digamos
2, y determinar la puntuacin de las dems historias en relacin a esta. Dado que la precisin absoluta es
imposible y una posible fuente de discusin, suele usarse la Cohn Scale (por Mike Cohn), que establece un
conjunto de puntuaciones posibles: 0, , 1, 2, 3, 5, 8, 13, 20, 40, 100. La escala fija niveles separados por una
barrera de +/-50%. (Otra escala comn se basa en los nmeros de Fibonacci: 0, , 1, 2, 3, 5, 8, 13, 21, 34,
89.)
La estimacin es un tema delicado. La estimacin es uno de los puntos dbiles en la planificacin de
software y una de las razones del fracaso del sistema basado en proyectos. Se ha de asumir que el error es
posible pero tenerlo presente para, con la experiencia, reducirlo a un mnimo.
Los puntos de historia son como la distancia al objetivo. Y alcanzar el objetivo requiere recorrer una
distancia a cierta velocidad. La velocidad se puede determinar con mayor precisin despus de los primeros
dos o tres Sprints. De esa experiencia puede surgir el factor das de trabajo por punto de historia.
Hay un problema con la asignacin de puntos: el anclaje, una tendencia a decir una cantidad similar a la
ltima que se ha odo. Como ilustra http://www.crisp.se/planningpoker:

Una de las tcnicas que permiten empezar a estimar costes es el Planning Poker, tcnica propuesta por
James Grenning y popularizada por Mike Cohn. Se juega con una baraja de cartas con puntos en la escala
de Cohn. Esta imagen, de Wikipedia (http://en.wikipedia.org/wiki/File:CrispPlanningPokerDeck.jpg) muestra
la baraja:

297

Buenas prcticas en desarrollo de software con .NET

(La carta con el interrogante significa no tengo ni idea y la taza de caf es una broma de los autores del
juego de cartas (la empresa sueca CRISP AB): demasiado cansado para pensar: descansemos.)
La partida transcurre como sigue. El moderador, que no juega, coordina el proceso. El desarrollador con
ms experiencia en el mbito de la User Story da una visin general y se abre un debate breve. El
ScrumMaster resume las conclusiones. En la discusin est prohibido hablar de puntos para evitar el anclaje
del resultado a los nmeros que se hayan usado en la discusin. Cada desarrollador saca una carta boca
abajo con su estimacin. Los desarrolladores con la estimacin ms alta y ms baja hacen un comentario
argumentando su postura.

Se repite el proceso hasta alcanzar un consenso. Quien tiene ms peso en el establecimiento del consenso
son los desarrolladores a los ms probablemente se asigne la User Story.
La principal utilidad de Planning Poker es pautar las discusiones con un modelo que invita a la brevedad. En
lugar de dedicar media hora a cada User Story, unos poco minutos bastan para la mayora de ellas.
Al finalizar un Sprint se dividir la carga de trabajo en puntos de historia por el tiempo empleado y se
calcular as la velocidad (velocity) de trabajo. Es un dato que ayudar a planificar el siguiente Sprint.
20.1.3.4 Priorizacin
Una vez se ha estimado cada User Story, el Product Owner prioriza las User Story que se pueden acometer
en un Sprint. Necesitar la gua del ScrumMaster para que la cantidad de trabajo sea alcanzable en una
iteracin, pero la responsabilidad ltima de la priorizacin es el Product Owner.
20.1.3.5 El resultado
El resultado del proceso es una relacin de User Story con prioridades y asignacin de User Story a
desarrolladores concretos, que asumen la responsabilidad de seguir con la User Story. En
http://www.mountaingoatsoftware.com/scrum/product-backlog encontramos un ejemplo de resultado
tpico:

298

Buenas prcticas en desarrollo de software con .NET

Ntese que una simple hoja de clculo puede dar soporte al Product Backlog. Herramientas colaborativas
como Google Docs son de gran ayuda para compartir la informacin en el equipo.
Recuerde que el Product Backlog es responsabilidad de todos. No slo el Product Owner crear User Stories,
pero l se asegura de que estn las que se necesitan para que el producto entregue el valor esperado.
20.1.4 Tasks
El siguiente paso es descomponer las User Stories del Sprint hasta llegar al nivel de las Task con las que se
forma el Sprint Backlog. Es un proceso de refinamientos sucesivos en la descomposicin hasta alcanzar
elementos tan sencillos que est claro cmo implementarlo y qu esfuerzo requiere.
Esta imagen de http://www.romanpichler.com/blog/user-stories/decomposing-user-stories/ muestra un
proceso de refinamiento de una User Story:

Las User Stories excesivamente ambiciosas se adjetivan de picas (epic) y normalmente no se pueden
abordar en un solo Sprint. Se requiere su descomposicin en historias que quepan en un Sprint.

299

Buenas prcticas en desarrollo de software con .NET

Cada tarea debe identificarse de algn modo y se le debe asignar una estimacin de esfuerzo (y la suma de
todas las tareas de una User Story debera coincidir con la estimacin que se hizo).
20.1.5 Task Board
En principio, el Task Board se divide en tres columnas:

Not Started
In Progress
Completed.

Y suele haber una fila por User Story en marcha.


De todos modos, puede convenir refinar las columnas, especialmente la In Progress, que puede contar con
varias etapas internas (imagen de http://olemortenamundsen.wordpress.com/2010/03/19/kanban-andscrum-combined/):

Las Task empiezan en la primera columna y van progresando. Es importante que todo el mundo pueda
visualizar qu se est haciendo por parte de quin, as que las tareas llevan asignado uno o ms
desarrolladores (aunque lo normal es que sea slo uno) cuando estn en la columna In Progress (antes no es
necesario hacer una asignacin).
He aqu algunas Task Board:

300

Buenas prcticas en desarrollo de software con .NET

http://www.mountaingoatsoftware.com/scrum/task-boards.

http://20.targetprocess.com/labels/task%20board.html:

http://www.microtool.de/instep/en/prod_scrum_edition.asp:

20.1.6 Burndown Chart


El Burndown Chart es otra herramienta visual necesaria. Es un grfico en el que el eje horizontal representa
el tiempo del Sprint y el vertical, los Story Points del Sprint. Una lnea de referencia parce del da 0 y la suma
de Story Points del Sprint y finaliza en el ltimo da del Sprint con el valor 0. Cada da, el ScrumMaster
actualiza el Burndown Chart.
Algunos ejemplos de Burndown Chart:

301

Buenas prcticas en desarrollo de software con .NET

http://www.microtool.de/instep/en/prod_scrum_edition.asp:

http://www.infoq.com/articles/agile-kanban-boards:

http://www.scrumalliance.org/articles/55:

Es recomendable actualizar el Burndown Chart slo con User Story acabadas, no con Task acabadas. De otro
modo puede priorizarse el desarrollo de tareas en varios frentes y no estar progresando en el objetivo del
Scrum: la entrega de valor.
20.1.7 Daily Scrum (o Standup Meeting)
El objetivo de la reunion, que ha de ser diaria y breve (15 minutos), es responderse a tres preguntas:

Qu he conseguido desde la ltima reunin?


Qu pienso hacer hasta la prxima reunin?
Qu problemas se interponen en el camino?

El Burndown Chart es un instrumentao fundamental para los Daily Scrum, pues son los que ofrecen
informacin visual sobre el progreso real del proyecto.

302

Buenas prcticas en desarrollo de software con .NET

Imgenes de Daily Scrums:

http://www.xqa.com.ar/visualmanagement/2009/04/daily-scrum-against-the-board/:

http://blog.jibjab.com/2009/10/02/daily-scrum/:

http://www.danko.org.il/Who_am_I.htm:

303

Buenas prcticas en desarrollo de software con .NET

http://www.ademiller.com/blogs/tech/2008/07/daily-standup-meetings/:

20.1.8 Scrum Review (o Scrum Demo)


Para clausurar el Sprint se convoca una reunin en la que participan todos, incluido el Product Owner y los
stakeholders: La Sprint Review. En la reunin, de duracin similar al Sprint Planning, se pretende:

Que el equipo de desarrollo y el Product Owner discutan qu se hizo y qu no.


Que el equipo de desarrollo demuestre el producto confeccionado y obtenga realimentacin del
Product Owner y los stakeholders.
Actualizar el Product Backlog con la experiencia adquirida por el Product Owner con el nuevo
incremento del producto.

20.1.9 Retrospective Meeting


Un principio bsico de Scrum es la realimentacin continua. Los Daily Scrum la facilitan da a da, pero es
necesario hacer una revisin global al final de Sprint. El objetivo es responder a:

Qu funcion bien en el Sprint y deberamos seguir haciendo?


Qu no funcion bien y deberamos dejar de hacer?
Qu cosas nuevas deberamos empezar a hacer en el siguiente Sprint?

EL ScrumMaster llevar un registro de las conclusiones de cada retrospectiva, que adoptan la forma de
acciones y moderar la discusin. Las conclusiones se expondrn en un lugar visible y el ScrumMaster mirar
el grado de cumplimiento durante el siguiente Sprint.
Imgenes de resultados de retrospectivas:

304

Buenas prcticas en desarrollo de software con .NET

http://fabiopereira.me/blog/2008/11/23/goal-driven-retrospective/:

http://ducquoc.wordpress.com/2011/03/30/agile-scrum-summary/:

20.2 Kanban
Pongmonos mentalmente en una fbrica de Toyota, en la cadena de produccin.

Estamos en la etapa que monta puertas. Tenemos una pila de 10 puertas. Entra la quinta y la sexta hay una
tarjeta que dice fabricar 10 puertas.

305

Buenas prcticas en desarrollo de software con .NET

Inmadiatamente llevamos la tarjeta a la unidad encargada de producir puertas, que se pone a preparar el
pedido justo ahora que sabe que se van a necesitar ms puertas y justo con el tiempo medido para
entregarlas cuando quien las necesita se haya quedado sin puertas que montar. En el peor caso, nos
quedaremos con 15 puertas de excedente de stock. No ms.
Kanban significa tarjeta (ban) visual (kan) y es una tecnologa que nace en Toyota, como forma de agilizar
la construccin de coches con produccin Just In Time. El objetivo es doble: no producir ms de lo necesario
y no dejar que los cuellos de botella dejen a gente ociosa en etapas que se nutren de sus salidas. Se enmarca
en los procesos de fabricacin ligeros (Lean Manufacturing). No es una metodologa para desarrollo de
software, sino una tcnica de control del flujo de trabajo que puede aplicarse exitosamente a Scrum (y a
otras metodologas).

La saturacin de las cadenas de produccin suele producirse por el tamao de los paquetes de trabajo
(bacth). Los paquetes excesivamente grandes conducen a tiempos de respuesta ms elevados. Es preferible
la concentracin en unas pocas cosas que la multitarea en muchas cosas. Como muestra esta imagen de
http://www.jfokus.se/jfokus10/preso/jf-10_KanbanALeanApproachToAgileSoftwareDevelopment.pdf, la
multitarea produce mayores tiempos de respuesta. Cuantas ms tareas se llevan en marcha
simutneamente, ms aumenta el tiempo de produccin.

306

Buenas prcticas en desarrollo de software con .NET

Qu hace que se entre en multitarea? El intento de optimizar el uso de los recursos anima a concentrarse
en empezar muchas actividades a la vez en lugar de concentrarse en terminarlas. Es ms sensato
concentrarse en una sola actividad, y usar mejor los recursos humanos. Para garantizar ciclos de respuesta
rpidos, las tareas han de ser breves.
Kanban pretende evitar estos problemas. No es Scrum, pero comparte algunos elementos con Scrum. En
particular, la descomposicin del problema en unidades de trabajo abordables en un plazo de tiempo corto y
la visualizacin del proceso en un panel.
20.2.1 Visualizar: el Kanban Task Board
El primer paso para llegar a tiempos de respuesta cortos es visualizar la informacin. Es ah donde entra
Scrum, que ya basa la organizacin del proceso en informacin visual.
El Task Board de Kanban agrega algunos elementos (imagen de
http://www.agileproductdesign.com/blog/2009/kanban_over_simplified.html):

Los nmeros que hay debajo de cada etapa son un lmite al nmero de tareas que puede haber
simultneamente en ella. Y es que un objetivo de Kandan es controlar el WIP (Work in Progress) limitndolo.

307

Buenas prcticas en desarrollo de software con .NET

20.2.2 Limitar el WIP


Lo siguiente es limitar el WIP (Work In Progress). El objetivo es limitar la cantidad de trabajo que entra a
nuestra capacidad para absorberlo:

Para que se absorba la carga de trabajo se pasa de un modelo basado en empujar a otro basado en
estirar:

Esto supone, por ejemplo, no hacer ms trabajo que el estrictamente necesario, algo que ya se haba
manifestado como un buen principio de diseo de software: You Aint Gonna Need It!
Se produce a demanda del extremo final de la cadena, no en funcin de la entrada, que est fuera de
control.
20.2.3 Ms sobre el Kanban Task Board
Sigamos viendo los elementos que conforman el Task Board:

308

Buenas prcticas en desarrollo de software con .NET

En la parte izquierda hay una columna Golas (metas). Es una relacin de los grandes objetivos en los que
estamos trabajando. El objetivo es que todo el mundo las visualice y se evite as aadir basura al resto de
columnas.
La segunda columna, Stories Queue (cola de historias), son las User Stories que estn listas para empezar. Es
una cola porque la historia de ms arriba es la siguiente en entrar en el proceso.
Las siguientes columnas representan el proceso de avance en la implementacin, hasta llegara Done (o
Completed).
Hay una pista abajo que recibe el nombre de expedite. Es una pista rpida, para asuntos extremadamente
urgentes. Cuando una tarea entre en la pista expedite, adquiere la mxima prioridad instantneamente.
Ntese que Kanban no tiene un paso que rompa una User Story en tareas: opta por crear User Stories ms
pequeas, que no necesiten esa descomposicin adicional. Es lo que se denomina Minimal Marketable
Feature (MMF) en la terminologa de la fabricacin ligera (Lean Manufacturing).
20.2.4 Medir y optimizar el flujo
Cmo se determinan los lmites al WIP? En primer lugar se decide un nmero total de tems que puede
haber simultneamente en el panel. Una buena regla del pulgar consiste en dividir por dos el tamao del
grupo de desarrollo. Esto obliga a los desarrolladores a trabajar juntos en las historias.
La Story Queue debera tener una capacidad de un medio de la capacidad de la zona in progress. A
continuacin se debe imponer un lmite al nmero de actividades que pueden estar en una etapa
simultneamente. En el panel de la imagen anterior los Kanban son las cintas azules, de las que hay una por
tarea aceptable en cada etapa.
Se debe medir el flujo de trabajo y estimar el tiempo medio de flujo de la entrada a la salida de cada tarea.
Cuando una tarea entra en la cadena se anota la fecha de entrada, cuando pasa a in progress, se anota la
fecha de inicio, y cuando pasa a Done, se anota nuevamente la fecha. Se denomina cycle time al tiempo
entre la entrada y la salida, lo que incluye el tiempo de espera en cola. El promedio de cycle times permite
estimar la fecha de entrega de otras historias. Se denomina working cycle time al tiempo entre el inicio del
trabajo en la tarea hasta su complecin.

309

Buenas prcticas en desarrollo de software con .NET

En el panel Kanban se anota, en la columna de Story Queue, el working cycle time en la zona superior, y el
cycle time en la inferior. De ese modo se puede predecir el tiempo esperado de salida de una tarea cuando
entra en la cola y cuando sale de ella.
El objetivo de todo este proceso es conseguir que el tiempo necesario para completar un elemento sea
pequeo y predecible. En cualquier caso, determinar los lmites WIP es un arte y requiere de un proceso de
prueba y error.

20.2.5 Un escenario Kanban


Empezamos con un Backlog repleto de historias y un panel Kanban que limita a dos las historias de la Story
Queue y a 3 las que puede haber en desarrollo. La ltima columna corresponde a explotacin.

Se escogen dos historias para la cola Next:

310

Buenas prcticas en desarrollo de software con .NET

La historia A pasa a desarrollo.

Tambin B entra en desarrollo. A se completa y C y D pasan a la cola.

El equipo de produccin ve que A ha sido completado y la pasa a produccin. Los desarrolladores completan
B y trabajan en C:

311

Buenas prcticas en desarrollo de software con .NET

Este es el ritmo natural de trabajo.


20.2.6 Un escenario problemtico
Supongamos ahora que se produce un problema. Veamos qu ocurre paso a paso. En este grfico se
muestran los recursos humanos: el Product Owner, cuatro desarrolladores y dos personas en explotacin.

El Product Owner selecciona dos MMF y los pone en la cola:

Los desarrolladores, en grupos de dos, abordan las tareas. El Product Owner selecciona otras dos tareas.

312

Buenas prcticas en desarrollo de software con .NET

Tan pronto la historia A est completada, explotacin detecta que hay trabajo para desplegar y se encarga
de la tarea. Entonces detectan que A da problemas (no compila, o no pasa las pruebas unitarias, por
ejemplo).

Mientras, la tarea B se completa y pasa a produccin. Los tcnicos de produccin se dividen entre A y B. Pero
ahora tenemos un recurso ocioso: dos desarrolladores. Deberan asumir empezar a trabajar en la tarea D?
No: si lo hiciesen superaran el lmite WIP, que es de 3 actividades.

Los dos desarrolladores pueden ayudar a la gente de explotacin gracias al lmite WIP. De este modo se
ayuda a resolver el problema que plantea la generacin de un cuello de botella en el paso a produccin.
Supongamos que tambin B da problemas y que la tarea C se complet. Tenemos a dos desarrolladores que
no deben asumir ms trabajo nuevo.

313

Buenas prcticas en desarrollo de software con .NET

Deben ayudar a desatascar el trabajo en progreso.

Cuando se solucionan los problemas con A y B, el equipo puede reorganizarse y seguir con la actividad
normal.

314

Buenas prcticas en desarrollo de software con .NET

20.2.7 Ms informacin visual


El tablero Kanban debe recoger toda la informacin que represente el estado real del flujo de trabajo. Es una
herramienta de comunicacin para el equipo, pero tambin para los gestores de la empresa, que pueden
entender rpidamente el estado del proyecto.

315

Buenas prcticas en desarrollo de software con .NET

20.2.8 Realimentacin
Dado que no hay Sprints en Kanban, se ha ser metdico para establecer puntos de realimentacin. Es
recomendable hacer una retrospectiva cada cierto nmero de semanas. Tambin conviene fijar fechas de
demostracin, que obliguen a tomar en serio el proceso de priorizacin para orientarlo a productos
completos tan pronto sea posible.

20.3 Scrum vs Kanban


Hay ciertos puntos de conflicto entre Scrum y Kanban. Scrum prescribe el trabajo con tres roles diferentes y
Kanban no. Scrum prescribe trabajar en ciclos acotados temporalmente y Kanban se preocupa ms del flujo
continuo.

Ambos sistemas limitan de un modo u otra el WIP. En Scrum, el lmite lo impone la seleccin de tareas para
el Sprint. En Kanban, el lmite se explicita en cada etapa.
Scrum no acepta fcilmente el cambio a mitad de Sprint, por lo que en entornos con un alto nivel de
interrupciones puede resultar problemtico. Kanban es ms adaptativo al cambio continuo.
El Task Board de Scrum se vaca completamente al finalizar el Sprint. En Kanban el panel est
constantemente lleno. No hay etapas que acaben en un instante dado.
Scrum prescribe equipos de personas sin especialistas, cuando Kanban admite mejor la especializacin:

316

Buenas prcticas en desarrollo de software con .NET

Scrum requiere que conozcamos la estimacin de carga y la velocidad de trabajo para predecir fiablemente
la entrega de valor.
Kanban hace estimaciones de esfuerzo ms groseras:

En Scrum se estima en punto de historia que, combinados con la velocidad, proporcionan la estimacin en
das.

Scrum prescribe un Backlog priorizado por valor de negocio, que en Kanban es opcional. Los cambios del
Backlog no afectan al Sprint en Scrum, mientras que en Kanban pueden tomarse en cuenta tan pronto hay
capacidad disponible.
Scrum prescribe los Burndown Charts. En Kanban no hay Sprint as que no hay grfico de progreso que parta
de una carga y tenga por objeto llegar a cero. Se puede, no obstante, usar un diagrama de flujo acumulativo:

317

Buenas prcticas en desarrollo de software con .NET

No hay una forma nica de combinar Scrum y Kanban. La idea es escoger aquello que ms convenga a
nuestro entorno de trabajo.
Se pueden consultar http://leansoftwareengineering.com/ksse/scrum-ban/ para tomar ideas acerca de la
combinacin de ambas tcnicas.

20.4 Crditos y recursos

Buena parte del discurso de esta seccin se ha creado siguiendo Scrum in Action, libro escrito por
Andrew Phan y Phuong-Van Pham.
En la entrada de blog http://www.scrum-breakfast.com/2008/02/explaining-story-points-tomanagement.html hay una discusin interesante sobre puntuacin de historias. La discusin sigue
en http://www.scrum-breakfast.com/2008/02/more-on-selling-story-points-to.html.
El trabajo donde se present Planning Poker est disponible en
http://renaissancesoftware.net/files/articles/PlanningPoker-v1.1.pdf.
La pgina http://www.crisp.se/planningpoker contiene material relacionado con Planning Poker.
Se puede adquirir un juego de carta de Planning Poker en
http://www.agile42.com/cms/pages/poker/ y en
http://www.agilehardware.com/categories/Planning-Poker-Cards/.
Mike Cohn mantiene una pgina web para hacer Planning Poker online:
http://www.planningpoker.com/. Es de ayuda para equipos distribuidos.
Hay informacin interesante en http://www.romanpichler.com/blog/user-stories/decomposinguser-stories/.
El blog de Xavier Quesada es una referencia internacional en sistemas de gestin que, como Scrum,
se basan en el uso de informacin visual: http://www.xqa.com.ar/visualmanagement/. Da muchas
ideas sobre cmo implementar paneles fsicos para visualizar informacin y cmo mejorar los
paneles clsicos de Scrum y Kanban.
Hay una buena comparacin en Scrum y Kanban em http://www.crisp.se/henrik.kniberg/Kanban-vsScrum.pdf.
En https://scrumy.com/demo hay software para gestionar un panel Scrum/Kanban
electrnicamente. La versin bsica es gratuita y la profesional cuesta 60 dlares ao. Hay otros
productos en esta lnea: http://agilezen.com/, http://www.pivotaltracker.com/,
http://www.axosoft.com/ontime, http://www.bananascrum.com/, https://www.seenowdo.com,
http://scrumie.cjb.net/ http://www.projectcards.com, http://www.versionone.com/,
http://xplanner.org/, http://www.scrumdesk.com/, http://kanbantool.com/,

318

Buenas prcticas en desarrollo de software con .NET

http://leankitkanban.com/, . La solucin profesional con ms prestigio es JIRA con el plugin


GreenHopper (http://confluence.atlassian.com).

Si tu corazn se ata a algo, tu espada no estar libre.


Miyamoto Mushashi, Samurai del siglo XVII.

Contenido
1

Aprenda a programar en cuntas horas? ............................................................................................................ 1


1.1 El modelo Dreyfus de adquisicin de habilidades ......................................................................................................................... 1
1.1.1
Principiante........................................................................................................................................................................ 2
1.1.2
Principiante avanzado ....................................................................................................................................................... 2
1.1.3
Competente ....................................................................................................................................................................... 2
1.1.4
Eficiente ............................................................................................................................................................................. 3
1.1.5
Experto .............................................................................................................................................................................. 3
1.1.6
El nivel esperado de un curso de formacin ...................................................................................................................... 3
1.2 La regla de las 10.000 horas .......................................................................................................................................................... 3
1.2.1
Aprenda a programar en 10 aos ...................................................................................................................................... 5
1.2.2
Coding Dojo ....................................................................................................................................................................... 6

Metodologas y tecnologas .................................................................................................................................. 7


2.1
2.2
2.3
2.4
2.5

Metodologas pesadas y metodologas ligeras .............................................................................................................................. 8


Metodologas giles .................................................................................................................................................................... 10
El manifiesto gil ......................................................................................................................................................................... 11
Los principios de la agilidad ......................................................................................................................................................... 13
Conjuntos de prcticas con nombre propio ................................................................................................................................ 14

Principios de diseo de software .........................................................................................................................17


3.1 Open Closed Principle (OCP) ....................................................................................................................................................... 17
3.2 Liskov Substitution Principle (LSP) ............................................................................................................................................... 18
3.3 Dependency Inversion Principle (DIP) ......................................................................................................................................... 18
3.4 Interface Segregation Principle (ISP) ........................................................................................................................................... 19
3.5 Single Responsibility Principle (SRP) ............................................................................................................................................ 19
3.6 Release/Reuse Equivalency Principle (REP) ................................................................................................................................. 20
3.7 Common Closure Principle (CCP) ................................................................................................................................................ 20
3.8 Common Reuse Principle (CRP) ................................................................................................................................................... 20
3.9 Acyclic Dependencies Principle (ADP) ......................................................................................................................................... 20
3.10 Stable Dependencies Principle (SDP) .......................................................................................................................................... 21
3.11 Stable Abstractions Principle (SAP) ............................................................................................................................................. 22
3.12 Law of Demeter for Functions/Methods (LoD-F/M) o Least Knowledge Principle (LKP) ............................................................. 23
3.13 Dont Repeat Yourself (DRY) o Duplication is Evil (DE) ................................................................................................................ 23

319

Buenas prcticas en desarrollo de software con .NET

3.14 You Aint Gonna Need It (YAGNI) ................................................................................................................................................ 24


3.15 Keep It Simple, Stupid! (KISS) ...................................................................................................................................................... 24
3.16 Los principios SOLID .................................................................................................................................................................... 24
3.17 Crditos y recursos de esta seccin............................................................................................................................................. 25

Patrones de diseo..............................................................................................................................................26
4.1 Patrones de diseo en ingeniera del software ........................................................................................................................... 27
4.2 El patrn de diseo Decorator .................................................................................................................................................... 30
4.2.1
Un ejemplo: procesadores de cadenas ............................................................................................................................ 31
4.2.2
El patrn Decorator ......................................................................................................................................................... 35
4.2.3
Un ejemplo de Decorator en el mundo real: la librera de flujos de entrada/salida ................................................. 40
4.2.4
Ejercicio ........................................................................................................................................................................... 42
4.3 El patrn Adapter ........................................................................................................................................................................ 43
4.4 Abstract Factory .......................................................................................................................................................................... 44
4.5 Singleton ..................................................................................................................................................................................... 47
4.6 Observer ...................................................................................................................................................................................... 50
4.7 Un estilo: Fluent Interface ........................................................................................................................................................... 53
4.8 Builder ......................................................................................................................................................................................... 54
4.9 Unas reflexiones finales .............................................................................................................................................................. 56
4.10 Crditos y recursos ...................................................................................................................................................................... 57

Reflexin .............................................................................................................................................................58
5.1.1
GetType y typeof ............................................................................................................................................................. 58
5.1.2
GetMethods, GetMembers ............................................................................................................................................. 59
5.1.3
InvokeMember ................................................................................................................................................................ 60
5.2 Creacin dinmica de objetos ..................................................................................................................................................... 61

Atributos .............................................................................................................................................................62
6.1 Uso de atributos .......................................................................................................................................................................... 62
6.2 Definicin de atributos de usuario .............................................................................................................................................. 64

Testeo unitario con NUnit y TDD .........................................................................................................................65


7.1 xUnit ............................................................................................................................................................................................ 67
7.2 Instalacin de NUnit .................................................................................................................................................................... 67
7.3 Primeros pasos ............................................................................................................................................................................ 68
7.3.1
Un accesorio de pruebas ................................................................................................................................................. 68
7.3.2
Ejecucin de las pruebas ................................................................................................................................................. 70
7.4 La clase Assert ............................................................................................................................................................................. 75
7.4.1
IsTrue/IsFalse ................................................................................................................................................................... 75
7.4.2
AreEqual/AreNotEqual/AreSame/AreNotSame .............................................................................................................. 75
7.4.3
Greater/Less .................................................................................................................................................................... 75
7.4.4
Contains ........................................................................................................................................................................... 75
7.4.5
IsInstanceOfType/IsNotInstanceOfType .......................................................................................................................... 75
7.4.6
IsAsssignableFrom/IsNotAssignableFrom ........................................................................................................................ 75
7.4.7
IsNull/IsNotNull ............................................................................................................................................................... 75
7.4.8
IsNaN ............................................................................................................................................................................... 76
7.4.9
Para cadenas.................................................................................................................................................................... 76
7.4.10 Utilidades ......................................................................................................................................................................... 76
7.5 Atributos ..................................................................................................................................................................................... 76
7.5.1
[TestFixture] clase............................................................................................................................................................ 76
7.5.2
[Test] mtodo .................................................................................................................................................................. 76
7.5.3
[Setup] mtodo ............................................................................................................................................................... 76
7.5.4
[Teardown] mtodo......................................................................................................................................................... 76
7.5.5
[TestFixtureSetup] mtodo.............................................................................................................................................. 76
7.5.6
[TestFixtureTeardown] mtodo ....................................................................................................................................... 77
7.5.7
[ExpectedException(Type type)] mtodo ........................................................................................................................ 77
7.5.8
[Platform(string s)] clase o mtodo ................................................................................................................................. 77
7.5.9
[Category(string s)] clase o mtodo ................................................................................................................................. 77

320

Buenas prcticas en desarrollo de software con .NET

7.5.10 [Explicit] clase o mtodo ................................................................................................................................................. 77


7.5.11 [Ignore] clase o mtodo................................................................................................................................................... 77
7.5.12 [Combinatorial] mtodo y [Values] o [Range] parmetro ............................................................................................... 77
7.5.13 [Sequential] mtodo y [Values] o [Range] parmetro ..................................................................................................... 78
7.5.14 [Timeout(int ms)] mtodo ............................................................................................................................................... 78
7.5.15 [Maxtime(int ms)] mtodo .............................................................................................................................................. 78
7.6 Desarrollo guiado por pruebas (Test Driven Development) ........................................................................................................ 78
7.7 Ejecucin de pruebas .................................................................................................................................................................. 79
7.8 Una buena prctica: nombrado de los mtodos de prueba ........................................................................................................ 79
7.9 Un ejemplo de TDD con NUnit .................................................................................................................................................... 80
7.10 Ejercicio: orden natural ............................................................................................................................................................... 96
7.11 Ejercicio: el juego de la vida ........................................................................................................................................................ 97
7.12 Una herramienta integrable en Visual Studio ............................................................................................................................. 98
7.13 Crditos y recursos .................................................................................................................................................................... 100

Dobles de prueba ..............................................................................................................................................100


8.1 Nuestra aplicacin de ejemplo: un lector de RSS ...................................................................................................................... 101
8.2 Aplicacin RssReaderApp y librera Rss ..................................................................................................................................... 103
8.3 TDD104
8.4 Depuracin con NUnit ............................................................................................................................................................... 109
8.5 Hemos violado el principio de diseo SRP................................................................................................................................. 111
8.5.1
Refactorizando .............................................................................................................................................................. 112
8.5.2
Eliminando dependencias .............................................................................................................................................. 115
8.6 Qu queremos probar realmente de Rss.Reader? Introduciendo los dobles de prueba ........................................................ 116
8.7 Mocks con Moq ........................................................................................................................................................................ 118
8.7.1
Instalacin de Moq ........................................................................................................................................................ 118
8.7.2
Primeros pasos con Moq ............................................................................................................................................... 118
8.8 Aadiendo un cargador de contenido por HTTP ....................................................................................................................... 121
8.9 Pruebas con clculos lentos ...................................................................................................................................................... 123
8.10 Verificacin de expectativas ...................................................................................................................................................... 126
8.10.1 Verificacin del nmero de llamadas ............................................................................................................................ 126
8.10.2 Verificacin con control relajado de parmetros .......................................................................................................... 128
8.10.3 Una dependencia ms que eliminar y control de propiedades ..................................................................................... 129
8.11 Revisando la privacidad de las clases ........................................................................................................................................ 132
8.12 Una refactorizacin ................................................................................................................................................................... 134
8.13 Ms sobre Moq ......................................................................................................................................................................... 138
8.13.1 Acceso a argumentos en el valor de retorno ................................................................................................................. 138
8.13.2 Lanzamiento de excepciones cuando se llama a la funcin .......................................................................................... 138
8.13.3 Evaluacin perezosa del valor de retorno ..................................................................................................................... 138
8.13.4 Retrollamadas (callbacks) .............................................................................................................................................. 139
8.13.5 Devolucin de valores diferentes para diferentes invocaciones a un mtodo con los mismos argumentos ................ 139
8.13.6 Acceso y asignacin de propiedades ............................................................................................................................. 139
8.13.7 Indicar que una propiedad se comporte como un stub ................................................................................................. 140
8.13.8 Indicar que todas las propiedades deben comportase como un stub ........................................................................... 140
8.13.9 Lanzamiento de eventos................................................................................................................................................ 140
8.13.10 Verificacin de que se ha accedido a una propiedad .................................................................................................... 140
8.13.11 Verificacin de que se ha asignado valor a una propiedad ........................................................................................... 140
8.14 Antes de acabar ......................................................................................................................................................................... 141
8.15 Un hecho (no tan) inesperado ................................................................................................................................................... 141

Configuracin de aplicacin va fichero de configuracin ..................................................................................144


9.1 Creacin de un fichero App.Config ............................................................................................................................................ 144
9.2 Secciones definidas por el usuario ............................................................................................................................................ 145
9.2.1
Definicin de una seccin propia con atributos de usuario ........................................................................................... 145
9.2.2
Un atributo con tipo definido por el usuario ................................................................................................................. 147
9.2.3
Secciones con elementos anidados ............................................................................................................................... 149
9.2.4
Secciones con elementos de tipo coleccin .................................................................................................................. 151

321

Buenas prcticas en desarrollo de software con .NET

10

Registro de actividad .........................................................................................................................................155


10.1 System.Diagnostics .................................................................................................................................................................... 155
10.2 Instalacin de Log4Net .............................................................................................................................................................. 156
10.3 Lo bsico.................................................................................................................................................................................... 156
10.4 Un primer ejemplo .................................................................................................................................................................... 157
10.5 Configuracin XML .................................................................................................................................................................... 158
10.6 Appenders ................................................................................................................................................................................. 160
10.6.1 ConsoleAppender/ColorConsoleAppender ................................................................................................................... 160
10.6.2 DebugAppender/TraceAppender .................................................................................................................................. 161
10.6.3 FileAppender ................................................................................................................................................................. 162
10.6.4 RollingFileAppender ...................................................................................................................................................... 162
10.6.5 Mltiples appenders ...................................................................................................................................................... 162
10.7 Layouts ...................................................................................................................................................................................... 163
10.8 Configuracin jerrquica de Loggers ......................................................................................................................................... 165
10.9 Contextos .................................................................................................................................................................................. 165
10.10 Filtros.................................................................................................................................................................................. 166
10.10.1 LoggerMatchFilter ......................................................................................................................................................... 166
10.10.2 LevelMatchFilter ............................................................................................................................................................ 166
10.10.3 LevelRangeFilter ............................................................................................................................................................ 167
10.10.4 StringMatchFilter ........................................................................................................................................................... 167
10.10.5 PropertyFilter ................................................................................................................................................................ 167
10.10.6 DenyAllFilter .................................................................................................................................................................. 167
10.11 Un ejemplo de uso ............................................................................................................................................................. 167
10.12 Buenas prcticas................................................................................................................................................................. 171

11

Serializacin ......................................................................................................................................................171
11.1 Serializador XML ........................................................................................................................................................................ 172
11.1.1 Un ejemplo sencillo ....................................................................................................................................................... 172
11.1.2 Proteccin de propiedades y campos ............................................................................................................................ 174
11.2 Serializador basado en contratos .............................................................................................................................................. 175
11.2.1 Formateadores .............................................................................................................................................................. 175
11.2.2 Uso del serializador ....................................................................................................................................................... 175
11.2.3 Un asunto avanzado: declaracin de espacios de nombres ....................................................................................... 177
11.2.4 Serializacin de referencias ........................................................................................................................................... 178
11.2.5 Clonacin profunda ....................................................................................................................................................... 182

12

Inyeccin de dependencias ...............................................................................................................................183


12.1 Inyeccin (manual) de dependencias ........................................................................................................................................ 184
12.1.1 Inyeccin de dependencias por constructor ................................................................................................................. 185
12.1.2 Inyeccin por propiedades ............................................................................................................................................ 186
12.1.3 Inyeccin con un Builder ............................................................................................................................................... 186
12.1.4 Inyeccin mediante factoras abstractas ....................................................................................................................... 187
12.1.5 Inyeccin mediante localizador de servicios ................................................................................................................. 188
12.2 El principio de Hollywood.......................................................................................................................................................... 189
12.3 Libreras para Inyeccin de Dependencias ................................................................................................................................ 189

13

Castle Windsor ..................................................................................................................................................190


13.1 Instalacin ................................................................................................................................................................................. 191
13.2 Patrn de uso de un contenedor Castle Windsor ..................................................................................................................... 191
13.2.1 Register .......................................................................................................................................................................... 191
13.2.2 Resolve .......................................................................................................................................................................... 192
13.2.3 Release .......................................................................................................................................................................... 192
13.3 Una API con interfaz fluida ........................................................................................................................................................ 193
13.3.1 Registro elemento a elemento ...................................................................................................................................... 193
13.3.2 Dependencias en lnea................................................................................................................................................... 195
13.3.3 Registro por convenio .................................................................................................................................................... 196
13.3.4 Registro condicional ...................................................................................................................................................... 197

322

Buenas prcticas en desarrollo de software con .NET

13.4 Proceso interno de resolucin de dependencias ...................................................................................................................... 198


13.5 Configuracin con app.config (o, en general, con fichero XML) ................................................................................................ 198

14

Aspect Oriented Programming ..........................................................................................................................199


14.1 Una solucin basada en el patrn Proxy ................................................................................................................................... 199
14.1.1 Castle DynamicProxy ..................................................................................................................................................... 200
14.2 AOP con Castle Windsor ............................................................................................................................................................ 202
14.2.1 Un ejemplo til: cach de llamadas ............................................................................................................................... 206
14.2.2 Otro ejemplo: Logging con inters ortogonal ................................................................................................................ 209

15

MSBuild .............................................................................................................................................................211
15.1 Un fichero de definicin de proyecto ........................................................................................................................................ 211
15.2 Estructura de los ficheros de proyecto...................................................................................................................................... 216
15.3 Invocacin directa de msbuild................................................................................................................................................... 217
15.4 Propiedades .............................................................................................................................................................................. 218
15.5 tems ......................................................................................................................................................................................... 219
15.5.1 Metadatos ..................................................................................................................................................................... 220
15.5.2 Metadatos de usuario.................................................................................................................................................... 220
15.6 Condiciones ............................................................................................................................................................................... 221
15.7 Objetivos por defecto ................................................................................................................................................................ 221
15.8 La orden msbuild ....................................................................................................................................................................... 221
15.9 Propiedades dinmicas ............................................................................................................................................................. 222
15.10 Tareas ................................................................................................................................................................................. 222
15.10.1 Copy ............................................................................................................................................................................... 224
15.11 MSBuild Extension Pack ..................................................................................................................................................... 224
15.11.1 Instalacin ..................................................................................................................................................................... 224
15.11.2 Documentacin ............................................................................................................................................................. 224
15.11.3 NUnit ............................................................................................................................................................................. 225
15.11.4 Email .............................................................................................................................................................................. 226
15.11.5 Twitter ........................................................................................................................................................................... 227
15.11.6 Zip .................................................................................................................................................................................. 227
15.11.7 Sound ............................................................................................................................................................................. 228
15.12 MSBuild Explorer ................................................................................................................................................................ 229
15.13 Referencias ......................................................................................................................................................................... 229

16

Instaladores ......................................................................................................................................................229
16.1 Un instalador de ejemplo .......................................................................................................................................................... 230
16.2 Un parntesis ............................................................................................................................................................................ 232
16.3 Seguimos con el ejemplo........................................................................................................................................................... 234

17

Sistemas de control de versiones distribuidos ...................................................................................................237


17.1 Mercurial ................................................................................................................................................................................... 238
17.1.1 Interfaces de usuario ..................................................................................................................................................... 238
17.1.2 Instalacin ..................................................................................................................................................................... 238
17.1.3 Configuracin ................................................................................................................................................................ 239
17.1.4 Conceptos previos ......................................................................................................................................................... 239
17.1.5 Una sesin de ejemplo .................................................................................................................................................. 240
17.2 Tutorial ...................................................................................................................................................................................... 241
17.2.1 Configuracin ................................................................................................................................................................ 242
17.2.2 Creacin de un proyecto y su repositorio ................................................................................................................... 242
17.2.3 Adicin de un fichero ..................................................................................................................................................... 243
17.2.4 Consigna de cambios ..................................................................................................................................................... 244
17.2.5 Registro de cambios ...................................................................................................................................................... 246
17.2.6 Cambios ......................................................................................................................................................................... 247
17.2.7 Vuelta atrs ................................................................................................................................................................... 248
17.2.8 Analizando los cambios ................................................................................................................................................. 250
17.2.9 Borrado de un fichero.................................................................................................................................................... 251
17.2.10 Viajando en el tiempo.................................................................................................................................................... 253

323

Buenas prcticas en desarrollo de software con .NET

17.2.11 Publicacin del repositorio con un servidor .................................................................................................................. 254


17.2.12 Mantener la sincrona .................................................................................................................................................... 255
17.2.13 Un resbaln ................................................................................................................................................................... 258
17.2.14 Repositorio central y flujo de trabajo ............................................................................................................................ 261
17.2.15 Conflictos ....................................................................................................................................................................... 262
17.2.16 El da a da...................................................................................................................................................................... 265
17.2.17 Colas de parches ............................................................................................................................................................ 265
17.3 Integracin con Visual Studio .................................................................................................................................................... 266
17.3.1 Configuracin de Visual Studio 2010 ............................................................................................................................. 266
17.3.2 Alta del sistema de control de versiones para una solucin .......................................................................................... 266
17.3.3 Un ejemplo .................................................................................................................................................................... 269
17.4 Referencias y recursos............................................................................................................................................................... 270

18

Seguimiento de errores .....................................................................................................................................271


18.1 Bitbucket ................................................................................................................................................................................... 271
18.1.1 Creacin de una cuenta y un repositorio ....................................................................................................................... 271
18.1.2 Alta de contenido .......................................................................................................................................................... 272
18.1.3 Algunos elementos de la interfaz .................................................................................................................................. 273
18.1.4 Issues ............................................................................................................................................................................. 274

19

Integracin Continua .........................................................................................................................................276


19.1 Instalacin ................................................................................................................................................................................. 277
19.2 Gestin de un proyecto ............................................................................................................................................................. 279

20

Scrum y Kanban ................................................................................................................................................291


20.1 Scrum ........................................................................................................................................................................................ 292
20.1.1 Un vistazo rpido ........................................................................................................................................................... 293
20.1.2 Roles .............................................................................................................................................................................. 295
20.1.3 El Product Backlog ......................................................................................................................................................... 295
20.1.4 Tasks .............................................................................................................................................................................. 299
20.1.5 Task Board ..................................................................................................................................................................... 300
20.1.6 Burndown Chart ............................................................................................................................................................ 301
20.1.7 Daily Scrum (o Standup Meeting) .................................................................................................................................. 302
20.1.8 Scrum Review (o Scrum Demo) ..................................................................................................................................... 304
20.1.9 Retrospective Meeting .................................................................................................................................................. 304
20.2 Kanban ...................................................................................................................................................................................... 305
20.2.1 Visualizar: el Kanban Task Board ................................................................................................................................... 307
20.2.2 Limitar el WIP ................................................................................................................................................................ 308
20.2.3 Ms sobre el Kanban Task Board ................................................................................................................................... 308
20.2.4 Medir y optimizar el flujo .............................................................................................................................................. 309
20.2.5 Un escenario Kanban ..................................................................................................................................................... 310
20.2.6 Un escenario problemtico ........................................................................................................................................... 312
20.2.7 Ms informacin visual .................................................................................................................................................. 315
20.2.8 Realimentacin .............................................................................................................................................................. 316
20.3 Scrum vs Kanban ....................................................................................................................................................................... 316
20.4 Crditos y recursos .................................................................................................................................................................... 318

324

Das könnte Ihnen auch gefallen