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
Це далеко не вершина оптимізації та гнучкості і тут ще багато чого можна спростити та вдосконалити але на даному кроці я думаю варто зупинитись, тому що метою є показати те як застосовують паттерни і надіюсь і цією задачою я справився. На часте питання “як взанти який патерн використати?” відповідь досить проста -- знаючи їх просто треба знати. Якщо щось висвітлив недостатньо буду радий доповнити і тд.

Tuesday 19 July 2011

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

Це доволі часте питання серед молодих девелоперів і іноді саме слово відлякує так як вчити й так ще багато, а тут ще й якісь патерни повидумували, але не все так страшно.

Отож патерни - це  підхід до вирішення популярних проблем в програмуванн, який оправдав себе за багато років. Тобто досить часто програмісти стикаються з подібними задачами і для того щоб не винаходити велосипед заново, згрупували певні підходи для вирішення проблем і назвали їх патернами. Значення самого слова можна прочитати в вікіпедії тут.

Розрізняють три види патернів:

  • Породжувальні - патерни основною метою яких є створення обєктів.
  • Поведінкові - патерни для реалізації певної поведінки в обєкті.
  • Структурні - опишу окремо, поки що достатньо знань про те що вони є.

Хорошу табличку з описанням цих патернів можа знайти на сайті Львівської групи .NET

Перший патерн з яким я вам раджу ознайомитись це Стратегія, його я й використаю в тестовій програмі.

Завдання:

Перевірити масив стрічок на відповідність певним правилам, та вивести на екран результат перевірки.

Рішення:

using System;
 
namespace StringValidator
{
    internal class Program
    {
        private static string[] strings = { "one", "two", "three", "four" };
 
        static void Main(string[] args)
        {
            Console.WriteLine("Введіть одне з правил валідації (length, space, other)");
            
            string pravylo = Console.ReadLine();
 
            foreach (string s in strings)
            {
                if (pravylo == "length")
                {
                    checkLength(s);
                }
                if (pravylo == "space")
                {
                    checkSpaces(s);
                }
                if (pravylo == "other")
                {
                    checkOther(s);
                }
            }
        }
 
        private static bool checkOther(string s)
        {
            Console.WriteLine("Check other");
            return true;
        }
 
        private static bool checkSpaces(string s)
        {
            Console.WriteLine("Check space");
            return true;
        }
 
        private static bool checkLength(string s)
        {
            Console.WriteLine("Check length");
            return true;
        }
    }
}

Це найпростіше рішення, воно робить все що треба але якщо нам треба буде добавити нове правило валідації, то ми будемо змушені додати ще один if в цикл перебору стрічок і ще один метод для перевірки відповідного правила. В випадку великої системи це буде зробити важче ніж в даному випадку, а при умові того що доступу до класу з циклом перебору немає(наприклад він зроблений у вигляді dll) то це вже стає практично неможливою задачею.

Отже метою змін буде розділення цього коду на такий який би було легко доповнювати новими правилами.

Перше що нам треба зробити це винести цикл перевірки слів в окремий метод, назвемо його Validate(string pravylo) – як бачимо метод буде приймати назву правила за яким необхідно перевірити стрічки.

using System;
 
namespace StringValidator
{
    internal class Program
    {
        private static string[] strings = { "one", "two", "three", "four" };
 
        static void Main(string[] args)
        {
            Console.WriteLine("Введіть одне з правил валідації (length, space, other)");
            
            string pravylo = Console.ReadLine();
 
            Validate(pravylo);
        }
 
        private static void Validate(string pravylo)
        {
            foreach (string s in strings)
            {
                if (pravylo == "length")
                {
                    checkLength(s);
                }
                if (pravylo == "space")
                {
                    checkSpaces(s);
                }
                if (pravylo == "other")
                {
                    checkOther(s);
                }
            }
        }
 
        private static bool checkOther(string s)
        {
            Console.WriteLine("Check other");
            return true;
        }
 
        private static bool checkSpaces(string s)
        {
            Console.WriteLine("Check space");
            return true;
        }
 
        private static bool checkLength(string s)
        {
            Console.WriteLine("Check length");
            return true;
        }
    }
}

Це спростило наш головний метод і в ньому лише залишилось те що там повинно на даний час бути, вивід повідомлення для користувача, зчитування правила і виклик метода який вже й зробить перевірку даного правила.

Хвилинку на роздуми… Було б дуже добре якщо б наш метод Validate мав код схожий на такий:

private static void Validate(string pravylo)
{
    konkretnePravylo = GetPravylo(pravylo); // тут ми отримуємо обєкт який вміє валідувати за конкретним правилом
    foreach (string s in strings)
    {
        konkretnePravylo.CheckString(s);   // тут викликаємо валідацію передаючи в чкості аргумента нашу стрічку
    }
}

