19 жовтня 2010 р.

"Патерн Вівторка" #11: Прототип (Prototype)

Чи ви коли небуть процювали із Outlook або ж якимось іншим календарем, що дозволяє копіювати календарні зустрічі із одного дня на інший?

Для прикладу уявімо собі, що ваш друг назначив маленьку вечірку із пивом на п'ятницю, 22 жовтня, також він виділив час під вечірку із 7-мої вечора до 3-тьої ночі, поставив високий приорітет, а ще він зазначив, що вечірка в п'ятницю має бути всім до душі, оскільки то останній робочий день. Оскільки ви були запрошені, вечірка пройшна надзвичайно добре. Під кінець вечірки ваш друг вирішив вислати таке ж запрошення на наступну п'ятницю, але оскільки він уже добряче випив, для нього заповнити календарну форму видалося занадто важко. Яку можливість можна добавити в клалендар, щоб вона була використана другом? Швидше за все "copy-paste" функціональність.


ПРОТОТИП

Прототип, це такий дизайн патерн, який дозволяє нам створювати копії об'єктів, що уже визначені на стадії дизайну (наприклад, список можливих типів зустрічей) або ж визначаються під час виконання програми ("п'ятнична вечірка"), таким чином відпадає необхідність заповняти всі елементи об'єкту від А до Я. Вже створені або визначені екземпляри об'єкту називаються прототипічними екземплярами (prototypical instances).

Як уже було описано один приклад використання цього патерну із  копіюванням екземплярів об'єктів в час виконання програми, цей дизайн патерн дозволяє нам уникати великої кількості похідних класів. Для прикладу, замість того, що мати такі класи як "5 floors apartment block with 3 rooms flats", "9 floors apartment block with 2-3-4 rooms flats" і "12 floors apartment block with 1-2-3 rooms flats" ми можемо мати просто 3 екземпляри класу ApartmentBlock ініціалізовані вже із потрібними властивостями, а потім ми просто копіюємо один із екземплярів, коли  нашій будівельній компанії потрібно збудувати якийсь конкретний будинок десь в місті. Іншими словами ми можемо обійтися від написання подібного: "new ApBlockWith9FloorsAnd234Flats()" або "new ApartmentBlock(){Floors = 9; FlatsDic = {2,3,4}}".

Все чого нам буде достатньо є щось схоже на "_9FloorsApBlock.Clone()". Звичайно ми можемо поєднювати цей патерн із Фабричним Методом, таким чином ми будемо мати метод схожий на "GetMe9FloorsAppBlock()", який всередині буде викликати копіювання прототипічного екземпляру.


Приклад


Нумо глянемо на Прототип (Prototype), що визначає метод Сlone() для всіх наших конкретних прототипчиків.


    public class CalendarPrototype
    {
        public virtual CalendarPrototype Clone()
        {
            var copyOfPrototype = (CalendarPrototype)this.MemberwiseClone();
            return copyOfPrototype;
        }
    }

Нашим конкретним Прототипом є подія в календарі, яка виглядає приблизно так:


    public class CalendarEvent : CalendarPrototype
    {
        public Attendee[] Attendees { get; set; }
        public Priority Priority { get; set; }
        public DateTime StartDateAndTime { get; set; }
        // покищо ми не перевизначаємо метод копіювання
    }

Клієнтський код (Client code) виконується коли ваш друг відкриває календар і правою кнопкою мишки копіює подію, а потім вставляє в інше місце, таким чином автоматично помінявши дату та час початку події.


