Comments 98
Совсем недавно по мотивам этой книги создал репозиторий с имплементацией базовых паттернов (не все, что описаны в книге, только на сколько хватило энтузиазма) на php. Не сочтите за рекламу 2.
"""
Define a represention for a grammar of the given language along with an
interpreter that uses the representation to interpret sentences in the
language.
"""
import abc
class AbstractExpression(metaclass=abc.ABCMeta):
"""
Declare an abstract Interpret operation that is common to all nodes
in the abstract syntax tree.
"""
@abc.abstractmethod
def interpret(self):
pass
class NonterminalExpression(AbstractExpression):
"""
Implement an Interpret operation for nonterminal symbols in the grammar.
"""
def __init__(self, expression):
self._expression = expression
def interpret(self):
self._expression.interpret()
class TerminalExpression(AbstractExpression):
"""
Implement an Interpret operation associated with terminal symbols in
the grammar.
"""
def interpret(self):
pass
def main():
abstract_syntax_tree = NonterminalExpression(TerminalExpression())
abstract_syntax_tree.interpret()
if __name__ == "__main__":
main()
Ну первое это вообще больше техника рефакторинга. Нет?
Современно, понятно, разобраны плюсы и минусы каждого паттерна, плюс прикладное использование. Подходит для любого языка.
Шаблоны мне не нравятся тем, что их всегда подают под неправильным соусом. Лично я не согласен с:
Шаблоны проектирования — это способ решения периодически возникающих проблем. Точнее, это руководства по решению конкретных проблем
Шаблоны проектирования — это не способ решения проблем, это способ записи решения проблемы на объектно-ориентированном языке программирования в некотором устоявшемся виде. Оформление кода в соответствии с шаблоном, а не использование велосипеда, делает код более структурированным и читаемым, только и всего.
Ну почему же. Просто возникли они в ООП среде и исторически большинство каноничных примеров на соответствующих языках. Но концептуально-то, если есть задача что-то, например, порождать, фабрику можно реализовать и в виде функции. Если воспринимать шаблоны, как концепции решения абстрактных задач программирования, то не так уж они и привязаны к языкам и парадигмам. Шаблоны, это скорее про разделение ответственности между подпрограммами, ради более прозрачной и гибкой архитектуры. Но к этому стремились и до, и после появления ООП языков.
Паттерны настолько элементарны, что любой программист, не знающий их, все равно будет их использовать. Знание паттернов просто позволяет оформлять код в универсальном виде.
А что в данном случае значит "написать реализацию фасада"? Самому придумать пару классов и завернуть их в класс-фасад? Или что-то другое?
А как вам мода — напишите на листочке реализацию фасада, на собеседованиях
А это результат того, что в резюме пишут: «знаю Design Patterns», а потом не могут назвать ни одного примера (хоть название)
Реализацию шаблона нельзя помнить наизусть, на то это и шаблон. Вы либо понимаете что это за шаблон и можете написать в нужном месте конкретную реализацию этого шаблона, либо не понимаете и не можете написать. Запоминать конкретные реализации — это все равно что запоминать конкретную программу или скрипт — нафиг никому не нужно.
Паттерны на то и паттерны, что у них нет образцовой реализации, это просто шаблоны по которым должны строиться конкретные решения конкретных задач, чтобы иметь право называться реализациями того или иного паттерна. Соответствует решение шаблону (прежде всего по списку участников и типов взаимодействия между ними) — решение реализует данный паттерн. Не соответствует — нешаблонное решение или реализует другой паттерн, например вместо фасада реализует адаптер или прокси.
напишите на листочке реализацию фасада, на собеседованиях?
сразу уточню что подразумевается. Вдруг интервьювер слишком много писал на ларавели и у нас различается понимание этого слова.
А так — я чаще просто спрашиваю чем адаптер от декоратора отличается. Бывает весьма интересно послушать. В целом мало кто пытается строить связь даже между названием паттерна и его предназначением… просто заучивают. Грустно. При том что пользы от этого ноль.
Насколько я помню, идея GoF была в том, чтобы создать глоссарий для облегчения коммуникации между программистами… а не в том, чтобы кого-то учить программировать.
Просто возникли они в ООП среде и исторически большинство каноничных примеров на соответствующих языках.
Многие паттерны созданы для преодоления проблем ООП, которых в, например, ФП просто нет. Более того, некоторые паттерны созданы для решения проблем исключительно статически типизируемых ООП языков и в языках типа PHP, Ruby, Python и JS решаются нативно в рантайме.
Про Компоновщик неверно.
Суть данного шаблона в том, что и одиночные объекты, и композит (компоновщик) реализуют один и тот же интерфейс. Именно это позволяет клиенту работать с группой объектов (инкапсулированной в компоновщике) точно так же, как и с одиночным объектом.
В примере же, приведенном в статье, класс Organization не реализует интерфейс Employee — таким образом, он не является композитом в том смысле, который закладывается в данном шаблоне.
Более правильный пример был бы что-то вроде:
interface Assignee {
public function canHandleTask($task): bool;
public function takeTask($task);
}
class Employee implements Assignee {
// реализуем методы интерфейса
}
class Team implements Assignee {
/** @var Assignee[] */
private $assignees;
// вспомогательные методы для управления композитом:
public function add($assignee);
public function remove($assignee);
// метода интерфейса Employee
public function canHandleTask($task): bool {
foreach ($this->assignees as $assignee) if ($assignee->canHandleTask($task)) return true;
return false;
}
public function takeTask($task) {
// может быть разная имплементация - допустим, некоторые задания требуют нескольких человек из команды одновременно
// в простейшем случае берем первого незанятого работника среди this->assignees
$assignee = ...;
$assignee->takeTask($task);
}
}
// Использование:
class TaskManager {
private $assignees;
public function performTask($task) {
foreach ($this->assignees as $assignee) {
if ($assignee->canHandleTask($task)) {
$assignee->takeTask($task);
return;
}
}
throw new Exception('Cannot handle the task - please hire more people');
}
}
$employee1 = new Employee();
$employee2 = new Employee();
$employee3 = new Employee();
$employee4 = new Employee();
$team1 = new Team([$employee3, $employee4);
// ВНИМАНИЕ: передаем команду в taskManager как единый композит.
// Сам taskManager не знает, что это команда и работает с ней без модификации своей логики.
$taskManager = new TaskManager([$employee1, $employee2, $team1]);
$taskManager->preformTask($task);
// интерфейс-состояние
interface IPhoneState {
pickUp():IPhoneState;
hangUp():IPhoneState;
dial():IPhoneState;
}
// несколько реализаций
class PhoneStateIdle implements IPhoneState {
pickUp():IPhoneState {
return new PhoneStatePickedUp();
}
hangUp():IPhoneState {
throw new Exception("already idle");
}
dial():IPhoneState {
throw new Exception("unable to dial in idle state");
}
}
class PhoneStatePickedUp implements IPhoneState {
pickUp():IPhoneState {
throw new Exception("already picked up");
}
hangUp():IPhoneState {
return new PhoneStateIdle();
}
dial():IPhoneState {
return new PhoneStateCalling();
}
}
class PhoneStateCalling implements IPhoneState {
pickUp():IPhoneState {
throw new Exception("already picked up");
}
hangUp():IPhoneState {
return new PhoneStateIdle();
}
dial():IPhoneState {
throw new Exception("already dialing");
}
}
// автомат
class Phone {
private IPhoneState state;
constructor() {
this.state = new PhoneStateIdle();
}
pickUp():void {
this.state = this.state.pickUp();
}
hangUp():void {
this.state = this.state.hangUp();
}
dial():void {
this.state = this.state.dial();
}
}
Лично меня особенно восхищает этот шаблон за его умение организовать ветвящуюся логику без if'ов.
Что, опять? Банды четырёх мало что ли?
Вобщем-то да. Это далеко не лучшее чтиво на данную тему на сегодняшний день.
А почему бы и нет?
Посмотрите на статистику этой статьи.
Пипл до сих пор хавает пересказ книги начала нулевых с ухудшениями от Рабиновичей, а мыло готово кормить этим досыта.
И пофиг, что синглтон — антипаттерн, что создающие паттерны давным-давно реализованы в DI-контейнерах, что State — это тихий ужас, что компоновщик в статье перевран и т.д. и т.п.
Интересно, а почему это синглтон внезапно антипаттерн? Вы ведь наверняка не про обращение в глобальном контексте (это его использование, которые вполне возможно через DI), а уверенность автора класса в том, что больше одного инстанса пользователь не создаст?
Синглтон — антипаттерн по построению.
Он имеет две ответственности: делает что-то полезное и контролирует количество собственных экземпляров.
Это нарушение SRP с особым цинизмом.
На практике это выливается в многослойный геморрой:
- Приходится наследоваться от специального класса или копипастить
- Тестирование синглтона затруднено
- Тестирование синглтона с зависимостями от других синглтонов еще сложнее
- Когда становится нужно иметь два экземпляра класса-синглтона — у программиста большие проблемы
Самое забавное, что правильное решение тривиально — достаточно, чтобы количество экземпляров контролировал отдельный объект. Причем его даже не надо писать самому — полно готовых реализаций, от Lazy до DI-контейнеров.
А синглтону место в параде антипаттернов, как безусловно вредному, но крайне распространенному и живучему решению.
Когда становится нужно иметь два экземпляра класса-синглтона — у программиста большие проблемы
зачем иметь два экземпляра класса СИНГЛтона? это не проблема паттерна, это проблема того, кто хочет «два экземпляра класса-синглтона». Это тоже самое, если бы Вы купили шоколадное мороженое, а потом сказали бы «ну блин, я же хочу ванильное. с этим мороженым что-то не так»
Это проблема антипаттерна "синглтон".
Требования имеют свойство меняться и более простой класс, не занимающийся контролем числа собственных экземпляров, оказывается заведомо более удобным и гибким, чем синглтон.
Я не спорю, что синглтон атипаттерн и что его функционал (слежение за количеством экземпляров) реализован в DI-контейнерах, но Вы приводите смешные примеры.
Это проблема нарушения SRP паттерном. Никому (наверное) не нужно два синглтона, но бывает становится нужно два и более соединения к СУБД или иному серверу, два и более экземпляра конфигов, два и более экземпляра профиля пользователя и т. п., но по коду уже везде разбросаны вызовы SomeSingleton::getInstance() даже не из требования строго ограничить количество экземпляров одним, а просто потому что так удобно и экономятся ресурсы.
Так внедрение неявных зависимостей с тяжелой инициализацией и есть основной способ применения синглтона де-факто. Просто замена многочисленных new Class() на Class::getInstance. Если бы его применяли где-то в самом начале стэка вызовов, а потом передавали параметрами, то может он бы антипаттерном и не считался, вот только смысла бы в нём было мало, а так смысл в том, что он доступен глобально.
Глобальная доступность — свойство синглтона по определению.
Ну открываем определение сингелтона и читаем требования к реализации:
An implementation of the singleton pattern must:
- ensure that only one instance of the singleton class ever exists; and
- provide global access to that instance.
последний пункт как раз про это
Именно поэтому он синглтон, а не глобалтон какойто
глобальный доступ это не цель, это деталь реализации.
Класс может таким образом защищаться от неправильного использования. С тем же успехом можно и конструктор называть "второй ответственностью", что же, у класс и себя конфигурирует, и http-запросы посылает? Вот негодяй! Синглтон это такой подвид конструктора, который не принимает параметров. Мокать его как правило тоже не нужно, потому что мокать нужно интерфейс, который этот синглтон реализует. А параметров у синглотнов быть не должно, иначе это запах и использование паттерна не по-назначению.
Если вдруг нужно 2 инстанса — не вопрос, убираем синглтон и делаем какую-то обертку, но ведь рефакторинг под изменившиеся требования это ок. Или вы всегда предусматриваете ВСЕ возможные изменения в будущем во ВСЕХ местах? Звучит как оверинжинеринг.
и себя конфигурирует, и http-запросы посылает? Вот негодяй!
поэтому придумали фабрики.
Хотя в целом я с вами согласен.
Класс может таким образом защищаться от неправильного использования.
Может. Но реальному использованию класса такой способ защиты скорее вреден, чем бесполезен.
С тем же успехом можно и конструктор называть "второй ответственностью", что же, у класс и себя конфигурирует, и http-запросы посылает?
Конструктор ничего не конфигурирует — он отвечает за создание полноценного рабочего экземпляра объекта. Все что нужно для этого должно передаваться в параметрах.
Синглтон это такой подвид конструктора, который не принимает параметров.
Это прямо противоречит определению от "банды четырех".
Если вдруг нужно 2 инстанса — не вопрос, убираем синглтон и делаем какую-то обертку, но ведь рефакторинг под изменившиеся требования это ок.
Рефакторинг, который появляется исключительно от использования синглтона — это совсем другое слово: "Антипаттерн".
Звучит как оверинжинеринг.
С точностью до наоборот. Перепроектирование — это добавление к классу вредного кода, контролирующего число его экземпляров.
Альтернатива — одна строка кода (при использовании Lazy) или один дополнительный вызов (при использовании DI-контейнера) или вообще бесплатно (если регистрация в контейнере ограничивает число экземпляров по умолчанию).
Представьте что вам нужно написать библиотеку для работы с COM портом. Доступ к нему должен быть строго у одного инстанса на процесс. Предложите способ организовать это так, что бы клиентскому коду небыло проблем? Я вижу один — сингелтон. Просто и железобетонно. А недостатки вроде "глобального доступа" можно скрыть с клиентской стороны добавив в тот же контейнер. Но зато мы с нимем с разработчика, который будет использовать нашу библиотеку риски того, что он "случайно" может сделать что-то не то.
Но таких кейсов 1 на тысячу. В большинстве же случаев сингелтон лепят просто так.
Ровно один ком-порт на машину? Ну-ну.
Добавив контейнер, синглтон можно выкинуть сразу.
Это для примера. То есть вы обязанность вашей библиотеки будете перекидывать на клиента (или заставлять его использовать ажно целый контейнер, причем именно тот который вам захотелось).
Нет. Я полагаю, что библиотека не должна принуждать клиента к определенной композиции.
Что характерно, даже умозрительного примера пользы синглтона с ходу привести не удалось.
библиотека не должна принуждать клиента к определенной композиции.
В тоже время вы именно этот вариант и предлагаете.
Я не пытаюсь доказать что "сингелтоны полезны", для меня этот паттерн это как "множественное наследование" например. То есть за время коммерческой разработки может быть один раз и оправдано.
Может. Но реальному использованию класса такой способ защиты скорее вреден, чем бесполезен.
А у меня с точностью до наоборот мнение.
Рефакторинг, который появляется исключительно от использования синглтона — это совсем другое слово: "Антипаттерн".
Рефакторинг, который появляется от изменения требований.
С точностью до наоборот. Перепроектирование — это добавление к классу вредного кода, контролирующего число его экземпляров.
Альтернатива — одна строка кода (при использовании Lazy) или один дополнительный вызов (при использовании DI-контейнера) или вообще бесплатно (если регистрация в контейнере ограничивает число экземпляров по умолчанию).
Надо полагать, что если DI в проекте, нет, то авторы — лохи, которым нужно ткнуть в нос SOLID и пинать, пока они не поймут, что без IoC жизни нет.
А у меня с точностью до наоборот мнение.
Отсюда и понятие "антипаттерн": несмотря на объективный вред, у многих другое субъективное мнение.
Рефакторинг, который появляется от изменения требований.
Без синглтона этот рефакторинг не нужен и после изменения требований.
Надо полагать, что если DI в проекте, нет, то авторы — лохи, которым нужно ткнуть в нос SOLID и пинать, пока они не поймут, что без IoC жизни нет.
Проект от среднего размера без DI (не контейнера, а паттерна) — либо легаси, либо авторы таки да.
Теги: "Никто не читает теги" — порадовало
А статья — весьма монументальна, приятно видеть
interface WebPage
{
public function __construct(Theme $theme);
public function getContent();
}
Конструктор как элемент интерфейса, серьёзно? И почему бы не использовать наследование от абстрактного класса, например:
abstract class AbstractWebPage
{
protected $theme;
public function __construct(Theme $theme)
{
$this->theme = $theme;
}
abstract public function getContent();
}
class About extends AbstractWebPage
{
public function getContent()
{
return "About page in " . $this->theme->getColor();
}
}
class Careers extends AbstractWebPage
{
public function getContent()
{
return "Careers page in " . $this->theme->getColor();
}
}
Чем объясняется повсеместное применение реализаций интерфейсов вместо наследования?
Видимо, следованием принципов SOLID. Но конкретный пример действительно ужасен: если уж следовать паттернам и делать по-человечески, то интерфейс должен называться IWebContentProvider
, а не WebPage
, а конструирование вынесено в отдельную фабрику.
Но опять же, плодить сущности только ради того, чтобы заюзать какой-нибудь паттерн, плохо. Здесь действительно можно обойтись абстрактным классом.
Видимо, следованием принципов SOLID
ни один из принципов SOLID никак не влияет на выбор между implements
и extends
. Они немного подсказывают что интерфейсы чаще лучше для организации иерархии типов, но конкретно в этом случае наличие конструктора в интерфейсе явный признак того что что-то пошло не так.
Вообще на тему implemets vs extends есть неплохая статья: Why extends is evil
то интерфейс должен называться IWebContentProvider, а не WebPage
скорее просто ContentProvider
. Нам же в целом только это важно.
Чем объясняется повсеместное применение реализаций интерфейсов вместо наследования?
1. Строка public function __construct(Theme $theme); в интерфейсе, еще не повод ставить под сомнение повсеместное применение интерфейсов. Это все равно, если запретить спички, только потому, что один ребенок обжег ими палец. Или поставить под вопрос использование автомобилей, ведь за их рулем часто оказываются нетрезвые люди. Просто, нужно правильно использовать возможности языка, а не писать определение конструктора в интерфейсе, только лишь потому, что язык этого не запрещает.
2. Почему же их все-таки нужно использовать и желательно почаще?
Не будем далеко ходить и возьмем ваш пример. Мы имеем один абстрактный класс и два класса реализующие его. Допустим, у нас есть некий клиентский класс, который обращается к getContent().
class Client
{
private $page;
/**
* Client constructor.
*
* @param $page
*/
public function __construct(AbstractWebPage $page)
{
$this->page = $page;
}
public function showPage()
{
return $this->page->getContent();
}
}
Таким образом мы научили наш клиент работать только с AbstractWebPage. Что, если мы хотим добиться от клиента работы не только с AbstractWebPage типом но и другими похожими типами? Стоит отметить, клиенту важно только одно, это метод getContent(), все что он должен знать о типе AbstractWebPage.
Тут вспоминается пример из жизни, когда обезьянку научили приносить воду в ведре. Та ходила по искусственной дорожке, где в конце был небольшой резервуар с водой. Позже, к этим условиям добавили бассейн с водой, однако обезьянка продолжила ходить за водой по привычному маршруту, тогда как бассейн с водой был намного ближе.
Также и тут, в нашем примере, мы словно учим обезьяну. Мы говорим ей вместо «воды» «принести», «воооон там, есть дорога, ты должна пройти по ней и только по ней, а в конце дороги будет резервуар и из него набрать воды» «принести»
Возвращаясь к нашему примеру. С помощью интерфейса мы можем определить более абстрактно тип страницы PageInterface. И любой класс реализующий данный тип будет обязан описать логику для метода getContent()
3. Почему не использовать в нашем примере абстрактный класс вместо интерфейса? — это неправильный вопрос. Почему мы можем использовать их вместе? — а вот это уже правильный.
Если мы будем использовать и то и другое, то все сразу встанет на свои места. Интерфейс даст возможность определить корневой тип, суперабстракцию, а абстрактный класс сможет ее уточнить/конкретизировать до необходимой степени. Таким образом мы правильно распределим ответственность за степень абстракции между классами и сделаем более понятной их иерархию.
В итоге у нас может быть такое решение, когда типов страниц может быть больше одного, а клиент не хочет брать на себя ответственность за их обработку. Он знает только о getContent и что на входе у него уже есть какая то реализация типа PageInterface.
abstract class AbstractWebPage implements PageInterface
{
protected $theme;
public function __construct(WebTheme $theme)
{
$this->theme = $theme;
}
}
class About extends AbstractWebPage
{
public function getContent()
{
return "About page in ".$this->theme->getStyle();
}
}
abstract class AbstractPdfPage implements PageInterface
{
protected $theme;
public function __construct(PdfTheme $theme)
{
$this->theme = $theme;
}
}
class Cv extends AbstractPdfPage
{
public function getContent()
{
return "Cv page in ".$this->theme->getStyle();
}
}
class Client
{
private $page;
/**
* Client constructor.
*
* @param $page
*/
public function __construct(PageInterface $page)
{
$this->page = $page;
}
/**
* @return PageInterface
*/
public function showPage(): PageInterface
{
return $this->page->getContent();
}
}
Сложилось впечатление, что в понимании автора (оригинальной статьи) интерфейс — это представление некой абстрактной сущности, а не её поведения (даже судя просто по названиям интерфейсов).
Забыли реализовать интерфейс Observer в примере с наблюдателем
Правда, мне примеры не всегда нравятся.
Но почему вы молчали так долго? :)
Только вас, так итератору не нужно явное позиционирование.не совсем понял, я туплю или чего-то не знаю о итераторах…
public function current(): RadioStation
{
return $this->stations[$this->counter]; //$this->counter по умолчанию пустая
}
Но надо учитывать, что тут итератор сам по себе… нестандартный. Обычной имеется ввиду отдельный объект, позволяющий последовательно перебрать все элементы в исходной коллекции. И ничего кроме Current, Next и Reset не должно быть в итераторе.
public function key()
{
return $this->counter;
}
но тут она просто NULL, что не так страшно.
Шаблоны проектирования с человеческим лицом