Compartir a través de


Windows Phone

Extinción y el zen de async en Windows Phone

Ben Day

Descargar el ejemplo de código

¿Alguna vez escribió una aplicación y prácticamente en cuanto la terminó deseó haberla escrito en forma diferente? Ese instinto visceral que dice que algo no está bien con la arquitectura. Los cambios que deberían ser sencillos resultan prácticamente imposibles, o al menos toman mucho más tiempo de lo que deberían. Y luego están los errores. ¡Ay, los errores! Si somos programadores decentes. ¿Cómo pudimos escribir algo tan plagado de errores? 

¿Le suena conocido? Bueno, a mí me pasó cuando escribí mi primera aplicación para Windows Phone, NPR Listener. NPR Listener habla con los servicios web de National Public Radio (npr.org/api/index.php) para recibir el listado de las noticias disponibles para sus programas y permite que el usuario las escuche en su dispositivo Windows Phone. Cuando la escribí por primera vez, había estado desarrollando varias cosas con Silverlight y estaba muy conforme con lo bien que mis conocimientos y capacidades se trasladaban a Windows Phone. Terminé la primera versión bastante rápido y la envié al proceso de certificación de la Tienda. Todo el tiempo estuve pensando “bueno, eso sí que fue fácil”. Y luego desaprobé la certificación. Este es el caso que fracasó: 

Paso 1: Ejecute la aplicación.  

Paso 2: Presione el botón Inicio para llegar a la página principal del teléfono.

Paso 3: Presione el botón Atrás para volver a la aplicación.

Al presionar el Atrás, la aplicación se debería reanudar sin errores e, idealmente, debería volver exactamente a la misma pantalla donde el usuario abandonó la aplicación. En mi caso, el evaluador navegó a un programa de National Public Radio (como por ejemplo “All Things Considered”), hizo clic en una de las noticias actuales y luego presionó el botón Inicio para ir a la pantalla principal del dispositivo. Cuando presionó el botón Atrás para volver a mi aplicación, esta se volvió a presentar… y se encontró con un carnaval de excepciones por referencias a objetos nulos. Mala cosa.

Ahora le mostraré cómo diseño mis aplicaciones basadas en XAML. Para mí, todo tiene que ver con el patrón Model-View-ViewModel; tengo una fijación casi religiosa por separar las páginas en XAML y la lógica de mi aplicación. Si el código subyacente (*.xaml.cs) de mis páginas va a contener algún código, más vale que sea por muy buen motivo. Gran parte de esto está motivado por mi necesidad casi patológica de poder realizar pruebas unitarias. Estas son de importancia vital, ya que nos permiten saber cuándo la aplicación está funcionando y, más importante aún, permiten refactorizar el código y cambiar la manera en que funciona la aplicación.

Pero si soy tan fanático de las pruebas unitarias, ¿cómo es que llegué a esa enorme cantidad de excepciones? El problema es que escribí la aplicación para Windows Phone como si fuera una aplicación Silverlight. Claro, Windows Phone es Silverlight, pero el ciclo de vida de una aplicación Silverlight y una para Windows Phone son completamente diferentes. En Silverlight, el usuario abre la aplicación, interactúa con esta hasta que termina y luego la cierra. En cambio en Windows Phone, el usuario abre la aplicación, trabaja con esta y entra y sale en el sistema operativo o en cualquier otra aplicación cuando quiere. Cuando sale de la aplicación, esta se desactiva o “extingue”. Cuando se encuentra en este estado, la aplicación se deja de ejecutar, pero la “pila de retroceso” de navegación (las páginas dentro de la aplicación, en el orden en que fueron visitadas) siguen estando disponibles en el dispositivo.

Es posible que se haya dado cuenta que en su dispositivo Windows Phone puede recorrer varias aplicaciones y presionar el botón Atrás repetidas veces para regresar a estas en orden inverso. Esa es la pila de retroceso de navegación en acción, y cada vez que pasa a una aplicación diferente, esta se reactiva a partir de los datos de extinción almacenados. Cuando se va a extinguir la aplicación, recibe una notificación del sistema operativo, que le indica que este se va a desactivar y que debe almacenar el estado de la aplicación para poder reactivarla más adelante. En la Ilustración 1 se aprecia un ejemplo sencillo para activar y desactivar una aplicación en App.xaml.cs.

