Перевод статьи Vitalij Mik Clean Code Architecture and Test Driven Development in PHP

Понятие «архитектура чистого кода» (Clean Code Architecture) ввел Роберт Мартин в блоге 8light. Смысл понятия в том, чтобы создавать архитектуру, которая не зависела бы от внешнего воздействия. Ваша бизнес-логика не должна быть объединена с фреймворком, базой данных или самим вебом. Подобная независимость даёт ряд преимуществ. К примеру, при разработке вы сможете откладывать какие-то технические решения, например выбор фреймворка, движка/поставщика БД. Также вы сможете легко переключаться между разными реализациями и сравнивать их. Но самое важное преимущество такого подхода — ваши тесты будут выполняться быстрее.

Просто подумайте об этом. Вы действительно хотите пройти роутинг, подгрузить абстрактный уровень базы данных или какое-нибудь ORM-колдовство? Или просто выполнить какой-то код, чтобы проверить (assert) те или иные результаты?

Я начал изучать такую архитектуру и практиковаться в ее создании из-за моего любимого фреймворка Kohana. Его основной разработчик однажды перестал поддерживать код, поэтому мои проекты не обновлялись и не получали патчи системы безопасности. А это означало, что мне понадобилось либо довериться версии, которая разрабатывается сообществом, либо переходить на новый фреймворк и переписывать проекты целиком.

Да, я мог бы выбрать другой фреймворк. Возможно, Symfony 1 или Zend 1. Но что бы я ни выбрал, с тех пор изменился бы и этот фреймворк. Они постоянно меняются и развиваются. Composer облегчает не только установку и замену пакетов, но и их исключение (в нём есть даже возможность помечать пакеты как исключённые), так что ошибиться довольно просто.

В этой публикации я покажу вам, как внедрить в PHP архитектуру чистого кода, которая позволит контролировать логику, не завися от внешних решений, но имея возможность их использовать. Мы изучим вопрос на примере создания простенького приложения гостевой книги.



На этой иллюстрации изображены разные слои приложения. Внутренние ничего не знают о внешних, при этом все они взаимодействуют друг с другом через интерфейсы.

Самое интересное — в правом нижнем углу: поток управления. Схема объясняет, как фреймворк взаимодействует с бизнес-логикой. Контроллер передаёт данные на порт ввода, информацию с которого обрабатывает интерактор, а результат передаётся на порт вывода, содержащий данные для презентера.

Начнём со слоя сценариев использования, поскольку здесь находится наша специфическая логика приложения. Внешние слои, включая контроллер, относятся к фреймворку.

Обратите внимание, что все описываемые далее этапы можно взять из репозитория. Они аккуратно разделены по шагам с помощью Git-тэгов. Просто скачайте нужный шаг, если хотите посмотреть, как это работает.

Первый тест


Обычно мы начинаем работу с пользовательского интерфейса. Что человек ожидает увидеть в гостевой книге? Наверное, форму ввода, записи других посетителей, возможно, навигационную панель с поиском по страницам записей. Если книга пуста, может отображаться сообщение «Записей нет».

В первом тесте нам нужно проверить (assert) пустой список записей:

<?php
require_once __DIR__ . '/../../vendor/autoload.php';
class ListEntriesTest extends PHPUnit_Framework_TestCase
{
    public function testEntriesNotExists()
    {
        $request = new FakeViewEntriesRequest();
        $response = new FakeViewEntriesResponse();
        $useCase = new ViewEntriesUseCase();
        $useCase->process($request, $response);
        $this->assertEmpty($response->entries);
    }
}

Здесь я использовал немного другую нотацию по сравнению с нотацией Дяди Боба. Интеракторы — это useCase, порты ввода — request, порты вывода — response. Все useCase содержат метод, в котором есть type hint для конкретного интерфейса request и response.

Если следовать принципам разработки через тестирование (test-driven development, TDD) — красный цикл, зелёный цикл, цикл рефакторинга, — тест не будет пройден, поскольку классы не существуют. Для прохождения теста достаточно создать файлы классов, методы и свойства. Поскольку классы пусты, нам пока рано приступать к циклу рефакторинга.

Теперь нужно проверить отображение записей:

<?php
require_once __DIR__ . '/../../vendor/autoload.php';
use BlackScorp\GuestBook\Fake\Request\FakeViewEntriesRequest;
use BlackScorp\GuestBook\Fake\Response\FakeViewEntriesResponse;
use BlackScorp\GuestBook\UseCase\ViewEntriesUseCase;
class ListEntriesTest extends PHPUnit_Framework_TestCase
{
    public function testEntriesNotExists()
    {
        $request = new FakeViewEntriesRequest();
        $response = new FakeViewEntriesResponse();
        $useCase = new ViewEntriesUseCase();
        $useCase->process($request, $response);
        $this->assertEmpty($response->entries);
    }
    public function testCanSeeEntries()
    {
        $request = new FakeViewEntriesRequest();
        $response = new FakeViewEntriesResponse();
        $useCase = new ViewEntriesUseCase();
        $useCase->process($request, $response);
        $this->assertNotEmpty($response->entries);
    }
}

Тест не пройден, мы находимся в красной части цикла TDD. Для прохождения нужно добавить логику в наши useCase.

Наброски логики для useCase


Но сначала воспользуемся type hint’ами в качестве параметров и создадим интерфейсы:

<?php
namespace BlackScorp\GuestBook\UseCase;

use BlackScorp\GuestBook\Request\ViewEntriesRequest;
use BlackScorp\GuestBook\Response\ViewEntriesResponse;

class ViewEntriesUseCase
{
    public function process(ViewEntriesRequest $request, ViewEntriesResponse $response){

    }
}

Художники работают так же. Вместо рисования всей картины от начала и до конца они первым делом создают базовые формы и линии, чтобы представлять основу будущего изображения. А потом добавляют к формам всевозможные детали. Но вначале появляется эскиз.

Мы же вместо форм и линий используем, например, репозитории и фабрики. Репозиторий — это абстрактный уровень для получения данных из хранилища. Хранилищем может быть база данных, файл, внешний API и даже память.

