Beruflich Dokumente
Kultur Dokumente
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
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.
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.
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
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.
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.
Andrs Marzal
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.
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
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
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
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
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:
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
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)
Andrs Marzal
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.
10
Andrs Marzal
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
Kent Beck
Steve Mellor
Alistair Cockburn
Ward Cunningham
Martin Fowler
12
Andrs Marzal
Andy Hunt
Ron Jeffries
Mike Beedle
Dave Thomas
Jim Highsmith
Jon Kern
Brian Marick
James Grenning
Ken Schwaber
Jeff Sutherland
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
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.
14
Andrs Marzal
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
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
En este texto slo vamos a enunciar algunos de los principales principios de diseo (muchos de ellos
recogidos en el trabajo citado):
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
Bertrand Meyer en su libro de 1988 Object Oriented Software Construction). Hoy se prefiere el diseo
orientado a interfaces.
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
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.
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.
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
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.
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.
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.
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
Las dependencias cclicas se pueden romper, por ejemplo, con DIP. Este diagrama, del mismo artculo, ilustra
cmo romper la dependencia cclica invirtiendo la dependencia:
21
Andrs Marzal
Hay una mtrica de estabilidad posicional que depende del nmero de dependencias que entran y
salen de un 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).
, 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
En la programacin orientada a objetos, se entiende que unidad es cada mtodo individual y que las
unidades relacionadas estrechamente son
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.
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
24
Andrs Marzal
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
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
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.
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
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
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:
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
Lazy Initialization,
Blackboard,
Active Object,
Object pool,
Null object,
Balking,
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.
30
Andrs Marzal
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
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();
}
}
32
Andrs Marzal
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
{
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
}
}
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);
}
}
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.
Aplicabilidad
Use Decorator:
36
Andrs Marzal
2.
Estructura
Participantes
ConcreteDecorator (BorderDecorator,
ScrollDecorator): aade responsabilidades al
componente.
3.
4.
Colaboraciones
Implementacin
Deben tenerse en cuenta varias cuestiones al aplicar el
patrn Decorator:
1.
2.
3.
37
Andrs Marzal
4.
38
Andrs Marzal
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
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
StreamReader
Text
Int,
float,
string...
FileStream
StreamWriter
DeflateSream
BinaryReader
GZipStream
BinaryWriter
CryptoStream
XmlReader
BufferedStream
XML
data
MemoryStream
NetworkStream
XmlWriter
Raw
bytes
IsolatedStorageStream
40
(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
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_/_(++
++)____(++
++`-'++
+++++++++++++++++
+++++++++++++++++
42
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
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
}
public bool IsEmpty
{
get { return _count == 0; }
}
}
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.
45
}
}
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
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;
}
}
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
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; }
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
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
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:
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
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
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});
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
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;
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
{
_store.Add(item);
return this;
}
}
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
55
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");
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
57
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
}
}
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
59
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
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
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();
}
}
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.
62
{
[Conditional("DEBUG")]
public static void Avisa() {
Console.WriteLine("El aviso");
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Voy");
UnaClase.Avisa();
Console.ReadKey();
}
}
}
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 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
[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.
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
{
Console.WriteLine("{0}: {1}", mct, ayuda.TextoDeAyuda);
}
}
Console.ReadKey();
}
}
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:
65
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:
66
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.
67
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
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
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:
70
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);
}
}
}
71
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);
}
72
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
}
[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);
}
}
}
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);
}
74
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:
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
Greater/Less
Greater(T exp1, T exp2), donde T es int, uint, decimal, float, double o IComparable:
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
7.4.8
IsNaN
7.4.9
Para cadenas
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
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
[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).
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
Escribir una
prueba
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.
79
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.
(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
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
Assert.AreEqual(expected, result);
}
}
}
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
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
[Test]
public void Transcribe_9_ReturnsNueve()
{
string result = transcriptor.Transcribe(9);
string expected = "nueve";
Assert.AreEqual(expected, result);
}
}
}
84
{
return "seis";
}
case 7:
{
return "siete";
}
case 8:
{
return "ocho";
}
case 9:
{
return "nueve";
}
default:
{
return "";
}
}
}
}
}
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
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 "";
}
}
}
}
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
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
e Y es la transcripcin de
e Y es la transcripcin de
Ejecutamos y rojo!
87
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
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
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
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
}
}
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
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
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);
}
94
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void Transcribe_GreaterThan9999_ThrowsArgumentOutOfRangeException()
{
string result = transcriptor.Transcribe(10000);
}
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)
{
95
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.
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
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
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
Al instalarla, Visual Studio enriquece algunos mens con opciones para ejecutar pruebas.
99
En cualquier caso puede resultar ms cmodo que andar entrando y saliendo de Visual Studio cada vez que
se ejecutan las pruebas.
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).
construir nuestro software del modo ms desacoplado posible, haciendo tan sencillo como sea
posible el sustituir una clase por otra.
100
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:
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.
101
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
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.
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
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
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);
}
}
}
105
{
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);
}
}
}
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
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();
}
107
{
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
El programa no compila. Podemos hacer que compile con una implementacin que no hace nada relevante:
public class Feed
{
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>
109
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
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.
111
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
}
}
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
)
});
}
[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
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
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);
}
116
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
117
}
#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.
118
{
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>();
119
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 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
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);
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;
Loader.cs
using System.Net;
using System.IO;
namespace Rss
{
public class Loader : ILoader
{
#region ILoader Members
public string Load(string url)
121
{
var client = new WebClient();
using (var s = client.OpenRead(url))
using(var r = new StreamReader(s))
{
return r.ReadToEnd();
}
}
#endregion
}
}
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
</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.
123
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"));
}
}
}
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
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
Cada cierto tiempo podemos incluir esa prueba unitaria en la ejecucin. Pero slo cada cierto tiempo.
126
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:
127
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.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);
}
128
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
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
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
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);
}
132
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
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.
134
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
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
{
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);
137
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);
}
}
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");
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
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));
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());
139
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.10
140
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.
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
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);
}
}
}
142
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
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.
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.
144
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.
<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
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
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
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
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; }
}
}
}
148
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
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
{
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
}
}
}
151
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
{
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
}
}
}
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; }
}
}
}
154
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:
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
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.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
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.
156
ERROR
Se ha detectado un acontecimiento que no debera ocurrir, por lo que el software debera entrar en
un proceso de recuperacin.
WARN
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
Ni tampoco cambiar el Target de la aplicacin para evitar el error apuntado en la anterior nota al pie de pgina.
157
.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.
158
<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>
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>
159
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
log4net.Appender.ColoredConsoleAppender
log4net.Appender.ConsoleAppender
log4net.Appender.DebugAppender
log4net.Appender.EventLogAppender
log4net.Appender.ForwardingAppender
log4net.Appender.FileAppender
Aade a un fichero.
log4net.Appender.MemoryAppender
log4net.Appender.RemoteSyslogAppender
log4net.Appender.RemotingAppender
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
160
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>
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
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:
<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.
162
<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
%level
%line
%message
%method
%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
%timestamp
%thread
%%
Smbolo de %.
%username
163
%utcdate
164
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).
165
"%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();
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:
10.10.2 LevelMatchFilter
Filtra por nivel:
166
10.10.3 LevelRangeFilter
Filtra por valor del nivel en un rango
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:
10.10.5 PropertyFilter
Filtra en funcin del contenido de texto del valor de una propiedad:
10.10.6 DenyAllFilter
Poco que decir de este, que lo filtra todo.
167
_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
.Returns(SomeConstants.input);
_parserMock = new Mock<Rss.IParser>(MockBehavior.Strict);
_parserMock.Setup(p => p.Parse(It.IsAny<string>()))
.Returns(SomeConstants.intermediate);
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
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
System.Console.ReadKey();
}
}
}
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
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
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
172
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
174
}
Console.ReadKey();
}
}
}
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
[DataContract]
public class Persona
{
[DataMember] public
[DataMember] public
[DataMember] public
[DataMember] public
}
176
<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; }
}
}
177
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; }
}
}
178
};
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();
}
}
}
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
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; }
}
}
180
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
[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>
182
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:
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
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
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();
}
}
}
185
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
}
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());
}
}
187
}
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();
}
}
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
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.
189
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
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.
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());
}
La clase esttica FromAssembly, que permite dar de alta todos los instaladores de un ensamblado.
191
13.2.1.1 FromAssembly
Estudiemos con un poco ms de calma las posibilidades que ofrece FromAssembly:
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")));
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
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>());
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<>));
193
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
194
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>());
195
);
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")));
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;
}));
Sin restricciones.
AllTypes.FromAssemblyNamed("Acme.Crm.Services").Pick()
196
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());
197
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));
198
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>();
199
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
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
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.
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
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
}
}
203
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
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
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
}
}
206
}
}
}
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
#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
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();
}
}
}
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
210
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.
211
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
<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>
213
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
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" />
215
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.
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>
("RootNameSpace", "HolaMundo")
216
("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.
La tarea Message muestra el valor del atributo Text por la salida estndar.
217
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>
218
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>
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
<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:
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
<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>
221
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.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
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
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:
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>
He experimentado problemas con la versin de 64 bits, por lo que las demostraciones usan la de 32 bits.
224
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
<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
<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
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
</Project>
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
Llamaremos a nuestro programa RssReaderInstaller. Visual Studio presentar un aspecto similar a ste:
230
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:
231
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:
232
233
Nos aseguramos de que slo est seleccionado el framework necesario para nuestra aplicacin y pulsamos
Ok/Aceptar en los dos cuadros abiertos.
234
Y esperar confirmacin:
235
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:
236
Hemos creado un sistema de instalacin idntico al de las aplicaciones de uso comn en Windows.
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.
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.
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
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
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
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
239
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
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
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
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.
242
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
Nos pide que demos una descripcin de los cambios que introduce la consigna. Escribimos algo como
Primera versin.:
244
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
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:
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
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.
247
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. :
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
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
Iconos que indican que no hay sincrona? La integracin con el escritorio no es perfecta. Si queremos estar
seguros, seleccionamos TortoiseHg Update Icons.
249
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
251
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
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
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
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
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
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
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:
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:
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.
258
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
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
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.
261
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.
El mensaje There were merge conflicts that must be resolved contiene un hiperenlace: si
pulsamos en resolved aparecer un nuevo cuadro de dilogo:
262
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:
263
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
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
266
*.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
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
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
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
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:
271
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:
272
273
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:
Una vez dada de alta, la entrada se muestra en la pestaa Issues de este modo:
274
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:
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
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
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
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
278
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.
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.
279
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
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
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
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
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
Podemos programar ahora la accin que disparar la ejecucin. Seleccionamos el paso 4: Build Triggering.
Aparecer esta pantalla:
285
Aadimos un trigger con Add new trigger y seleccionamos el tipo VCS trigger, esto es, disparador por
sistema de control de versiones:
286
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
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:
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
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
290
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
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
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
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:
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
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:
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
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.
295
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
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.
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
(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
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
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.
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
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:
301
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:
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
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
http://www.ademiller.com/blogs/tech/2008/07/daily-standup-meetings/:
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
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
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
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
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
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
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.
310
El equipo de produccin ve que A ha sido completado y la pasa a produccin. Los desarrolladores completan
B y trabajan en C:
311
Los desarrolladores, en grupos de dos, abordan las tareas. El Product Owner selecciona otras dos tareas.
312
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
Cuando se solucionan los problemas con A y B, el equipo puede reorganizarse y seguir con la actividad
normal.
314
315
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.
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
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
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.
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
Contenido
1
319
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
320
321
10
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
13
322
14
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
323
18
19
20
324