Saturday, 30 July 2011

Що таке патерни в програмуванні (II)

В минулому пості, я намагався обяснити що таке патерни в програмуванні, я назвав три види патернів і привів приклад патерну “стратегія”. Метою цього посту буде заповнити пробіли які залишились та виправити деякі проблеми з кодом.

Теорія

Патерн – це підхід до вирішення частих задач в програмуванні.
Програміст пишучи код може організувати його так як треба навіть незнаючи про патерни але це зазвичай займає більше часу, тому всі найбільш часті проблеми та способи їх вирішення були описані в книзі “банди чотирьох”. До цього їх використовували але автори першими додумались обєднати найкращі практики програмування в одній книзі за що й здобули всесвітню славу. Серед основних переваг які дає знання патернів, є те, що вам не треба довго описувати те, що ви хочете зробити і як ви це будете реалізовувати, вам необхідно лише вказати назву патерну і все, для всіх зразу ж стане зрозуміло що ви маєте на увазі. Також ці знання дають можливість краще зрозуміти сам принцип роботи деяких механізмів .NET. Дуже часто просто їхнє перечитування не дає жодного ефекту до того часу, поки ви самі не захочете їх реалізувати в конкретному прикладі…
Отож повертаємось до типів патернів, в нас як я вже писав є три типи: породжувальні, поведінкові та структурні.
Породжувальні патерни використовують для створення екземплярів обєктів. Наприклад іноді для більшої гнучкості створення всіх обєктів ми виносимо в один класс. Маючи доступ до цього класу в любому місці програми ми можемо отримати необхідний нам обєкт. Це знову ж таки в простих прикладах буде вимагати багато “лишнього” коду, але такі практики досить часто використовуються в великих системах і саме там вони дозволяють полегшити життя.
Поведінкові патерни використовують для того щоб запрограмувати певну поведінку обєкту, наприклад ця ж стратегія, в якій працюючи з обєктом приведеним до базового ми можемо досягати різної поведінки, хочемо добавити новий функціонал – без проблем пронаслідувались від базового класу, реалізували те що нам треба і добавили в список з іншими обєктами.
Структурні патерни описують те як можна на основі декількох малих обєктів створити більший, призначений для конкретної цілі.
Повертаємось до нашого славнозвісного проекту. Сьогодні я постарюсь обяснити чому той “лишній код” насправді не є лишнім і ми попробуємо виділити з нашої прорами той код який би можна було перевикористовувати.
Отож спочатку приведемо наш код до такого вигляду:
Program.cs
using System;
 
namespace StringValidator
{
    internal class Program
    {
        private static string[] strings = { "one", "two", "three", "four" };
 
        private static void Main(string[] args)
        {
            Console.WriteLine("Введіть одне з правил валідації (length, space, other)");
 
            string validationType = Console.ReadLine();
 
            Validate(validationType);
        }
 
        private static void Validate(string validationType)
        {
            Logic logic = new Logic();
            BaseValidation validator = logic.GetRule(validationType);
 
            foreach (string s in strings)
            {
                validator.CheckString(s);
            }
        }
    }
}
BaseValidation.cs
namespace StringValidator
{
    public abstract class BaseValidation
    {
        public abstract bool CheckString(string s);
    }
}
Logic.cs
namespace StringValidator
{
    public class Logic
    {
        public BaseValidation GetRule(string pravylo)
        {
            if (pravylo == "length")
            {
                return new LengthValidation();
            }
            if (pravylo == "space")
            {
                return new SpaceValidation();
            }
            if (pravylo == "other")
            {
                return new OtherValidation();
            }
            return null;
        }
    }
}
LengthValidatoin.cs
using System;
 
namespace StringValidator
{
    public class LengthValidation : BaseValidation
    {
        public override bool CheckString(string s)
        {
            Console.WriteLine("Check length");
            return true;
        }
    }
}
SpaceValidation.cs
using System;
 
namespace StringValidator
{
    public class SpaceValidation: BaseValidation
    {
        public override bool CheckString(string s)
        {
            Console.WriteLine("Check space");
            return true;
        }
    }
}
OtherValidation.cs
using System;
 
namespace StringValidator
{
    public class OtherValidation: BaseValidation
    {
        public override bool CheckString(string s)
        {
            Console.WriteLine("Check other");
            return true;
        }
    }
}
В принципі я лише перейменовував змінні та назви класів, так що відслідкувати, що де змінилось важко не буде.
Тепер попробуємо з нашого проекту виділити ту частину яка б відповідала за валідацію. Нашим завданням буде розробка бібліотеки, яка б могла валідувати стрічки по певним правилам. Самі правила можуть добавлятись до неї динамічно в залежності від того що нам треба в даному випадку. Отож додаємо новий проект до солюшина типу ClassLibrary і називаємо його StringValidatorLib. В нього переносимо всі файли окрім Program.cs . В перенесених файлах міняємо неймспейси, для того щоб вони відповідали новій локації.
В результаті ми отримуємо :
image
Не забуваємо про референс з нашої консольної аплікації на нашу бібліотеку.
Тепер давайте зробимо наш клас з логікою універсальним, щоб він нічого не знав про існуючі рули.
using System.Collections.Generic;
 
namespace StringValidatorLib
{
    public class Logic
    {
        Dictionary<string, object> rules = new Dictionary<string, object>(); 
 
        public void AddRule(string ruleName, object rule)
        {
            if(!rules.ContainsKey(ruleName))
            {
                rules.Add(ruleName, rule);
            }
        }
 
        public BaseValidation GetRule(string ruleName)
        {
            if(rules.ContainsKey(ruleName))
            {
                return (BaseValidation)rules[ruleName];
            }
            return null;
        }
    }
}
Тепер ми зможемо додавати нові рули і отримувати рули по назві. Погодьтесь це досить гнучко так як наша логіка нічого про те з якими обєктами вона працює не знає(далі можна буде приймати не object а нащадків BaseValidation і це забезпечить нас від хибних обєктів в словнику).
Для того щоб використати нашу логіку нам тепер спочатку треба зареєструвати всі необхідні правила, тобто ми можемо на підставі певних умов додавати ті чи інші рули при тому логіка в нас абсолютно незмінна і відповідно ймовірність нових помилок зменшується в рази. Виглядає це наступним чином:
using System;
using StringValidatorLib;
 
namespace StringValidator
{
    internal class Program
    {
        private static string[] strings = { "one", "two", "three", "four" };
 
        private static void Main(string[] args)
        {
            Console.WriteLine("Введіть одне з правил валідації (length, space, other)");
 
            Logic logic = new Logic();
            logic.AddRule("length", new LengthValidation());
            logic.AddRule("space", new SpaceValidation());
            logic.AddRule("other", new OtherValidation());
 
            string validationType = Console.ReadLine();
 
            Validate(validationType);
        }
 
        private static void Validate(string validationType)
        {
            Logic logic = new Logic();
            BaseValidation validator = logic.GetRule(validationType);
            if (validator != null)
            {
                foreach (string s in strings)
                {
                    validator.CheckString(s);
                }
            }
        }
    }
}
Також слід зазначити що навіть у випадку втрати коду бібліотеки, ми всеодно зможемо розширювати її новими правилами а це дуже великий плюс.
PS
Це далеко не вершина оптимізації та гнучкості і тут ще багато чого можна спростити та вдосконалити але на даному кроці я думаю варто зупинитись, тому що метою є показати те як застосовують паттерни і надіюсь і цією задачою я справився. На часте питання “як взанти який патерн використати?” відповідь досить проста -- знаючи їх просто треба знати. Якщо щось висвітлив недостатньо буду радий доповнити і тд.