Для просмотра записей в гостевой книге нам нужно найти эти записи в репозитории, конвертировать в виды (view) и вернуть.

<?php
namespace BlackScorp\GuestBook\UseCase;

use BlackScorp\GuestBook\Request\ViewEntriesRequest;
use BlackScorp\GuestBook\Response\ViewEntriesResponse;

class ViewEntriesUseCase
{
    public function process(ViewEntriesRequest $request, ViewEntriesResponse $response){
	$entries = $this->entryRepository->findAllPaginated($request->getOffset(), $request->getLimit());

        if(!$entries){
            return;
        }

        foreach($entries as $entry){
            $entryView = $this->entryViewFactory->create($entry);
            $response->addEntry($entryView);
        }
    }
}

Наверное, вы спросите, для чего понадобилось конвертировать сущность Entry в вид? Дело в том, что сущность не должна покидать пределы уровня useCase. Мы можем найти её только с помощью репозитория, при необходимости изменить/скопировать и положить обратно в репозиторий. Когда мы начнём перемещать сущность во внешний слой, то лучше добавить дополнительные методы для улучшения взаимодействия. Однако в сущности должна присутствовать только основная бизнес-логика.

Поскольку мы пока не знаем, какой формат нужно придать сущности, пропустим этот шаг.

Теперь отвечу на ваш возможный вопрос о фабриках. Создав новый экземпляр в цикле:

$entryView = new EntryView($entry);
$response->addEntry($entryView);

мы нарушим принцип инверсии зависимостей. И если потом в той же логике useCase нам понадобится ещё один объект вида (view object), то придётся переписывать код. А с помощью фабрики можно легко внедрять разные виды с разной логикой форматирования, при этом будет использоваться один и тот же useCase.

Реализация внешних зависимостей


Нам уже известны зависимости useCase: $entryViewFactory и $entryRepository. Также известны и методы этих зависимостей. EntryViewFactory создаёт метод, который получает EntryEntity, а у EntryRepository есть метод findAll(), возвращающий массив EntryEntities. Теперь можно создать интерфейсы для методов и применить их к useCase.

EntryRepository выглядит так:

<?php
namespace BlackScorp\GuestBook\Repository;

interface EntryRepository {
    public function findAllPaginated($offset,$limit);
}

Тогда useCase:

<?php
namespace BlackScorp\GuestBook\UseCase;

use BlackScorp\GuestBook\Repository\EntryRepository;
use BlackScorp\GuestBook\Request\ViewEntriesRequest;
use BlackScorp\GuestBook\Response\ViewEntriesResponse;
use BlackScorp\GuestBook\ViewFactory\EntryViewFactory;

class ViewEntriesUseCase
{
    /**
     * @var EntryRepository
     */
    private $entryRepository;
    /**
     * @var EntryViewFactory
     */
    private $entryViewFactory;

    /**
     * ViewEntriesUseCase constructor.
     * @param EntryRepository $entryRepository
     * @param EntryViewFactory $entryViewFactory
     */
    public function __construct(EntryRepository $entryRepository, EntryViewFactory $entryViewFactory)
    {
        $this->entryRepository = $entryRepository;
        $this->entryViewFactory = $entryViewFactory;
    }


    public function process(ViewEntriesRequest $request, ViewEntriesResponse $response)
    {
        $entries = $this->entryRepository->findAllPaginated($request->getOffset(), $request->getLimit());
        if (!$entries) {
            return;
        }

        foreach ($entries as $entry) {
            $entryView = $this->entryViewFactory->create($entry);
            $response->addEntry($entryView);
        }
    }
}

Как видите, тесты всё ещё не проходятся, так как нет реализации зависимости. Создадим несколько фальшивых объектов:

<?php
require_once __DIR__ . '/../../vendor/autoload.php';
use BlackScorp\GuestBook\Fake\Request\FakeViewEntriesRequest;
use BlackScorp\GuestBook\Fake\Response\FakeViewEntriesResponse;
use BlackScorp\GuestBook\UseCase\ViewEntriesUseCase;
use BlackScorp\GuestBook\Entity\EntryEntity;
use BlackScorp\GuestBook\Fake\Repository\FakeEntryRepository;
use BlackScorp\GuestBook\Fake\ViewFactory\FakeEntryViewFactory;
class ListEntriesTest extends PHPUnit_Framework_TestCase
{
    public function testEntriesNotExists()
    {
        $entryRepository = new FakeEntryRepository();
        $entryViewFactory = new FakeEntryViewFactory();
        $request = new FakeViewEntriesRequest();
        $response = new FakeViewEntriesResponse();
        $useCase = new ViewEntriesUseCase($entryRepository, $entryViewFactory);
        $useCase->process($request, $response);
        $this->assertEmpty($response->entries);
    }
    public function testCanSeeEntries()
    {
	$entities = [];
        $entities[] = new EntryEntity();
        $entryRepository = new FakeEntryRepository($entities);
        $entryViewFactory = new FakeEntryViewFactory();
        $request = new FakeViewEntriesRequest();
        $response = new FakeViewEntriesResponse();
        $useCase = new ViewEntriesUseCase($entryRepository, $entryViewFactory);
        $useCase->process($request, $response);
        $this->assertNotEmpty($response->entries);
    }
}

Поскольку мы уже создали интерфейсы для репозитория и фабрики видов, значит, можем внедрить их в фальшивые классы, а заодно реализовать интерфейсы для request/response.

Теперь репозиторий выглядит так:

<?php
namespace BlackScorp\GuestBook\Fake\Repository;

use BlackScorp\GuestBook\Repository\EntryRepository;

class FakeEntryRepository implements EntryRepository
{
    private $entries = [];

    public function __construct(array $entries = [])
    {
        $this->entries = $entries;
    }

    public function findAllPaginated($offset, $limit)
    {
        return array_splice($this->entries, $offset, $limit);
    }
}

А фабрика видов — так:

<?php
namespace BlackScorp\GuestBook\Fake\ViewFactory;