Ilustración 1 Implementación sencilla de la extinción en App.xaml.cs

// Code to execute when the application is deactivated (sent to background).
// This code will not execute when the application is closing.
private void Application_Deactivated(object sender, DeactivatedEventArgs e)
{
  // Tombstone your application.
  IDictionary<string, object> stateCollection =
    PhoneApplicationService.Current.State;
  stateCollection.Add("VALUE_1", "the value");
}
// Code to execute when the application is activated
// (brought to foreground).
// This code will not execute when the application is first launched.
private void Application_Activated(object sender, ActivatedEventArgs e)
{
  // Un-tombstone your application.
  IDictionary<string, object> stateCollection =
    PhoneApplicationService.Current.State;
  var value = stateCollection["VALUE_1"];
}

El problema de las excepciones por referencias a objetos nulos se debió a la ausencia total de una planeación —y codificación— para procesar estos eventos de extinción. Esto, junto con mi implementación exhaustiva, sofisticada y compleja de ViewModel, reunía todos los elementos necesarios para acabar mal. Piense acerca de qué ocurre cuando un usuario hace clic en el botón Atrás para volver a entrar en la aplicación. Que el usuario no termine en la página de inicio sino que en la última página que visitó en la aplicación. En el caso del evaluador de Windows Phone, cuando el usuario reactivó mi aplicación, entró en la mitad de la aplicación y la interfaz de usuario supuso que el ViewModel estaba relleno y que estaba preparada para controlar esa pantalla. Pero como el ViewModel no respondía a los eventos de extinción, prácticamente todas las referencias de objeto eran null. Uy. Y claramente no escribí pruebas unitarias para ese caso, ¿no es así? (¡Kabuuum!)

La lección es que la interfaz de usuario y los ViewModel para desplazarse hacia delante y hacia atrás se deben planear como corresponde.

Implementación a posteriori de la extinción

En la Ilustración 2 aparece la estructura de mi aplicación original. Para que la aplicación aprobara la certificación, debía controlar el caso del botón de Inicio y Atrás. Podía implementar la extinción en el proyecto de Windows Phone (Benday.Npr.Phone) o lo podía imponer a la fuerza en el ViewModel (Benday.Npr.Presentation). Ambas soluciones ponían en entredicho la arquitectura en forma molesta. Si agregaba la lógica al proyecto Benday.Npr.Phone, la interfaz de usuario tendría demasiada información sobre la forma en que funciona el ViewModel. Si la agregaba al proyecto del ViewModel, necesitaría una referencia desde Benday.Npr.Presentation a Microsoft.Phone.dll para obtener acceso al diccionario de valores de la extinción (PhoneApplicationService.Current.State) en el espacio de nombres Microsoft.Phone.Shell. Eso contaminaría el proyecto ViewModel con detalles de implementación innecesarios e infringiría el principio de Separación de conceptos (SoC). 

The Structure of the Application
Ilustración 2 Estructura de la aplicación

Finalmente opté por dejar la lógica dentro del proyecto de Windows Phone, pero de crear también algunas clases que sabrían cómo serializar el ViewModel en una cadena XML que se podría almacenar en el diccionario con los valores de la extinción. Este método me permitió evitar la referencia del proyecto Presentation a Microsoft.Phone.Shell y simultáneamente obtuve un código limpio que respeta el Principio de responsabilidad única. Esas clases las llamé *ViewModelSerializer. En la Ilustración 3 se muestra parte del código necesario para convertir una instancia de StoryListViewModel en XML.

Ilustración 3 Código en StoryListViewModelSerializer.cs para convertir IStoryListViewModel en XML

private void Serialize(IStoryListViewModel fromValue)
{
  var document = XmlUtility.StringToXDocument("<stories />");
  WriteToDocument(document, fromValue);
  // Write the XML to the tombstone dictionary.
  SetStateValue(SERIALIZATION_KEY_STORY_LIST, document.ToString());
}
private void WriteToDocument(System.Xml.Linq.XDocument document,
  IStoryListViewModel storyList)
{
  var root = document.Root;
  root.SetElementValue("Id", storyList.Id);
  root.SetElementValue("Title", storyList.Title);
  root.SetElementValue("UrlToHtml", storyList.UrlToHtml);
  var storySerializer = new StoryViewModelSerializer();
  foreach (var fromValue in storyList.Stories)
  {
    root.Add(storySerializer.SerializeToElement(fromValue));
  }
}

