Archive for September, 2010



TDD en un proyecto avanzado

Muy bien. Has ido haciendo TDD y ya tienes un producto maduro y con miles de lines de código en producción. Tienes que añadir una nueva funcionalidad a un módulo y te dispones a escribir primero el ejemplo-especificación, como siempre.

¡No lo hagas en una clase nueva sin mirar lo que ya hay! 🙂

Parece de cajón pero pasa de vez en cuando. Para no perder el norte y ser coherentes con la arquitectura y la estructura de código que tenemos, el primer paso es mirar, en qué parte de nuestro código existente, vamos a llamar al nuevo código. Eso nos ayudará a decidir la API del nuevo método, ya que podria no ser totalmente libre, es decir, puede que ya tengamos impuestas algunas restricciones o consideraciones que hacer. Una vez hecho eso es bueno escribir la llamada a la funcion aunque todavia no exista, en este código que ya tenemos y que va a consumir la nueva función. Si nos gusta la forma en que la utilizamos y encaja con lo que ya hay, entonces podemos hacer ya TDD como siempre.

Sino, acabaremos teniendo muchas clases separadas por ahi, cuando debieran de ser una sola clase con varios métodos. TDD produce código muy muy modular, pero no hay que llegar al punto de que haya dos clases con la misma responsabilidad.

¿Por donde empiezas a refactorizar una vieja aplicación de codigo legado?
Lo primero en mi opinión es tratar de escribir tests automáticos de algún tipo,
que nos ayuden a asegurar que no rompemos funcionalidad. Lo normal es que sean
tests de extremo a extremo (tests de sistema). Sin embargo, el código puede
ser tan duro que escribir tests es casi imposible. Por eso tenemos que
contar con el respaldo de un equipo de QA que tenga un buen conocimiento
de la aplicación y de su dominio. Una vez tenemos tests y el apoyo
del equipo de QA, ¿por dónde empezamos a refactorizar?

Eric Evans dice lo siguiente en su libro, Domain Driven Design:

En la comunidad XP las respuestas suelen ser las siguientes:
1. Simplemente empieza por donde sea, ya que todo tiene que refactorizarse
2. Empieza por aquel lugar que está causando más problemas o molestias. Refactoriza lo que vayas necesitando para completar la tarea que estas desarrollando.

No me quedo con ninguna de estas. La primera no es práctica excepto en unos pocos proyectos en los que todos los desarrolladores del equipo tienen un nivel muy alto. La segunda tiende a dar rodeos, ocupándose de síntomas pero sin atacar a la raíz de los problemas. Al final el código va quedando cada vez más difícil de refactorizar.

Como alternativas Eric propone:

1. En un refactoring guiado por penurias (pain-driven), miras si la raíz tiene que ver con el núcleo del dominio (CORE DOMAIN) o con alguna de sus relaciones. Si la tiene, entonces cruzas los dedos y arreglas eso antes que nada.

2. Si te puedes permitir el lujo de refactorizar libremente, primero te centras
en un refactoring del núcleo del dominio, mejorando sus relaciones y facilitando
el soporte para conectar subdominios genéricos.

Mapreduce API is great. We've got a tool now that can process tasks taking more than 30 seconds. Yeaaaahhh! This is a huge improvement. I wish we have had this tools months ago. All the examples in the documentation use the webapp framework, there aren't many examples using the Django helper in the internet. This post is about that.

mapreduce.yaml:

  1.  
  2. mapreduce:
  3.  
  4. - name: Delete SearchableTowns
  5. mapper:
  6. input_reader: mapreduce.input_readers.DatastoreInputReader
  7. handler: main_map_reduce.delete_searchable_towns
  8. params:
  9. - name: entity_kind
  10. default: mapreduce_models.SearchableTown
  11.  
  12. - name: Create SearchableTown from Town
  13. mapper:
  14. input_reader: mapreduce.input_readers.DatastoreInputReader
  15. handler: main_map_reduce.town_to_searchable
  16. params:
  17. - name: entity_kind
  18. default: mapreduce_models.Town
  19.  
  20. - name: Create Town and SearchableTown from csv for USA
  21. mapper:
  22. input_reader: mapreduce.input_readers.BlobstoreLineInputReader
  23. handler: main_map_reduce.csv_to_towns
  24. params:
  25. - name: blob_keys
  26. default: AMIfv97g-x4G9-KM24YXQi6dSyBddAb97p0n98NgJlCL68jJA9jcvwETojEcF7MGGlZsDLEFVcJeeLHGgwxo9Nlay9GR33LniA06Obw3C781Te9yAn9Dk1EkwxjrFqHEBo4-WbZ7GUS9nKa3NOpDGdbxBBkD2sTYUg
  27.  

