Копипаст из блога http://lex-kravetski.livejournal.com
С месяц назад
дорогой товарищ dargot
в личной беседе посетовал на странные методы объектного
программирования, практикуемые некоторыми программистами. Совершенно
неясно, — говорил он, — зачем эти люди плодят такое множество
интерфейсов, а потом постоянно кастуют их к реализациям и обратно.
Неясно, зачем к каждому объекту граждане создают свою собственную
фабрику, которая к тому же не является доопределением других, а наоборот
каждый раз новая, своя собственная — скопипасченная.
Я согласен с
товарищем Дарготом: действительно огромное множество программистов
совершенно не в курсе, зачем нужны объектные формы абстракций, поэтому
применяют их как попало, руководствуясь где-то подслушанными общими
фразами. Вообще, на мой взгляд, проблема в том, что традиционно обучение
программированию начинается в лучшем случае с процедурного языка — если
не с псевдо-языка, лишённого даже процедур и выливающегося в огромную
простыню кода в методе main. В результате такого обучения программисту в
последствии приходится буквально переучиваться с одного метода
программирования на другой, что зачастую тяжелее даже обучению с нуля.
Нет, я настоятельно рекомендую учить сразу объектной парадигме (или,
буде оно разовьётся, функциональной). Так, будто без объектов писать и
нельзя: первое впечатление ведь — самое сильное. Оно запоминается.
Так вот, когда в
руки гражданину, долго обучавшемуся процедурным языкам, попадает,
скажем, объектный язык С++, такой гражданин сходу начинает писать на нём
как на процедурном С. Объект в его представлении — это такая непонятно
зачем нужная обёртка над процедурами. Ну, типа структуры.
Соответственно, хранить в ней следует строго данные, а обрабатывать их
всё так же процедурами, пусть даже они оформлены как методы классов. И
это по коду сразу видно. Как это ни печально, процентов эдак девяносто
си-плюс-плюсников упорно не желают отходить от своих низкоуровневых
корней. И это чревато. Ибо знание, как работать со строками на
чар-поинтерах, совершенно не заменяет умения написать масштабный проект.
Который, в свою очередь, без объектов, во-первых, крайне тяжёл в
разработке, а во-вторых, неизбежно будет валиться под сонмищем багов.
Про повторное использование кода я даже и не говорю — код пишется так,
будто его и в этот раз не особенно-то собирались использовать, а
написали в качестве временной затычки.
Через некоторое время такой программист
начинает подозревать, что его низкоуровневые извраты, они как-то не
того, не для настоящих пацанов. То есть, он — сто пудов гуру в
манипуляциях с чар-поинтерами и любого тут заткнёт за пояс, но вот как
чуть дальше программы из десяти функций, так туши свет — в этой мешанине
где тут кто? Иногда для самооразвития, иногда для новых понтов, а
иногда для совмещения первого со вторым он поверхностно ознакамливается с
«пацанскими» объектными технологиями и яростно их внедряет в жизнь. Как
правило, оное ознакомление сводится к выучиванию пары-тройки
заклинаний, которые можно с умным видом произносить в обществе и поэтому
чувствовать себя мега-крутым. И пока он просто произносит, всё ещё
ничего, однако он их ведь вдобавок внедряет…
Мега-заклинания, которые так огорчили
товарища Даргота, следующие: «интерфейсы — это круто, надо везде
использовать интерфейсы» и «фабрики — это круто, надо всё создавать
через фабрики». Буквальное понимание этих фраз без соответствующей
практики и сопутствующего ей владения объектными технологиями даёт
чудовищный результат. На фоне которого даже использование С++ как
чистого С не так шокирует. Программист теперь не просто рассматривает
объекты как структуры, но и плодит к каждой из них отдельные интерфейсы,
которые фигурируют в каждой его процедуре. Создаются структуры теперь
не просто через «new», а через спец-методы спец-фабрик, которые вдобавок
называются везде по-разному.
Это, товарищи, полный пинцет — когда вместо одного
объекта-структуры имеют место быть три. И которые, все три настолько
сильно вплетены в код, что выкинуть две лишние сущности на фиг, не
представляется возможным без полного переписывания кода.
Ну, скажем,
программист пишет стратегическую игру. У него есть какие-то там юниты,
которые ходят по карте и всё такое. Их, соответственно, надо создать,
ими надо дать возможность управлять, их надо отрисовать, их надо где-то
хранить. Я несколько утрирую, но под это дело программист создаёт
структуры типа Dragoon, Hussar и Cannon, где описывает поведение этих
юнитов.
Хотя
вообще говоря структуры хватило бы и одной.
После этого он
вспоминает, что «надо писать с интерфейсами» и делает интерфейсы
IDragoon, IHussar, ICannon. Само собой, создаётся всё это при помощи
DragoonFactory, HussarFactory и CannonFactory, разруливание между
которыми производится при помощи огромного switch-а, равно как и все
манипуляции с юнитами тоже. При этом временами IDragoon ему приходится в
явном виде преобразовывать к Dragoon, а то не работает.
При паре сотен юнитов
мы получаем шестьсот классов, которые копируют друг друга копипастой.
Зато можно гордо говорить «я пишу всё с интерфейсами и фабриками».
Потому что настоящие пацаны делают так.
Товарищи, объектный подход придуман не
для того, чтобы писать больше. Наоборот, как и любой полезный подход он
позволяет писать меньше. В описанной выше схеме код непомерно раздулся, а
следовательно подход был использован неправильно. Если у вас для
каждого объекта свой интерфейс и своя фабрика, то вы на самом деле не
используете интерфейсы и фабрики, а просто засоряете свою программу
кучей мусора.
Интерфейс
— это декларация того, что одна часть программы работает с другими
частями, которые настолько отличаются по реализации, что не имеют вообще
ничего общего между собой. Фабрика, она не просто, чтобы создавать, она
для упрощения создания. Для сокращения кода в конечном счёте.
Радикального сокращения, а не замены одного единственного «new» на пару
десятков каких-то других строк.
Это вообще верный знак концептуальной ошибки —
когда использование концепции привело к усложнению программы и
увеличению её размеров. Вы в этом случае что-то делаете не так.
Но раз уж пошла такая
пьянка, надо наверно рассказать, зачем нужны интерфейсы и фабрики —
многие ведь, по ходу, совершенно не в курсе.
Интерфейсы
Интерфейс — это
такой класс, где методы заявлены, но ни один из них не имеет реализации.
Кроме того, в данном классе нет полей.
Таким образом,
интерфейс — это декларация вместо реализации. Абстракция, призванная
обобщить алгоритмы, различающиеся только малыми деталями в один более
универсальный. Так, скажем, сортировка контейнера зависит только от
наличия функции сравнения для его элементов. Что бы в контейнере не
хранилось, алгоритму сортировки достаточно возможности сравнения каждого
элемента с каждым. Поскольку возможных объектов для сортировки великое
множество, разработчик алгоритма не берёт на себя груз ответственности
за реализацию метода сравнения каждого возможного объекта с каждым и
определяет интерфейс IComparable с единственным заявленным методом
«compare», в качестве же входного параметра своего алгоритма он требует
List<IComparable>, вынуждая тем самым потенциальных пользователей
его алгоритма унаследовать интерфейсу и реализовать метод compare для
тех объектов, которые они собираются реализовать. Метод при этом
остаётся всего один. Для всех возможных случаев.
Вообще, для
типобезопасности следует несколько усложнить иерархию. Так, в частности,
String и Integer оба будут IComparable, однако сравнить их друг с
другом и отсортировать не всегда возможно. Для произвольных же классов А
и B, каждый из которых является наследником IComparable, сравнение
может быть не определено в принципе (Integer ведь хотя бы в String можно
преобразовать). То есть, иерархия должна позволять сортировку
контейнера только тех объектов, которые наследники IComparable и при
этом сводимы друг к другу.
Иными словами, интерфейс — это такая штука,
которая по определению нужна для максимально общих случаев. В вашем
частном приложении таковых случаев обычно очень мало. Более того, даже
там, где они есть, разница в требуемых методах не настолько велика,
чтобы к каждому классу сделать свой интерфейс. Интерфейс, он ведь всегда
подразумевает наличие множества своих наследников, а не одного
единственного. IDraggon и IHussar в этом плане совершенно излишни. Даже
IUnit — общий для всех драгунов, гусаров и пушек — скорее всего не
понадобится. Интерфейсы начнутся где-то на уровне IPaintable и
IResource, и то не факт.
Вместо определения интерфейса в большинстве случаев гораздо
полезнее сделать хорошо структурированную реализацию класса. Возможно, с
некоторым количеством абстрактных методов, но не обязательно. Если
некто в последствии захочет её расширить, то он сделает это
переопределением малой части методов вашей реализации, а не полной
реализацией вашего интерфейса.
Кстати, даже в общих случаях, где интерфейсы
необходимы, имеет смысл самостоятельно сделать несколько их реализаций.
Чтобы остальным было куда посмотреть с целью понять, чего вы вообще под
данным интерфейсом подразумевали.
Увы, некоторые языки накладывают
ограничение на множественное наследование и ряд других ценных вещей,
поэтому интерфейсы обретают и второй смысл — имитация множественного
наследования, реализация замыканий и всё такое. Однако в первом случае
интерфейс обычно целиком делегируется инкапсулированной реализации, а во
втором — он мал по объёму и содержит как правило единственный метод.
Так что этот костыль, хоть и костыль, но хотя бы прозрачен для
разработчика и пользователей его разработки.
Лично я вообще рекомендую сначала писать
реализации вместе с их использованием, а после некоторого их количества
уже извлекать из них интерфейсы. Результат такового в подавляющем
большинстве случаев куда как более прост в использовании и понятен,
нежели априорные интерфейсы, которые попытались угадать на стадии
проектирования. Благо, среды разработки сейчас позволяют извлечь
интерфейс ценой десятка кликов мышью.
Ну и ещё раз:
Интерфейсов, товарищи, в каждой программе должно
быть на порядки меньше, чем классов.
Фабрики
Фабрики, как я уже говорил, нужны не просто для
замены «new» и тем стимулирования чувства собственного величия. Цель
введения в код фабрик иная.
На языках без автоматического управления памятью, к
которым относится и С++ тоже, один из скрытых смыслов фабрики — не
поверите, управление памятью. Локализация создания и удаления объектов в
одном, легко контролируемом месте. Крайне тяжело отыскать, кто убил всё
ещё нужный объект, когда удаление может быть вписано где угодно. Крайне
тяжело понять, кто создал ещё один объект, когда точно такой же уже
был, и почему теперь имеются несколько копий одной и той же сущности.
Чтобы отследить было проще, пишется фабрика, в отношении которой
подразумевается, что создавать и удалять объекты имеет право только она.
Теперь создающий объект пишет что-то типа
MyObject myObject =
Factory.get().newInstance(id);
И получает в ответ экземпляр класса. Фабрика же
сама разбирается, надо ли создавать новый, или же для этого id уже есть
экземпляр или ещё что-то там. Если объект требуется удалить, то пишется
не «delete myObject», а
Factory.get().delete(myObject);
или
Factory.get().delete(id);
Эти строки выглядят
несколько длиннее, чем просто «new MyObject()» и «deletу myObject»,
однако, если учесть сопутствующий код — проверки валидности удаления и
создания, подсчёт ссылок и т.д. и т.п. — оно получается сильно короче. А
главное, гораздо лучше контролируется.
Собственно, клиентский код, вызывающий
эти самые «newInstance», в случае изменения механизмов создания
останется незатронутым. Буде кто-то введёт кэширование, подсчёт ссылок
или что-то типа того, места, где вызываются методы фабрики, переписывать
будет не надо, в отличие от случая с явными вызовами «new» и «delete».
Это, типа, бонус.
Однако настоящий бонус — встроенный в язык сборщик мусора.
Да-да, программист всегда уверен, что он-то точно знает, как наиболее
эффективно создавать и удалять свои объекты. С++ дал ему возможность
самому решать… Хотя на самом деле он дал ему лишь возможность попытаться
написать свой сборщик мусора, 999 из 1000 которых будут на порядок хуже
встроенного, например, в Java. Не потому, что программист — тупой, хотя
и это не редкость, а потому, что данный программист занимается
написанием своего собственного сборщика мусора, его оптимизацией и
отладкой гораздо меньше по времени и гораздо менее пристально, чем те,
кто разрабатывают только сборщик мусора. Соответственно, результат у
последних будет гораздо более эффективным, удобным в использовании и так
далее. Самое главное, их результат будут использовать все, кто
использует язык, а самопальный сборщик, увы, сторонние библиотеки
использовать не будут, что породит всевозможные обёртки к ним и всё
такое. А это — время, баги, геморрой.
Кроме того, в ряде случаев внешний по
отношению к коду сборщик мусора имеет гораздо больше возможностей для
эффективного освобождения памяти. Например, он может очищать память
блоками, даже если она использовалась разными объектами. В С++ без
хитрых хаков такое организовать невозможно. delete очищает только то,
что для этого объекта выделил new (и то не всегда — ловкими кастами
вполне можно обмануть delete). Чтобы освободить память сразу для
множества указателей, надо переопределять new через сишные функции.
Ну и вообще, оно
— бардак, когда каждая программа начинается с разработки собственного
метода управления памятью.
Вторая, более актуальная в языках со сборщиками
мусора, цель использования фабрик — создания компонентов по
идентификаторам. Что это, собственно, такое?
Есть у нас, положим, класс А, который для
каких-то своих надобностей использует класс B внутри себя. Как-то так:
class A { B b; A() { b = new B(); } void doSomething() { b.doSomethingElse(); } }
В
некоторый момент может понадобиться подменить класс B на его наследника,
но сохранить основную функциональность класса A. Как это сделать? В
данном случае выход, например, такой: сделать в классе A метод setB(B
b), куда желающий может засандалить своего наследника этого класса.
Это можно
определить и прямо в конструкторе, однако при большом количестве
компонент, количество параметров конструктора тоже будет весьма большим.
И всё бы ничего,
однако класс B внутри себя может содержать ещё и класс С. Который кто-то
тоже может захотеть подменить. Механизм создания правильной композиции в
этом случае становится каким-то уж чересчур длинным и запутанным.
A a = new A();
a.getB().setC(new
C1());
И это
пока ещё случай, когда порядок создания компонентов не важен. И с
предположением, что B в этот момент уже создан.
Вдобавок некто имеет
возможность по недомыслию в любой момент времени подменить компонент,
просто вызвав a.setB(new B1()). Когда объект A вовсю уже используется и
сам использует компонент B. Коллапс.
Однако сего можно избежать при помощи фабрики. В
этом случае в конструкторе A уже не будет явного «b = new B()», как не
будет методов «setB» и «getB». Вместо этого конструктор (или какой-то
ещё метод) А запросит у фабрики новый экземпляр B.
b =
Factory.get(B.class)
Вместо B.class можно использовать и какой-то иной
идентификатор.
Фабрика по умолчанию подразумевает, что в ответ на запрос
B.class следует создать новый экземпляр B и его вернуть. Однако
пользователь может подменить реализацию, возвращаемую по этому
идентификатору. Например, так:
Factory.bind (B.class, B1.class)
C этого момента
фабрика будет возвращать на запрос B.class не новый экземпляр B, а новый
экземпляр B1. Таким же образом легко подменить и компонент С в классе
B.
Factory.bind
(C.class, C1.class)
В принципе, вокруг всего этого можно навернуть ещё много
разных вариантов, я даже наверно как-нибудь более подробно опишу процесс
написания и использования такого рода фабрик, но суть остаётся та же:
для подмены компонентов конфигурируются не сами объекты, а фабрики, их
создающие. Таким образом гораздо легче управляться с глубоко вложенными
компонентными структурами. И, самое главное, оно короче получается —
ведь подмены компонентов в данном случае можно сделать сразу для всего
проекта где-то на этапе инициализации приложения. Если у вас в проекте
используется вместо класса С класс С1, то подмена одного класса на
другой в фабрике всем объектам, использующим компонент С, будет на самом
деле выдавать по их запросу С1. При этом порядок создания компонентов
всегда будет правильным, возможности невовремя подменить компонент,
когда уже всё давно работает, не будет и прочие плюшки.
Надо отметить, что
фабрика в данном случае не случайно используется так, будто бы она —
синглтон, да ещё и с единственной реализацией для всех классов.
Действительно, фабрика — одна. Точнее, один класс, скажем так, у
менеджера фабрик. Именно к нему обращаются все объекты, желающие создать
нечто при помощи фабрики. Конечно, некие логические подразделы
приложения могут использовать независимые экземпляры менеджера фабрик и,
кроме того, сохраняется возможность внутри менеджера подменить и
фабрику для какого-то идентификатора на свою собственную, однако
большинство случаев должно покрываться одной реализацией менеджера и
одной же реализацией фабрики для всех используемых классов, а не
отдельных их реализаций для каждого класса.
Когда фабрик в проекте много, тяжело
контролировать, что они делают. Тяжело подменять компоненты. Да и код
фабрик по сути дублируется.
Фабрики нужны, чтобы писать меньше кода, а не
больше.
Метки: разработка по