Шаблоны проектирования — это допускающие многократное использование оптимизированные решения проблем программирования, с которыми мы сталкиваемся каждый день. Шаблон проектирования — это не класс или библиотека, которые мы можем просто вставить в нашу систему. Он — много больше. Это — некоторый шаблон, который должен быть реализован в надлежащей ситуации. Он не зависит от языка. Хороший шаблон проектирования должен быть таким, чтобы его можно было использовать с большинством языков (если не со всеми) в зависимости от характеристик языка. Чрезвычайно важно то, что любой шаблон проектирования необходимо использовать очень осторожно — если он применён в ненадлежащем месте, то его действие может быть разрушительным и породить много проблем для вас. Однако применённый в нужном месте в нужное время он может стать вашим спасителем.
Есть три основных типа шаблонов проектирования:
• структурный
• порождающий
• поведенческий
Структурные шаблоны, в общем случае, имеют дело с отношениями между объектами, облегчая их совместную работу.
Порождающие шаблоны обеспечивают механизмы инстанцирования, облегчая создание объектов способом, который наиболее соответствует ситуации.
Поведенческие шаблоны используются в коммуникации между объектами, делая её более лёгкой и гибкой.
Почему их следует использовать?
Шаблоны проектирования в принципе являются хорошо продуманными решениями проблем программирования. Многие программисты уже сталкивались ранее с этими проблемами и использовали для преодоления эти «решения». Встречая какую-то проблему, зачем заново искать решение, когда можно применить уже проверенное?
Пример
Давайте представим, что вам поручено создать способ объединить два класса, которые выполняют два разных действия в зависимости от ситуации. Эти два класса интенсивно используются существующей системой в разных местах, что затрудняет их удаление и изменение существующего кода. Дополнительно к этому изменение существующего кода требует проверки всего изменённого кода, т.к. правка такого рода в системе, построенной на разных компонентах, почти всегда вносит новые ошибки. Вместо перечисленного можно использовать вариант шаблона «Стратегия» и шаблона «Адаптер», которые могут легко обращаться с названными типами сценариев.
01 <?php
02 class StrategyAndAdapterExampleClass {
03 private $_class_one;
04 private $_class_two;
05 private $_context;
06
07 public function __construct( $context ) {
08 $this->_context = $context;
09 }
10
11 public function operation1() {
12 if( $this->_context == "context_for_class_one" ) {
13 $this->_class_one->operation1_in_class_one_context();
14 } else ( $this->_context == "context_for_class_two" ) {
15 $this->_class_two->operation1_in_class_two_context();
16 }
17 }
18 }
Довольно просто, не так ли? Теперь посмотрим внимательнее на шаблон «Стратегия».
Шаблон «Стратегия»
Изображение размещено с разрешения владельцев сайта cioinnervoice.wordpress.com
Шаблон «Стратегия» является поведенческим шаблоном проектирования, который позволяет вам решать, какой план действий должна принять программа, основываясь на определённом контексте при выполнении. Вы закладываете два различных алгоритма внутри двух классов и решаете в ходе выполнения, с какой стратегией следует работать.
В нашем примере выше стратегия устанавливается в зависимости от того, какой была переменная $context в то время, когда класс подвергался обработке. Если вы даёте ей контекст для класса_один, то будет использован класс_один и наоборот.
Замечательно, но где я могу использовать это?
Предположим, что вы в данный момент разрабатываете класс, который может или обновить или создать новую пользовательскую запись. Хотя ему требуются те же самые входы (имя, адрес, номер мобильного телефона и т.п.), но в зависимости от ситуации он должен использовать различные функции при обновлении и создании. Здесь для выполнения можно, вероятно, сразу же использовать условный переход «if-else», но как быть, если этот класс понадобится и в другом месте? Тогда нужно будет переписать полностью весь этот оператор условного перехода. Не было бы проще просто указать ваш контекст?
01 <?php
02 class User {
03
04 public function CreateOrUpdate($name, $address, $mobile, $userid = null)
05 {
06 if( is_null($userid) ) {
07 // Это значит, что пользователь ещё не существует, надо создать новую запись
08 } else {
09 // Это значит, что пользователь уже существует, необходимо обновить на базе данного идентификатора пользователя
10 }
11 }
12 }
Теперь «обычный» шаблон «Стратегия» предполагает помещение ваших алгоритмов внутри другого класса, но в данном случае другой класс был бы нерациональным решением. Помните, что вы не обязаны точно следовать шаблону. Варианты работают, пока концепция остаётся прежней, и это решает проблему.
Шаблон «Адаптер»
Изображение размещено с разрешения владельцев сайта www.uxcell.com
Шаблон «Адаптер» является структурным шаблоном проектирования, который позволяет перепрофилировать класс с другим интерфейсом, делая его доступным для системы, которая использует различные методы вызова.
Это также позволяет изменять некоторые из входов, получаемых от класса клиента, превращая его в нечто совместимое с функциями класса Adaptee.
Как можно использовать это?
Другим понятием для ссылки на класс адаптера является «обёртка», которая по существу позволяет «обернуть» действия в класс и повторно использовать эти действия в надлежащих ситуациях. Классическим примером может быть создание доменного класса для классов таблиц. Вместо вызова различных классов таблиц и последовательного вызова их функций можно вложить все эти методы в один, используя класс адаптера. Это не только позволяет вам повторно использовать любые требуемые действия, но также избавляет от необходимости переписывать код, если вам нужно использовать то же действие в другом месте.
Сравните эти две реализации:
Подход без адаптера
1 <?php
2 $user = new User();
3 $user->CreateOrUpdate( //inputs );
4
5 $profile = new Profile();
6 $profile->CreateOrUpdate( //inputs );
Если бы нам нужно было сделать это снова в другом месте или даже использовать этот код в другом проекте, то мы должны были бы набрать всё заново.
Лучше
Указанное противоположно действиям вроде приведённых ниже:
1 <?php
2 $account_domain = new Account();
3 $account_domain->NewAccount( //inputs );
В данной ситуации мы имеем обёрточный класс, который был бы нашим доменным классом Account:
01 <?php
02 class Account()
03 {
04 public function NewAccount( //inputs )
05 {
06 $user = new User();
07 $user->CreateOrUpdate( //subset of inputs );
08
09 $profile = new Profile();
10 $profile->CreateOrUpdate( //subset of inputs );
11 }
12 }
Таким образом, можно использовать домен Account снова везде, где требуется, — дополнительно вы оказываетесь в состоянии обёртывать другие классы также под вашим доменным классом.
Шаблон «Фабричный метод»
Изображение размещено с разрешения владельцев сайта www.lankanewspappers.com
Шаблон «Фабричный метод» является порождающим шаблоном проектирования, который делает именно то, что означает это слово: этот класс действует как фабрика экземпляров объектов.
Основной целью этого шаблона является вложение порождающей процедуры, которая может свести различные классы в одну функцию. Если фабричному методу обеспечить надлежащий контекст, то он будет в состоянии вернуть правильный объект.
Когда можно использовать это?
Наилучшей ситуацией для использования шаблона «Фабричный метод» является наличие нескольких различных вариантов одного объекта. Допустим, имеется класс «кнопка»; у этого класса есть различные варианты — например, ImageButton (кнопка изображения), InputButton (кнопка ввода) и FlashButton (флэш-кнопка). В зависимости от места может потребоваться создать различные кнопки — именно здесь можно использовать «фабрику» для создания кнопок для вас!
Начнём с создания наших трёх классов:
01 <?php
02 abstract class Button {
03 protected $_html;
04
05 public function getHtml()
06 {
07 return $this->_html;
08 }
09 }
10
11 class ImageButton extends Button {
12 protected $_html = "..."; //Здесь должен быть задан HTML, требуемый для вашей кнопки на базе изображения
13 }
14
15 class InputButton extends Button {
16 protected $_html = "..."; //Здесь должен быть задан HTML, требуемый для вашей нормальной кнопки (<input type="button"... />);
17 }
18
19 class FlashButton extends Button {
20 protected $_html = "..."; //Здесь должен быть задан HTML, требуемый для вашей флэш-кнопки
21 }
Теперь можно создать наш фабричный класс:
01 <?php
02 class ButtonFactory
03 {
04 public static function createButton($type)
05 {
06 $baseClass = 'Button';
07 $targetClass = ucfirst($type).$baseClass;
08
09 if (class_exists($targetClass) && is_subclass_of($targetClass, $baseClass)) {
10 return new $targetClass;
11 } else {
12 throw new Exception("The button type '$type' is not recognized.");
13 }
14 }
15 }
Полученный код можно использовать, например, так:
1 $buttons = array('image','input','flash');
2 foreach($buttons as $b) {
3 echo ButtonFactory::createButton($b)->getHtml()
4 }
Выходом должен быть HTML всех ваших типов кнопок. Таким образом, вы могли бы указать, какую кнопку создать в зависимости от ситуации, а также повторно использовать условие.
Шаблон «Декоратор»
Изображение размещено с разрешения владельцев сайта www.decoratorsdarlington.co.uk
Шаблон «Декоратор» является структурным шаблоном проектирования, который позволяет нам добавлять новое или дополнительное поведение к объекту в ходе выполнения в зависимости от ситуации.
Целью является сделать так, чтобы расширенные функции могли быть применены к одному конкретному экземпляру и в то же время иметь возможность создать оригинальный экземпляр, который не имеет этих новых функций. Это также позволяет комбинировать несколько декораторов для одного экземпляра, благодаря чему отсутствует привязка к одному декоратору для каждого экземпляра. Этот шаблон является альтернативой созданию подкласса, что относится к созданию класса, который наследует функциональность от родительского класса. В противоположность к созданию подкласса, которое добавляет поведение во время компиляции, «декорирование» позволяет добавить новое поведение в ходе выполнения, если ситуация требует этого.
Чтобы реализовать шаблон «Декоратор», можно выполнить следующие шаги:
1. Выделите оригинальный класс «Компонент» как подкласс класса «Декоратор».
2. В классе «Декоратор» добавьте указатель «Компонент» как поле.
3. Переместите «Компонент» в конструктор «Декоратора», чтобы инициализировать указатель «Компонент».
4. В классе «Декоратор» перенаправьте все методы «Компонент» на указатель «Компонент».
5. В классе «Декоратор» отмените любой метод (любые методы) «Компонент», поведение которого (которых) должно быть модифицировано.
Данные этапы размещены с разрешения владельцев сайта en.wikipedia.org/wiki/Decorator_pattern
Когда можно использовать это?
Наиболее удобно использовать шаблон «Декоратор» для объекта, требующего нового поведения, только когда на это имеется запрос по ситуации. Допустим, имеется HTML-элемент компоновки, ссылка на выход из системы, и вы желаете, чтобы она делала немного различающиеся действия, основываясь на текущей странице. Для этого можно использовать шаблон «Декоратор».
Сначала зададим различные требуемые «декорации»:
• Если мы находимся на главной странице и зарегистрированы, то эта ссылка должна быть «обёрнута» в теги h2.
• Если мы находимся на другой странице и зарегистрированы, то эта ссылка должна быть «обёрнута» в теги подчёркивания.
• Если мы зарегистрированы, то эта ссылка должна быть «обёрнута» в теги полужирного шрифта.
После задания наших «декораций» можно их запрограммировать:
01 <?php
02 class HtmlLinks {
03 //Некоторые способы, имеющиеся для всех html-ссылок
04 }
05
06 class LogoutLink extends HtmlLinks {
07 protected $_html;
08
09 public function __construct() {
10 $this->_html = "<a href=\"logout.php\">Logout</a>";
11 }
12
13 public function setHtml($html)
14 {
15 $this->_html = $html;
16 }
17
18 public function render()
19 {
20 echo $this->_html;
21 }
22 }
23
24 class LogoutLinkH2Decorator extends HtmlLinks {
25 protected $_logout_link;
26
27 public function __construct( $logout_link )
28 {
29 $this->_logout_link = $logout_link;
30 $this->setHtml("<h2>" . $this->_html . "</h2>");
31 }
32
33 public function __call( $name, $args )
34 {
35 $this->_logout_link->$name($args[0]);
36 }
37 }
38
39 class LogoutLinkUnderlineDecorator extends HtmlLinks {
40 protected $_logout_link;
41
42 public function __construct( $logout_link )
43 {
44 $this->_logout_link = $logout_link;
45 $this->setHtml("<u>" . $this->_html . "</u>");
46 }
47
48 public function __call( $name, $args )
49 {
50 $this->_logout_link->$name($args[0]);
51 }
52 }
53
54 class LogoutLinkStrongDecorator extends HtmlLinks {
55 protected $_logout_link;
56
57 public function __construct( $logout_link )
58 {
59 $this->_logout_link = $logout_link;
60 $this->setHtml("<strong>" . $this->_html . "</strong>");
61 }
62
63 public function __call( $name, $args )
64 {
65 $this->_logout_link->$name($args[0]);
66 }
67 }
Затем мы должны быть в состоянии использовать это, например, следующим образом:
01 $logout_link = new LogoutLink();
02
03 if( $is_logged_in ) {
04 $logout_link = new LogoutLinkStrongDecorator($logout_link);
05 }
06
07 if( $in_home_page ) {
08 $logout_link = new LogoutLinkH2Decorator($logout_link);
09 } else {
10 $logout_link = new LogoutLinkUnderlineDecorator($logout_link);
11 }
12 $logout_link->render();
Здесь можно видеть, как мы оказываемся в состоянии скомбинировать несколько декораторов, если это требуется. Поскольку все декораторы используют магическую функцию __call, то мы можем всё ещё вызвать методы оригинальной функции. Если принять, что мы в настоящий момент находимся внутри главной страницы и зарегистрированы, то HTML-выход должен быть следующим:
1 <strong><h2><a href="logout.php">Logout</a></h2></strong>
Шаблон «Одиночка»
Изображение размещено с разрешения владельцев сайта intoxicologist.wordpress.com
Шаблон проектирования «Одиночка» является порождающим шаблоном проектирования, который обеспечивает наличие одного единственного экземпляра какого-то конкретного класса во время выполнения и глобальную точку доступа к этому единственному экземпляру.
Это облегчает задание точки «координации» для других объектов, также использующих данный единственный экземпляр, поскольку его переменные будут неизменными для любых вызовов.
Когда можно использовать это?
Если требуется перевести какой-либо специфический экземпляр из одного класса в другой, то можно использовать шаблон «Одиночка», чтобы устранить проведение этого экземпляра через конструктор или аргумент. Предположим, вы создали класс Session, который имитирует глобальный массив $_SESSION. Поскольку для этого класса его экземпляр требуется создать только один раз, то можно реализовать шаблон «Одиночка», как, например:
01 <?php
02 class Session
03 {
04 private static $instance;
05
06 public static function getInstance()
07 {
08 if( is_null(self::$instance) ) {
09 self::$instance = new self();
10 }
11 return self::$instance;
12 }
13
14 private function __construct() { }
15
16 private function __clone() { }
17
18 // Мы можем использовать любые другие способы сеанса
19 ...
20 ...
21 ...
22 }
23
24 // Получить экземпляр сеанса
25 $session = Session::getInstance();
Сделав так, мы можем получать доступ к нашему экземпляру сеанса из различных частей нашего кода, даже в разных классах. Эти данные будут сохраняться в течение всех вызовов getInstance.
Заключение
Имеется ещё много шаблонов проектирования, которые полезно знать; в этой статье я остановился только на некоторых из наиболее известных, которые я использую при программировании. Если есть интерес прочитать о других шаблонах проектирования, то страница Википедии Design Patterns (Шаблоны проектирования) содержит много информации об этом. Если этого недостаточно, то вы всегда можете прочитать книгу «Design Patterns: Elements of Reusable Object-Oriented Software» («Шаблоны проектирования: элементы многократно используемого объектно-ориентированного программного обеспечения»), которая считается одной из лучших по рассматриваемой теме.
И последнее: используя эти шаблоны проектирования, всегда проверяйте, что вы пытаетесь решить правильно поставленную задачу. Как я упоминал ранее, эти шаблоны проектирования необходимо использовать очень осторожно: они — при использовании в ненадлежащем контексте — могут ухудшить ситуацию, но при правильном использовании они просто жизненно необходимы.
Если вы нашли данную статью полезной, то почему бы не ознакомиться с предложением PHP-скриптов на Envato Market. Там представлены тысячи полезных скриптов, которые могут ускорить вашу разработку и улучшить конечный результат. Имеются системы бронирования, контактные формы AJAX, системы информационных бюллетеней и многое другое.
Комментарии (75)
babylon
20.08.2016 09:00-5Паттерн это ТЗ, которое вы пишете самому себе на том языке на котором программируете и затем имплементируете в классах и фреймворках.
sshikov
20.08.2016 09:39+2Он не зависит от языка.
Ага. Не зависит. Пока это язык вполне определенного класса. А потом вдруг бац — и сразу либо исчезает за ненужностью, либо трансформируется во что-то совсем другое.
AndreyRubankov
20.08.2016 10:45+2Так и есть, в каждом языке свои паттерны.
В мире ООП одни подходы, описанные в статье, в функциональном мире — другие, а в js как получится =)
Вот к примеру в js любой callback — это реализация паттерна стратегия, хоть и выглядит совсем не так, но суть у них одна.
А система обработки событий — это ничто иное как chain of responsibility.miktim
20.08.2016 11:44Я полагаю, паттерны языковонезависимы. Это как соглашения, просто на уровень выше. Люди смертны, а код вечен?
AndreyRubankov
20.08.2016 11:58+1Вот именно, это соглашение как писать код и в каждом языке своя идеология написания кода. То, что на ООП делается через классы, в ФП делается через коллбеки, замыкания и керринг.
А в js через классы, коллбеки, замыкание и керринг.
Перенося паттерны из одной идеологии в другую они видоизменяются и часто даже называются иначе.miktim
20.08.2016 12:24Так дело в терминах? Ага. «Фабрика» неокрепшие умы в ступор вводит.
sshikov
22.08.2016 11:50+1Да не, дело скорее не в терминах.
Есть определение паттерна Фабрика (в чем суть проблемы, и что делать), и есть его реализация. Так вот, реализация очень сильно языковозависима. Вплоть до полного ее отсутствия за ненужностью, потому что в конкретном языке именно эта проблема не актуальна/тривиальна.
Flammar
24.08.2016 10:24Вплоть до полного ее отсутствия за ненужностью
Фабрика — это, фактически, улучшенная разновидностьконструктораоператораnew
, способная динамически заменять класс подклассом, возвращат относительно древним языком Smalltalk, но краем уха слышал, что тамnew
является методом объекта типа «класс» и может быть переопределён для конкретного класса. Это, наверное, и есть пример встроенной в язык фабрики.
sshikov
22.08.2016 11:54+1Погуглите скажем несколько заметок Mario Fusco, о том, во что вырождаются ряд паттернов GoF после появления в Java 8 лямбд.
miktim
23.08.2016 09:27Так ведь он не отвергает все на корню. Просто указывает, что при определенных условиях теория перестает работать. Так на то она и теория.
Flammar
24.08.2016 10:09Публикации на тему вырождения дизайн-паттернов в нечто тривиальное в функциональных ЯП были сделаны Петером Норвигом ещё в 1996, всего лишь через два года после книги GoF.
sshikov
28.08.2016 15:32Я в курсе. Просто это свежий пример того, как в давно существующем языке при добавлении в него новых возможностей, паттерны вдруг стали совершенно непохожи на то, какими были недавно.
Кстати, вырождаются не паттерны вообще, а некоторые (как правило, специфичные для ООП), потому что у функциональных языков есть свои типовые надязыковые конструкции, даже если их паттернами не зовут.
Flammar
24.08.2016 09:58Вот к примеру в js любой callback — это реализация паттерна стратегия
С объектно-ориентированной точки зрения, всё функциональное программирование — это применение паттерна «стратегия». А асинхронная обработка с continuation-passing всегда будет напоминать chain of responsibility.AndreyRubankov
24.08.2016 10:33Да, оно то похоже, но все же есть пара замечаний.
Паттерны «стратегия», если простыми словами, — это когда аргумент функции (метода) принимает на вход «кусок кода» (функцию или имплементацию стратегии в ООП), который будет выполняться в работе данной функции.
Т.е. когда есть конкретная функция, которая вызывает другие конкретные функции — это не будет стратегией.
Функция, которая вызывает другую функцию для получения третьей (к примеру керринг) — это не будет стратегией.
А вот если в функцию передается другая функция — это уже будет стратегией.
> асинхронная обработка с continuation-passing
Это больше похоже на стратегию. Вызывается асинхронный метод, и в него передается callback функция, в которую будет выполнен возврат после выполнения асинхронного кода.
Chain of responsibility — это механизм, лежащий в основе pub/sub.
Dispatcher (на него подписываются и через него кидают event) внутри содержит хеш-таблицу сообщение -> список слушателей. Когда кто-то публикует сообщение, диспатчер берет список слушателей и поочередно выполняет их.
(но тут я не прав: «chain of responsibility по учебнику» будет заходить в каждую функцию и говорить «пришло такое сообщение», функция же должна будет посмотреть на это сообщение и решить выполнять над ним логику или нет)
sasha1024
22.08.2016 11:22+1Вы очень правы. Паттерны — это набор общепринятых костылей, которыми обходят уже изученные (но не решённые) изъяны языков. Появляется новая фича в языках, и какой-то паттерн отпадает за ненадобностью или превращается в что-то совсем очевидное.
sshikov
22.08.2016 11:39Ну я бы не называл это костылями, по одной простой причине — это все-таки изначально некие над-языковые конструкции, которые описывают, как решать типовую проблему, имея в наличии некоторый набор инструментов. Ну т.е., грубо говоря, есть список чисел, нужно получить "сумму". На выходе для одного языка цикл, а для другого скажем какой-то вариант fold/reduce. И кто из них костыли?
В той же фабрике в общем-то нет ничего плохого — это просто механизм абстрагироваться от деталей создания чего-либо. Костылем она становится тогда и потому, когда в языке нет удобных механизмов для выражения этого в общем-то весьма простого действия.
Иными словами — "что-то совсем очевидное" не перестает быть решением типовой проблемы. Оно просто становится простым решением.
sasha1024
22.08.2016 13:18Скажем так: это некие внеязыковые конструкции, которые описывают, как решать типовую проблему.
Но говорят о такой конструкции как о паттерне люди как правило тогда, когда реализация такой внеязыковой конструкции является не абсолютно тривиальной на некотором количестве значимых языков. Например, редко говорят о паттерне «цикл» или «вызов подпрограммы» (хотя во времена ассемблера это были самые что ни на есть настоящие паттерны).
Т.е. паттерн в широком смысле — полезная внеязыковая конструкция. Паттерн в узком смысле — полезная внеязыковая конструкция, не совсем тривиальная в реализации.
AlexeyFrolov
20.08.2016 11:02+6шел 2016 год…
AndreyRubankov
20.08.2016 11:18+9Шел 2016 год… Многие до сих пор не знают чем отличается массив от списка, абстрактный класс от интерфейса и как устроена хеш-таблица…
marshinov
20.08.2016 13:32+7я бы сказал, что есть обратная тенденция: современные задачи зачастую требуют не глубоких (или хотя-бы поверхностных) знаний, а возможности накодить здесь и сейчас.
OnYourLips
20.08.2016 18:28-6> абстрактный класс от интерфейса
Это очень хороший вопрос, который показывает, имел ли человек опыт в разработке ПО или нет.
Те, кто не имел, отвечают примерно так: «Абстрактный класс может иметь реализованные методы, а интерфейс — нет» и прочую ерунду, связанную с технической стороной.ZurgInq
20.08.2016 18:53+4Я даже не побоюсь спросить, а кроме технической ерунды, какие еще различия?
DistortNeo
20.08.2016 19:23Принципиальное различие: в языках, отделяющих абстрактный класс от интерфейса, возможно наследование только от одного базового класса, но от нескольких интерфейсов.
Второй важный момент: интерфейс работает как концепт. Фактически, он требует, чтобы класс имел определённый набор методов и свойств, при этом не имеет значения, где они реализованы: в текущем классе или базовых, ничего не знающих об интерфейсе. Такого эффекта невозможно достичь в языках, допускающих множественное наследование.
Всё остальное — действительно техническая ерунда. Важно только понимать, что интерфейсы дешёвые, они не замусоривают vtable и накладные расходы при их использовании ниже, чем при использовании полноценных классов. Особенно актуально это для C++, где виртуальное множественное наследование, используемое для эмуляции интерфейсов, приводит к раздуванию размеров объектов.Kitmouse
22.08.2016 11:23Простите, за, возможно, глупый вопрос, но как с технической стороны сделаны «быстрые» интерфейсы с полиморфизмом (времени выполнения) без vtable или более дорогих средств?
Звучит очень круто, хотелось бы узнать. В голову ничего не лезет, а запросы в гугл ничего не дали.DistortNeo
22.08.2016 12:06+1Нет, vtable, естественно, используются, без них никуда. Разница же такая:
1. Разная стратегия использования при наследовании. Если вы хотите иметь возможность перезаписать метод в классе-потомке, то делаете его виртуальным и оставляете открытым. При этом любой вызов открытых виртуальных методов будет осуществляться через vtable.
Методы интерфейса же являются автоматически virtual и sealed. Если хотите перезаписать метод интерфейса, то просто наследуете его второй раз, при этом старые методы будут неявно объявлены во второй раз. Это приводит к тому, что при вызове метода компилятор генерит оптимальный код, а не лезет в vtable.
Единственный подвох такого подхода: если преобразовать класс к базовому, а потом к интерфейсу, то будет использоваться интерфейс от базового класса. Но на практике я с таким не сталкивался.
2. Огромная разница в потреблении памяти. В C++ создаётся по отдельному полю vtable для каждого из множественно наследуемых классов, дополнительная память тратится на разруливание виртуального наследования. В C# информация о множественном наследовании сидит в единственном vtable на каждый объект в виде ссылок на vtable интерфейсов.
Обычно такая двойная адресация не вызывает проблем, т.к. выполняется только один раз. Единственное исключение: generic-методы с interface constraint, где двойная адресация происходит при каждом вызове интерфейсного метода.
В C++ же такое сделать попросту невозможно, т.к. объекты при множественном наследовании не могут располагаться по одному адресу. Из-за этого для каждого из базовых классов и приходится заводить по vtable.
P.S. Подумал — а ведь если это развить, то на отдельную публикацию пойдёт, надо бы заняться. Реально куча интересных нюансов вылезает.
OnYourLips
20.08.2016 19:29Разница в причине использования.
Интерфейс — это в первую очередь соглашение о том, в какой роли может быть использован объект. Это контракт, по которому работают с объектом, реализовавшим его. И нет никакой связи с реализацией. Иногда даже это просто маркер (интерфейс без методов).
AndreyRubankov
20.08.2016 19:43К предыдущим двум ответам, стоит лишь добавить, что Абстрактный класс определяет иерархию («это есть», «является»), в то время, как Интерфейс лишь говорит о возможности («может»).
Самолет — это механизм.
Птица — это живое существо.
Самолет и Птица может летать (летающий объект).
В небе можно увидеть Летающие объекты.
Danik-ik
20.08.2016 18:33+3Не знают. А что удивительного? Где-то рождаются дети. Кто-то осваивает новую специальность или специализацию. Новые люди, Карл!
Между тем вся страна электрифицирована, а коммунизм не наступил, хотя должен был, кажется.
Даже если принять как догму, что «все в отрасли должны это знать» (кому, кстати, должны-то?) — в отрасли всегда полно и новичков, и кустарей «на пару строк».
А когда все программисты до одного будут это знать — это будет признаком застоя и печали.AndreyRubankov
20.08.2016 19:30А ведь не только новички не знают. Вот к новичкам претензий нету. Многие, у кого за плечами уже по 2-4 года не знают ответов на указанные вопросы.
А на счет застоя, я не соглашусь. Когда все будут знать ответы на базовые вопросы, тогда произойдет качественный скачек развития всего ИТ мира.
Вот только чтобы знать ответы на все базовые вопросы, нужно очень много времени потратить. «Программирование — это не работа, это образ жизни».c4boomb
22.08.2016 02:30+1(Я знаю как устроена хеш-таблица)
Сколько раз вам за карьеру программиста пригодилось знание о том как устроенна хеш-таблица? То что редко используется — забывается, это естественный процесс. В нужный момент прочитать/вспомнить — не составит проблем.withoutuniverse
22.08.2016 11:03Вот есть UI поток, а есть другие Background потоки. И вы такой, на зная сложность вставки в хэштаблицу, решили добавить 1 запись туда. А она взяла и пересоздалась и начала пересчитывать хэши для корзин, так как места не хватало уже и UI подвис.
Или привыкли вы за O(1) забирать из ArrayList в UI, а сейчас решили из середины LinkedList достать и снова UI подвис.
Или удалить из середины ArrayList запись, или, что еще хуже, прямо в UI перебрать все элементы в нем и удалить по фильтру что-то.
Продолжать примеры?
Хорошего разработчика от посредственного эти вещи и отличают — он знает, что использовать и как это использовать.DistortNeo
22.08.2016 11:24Я бы сказал, что это особенно критично для сетевых игр, где огромное значение имеет не время в среднем, а наихудший сценарий.
red75prim
22.08.2016 11:21+1Массив от списка — ладно, вставка/удаление О(n) против О(1), индексация O(1) против O(n). Хотя неплохо бы ещё знать про константные составляющие, которые на современных процессорах в некоторых случаях делают выгодным использование массива вместо списка.
Устройство хеш-таблицы? Хм. Вообще-то их много разных. Достаточно знать, что при большом количестве коллизий производительность падает.
А общее отличие абстрактного класса от интерфейса зачем? Слишком зависит от языка. В С++ абстрактные классы и интерфейсы различаются только наличием non-pure methods. В C# интерфейсы — это отдельная языковая сущность. В Rust вообще абстрактных классов нет.
warlock13
20.08.2016 11:14+1Как сказал В. И. Ленин, паттерны нужны для того, чтобы быстро объяснить другому программисту что за #$!*% вы написали, а не для того, чтобы создавать фабрики фабрик (это и без паттернов отлично делается).
DistortNeo
20.08.2016 16:11+1Именно так. Код первичен, паттерн вторичен, а не наоборот.
Если вы пишете код и вам кажется, что этот кусок похож на паттерн, тогда и оформите его как паттерн — код станет легче читать. Применять же паттерны не к месту, начитавшись умных книжек — только портить артихектуру.
Фабрики фабрик вообще идут параллельно и растут корнями из TDD.
heilage
20.08.2016 11:34По сути все паттерны сводятся так или иначе к идее введения дополнительного уровня абстракции. Различаются только способы взаимодействия между уровнями. По идее эти способы могут быть очень гибкими и не ограничиваться только общепринятыми шаблонами. Почему и зачем насаждаются конкретные реализации, под которые люди начинают подгонять свой код (вместо того, чтобы на основе базовой идеи создать наиболее подходящее в конкретном случае взаимодействие) — мне лично не очень понятно.
AndreyRubankov
20.08.2016 12:08На самом деле никто не насаждает писать только через описанные паттерны. Более того, никто даже не обязывает реализовывать паттерны 1 в 1 как в примерах. Пример написан лишь для понимания как это может выглядеть.
У всего хорошего всего есть побочный эффект, если этим злоупотреблять. Витамины в большом количестве тоже причиняют вред. Понимание когда нужно использовать конкретный паттерн — это показатель зрелости специалиста.
miktim
20.08.2016 14:56-1Полезная статья. Паттерны, дети — это типовые решения. Никто не обязывает им следовать. Никто не обязывает натягивать ваше решение, как сову на глобус.Но, если желаете быть понятыми
Shamov
20.08.2016 15:41Серьёзно? Базовая статья про паттерны в 2016-м… через 20 лет после книги «Банды четырёх»?
Что дальше? Туториал по использованию колеса? Или добротная обзорная статья о всех плюсах и минусах использования огня?Danik-ik
20.08.2016 19:09+5Серьёзно? Вы думаете, в отрасли застой и нет новых людей, том числе без нормального образования? Может, и школы с банальным 1+1 стоит закрыть? Мы-то всё уже выучили!
Shamov
20.08.2016 21:44-2Хорошо, принимается. Честно говоря, я сразу не заметил, что код в статье на PHP.
withoutuniverse
21.08.2016 12:18Словно в других языках полнейший застой, и новых разработчиков не прибывает.
Shamov
21.08.2016 12:50-2Ключевой момент не в том, что в PHP нет застоя, а в том, что в нём приток новых людей обеспечивается в основном за счёт людей без нормального образования (которые даже про паттерны не в курсе). В других языках такой проблемы либо нет, либо она стоит не настолько остро. Я, например, не представляю себе человека, который окончил бы университет по специальности Computer Science, а затем вдруг взял и выбрал PHP в качестве точки приложения полученных знаний и навыков. Это был бы нонсенс. Образование помешало бы ему совершить подобный выбор. Но такой выбор совершенно спокойно делают люди без образования. Он не кажется им ошибочным. В общем, PHP — это Basic наших дней.
withoutuniverse
21.08.2016 12:58Наверное для вас будет новостью, но фейсбук и вконтакте написаны на PHP.
Мыслите стереотипно. Я провел множество собеседований по Java, почти все люди с высшим образованием в сфере it. Но и имея 5-летний опыт работы люди могли не знать, как работает Хэш Таблица, не знать, что такое шаблон Адаптер и т.д.Shamov
21.08.2016 13:52-1Про фейсбук и вконтакт я в курсе. Они активно используют дешёвую рабочую силу. Можно сказать, это главная составляющая их успеха. По крайней мере, была на начальном этапе. Сейчас-то они уже могут себе позволить и более квалифицированные кадры, но генотип так просто не изменишь. Приходится работать с тем, что есть. Это суровая реальность. Эффектно взлететь и быстро раскрутиться, сварганив прототип в собственном гараже, можно только на дешёвом ресурсе. И, к сожалению, эта основа останется в проекте навсегда. Если ты построил в гараже моторизированный велосипед, то в дальнейшем можно будет только увеличивать его размер, наращивать мощность двигателя и прикручивать к нему всякие штуки типа полифонического гудка с поддержкой mp3. Развить этот велосипед в атомную подводную лодку никак не получится. У такой лодки совершенно иной цикл развития. И начинается он не в гараже.
И, кстати, мыслить стереотипно не так уж и плохо. Стереотипы — это шаблоны мышления. С их помощью очень легко делать выводы, не погружаясь глубоко в детали. Иногда выводы получаются неверными, но не в данном случае. Если человек не знает, как работает хэш-таблица, то про него никак нельзя сказать, что у него есть профильное образование. Возможно, у него есть какой-то документ, который позволяет ему вводить окружающих в заблуждение, но не более того. О действительном наличии того, что называется словом «образование», речь в данном случае не идёт.Serg046
23.08.2016 14:13-1Пусть и не поддерживаю насмешку над php, но хочу немного поддержать. Действительно и вк и фейсбук в дальнейшем были вынуждены писать свои трансляторы и движки для php (если не ошибаюсь, это про компиляцию в с/с++), чтобы не выкидывать тонну существующего кода. Однако перетестировать его все равно пришлось по новой (разве что повторное юнит тестирование не требовалось).
Shamov
23.08.2016 14:41-1На самом деле, никакой насмешки у меня тоже не было. Только трезвый и хладнокровный анализ ситуации. Не понимаю, как здравомыслящий человек может отрицать то, что PHP — это современный Basic, а Java — современный Visual Basic. Самое смешное в том, что когда я выбирал движок для блога, я выбрал WordPress, а не Octopress, который написан на Ruby. При том, что Ruby как язык намного круче PHP. Но ключевую роль при выборе сыграло то, что комьюнити вокруг WordPress намного больше.
Shamov
23.08.2016 21:30-1Кстати, интересный вопрос, почему так получилось. Было ли это предопределено с самого начала или просто случайность?
Ответ на этот вопрос содержится в цитате самого Расмуса Лердорфа: «Я ненастоящий программист. Я компоную всякие штуки, пока это не начинает работать. Потом иду дальше. Настоящий программист скажет: „Ок, это работает, но тут утечка памяти, надо пофиксить“. А я просто перезапускаю Apache каждые 10 запросов»GlukKazan
22.12.2016 09:37Только канон, только хардкор. Я очень уважаю творчество Макса Фрая, но скандинавские боги выступающие плечом к плечу вместе с олимпийскими — это уже из цикла "бывал ли Перун в Афинах". Нордический фольклор вполне самодостаточен без Кецалькоатля.
Shamov
24.08.2016 12:10-1Ну, если у молодых горячих разработчиков это считается оскорблением, то я тогда тоже не удивлён.
Вот, кстати, про Java любопытно. Вы часто встречали проекты, в которых Java компилируется в бинарный код? Я имею в виду, кроме как под Android. А ведь такие компиляторы есть и для десктопных платформ. Но только никто ими не пользуется. Потому что это никому не нужно.
Serg046
24.08.2016 15:51-1Т.е. смена технологии в середине проекта и необходимость перетестирования огромного кол-ва уже протестированного кода — это абсолютно нормальное развитие событий? Причем прошу заметить, вк начал развитие после фб и вполне мог подумать над выбором технологии.
Я и не говорю, что php плох. Я ничего про уровень языка не писал. Все языки хороши по своему. Но у каждого свой скоуп применения.withoutuniverse
24.08.2016 18:12Разве кто-то сменил технологию? Транслятор сменили и всё. Как писали на php, так и пишут.
Да и Windows множество участков кода имеет на c# — нет проблем в таком подходе.Serg046
24.08.2016 19:50-1Разве кто-то сменил технологию? Транслятор сменили и всё. Как писали на php, так и пишут.
Ну это смотря что под технологией понимать. К тому же насколько помню сам язык тоже притерпел некоторые модификации.
Разве кто-то сменил технологию? Транслятор сменили и всё. Как писали на php, так и пишут.
Да и Windows множество участков кода имеет на c# — нет проблем в таком подходе.
Я выделил жирным проблему. Windows начал использовать c# не вместо, а вместе с существующим кодом. И к тому моменту c# уже протестирован. Ну совсем разные вещи. Попробуйте взглянуть на ситуацию с точки зрения бизнеса.
varanio
20.08.2016 21:15В паттерне «фабричный метод» в примере я бы пихнул стат. метод createButton в абстрактный класс, а класс ButtonFactory грохнул бы.
Зачем плодить полупустые сущности, раз там все равно тупой статический метод?withoutuniverse
21.08.2016 13:07-1И это очень плохо, так как базовый класс знает, как будут создаваться наследники. И что еще хуже, он будет знать, куда этот объект будет сохраняться или откуда будут браться данные. Например, есть у вас статичный метод, принимающий код ошибки, а возвращающий строку с её описанием для UI. И лезет он за этими данными в BD/Network/XML/Etc. — вот тут жесткая связь данных и их способа хранения, никогда так не делайте :)
varanio
21.08.2016 17:02> И это очень плохо, так как базовый класс знает, как будут создаваться наследники.
Согласен с этим.
Но как это знание тут может помешать на практике? Т.е. грубо говоря, что в случае с отдельным классом, что в случае с тем же классом — при добавлении наследников в иерархию, статический метод должен дорабатываться.
> Например, есть у вас статичный метод, принимающий код ошибки, а возвращающий строку с её описанием для UI
А при чем тут фабрика вообще, если возвращается строка?withoutuniverse
21.08.2016 17:16Я в синтаксисе php не силен и мои рассуждения строятся на том, что я реализую некий интерфейс, чего со статик методами не сделать. Какая будет реализация — уже абстракция.
Т.е. я могу прокинуть любую фабрику для инстанциирования объекта.
Возвращается объект типа String — каким он будет, определяет фабрика.
Если на выходе будет не String, а Product, как в примерах из википедии — суть не изменится, это фабрика.
samizdam
21.08.2016 13:55+1Порой такой компромиссный подход может сработать.
Но, по мере усложнения приложения, как правило не долго.
Разделение ответственности: Инстанцирование кнопки — одна ответственность. Реализация функциональности кнопки — другая.
Т.о. отделив создание объекта от него самого, через ввод фабрики, вы сможете
а). Вводить в приложение различные фабрики, например на этапе исполнения, либо через конфигурацию
б). Замочить сам процесс создания кнопки, что позволит оттестировать код с разных сторон.
AndreyRubankov
21.08.2016 14:20У static factory method немного другой смысл. Он предназначен, чтобы создавать закрытые библиотеки / фреймворки, который можно замокать, но нельзя расширить.
Это облегчает жизнь, когда нужно поддерживать обратную совместимость. Появляется возможность безболезненно менять строение внутренних классов, главное, чтобы они соответствовали интерфейсу. Можно добавлять, удалять, изменять имплементации (они внутренние и никто не может на них завязаться).
Пример тому в Java — EnumSet.
Позволяет создать множество из enum объектов. При необходимости, в метод, принимающий EnumSet можно передать мок или свою реализацию. Но вот залезть в этот «фреймворк» и каким-то образом изменить его стандартное поведение, не получится.
В зависимости от количества элементов в Enum, этот класс может выдать разные реализации, некоторые будут построены на битовой маске int, некоторые на битовой маске long, если 32 или 62 бита не хватит — можно использовать массив. Нужно будет создать множество из одного элемента — пожалуйста. Если тесты производительности покажут, что Конкретная имплементация для 2 элементов будет работать лучше, чем имплементация с битовой маской — можно добавить ее и просто обновив версию пользователь получит прирост производительности без каких либо изменений своего кода.
kanstantsin
22.08.2016 11:22Если убрать номера строк примерах, есть шанс, что будет работать подсветка синтаксиса (не уверен, как она работает на хабре).
Visseri
22.08.2016 11:23Про паттерны уже много более подробных статей. А еще есть GoF, которая лучше любой статьи.
lyhoshva
22.08.2016 11:24В статье у паттерна «Адаптер» пример реализации паттерна «Фасад».
Шаблон фасад (англ. Facade) — структурный шаблон проектирования, позволяющий скрыть сложность системы путём сведения всех возможных внешних вызовов к одному объекту, делегирующему их соответствующим объектам системы.
stepanp
Обмажутся своими паттернами, а потом создают абстрактный класс через абстрактный метода абстрактной фабрики для того чтобы вызвать метод сигнлтона через обсервер
AndreyRubankov
Да, бывает и такое. Все результат будет отличаться от того, в каких руках инструмент.
Один напишет абстрактную фабрику по созданию абстрактных фабрик для создания абстрактных молотков.
А другой напишет удобную расширяемую систему, с которой приятно работать.
KvanTTT
Не забываем также про принципы KISS (Keep it short and simple) и YAGNI (You ain't gonna need it).
Flammar
Одно дело — не применять инструмент потому, что знаешь, что всегда можешь применить его, или другие аналогичные, позже, и другое — не применять потму, что не знаешь о существовании таких инструментов. Хотя на первый взгляд оба случая похожи.
VladimirKochetkov
Одно из наиболее адекватных мнений о злоупотреблении паттернами (на мой взгляд) было озвучено много лет назад на просторах RSDN:
lair
Вообще, эта позиция плохо сочетается с A Pattern Language Александера, который и заложил формат "проблема — контекст — решение". Даже если говорить о распознавании, то оно здесь применимо к "проблемной" части, а не к решению. Но на самом же деле, pattern у Александера — это не что-то, по чему распознают. Это образец, по которому решают проблему.
Другое дело, что люди часто забывают ознакомиться с проблемой-контекстом-последствиями, и сразу хватаются за решение.
VladimirKochetkov
В большей степени соглашусь, хотя в своей книге «Применение шаблонов проектирования: дополнительные штрихи» Влиссидес критикует определение Александера, называя его первым в числе основных заблуждений о шаблонах :) Критикует правда — не вполне за то, о чем идет речь здесь, но тем не менее. С другой стороны, сам Александер признает, что описываемые им шаблоны появились именно вследствие сопоставления успешных архитектурных проектов и выделения (распознавания) в них общего, направленного на решение той или иной конкретной проблемы в заданном контексте. Т.е. применимость шаблонов для распознавания заложена в них изначально, по их определению и способу появления на свет.
Полагаю, здесь будет уместной уже упомянутая в цитате аналогия с регулярными выражениями, по которым всегда можно построить как распознающий, так и генерирующий автоматы. Т.е. шаблоны безусловно можно применять двояко: как для решения ими каких-либо проблем, так и для разбора архитектуры и понимания того, какую проблему решает та или иная часть проекта. Но во втором случае, применение паттернов направо и налево не влечет за собой каких-либо негативных последствий, в отличии от. Думаю, это и было основной мыслью процитированного мной сообщения.
lair
Можно сколько угодно критиковать определение Александера, но отрицать тот факт, что GoF построен по тому же принципу, достаточно сложно.
Во-первых, если что-то появилось в результате распознавания, это еще не значит, что это что-то само подходит для распознавания. Во-вторых, важный вопрос — какая часть паттерна подходит для распознавания: проблема или реализация?
Влечет. Можно увидеть проблему, которой не было.