→ Пошук по сайту       Увійти / Зареєструватися
Знання Патерни Породжуючі патерни – Creational patterns

4. Прототип — Prototype

4.	Прототип — Prototype

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

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

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

Ми можемо використовувати цей дизайн патерн для копіювання екземплярів об’єктів в час виконання програми, що дозволяє нам уникати великої кількості похідних класів. Для прикладу, замість того, щоб мати такі класи як «п’ятиповерхова будівля із трикімнатними квартирами», «дев’ятиповерхова будівля із 2-3-4-кімнатними квартирами» і «дванадцятиповерхова будівля із 1-2-3-кімнатними квартирами», ми можемо мати просто 3 екземпляри класу ApartmentBlock, ініціалізовані вже з потрібними властивостями, а потім ми просто копіюємо один із екземплярів, коли нашій будівельній компанії потрібно збудувати якийсь конкретний будинок десь в місті.

Іншими словами, ми можемо обійтися без написання подібного: «new ApBlockWith9FloorsAnd234Flats()» або «new ApartmentBlock(){Floors = 9; FlatsDic = {2,3,4}}».

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

Уривок коду 4.1. Метод Сlone()

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

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

Уривок коду 4.2. Подія в календарі

 class CalendarEvent : CalendarPrototype
{
    public Attendee[] Attendees { get; set; }
    public Priority Priority { get; set; }
    public DateTime StartDateAndTime { get; set; }

    // Зауважимо, що метод Clone не перевантажений (покищо)
}

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

Уривок коду 4.3. Клієнтський код, що використовує патерн

 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: Update to my e-mail address will be reflected in all events.");
        }
        if (beerParty.Priority.IsHigh() != nextFridayEvent.Priority.IsHigh())
        {
            Console.WriteLine(
            "GOOD: Each event should have own priority object, fully-copied.");
        }
    }
}

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

Уривок коду 4.4. Зміна приорітету зустрічі

 nextFridayEvent.Priority.SetPriorityValue(0);

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

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

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

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

Корекція методу Clone у відповідності до наших потреб

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

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

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

Уривок коду 4.5. Підходящий метод Clone

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

    // Це дозволить нам мати інший список із посиланнями на тих же відвідувачів
    var copiedAttendees = (Attendee[])Attendees.Clone();
    copy.Attendees = copiedAttendees;
    // Також скопіюємо приорітет
    copy.Priority = (Priority)Priority.Clone();
    // День і час не варто копіювати – їх заповнять
    // Повертаємо копію події
    return copy;
}

На консоль вивелися усі три речення із словом «GOOD» — підтвердження, що все зроблено згідно плану. Оскільки я також маю той же приклад, написаний на Java, і в дебаг режимі IDEA відображає змінні із однозначними ідентифікаторами посилань (числа після «собачок»), то я ще добавлю скріншот нижче (див. Малюнок 1). Зеленим виділено ті самі елементи із тими ж посиланнями, що і в попередній зустрічі. Червоним підкреслено, що списки людей насправді різні, і на нову подію можуть бути запрошені і інші бажаючі. Також червоним показано спеціально скопійований приорітет. Дати і події, звичайно, різні.

Малюнок 1. Зображення об’єктів beerParty та nextFridayEvent в відлагоджуючому режимі IDEA

На цій сторінці є ще трохи місця і для UML-діаграми нашого прикладу.

UML-діаграма 4. Прототип

По матеріалам книги Андрія Будая "Дизайн патерни – просто, як двері". Матеріал розміщується за домовленістю з автором.
Робота представлена за умовами ліцензії Creative Commons Attribution-NonCommercial 3.0 Unported License.

загрузка...
Сторінки, близькі за змістом