lut
8
2012

Prosty moduł zarządzający konfiguracją

Ostatnio stworzyłem dość prosty i wydaje mi się fajny moduł konfiguracyjny :) Potrafi trzymać np. w bazie danych dowolne obiekty konfiguracyjne, pobierać je i zmieniać jednocześnie dbając o ich unikalność. Całość  kodu to jeden interfejs, jedna klasa abstrakcyjna i dwie klasy implementujące :) Zacznijmy od ogólnego zarysu architektury (o ile przy takim prostym rozwiązaniu można mówić o architekturze :)) Za jej całość odpowiada: interfejs IConfigurationProvider oraz klasa abstrakcyjna ConfigObject. Przyjrzyjmy się ich kodowi:

public interface IConfigurationProvider
    {
        void RegisterConfigurationObject(ConfigObject obj);
        T GetConfigObject<T>(string id) where T:ConfigObject;
        void Save(ConfigObject obj);
    }
public abstract class ConfigObject
    {
        protected IConfigurationProvider _configProvider;
        public bool IsValid { get; set; }

        public ConfigObject(IConfigurationProvider provider)
        {
            _configProvider = provider;
            _configProvider.RegisterConfigurationObject(this);
        }

        public abstract void RestoreState(string xmlState);
        public abstract string GetState();
        public abstract string UniqueID { get; }
        public string RealID 
        {
            get { return this.GetType() + UniqueID; } 
        }

        public void Save()
        {
            _configProvider.Save(this);
        }
    }

Obiekt implementujący IConfigurationProvider ma zadanie zarządzać obiektami typu ConfigObject. Sa tam 3 metody: RegisterConfigurationObject służąca do zarejestrowania obiektu, GetConfigObject służąca do pobrania obiektu danego typu o podanym kluczu oraz Save zapisująca "trwale" stan obiektu. Wydzielenie tej logiki do interfejsu ma oczywiście tę zaletę, że te funkcje możemy zaimplementować na różne sposoby (np. wykorzystując bazę danych albo zwykłe pliki ew. jeszcze inne źródło do trwałego zapisu stanu). Rodzi się tu od razu pytanie: co to za klucz który należy podać w metodzie GetConfigObject ? Otóż jest to jakiś string pozwalający zidentyfikować obiekt w przypadku kiedy mamy ich zarejestrowanych kilka (o tym samym typie). Ok, ale skąd go bierzemy ? To już jest zadanie obiektu ConfigObject - jego metoda UniqueID ma zwrócić ciąg pozwalający jednoznacznie zidentyfikować obiekt. Jeżeli przyjrzymy się implementacji ConfigObject zauważymy, że wykorzystuje ona metody Save i RegisterConfigurationObject interfejsu IConfigurationProvider  dostarczonego jej w konstruktorze więc de facto nie ma potrzeby wywoływać ich ręcznie. Wystarczy utworzyć instancję typu ConfigObject, ona się sama zarejestruje a przy zapisie sama wywoła provider. 

Przykładowa implementacja IConfigurationProvider trzymająca zapisy w bazie danych wygląda następująco:

public class DbConfigurationProvider : IConfigurationProvider
    {
        protected List<ConfigObject> _configObjects;
        protected string _connectionString;
        private SqlConnection _sqlConnection;
        protected SqlConnection Connection
        {
            get
            {
                if (_sqlConnection == null)
                {
                    _sqlConnection = new SqlConnection(_connectionString);
                }
                if (_sqlConnection.State != System.Data.ConnectionState.Open)
                {
                    try
                    {
                        _sqlConnection.Open();
                    }
                    catch
                    {
                        _sqlConnection = new SqlConnection(_connectionString);
                        _sqlConnection.Open();
                    }
                }
                return _sqlConnection;
            }
        }

        public DbConfigurationProvider(string connString)
        {
            _connectionString = connString;
            _configObjects = new List<ConfigObject>();
        }

        #region IConfigurationProvider Members

        public void RegisterConfigurationObject(ConfigObject obj)
        {
            if (_configObjects.FirstOrDefault(f => (f.RealID == obj.RealID)) != null)
            {
                throw new ArgumentException("Istnieje już objekt o tym typie i ID: " + obj.RealID);
            }
            _configObjects.Add(obj);
        }

        public T GetConfigObject<T>(string id) where T : ConfigObject
        {
            string realId = typeof(T) + id;
            var ob = _configObjects.FirstOrDefault(f => f.RealID == realId);
            if (ob == null)
            {
                throw new ArgumentException("Nie zarejestrowano typu o id: " + realId);
            }
            var obj = (T)ob;
            if (!obj.IsValid)
            {
                RestoreState(obj);
            }
            return obj;
        }

        private void RestoreState(ConfigObject obj)
        {
            string sql = "select value from Setting where [key] = @key";
            string state = null;
            using (SqlCommand cmd = new SqlCommand(sql, Connection))
            {
                cmd.Parameters.AddWithValue("key", obj.RealID);
                object res = cmd.ExecuteScalar();
                if (res != null)
                {
                    state = (string)res;
                }
                else
                {
                    sql = "insert into Setting([key],value) values (@key, @value)";
                    cmd.CommandText = sql;
                    state = obj.GetState();
                    cmd.Parameters.AddWithValue("value", state);
                    cmd.ExecuteNonQuery();
                }
            }
            obj.RestoreState(state);
        }

        public void Save(ConfigObject obj)
        {
            if (!obj.IsValid)
            {
                RestoreState(obj);
            }
            string sql = "update Setting set value = @value where [key] = @key";
            using (SqlCommand cmd = new SqlCommand(sql, new SqlConnection(_connectionString)))
            {
                cmd.Connection.Open();
                cmd.Parameters.AddWithValue("key", obj.RealID);
                cmd.Parameters.AddWithValue("value", obj.GetState());
                if (cmd.ExecuteNonQuery() != 1)
                {
                    throw new ConfigurationErrorsException("Nie udało się zaktualizować konfiguracji:");
                }
            }
            obj.IsValid = false;
        }

        #endregion
    }

Natomiast najprostszy obiekt konfiguracyjny przedstawia fragment kodu:

public class SimpleConfigValue<T> : ConfigObject
    {
        private string _key;
        public T Value { get; set; }

        public SimpleConfigValue(string key, IConfigurationProvider provider)
            : base(provider)
        {
            _key = key;
        }

        public override void RestoreState(string xmlState)
        {
            XmlSerializer serializer = new XmlSerializer(typeof(T));
            MemoryStream ms = new MemoryStream(Encoding.Unicode.GetBytes(xmlState));
            ms.Position = 0;
            Value = (T)serializer.Deserialize(ms);
        }

        public override string GetState()
        {
            XmlSerializer serializer = new XmlSerializer(typeof(T));
            StringBuilder sb = new StringBuilder();
            XmlWriter xmlWriter = XmlWriter.Create(sb);
            serializer.Serialize(xmlWriter, Value);
            return sb.ToString();
        }

        public override string UniqueID
        {
            get { return _key; }
        }
    }