Laravel을 사용하여 클린 아키텍처를 구현하는 방법

Uncle BobClean Architecture는 현재 건축가계의 핫이슈다.그러나 실제 실현에 있어서는 아무도 주의할 만한 건의를 하지 않았다Laravel.
이것은 이해할 수 있다. 라벨MVC의 체계 구조와 라벨Facades이 너로 하여금 줄곧 The Clean Architecture를 사용하게 하는 경향은 깨끗하고 결합된 소프트웨어 부품을 설계하는 데 도움이 되지 않는다.
따라서 오늘 저는 Laravel 응용 프로그램에서 청결 체계 구조 원칙을 실현하는 실용 프로그램을 보여 드리겠습니다. Robert C. Martin 에서 말한 바와 같이.
나의 GitHub repository에는 여기에서 기술한 개념의 완전하고 효과적인 실현을 제공하였다.나는 당신이 본문을 읽을 때 실제 코드를 보는 것을 건의합니다.
이번에는 우리 손을 깨끗이 씻자👍

이 모든 것은 한 장의 도표에서 시작된 것이다



The architecture must support the use cases. [...] This is the first concern of the architect, and the first priority of the architecture.


(The Clean Architecture 16장, 148쪽.)
만약 네가 지금까지 용례를 들어 본 적이 없다면, 너는 그것을 하나의 특성, 하나의 시스템이 의미 있는 일을 하는 능력으로 간주할 수 있다.UML 좋은 이름Use Case Diagrams으로 설명해 드릴게요.
CA에서 용례는 응용 프로그램의 핵심이다.그것들은 응용 프로그램 기계를 제어하는 마이크로칩이다.

그렇다면 우리는 어떻게 이런 용례를 실현해야 합니까?


이렇게 물어봐서 반갑습니다!다음은 두 번째 그림입니다.

내가 간단하게 설명한 후에 우리는 실제 코드에 깊이 들어갈 것이다.
분홍색 선은 흐름을 컨트롤한다.이것은 서로 다른 구성 요소의 실행 순서를 나타낸다.우선, 사용자는 보기에서 일부 내용을 변경했다. (예를 들어, 그는 등록표를 제출했다.)이 상호작용은 Request의 대상이 된다.컨트롤러가 그것을 읽고 RequestModel를 생성하여 UseCaseInteractor에 사용하도록 합니다.
그리고 UseCaseInteractor 새로운 사용자를 만드는 등 ResponseModel 형식으로 응답을 준비하고 Presenter에 전달합니다.또한 ViewModel를 통해 뷰를 업데이트합니다.
와, 너무 많다.😵 이것은 CA에 대한 주요 비판일 수 있다.너무 길어!
호출 계층은 다음과 같습니다.
Controller(Request)
  ⤷ Interactor(RequestModel)
      ⤷ Presenter(ResponseModel)
          ⤷ ViewModel

항구는?


나는 네가 관찰자라는 것을 알아차렸다.저층(용례와 실체는 일반적으로 역이라고 불리는데 위의 모드에서 빨간색과 노란색 동그라미로 표시한다)과 고층(틀, 파란색 동그라미로 표시)을 하려면 어댑터(녹색 동그라미)가 필요하다.그들의 업무는 각자의 API와 계약(또는 인터페이스)을 사용하여 고위층과 하층 간에 소식을 전달하는 것이다.
어댑터는 CA에서 매우 중요합니다. 프레임의 변경은 도메인의 변경이 필요하지 않고, 반대로 변경될 수도 있습니다.CA에서 우리는 우리의 용례가 구조(실제 실현)에서 추상화되어 둘 다 임의로 변경할 수 있고 다른 층에서 변경 사항을 전파할 필요가 없기를 바란다.
따라서 깨끗한 체계 구조로 설계된 전통적인 PHP/HTML 응용 프로그램은 컨트롤러와 표현자를 변경해야만 REST API로 전환할 수 있습니다. 용례는 변하지 않습니다!또는 HTML 및 REST와 같은 용례를 사용할 수 있습니다.만약 네가 나에게 묻는다면, 그것은 정말 다행이다🤩
이를 위해서는 각 계층에 필요한 방식으로 어댑터를 "실행"해야 합니다.우리는 인터페이스를 사용하여 입력과 출력 포트를 정의할 것이다.그들은 본질적으로, "네가 나와 이야기하고 싶다면, 너는 반드시 이렇게 해야 한다."라고 말했다.

쓸데없는 소리.코드를 보고 싶어요!