Una vez que escribí esos serializadores, tenía que agregar la lógica necesaria a App.xaml.cs para desencadenar esa serialización en función de la pantalla que se visualiza actualmente (ver Ilustración 4).

Ilustración 4 Activación de los serializadores de ViewModel en App.xaml.cs

private void Application_Deactivated(object sender, 
  DeactivatedEventArgs e)
{
  ViewModelSerializerBase.ClearState();
  if (IsDisplayingStory() == true)
  {
    new StoryListViewModelSerializer().Serialize();
    new StoryViewModelSerializer().Serialize();
    ViewModelSerializerBase.SetResumeActionToStory();
  }
  else if (IsDisplayingProgram() == true)
  {
    new StoryListViewModelSerializer().Serialize();
    new ProgramViewModelSerializer().Serialize();
    ViewModelSerializerBase.SetResumeActionToProgram();
  }
  else if (IsDisplayingHourlyNews() == true)
  {
    new StoryListViewModelSerializer().Serialize();
    ViewModelSerializerBase.SetResumeActionToHourlyNews();
  }               
}

Finalmente pude echarlo a andar y logré certificar la aplicación, pero desgraciadamente el código era lento, feo y estaba lleno de errores. Lo que debería haber hecho fue diseñar mi ViewModel para que tuviera menos estado que se debía almacenar y luego crearlo de tal forma que se conservara por su propia cuenta mientras se ejecutaba, en vez de tener que realizar un evento de extinción gigantesco al final. ¿Cómo podría hacerlo?

Programación asincrónica al estilo del maniático del control

Le voy a hacer una pregunta: ¿tiene tendencias de “maniático del control”? ¿Le cuesta soltar? ¿Opta por hacer caso omiso de las verdades evidentes, por pura fuerza de voluntad? ¿Soluciona los problemas en formas que pasan por alto la realidad que es tan clara como el día y que le está mirando directamente a los ojos? Sí, así es como yo procedí con las llamadas asincrónicas en la primera versión de NPR Listener. En concreto, así es como me comporté frente a las operaciones en red asincrónicas en la primera versión de la aplicación.

En Silverlight todas las llamadas de red deben ser asincrónicas. El código inicia una llamada en red y devuelve inmediatamente. El resultado (o una excepción) se entrega en algún momento posterior, mediante una devolución de llamada asincrónica. Esto significa que la lógica de las operaciones en red siempre se compone de dos partes: la llamada saliente y la llamada devuelta. Esta estructura tiene consecuencias y es un secretillo oscuro en Silverlight que cualquier método que depende de los resultados de una llamada en red no puede devolver un valor, sino que debe devolver void. Esto tiene un efecto lateral: cualquier método que llama otro método que depende de los resultados de una llamada en red también debe devolver void. Como se podrá imaginar, esto puede tener efectos devastadores en las arquitecturas en capas, ya que las implementaciones de patrones de diseño de n niveles, tales como Capa de servicio, Adaptador y Repositorio, dependen significativamente de los valores devueltos de las llamadas a métodos.

Mi solución es una clase llamada ReturnResult<T> (que aparece en la Ilustración5), que sirve como pegamento entre el método que solicita la llamada en red y el método que controla los resultados de la llamada y proporciona una forma para que el código devuelva valores útiles. En la Ilustración 6 se aprecia parte de lógica del Patrón de repositorio que realiza una llamada al servicio Windows Communication Foundation (WCF) y luego devuelve una instancia rellena de IPerson. Este código nos permite llamar LoadById(ReturnResult<IPerson>, int) y recibir al final la instancia rellena de IPerson, cuando client_LoadBy­IdCompleted(object, LoadByIdCompleted­EventArgs) llama uno de los métodos Notify. Básicamente nos permite crear código que es similar a lo que emplearíamos su pudiéramos usar valores devueltos. (Para obtener más información sobre ReturnResult<T>, consulte bit.ly/Q6dqIv.) 

ReturnResult
Ilustración 5 ReturnResult<T>

Ilustración 6 Uso de ReturnResult<T> para iniciar una llamada en red y devolver un valor desde el evento finalizado