use BlackScorp\GuestBook\Entity\EntryEntity;
use BlackScorp\GuestBook\Fake\View\FakeEntryView;
use BlackScorp\GuestBook\View\EntryView;
use BlackScorp\GuestBook\ViewFactory\EntryViewFactory;

class FakeEntryViewFactory implements EntryViewFactory
{
    /**
     * @param EntryEntity $entity
     * @return EntryView
     */
    public function create(EntryEntity $entity)
    {

        $view = new FakeEntryView();
        $view->author = $entity->getAuthor();
        $view->text = $entity->getText();
        return $view;
    }
}

Вы спросите, почему бы просто не использовать mocking-фреймворки для создания зависимостей? Тому есть две причины:

  1. С помощью редактора можно легко создать необходимые классы, поэтому фреймворки не нужны.
  2. Когда мы начинаем создавать реализацию для фреймворка, то можем использовать эти фальшивые классы в DI-контейнере и играться с шаблонами без необходимости настоящей реализации.

Теперь тесты пройдены, можем заняться рефакторингом. По сути, в классе useCase рефакторить нечего, разве только в тестовом классе.

Рефакторинг теста


Исполняться будет так же, просто с другой настройкой (setup) и проверкой. Можем перенести инициализацию фальшивых классов и обработку useCase в частную функцию processUseCase.

Тестовый класс выглядит так:

<?php
require_once __DIR__ . '/../../vendor/autoload.php';

use BlackScorp\GuestBook\Entity\EntryEntity;
use BlackScorp\GuestBook\Fake\Repository\FakeEntryRepository;
use BlackScorp\GuestBook\Fake\ViewFactory\FakeEntryViewFactory;
use BlackScorp\GuestBook\Fake\Request\FakeViewEntriesRequest;
use BlackScorp\GuestBook\Fake\Response\FakeViewEntriesResponse;
use BlackScorp\GuestBook\UseCase\ViewEntriesUseCase;

class ListEntriesTest extends PHPUnit_Framework_TestCase
{

    public function testCanSeeEntries()
    {
 	$entries = [
            new EntryEntity('testAuthor','test text')
        ];
        $response = $this->processUseCase($entries);
        $this->assertNotEmpty($response->entries);
    }

    public function testEntriesNotExists()
    {
        $entities = [];
        $response = $this->processUseCase($entities);
        $this->assertEmpty($response->entries);
    }

    /**
     * @param $entities
     * @return FakeViewEntriesResponse
     */
    private function processUseCase($entities)
    {
        $entryRepository = new FakeEntryRepository($entities);
        $entryViewFactory = new FakeEntryViewFactory();
        $request = new FakeViewEntriesRequest();
        $response = new FakeViewEntriesResponse();
        $useCase = new ViewEntriesUseCase($entryRepository, $entryViewFactory);
        $useCase->process($request, $response);
        return $response;
    }
}

Независимость


Теперь мы можем, например, легко создать новые тесты с неверными сущностями, переместить репозиторий и фабрику в метод setup и прогнать тесты с настоящими реализациями.

Также мы можем внедрить в DI-контейнер готовый к использованию useCase и использовать его внутри фреймворка. При этом логика не будет зависеть от фреймворка.

Кроме того, ничто не мешает создать другую реализацию репозитория, которая будет общаться с внешним API, например, и передавать его в useCase. Логика будет независима от базы данных.

При желании можно создать CLI-объекты request/response и передавать их тому же useCase, используемому внутри контроллера. В этом случае логика не будет зависеть от платформы.

Даже можно исполнять по очереди разные useCase, каждый из которых изменяет объект response.

class MainController extends BaseController
{
    public function indexAction(Request $httpRequest)
    {
        $indexActionRequest = new IndexActionRequest($httpRequest);
        $indexActionResponse = new IndexActionResponse();
        $this->getContainer('ViewNavigation')->process($indexActionRequest, $indexActionResponse);
        $this->getContainer('ViewNewsEntries')->process($indexActionRequest, $indexActionResponse);
        $this->getContainer('ViewUserAvatar')->process($indexActionRequest, $indexActionResponse);
        $this->render($indexActionResponse);

    }
}

Разбивка на страницы


Добавим в нашу гостевую книгу разбивку на страницы. Тест может выглядеть так:

   public function testCanSeeFiveEntries(){
        $entities = [];
        for($i = 0;$i<10;$i++){
             $entities[] = new EntryEntity('Author '.$i,'Text '.$i);
        }

        $response = $this->processUseCase($entities);
        $this->assertNotEmpty($response->entries);
        $this->assertSame(5,count($response->entries));
    }

Он не будет пройден, так что нужно модифицировать метод process в useCase, а заодно переименовать метод findAll в findAllPaginated.

public function process(ViewEntriesRequest $request, ViewEntriesResponse $response){
        $entries = $this->entryRepository->findAllPaginated($request->getOffset(), $request->getLimit());
//....
    }

Теперь можно применить в интерфейсе и фальшивом репозитории новые параметры, а также добавить новые методы в интерфейс request.

В репозитории немного изменится метод findAllPaginated:

    public function findAllPaginated($offset, $limit)
    {
        return array_splice($this->entries, $offset, $limit);
    }

Нужно перенести в тесты объект request. Также для конструктора объекта request понадобится параметр ограничения (limit parameter). Таким образом, мы заставим setup создать ограничение вместе с новым экземпляром.

  public function testCanSeeFiveEntries(){
        $entities = [];
        for($i = 0;$i<10;$i++){
            $entities[] = new EntryEntity();
        }
        $request = new FakeViewEntriesRequest(5);
        $response = $this->processUseCase($request, $entities);
        $this->assertNotEmpty($response->entries);
        $this->assertSame(5,count($response->entries));
    }

Тест пройден. Но нужно ещё протестировать возможность просмотра следующих пяти записей. Для этого придётся добавить в объект request метод setPage.

<?php
namespace BlackScorp\GuestBook\Fake\Request;
use BlackScorp\GuestBook\Request\ViewEntriesRequest;
class FakeViewEntriesRequest implements ViewEntriesRequest{
    private $offset = 0;
    private $limit = 0;

