良いコード

なんか良いコードと悪いコードがなんちゃらみたいな本がしばらく前に話題になっていて、随分と主観的なタイトルだなと思って近づかないようにしてるんだけど、別に主観的で良いじゃんと思ったので自分が現時点で思う良いコード(否、設計)の条件を挙げてみる。 あくまで主観なのでめっちゃそのような指標が出るけど構わない。

  • テスト実行速度が十分に速い
  • テストデータを作るのに苦労しない
  • 副作用と純粋な処理が分けられている(=DIが楽)
  • 依存の向きが一方向である
  • SOLID原則を前提に考えられている
  • 問題の発生場所をすぐに特定できる
  • 問題の解消が難しくない
  • 実行時リソースの無駄遣いをしていない
  • 宣言的かつ説明的な命名がされている
  • ビジネスロジックフレームワークの事情が染み込んでいない

ざっと上げてみたけど結構少なかった。

これらを蔑ろにすると高速でゴミを作るようなことになりかねないとおもっちょる。

elmで頑張ってValueObjectを定義したけどやっぱ型クラスないと無理矢理感が否めない

Haskellっぽいでおなじみのelmにはtypeclassがない。これによりMaybe.andThenを使わざるを得ない局面などにぶつかることが多々ある。

これ自体はelmの設計方針だったりするので特に文句を言う筋合いもないので気に入らなければpurescriptやfp-tsでも使えば終わる話だと思うのだけどelmは関数型およびjavascript界隈弱者にとって学習コストがめちゃくちゃ低いというメリットがそれなりの存在感を持っているのでやっぱり使いたい。

で、この度ValueObjectっぽい型を表現しようとして試行錯誤したところ以下のような形が限界だった。

module ValueObject exposing (..)

type ValueObject a = ValueObject a

type ValidateResult a e
    = Ok (ValueObject a)
    | Err e

validate : (a -> ValidateResult a e) -> ValueObject a -> ValidateResult a e
validate func (ValueObject a) = func a

これ結構いびつで、ValueObjectをラップするコンテキストが必要になるので以下のような使用イメージになる。

module ValueObjectSpec exposing (..)

import Test exposing (..)
import Expect
import ValueObject exposing (ValueObject(..), validate, ValidateResult(..))

type PositiveIntItem = PositiveIntItem (ValueObject Int)

positiveIntItem : Int -> PositiveIntItem
positiveIntItem = ValueObject >> PositiveIntItem

unwrapPositiveIntItem : PositiveIntItem -> Int
unwrapPositiveIntItem (PositiveIntItem (ValueObject a)) = a

invalidPositiveIntItemErrorMessage : Int -> String
invalidPositiveIntItemErrorMessage = String.fromInt >> (++) "Value must be positive but: "

validatePositiveIntItem : PositiveIntItem -> ValidateResult Int String
validatePositiveIntItem (PositiveIntItem a) =
    let
        func : Int -> ValidateResult Int String
        func x =
            if x > 0 then ValueObject.Ok a
            else ValueObject.Err (invalidPositiveIntItemErrorMessage x)
    in
    validate func a

type RecordItem = RecordItem (ValueObject { x: Int, y: String })

recordItem : Int -> String -> RecordItem
recordItem x y = RecordItem (ValueObject { x = x, y = y })

unwrapRecordItem : RecordItem -> { x: Int, y: String }
unwrapRecordItem (RecordItem (ValueObject a)) = a

invalidRecordItemXErrorMessage : Int -> String
invalidRecordItemXErrorMessage = String.fromInt >> (++) "property `x` must be positive but: "

invalidRecordItemYErrorMessage : String
invalidRecordItemYErrorMessage = "property `y` must include some char but empty"

invalidRecordItemTotallyErrorMessage : Int -> String
invalidRecordItemTotallyErrorMessage = invalidRecordItemXErrorMessage >> (++) invalidRecordItemYErrorMessage