public void LoadById(ReturnResult<IPerson> callback, int id)
{
  // Create an instance of a WCF service proxy.
  var client = new PersonService.PersonServiceClient();
  // Subscribe to the "completed" event for the service method.
  client.LoadByIdCompleted +=
    new EventHandler<PersonService.LoadByIdCompletedEventArgs>(
      client_LoadByIdCompleted);
  // Call the service method.
  client.LoadByIdAsync(id, callback);
}
void client_LoadByIdCompleted(object sender,
  PersonService.LoadByIdCompletedEventArgs e)
{
  var callback = e.UserState as ReturnResult<IPerson>;
  if (e.Error != null)
  {
    // Pass the WCF exception to the original caller.
    callback.Notify(e.Error);
  }
  else
  {
    PersonService.PersonDto personReturnedByService = e.Result;
    var returnValue = new Person();
    var adapter = new PersonModelToServiceDtoAdapter();
    adapter.Adapt(personReturnedByService, returnValue);
    // Pass the populated model to the original caller.
    callback.Notify(returnValue);   
  }           
}

Cuando terminé de escribir la primera versión de NPR Listener, rápidamente descubrí que la aplicación era lenta (o al menos se sentía lenta) debido a que no realizaba ningún tipo de almacenamiento en caché. Lo que realmente necesitaba en la aplicación era una forma de llamar un servicio web de NPR, recibir una lista de noticias para un programa dado y luego almacenar esos datos en caché, para no tener que volver al servicio cada vez que había que dibujar la pantalla. Agregar esa funcionalidad, sin embargo, era bastante difícil, ya que estaba tratando de hacer como si no existieran las llamadas asincrónicas. Esencialmente, al ser un maniático del control e intentar negar la estructura inherentemente asincrónica de la aplicación, estaba limitando mis opciones. Estaba luchando contra la plataforma y, por lo tanto, desfigurando la arquitectura de mi aplicación.

En una aplicación asincrónica, las cosas comienzan a ocurrir en la interfaz de usuario; el flujo de control atraviesa las capas de la aplicación y devuelve datos a medida que se desbobina la pila. Todo ocurre dentro de una misma pila de llamadas, donde se inicia el trabajo, se procesan los datos y se devuelve un valor devuelto hacia arriba en la pila. En las aplicaciones asincrónicas, el proceso se parece más a cuatro llamadas conectadas libremente entre sí: la interfaz de usuario solicita que ocurra algo; puede producirse procesamiento o no; si el procesamiento se produce y la interfaz de usuario está suscrita al evento, el procesamiento notifica la interfaz de usuario que finalizó una acción; y la interfaz de usuario actualiza la pantalla con los datos de la acción asincrónica.

Ya puedo imaginarme sermoneando a algún mequetrefe sobre lo dura que era la vida antes de que existiera async y await. “En mis tiempos, teníamos que administrar nuestra propia lógica de conexiones en red y devoluciones de llamadas asincrónicas. Era atroz, ¡y nos gustaba! Y ahora, ¡largo de aquí!” Bueno, la verdad es que a nosotros tampoco nos gustaba. Era atroz.

Esta es otra lección: Al luchar contra la arquitectura subyacente de la plataforma, siempre nos meteremos en problemas.

Reescritura de la aplicación con almacenamiento aislado

Primero escribí la aplicación para Windows Phone 7 y solo realicé una actualización menor para Windows Phone 7.1. En una aplicación cuya única función consistía en transmitir audio por secuencias, siempre había sido una decepción que los usuarios no pudieran escuchar música mientras exploraban otras aplicaciones. Cuando salió Windows Phone 7.5, quería aprovechar las nuevas características de transmisión por secuencias en segundo plano. También quería acelerar la aplicación y eliminar una gran cantidad de llamadas a servicios web innecesarias mediante algún tipo de almacenamiento en caché local de los datos. Pero cuando comencé a pensar acerca de cómo podía implementar estas características, las limitaciones y la fragilidad de mi implementación de la extinción, ViewModel y async se hicieron cada vez más patentes. Había llegado el momento de corregir mis errores previos y reescribir la aplicación por completo.

Como ya había aprendido la lección en la versión previa de la aplicación, decidí que iba a comenzar con el diseño de la capacidad de extinción y que adoptaría plenamente el carácter asincrónico de la aplicación. Como quería agregar almacenamiento en caché local de los datos, comencé a estudiar el almacenamiento aislado. El almacenamiento aislado es un lugar dentro del dispositivo donde la aplicación puede leer y escribir datos. La interacción con este es similar al sistema de archivos en una aplicación .NET corriente.