    /**
     * FakeViewEntriesRequest constructor.
     * @param int $limit
     */
    public function __construct($limit)
    {
        $this->limit = $limit;
    }

    public function setPage($page = 1){
        $this->offset = ($page-1) * $this->limit;
    }
    public function getOffset()
    {
        return $this->offset;
    }

    public function getLimit()
    {
        return $this->limit;
    }

}

С помощью этого метода мы можем протестировать отображение следующих пяти записей:

   public function testCanSeeFiveEntriesOnSecondPage(){
        $entities = [];
        $expectedEntries = [];
        $entryViewFactory = new FakeEntryViewFactory();
        for($i = 0;$i<10;$i++){
            $entryEntity = new EntryEntity();
            if($i >= 5){
                $expectedEntries[]=$entryViewFactory->create($entryEntity);
            }
            $entities[] =$entryEntity;
        }
        $request = new FakeViewEntriesRequest(5);
        $request->setPage(2);
        $response = $this->processUseCase($request,$entities);
        $this->assertNotEmpty($response->entries);
        $this->assertSame(5,count($response->entries));
        $this->assertEquals($expectedEntries,$response->entries);
    }

Пройдено, можем рефакторить. Перенесём FakeEntryViewFactory в метод setup, и готово. Последний тестовый класс выглядит так:

<?php
require_once __DIR__ . '/../../vendor/autoload.php';

use BlackScorp\GuestBook\Entity\EntryEntity;
use BlackScorp\GuestBook\Fake\Repository\FakeEntryRepository;
use BlackScorp\GuestBook\Fake\Request\FakeViewEntriesRequest;
use BlackScorp\GuestBook\Fake\Response\FakeViewEntriesResponse;
use BlackScorp\GuestBook\Fake\ViewFactory\FakeEntryViewFactory;
use BlackScorp\GuestBook\UseCase\ViewEntriesUseCase;

class ListEntriesTest extends PHPUnit_Framework_TestCase
{
    public function testEntriesNotExists()
    {
        $entries = [];
        $request = new FakeViewEntriesRequest(5);
        $response = $this->processUseCase($request, $entries);
        $this->assertEmpty($response->entries);
    }

    public function testCanSeeEntries()
    {
        $entries = [
            new EntryEntity('testAuthor', 'test text')
        ];
        $request = new FakeViewEntriesRequest(5);
        $response = $this->processUseCase($request, $entries);
        $this->assertNotEmpty($response->entries);
    }

    public function testCanSeeFiveEntries()
    {
        $entities = [];
        for ($i = 0; $i < 10; $i++) {
            $entities[] = new EntryEntity('Author ' . $i, 'Text ' . $i);
        }
        $request = new FakeViewEntriesRequest(5);
        $response = $this->processUseCase($request, $entities);
        $this->assertNotEmpty($response->entries);
        $this->assertSame(5, count($response->entries));
    }

    public function testCanSeeFiveEntriesOnSecondPage()
    {
        $entities = [];
        $expectedEntries = [];
        $entryViewFactory = new FakeEntryViewFactory();
        for ($i = 0; $i < 10; $i++) {
            $entryEntity = new EntryEntity('Author ' . $i, 'Text ' . $i);
            if ($i >= 5) {
                $expectedEntries[] = $entryViewFactory->create($entryEntity);
            }
            $entities[] = $entryEntity;
        }
        $request = new FakeViewEntriesRequest(5);
        $request->setPage(2);
        $response = $this->processUseCase($request, $entities);
        $this->assertNotEmpty($response->entries);
        $this->assertSame(5, count($response->entries));
        $this->assertEquals($expectedEntries, $response->entries);
    }

    /**
     * @param $request
     * @param $entries
     * @return FakeViewEntriesResponse
     */
    private function processUseCase($request, $entries)
    {
        $repository = new FakeEntryRepository($entries);
        $factory = new FakeEntryViewFactory();
        $response = new FakeViewEntriesResponse();
        $useCase = new ViewEntriesUseCase($repository, $factory);
        $useCase->process($request, $response);
        return $response;
    }
}

Завершение


Мы рассмотрели, как тесты привели нас к useCase, тот привёл к интерфейсам, а они привели к фальшивым реализациям. Повторюсь, что исходный код для этой публикации можно скачать с Github. Обратите внимание на тэги, обозначающие разные стадии.

Этот туториал демонстрирует, как для любого нового проекта можно легко применять разработку через тестирование и архитектуру чистого кода. Главное преимущество такого подхода — полная независимость логики. Такой подход также позволяет использовать сторонние библиотеки.

Комментарии (38)


  1. Delphinum
    20.02.2016 19:25

    Никогда еще не сталкивался с проектом, в котором сначала бы реализовывалась бизнес-логика, а затем подбирался фрейворк.

    А не проще использовать старые добрые контролеры с DI конструктором вместо предложенных в статье UseCase? По мне так одно и то же получается в плане тестирования.


    1. Fesor
      23.02.2016 23:37

      Никогда еще не сталкивался с проектом, в котором сначала бы реализовывалась бизнес-логика, а затем подбирался фрейворк.

      Ну видимо у вас это еще впереди. Оно конечно по дефолту рассматривается конкретный фреймворк, но в зависимости от нефункциональных требований все может поменяться.

      А не проще использовать старые добрые контролеры с DI конструктором вместо предложенных в статье UseCase?

      Тогда не будет изоляции от HTTP. Допустим вы сегодня делаете проект с микросервисами. И решили что бы не тормозить пока связать все обычным REST/HTTP. А завтра у вас уже требования выше, и HTTP заменяется на какой-нибудь ZeroMQ + MessagePack или еще чего такое.

      Суть этого подхода — изолировать код, который может в будущем поменяться. Ну то есть если вы на 100% не уверены в том, нормальное это решение или нет, то проще изолировать это решение, что бы потом можно было "передумать". В этом суть "гибких" архитектур, всегда можно все поменять относительно безболезненно.

      Да и скажу так, оверхэда от введения UseCase (обычно все же юзают Command) ооочень мало. А введение вещей вроде CQRS дает еще целую кучу возможностей и пространства для моневра для изоляции будущих изменений.

      Конечно же когда мы выбираем тот или иной подход, и в особенности в от эти "сложности" с изоляцией это должно быть не просто так, а потому что ситуация того требует. Для большинства проектов это не нужно. У доброй половины проектов можно спокойно логику по контроллерам размазывать и все будет хорошо. Но вот если вам попадется серьезный проект, где есть такие вот потребности в духе "сегодня так а завтра подругому" — то не имея опыта с подобными концепциями вы рисуете быстро проиграть.


      1. Delphinum
        25.02.2016 08:13

        Оно конечно по дефолту рассматривается конкретный фреймворк, но в зависимости от нефункциональных требований все может поменяться.

        Как уже упомяналось в комментах на хабре, и PHP может завтра внезапно самоудалиться с тырнета, и компьютеры могут исчезнуть с планеты, может тогда сразу на шестеренках реализовывать?

        Но вот если вам попадется серьезный проект, где есть такие вот потребности в духе «сегодня так а завтра подругому»

        Ну что вы все термином "серьезности" оперируете? Что же серьезного в проекте, у которого завтра может все поменять? Гибкость это хорошо, но я к тому, что гибкость, как и закостенелось, может быть очень вредной, еси не уметь вовремя остановится. Предложенное в статье решение мне кажется примером излишней гибкости. Еще немного, и процесс создания нового сервиса в системе будет аналогичен написанию всей этой системы, а ведь есть еще и тесты, которые надо поддержать, и документация, и новички на проекте, которые будут месяцами вдуплять в эту необычную архитектуру, а это для "серьезного проетка" куда важнее, чем возможность завтра поменять всю архитектуру приложения путем замены двух строчек кода (почему бы и не поутрировать?).


        1. Fesor
          25.02.2016 11:28

          Как уже упомяналось в комментах на хабре, и PHP может завтра внезапно самоудалиться с тырнета, и компьютеры могут исчезнуть с планеты, может тогда сразу на шестеренках реализовывать?

          Повторюсь — помимо вашего популярного фреймворка, есть не не такие популярные библиотеки которые сейчас нужны а завтра их могут сломать, на них могут забить и т.д. Ну то есть надо оценивать вероятность. Да и потом, сегодня вы юзаете библиотеку A из-за какой-то фичи, а завтра вам может понадобиться библиотека B потому что там эта фича реализована безопаснее. Ну и т.д.

          Вся суть в том, что бы то что может поменяться было отделено от всего остального. Соответственно у вас система будет разделена на блоки с разной вероятностью внесения изменений. Бизнес логика как правило тоже меняется, но вот изменения в инфраструктуре не должны ее трогать.

          Что же серьезного в проекте, у которого завтра может все поменять?

          Да собственно все. Сегодня пользователи хотят фичу А, завтра фичу Б, через месяц окажется что фича Б им таки не нужна была, и надо ее выпиливать… типичный стартап.


          1. Delphinum
            25.02.2016 14:18

            Повторюсь — помимо вашего популярного фреймворка, есть не не такие популярные библиотеки которые сейчас нужны а завтра их могут сломать, на них могут забить и т.д.

            Фреймворк и библиотека это сильно разные вещи. Если меняется библиотека, достаточно сделать гибкой часть системы, которая использует эту библиотеку, но если менять фреймворк, нужно делать гибкой саму архитектуру приложения. Фреймворк как ни как задает будущий скелет системы, а библиотека всего лишь предоставляет новые инструменты. Я говорю о фреймворках и всей структуре приложения, а вас уносит на уровень пакетов.

            Вся суть в том, что бы то что может поменяться было отделено от всего остального

            Думаю это всем прекрасно известно, но я говорю о другом. Гибкость столь же опасна как закостенелость, потому обращаться с ней нужно так же осторожно. Меня даже немного удивляет, почему есть шаблоны для повышения гибкости, но нет шаблонов для повышения закостенелости приложения? Довольно часто встречаюсь с убийством времени на "гибконизацию" части системы, которая может и будет меняться, но очень не скоро. Другими словами, если вы собираетесь срубить дерево, не надо ножом выпиливать из его ствола щепки параллельно думая, чем же лучше это сделать, двуручно- или бензопилой. Лучше отложить это занятие на денек другой, определиться с архитектурой будущего приложения и уже потом начинать.

            Да собственно все. Сегодня пользователи хотят фичу А, завтра фичу Б, через месяц окажется что фича Б им таки не нужна была, и надо ее выпиливать… типичный стартап

            А при чем здесь фреймворк и библиотека? Если вам в стартапе не хватает фичи Б, берете и реализовываете ее, если для этого не хватает инструмента, добавляете зависимость в composer. При чем тут реализация бизнес логики до определения архитектуры и фреймворка?


    1. flancer
      25.02.2016 12:40
      +1

      У меня сейчас такой. Пишу код, который должен функционировать или под Magento 1, или под Magento 2. В первом случае в качестве DI я использую Zend'овский (Magento 1 обошлась без DI), во втором — собственно Magento'овский. Ну и еще по мелочи.


      1. Fesor
        25.02.2016 13:01

        Хороший пример, к слову.

        У меня чуть другая ситуация. У меня есть проекты, которые написаны на Angular1 из расчета, что через месяца 3 мы перейдем на Angular2.


        1. Delphinum
          25.02.2016 14:29

          А есть проект на Angular и через пару месяцев вы переведете его на BackboneJS? У меня есть. Только вот какой гибкой архитектуру в этом случае не делай, все равно все решения, которые были написаны под Angular придется переписывать под BackboneJS, потому что меняется архитектура. В вашем случае изменения архитектуры малы.

          Можно сказать так:
          Если вы не можете выбрать между двумя версиями одного фреймворка, это повод задуматься об увеличении гибкости бизнес-логики.
          Если вы не можете выбрать между двумя разными фреймворками (читать — архитектурами), это повод отложить разработку, провести пару заседаний, выбрать архитектуру, а уже затем начинать работу.
          Если вы не можете выбрать между двумя разными фреймворками потому что оба популярные и вам хотелось бы их попробовать, то вы не разрабатываете "серьезный проект", а в этом случае не стоит усложнять его излишней гибкостью, иначе он никогда не родится.


      1. Delphinum
        25.02.2016 14:21

        То есть вы не можете выбрать между двумя фреймворками, потому что в одном есть некий пакет, а в другом его нет? Тогда вопрос — а зачем вообще рассматривается Magento 1? Второй вопрос — а разве DI у Zend и DI у Magento2 как то изменят архитектуру приложения? Другими словами, используя первый DI вы будете перечислять зависимости в конструкторе, а используя второй DI в xml файле или что?

        Я считаю фреймворк архитектурой проекта. Если встает выбор между фреймворками из за их библиотечной составляющей, то вы выбираете не фреймворки, а инструменты. Почему бы вам не выбрать архитектуру, а потом собрать ее из пакетов разных фреймворков, если уж возникают такие проблемы?


        1. flancer
          26.02.2016 08:43
          -1

          Не я, клиент :) У Magento 1 гораздо большее кол-во расширений. Для него это важно.

          разве DI у Zend и DI у Magento2 как то изменят архитектуру приложения?

          Моего расширения — нет. Но если бы я не ввел DI Zend'а, то — да.

          Почему бы вам не выбрать архитектуру, а потом собрать ее из пакетов разных фреймворков, если уж возникают такие проблемы?

          Я делаю расширения к уже существующим приложениям и встраиваюсь в них. Они и диктуют "правила игры". Может завтра клиент решит в OroCRM или WordPress встроиться — я ж не знаю :)


          1. Delphinum
            26.02.2016 20:15

            У Magento 1 гораздо большее кол-во расширений

            Мы об архитектуре говорим, не о пакетах и библиотеках.

            Но если бы я не ввел DI Zend'а, то — да

            То есть от выбора той или иной DI архитектура не меняется, меняется она если вообще не использовать DI? Как же тогда автор предлагает писать бизнес-логику, если заранее не известно будет в вашем фреймворке использоваться DI или не будет? Представляете какой космический корабль городить придется чтобы это обойти )

            Они и диктуют «правила игры»

            Ну так значит архитектура уже давно определена. Ваше дело выбрать пакет и с его помощью решить 1 задачу, а не изменять архитектуру. Тут тем более не нужно заботится о выборе фреймворка. Я надеюсь, вы не заменяете фреймворк в приложении, только потому что в текущем фреймворке нет 1 нужного вам пакета?


            1. Fesor
              26.02.2016 23:48

              если заранее не известно будет в вашем фреймворке использоваться DI или не будет?

              шел 2016-ый год, есть composer, какая разница что там есть в фреймворке, если это чуть что можно добавить?

              Вообще вся соль в dependency inversion а не в dependency injection. Помимо DI есть еще и другие варианты, хоть фабрики юзать.

              Ну так значит архитектура уже давно определена.

              Типичное заблуждение что фреймворк диктует архитектуру приложения. Как правило все существующие фреймворки диктуют только архитектуру слоя презентационной логики и немножко как отделяется приложение и UI. Так же они предоставляют способы более удобного проектирования приложения (ORM-ки, тот же DI или IoC для удобной организации сервисного слоя). Инфраструктура.

              Все эти решения важные. А хорошая архитектура должна позволить нам в случае ошибки (а предугадать все нереально) быстро исправить ситуацию.


              1. Delphinum
                27.02.2016 13:59

                Вообще вся соль в dependency inversion а не в dependency injection. Помимо DI есть еще и другие варианты, хоть фабрики юзать

                Тем более к чему этот пример с Magento и Zend?

                Типичное заблуждение что фреймворк диктует архитектуру приложения

                Давайте тогда порассуждаем об архитектуре приложения. Возьмем, к примеру, скелетон для ZF2. Без него ZF2 не более чем библиотека пакетов, а вот собранный с их помощью скелетон определил будущую архитектуру проекта, а именно принцип разделения бизнес-логики на модули, способы взаимодействия этих модулей и обязательные компоненты (класс Module). Теперь расскажите мне, как вы будете писать бизнес-логику без этих условностей?


                1. Fesor
                  27.02.2016 15:22

                  Я не очень знаком с ZF2 но просмотрев скелетон — он не задает архитектуру приложения. Да есть пример с декомпозицией через модули, но это не более чем структура проекта. Меня никто этой структурой не ограничивает ни в чем. Я могу вынести бизнес логику в отдельную директорию (Domain), реализовать все там на plain php с интерфейсами, которые уже буду реализовывать в модулях и т.д. Или там могу отказаться от ORM и использовать event sourcing, cqrs и т.д.

                  Фреймворк в этом случае даст мне определенный костяк и какую-то дефолтную структуру. Но как я буду пользоваться этой структурой — решать мне. И что и как я буду использовать из фреймворка — так же решать мне.


                  1. Delphinum
                    27.02.2016 16:46

                    Естественно вы можете установить ZF2 (или любой другой фреймворк) и использовать архитектуру вопреки предлагаемой этим фреймворком, но это значит, что ZF2 не определяет архитектуры. К примеру ZF2 предлагает реализовывать взаимодействие между модулями с помощью событий. Вы можете реализовать у себя другой тип взаимодействия, но тогда возникает вполне резонный вопрос — а зачем тогда вы используете ZF2 с подключенной логикой взаимодействия модулей через события? Другими словами ставя скелетон ZF2 (важно, что именно скелетон, а не фреймворк), вы соглашаетесь с тем, что в этом скелетоне поключаются те или иные пакеты фреймворка, следовательно вы соглашаетесь их использовать (иначе зачем они подключаются?). Это и определяет архитектуру.


                    1. Fesor
                      28.02.2016 11:42

                      Говорю же, мне сложно что-то тут возражать и т.д. поскольку я не работал с ZF2 и понятия не имею о чем вы говорите, это надо разбираться. Но мне кажется вот этот момент с модулями и событиями — это не столь страшно. Если нам это ненадо — мы можем сделать свой скелетон. Если нас это устраивает — то почему бы и нет. Это "архитектура" на все еще довольно высоком уровне, и мы все еще вольны выбирать какую угодно архитектуру в пределах конкретных модулей (а они могут быть немаленькими).

                      Я же для себя просто сделал свой скелетон, откуда убрал все лишнее и добавил то что нужно конкретно мне.


                      1. Delphinum
                        28.02.2016 12:37

                        Я же для себя просто сделал свой скелетон, откуда убрал все лишнее и добавил то что нужно конкретно мне

                        Это я и называю фреймворком. Вы возьмете пакеты из состава фреймворка, соберете из них свой фреймворк.


            1. flancer
              27.02.2016 09:12

              Так и есть, composer вытягивает это дело :)

              Как же тогда автор предлагает писать бизнес-логику, если заранее не известно будет в вашем фреймворке использоваться DI или не будет

              Я создавал у себя в приложении обертку, если фреймворк предоставляет DI — использую его, нет — использую Zend'овский. Ну а классы с бизнес-логикой вообще не интересует, кто и как в них заинжектил нужные зависимости.


              1. Delphinum
                27.02.2016 14:01

                Ну а классы с бизнес-логикой вообще не интересует, кто и как в них заинжектил нужные зависимости

                Повторю вопрос вам — так к чему этот пример с Magento и Zend? Вы выбираете между двумя одинаковыми пакетами, что (каким то неведомым для меня образом) вылилось в выбор между двумя фреймворками.


                1. flancer
                  27.02.2016 15:13

                  Никогда еще не сталкивался с проектом, в котором сначала бы реализовывалась бизнес-логика, а затем подбирался фрейворк.

                  к этому.


                  1. Delphinum
                    27.02.2016 16:51

                    Я не знаком с вашей задачей подробно, но на сколько я понял из ваших слов, вы пытались выбрать между двумя фреймворками для выбора между двумя пакетами. Это конечно меня удивляет, но не относится к моему опыту. DI это один пакет. Да, он влияет на архитектуру, в зависимости от того, подключен он или не подключен, но он не влияет на архитектуру в вашем случае. Как уже сказал выше товарищь Fesor, DI это скорее для dependency inversion. Другими словами если вы уж решили следовать пути dependency inversion, то вы уже определили архитектуру проекта, и не важно DI вы будете пользовать или что то другое.

                    Учитывая это, представьте как нужно будет вывернуться для реализации бизнес-логики, поддерживающей dependency inversion и не поддерживающей онное )


                    1. flancer
                      28.02.2016 10:18

                      но на сколько я понял из ваших слов, вы пытались выбрать между двумя фреймворками для выбора между двумя пакетами

                      из моих слов:

                      У Magento 1 гораздо большее кол-во расширений.

                      Здесь "расширение" не равно "пакет". В одном случае в конечной системе требуется поддержка helpdesk'а, в другом — мультисклад. Вот у Magento 1 гораздо больше таких вот расширений, чем у второй версии. Никто не знает, будут ли эти расширения портированы на М2 через полгода. Если будут — завернемся на вторую версию, если нет — на первую.


                      1. Delphinum
                        28.02.2016 12:39

                        А что мешает портировать их самостоятельно или просто выбрать аналогичное решение и приустановить сбоку с помощью того же composer?


                        1. flancer
                          28.02.2016 19:31

                          То, что Magento 1 из до'composer'овской эпохи.


                          1. Delphinum
                            29.02.2016 01:21

                            Ну не с помощью composer, какая разница?! Прикрутить сбоку можно и руками.


                            1. flancer
                              29.02.2016 08:18
                              -2

                              Можно и штаны через голову надеть, при должном усердии.


                              1. Delphinum
                                29.02.2016 14:43
                                +1

                                Вы думаете что "правильные решения" должны подключаться только с помощью "правильного менеджера"? )


                                1. flancer
                                  29.02.2016 17:31

                                  Нет. Я думаю, что в сложных вопросах бывает более одного правильного решения, оптимальность каждого из которых зависит от критериев оценки.


  1. cawakharkov
    20.02.2016 19:50
    -2

    Виталик кстате не плохо говорит на русском.


  1. saggid
    21.02.2016 06:39
    +4

    Если фреймворк перестал разрабатываться, всегда можно продолжит разрабатывать и дорабатывать его самостоятельно. Как правило, любой хороший фреймворк написан профессионалами своего дела и довольно прост в изучении. Никогда не понимал, почему люди не думают об этом. Если проект действительно важный и большой, допилить что-то в самом фреймворке — это же вообще не проблема.

    По основной теме статьи… Как же всё сложно) Столько работы, и это лишь создание абстракции. Сколько месяцев придётся пилить эту гостевую книгу, если следовать такой методологии?)


    1. FractalizeR
      21.02.2016 13:20
      +1

      Если Symfony2 вдруг перестанет разрабатываться, осилите продолжение разработки небольшой командой? :)


      1. saggid
        22.02.2016 07:54

        Так не надо ведь там вообще всё перелопачивать. В теории, если при проверке вашего сервиса на устойчивость к взлому, если в работе фреймворка найдут дыру безопасности (как-будто дыр безопасности не существует без фреймворков. Скорее опять же, фреймворк — это наименее потенциальное место для дыр, ибо его там по 10 раз перепроверяют профессионалы) — то вы эту дыру не сможете чтоли найти и залатать?

        Далее, говоря о «продолжении разработки» — я не имел ввиду полноценную разработку, клонирование репо, информирование сообщества об этом, и так далее. Я имел ввиду, что можно где-то чуток изменить логику самого фреймворка чисто в своём конкретном проекте, если это вдруг понадобится. Это не так уж и сложно. Тем более, сейчас уже многие фреймворки перешли на использование контрактов, что сильно облегчает замену компонентов. По сути, это как собираются же сейчас модульные телефоны продавать. Захотел — поменял процессор, или оперативку. Также и фреймворки сейчас пишут. Захотел изменить какую-то подсистему — пожалуйста, реализуй свой класс, который будет удовлетворять условиям контракта, и ставь его вмето оригинального класса фреймворка.

        В общем, всё это вполне решаемо, нет чего-то нереального. Я лично в проект на Кохане затащил некоторые классы из Laravel 5, коллекции, некоторые хелперы. Потому что они удобные, почему нет? И эти классы вполне себе нормально живут вне ларавел. Считай, немного изменил Кохану в угоду нашего конкретного проекта. Также мы расширили ORM, добавив ему некоторые полезные функции, добавили возможность получения из БД текстовых данных объектов на текущем активном языке. Этого ведь не было в основном коде фреймворка. И, кстати, авторы Коханы вообще изначально там предполагали, что разработчики проектов будут расширять их классы, они даже изначально все основные классы свои сделали таким образом, что можно легко переопределить и расширить любой класс. Автор статьи писал о том, что он использовал Кохану, думаю, он поймёт, о чём я.

        Поэтому я и не вижу на самом деле особых проблем в зависимости от какого-то фреймворка. Всё это решаемо)


      1. cawakharkov
        22.02.2016 10:29

        Вот как раз с сф такого не случиться, как с комьюнити фрейморвками.


    1. Fesor
      23.02.2016 23:40

      Если фреймворк перестал разрабатываться, всегда можно продолжит разрабатывать и дорабатывать его самостоятельно.

      В долгосрочной перспективе это дороже чем сменить фреймворк.

      Как правило, любой хороший фреймворк написан профессионалами своего дела и довольно прост в изучении.

      Не стоит забывать о том что к "фреймворку" относятся все сторонние либы, бандлы, а не только ваш любимый laravel/yii/ci/etc. Изоляция позволяет нам быстро адаптироваться в случае нарушения BC у важного обновления и т.д. Увы мы не застрахованы от того, что какой-то вася пупкин сломал BC где-нибудь в важном месте, но нам вот очень нужен именно этот релиз. Я много раз с подобным сталкивался. Так что по поводу "профессианалов", если расширить смысл слова "фреймворк", я бы засомневался.


  1. romy4
    22.02.2016 17:20

    del


  1. Adelf
    23.02.2016 20:58

    Автор говорит переводчику спасибо — http://phpclub.ru/talk/threads/%D1%81%D0%BF%D0%B0%D1%81%D0%B8%D0%B1%D0%BE-%D0%B7%D0%B0-%D0%BF%D0%B5%D1%80%D0%B5%D0%B2%D0%BE%D0%B4-%D0%BC%D0%B0%D0%B8%D0%BB-%D1%80%D1%83.81481/

    Как видите, русским он владеет, но не в совершенстве :)


  1. Fesor
    23.02.2016 23:46
    -1

    1) ни слова о пирамиде тестирования
    2) ни слова о сокращении цикла обратной связи
    3) ни слова о моках
    4) ни слова о том, что тесты не должны быть сильно завязаны на реализацию (многие на этом спотыкаются и из-за этого тесты становятся уже не так эффективны)
    5) смешение TDD и ATDD (я про интеграционные и e2e тесты)
    6) Ни слова о том, что TDD должны направлять разработку, Ни слова о том, что TDD это скорее Test Driven Design чем Development.

    Что до концепции юзкейсов, тут интереснее послушать Дядю Боба, который в своих Agility and Architect об этом вещает.


  1. BlackScorp
    29.02.2016 23:21

    попробую ответить на русском языке

    Какие проблемы хочет решит ета "Чистая Архитектура"?

    Подумаем во первых о том как мы работаем каждый день, точнее как мы тестируем. В основном процесс такой: Программист задает код, открывает браузер, нажимает Ф5 и контролирует результат. Ето самый простой пример. Есть другие примеры, например несколько форм с кнопкой "далее" в первой форме задаются данные, нажимая на кнопку отркывается следующая форма, может перед проверки нужно ище задать данные в базу.

    следующая проблема ето рефакторинг, многие программисты видят каждый день код и думают "почему я ето так написал?" или "кто до таково додумался?" но боятся переписать ету строку, особенно в чужом коде, да и придется же после того как переписал код, проверять, и кто знает может где то что то сломается.

    большое время программисты тратят либо на тестирование в ручную, либо читать и копаться в чужом коде и молится богу чтоб на другом, неожиданном месте не что не сломалось.

    те, которые заметили что трудно добовлять фичи, в основном начинают использовать автоматичиские тесты, типа с selenium, но в таких тестов появляются другие проблемы, они медленныи, Code Coverage не существует, поменялся HTML тесты сломались.

    следующий уровень тесты логике с PHPUnit и тут иже проблемы наченаются, как тестировать мою логику? мой ORM имеет кучу ->where() ->join() и.т.д и если что то в базе данных поменялось то и тест сломался. весь код находится в контроллере, добраться до ево через роутер, View тоже где-то собираетса и в ней иногда "хелперы" которые внутренно следующий ORM используется.

    в чистой архитектуре решаются проблемы с Test Driven Design, то есть сперва создаем тест, потом создаем логику для проблемы, ета логика не конкретная, она просто описывает что нам нужно штобы добратся до нашей цели. Типа "ищем дании" "используем сервис А" для передела данных и передаем переделанные в высший слой.
    наши тесты создают интерфейсы коториы мы потом имплементируем. если придет новый человек в команду, он не должен теперь всю логику знать, нажатие на кнопку он быстро узнает сломал ли он что нибудь или нет. так же и мы сами, можем не боятся нашего кода.

    не зависимость от фреймворка, веб, дб ето естественно тоже плюс, не мало фирм меняют проект с монолита на микро сервис, и не мало фирм меняют или даже мешают несколько базы данных (например MySQL и CouchDB) многие процессы также и нужно на консоль вызывать и в контролере.

    Дядя боб говорил в одной презентации, дружи с фреймворком, развлекайся но не в коем случае не женись на нем, в конце попадешь в ситуацию что ты вышел замуж а не женился.

    надеюсь на многие вопросы я смог ответить