validateRecordItem : RecordItem -> ValidateResult { x: Int, y: String } String
validateRecordItem (RecordItem a) =
    let
        func : { x: Int, y: String } -> ValidateResult { x: Int, y: String } String
        func r =
            case (r.x < 0, String.length r.y == 0 ) of
                (True, True) -> ValueObject.Err (invalidRecordItemTotallyErrorMessage r.x)
                (False, True) -> ValueObject.Err invalidRecordItemYErrorMessage
                (True, False) -> ValueObject.Err (invalidRecordItemXErrorMessage r.x)
                (False, False) -> ValueObject.Ok a
    in
    validate func a

suite : Test
suite = describe "validate"
    [ test "works against a valid primitive value" <|
        \_ ->
            let
                correctItem = positiveIntItem 7

            in
            Expect.equal (validatePositiveIntItem correctItem) (ValueObject.Ok (ValueObject <| unwrapPositiveIntItem correctItem))
    , test "works against an invalid primitive value" <|
        \_ ->
            let
                incorrectItem = positiveIntItem -1
            in
            Expect.equal (validatePositiveIntItem incorrectItem) (ValueObject.Err (invalidPositiveIntItemErrorMessage <| unwrapPositiveIntItem incorrectItem))
    , test "works against a valid record " <|
        \_ ->
            let
                correctItem = recordItem 1 "a"
            in
            Expect.equal (validateRecordItem correctItem) (ValueObject.Ok (ValueObject <| unwrapRecordItem correctItem))
    , test "works against an invalid record (1)" <|
        \_ ->
            let
                incorrectXItem = recordItem -1 "a"
            in
            Expect.equal (validateRecordItem incorrectXItem) (ValueObject.Err (invalidRecordItemXErrorMessage <| (unwrapRecordItem >> (.x)) incorrectXItem) )
    , test "works against an invalid record (2)" <|
        \_ ->
            let
                incorrectYItem = recordItem 1 ""
            in
            Expect.equal (validateRecordItem incorrectYItem) (ValueObject.Err (invalidRecordItemYErrorMessage))
    , test "works against an invalid record (3)" <|
        \_ ->
            let
                incorrectItem = recordItem -1 ""
            in
            Expect.equal (validateRecordItem incorrectItem) (ValueObject.Err (invalidRecordItemTotallyErrorMessage <| (unwrapRecordItem >> (.x)) incorrectItem))
    ]

このような感じになってしまうしこれ以外にもMonadicになんか扱いたいみたいな場合のアドホック多相を実現する術がなくて冒頭で述べたように困らないでもない。

まあ、elmの思想に合わせて素直に関数のオーバーロードとすれば済む話ではあると思う。

ではどのようなものを求めているかをHaskellで書いてみるとこんな感じになるだろう。

{-# LANGUAGE  MultiParamTypeClasses #-}
module ValueObject where
import Data.Aeson (Value)

data ValueObjectValidationResult a e = Ok a | Err e
    deriving(Show, Eq)

class ValueObject a e where
    validate :: a -> ValueObjectValidationResult a e

newtype Under100PositiveIntItem = Under100PositiveIntItem { getValue :: Int } deriving (Show, Eq)

data Under100PositiveIntItemValidationError = Negative | OverLimit deriving (Show, Eq)

type Under100PositiveIntItemValidationResult = ValueObjectValidationResult Under100PositiveIntItem Under100PositiveIntItemValidationError

instance ValueObject Under100PositiveIntItem Under100PositiveIntItemValidationError where
    validate a
        | v < 0 = Err Negative
        | v > 100 = Err OverLimit
        | otherwise = Ok a
        where
            v = getValue a


under100positiveIntItem :: Int -> Under100PositiveIntItemValidationResult
under100positiveIntItem = validate . Under100PositiveIntItem

main :: IO ()
main = do
    print $ under100positiveIntItem 1 == Ok (Under100PositiveIntItem {getValue= 1})
    print $ under100positiveIntItem (-1) == Err Negative
    print $ under100positiveIntItem 101 == Err OverLimit

これはこれでなんか冗長な感じはあるけど結構堅牢だと思うので型クラスって強力だなって感じ。

システム開発するときの難しさについて

難しさとは

適当に動くだけなら簡単かもしれないが利用者がいて利害が発生するようなシステム開発は本当に難しい。

システム開発で開発者が主に実装というフェーズで気にすることというとざっと挙げてみて要件定義、アーキテクチャ選定、データ設計、プログラミングというのがあり、そのどれもが一様に難しい。

なぜ難しいか。人間は自分の都合しか考えないし関心のあるところにしか注意が向かないので異様に複雑な文脈が絡み合うので、そのような要件を記述するとなると並大抵のことではスムーズにいかない。

詰まるところシステム開発の難しさは端的に境界が曖昧なことに起因すると思う。

自分がそういう考えなので、なにか機能追加や改修が必要となったとき誰かが安易に「簡単にできます」とかいうのを聞くとものすごく身構えてしまう。

要件というのは代替がきかない本質の部分であってそれ以外のプログラミングの技術やらアーキテクチャの理解度やらは代替のきく表層の部分だと思っているし、実際この感覚はそこまで世間とずれていないから一般的にドメイン駆動開発やらクリーンアーキテクチャなどと名前をつけて日々論じられてるんだろうなと思っている。

この難しさを少しでも薄めるにはどうしたらいいのだろうというのをぼんやり考えてみる。

要件の文脈とは

要件というものは必ず誰か何かなぜしたいという文脈を伴っている。

そしてこれはたびたび開発者が「再利用性」といった免罪符を使って文脈を無視して似たような振る舞いを共通化しようとした時点でいらぬ複雑さを生むことがある。

ものの本にこのようなことが書かれている。

Be careful. Resist the temptation to commit the sin of knee-jerk elimination of duplications. Make sure the duplication is real.

要件の文脈を無視して何かしたいかだけに注目することによって文脈の境界が曖昧なまま入り組んでしまって面倒なことになる。

引用元の書籍はまさにこの点についての問題意識や考え方を提示してくれるものなので一読の価値がある。まあ有名すぎる本なので何を今更という感じだけど。

難しさを薄めるには

具体的な方法の一つとして結論からいうと、プログラミングの手法としては型の定義をすることで一役を担うと思っている。

たとえば「支払いをする」という要件があったとき、暗黙の文脈があるのでそれをはっきりさせなくてはいけない。

経理部が金融機関に支払う」(以下ケースA)というのと「エンドユーザーが弊社に支払う」(ケースB)では同じ支払いという動作でも全く意味が違う。

これをものすごく恣意的に例示してみるとこのようになる。

charge :: String -> String -> Maybe Int -> Int -> ()
charge "AccountingDep" "BankName" (Just accountNumber) amount = accountingToBank(accountNumber,amount)
charge enduserName "OurService" Nothing amount = accountingToUs(enduserName,amount)
chaarge _ _ _ = undefined

chargeという関数で見た目上は共通化しているが、実際は場合分けが発生しており文脈の判断を引数の組み合わせで行わなくてはいけない。 その上プリミティブな値を用いているため過度に汎用的で各文脈におけるそれぞれのパラメーターについての制約が表現できていない。

そもそもケースAとケースBは実装する機能としてそれぞれ端的に要件を表すことができるはずで、ここではそれをしないが関係者間で意味が通じればいいのでそのままCaseAとCaseBというエイリアスを用いる。

Clean Architecture はそれらの問題に対する一つの対処法を提示しているのだが、それをふまえると以下のように改善できる。

newtype Amount = Amount { getAmount :: Int }
newtype AccountNumber = { getAccountNumber :: Int }
data OurAccountingDep = OurAccountingDep
newtype Bank = Bank { name :: String,accountNumber :: AccountNumber }
newtype EndUser = EndUser { name :: String }
newtype OurAccount = { accountNumber :: AccountNumber }

class Validate a where
  validate :: a -> Bool

instance Validate Bank where
  validate a = not . null $ name a

chargeOnCaseA :: Bank -> Amount -> ()
chargeOnCaseA bank amount = ...

chargeOnCaseB :: EndUser -> Amount -> ()
chargeOnCaseB endUser amount = ...

場合分けが必要なくなり文脈が明確になるというのと、テストデータを考えたときに専用の型を定義していることで定義域がはっきりすることがわかる。

ただ、これを面倒だと感じることもあるだろう。

面倒くささとは

Clean Architectureで言うところのEntityやValueObjectを実現するためのデータ型定義は必要だからやるのであって決して面倒なのではない。

これを面倒だと言い出すと極端な話としてスカラ値と素朴なコレクション、そして申し訳程度のクラス定義だけでよくなる。これは極端に言ってるが往々にして本質としてこのようなことが割と受け入れられている現状はあるとは思う。

面倒だというのは厳密な文脈の境界を定義することを後回しにし、神テーブルよろしく神クラスがあることでSOLID原則もクソもないごった煮のデータ構造を相手にすることを意味している。

これがメンテやCI/CDが不要というような書き捨てスクリプトだったりプロトタイプなら話は別で面倒ではあるが、複数人開発の保守管理前提大規模開発だったら必要な配慮であると思っている。

そのような状況で心強いのはコンパイラの存在であって表面的な面倒くささはコンパイラによってだいぶ軽減される。 むしろコンパイラは不確定な人間の判断と違って常に同じロジックで動くので客観的に見たときの信頼度が段違いで、そういう観点では積極的にコンパイラのある言語を採用したいところだ。

ただ、プロトタイプを作ってそのコードベースを参考にそのままプロトタイプの延長でコードを改修し続けているような場合、一度コードベースと現状のビジネス要件を照らし合わせて再評価しなくてはいけない。これってめちゃくちゃ面倒かもしれないが、必要なことで、このセクション冒頭で述べたことと同じようなことである。

そもそも実行時エラーでしか検知できないというのは精神衛生上よくないし、持続可能な開発とは言えない。

Shell scriptで大規模なwebアプリケーションを作りたいと思う人がどれだけいるだろうか。おそらく大体の人が抵抗感を感じより言語仕様が高級な技術選定をすると思う。Shell script自体優劣を主張するものではなく適材適所を考えたときに大規模なwebアプリケーションを作るにあたってより適切な道具があるはずだということを言いたい。

大規模なwebアプリケーションに加えて複数人開発でCI/CDを前提とする場合、考慮することがさらに増えるということだし、となれば機械的なチェックや定性的なチェックは自動化したいところだろう。

どうして表面的な実装コストは気にするのに持続可能性は軽視できるのか、そういう局面に立ってしまう場合はよくよく考えたいところだ。

やらない理由が面倒だからという安直なもののみの場合、明らかにその判断はおかしいと警戒したほうがいい。

結局なんなのか

実際のところシステム開発はプログラミングスキルや便利ツールでどうにかなるものではなく、間違いを起こすし病気にもなるような不安定要素の塊である人間がつくるものなので組織の問題だったり外部環境だったり様々な要素が難しさとして表出してくる。

ただ、自分は今のところシステム開発と御用聞きでおマンマを食べているので健康を維持して安眠するためにもシステム開発に関わる相対的な難しさをどうにか減らしたいと思っている。

そうした場合実は技術選定でどうにかなる部分があるんだろうなと思ってもいて、多分それはぼんやりした難しさ全体の2%くらいなんだけどまあどうにかしたいなという気持ちしかない。

そしてmartin fowlerが言っていたように我々が考える品質というものが何から成り立っているのか忘れないようにしたい。

おわり

間違えてImplementation Patterns買ってしまったのだけどそのまま読んだ

全然ほしくなかったのになぜか Kent Beck の Implementation Patterns を買ってしまったので仕方なく読んだ。

内容としては抽象化やら再利用化の落としどころとかCollection/Iterableの説明だったりメッセージパッシングに関するものだったり主にOOPの手法みたいな話であんまり新規性のある話ではなかったし読んでみてやっぱり興味惹かれなかったけど、関連する話題の書籍を読んだことがない人にとっては含蓄に富んだ良書だと思った。

さすが Kent Beck という感じで簡潔かつ読みやすいのが約束された本だったので人に薦めやすいという利点があったことだけを記録として残しておく。

モナドわかった気になりたくてRustで試してみたけどなんだかわからないまま終わった

Haskellは楽しいことが分かったが、圏論のことを1ミリも知らないので数学上の定義とプログラムの対応がよくわからないままLYHGG読んでなんかわかったようなわかんねえような状態になっていた。

要は同じcontext同士で演算できてその結果もcontextですよって話で雑に納得しておいた。あと副作用を値として扱うというのはLYHGGに書いてある範囲で納得いった気にはなれた。

普通にRust書いててResultだのOptionだのにコンビネーター生えててそこでやってるのと変わらんって感じ。だからあいつらmonadicとか言われてたのか。なるほどな。

ということであえてRustでFunctorとかApplicative FunctorとMonadを自前してみようと息巻いてみたものの、Rustには型クラスみたいな概念がないと思ったのでどうやるのかよくわからないなあと思いながら手を付けていったら実力が足りず終わっていった。

準備

何はともあれ適当に恒等関数を作っておく。

fn id<T: Clone>(x: &T) -> T {
    x.clone().to_owned()
}

それと先述した通りRustには型クラスって概念が基本的にはないはずだからうまいことhigher kindをなんとかしないといけない。ところで higher kindとは型コンストラクタを表すやつでこのようなもの。

Prelude Data.Functor> :k Functor
Functor :: (* -> *) -> Constraint

高階という名に違わず型を生み出すための型クラスのシグネチャになっていることがわかる(Constraintカインドは割愛)。

* を型パラメーターとして関連型で持っておいて Constraint にあたるものが higher kinded な trait とすればいいんだろう。

現時点で必要なのは (* -> *) なので、そのような型コンストラクタを表現する必要がある。

極力自分で考えたいが、流石に全部自前は無理だと思うので適宜先人の知恵を参考にする

trait HKT<B> {
    type A; 
    type TB;
}

関連型を使って A -> B という射を表せばいいんだろうから型パラメーターとしてBを定義しておけばいいのでこんな感じか。

TBはBをcontextでラップしなおした値として表現している。

Functor

Functorはcontextの中身の値を関数に適用してその結果を同じcontextに戻してして返すというやつ。

まずFunctor則はhackageから拝借すると

The Functor class is used for types that can be mapped over. Instances of Functor should satisfy the following laws:

fmap id == id fmap (f . g) == fmap f . fmap g

ということなので恒等射と射の合成の保存を満たさないといけない。 カリー化するの面倒だし、とりあえずはメソッド形式にするとしてfmapはこんな感じだろうか。

trait Functor<B>: HKT<B> {
  fn fmap<F: FnOnce(&Self::A) -> B>(&self, f: F) -> Self::TB;
}

実際にMaybeを実装してみるとして、HKTでtrait束縛してるからMaybeにも実装しないといけない。

#[cfg(test)]
mod functor_spec {
    use super::{HKT, id, Functor};

    #[derive(PartialEq, Eq, Debug, Clone)]
    enum Maybe<T> {
        Just(T),
        Nothing,
    }

    impl<A, B> HKT<B> for Maybe<A> {
        type A = A;
        type TB = Maybe<B>;
    }

    impl<A, B> Functor<B> for Maybe<A> {
        fn fmap<F: FnOnce(&Self::A) -> B>(&self, f: F) -> Self::TB {
            match self {
                Self::Just(x) => Maybe::Just(f(x)),
                Self::Nothing => Maybe::Nothing,
            }
        }
    }

    #[test]
    fn satisfied_id() {
        let just_2 = Maybe::Just(2);
        assert_eq!(just_2.fmap(id), id(&just_2));

        let nothing: Maybe<usize> = Maybe::Nothing;
        assert_eq!(nothing.fmap(id), id(&nothing));
    }

        #[test]
    fn satisfied_composition() {
        fn f(x: &usize) -> String {
            format!("value: {}", x)
        }

        fn g(x: &usize) -> usize {
            *x + 1
        }

        fn f_g(x: &usize) -> String {
            f(&g(x))
        }

        let just_2 = Maybe::Just(2);
        assert_eq!(
            just_2.fmap(f_g),
            just_2.fmap(g).fmap(f),
        );
    }
}

なんかできたような気がする。

Applicative Functor

自分には無理だった。 Functor の実装ができた感じがあるのでこのままApplicative Functorに挑戦しようと思ったが思わぬ型パズルに苦戦したところでむなしくなったのであきらめた。

Applicative Functorは (<*>) :: f (a -> b) -> f a -> f b と定義されるように関数を値としてもつことができるFunctorの強化版であり、型コンストラクタには Functor である必要がある。 ここで <*> にあたる操作をメソッドチェーンとして表現することになりそうな時点であきらめたくなってきているが、とりあえずできるところまでやってみよう。

知恵を参考にしつつこんな感じになった。

trait Applicative<B>: Functor<B> {
    fn pure(v: B) -> Self::TB where Self: HKT<B, A=B>;
    fn combine<F: FnOnce(&<Self as HKT<B>>::A) -> B>(&self, f: <Self as HKT<F>>::TB) -> <Self as HKT<B>>::TB
    where
        Self: HKT<F>
    ;
}

意図としてはApplicative Functorとしてラップしておく関数をHKTの型パラメーターに指定することでTBがApplicative Functorのcontextでラップされた関数であるように表現してる感じ。

でもなんかもういろいろと厳しさを感じる。

そもそも関連型のTBをってちゃんとコンパイラに伝えられるんだろうか。Maybeに実装してみよう。

impl<A, B> Applicative<B> for Maybe<A> {
    fn pure(v: B) -> <Maybe<A> as HKT<B>>::TB {
        Maybe::Just(v)
    }
    fn combine<F: FnOnce(&<Self as HKT<B>>::A) -> B>(&self, f: <Self as HKT<F>>::TB) -> Self::TB
    where
        Self: HKT<F>
    {
        match self {
            Self::Just(x) => match f {
                Maybe::Just(f_) => Maybe::Just(f_(x)),
                Maybe::Nothing => Maybe::Nothing,
            },
            Self::Nothing => Maybe::Nothing,
        }
    }
}

コンパイルしてみると

error[E0308]: mismatched types
  --> src/main.rs:53:17
   |
25 |     Nothing,
   |     ------- unit variant defined here
...
53 |                 Maybe::Nothing => Maybe::Nothing,
   |                 ^^^^^^^^^^^^^^ expected associated type, found enum `Maybe`
   |
   = note: expected associated type `<Maybe<A> as HKT<F>>::TB`
                         found enum `Maybe<_>`
   = help: consider constraining the associated type `<Maybe<A> as HKT<F>>::TB` to `Maybe<_>` or calling a method that returns `<Maybe<A> as HKT<F>>::TB`
   = note: for more information, visit https://doc.rust-lang.org/book/ch19-03-advanced-traits.html

ああ……

Monad はさらに Applicative Functor の強化版なのでこの時点で無理なので以降実装はあきらめる。

Monad

数学的な厳密な定義とかはなんもわからんので抜きにしてMonadがなんなのかといえばものすごく実用上の視点で見れば、演算をおこなう >>=シグネチャを見てみると (>>=) :: forall a b. m a -> (a -> m b) -> m b とある。

RustのResultやOptionのmapそのものではないか。

以上!

Haskellの定義上は fail があるので全く同じではないんだけど、Applicative Functorと違ってbindされる関数はcontextを意識することを強制されずに値を取ることができるという点がありそれに着目してしまうと Haskellのdo式が便利ですねという身も蓋もない話に帰結しまうのであった。

結論

やりようはあるんだろうけど適材適所がある。

Rustも楽しいけど考え方が変わるHaskellも楽しくていい教材だ。

ORMねえ、うーん、ORMかぁ

別にORM完全否定派というわけではないが、データ設計と管理をするにあたって本当にORMが適切な実装時の道具かはよく考えたほうがいい気がしている。

あと自分の触ったことあるORMなんて世の中に数多存在するもののうち0.00000001%くらいだと思うのでかなり偏った意見であるけど将来の自分に向けてのこしておく。あとこんな息巻いてる自分もOSSのORMを使わせてもらってはいるのでORMコントリビュータ各位への敬意はあっても他意は全くない。

データ設計について

Clean Architecture やら DDD やらに当てられたというわけでもないが、補強はされたなという所感ではあるものの Context Map とか Aggregate を意識したドメイン定義した時点でそれ以上の内部表現はいらなくなる。

また、(dieselなど)ORMによってはサロゲートキーを強制したりしていて、正直な話not for meな場面がないわけではない。

そもそもORMに強い依存を強いられる時点で取り回しが悪い。

また、ORMはRDBを前提としているものもあり、そのような場合バックエンドの詳細にまで依存させることがあってこれはテストのしやすさを阻害する一因にならないでもない。

とにかく、ビジネス個別に作られたわけではない汎用ツールたるORMはどれだけ自分らのシステム構成に侵食してくるかよく注意すべきだろう。

それに加えてEntity層にコアドメインとなるエンティティを定義しておけば永続化層のマッピングはどうとでもなるが、RDBだけを想定しているようなORMを使っていた場合にNoSQLにしようとか思う場合結局ORMを捨てる選択が必要になって徒労感が否めない。(実際にこういう検討が行われるかは度外視してる)

データ操作について

ORMについて一番疑問を持つのが直接クエリを書けばいいものを、なぜどんなクエリが組み立てられるかわからないクエリビルダを使ってN+1を回避するための記事がこの世にはびこってるのか不思議でならない。

ORMは適当に使えてしまうので射影をしないような実装をカジュアルにできてしまう問題もある。

要は気にしなくていいことを気にしなくてはいけない手間がいくつか増えるような感覚を覚えるのだ。

ORMがうまいことDBエンジンによって文法の差異を吸収してくるって意見も分からないではないけど、Repositoryパターンを使ってれば問題になることはないので、ORMのクエリビルダがある人にとっては「楽ができる」とか「わかりやすい」というのならばよく相談したほうがいいと思う。

個人的にはビジネス要件に関わりうるDMLをなぜブラックボックスにしていいのか理解に苦しむところではある。

DDL的な使い方について

これはRDBに限った話だけど。

テーブル定義の管理をコードで管理したいというのは理解できるけど結局ORMの乗り換えが実質無理ということなので正直うまみを全く感じない。

sqlxとかみたいなもので生DDLとしてマイグレーション履歴の管理を行うということはそんなに突飛なことではないし、個人的な話ではあるけどテーブル定義は制約含めてRDBMSから直接見たほうが明確。

なんといってもわざわざテーブル定義を書くためだけにORMのドキュメントを適宜参照しなくてはいけない場合がある時点で問題のレイヤーを無駄に増やしているように思えてならない。

ORMの利点について

因果が逆かもしれないけど、多分普通に使われてるから衝突が少なくて済む。

しめ

結局のところ自分はORM否定派だったことがわかったけどすでに動いちゃってんならまあ、デメリット理解したうえでうまく付き合うよってくらいのスタンス。 消極的なこと言っても仕方ないのでORMとうまく付き合う方法とやらを模索するしかない。

Clean Architecture 読んだ

人生を豊かにするためにかの有名なRobert C. Martinの Clean Architecture を遅ればせながら読んだ。

探せばいくらでも優良な Clean Architecture の解説記事が出回っている(ゆえにこの記事になにも新規性はない)し、自分がそこまで咀嚼できていないので詳細を語ることはしないが、なんとなく心打たれた印象の強いものについて残しておきたい。

原則

本書で何度も繰り返し出てくるのは detail, policy, boundary, abstract だった。 SOLID原則をコードレベルではなく抽象度のレベルをあげてビジネスルールにまで持ってこようという話であると理解した。

要は大昔から言われているDIPだのSRPといったメンテ前提のコードとして適用したい原則を守ろうということで目新しい情報はなかったけれども体型的に説明されていたおかげでものすごくすんなり入ってきた。

Clean Architecture がどうのというより、まともに設計しようとしたらこうなるよな。と言うものだった。 Clean Architecture はフレームワークではなくて単なる鉄則と言うだけだったのが意外だった。それゆえ難しいことは言っていない。

日頃自分がある程度のコードを書くときに意識してること、特にテストのしやすさを担保するためにDI可能なようにしておくなどが改めて書かれていた。本書が世の中に広く持て囃されている理由はこう言うプログラマが普段意識的にも無意識的にも体験していることを明文化されているところにあるんだろう。

Boundary

本書では特に boundary についての言及に重点が置かれていたと思う。 boundary がなんなのかは本書の文脈上でも全く単語の意味通りであるから割愛するとして、boundary を意識することが上記原則を適用するにあたり肝要であろうことがうかがえたし、実際これを実践するとなるとそれなりに訓練がいるものであった。

特に依存の矢印を見たときの direction については読者の注意を促すようなものが書かれていたし、まあ、そうだよなと言う感じ。

Clean Code と合わせて読む意味

正直な話、心構え以上のことが書かれているわけではないと思ったのでそう言う意味では同著者の Clean Code と立ち位置は本質的にかわらんだろうなと感じた。

Clean Architecture が概念的なことに書かれているのに対して Clean Code はコーディングよりの指南書という様相を呈する。 Clean Code で書かれていることはそれなりに多岐にわたるんだけど2冊とも同じで依存には気を使えと言っている。 それ以外にも Clean Code では命名規則がどうとかデータ構造の設計方法とかトピックはあるので2冊とも読んで無駄ということはないと思うけどまず最初は Clean Architecture を読んでからの方がいいかなと思う。 Clean Code もコーディングする上での指針としてとても参考になることが書かれている。

極めるとすると

Clean Architecture で有名な図で4層の同心円がある。 あれは一番外側の層が置換(DI)可能なものであって、より内部に行けばいくほど抽象度が高くビジネス要求を表すことになり、依存の流れを表すことになっている。 Clean Architecture を極めるとする最深部の Entity は特定のプログラミング言語に固定されないように汎用的なデータフォーマットで定義されるべきだろうと思った。 それが yaml なのか toml なのか protobuf なのか xml なのかはわからないが、一定のルールをもつ構造化された自然言語で記述されるのが理想なんだろう。 それがいわゆるユビキタス言語とそれを使って表現されるビジネス要求だったりするはずで、それをうまく体系立てようというのが DDD なのだろうか。DDD はこの文章を書いてる時点で知らないのでわからない。

言いたいことは、Clean Architecture でビジネス要求というのはシステム化されようがされまいが関係なく存在することで、むしろそれ自体を気にしては行けないということである。 Entity の表現方法に正解はあるのか、DDD の原典ぽいエリックエヴァンスのあれを読んで見たいと思う。

役に立つか

自分の携わるプロジェクトに関わるエンジニア全員に本書を読ませて共通認識を持った上でないと噛み合わないんだろうなと思うことはあった。 ただ、先に言ったように本書は基本的に原則まとめでしかないのでまあ、原則知っておこうやという感じなので概ね役に立つだろうと思う。

そんな人間がいるかわからないが原則を盲信するなみたいなことを仮に言われたとしても、原則を理解してますか?と返す刀で同じ土俵に立ってるかどうかの判断をする試験紙としては使えるだろう。

また、適当に書いて適当に動いたり動かないとか、めちゃくちゃテストしづらいみたいなコードが簡単に産み落とされるみたいな災禍はお互いのレビューにより起きづらいだろう。