Almacenamiento aislado para el almacenamiento en caché y operaciones de red simplificadas

Uno de los lados positivos del almacenamiento aislado es que esas llamadas, a diferencia de las llamadas en red, no tienen por qué ser asincrónicas. Esto significa que puedo usar una arquitectura más convencional, que depende de los valores devueltos. Una vez que me percaté de esto, comencé a reflexionar sobre cómo separar las operaciones que tienen que ser asincrónicas de las que pueden ser asincrónicas. Las llamadas en red tienen que ser asincrónicas. Las llamadas al almacenamiento aislado pueden serlo. ¿Entonces, qué pasa si siempre escribo los resultados de las llamadas en red en el almacenamiento aislado antes de realizar cualquier análisis? Esto me permite cargar los datos en forma sincrónica y me entrega una forma económica y fácil para realizar el almacenamiento en caché local de los datos. El almacenamiento aislado me permite solucionar dos problemas a la vez.

Comencé a reelaborar la forma en que realizo las llamadas en red y acepté la realidad de que se trata de una serie de pasos asociados libremente, en vez de un gran paso sincrónico. Por ejemplo, cuando quiero recibir la lista de las noticias de un programa determinado de NPR, esto es lo que hago (ver Ilustración 7):

  1. El ViewModel se suscribe a un evento StoryListRefreshed en StoryRepository.
  2. El ViewModel llama a StoryRepository para solicitar un listado actualizado de las noticias del programa actual. Esta llamada se termina inmediatamente y devuelve void.
  3. StoryRepository emite una llamada asincrónica en red a un servicio web REST NPR para obtener el listado de noticias del programa.
  4. En algún momento se desencadena el método de devolución de llamada y StoryRepository ahora tiene acceso a los datos del servicio. Los datos vuelven del servicio en forma de XML y, en vez de convertirlos en objetos rellenos que se devuelven al ViewModel, StoryRepository inmediatamente escribe el XML en el almacenamiento aislado.
  5. StoryRepository desencadena un evento StoryListRefreshed.
  6. ViewModel recibe el evento StoryListRefreshed y llama el método GetStories para obtener el último listado de noticias. GetStories lee los datos XML del listado de noticias en caché del almacenamiento aislado, los convierte en los objetos necesarios para el ViewModel y devuelve los objetos rellenos. Este método puede devolver objetos rellenos, ya que se trata de una llamada sincrónica que realiza la lectura en el almacenamiento aislado.

Sequence Diagram for Refreshing and Loading the Story List
Ilustración 7 Diagrama de secuencia para actualizar y cargar el listado de noticias

El punto importante aquí es que el método RefreshStories no devuelve datos. Solamente solicita la actualización de los datos de noticias almacenados en caché. El método GetStories toma los datos XML almacenados actualmente en caché y los convierte en objetos. Como GetStories no tiene que llamar ningún servicio, es extremadamente rápido; así que la pantalla Story List se rellena rápidamente y la aplicación se siente mucho más rápida que la primera versión. Si no hay datos en caché, GetStories simplemente devuelve un listado vacío de objetos IStory. Esta es la interfaz IStoryRepository:

public interface IStoryRepository
{
  event EventHandler<StoryListRefreshedEventArgs> StoryListRefreshed;
  IStoryList GetStories(string programId);
  void RefreshStories(string programId);
    ...
}

Otra ventaja adicional de ocultar esta lógica detrás de una interfaz es que conduce a un código limpio en los ViewModel y desacopla el trabajo de desarrollo de los ViewModel de la lógica de almacenamiento y servicio. Esta separación facilita las pruebas unitarias para el código, y este se puede mantener mucho mejor.

Almacenamiento aislado para una extinción continua

La primera implementación de la extinción en la primera versión de mi aplicación tomaba los ViewModel y los convertía en XML, que se almacenaba en el diccionario de valores de extinción del teléfono, PhoneApplicationService.Current.State. Me gustaba la idea del XML, pero no me parecía bien que la persistencia del ViewModel estuviera a cargo del nivel de la interfaz de usuario en vez del nivel del propio ViewModel. Tampoco me gustaba que el nivel de la interfaz de usuario esperara hasta el evento Deactivate de la extinción para conservar todo el conjunto de ViewModels. Cuando se ejecuta la aplicación, realmente solo hace falta conservar un puñado de valores, y estos solo cambian poco a poco, a medida que el usuario se desplaza de una pantalla a la otra. ¿Por qué no escribir los valores al almacenamiento aislado en la medida que el usuario se desplaza por la aplicación? De esta forma, la aplicación siempre está preparada para desactivarse y la extinción no resulta tan dramática. 

