В своей незаконченной книге по Джит Кун До известный актёр и боец Брюс Ли написал:
«До изучения боевого искусства удар рукой был для меня просто ударом рукой, а удар ногой просто ударом ногой. После изучения боевого искусства удар рукой перестал быть ударом рукой, а удар ногой - ударом ногой. Теперь, когда я понимаю боевое искусство, удар рукой - это просто удар рукой, удар ногой - просто удар ногой».
Может показаться странным начинать статью про паттерны проектирования в ООП с цитаты Брюса Ли, но именно это высказывание отражает суть процесса, с которым сталкиваются люди, начинающие систематически изучать то, что раньше делали не задумываясь.
1 - о нём речь пойдёт дальше в этой статье. 2 - это пример паттерна Прототип (Prototype). 3 - не путайте лисповские символы с элементарными частями строки (char) и с символами в Ruby (где они равнозначны ключевым словам в CL). В данном случае символ означает имя некоторого объекта, например, имя класса, который нужно инстанциировать. 4 - о декорировании функций речь пойдёт в 3-ей статье цикла. 5 - Haskell – один из самых математизированных языков программирования, поэтому данная конструкция, как и паттерн Абстрактная Фабрика, является выражением математического понятия «алгебраический тип данных». 6 - в Java 7, правда, обещают ввести специальный тип для создания ссылок на методы - MethodHandle.
Введение, или Моё кунг-фу сильнее твоего
Если вы когда-либо занимались боевыми искусствами, то наверняка запомнили то ощущение «неправильности» всех движений, которые вы делали до прихода в секцию или школу. «Ты неправильно стоишь», «ты неправильно бьёшь», «так делать нельзя, надо делать вот так» – примерно такие фразы, скорее всего, говорил вам тренер. И вы в точности следовали его инструкциям, переделывая свои «неправильные» удары, которые, тем не менее, много раз спасали вас на улицах, в «правильные», хотя и неудобные и неестественные. Через месяц или два занятий вы случайно ввязываетесь в потасовку, где как раз и должны бы пригодиться новые знания, но вместо того, чтобы помочь вам, «правильные», но всё ещё неудобные движения замедляют и сковывают вас. После нескольких неудачных попыток применить «крутой приём» вы бросаете это дело и начинается драться так, как привыкли. И побеждаете. Так что же? Вы зря занимались и все боевые искусства – это чушь? Примерно в такую же ситуацию часто попадают люди, изучающие паттерны проектирования. Вначале они «отрабатывают» определённые паттерны, стараясь применить их везде, где это возможно, попутно пытаясь отучиться от «неправильного» стиля программирования. Паттерны кажутся неестественными и надуманными, но ведь большой умный «тренер» (старший товарищ, менеджер, автор книги) сказал, что так надо, значит так надо. В какой-то момент эти люди начинают чувствовать подвох. Модуль становится всё сложнее и сложнее модифицировать, а затраты на поддержку паттерна становятся сравнимы с затратами на переписывание всего модуля без него. В конечном итоге они отказываются от изначальной идеи и выбрасывают из кода все паттерны. И, voilà! Система становится проще, красивей, да ещё и производительней впридачу. Значит ли это, что паттерны – зло? Нет, не значит. Это значит лишь то, что цепочка размышлений при проектировании системы была выстроена неверно. Паттерны становятся культом, и тогда порядок мыслей выглядит так: вместо того чтобы выглядеть так: Другими словами, не нужно искать паттерны там, где их нет. Равно как и пытаться соблюсти все правила их написания. Тогда, когда вы поймёте паттерны, Адаптер станет для вас просто адаптером между двумя интерфейсами, а Стратегия1 - просто стратегией некоторого действия.Factory Method, или Существительные и глаголы
Однако всё это философия и общая теория. Следовать ей, несомненно, надо, но также надо и делать что-то на практике. А на практике мы будем писать библиотеку для моделирования автомобилей. Суть в том, чтобы дать пользователю нашей библиотеки возможность создавать автомобили, модернизировать их, замерять параметры и даже проводить соревнования между разными марками. Обратите внимание: писать мы будем не законченную программу, а именно библиотеку, а это значит, что мы не сможем делать никаких предположений насчёт того, откуда и в каком контексте будет вызван наш код. Это также значит, что мы будем обязаны сохранить обратную совместимость при выходе новой версии нашей библиотеки. Итак, начнём. В соответствии с принципом программирования через интерфейсы, который мы определили в части 1, сразу же создадим интерфейс Car и заставим все классы автомобилей его реализовать. Код для работы с автомобилями, в таком случае, будет выглядеть примерно так: Car c1 = new Ford(); Car c2 = new Audi(); Car c3 = new Ferrari(); // do something with the cars От каждого класса автомобиля могут быть унаследованы другие классы, уточняющие его. Например, у класса Ford могут быть наследники FordFocus, FordFiesta, FordTaurus и т.д. Хорошо, если вам и пользователям вашей библиотеки известно, какие именно классы машин необходимо использовать в коде, однако, что делать, если конкретные классы во время написания кода неизвестны? Например, если вы хотите эмулировать соревнования по скорости между двумя марками автомобилей, вы наверняка захотите выбрать для этого самые быстрые модели из своих семейств. Можно, конечно, просчитать вручную все скорости и подставить в нужные места конкретные классы, но что делать, если пользователь вашей библиотеки захочет переопределить это поведение? Естественный выход – инкапсулировать знание о выборе конкретной модели в отдельном методе и позволить пользователю переопределять его. Код создания экземпляра автомобиля будет выглядеть так: Car fastestFord = fordFactory.createFastest(); Переменная fordFactory соответствующего класса FordFactory называется фабрикой, а метод createFastest – Фабричным методом (Factory Method). О фабриках речь пойдёт в следующем разделе, а вот о методах поговорим сейчас. Так какие конкретно преимущества дало нам использование отдельного метода вместо обычного конструктора? Во-первых, в нашем примере метод инкапсулирует логику выбора класса, что было бы невозможно осуществить в конструкторе. А во-вторых, он позволяет отложить вопрос инстанциирования до момента написания клиентского кода или, при желании, даже до рантайма. Кроме того, если оторваться от данного примера, можно увидеть ещё несколько преимуществ:- порождающий метод, в отличие от конструктора, может иметь любое имя, не обязательно совпадающее с именем класса. Следовательно, для создания объекта можно использовать методы с разными именами, но одинаковым списком параметров. На английской Википедии есть пример с комплексными числами: вместо одного конструктора Complex(double, double) используется два статических метода – fromCartesian и fromPolar;
- конструктор невозможно передать в качестве параметра (для обычных функций это реализуется посредством паттерна Стратегия (Strategy), который описан ниже);
- порождающий метод не имеет ограничений, присущих конструктору, таких, как обязательная виртуальность, невозможность вернуть пустую ссылку (null), невозможность быть описанными в интерфейсе и пр;
- метод может создавать объект способом, отличным от стандартного. Например, вместо обычного выделения памяти и запуска кода инициализации, метод может клонировать некоторый объект с уже инициализированными полями2.
Abstract Factory, или Берегите свою семью, дон Карлеоне
Итак, мы можем создавать автомобили, заставлять их ездить, считывать показатели, сравнивать между собой и т.д. Но что, если мы захотим протюнинговать свою машину? Разумеется, для этого понадобится влезть в конструкцию и заменить некоторые запчасти. Запчасти! Именно они нам и нужны. Однако вряд ли колёса от Land Rover'а подойдут для Lamborghini (по крайней мере, это будет смотреться неэстетично). Следовательно, нам нужен некий механизм, который бы позволил разделять «семейства» запчастей таким образом, чтобы можно было сохранить совместимость и правильную работу всех деталей внутри таких «семей». И здесь нам дважды повезло. Во-первых, в реальном мире уже есть нужная нам модель: совместимость запчастей обеспечивают сами производители автомобилей, так что нам никто не мешает проделать такой же трюк и выделить каждого из таких «производителей» в отдельный класс. Во-вторых, по сути, эту проблему мы уже решили на предыдущем шаге, когда говорили про Фабричный метод: каждая фабрика (например, FordFactory, LandRoverFactory, LamborghiniFactory) уже является производителем автомобилей, и было бы логично повесить задачу производства деталей на те же самые классы. Это и называется Абстрактной фабрикой (Abstract Factory). Несмотря на мою нелюбовь к UML-диаграммам при объяснении паттернов, я вынужден признать, что в данном случае они наилучшим образом отражают суть это приёма проектирования. Абстрактная фабрика задаёт интерфейс для создания семейств совместимых объектов. Каждый элемент такого семейства знает всё о «членах своей семьи». Класс автомобиля Lamborghini может знать, куда и как именно установить колёса LamborghiniWheel и какие для этого можно вызывать методы. Например, он может вызвать у LamborghiniWheel метод changeTyre() и не заботиться о том, что такой метод не определён в классах AbstractWheel и других его наследниках. Фабрики и Фабричные методы особенно полезны при конфигурировании системы через внешние настроечные файлы. Например, Hibernate настраивает свои SessionFactory's при инициализации приложения, а уже настроенная SessionFactory отдаёт пользователю объект с интерфейсом Session и реальным типом, зависящим от драйвера базы данных, прописанного в конфиге. Таким образом, логика выбора конкретного класса вообще выносится за пределы программного кода. Абстрактная фабрика и Фабричный метод тесно связаны, и не всегда можно сделать чёткое разделение между ними, поэтому во многих источниках они описываются одним именем (в основном, Абстрактной фабрикой). Интересно, что некоторые языки программирования имеют конструкции, напрямую реализующие эти паттерны. Так, например, в Common Lisp порождение всех объектов осуществляется единой функцией – make-instance, аргументом которой является символ3 нужного класса. Этот символ можно передавать как параметр, закладывать в конфиги, сам метод make-instance можно перекрыть или декорировать4. В итоге, практически неограниченная свобода по созданию класса. В языках ML-семейства есть прямое отражение Абстрактной Фабрики. Например, в Haskell, есть конструкция data5, имеющая следующий синтаксис: data Color = Red | Yellow | Green | Blue | Violet | RGB Int Int Int data Color – объявление «интерфейса» для всех наших классов, а Red, Yellow и т.д. – конкретных типов. Вообще Хаскелл имеет очень развитую типизацию, в том числе, он использует довольно необычный подход к реализации ООП. Однако это тема для отдельного разговора, а мы вернёмся к нашим автомобилям.Singleton, или Оставьте меня одного!
Фабрики – это, конечно, хорошо, но как-то плодить кучу инстансов одной фабрики, чтобы они делали одно и то же, мягко говоря, не кошерно. Было бы неплохо иметь в системе ровно по одному экземпляру каждой используемой фабрики. Как это сделать? Можно объявить все фабричные методы статическими. Этим мы гарантируем «неразмножение» инстансов, но ценой за это будет невозможность описать данные методы в интерфейсе, а это убийство всей идеи Абстрактной фабрики. Мы также можем инстанциировать все фабрики при загрузке приложения и пользоваться только ими, но если нам нужны всего 3 фабрики, а в системе их объявлено 50, то 47 из них будут просто засорять память. Со всеми этими проблемами успешно борется паттерн Одиночка (Singleton): public class Singleton { private static Singleton singleton; private Singleton(){} // (0) public static volatile Singleton getInstance(){ // (1) if (singleton == null) { // (2) synchronized(Singleton.class){ // (3) if (singleton == null) { // (4) singleton = new Singleton(); // (5) } } } return singleton; } } Суть паттерна в том, чтобы запретить прямое создание объекта, сделав его конструктор невидимым извне (0), а для создания определить статический фабричный метод getInstance() (1). Внутри этого метода мы создаём объект синглтона и сохраняем его в соответствующую переменную. Поскольку метод может быть вызван из разных потоков, сохранение объекта в переменную производится в блоке синхронизации (3). Может оказаться так, что перед самой синхронизацией какой-то поток всё же успел создать и сохранить инстанс объекта, поэтому мы на всякий случай проверяем это (4). Эта проверка является обязательной, в отличие от первой проверки на null (2). Для чего же нужна первая проверка? Дело в том, что синхронизация – очень «тяжёлая» операция. Процессор обновляет значение синхронизируемой переменной, а для этого сбрасывает и перезаписывает целую строку своего кэша. В 99% запросов к getInstance объект уже будет существовать, поэтому дополнительная проверка значительно ускорит вызов этого метода. upd. Как правильно заметил nobullet, для правильной синхронизации поле singleton также должно иметь модификатор volatile, гарантирующий, что обращение к этой переменной будет таким же, как если бы оно было заключено в блок synchronized. Без этого модификатора ошибка может произойти в следующем случае. Пусть T1 и T2 - это потоки, выполняемые на процессорах P1 и P2 соответственно, и пытающиеся одновременно выполнить код метода getInstance.- T1 входит в критическую секцию, выделяет под новый объект память и записывает её адрес в переменную singleton, однако ещё не успевает вызвать конструктор;
- T2, не доходя до критической секции, считывает значение переменной singleton, которое уже не равно null, но указывает на неинициализированный объект;
- T2 не ждёт входа в критическую секцию и возвращает ссылку на "недоделанный" объект вызывающей функции;
- T1 завершает создание объекта и выходит из критической секции.
Strategy, или И каждый мнит себя стратегом…
Конечно, фабрики решили проблемы с перегрузкой методов, но:- если наследовать каждый раз, когда нам нужно заменить один метод, это приведёт к комбинаторному росту количества классов;
- наследование производится статически, во время компиляции, а что делать, если мы хотим заменить некоторый метод прямо во время работы программы?
- в тредах, которые реализуют единственный метод run;
- в Swing все элементы управления реализуют интерфейс ActionListener с единственным методом actionPerformed;
- при реализации интерфейса Comparator, например, для сортировки элементов массива и пр.
Prototype, или Атака клонов
По определению, фабрика - это место массового производства. Наши CarFactory-s не являются исключением. Если фабрика представляет собой Singleton, то различные части системы могут постоянно запрашивать создание всё новых и новых экземпляров автомобилей, тем самым создавая эффект массового производства. А при массовом производстве любого продукта остро стоит вопрос о минимизации расходов на создание одного объекта. Рассмотрим процесс создания одного экземпляра. Он состоит из двух основных частей:- выделение памяти под объект;
- инициализация.
1 - о нём речь пойдёт дальше в этой статье. 2 - это пример паттерна Прототип (Prototype). 3 - не путайте лисповские символы с элементарными частями строки (char) и с символами в Ruby (где они равнозначны ключевым словам в CL). В данном случае символ означает имя некоторого объекта, например, имя класса, который нужно инстанциировать. 4 - о декорировании функций речь пойдёт в 3-ей статье цикла. 5 - Haskell – один из самых математизированных языков программирования, поэтому данная конструкция, как и паттерн Абстрактная Фабрика, является выражением математического понятия «алгебраический тип данных». 6 - в Java 7, правда, обещают ввести специальный тип для создания ссылок на методы - MethodHandle.
Релоцировались? Теперь вы можете комментировать без верификации аккаунта.