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 для кожної з стрічок і робиться перевірка на відповідність правилу яке описане в дочірньому класі. Якщо з розумінням поліморфізму виникнуть проблемо то в інтернеті можна знайти купу статей які це детально описують.

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