Por lo demás, en vez de conservar todo el estado de la aplicación, ¿por qué no guardar solo el valor que se seleccionó actualmente en cada página? Los datos se almacenan en caché en forma local, así que ya se deberían encontrar en el dispositivo, y los puedo volver a cargar fácilmente desde el almacenamiento caché sin cambiar la lógica de la aplicación. Así se reduce la cantidad de valores que se deben conservar desde cientos en la versión 1 a unos cuatro o cinco en la versión 2. Eso es una cantidad radicalmente menor de datos de la que debemos preocuparnos, lo que simplifica mucho las cosas.

La lógica para todo el código de persistencia, para leer y escribir en el almacenamiento aislado está encapsulada en una serie de objetos Repository. Para la información sobre los objetos Story, habrá una clase StoryRepository correspondiente. En la Ilustración 8 aparece el código que toma el identificador de una noticia, lo convierte en un documento XML y lo guarda en el almacenamiento aislado.

Ilustración 8 Lógica de StoryRepository para guardar el identificador de la noticia actual

public void SaveCurrentStoryId(string currentId)
{
  var doc = XmlUtility.StringToXDocument("<info />");
  if (currentId == null)
  {
    currentId = String.Empty;
  }
  else
  {
    currentId = currentId.Trim();
  }
  XmlUtility.SetChildElement(doc.Root, "CurrentStoryId", currentId);
  DataAccessUtility.SaveFile(DataAccessConstants.FilenameStoryInformation, doc);
}

Al encapsular la lógica de persistencia dentro de un objeto Repository, se separa la lógica de almacenamiento y recuperación de toda la lógica del ViewModel y se ocultan los detalles de la implementación de las clases ViewModel. En la Ilustración 9 aparece el código en la clase StoryListViewModel para guardar el identificador de la noticia actual cuando cambia la selección de la noticia. 

Ilustración 9 StoryListViewModel guarda el identificador de la noticia actual cuando cambia el valor

void m_Stories_OnItemSelected(object sender, EventArgs e)
{
  HandleStorySelected();
}
private void HandleStorySelected()
{
  if (Stories.SelectedItem == null)
  {
    CurrentStoryId = null;
    StoryRepositoryInstance.SaveCurrentStoryId(null);
  }
  else
  {
    CurrentStoryId = Stories.SelectedItem.Id;
    StoryRepositoryInstance.SaveCurrentStoryId(CurrentStoryId);
  }
}

Y este es el método Load de StoryListViewModel, que revierte el proceso cuando StoryListViewModel tiene que volver a llenarse desde el disco:

public void Load()
{
  // Get the current story Id.
  CurrentStoryId = StoryRepositoryInstance.GetCurrentStoryId();
  ...
  var stories = StoryRepositoryInstance.GetStories(CurrentProgramId);
  Populate(stories);
}

Planeación por adelantado

En este artículo lo guié por algunas de las decisiones arquitectónicas y errores que cometí en mi primera aplicación para Windows Phone, NPR Listener. No olvide prepararse para la extinción y adoptar —en vez de luchar contra— async en sus aplicaciones para Windows Phone. Si quiere ver el código de ambas versiones de NPR Listener, puede descargarlo en archive.msdn.microsoft.com/mag201209WP7.

Benjamin Day es consultor e instructor especializado en procedimientos recomendados de desarrollo mediante herramientas de desarrollo de Microsoft, con un enfoque en Visual Studio Team Foundation Server, Scrum y Windows Azure. Es MVP de Microsoft Visual Studio ALM, instructor certificado de Scrum a través de Scrum.org y orador en congresos como TechEd, DevTeach y Visual Studio Live! Cuando no desarrolla software, se ha sabido que Day sale a correr y practicar kayak para equilibrar su amor por el queso, carnes curadas y champaña. Puede ponerse en contacto con él a través de benday.com.

Gracias a los siguientes expertos técnicos por su ayuda en la revisión de este artículo: Jerri Chiu y David Starr