Repository Pattern para ApplicationData en Windows 8

Con la llegada de Windows 8 y sus aplicaciones para el Store, muchas veces nos toca lidiar con datos almacenados en local.

La idea de este post es implementar el patrón repositorio para independizar nuestras aplicaciones del trabajo con los distintos tipos de almacenamientos en local que existen en Windows 8.

Para profundizar en este patrón puedes echar un ojo a este enlace de Martin Fowler: http://martinfowler.com/eaaCatalog/repository.html

El objetivo

– Evitar código duplicado
– Mejor gestión de errores (código centralizado)
– Menos dependencia de nuestras reglas de negocio a la persistencia
– Posibilidad de centralizar políticas relacionadas con datos como el almacenamiento en caché o Lazy load.
– Posibilidad de “testear” la lógica de negocio de forma aislada a la persistencia

Empezamos creándonos una clase abstracta que nos permita de forma genérica trabajar con cualquier tipo de datos. La clase incluirá los métodos más generales para interactuar con el almacenamiento.

Constructores

Nuestra clase genérica incluye dos constructores, uno que por defecto usará el LocalFolder y otro que nos permitirá especificar el lugar de almacenamiento.

protected Repository(string fileName) : this(fileName, StorageType.Local)
{
}

protected Repository(string fileName, StorageType storageType)
{
   _fileName = fileName.EndsWith(".json") ? fileName : string.Format("{0}.json", fileName);

   // set the storage folder
   switch (storageType)
   {
      case StorageType.Local:
         _storageFolder = _appData.LocalFolder;

         break;
      case StorageType.Temporary:
         _storageFolder = _appData.TemporaryFolder;

         break;
      case StorageType.Roaming:
         _storageFolder = _appData.RoamingFolder;

         break;
      default: throw new Exception(String.Format("Unknown StorageType: {0}", storageType));
   }
}

El parámetro fileName nos sirve para especificar un nombre de archivo para nuestros datos. El archivo será identificado siempre por la extensión json y podemos decidir pasar el parámetro con o sin ella, en cualquier caso el constructor se encargará de realizar el chequeo necesario.

El parámetro storageType es un enumerado que incluye los distintos tipos de almacenamiento.

public enum StorageType
{
   Local,
   Temporary,
   Roaming
}

Dependiendo de este parámetro, el constructor inicializa  el StorageFolder a usar por nuestro repositorio.

Los métodos

/// <summary>
/// Delete a file asynchronously
/// </summary>
public async void DeleteAllAsync()
{
   var file = await GetFileIfExistsAsync(_fileName);
   if (file != null) await file.DeleteAsync();
}

Este método nos permite eliminar todos los datos almacenados. La operación se realiza de forma asíncrona y se resume a eliminar el archivo donde se encuentran almacenados nuestros datos.

El método GetFileIfExists nos retorna de forma asíncrona un StorageFile si el archivo existe, de lo contrario retorna nulo.

/// <summary>
/// At the moment the only way to check if a file exists to catch an exception... 
/// </summary>
/// <param name="fileName"></param>
/// <returns></returns>
private async Task<StorageFile> GetFileIfExistsAsync(string fileName)
{
   try
   {
      return await _storageFolder.GetFileAsync(fileName);
   }
   catch
   {
      return null;
   }
}

La única forma que tenemos de saber si un archivo existe es capturando el error. La razón que Microsoft nos da para no contar con un File.Exists en el API es que el estado del archivo puede cambiar entre que nosotros preguntamos si existe y realizamos alguna operación. Según Microsoft, si contáramos con un File.Exists, igualmente tendríamos que capturar posibles errores cuando trabajamos con el archivo.

/// <summary>
/// Persist data to StorageFolder
/// </summary>
public virtual void Flush()
{
   SaveAsync(Data.Value);
}

El método Flush usa Data.Value para indicar los datos a persistir. Esto no es más que el lugar en memoria donde está almacenado nuestro objeto.

protected Lazy<T> Data;

El uso de Lazy<T> nos permite acceder a StorageFolder en el momento en que se necesiten los datos  por primera vez.

Los datos de memoria se envían al StorageFolder que seleccionemos llamando a un método interno de nuestro repositorio que realiza la persistencia de forma asíncrona.

/// <summary>
/// Saves a serialized object to storage asynchronously
/// </summary>
/// <param name="data"></param>
protected async void SaveAsync(T data)
{
   if (data == null) return;

   var file = await _storageFolder.CreateFileAsync(_fileName, CollisionOption);
   await FileIO.WriteTextAsync(file, Serialize(data).Result);
}

El método interno SaveAsync crea nuestro archivo si este no existe, de lo contrario lo re-escribe.  Estamos usando ReplaceExisting como política de Colisión, de cualquier forma, esta  variable es protegida en nuestra clase base, así que puede ser cambiada por las clases que heredan.

protected CreationCollisionOption CollisionOption = CreationCollisionOption.ReplaceExisting;

