[gAF] Implementación del soporte de los formatos de archivo

En el anterior post, comentábamos que, actualmente, el software ActiLife nos permite recuperar la información en tres formatos de archivos diferentes: Texto plano (.dat), Valores separados por comas (.csv) y base de datos SQLite (.agd).

En gAF, una vez que un archivo es leído, su información es almacenada en una clase llamada GafFile. Esta clase contiene la información necesaria para realizar diferentes tipos de análisis y proceso de la información contenida en los diferentes ficheros.

Por lo tanto, necesitamos un mecanismo que nos permita, en función de la extensión del archivo, leer la información de los diferentes formatos y almacenarla en una instancia de GafFile.

Abstract Factory Pattern al rescate

Una buena opción para resolver esta situación es utilizando el patrón de factoría abstracta. Básicamente, tenemos dos interfaces que definen los contratos de las factorías y de los lectores de archivos. Más o menos así:

    interface IGafFileReaderFactory
    {
        string SupportedFormat { get; }

        IGafFileReader CreateReader();
    }
    interface IGafFileReader
    {
        GafFileHeader GetDatInfo(string path);

        GafFile GetDat(string path, bool useThirdAxis);
    }

De esta forma, cada factoría es responsable de instanciar un GafFileReader en función de si el formato del archivo a leer coincide con el formato soportado por la factoría. De esta forma, el GafFileReader instanciado para una extensión, conoce perfectamente qué es lo que tiene que hacer con ese archivo para convertirlo a un GafFile.

La implementación de una factoría es trivial. Sólo ha de definir la extensión que conoce y crear el GafFileReader específico para esa extensión.

    class PlainTextFileReaderFactory : IGafFileReaderFactory
    {
        public string SupportedFormat
        {
            get
            {
                return ".dat";
            }
        }

        public IGafFileReader CreateReader()
        {
            return new PlainTextFileReader();
        }
    }

Y por supuesto, la clase PlainTextFileReader, ha de implementar la interfaz IGafFileReader y especializarse en la interpretación de los archivos con la extensión .dat.

Y para dejarlo fácil: Facade Pattern

Para que la API sea sencilla de utilizar para los clientes, es necesario abstraerles del uso de esta factoría abstracta. Para conseguirlo se utiliza una fachada que expone las operaciones que el cliente necesita para obtener un GafFile.

En este caso, como fachada se utiliza una clase estática que ofrece las operaciones necesarias para recuperar un GafFile. Básicamente, ofrece las dos operaciones de la interfaz IGafFileReader, así como algunos métodos auxiliares. Esta clase mantiene una lista privada con las factorías registradas en el sistema (actualmente se cargan desde el archivo de configuración de la aplicación) y, lo que hace es delegar en las factorías registradas la ejecución de las operaciones necesarias.

    public static class GafFileReader
    {
        #region Campos privados

        private static readonly List<IGafFileReaderFactory> factories;

        #endregion

        #region Constructores

        static GafFileReader()
        {
            factories = new List<IGafFileReaderFactory>();

            var configfactories = GafConfigurationManager.GafSection.ReaderFactories
                .OfType<System.Configuration.NameValueConfigurationElement>()
                .Select(configFactory => Type.GetType(configFactory.Value))
                .Select(factoryType => (IGafFileReaderFactory)Activator.CreateInstance(factoryType));

            foreach (var factory in configfactories)
            {
                factories.Add(factory);
            }
        }

        public static IEnumerable<string> RegisteredFormats
        {
            get
            {
                return factories.Select(f => f.SupportedFormat);
            }
        }

        #endregion

        #region Métodos

        #region Métodos públicos

        public static GafFileHeader GetDatInfo(string path)
        {
            if (string.IsNullOrEmpty(path))
            {
                throw new ArgumentException(Resources.GafFileReader_El_valor_no_puede_ser_nulo_ni_una_cadena_vacia, "path");
            }

            var reader = GetParser(path);

            return reader.GetDatInfo(path);
        }

        public static GafFile GetDat(string path, bool useThirdAxix)
        {
            if (string.IsNullOrEmpty(path))
            {
                throw new ArgumentException(Resources.GafFileReader_El_valor_no_puede_ser_nulo_ni_una_cadena_vacia, "path");
            }

            var reader = GetParser(path);

            return reader.GetDat(path, useThirdAxix);
        }

        #endregion

        #region Métodos privados

        private static IGafFileReader GetParser(string path)
        {
            string extension = Path.GetExtension(path);

            if (string.IsNullOrWhiteSpace(extension))
            {
                throw new ArgumentException(string.Format(Resources.GafFileReader_No_se_encuentra_la_extension_del_archivo, path), "path");
            }

            extension = extension.ToLower();

            var factory = factories.FirstOrDefault(x => x.SupportedFormat.Contains(extension));

            if (factory == null)
            {
                throw new InvalidOperationException(string.Format(
                        CultureInfo.CurrentCulture,
                        "El archivo que se intenta analizar tiene una extensión desconocida: [{0}].",
                        extension));
            }

            return factory.CreateReader();
        }

        #endregion

        #endregion
    }

De esta forma cualquier cliente, para recuperar un GafFile de un archivo, lo único que tiene que hacer es llamar al método estático de la fachada de lectura de archivos sin preocuparse de qué tipo de archivo está intentando leer.

¡Hasta la próxima!