в даному випадку CheckString автоматично підставляє те правило за яким необхідно провірити стрічку.

Це б максимально спростило код і те що ми відмовились від виклику конкретних методів дало б нам змогу в майбутньому підставити туди нові правила валідації. Поки що не важно як саме це реалізувати головне сама концепція, що відповідно від того яке правило ми ввели наша програма сама підставляє його в метод CheckString. Для того щоб це реалізувати треба трошки покодити але перевага яка буде в результаті дійсно цього варта.

Перше що ми зроби так це створимо базовий клас для всіх класів з правилами валідації і в ньому опишемо лише один метод який всі дочірні класи повинні перевизначити.

namespace StringValidator
{
    public abstract class PravyloBase
    {
        public abstract bool CheckString(string s);
    }
}

Класс дійсно простий. Тепер для кожно правила зробимо свій класс і перенесемо туди відповідні методи валідаії:

LengthPravylo.cs

using System;
 
namespace StringValidator
{
    public class LengthPravylo : PravyloBase
    {
        public override bool CheckString(string s)
        {
            Console.WriteLine("Check length");
            return true;
        }
    }
}
OtherPravylo.cs
using System;
 
namespace StringValidator
{
    public class OtherPravylo : PravyloBase
    {
        public override bool CheckString(string s)
        {
            Console.WriteLine("Check other");
            return true;
        }
    }
}
SpacePravylo.cs
using System;
 
namespace StringValidator
{
    public class SpacePravylo : PravyloBase
    {
        public override bool CheckString(string s)
        {
            Console.WriteLine("Check space");
            return true;
        }
    }
}

Отож маємо три класи які наслідуються від одного й того ж класу і реалізують його метод CheckString

Давайте попробуємо змінимо наш основний клас для того щоб він використовував ці класи з валідацією:

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 pravylo = Console.ReadLine();
 
            Validate(pravylo);
        }
 
        private static void Validate(string pravylo)
        {
            foreach (string s in strings)
            {
                if (pravylo == "length")
                {
                    LengthPravylo lengthPravylo = new LengthPravylo();
                    lengthPravylo.CheckString(s);
                }
                if (pravylo == "space")
                {
                    SpacePravylo spacePravylo = new SpacePravylo();
                    spacePravylo.CheckString(s);
                }
                if (pravylo == "other")
                {
                    OtherPravylo otherPravylo = new OtherPravylo();
                    otherPravylo.CheckString(s);
                }
            }
        }
    }
}

Виглядає поки що не так як ми хотіли, але працює так само.

Тепер ці іф умови що залишились в нашому методі необхідно винести десь назовні в інший клас для того щоб їх можна було модифікувати за необхідності а головний клас привести до тої форми в якій він нічого не буде знати з яким правилом він працює, все що від нього вимагається це викли відповідного методу.

Ось цей клас:

namespace StringValidator
{
    public class Logic
    {
        public PravyloBase GetPravylo(string pravylo)
        {
            if (pravylo == "length")
            {
                return new LengthPravylo();
            }
            if (pravylo == "space")
            {
                return new SpacePravylo();
            }
            if (pravylo == "other")
            {
                return new OtherPravylo();
            }
            return null;
        }
    }
}

Він повертатиме нам відпоівдно до назви правила обєкт який має метод CheckString. Знову змінюємо код для того щоб він працював з нашим класом логіки.

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 pravylo = Console.ReadLine();
 
            Validate(pravylo);
        }
 
        private static void Validate(string pravylo)
        {
            Logic logic = new Logic(); 
            PravyloBase konkretnePravylo = logic.GetPravylo(pravylo);
 
            foreach (string s in strings)
            {
                konkretnePravylo.CheckString(s);
            }
        }
    }
}

В результаті ми отримуємо результат досить схожий на те що я описав швидше в даній статті.

Тут використовується поліморфізм, тобто в стрічці “PravyloBase konkretnePravylo = logic.GetPravylo(pravylo);” метод logic.GetPravylo(pravylo);  повертає нам дочірній обєкт який приводиться до свого базового класу, а далі викликається метод CheckString для кожної з стрічок і робиться перевірка на відповідність правилу яке описане в дочірньому класі. Якщо з розумінням поліморфізму виникнуть проблемо то в інтернеті можна знайти купу статей які це детально описують.

Приводячи нашу програму до такого вигляду ми фактично змініли її і реалізували патерн “стратегія”. Тут ми досягли гнучкості додавання нових правил з меншою кількістью модифіацій існуючого коду, а мінімальна зміна існуючого коду в свою чергу, це менша ймовірність щось зламати.