De la misma manera que guardamos en el StorageFolder, también necesitamos leer. El siguiente método nos permite recuperar un objeto de forma asíncrona.

/// <summary>
/// Load a object from storage asynchronously
/// </summary>
/// <returns></returns>
protected async Task<T> LoadAsync()
{
   try
   {
      var file = await _storageFolder.GetFileAsync(_fileName);
      var data = await FileIO.ReadTextAsync(file);

      return await Deserialize(data);
   }
   catch (FileNotFoundException)
   {
      //file not existing is perfectly valid so simply return the default 
      return default(T);
   }
}

Tanto para leer, como para escribir datos en nuestro repositorio, se usan métodos que se encargan del trabajo de serialización del objeto.

Como cada cual tiene su propio mecanismo para serializar, estos métodos son declarados como abstractos dentro de nuestra clase base, de esta forma, podemos indicar en cada clase que herede, cual será la manera en que se serializarán los objetos, incluso, pudiéramos tener dentro de la misma aplicación, objetos que se persistan usando mecanismos de serialización totalmente distintos.

protected abstract Task<string> Serialize(T data);
protected abstract Task<T> Deserialize(string data);

He dejado para el final una propiedad que nos retorna nuestro objeto desde el StorageFolder o de memoria según sea el caso.

/// <summary>
/// Return T
/// </summary>
public virtual T All
{
   get { return Data.Value; }
}

Teniendo en cuenta que nuestro Repositorio puede valer para almacenar un simple objeto o una lista de objetos, tener una propiedad pública que nos retorne “todo” no es una muy buena práctica. De cualquier forma esto no viene a sustituir grandes almacenes de datos, ya que para eso tenemos bases de datos locales como SQLLite, sino que está pensado para pequeños datos. Es por esto que me tomo la libertad de incluir una “mala” práctica en mi código.

Nuestra clase base está lista, vamos a ver cómo usarla.

Para la implementación, voy a usar Newtonsoft.Json para la serialización de los objetos. Las clases a utilizar con los repositorios serán una simple y otra una lista, para poder ver las dos formas de utilizar nuestro repositorio.

public class Account
{
   [JsonProperty]
   public string Name { get; set; }
}
public class Family : List<Person>
{
   [JsonProperty]
   public IList<Account> Itenms { get; set; }
}

public class Person
{
   [JsonProperty]
   public Guid Id { get; set; }

   [JsonProperty]
   public string Name { get; set; }

   public bool IsTransient
   {
      get { return Id == Guid.Empty; }
   }
}

Nuestro repositorio siempre trabaja con un objeto, por lo que la única diferencia entre guardar un objeto simple o una lista, no es otra que usar uno o lo otro.

En el ejemplo crearemos un repositorio para Account y otro para Family que es una lista de Person.

Vamos a ver el repositorio de Family:

    public class RepositoryFamily : Repository<Family>, IRepositoryFamily
    {
        public RepositoryFamily(): base("Family")
        {
            CollisionOption = CreationCollisionOption.OpenIfExists;
            Data = new Lazy<Family>(() => LoadAsync().Result ?? new Family(), true);
        }

        public async Task<Person> Get(Guid id)
        {
            return await Task.Factory.StartNew(() => All.Single(p => p.Id == id));
        }

        public void Save(Person person)
        {
            if (person.IsTransient)
            {
                person.Id = IdentityGenerator.NewSequentialGuid();
                Data.Value.Add(person);

                return;
            }

            var idx = Data.Value.FindIndex(a => a.Id == person.Id);
            Data.Value[idx] = person;
        }

        protected override Task<string> Serialize(Family data)
        {
            return JsonConvert.SerializeObjectAsync(data);
        }

        protected override Task<Family> Deserialize(string data)
        {
            return JsonConvert.DeserializeObjectAsync<Family>(data);
        }
    }

Como se ve, implementamos los métodos abstractos del repositorio para serializar y ya solo nos queda ir implementando los métodos que necesitemos según el tipo de datos que estemos trabajando.

Para el ejemplo, tengo el método Save que adiciona una persona en la lista si no existe, o de lo contrario, lo actualiza.

Cuando deseemos guardar una nueva persona en nuestro repositorio, el código a utilizar sería así:

private void SaveFamilyNameCommand(string name)
{
   _familySrv.Save(new Person { Name = name});
   _familySrv.Flush();

   Family = new ObservableCollection<Person>(_familySrv.All);
   RaisePropertyChanged(()=> Family);
}

La aplicación que les dejo de ejemplo, en la vista principal tiene dos TextBox, uno para que escriba su nombre y otro para que adicione los nombres de su familia.

screenshot_10072012_135829

Después de escribir su nombre y guardar y después de entrar nombres de sus familiares, puede cerrar la aplicación y volverla a abrir para ver cómo los datos se persisten.

screenshot_10072012_135936

El ejemplo requiere MVVM Light y Newtonsoft.json. Source

Salu2

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *