オブジェクト指向プログラミングが少しだけ理解できた話

# はじめに
こんにちは、入社1年目がそろそろ終わろうとしているサーバーサイドエンジニアです。

大学の授業では、C言語から始まり、Java、C++などを経験し、研究ではずっとC言語やPerlを触っていました。
いわゆる手続き型プログラミングをメインでやっていた感じです。
授業でJavaを使ってオブジェクト指向プログラミングを学ぼうみたいな授業はあったものの当時はちんぷんかんぷん、とりあえずクラスが登場すればそれはオブジェクト指向なんだという浅い(間違った)理解でした。

クラスを作っても、あっちこちにコードが散らばってやる意味あるの？とさえ思っていて、なんとなく変数と関数をまとめておくためのものくらいにしか思っていませんでした。
Cの構造体に関数も入る上位互換みたいな笑。

というわけでそんな学生が、社会人にもなり、Webアプリの業務コードを書くようになりました。
今現在はSpringBootとKotlinを使っています。

# 手続き型脳の新人が書いたコード
以下の管理業務要件を満たすロジックなりデータモデルなりを実装してねと言われたとしましょう。

- お客様に公開する用の記事を作成する。
- 記事は作成時点では公開されず、内容だけが決まっている状態。
- その記事は特定の承認フローを経る。
- 承認されれば公開期間を設定し、公開する。

山場は承認フローを流れていく記事の承認状態の遷移をどう実装するかですかね。
とりあえずAPIとしては、これだけ用意しておきましょう。

- 記事の作成
- 記事の承認
- 記事の差し戻し
- 記事の再審査
- 記事の公開
- 記事の取り扱いを止める

```kotlin
@Service
class ArticleService(
        private val articleRepository: ArticleRepository
) {
    companion object {
        private val log = LoggerFactory.getLogger(ArticleService::class.java)
    }

    fun createNewArticle(articleCreateRequest: ArticleCreateRequest) : Article? {}

    fun approveArticle(articleApproveRequest: ArticleApproveRequest) : Article? {}

    fun examineArticle(articleExamineRequest: ArticleExamineRequest) : Article? {}

    fun sendBackArticle(articleSendBackRequest: ArticleSendBackRequest) : Article? {}

    fun publishArticle(articlePublishRequest: ArticlePublishRequest) : Article? {}

    fun withdrawArticle(articleWithdrawRequest: ArticleWithdrawRequest) : Article? {}
}
```
（Kotlinコードの解説はしないことにします。）
記事のデータモデルは

```kotlin
@Entity
@Table(name = "article")
data class Article (
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    var id : Int? = null,

    @Column(name = "content")
    var content : String?,

    @Column(name = "publish_start_time")
    var publishStartTime : LocalDateTime?,

    @Column(name = "publish_end_time")
    var publishEndTime : LocalDateTime?,

    @Column(name = "status")
    var status : Byte,

    @Column(name = "publisherId")
    var publisherId : Int,

    @Column(name = "reviewerId")
    var reviewerId : Int
)
```

このstatusに記事の状態を格納します。
状態遷移図はこんな感じにしましょう。
![state.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/564587/4d95c1d2-36b9-844b-2079-e29a96da28b9.png)
以下記事の状態の定義です。

```kotlin
enum class ArticleStatus(val code: Byte) {
    UNDER_EXAMINATION(1),
    APPROVED(2),
    SEND_BACK(3),
    WITHDRAWAL(4),
    NOW_PUBLIC(5)
}
```