UseCaseInteractor가 모든 것의 핵심이 될 것이기 때문에 이것부터 시작하자.
class CreateUserInteractor implements CreateUserInputPort
{
    public function __construct(
        private CreateUserOutputPort $output,
        private UserRepository $repository,
        private UserFactory $factory,
    ) {
    }

    public function createUser(CreateUserRequestModel $request): ViewModel
    {
        /* @var UserEntity */
        $user = $this->factory->make([
            'name' => $request->getName(),
            'email' => $request->getEmail(),
        ]);

        if ($this->repository->exists($user)) {
            return $this->output->userAlreadyExists(
                new CreateUserResponseModel($user)
            );
        }

        try {
            $user = $this->repository->create(
                $user, new PasswordValueObject($request->getPassword())
            );
        } catch (\Exception $e) {
            return $this->output->unableToCreateUser(
                new CreateUserResponseModel($user), $e
            );
        }

        return $this->output->userCreated(
            new CreateUserResponseModel($user)
        );
    }
}
여기에 우리가 주의해야 할 세 가지 일이 있다.
  • 인터랙티브 구현CreateUserInputPort 인터페이스
  • 상호작용자는 CreateUserOutputPort,
  • 에 의존한다.
  • 상호작용자는 자기가 하는 것이 아니라ViewModel 시범자에게 하는 것이다
  • Presenter(이곳은 CreateUserOutputPort에서 추상적으로) 어댑터 (녹색)층에 위치하기 때문에 CreateUserInteractor에서 호출하는 것은 확실히 inversion of control의 좋은 예이다. 프레임워크가 용례를 제어하지 않고 용례로 프레임워크를 제어하는 것이다.
    만약 그것이 너무 무미건조하고 복잡하다고 생각한다면, 이 모든 것을 잊고, 모든 의미 있는 결정을 고려하는 것은 예시적인 단계에서 이루어진 것이다. 응답 경로 선택 (((userCreated, userAlreadyExists 또는 unableToCreateUSer 을 포함한다.통제자와 시범자는 순종적인 노예일 뿐 상업 논리가 부족하다.
    우리 는 영원히 리허설 이 부족하니 나와 함께 노래하자: 관제사👏 마땅하다👏 아니오.👏 포함👏 장사👏 논리학👏

    그렇다면 컨트롤러의 측면에서 볼 때 그것은 어떤 것입니까?


    디렉터의 삶은 간단합니다.
    class CreateUserController extends Controller
    {
        public function __construct(
            private CreateUserInputPort $interactor,
        ) {
        }
    
        public function __invoke(CreateUserRequest $request)
        {
            $viewModel = $this->interactor->createUser(
                new CreateUserRequestModel($request->validated())
            );
    
            return $viewModel->getResponse();
        }
    }
    
    실제 CreateUserInputPort 가 아닌 CreateUserInteractor 추상에 의존하는 것을 볼 수 있습니다.그것은 우리로 하여금 용례를 유연하게 마음대로 변경할 수 있게 하고 컨트롤러로 하여금 테스트할 수 있게 한다.이따가 다시 이야기합시다.

    좋아, 이건 간단하고 어리석어.사회자는요?


    마찬가지로 매우 간단합니다.
    class CreateUserHttpPresenter implements CreateUserOutputPort
    {
        public function userCreated(CreateUserResponseModel $model): ViewModel
        {
            return new HttpResponseViewModel(
                app('view')
                    ->make('user.show')
                    ->with(['user' => $model->getUser()])
            );
        }
    
        public function userAlreadyExists(CreateUserResponseModel $model): ViewModel
        {
            return new HttpResponseViewModel(
                app('redirect')
                    ->route('user.create')
                    ->withErrors(['create-user' => "User {$model->getUser()->getEmail()} alreay exists."])
            );
        }
    
        public function unableToCreateUser(CreateUserResponseModel $model, \Throwable $e): ViewModel
        {
            if (config('app.debug')) {
                // rethrow and let Laravel display the error
                throw $e;
            }
    
            return new HttpResponseViewModel(
                app('redirect')
                    ->route('user.create')
                    ->withErrors(['create-user' => "Error occured while creating user {$model->getUser()->getName()}"])
            );
        }
    }
    
    전통적으로 이 코드들은 모두 ifs 컨트롤러에 있다.이것은 컨트롤러에 무슨 일이 일어났는지 (예: 사용 $user->wasRecentlyCreated 하거나 이상을 던지는 방법) 을 찾아내도록 할 것이다.
    예를 들어 제어하는 프레젠테이터를 사용하면 컨트롤러에 닿지 않은 상태에서 결과를 선택하고 변경할 수 있습니다.이게 얼마나 좋아요?

    그래서 모든 것이 추상에 의존한다. 용기가 언제 개입될까?


    너는 완전히 옳다, 나의 좋은 친구!오늘 좋은 친구가 함께 있어서 나는 매우 기쁘다.
    이러한 모든 연결을 app/Providers/AppServiceProvider.php에 연결하는 방법은 다음과 같습니다.
    class AppServiceProvider extends ServiceProvider
    {
        /**
         * Register any application services.
         *
         * @return void
         */
        public function register()
        {
            // wire the CreateUser use case to HTTP
            $this->app
                ->when(CreateUserController::class)
                ->needs(CreateUserInputPort::class)
                ->give(function ($app) {
                    return $app->make(CreateUserInteractor::class, [
                        'output' => $app->make(CreateUserHttpPresenter::class),
                    ]);
                });
    
    
            // wire the CreateUser use case to CLI
            $this->app
                ->when(CreateUserCommand::class)
                ->needs(CreateUserInputPort::class)
                ->give(function ($app) {
                    return $app->make(CreateUserInteractor::class, [
                        'output' => $app->make(CreateUserCliPresenter::class),
                    ]);
                });
        }
    }
    
    CLI 변수를 추가하여 사용 사례가 다른 경우ViewModel를 반환하는 것이 얼마나 쉬운지 시연합니다.자세한 내용은 a the actual implementation 참조👍

    테스트 좀 해도 될까요?


    맙소사!제발!CA의 또 다른 장점은 추상에 매우 의존하기 때문에 테스트를 쉽게 할 수 있다는 것이다.
    class CreateUserUseCaseTest extends TestCase
    {
        use ProvidesUsers;
    
        /**
         * @dataProvider userDataProvider
         */
        public function testInteractor(array $data)
        {
            (new CreateUserInteractor(
                $this->mockCreateUserPresenter($responseModel),
                $this->mockUserRepository(exists: false),
                $this->mockUserFactory($this->mockUserEntity($data)),
            ))->createUser(
                $this->mockRequestModel($data)
            );
    
            $this->assertUserMatches($data, $responseModel->getUser());
        }
    }
    
    완전한 테스트 과정을 제공한다here.
    나는 Mockery로 조롱을 표시하지만, 그것은 무엇이든지 효과가 있다.코드가 많을 수도 있지만, 실제로는 아주 간단하게 작성되며, 100%의 예시 커버율을 조금도 힘들이지 않게 제공할 것입니다.

    이 실현은 이 책과 좀 다르지 않습니까?


    네.봐라, CA는 자바인이 설계한 것이다.그리고 대부분의 경우 자바 프로그램에서 보기를 업데이트하려면 Presenter에서 바로 업데이트할 수 있다.
    PHP는 아니에요.우리는 보기를 완전히 제어할 수 없을 뿐만 아니라, 프레임워크는 컨트롤러가 응답을 되돌려주는 개념을 둘러싸고 구축된 것이다.
    따라서, 나는 정확한 응답을 되돌리기 위해 ViewModel 컨트롤러로 올라가도록 원칙을 조정해야 한다.만약 당신이 더 좋은 디자인을 생각해 낼 수 있다면, 평론에서 저에게 알려주세요🙏
    너는 나에게 네가 댓글 속의 생각을 알려줄 수 있니?너의 관점은 나에게 매우 중요하다. 왜냐하면 내가 이 글을 쓰는 것은 나의 시야에 도전하고 매일 새로운 것을 배우기 위해서이기 때문이다.
    물론 제출pull-request을 통해 프레젠테이션 저장소를 변경하는 것을 권장합니다.공헌해 주셔서 감사합니다.🙏
    이 글은 내가 4일 동안 연구, 실현, 테스트, 쓰기를 진행했다.당신의 관심에 정말 감사드립니다. 아마도 당신의 소셜네트워크서비스(SNS)에서 공유할 수 있을 것입니다🙏
    여러분 감사합니다. 당신들 의 공헌 은 저 로 하여금 당신들 을 위해 더 많은 문장 을 쓸 동력 을 가지게 했습니다👍
    자세히 보기:
  • Clean Coder Blog
  • Entity-Control-Boundary
  • Clean Architecture: Use case containing the presenter or returning data?
  • A button, as a “Clean Architecture” plugin
  • UML Diagrams cheatsheet
  • 좋은 웹페이지 즐겨찾기