The file contain 3 tasks. 2 of them are intended to create or modify datastore entities. The other one is going to read a big csv from the blogstore, creating a datastore entity for every line in the file. This is the Python version of this blog post (which uses Java).

Now, main_map_reduce is a python file that I keep in the same location than mapreduce.yaml. Just a regular python file. The imports in that file might cause exceptions, specially if they try to load Django stuff. In order to avoid problems we had to copy our models.py into mapreduce_models.py removing almost all the imports. As mapreduce_models.py is placed at same level than mapreduce.yaml, we had to hack also the file appengine_django/models.py, replacing this line:

  1.  
  2. self.app_label = model_module.name.split('.')[-2]
  3.  

With this block:

  1.  
  2. self.app_label = 'my_app_name'
  3. try:
  4. self.app_label = model_module.__name__.split('.')[-2]
  5. except IndexError:
  6. pass
  7.  

main_map_reduce.py:

  1.  
  2. def delete_searchable_towns(town_entity):
  3. yield op.db.Delete(town_entity)
  4.  
  5. def town_to_searchable(town_entity):
  6. searchable = models.SearchableTown()
  7. searchable.code = town_entity.code
  8. searchable.lower_name = town_entity.name.lower()
  9. yield op.db.Put(searchable)
  10.  
  11. def csv_to_towns(input_tuple):
  12. line = input_tuple[1]
  13. offset = input_tuple[0]
  14. # process the line ...
  15. yield op.db.Put(town_entity)
  16.  

In the first two methods, the mapreducer passes in an entity. In the last one, it passes a tuple, where its second item is the line read from the blog, which is a big csv file.
This way, we can now upload a huge csv and then create entities from it. This tasks was really painful before, as we had to make a ton of dirty hacks in order to avoid the 30 seconds restriction.

GAE Blobstore is a great way to manage big files. This post is about using the Blogstore with Python and the Django GAE Helper (appengine_django).

Views.py:

  1.  
  2.  
  3. from google.appengine.ext import blobstore
  4. # import other django stuff
  5.  
  6. def show_upload_form(request):
  7. template = "admin_upload_blob.html"
  8. dict = {}
  9. dict['action'] = blobstore.create_upload_url('/admin/upload_blob/')
  10. return render_to_response(template,
  11. context_instance= context.get_context(
  12. dict, request))
  13.  
  14. def upload_blob(request):
  15. try:
  16. uploads = get_uploads(request, 'file')
  17. for upload in uploads:
  18. file = BlobFile(blob=upload)
  19. file.save()
  20. except Exception, e:
  21. return HttpResponseRedirect("/admin")
  22. return HttpResponse("Ok!")
  23.  
  24.  
  25. def get_uploads(request, field_name=None, populate_post=False):
  26. """
  27. http://appengine-cookbook.appspot.com/recipe/blobstore-get_uploads-helper-function-for-django-request/
  28.  
  29. Get uploads sent to this handler.
  30.  
  31. Args:
  32. field_name: Only select uploads that were sent as a specific field.
  33. populate_post: Add the non blob fields to request.POST
  34.  
  35. Returns:
  36. A list of BlobInfo records corresponding to each upload.
  37. Empty list if there are no blob-info records for field_name.
  38. """
  39.  
  40. logging.info(request)
  41. if hasattr(request,'__uploads') == False:
  42. request.META['wsgi.input'].seek(0)
  43. fields = cgi.FieldStorage(request.META['wsgi.input'], environ=request.META)
  44.  
  45. request.__uploads = {}
  46. if populate_post:
  47. request.POST = {}
  48. for key in fields.keys():
  49. field = fields[key]
  50. if isinstance(field, cgi.FieldStorage) and 'blob-key' in field.type_options:
  51. request.__uploads.setdefault(key, []).append(blobstore.parse_blob_info(field))
  52. elif populate_post:
  53. request.POST[key] = field.value
  54.  
  55. if field_name:
  56. try:
  57. return list(request.__uploads[field_name])
  58. except KeyError:
  59. return []
  60. else:
  61. results = []
  62. for uploads in request.__uploads.itervalues():
  63. results += uploads
  64. return results
  65.  

admin_upload_blob.html:

  1.  
  2. {% extends "base.html" %}
  3. {% load i18n %}
  4.  
  5. {% block content %}
  6.  
  7. Upload a big file:
  8.  
  9. <form action="{{ action }}" method="post" enctype="multipart/form-data">
  10. Upload File: <input type="file" name="my_file"><br>
  11. <input type="submit" name="submit" value="Submit"> </form>
  12. {% endblock content %}
  13.  

Make sure urls.py contains the methods in views.py as usual.
Just access show_upload_form with your browser and upload a file. It works!