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

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

8 comments:

  1. У мене є декілька зауважень. Можливо занадто простий приклад або це недолік того що я не знаю шарпа.

    Недолік 1: Назви змінних та класів. (konkretnePravylo, PravyloBase, pravylo) Надіюсь що ти в реальному проекті так не пишеш :)

    Недолік 2: Теоретично ти заюзав стратегію, але практично на мою думку ти добавив собі роботи. Припусти наступну ситуацію. Тобі потрібно добавити ще одне правило для валідації стрінга. В початковому варіанті, якщо я не помиляюсь тобі потрібно добавити лише один if. А от в кінцевому крім умови також потрібно створити ще один класс і добавити його створення в умову.

    А так узагалі виглядає непогано. Якщо трішки ускладнити приклад тоді справді буде видно переваги стратегії над іф кодом.

    Також хочу зауважити що на інших мовах програмування можливі інші варіанти реалізації стратегії. Для прикладу в с++ крім стандартного методу з використанням наслідування можна передавати класс як параметр шаблонного класу

    ПС: Це лише моя субєктивна думка. У мене навіть блогу немає :)
    vyadzhak )))

    ReplyDelete
  2. Привіт дякі за коментар.
    Діло в тому що в мене є два студентіка які хочуть вивчити програмування з нуля, справляються вони досить добре але пояснувати їм елементарні речі дуже важко. Отож:
    1 Назви змінних дійсно сумнівні але це лише тому що так виявилось легше обясняти значення і тд. в реальному коді фантазую набагто краще)
    2 коду збільшилось але ми відділили логіку виконання на частини які можна скомбінувати, в даному випадку це дійсно сумнівний плюс, але в наступному пості постараюсь добавити складності прикладу щоб показати переваги, так що to be continued...

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

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

    ReplyDelete
  3. >"куди метод повертає значення?"
    Дійсно цікаве питання. Мені навіть на думку таке не спадало. Але скільки ж можна було нафантазувати відповідаючи на це питання :)
    -
    vyadzhak

    ReplyDelete
  4. змінні тре навіть в прикладах нормально називати, навіть для студентів, бо навчаться так змінні називати і буде тяжко їх потім перевчити. ))))

    ReplyDelete
  5. Ок обовязково врахую це в наступних постах, можилво навіть проведу невеличкий рефакторинг...

    ReplyDelete
  6. "саме в цих випадках необхідно говорити на їхній мові, бо нашої поки що вони не розуміють"

    Я колись теж був студентом :))
    Так ось зі мною викладачі відразу розмовляли мовою програмістів :)

    Якщо з ними вперто розмовляти нашою мовою, а не їхньою, то вони врешті решт не витримають натиску і з часом почнуть розуміти чи захочуть зрозуміти :)

    Я вирішив написати блог-пост про один анекдот, який я часто розповідаю в подібних випадках:
    http://opolischuk.blogspot.com/2011/08/blog-post.html

    ReplyDelete
  7. "куди метод повертає значення?"

    Як куди??
    На зовні! :)
    Повертає з області видимості функції в тут область видимості де ця функція викликалась, і результат цього повернення бажано кудись присвоїти (записати) :)

    ReplyDelete
  8. >>"Повертає з області видимості функції в тут область видимості де ця функція викликалась"

    Треба попробувати таке сказати і сфотографувати вираз обличчя))

    ReplyDelete