Глянемо на цей процес:

    public class PrototypeDemo
    {
        public static CalendarEvent GetExistingEvent()
        {
            var beerParty = new CalendarEvent();
            var friends = new Attendee[1];
            var andriy = new Attendee {FirstName = "Andriy", LastName = "Buday"};
            friends[0] = andriy;
            beerParty.Attendees = friends;
            beerParty.StartDateAndTime = new DateTime(2010, 7, 23, 19, 0, 0);
            beerParty.Priority = Priority.High();

            return beerParty;
        }

        public static void Run()
        {

            var beerParty = GetExistingEvent();
            var nextFridayEvent = (CalendarEvent)beerParty.Clone();
            nextFridayEvent.StartDateAndTime = new DateTime(2010, 7, 30, 19, 0, 0);

            // про цей код побалакаємо трішки нижче
            nextFridayEvent.Attendees[0].EmailAddress = "andriybuday@liamg.com";
            nextFridayEvent.Priority.SetPriorityValue(0);

            if (beerParty.Attendees != nextFridayEvent.Attendees)
            {
                Console.WriteLine("GOOD: Each event has own list of attendees.");
            }
            if (beerParty.Attendees[0].EmailAddress == nextFridayEvent.Attendees[0].EmailAddress)
            {
                //В цьому випадку є добре мати поверхневу копію кожного із учасників
                //таким чином моя адреса, ім'я і персональні дані залишаються тими ж
                Console.WriteLine("GOOD: If I updated my e-mail address it will be updated in all events.");
            }
            if (beerParty.Priority.IsHigh() != nextFridayEvent.Priority.IsHigh())
            {
                Console.WriteLine("GOOD: Each event should have own priority object, fully-copied.");
            }
        }
    }

Як можна бачити мій друг зробив копію існуючої події і за допомогою чудо функціоналу drag-drop змінив дату події. Оскільки я сидів із нашим другом, я помітив що я можу змінити свою адресу через цю подію, тому я її поміняв на нову, оскільки вона у мене змінилася. Також оскільки багато кому було погано після вечірки, то ми вирішини що наступного разу не будемо брати аж так багато пива, але оскільки пива не дуже багато, то й пріорітет не може бути сильно великим: 
nextFridayEvent.Priority.SetPriorityValue(0)

На перший погляд виглядає, що ми отримали те що хотіли - копію існуючої події із запрошеними, приорітетом та іншими властивостями. Але коли я відкриваю стару подію я помітив що приорітет став нейтральним замість того, щоб бути високим. Як? Причина у тому, що оскільки ми ще не перевизначали метод Clone для нашого прототипу була виконане поверхневе копіювання (MemberwiseClone) зазначене у базовому класі.

Поверхневе копіювання (Shallow copy) копіює тільки прямі поля класу, таким чином залишає ті ж посилання, якщо поле було reference типу, а якщо поле було value-типу, що буде нова копія.

Глибоке копіювання (Deep copy) копіює ціле дерево об'єктів, таким чином що об'єкти мають різні фізичні адреси у купі (heap).


Метод CLONE


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

В дизнай патерні Прототип ми реалізуємо метод Clone. Інколи нам може знадобитися повна глибока копія, яку ми можемо досягнути шляхом ручного копіювання, що може бути складним, за допомогою рефлекшину, що може бути повільно, або за допомогою сериліазації та дисериалізації в новий екземпляр об'єкту, що також може бути  досить дорого. Але часто вам може знадобитися часково повне копіювання, як у нашому прикладі. Ось чому багато мов програмування добавили інтерфейс Cloneable, що має бути реалізований вами самостійно. Найбільш підходяща реалізація для нашого прикладу може виглядати так:


      public override CalendarPrototype Clone()
        {
            var copy = (CalendarEvent)base.Clone();

            // this allows us have another list, but same attendees there
            var copiedAttendees = (Attendee[])Attendees.Clone();
            copy.Attendees = copiedAttendees;

            // we also would like to copy priority
            copy.Priority = (Priority)Priority.Clone();

            return copy;
        }

Консольний вивід сказав що все зроблено згідно плану. Оскільки я також маю той же приклад на джаві і в дебаг моді IDEA відображає змінні із однозначними ідентифікаторами посилань (числа після "собачок"), то я ще добавляю скріншот:



Надіюся, що цей короткий опис дизайн патерну не був нудний, тупий і так далі.

Моя табличка Патернів
Developer's RoadMap To Success

3 коментарі:

  1. Чудовий опис.
    Може хто має досвід використання якоїсь бібліотеки для deep copy?

    ВідповістиВидалити
  2. Вообще прототип - замечательный паттерн!

    ВідповістиВидалити
  3. Я такого досвіду не маю, але стикався із клонуванням за допомогою сериалізації/десиріалізації.

    ВідповістиВидалити