「よし、じゃあ後は[Service*](#補足:3層アーキテクチャ)層にビジネスロジック書くだけや！！」
きっとこのようなコードが出来上がるでしょう。

```kotlin
@Service
class ArticleService(
        private val articleRepository: ArticleRepository
) {
    //中略
    fun publishArticle(articlePublishRequest: ArticlePublishRequest): Article? {
        articleRepository.findById(articlePublishRequest.articleId).let { article ->
            if (article.isEmpty) {
                log.warn("Article is not found")
                return null
            }
            article.get().let {
                if (it.status == ArticleStatus.APPROVED.code) {
                    it.status = ArticleStatus.NOW_PUBLIC.code
                    it.publishStartTime = articlePublishRequest.publishStartDate
                    it.publishEndTime = articlePublishRequest.publishEndDate
                    return it
                }
                log.warn("Invalid article status")
                return null
            }
        }
    }

    fun withdrawArticle(articleWithdrawRequest: ArticleWithdrawRequest): Article? {
        articleRepository.findById(articleWithdrawRequest.articleId).let { article ->
            if (article.isEmpty) {
                log.warn("Article is not found")
                return null
            }
            article.get().let {
                if (it.status == ArticleStatus.APPROVED.code ||
                        it.status == ArticleStatus.SEND_BACK.code ||
                        it.status == ArticleStatus.NOW_PUBLIC.code
                ) {
                    it.status = ArticleStatus.WITHDRAWAL.code
                    return it
                }
                log.warn("Invalid article status")
                return null
            }
        }
    }
}
```
現在の状態を確認して、OKなら記事に対して該当の操作を施し、statusを変えて返却する。
例えば、審査中状態なのにいきなり公開済み状態には行かないようにValidationチェックをかけているのですね。
Validationチェックをかけていること自体は良いのですが...このコードもう、辛くないですか？

仮に、この状態遷移図の仕様が変更されたらどうなるでしょうか？if文全て書き直しです。
別のService層の関数でこの`state`を利用している関数があれば、そこにも影響が波及するかもしれません。

# 僕が思うオブジェクト指向とは？
このように、ビジネスロジックとデータモデルを分けて考えた場合、つまり手続き型っぽく記述した場合、
このArticleに関係するビジネスロジックがService層全体に染み渡る可能性があります。

あくまで、ArticleServiceはArticleに関係するビジネスロジックを極力集めたところであるだけであって、
システム上のArticleに関係する全てのロジックがArticleServiceにあるとは限りません。
つまり、Articleに関する仕様が変更されるとService層に染み渡った全てのArticleに関する記述を探して、一つ一つ修正を加える必要が出てくるかもしれません。

手続き型の記述だと、処理の主体は「プログラマ」です。
でもオブジェクト指向だと処理の主体は「インスタンスたち」なのかなと思います。
つまりビジネスロジックを実現するのもインスタンスです。
ビジネスロジックを実現するためにプログラマが外からデータに対してなにか判断や加工をするのは、オブジェクト指向ではないのかなと思います。
ビジネスロジックを実現するためにデータそのものが自分で判断や、加工を行うイメージがオブジェクト指向なのかなと思いました。

データそのものが自分で判断や加工をするのならば、それはメソッドという形で実現できます。
そのデータモデルに関するビジネスロジックは、データモデルのクラス宣言箇所に集約されることになります。
つまり仕様変更があったとしても、Service層全体を確認する必要性をなくすようなコードを書くことができます。


# オブジェクト指向をかじってから書いたコード
Service層はデータモデルのメソッドをマクロ的に呼ぶだけとなっており、すごくすっきりした気がします。

```kotlin 
@Service
class ArticleService(
        private val articleRepository: ArticleRepository
) {
    // 中略
    fun publishArticle(articlePublishRequest: ArticlePublishRequest) : Article? {
        articleRepository.findById(articlePublishRequest.articleId).let { article ->
            if(article.isEmpty) {
                log.warn("Article is not found")
                return null
            }
            article.get().transitStatus(Event.PUBLISH)?.let { publishedArticle ->
                publishedArticle.setPublishPeriod(articlePublishRequest.publishStartDate,articlePublishRequest.publishEndDate)
                return articleRepository.save(publishedArticle)
            }
            return null
        }
    }

    fun withdrawArticle(articleWithdrawRequest: ArticleWithdrawRequest) : Article? {
        articleRepository.findById(articleWithdrawRequest.articleId).let { article ->
            if(article.isEmpty) {
                log.warn("Article is not found")
                return null
            }
            article.get().transitStatus(Event.WITHDRAW)?.let { withdrawalArticle ->
                return articleRepository.save(withdrawalArticle)
            }
            return null
        }
    }
```
データモデルとビジネスロジックの集約

```kotlin
@Entity
@Table(name = "article")
data class Article (
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    var id : Int? = null,

    @Column(name = "publish_start_time")
    var publishStartTime : LocalDateTime?,

    @Column(name = "publish_end_time")
    var publishEndTime : LocalDateTime?,

    @Column(name = "content")
    var content : String?,

    @Column(name = "status")
    var status : Byte,

    @Column(name = "publisherId")
    var publisherId : Int,

    @Column(name = "reviewerId")
    var reviewerId : Int
) : Serializable {

    constructor(articleCreateRequest: ArticleCreateRequest) : this (
            publishStartTime = null,
            publishEndTime = null,
            content = articleCreateRequest.content,
            status = ArticleStatus.UNDER_EXAMINATION.code,
            publisherId = articleCreateRequest.publisherId,
            reviewerId = articleCreateRequest.reviewerId
    )

    companion object {
        private var nextStatus: MutableMap<Pair<ArticleStatus, Event>, ArticleStatus> = mutableMapOf()
        private val log = LoggerFactory.getLogger(Article::class.java)
    }

    init {
        nextStatus[Pair(ArticleStatus.UNDER_EXAMINATION, Event.APPROVE)]   = ArticleStatus.APPROVED
        nextStatus[Pair(ArticleStatus.UNDER_EXAMINATION, Event.SEND_BACK)] = ArticleStatus.SEND_BACK
        nextStatus[Pair(ArticleStatus.APPROVED, Event.PUBLISH)]            = ArticleStatus.NOW_PUBLIC
        nextStatus[Pair(ArticleStatus.APPROVED, Event.WITHDRAW)]           = ArticleStatus.WITHDRAWAL
        nextStatus[Pair(ArticleStatus.SEND_BACK, Event.EXAMINE)]           = ArticleStatus.UNDER_EXAMINATION
        nextStatus[Pair(ArticleStatus.SEND_BACK, Event.WITHDRAW)]          = ArticleStatus.WITHDRAWAL
        nextStatus[Pair(ArticleStatus.NOW_PUBLIC, Event.WITHDRAW)]         = ArticleStatus.WITHDRAWAL
    }

    private fun getNextStatus(nowStatus: ArticleStatus, event: Event) : ArticleStatus? {
        return nextStatus[Pair(nowStatus, event)]
    }

    fun transitStatus(event : Event) : Article? {
        ArticleStatus.fromCodeToStatus(this.status)?.let { oldStatus ->
            this.status = getNextStatus(oldStatus, event)?.code ?: run {
                log.warn("Invalid event")
                return null
            }
            return this
        }
        log.warn("Invalid article status")
        return null
    }

    fun setPublishPeriod(publishStartTime : LocalDateTime, publishEndTime: LocalDateTime) {
        this.publishStartTime = publishStartTime
        this.publishEndTime = publishEndTime
    }
}

enum class Event {
    APPROVE,
    SEND_BACK,
    EXAMINE,
    PUBLISH,
    WITHDRAW
}

enum class ArticleStatus(val code: Byte) {
    UNDER_EXAMINATION(1),
    APPROVED(2),
    SEND_BACK(3),
    WITHDRAWAL(4),
    NOW_PUBLIC(5);
    companion object {
        fun fromCodeToStatus(statusCode : Byte) : ArticleStatus? {
            return values().firstOrNull { it.code == statusCode }
        }
    }
}
```

今現在の状態から、指定された状態へ遷移できるかどうかも全てデータ自身が判断する形になっています。
仮に状態遷移の仕様が変更されたとしても`nextStatus`の定義を変更するのみとなっています。

# 最後に
学生時代では組み込み分野をやっていて、思い切り手続き型の思考回路で考える癖がついていて、
業務系やWebの分野だとオブジェクト指向でものが表現されることが多いなという印象です。
ここへのパラダイムシフトが結構難しかったです。。。



# 補足:3層アーキテクチャ
業務アプリケーションでは3層アーキテクチャを採用していることが多いです。
そして一般的に各層は、

- プレゼンテーション層; 画面や外部接続インターフェース
- アプリケーション層; ビジネスロジック
- データソース層; データベースへの入出力

と呼ばれていますが、SpringBootでは、

- Controller
- Service
- Repository

がそれぞれに対応します。