Help us understand the problem. What is going on with this article?

Clean Architecture 1つのController/Presenterで複数のUseCaseを扱う

この記事はLIFULLその3 Advent Calendar 2019の14日目の記事です。
株式会社LIFULL テクノロジー本部のアーキテクトの冨田です。

はじめに

Clean ArchitectureのController/Presenter/UseCase周りは設計、実装するときに結構悩みます。

1つのController/Presenterで複数のUseCaseを扱うことは、よくあることではないかなと思います。
また、複数のController/Presenterから1つのUseCaseを使うこともあるはずです。

PCとスマートフォンで画面デザインが異なるショッピングサイトを運営しているという想定で設計してみましょう。
今回の主題はController/PresenterとUseCaseなのでFrameWorks & Drivers層、Enterprise Business Rules層についての説明は省きます。

仕様

UseCases

  • 商品を閲覧する
  • 一緒によく購入される商品をレコメンドを閲覧する

Controllers/Presenters

  • PCの商品ページ
    • 商品を閲覧する
    • 一緒によく購入される商品をレコメンドを閲覧する
  • スマートフォンの商品ページ
    • 商品を閲覧する

実装

前提

このサンプルは下記の環境で実装しています

  • Kotlin 1.3.50
  • JDK 1.8.0 202
  • Micronaut 1.2.7

UseCases

商品を閲覧する

サンプルのためinterfaceやDTOをまとめて定義しています

BrowseDefnition.kt
package net.tomiyan.shop.business.product

data class BrowseInput(val id: String)

data class BrowseOutput(val id: String, val name: String)

interface BrowseInputBoundary {
    fun browse(input: BrowseInput, outputBoundary: BrowseOutputBoundary)
}

interface BrowseOutputBoundary {
    fun present(output: BrowseOutput)
}
BrowseUseCase.kt
package net.tomiyan.shop.business.product

import io.micronaut.runtime.http.scope.RequestScope

@RequestScope
class BrowseUseCase: BrowseInputBoundary {
    override fun browse(input: BrowseInput, outputBoundary: BrowseOutputBoundary) {
        outputBoundary.present(
            BrowseOutput(
                input.id,
                "product ${input.id}"
            )
        )
    }
}

一緒によく購入される商品をレコメンドを閲覧する

RecommendDefinition.kt
package net.tomiyan.shop.business.product

data class RecommendInput(val id: String)

data class RecommendOutput(val name: String)

interface RecommendInputBoundary {
    fun browse(input: RecommendInput, outputBoundary: RecommendOutputBoundary)
}
interface RecommendOutputBoundary {
    fun present(output: RecommendOutput)
}
RecommendUseCase.kt
package net.tomiyan.shop.business.product

import io.micronaut.runtime.http.scope.RequestScope

@RequestScope
class RecommendUseCase: RecommendInputBoundary {
    override fun browse(input: RecommendInput, outputBoundary: RecommendOutputBoundary) {
        outputBoundary.present(RecommendOutput("recommend for product ${input.id}"))
    }
}

Controllers/Presenters

サンプルのためViewModelとPresenterを同じファイルで定義していますが、
肥大化する場合は分けたほうが良いでしょう。

PCの商品ページ

PcPresenter.kt
package net.tomiyan.shop.ui.product

import net.tomiyan.shop.business.product.BrowseOutput
import net.tomiyan.shop.business.product.BrowseOutputBoundary
import net.tomiyan.shop.business.product.RecommendOutput
import net.tomiyan.shop.business.product.RecommendOutputBoundary

data class PcViewModel(val name: String, val recommend: String)

class PcPresenter: BrowseOutputBoundary, RecommendOutputBoundary {
    private lateinit var browse: BrowseOutput
    private lateinit var recommend: RecommendOutput

    override fun present(output: BrowseOutput) {
        browse = output
    }

    override fun present(output: RecommendOutput) {
        recommend = output
    }

    fun viewModel(): PcViewModel {
        return PcViewModel(browse.name, recommend.name)
    }
}
PcController.kt
package net.tomiyan.shop.ui.product

import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import net.tomiyan.shop.business.product.*

@Controller("/pc/product")
class PcController(private val browse: BrowseInputBoundary, private val recommend: RecommendInputBoundary) {

    @Get(
        produces = [MediaType.TEXT_PLAIN],
        uri = "/{id}"
    )
    fun index(id: String): String {
        val presenter = PcPresenter()

        // 商品閲覧 UseCaseの利用
        browse.browse(BrowseInput(id), presenter)

        // レコメンド商品閲覧 UseCaseの利用
        recommend.browse(RecommendInput(id), presenter)

        // Presenterを利用しViewModelを生成する
        val viewModel = presenter.viewModel()

        // ViewModelをテンプレートに渡すが、今回は説明なので割愛
        return """
            for pc
            product name: ${viewModel.name}
            recommend: ${viewModel.recommend}
        """.trimIndent()
    }
}

スマートフォンの商品ページ

SpPresenter.kt
package net.tomiyan.shop.ui.product

import net.tomiyan.shop.business.product.BrowseOutput
import net.tomiyan.shop.business.product.BrowseOutputBoundary

data class SpViewModel(val name: String)

class SpPresenter: BrowseOutputBoundary {
    private lateinit var browse: BrowseOutput

    override fun present(output: BrowseOutput) {
        browse = output
    }

    fun viewModel(): SpViewModel {
        return SpViewModel(browse.name)
    }
}
SpController.kt
package net.tomiyan.shop.ui.product

import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import net.tomiyan.shop.business.product.*

@Controller("/sp/product")
class SpController(private val browse: BrowseInputBoundary) {

    @Get(
        produces = [MediaType.TEXT_PLAIN],
        uri = "/{id}"
    )
    fun index(id: String): String {
        val presenter = SpPresenter()

        // 商品閲覧 UseCaseの利用
        browse.browse(BrowseInput(id), presenter)

        // Presenterを利用しViewModelを生成する
        val viewModel = presenter.viewModel()

        // ViewModelをテンプレートに渡すが、今回は説明なので割愛
        return """
            for smart phone
            product name: ${viewModel.name}
        """.trimIndent()
    }
}

まとめ

実装する言語によってはメソッド名に気をつける必要はありますが、
商品詳細のOutputBoundaryInterface、レコメンドのOutputBoundaryInterfaceを実装したPcPresenter、SpPresenterを用意すれば実現できます。

明日は@masamichiさんの記事です。

参考文献

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away