Да. И не только. Например для соблюдения SRP, часть кода можно вынести в обработчик событий, но чтоб не размазывать бизнес логику по проекту, её можно сохранить в сущности.
Например, тот же пример от Fantyk можно решить через обработку событий в сущности заказа.
Я в это случае создаю CQRS команду на отправку SMS уведомления на каждое событие и кладу ее в очередь уникальных команд и по крону разбираю очередь.
Это даёт нам и сохранение уникальности уведомлений и позволяет не тормозить клиент на отправку SMS и контролировать нагрузку на сервер отправки SMS.
Кроме стандартного агрегирования событий ещё позволяет обрабатывать события в самой сущности, имеет функционал для реализации слушателей, подписчиков, очередей и middleware
Ну если прям для савсем савсем простых CRUD проектов которые не планируют развивается, то да. Сгодится. Правда я с такими проектами не работаю.
Мои проекты либо сразу сложнее, либо гарантированно будут усложняется и тогда внедрение сонаты только время сэкономит.
Спасибо за ссылку.
Не знал про EasyAdminBundle.
Но похоже EasyAdminBundle ещё больше завязан на антипаттерне Anemic model, чем SonataAdminBundle.
И конфигурирование форм в yaml файлах попахивает чем-то.
И не понятно как использовать свои FormType
Мне не очень понятно почему вы делаете билд на каждый коммит. Вы же с git работаете и билд у вас на push запускается. Зачем вам пушить каждый коммит в отдельности? Может вы ещё от svn не отвыкли?
С гитом обычно так — поработали немного, сделали несколько коммитов, дошли до какой-то логической точки и запушили.
Разве вы делаете не так?
Если это логическая точка, то почему не сделать релиз, пускай и микро? Вообще, новая сборка это и есть релиз, пускай и не стабильный.
Проблема засорения репозитория, как в примере от Artem_zin, связана с тем, что у них не очень удачно выбран алгоритм релизов и именование меток. Вместо:
Последняя цифра это номер сборки в текущем релизе. Релизы сборки можно создавать автоматом на Travis CI. Не обязательно явно делать релиз.
Если релизы создавать автоматом, то может возникнуть проблема, когда захочется сделать полноценный релиз. Придется пошаманить немного.
А теперь о главном. О преимуществах подхода через релизы и о недостатке вашего решения.
Проблема в том, что у вас сборка хранится в репозитории и отличить одну от другой можно только по хешу коммита. После того как пользователь скачает сборку, он ни как не сможет определить какая у него версия. Он ни как не сможет определить на сколько версия, скачанной им сборки, устарела. Если он обнаружит багу в приложении, то он ни как не сможет вам сказать в какой версии произошла ошибка. Всё что он может, это приложить к тикету архив со сборкой и вам уже придется ручками, по хошсумме файла искать сборку в истории коммитов.
Возможно я утрирую и есть какой-то более человеческий способ.
Я же лишь хочу сказать, что возможно для контроля удобнее выкладывать не файл myapp.apk, а myapp_1.1.60.19.apk или myapp_1.1.60.19.zip.
Добавлю свои 5 копеек.
Этот подход называется Data-Driven Design.
С недавних пор, я предпочитаю использовать Domain-Driven Design методологию.
Не буду утверждать, что она лучше. Пусть каждый решает сам как и где ее применять.
Скажу лишь, что я в процессе разработки описываю доменные сущности и бизнес транзакции и лишь потом описываю маппинг на БД и генерю миграцию.
Чтоб не повторяться, вот хорошая статья по сравнению этих методологий.
Вообще, двусторонние связи не очень хорошо, особенно связи ManyToMany. Помнится, об этом говорил Marco Pivetta в своих докладах.
Самая простая проблема с которой мы можем столкнуться, это синхронизация связей объектов. Хотя может её пофиксил. Я с ней сталкивался около 4 лет назад.
Немножко странная схема получилась и чек-лист тоже.
DDD напрямую связана с бизнес логикой. Соответственно слой с DDD должен идти сразу за слоем "Бизнес-требования" и до всяких там KISS и YAGNI.
Вы даже в статье говорите, что IoC связан с фреймворком. И как же оно будет прыгать через 2 уровня?
DIP это не только зависимости ваших компонентов друг от друга, но и зависимости от фраймвора и библиотек. То есть, по идее, правильней сформулировать, что DIP это уровень между вашим компонентом и окружением в роли которого выступает фраймворк и библиотеки.
Уровни "Расширение" и "Связанность" скорей относятся к особенностям реализации компонента. Я бы скорей выделял уровень "Реализация" с вложенными в него уровнями "Расширение" и "Связанность" между частями реализуемого компонента, а сверху над ним уровень DIP, связывания компонента с окружением.
Методологии TDD и BDD я бы вынес вообще куда-то в сторонку. Это относится скорее к методам написания кода. Это примерно тоже самое, что включить в пирамиду уровень "Редактор" (IDE, Vim, Блокнот). Редакторы тоже будут влиять на время и стоимость разработки. Я конечно утрирую, но все равно непонятно почему они находятся над DIP, LSP и прочими.
В чек-лист меня смущает порядок пунктов и путаница со временем/состоянием.
7. Не противоречат ли мои изменения выбранной мной методологии?
То есть вы уже сделали изменение и проверяете сделали ли вы его по TDD?
8. Соотносятся ли мои изменения с лучшими практиками используемого мной фреймворка? Не нарушают ли они общий архитектурный стиль моего кода?
Почему вы решаете вопросы совместимости после выполнения работы?
9. Могут ли использованные мной библиотеки решить поставленную подзадачу?
А если не могут, то что вы писали все это время решая поставленную задачу?
1. Реализуемы ли мои изменения с учетом отведенного мне бюджета, времени и прочих объективных ограничений?
А здесь вы говорите об исполнении задачи в будущем.
Обычно, для того чтобы полноценно оценить трудозатраты на выполнение задачи, нужно, хотя бы примерно, прикинуть бизнес модель, архитектуру компонента и возможности реализации задачи в условиях используемого фреймворка с используемыми библиотеками. То есть нельзя выполнить пункт 1 не выполнив часть последующих пунктов.
Я могу и ошибаться в суждениях. Сколько людей столько и мнений. В любом случае, спасибо за проделанную работу.
Я сейчас например сижу на среднем VDS за 200 р.
Собираюсь с него переползать и рассматриваю варианты.
Видел варианты VDS лучше и дешевле.
Интересуюсь преимуществами конкретно Scaleway.
Написать Bad код в котором будет явная ошибка из-за нарушения LSP.
Написать Good код который решает ошибку в Bad коде.
Good код должен не очень сильно отличаться от Bad кода чтобы новички могли понять как из одного получилось второе. (Майнтейнер тоже туговат и сложные примеры отклоняет)
Код Bad и Good примеров должен быть не слишком сложен чтоб новички могли в нем разобраться. Допустим лимит 100 строк.
Не обязательно реализовывать задание именно на квадратах. Можно взять уток. Мне например, нравится пример с бойлерами, но он более сложный на мой взгляд.
В целом, ваше решение мне нравится. У меня была такая же мысль, но я откланил её потому, что Good код в результате слишком сильно отличаться от Bad.
Возможно мы придем к чему-то похожему.
Похоже лучше вас. Если квадрат является подтипом прямоугольника, то в клиентский код можно безболезнено подставить прямоугольник вместо квадрата. И я привел этот пример в PR, но вы похоже пропустили его.
Фигур много, вам придется под каждую писать свою версию клиентского кода! Вы завязаны на реализацию, а не на абстракцию.
Естественно клиентский код завязан на реализации. Фигур то много, и свойства у них разные и действия над ними можно выполнять разные. Вы все свойства сущностей выносите в абстракцию?
Вы вынесли в абстракцию, метод расчета площади и думаете что всё хорошо. А вот и нет.
Например, прямоугольник можно повернуть, но для квадрата это действие бессмысленно. Прямоугольный треугольник можно зеркалировать, а прямоугольник нельзя, а трапеццию например можно зеркалировать только в одной оси. Задача вписаня окружности в фигуру так, что бы она касалась всех граней возможно для равносторонних и равноугольных фигур таких как квадрат, ромб и равносторонний прямоугольник. Однако это применимо не для всех равносторонних или равноугольных фигур и невозможно для таких фигур как прямоугольник, параллелограмм и тропеция.
Да, часть проблем можно решить воспользовавшись контрактами, но не все можно решить ими.
Если вернуться к вашему решению, то в нем есть несколько проблем.
Вы сделали класс неизменяемым как и я. Но посмотрите на реализацию. Одной лишь неизменяемости достаточно чтоб решить описанную проблему.
Ваша реализация
interface Shape
{
public function area();
}
class Rectangle implements Shape
{
private $width;
private $height;
public function __construct($width, $height)
{
$this->width = $width;
$this->height = $height;
}
public function area()
{
return $this->width * $this->height;
}
}
class Square implements Shape
{
private $length;
public function __construct($length)
{
$this->length = $length;
}
public function area()
{
return pow($this->length, 2);
}
}
function printArea(Shape $shape)
{
echo $shape->area();
}
Упрощенная форма
class Rectangle
{
private $width;
private $height;
public function __construct($width, $height)
{
$this->width = $width;
$this->height = $height;
}
public function area()
{
return $this->width * $this->height;
}
}
class Square extends Rectangle
{
public function __construct($length)
{
parent::__construct($length, $length);
}
}
function printArea(Rectangle $rectangle)
{
echo $rectangle->area();
}
Обратите внимание, что квадрат стал подтипом прямоугольника и код работает правильно. И в таком виде он не нарушает LSP.
function printSquareArea(Square $square)
{
echo $square->area();
}
Но с развитием проекта могут вылезти новые несоответствия и тогда появится необходимость разделить их на отдельные классы. И это уже относится к принципу YAGNI.
Также обратите внимание, что зависимость printArea() от Rectangle, а не от Shape это нарушения DIP, а не LSP.
Ваш пример слишком сильно отличается от оригинала. Новички и майнтейнер не поймут изменений.
Bad
class Rectangle
{
protected $width;
protected $height;
public function __construct()
{
$this->width = 0;
$this->height = 0;
}
public function render($area)
{
// ...
}
public function setWidth($width)
{
$this->width = $width;
}
public function setHeight($height)
{
$this->height = $height;
}
public function getArea()
{
return $this->width * $this->height;
}
}
class Square extends Rectangle
{
public function setWidth($width)
{
$this->width = $this->height = $width;
}
public function setHeight(height)
{
$this->width = $this->height = $height;
}
}
function renderLargeRectangles($rectangles)
{
foreach ($rectangles as $rectangle) {
$rectangle->setWidth(4);
$rectangle->setHeight(5);
$area = $rectangle->getArea(); // BAD: Will return 25 for Square. Should be 20.
$rectangle->render($area);
}
}
$rectangles = [new Rectangle(), new Rectangle(), new Square()];
renderLargeRectangles($rectangles);
Good
interface Shape
{
public function name(): string;
public function area(): float;
}
class Square implements Shape
{
private $length;
public function __construct(float $length)
{
$this->length = $length;
}
public function name(): string
{
return 'Square';
}
public function area(): float
{
return $this->length * $this->length;
}
}
class Rectangle implements Shape
{
private $width;
private $height;
public function __construct(float $width, float $height)
{
$this->width = $width;
$this->height = $height;
}
public function area(): float
{
return $this->width * $this->height;
}
public function name(): string
{
return 'Rectangle';
}
}
function printShape(Shape $shape)
{
echo sprintf('%s has area %.2f.', $shape->name(), $shape->area()).PHP_EOL;
}
$shapes = [
new Rectangle(3.6, 7.1),
new Square(3),
];
foreach ($shapes as $shape) {
printShape($shape);
}
Вы сделали объекты неизменяемым, но частой задачей является создание нового объекта на основе старого.
В задаче нужно посчитать и вывести площадь фигуры с заданными размерами. Ваш код не делает этого. Он делает что-то совершенно другое.
Если вы считаете себя умнее меня, зделайте свой PR. Предложите решение которое будет лучше и проще демонстрировать нарушение LSP и решение которое исправит нарушение принципа.
Сотрясать воздух каждый может, а как на счёт дела? Пока что я практически в одиночку пытаюсь привести проект в божеский вид.
Да. И не только. Например для соблюдения SRP, часть кода можно вынести в обработчик событий, но чтоб не размазывать бизнес логику по проекту, её можно сохранить в сущности.
Например, тот же пример от Fantyk можно решить через обработку событий в сущности заказа.
Я в это случае создаю CQRS команду на отправку SMS уведомления на каждое событие и кладу ее в очередь уникальных команд и по крону разбираю очередь.
Это даёт нам и сохранение уникальности уведомлений и позволяет не тормозить клиент на отправку SMS и контролировать нагрузку на сервер отправки SMS.
Добавлю и свою либу
https://github.com/gpslab/domain-event
бандл для интеграции с Symfony
https://github.com/gpslab/domain-event-bundle
Кроме стандартного агрегирования событий ещё позволяет обрабатывать события в самой сущности, имеет функционал для реализации слушателей, подписчиков, очередей и middleware
Тут я полностью поддерживаю. Под новые проекты стараюсь писать свои админки заточенные под конкретный проект.
Ну если прям для савсем савсем простых CRUD проектов которые не планируют развивается, то да. Сгодится. Правда я с такими проектами не работаю.
Мои проекты либо сразу сложнее, либо гарантированно будут усложняется и тогда внедрение сонаты только время сэкономит.
Спасибо за ссылку.
Не знал про EasyAdminBundle.
Но похоже EasyAdminBundle ещё больше завязан на антипаттерне Anemic model, чем SonataAdminBundle.
И конфигурирование форм в yaml файлах попахивает чем-то.
И не понятно как использовать свои FormType
Автор говорит что
preloadещё не работает в Firefox и судя по данным, Firefox не один отстающее звено.Мне не очень понятно почему вы делаете билд на каждый коммит. Вы же с git работаете и билд у вас на push запускается. Зачем вам пушить каждый коммит в отдельности? Может вы ещё от svn не отвыкли?
С гитом обычно так — поработали немного, сделали несколько коммитов, дошли до какой-то логической точки и запушили.
Разве вы делаете не так?
Если это логическая точка, то почему не сделать релиз, пускай и микро? Вообще, новая сборка это и есть релиз, пускай и не стабильный.
Проблема засорения репозитория, как в примере от Artem_zin, связана с тем, что у них не очень удачно выбран алгоритм релизов и именование меток. Вместо:
Можно былоб писать проще
Последняя цифра это номер сборки в текущем релизе. Релизы сборки можно создавать автоматом на Travis CI. Не обязательно явно делать релиз.
Если релизы создавать автоматом, то может возникнуть проблема, когда захочется сделать полноценный релиз. Придется пошаманить немного.
А теперь о главном. О преимуществах подхода через релизы и о недостатке вашего решения.
Проблема в том, что у вас сборка хранится в репозитории и отличить одну от другой можно только по хешу коммита. После того как пользователь скачает сборку, он ни как не сможет определить какая у него версия. Он ни как не сможет определить на сколько версия, скачанной им сборки, устарела. Если он обнаружит багу в приложении, то он ни как не сможет вам сказать в какой версии произошла ошибка. Всё что он может, это приложить к тикету архив со сборкой и вам уже придется ручками, по хошсумме файла искать сборку в истории коммитов.
Возможно я утрирую и есть какой-то более человеческий способ.
Я же лишь хочу сказать, что возможно для контроля удобнее выкладывать не файл
myapp.apk, аmyapp_1.1.60.19.apkилиmyapp_1.1.60.19.zip.Я не критикую, я только предлагаю.
Добавлю свои 5 копеек.
Этот подход называется Data-Driven Design.
С недавних пор, я предпочитаю использовать Domain-Driven Design методологию.
Не буду утверждать, что она лучше. Пусть каждый решает сам как и где ее применять.
Скажу лишь, что я в процессе разработки описываю доменные сущности и бизнес транзакции и лишь потом описываю маппинг на БД и генерю миграцию.
Чтоб не повторяться, вот хорошая статья по сравнению этих методологий.
Вообще, двусторонние связи не очень хорошо, особенно связи ManyToMany. Помнится, об этом говорил Marco Pivetta в своих докладах.
Самая простая проблема с которой мы можем столкнуться, это синхронизация связей объектов. Хотя может её пофиксил. Я с ней сталкивался около 4 лет назад.
Рекомендую всем к прочтению статью Marco Pivetta о гидрации данных:
https://ocramius.github.io/blog/doctrine-orm-optimization-hydration/
Немножко странная схема получилась и чек-лист тоже.
В чек-лист меня смущает порядок пунктов и путаница со временем/состоянием.
То есть вы уже сделали изменение и проверяете сделали ли вы его по TDD?
Почему вы решаете вопросы совместимости после выполнения работы?
А если не могут, то что вы писали все это время решая поставленную задачу?
А здесь вы говорите об исполнении задачи в будущем.
Обычно, для того чтобы полноценно оценить трудозатраты на выполнение задачи, нужно, хотя бы примерно, прикинуть бизнес модель, архитектуру компонента и возможности реализации задачи в условиях используемого фреймворка с используемыми библиотеками. То есть нельзя выполнить пункт 1 не выполнив часть последующих пунктов.
Я могу и ошибаться в суждениях. Сколько людей столько и мнений. В любом случае, спасибо за проделанную работу.
Да. Что-то я оплашал. Забыл какие там характеристики были.
Даже самый вкусный тариф дешевле выходит.
Есть значительно дешевле. Вопрос только в характеристиках.
https://poiskvps.ru/index.php?search_price_max=150
Я сейчас например сижу на среднем VDS за 200 р.
Собираюсь с него переползать и рассматриваю варианты.
Видел варианты VDS лучше и дешевле.
Интересуюсь преимуществами конкретно Scaleway.
Можно вопрос, а чём это отличается от обычной vds за 150 р. с теми же характеристики?
Это на ваше усмотрение. Можете площадь посчитать.
Давайте попробуем сформулировать задание.
Задача:
Не обязательно реализовывать задание именно на квадратах. Можно взять уток. Мне например, нравится пример с бойлерами, но он более сложный на мой взгляд.
В целом, ваше решение мне нравится. У меня была такая же мысль, но я откланил её потому, что Good код в результате слишком сильно отличаться от Bad.
Возможно мы придем к чему-то похожему.
Это уже получается шаблон Strategy.
Тоже неплохое решение хотя слишком сложное для примера.
Нет. Я его не проигнорировал, а как раз таки наоборот, я его использовал.
Как по вашему функция
ArrangeBirdInPattern()будет определять может ли птица летать?Да, это плохое решение, о чем говорят все кому не лень. И для решения этой проблемы вводят абстракцию.
В примере с птицами мы получаем ту же проблему что есть сейчас:
Похоже лучше вас. Если квадрат является подтипом прямоугольника, то в клиентский код можно безболезнено подставить прямоугольник вместо квадрата. И я привел этот пример в PR, но вы похоже пропустили его.
Естественно клиентский код завязан на реализации. Фигур то много, и свойства у них разные и действия над ними можно выполнять разные. Вы все свойства сущностей выносите в абстракцию?
Вы вынесли в абстракцию, метод расчета площади и думаете что всё хорошо. А вот и нет.
Например, прямоугольник можно повернуть, но для квадрата это действие бессмысленно. Прямоугольный треугольник можно зеркалировать, а прямоугольник нельзя, а трапеццию например можно зеркалировать только в одной оси. Задача вписаня окружности в фигуру так, что бы она касалась всех граней возможно для равносторонних и равноугольных фигур таких как квадрат, ромб и равносторонний прямоугольник. Однако это применимо не для всех равносторонних или равноугольных фигур и невозможно для таких фигур как прямоугольник, параллелограмм и тропеция.
Да, часть проблем можно решить воспользовавшись контрактами, но не все можно решить ими.
Если вернуться к вашему решению, то в нем есть несколько проблем.
Обратите внимание, что квадрат стал подтипом прямоугольника и код работает правильно. И в таком виде он не нарушает LSP.
Но с развитием проекта могут вылезти новые несоответствия и тогда появится необходимость разделить их на отдельные классы. И это уже относится к принципу YAGNI.
Также обратите внимание, что зависимость
printArea()отRectangle, а не отShapeэто нарушения DIP, а не LSP.В задаче нужно посчитать и вывести площадь фигуры с заданными размерами. Ваш код не делает этого. Он делает что-то совершенно другое.
Если вы считаете себя умнее меня, зделайте свой PR. Предложите решение которое будет лучше и проще демонстрировать нарушение LSP и решение которое исправит нарушение принципа.
Сотрясать воздух каждый может, а как на счёт дела? Пока что я практически в одиночку пытаюсь привести проект в божеский вид.