61
23

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

DDDをHaskellで考える 業務ロジックとシステムロジック

Posted at

DDD初心者が拙いHaskellを使って色々考える試みです。

はじめに

DDDは会社で見よう見まねで1年実践したけど、DDDの勉強はほぼ一切していないくらい。
Haskellは入門書をなんとか通読できたくらい、けど普段書いてないので全然馴染んでないくらい。

それくらいの奴が1年経ってやっと「ちょっとまじめに考えてみるかぁ」って思って考えたことをまとめる記事です。

記事の主軸はDDDなので、Haskellは読めなくても雰囲気だけ察してもらえると良いな、と思います。

また、Haskellを選んだ理由はこの記事を通して伝えたいと思います。

Haskellについて

この記事を読むに当たり最低限必要なHaskellの文法を記載します。
僕がDDDを捉えるに当たり用いた考え方が一番含まれる点なので、まとめてみたいと思います。

参照透過性と副作用

Haskellには「参照透過性が常に保たれる処理」と「副作用の存在する処理」を別のものとしています。

参照透過性

一言で言うと「同じ引数の場合は必ず同じ戻り値が得られる」処理のことです。

Javaの例で示すと以下の様な感じです。

public int add2(int x) {
    return x + 2;
}

これは引数を5で実行したら何度実行しても、何時に実行しても、どんなOSで実行しても、常に7が返ります。

副作用

対して副作用は「常に同じ結果が返るとは限らない」とか「他の処理に影響を与える(受ける)」処理のことです。

例えば以下の様な「状況次第では失敗するかも知れない」処理や、

public String fileLines(String path) {
    return new File(path).getText()
}
// groovy です... java でFileIOがあんなに面倒だとは知らなかった...

「何らかの値次第で結果が変わり得る」処理は副作用があると言われます。

public void setShift(int x) {
    shift = x;
}

public int add2AndShift(int x) {
    return x + 2 + shift;
}

関数

参照透過性を保証する処理をHaskellでは関数と言い、以下の様に定義します。

add2 :: Int -> Int
add2 x = x + 2

1行目は関数の型定義(省略可能ですがこの記事では省略しません)、2行目が本文です。
型の部分はとても乱暴に説明すると、->の一番右側が「戻り値」、それ以外が「引数」となります。

複数の引数がある場合は以下の様になります。

mkLang :: String -> Int -> String
mkLang lang version = lang ++ show version -- show は所謂 toString

これは「引数がStringInt」で「戻りがString」であると示します。

利用する場合は以下の様に引数を半角スペース区切りで渡し、let xxx =の形で結果を受け取ります。

let result = add2 5 -- 7
let lang = mkLang "java" 8 -- java8

アクション

対して副作用のある処理をHaskellではアクションと言い、以下の様に定義します。

fileLines :: String -> IO String
fileLines path = do
    readFile path

1行目の型定義の戻りにIOとついているのが一番の特徴です。(IOの他にもありますが記事と関係ないため割愛します)
標準ライブラリのreadFileの戻りがIO Stringのため、それを最後に行うfileLinesの戻りもIO Stringとなっています。

アクションの結果の値を取り出すにはxxx <-の形になります。

lines <- fileLines "/path/to/foo/bar.txt"

関数からアクションは呼べない

一番のポイントです。

アクションはアクションも関数も呼べますが、関数はアクションを呼べません。
それにより関数には失敗するかも知れない処理が混ざる余地がなく、関数から関数を呼んでも参照透過性が保たれる様になっています。

業務ロジックとシステムロジック

DDDについて考えた時、一番最初に次の様に感じました。
「とある要件を満たすプログラムは、大別して2つのロジックから成る」、と。

それらを便宜上「業務ロジック」と「システムロジック」と呼び、考えの根拠を示したいと思います。

業務ロジック

業務ロジックとは「仕様に起因して存在する」処理で、「システム都合には依存しない値の変換や条件判断」とします。

例えば「〜〜の場合は購入可能」とか「〜〜を満たせば解約出来る」とか「○と△と□を基に決済情報を組み立てる」とか、その様な処理を指すと考えました。

これ、全部関数なのでは?
と思ったのが今回の試みの発端です。

システムロジック

では対するシステムロジックはどうでしょうか。こちらはアクションなのでは?と考えるのは自然だと思います。

こちらはDBアクセスやHttp通信等の「システムを実現するために必要な」処理だと考えました。
そして事実、それらの処理はIOを用いなければ実装することは不可能です。

例えば?

以下の様な要求があったとします。

「有効な決済情報を登録しているユーザを探し、そのユーザにメールを送信する。件名はユーザ名と契約日時を結合したものである」

どの様な決済情報なら有効なのかはそのプロダクトの仕様に基づき決まりますし、件名もプロダクトの仕様によってその様な結合ルールになっているはずです。
対してユーザを探す部分とメールを送信する部分の実装は別に仕様起因ではなく、外部システムは仕様を実現するためのただの手段だと考えることが出来ます。

仮説

全ての業務ロジックは関数であり、そして関数で書けない他全ての処理はシステムロジックなのではないか、という仮説を持ちました。
関数で書けるのであればそれはドメイン層に書くべきであり、ドメイン層ではないところについ書いてしまう関数は実は業務ロジックなのでドメイン層に書くのが適切である。

という仮説です。

ドメイン層に業務ロジックを集める

この仮説を検証するために制約を考えてみました。

それは「ドメイン層は関数のみで、それ以外の層はアクションのみで実現する。例えばサービス層によく現れるprivateメソッドの様な処理も、関数で実現できるなら(それは業務ロジックだから)ドメイン層に書く」というルールです。

この仮説を検証するため、今小規模な仕様をお題としてHaskellで実装していますが、レイヤー設計や単体テスト等も気になっており、終えて経験値と納得を得るにはもう少しかかりそうです。

なのでこの記事の残りではありがちなJavaのコードをHaskellに置き換える過程を示すことで、アクションから関数を切り出す様を示してみたいと思います。

また、最初からHaskellで考え実装していたらどうかも示せればと思います。

例題

都合の良い適当なお題を考え、あえてJavaで少し悪いコードを書きます。
その次にそれをHaskellに置き換え、少しずつ改善していくことにします。

お題

ユーザが商品とオプションを購入する

  1. ユーザIDと商品とオプションを受ける
  • ユーザIDに該当するユーザが存在しない場合はその旨を返す
  • 商品とオプションの組み合わせが正しくなければその旨を返す
  • ユーザIDに対して商品とオプションを紐づけて永続化する
  • 永続化するとライセンスキーが発行される
  • 件名を「ユーザID + 商品名 + オプション名」としてメールを送信する
  • 受付に成功した場合はライセンスキーを、失敗した場合は理由を返す

商品は以下

  • PersonalComputer
  • Keyboard

オプションは以下

  • Backup
  • Replacement

制約は以下

  • オプションは任意項目とする
  • BackupPersonalComputerに対してのみ、ReplacementKeyboardに対してのみ付加可

Java製

Javaを悪く言うためでは無く、ありがちな悪いコードとして掲載する。

public class Main {
    public static void main(String[] args) {
        apply("user-id-123", Item.PersonalComputer, Optional.empty());

        apply("user-id-123", Item.PersonalComputer, Optional.of(Option.Backup));

        apply("user-id-123", Item.PersonalComputer, Optional.of(Option.Replacement));

        apply("user-id-123", Item.Keyboard, Optional.of(Option.Backup));
    }

    public static String apply(String userId, Item item, Optional<Option> option) {
        if (findUser(userId) == null) {
            return "ユーザが見つかりません";
        } else if (item == Item.PersonalComputer && option == Optional.of(Option.Replacement)) {
            return "PCに交換オプションは付加出来ません";
        } else if (item == Item.Keyboard && option == Optional.of(Option.Backup)) {
            return "キーボードにバックアップオプションは付加出来ません";
        } else {
            String license = save(userId, item, option);
            sendMail(userId, item, option);
            return license;
        }
    }

    public static String findUser(String userId) {
        return "John";
    }

    public static String save(String userId, Item item, Optional<Option> option) {
        return "license-key-123";
    }

    public static void sendMail(String userId, Item item, Optional<Option> option) {
        System.out.println(userId + " " + item.name() + " " + option.map(Enum::name).orElse(""));
    }
}

良くある形だと思う。
この状態だとまだパッと見ても業務ロジックとシステムロジックが混ざっていると言う事自体がわかりづらい。
ついでにまだあえてあんまりクラスは用意していないので、Stringがちらほら見える。

Haskellに単純に置き換える

出来るだけ同じようにそのまま置き換えてみる

import Data.Maybe

data Item = PersonalComputer | Keyboard deriving (Show, Eq) -- Enum定義

data Option = Backup | Replacement deriving (Show, Eq)

apply :: String -> Item -> Maybe Option -> IO String -- Maybe は Java で言う Optional<>
apply userId item option = do
    user <- findUser userId

    if user == ""
        then return "ユーザが見つかりません"
        else if item == PersonalComputer && option == Just Replacement
            then return "PCに交換オプションは付加出来ません"
            else if item == Keyboard && option == Just Backup
                then return "キーボードにバックアップオプションは付加出来ません"
                else do
                    license <- save userId item option
                    sendMail userId item option
                    return license

findUser :: String -> IO String
findUser userId = return "John"

save :: String -> Item -> Maybe Option -> IO String
save userId item option = return "license-key-123"

sendMail :: String -> Item -> Maybe Option -> IO ()
sendMail userId item option = do
    putStrLn (userId ++ " " ++ show item ++ " " ++ maybe "" show option) -- イメージ的には userId + item.toString + if option.none "" else option.get.toString

main = do
    r1 <- apply "user-id-123" PersonalComputer Nothing
    putStrLn r1

    r2 <- apply "user-id-123" PersonalComputer (Just Backup)
    putStrLn r2

    r3 <- apply "user-id-123" PersonalComputer (Just Replacement)
    putStrLn r3

    r4 <- apply "user-id-123" Keyboard (Just Backup)
    putStrLn r4

この時点でJavaの例と大きく違う点は、IOが付いているなら副作用があるということがわかる点です。
しかし全てにIOが付いています。それでは業務ロジックが存在しないということになってしまうので、少しずつ関数とアクションを切り分けていきましょう。

とりあえずStringを無くす

でもその前に、折角静的言語なのでStringは全てラップして新しい型を作ることにします。

data UserId = UserId { value :: String } deriving Show

data LicenseKey = LicenseKey { key :: String } deriving Show

置き換えてみます。

before
apply :: String -> Item -> Maybe Option -> IO String

findUser :: String -> IO String

save :: String -> Item -> Maybe Option -> IO String

sendMail :: String -> Item -> Maybe Option -> IO ()
after
apply :: UserId -> Item -> Maybe Option -> IO String

findUser :: UserId -> IO String

save :: UserId -> Item -> Maybe Option -> IO LicenseKey

sendMail :: UserId -> Item -> Maybe Option -> IO ()

こうして見るとUserIdは全てに現れていますが、Userと言うものは必要ないみたいです。
findUserBoolを返してくれれば十分な様なので変更します。(ここに付いては最後にまた取り上げます)

after
isExist :: UserId -> IO Bool

さて唯一残ったapplyの戻りのStringですが、ライセンスキーか失敗理由を返さなければ成りません。これは後ほど対応します。

sendMailの件名組み立てに注目

before
sendMail :: UserId -> Item -> Maybe Option -> IO ()
sendMail userId item option = do
    putStrLn ((show userId) ++ " " ++ show item ++ " " ++ maybe "" show option)

というのがあります。
これは「与えられた情報を組み立て」て、「メール送信を実行する」ことを行います。

ここでは手抜きでputStrの標準出力でメール送信を代用しますが、文字出力(=ファイル入出力)は副作用があるのでsendMailの戻りもIOになっています。
ですが件名組み立てはどうでしょうか?別に処理を切り出してみます。

after
sendMail :: UserId -> Item -> Maybe Option -> IO ()
sendMail userId item option = do
    putStrLn (mailTitle userId item option)

mailTitle :: UserId -> Item -> Maybe Option -> String
mailTitle userId item option = (show userId) ++ " " ++ show item ++ " " ++ maybe "" show option

初めて関数が現れました。
折角なのでmailTitleの戻りもStringではなくてMailTitleとでも言う新しい型を作ります。

after2
data MailTitle = MailTitle { title :: String } deriving Show

mailTitle :: UserId -> Item -> Maybe Option -> MailTitle
mailTitle userId item option = MailTitle ((show userId) ++ " " ++ show item ++ " " ++ maybe "" show option)

アクションで関数を用いるのではなく、関数の結果をアクションに渡す

sendMailで件名組み立てとメール送信の両方を行うのをやめます。

sendMail :: MailTitle -> IO ()
sendMail title = do
    print title

これでsendMailMailTitleを受け取り送信するだけになり、組み立てロジックは消えてなくなりました。
mailTitleは関数で出来た業務ロジックであり、sendMailはアクションで出来たただの外部システム(例えばsendmailコマンド)の利用手段になりました。

商品とコードの組み合わせチェックに注目する

次は行数やインデントのせいかとても目立つ組み合わせチェックの部分に注目します。

ユーザのチェックや正常時の後続処理等と絡み合っていますが、ここは思い切って切り出してみるために作る処理の型から考えます。

checkCombination :: Item -> Maybe Option -> ???

戻りはどうすれば良いでしょうか。
Stringでも実装できそうですが、やはり専用の型を作ってみたいと思います。

data InvalidReason = PersonalComputerAndReplacement | KeyboardAndBackup deriving (Show, Eq)

そして不正ではない場合もあるので、ここはMaybeを用いてみます。
この条件判断は外部の値や環境に依存しない、仕様から生まれた処理なので、当然関数です。

checkCombination :: Item -> Maybe Option -> Maybe InvalidReason

Haskellらしくcase ofを用いてみました。

checkCombination :: Item -> Maybe Option -> Maybe InvalidReason
checkCombination item option = case (item, option) of
    (PersonalComputer, Just Replacement) -> Just PersonalComputerAndReplacement
    (Keyboard,         Just Backup)      -> Just KeyboardAndBackup
    _                                    -> Nothing

applyアクションにどう組み込むかはまだわかりませんが、適切な関数を作ることを優先してこう実装してみました。
このcheckCombinationとしての分離により、商品やオプションの追加がapplyに影響しなくなりました。

それはつまり仕様変更が関数の改修だけで対応が出来るということです。
仕様変更がシステムロジックには一切の影響を与えないと言う事は、業務ロジックが関数にうまく分離できているということです。

applyの戻りの型はどうするのか

最後に、放置していたapplyに注目します。

apply :: UserId -> Item -> Maybe Option -> IO String

ライセンスも不正理由もStringだったため上手く動作していましたが、LicenseKeyInvalidReasonという専用クラスに別れてしまいました。
当然共通のInterfaceを用意すれば動きそうではありますが、そもそもこの2つは全く別の概念なので都合良くString、もしくは適当なInterfaceに突っ込んでしまうのは不適切です。

ここはEitherというものを用いてみたいと思います。

Either

Maybeと同じように、具体的な型と組み合わせて新たな型を表現する感じです。
EitherMaybeとは異なり、その具体的な型を2つ用います。

一般にEither Left Rightと言い、Leftには失敗時の型を、Rightには成功時の型を書き、どちらか片方を詰めて使います。
Right正しいがかかっているそうです。Either L RはHaskellに限らず他の言語でも同じです)

例えば以下の様に使います。

half :: Int -> Either String Int  -- 失敗時は文字列(でメッセージ)、成功時は演算結果を返す
half x = case odd x of
    True -> Left "not even!"      -- Left String で失敗を表現
    False -> Right (div x 2)      -- Right Int で成功を表現
print (half 3) -- Left "not even!"
print (half 4) -- Right 2

StringIntが返る訳ではなく、half 3half 4の結果のどちらもがEither String Int型なので、型の不整合は起きません。

applyの戻りをEitherにする

さっそく書いてみます。

apply :: UserId -> Item -> Maybe Option -> IO (Either InvalidReason LicenseKey)

少し長いですが、副作用を内包することと、正常時はLicenseKeyを、失敗時はInvalidReasonを返すことが型だけでわかるようになりました。
本文の実装はcheckCombinationと合う様によしなに実装し直します。

上記を踏まえて書き直す

最終的には以下の様になりました。

import Data.Maybe

data Item = PersonalComputer | Keyboard deriving (Show, Eq)

data Option = Backup | Replacement deriving (Show, Eq)

data UserId = UserId { value :: String } deriving (Show, Eq)

data LicenseKey = LicenseKey { key :: String } deriving Show

data InvalidReason = NoUser | PersonalComputerAndReplacement | KeyboardAndBackup deriving (Show, Eq)

data MailTitle = MailTitle { title :: String } deriving Show

isExist :: UserId -> IO Bool
isExist userId = return True

checkCombination :: Item -> Maybe Option -> Maybe InvalidReason
checkCombination item option = case (item, option) of
    (PersonalComputer, Just Replacement) -> Just PersonalComputerAndReplacement
    (Keyboard,         Just Backup)      -> Just KeyboardAndBackup
    _                                    -> Nothing

save :: UserId -> Item -> Maybe Option -> IO LicenseKey
save userId item option = return (LicenseKey "license-key-123")

sendMail :: MailTitle -> IO ()
sendMail title = do
    print title

mailTitle :: UserId -> Item -> Maybe Option -> MailTitle
mailTitle userId item option = MailTitle ((show userId) ++ " " ++ show item ++ " " ++ maybe "" show option)

apply :: UserId -> Item -> Maybe Option -> IO (Either InvalidReason LicenseKey)
apply userId item option = do
    b <- isExist userId

    -- ユーザが存在しなければ NoUser, 存在すれば組み合わせチェック次第
    let invalidReason = if b then (checkCombination item option) else Just NoUser

    case invalidReason of
        (Just reason) -> do                          -- 不正理由がある場合は
            return (Left reason)                     -- Left 理由
        Nothing -> do                                -- 不正理由がない場合は
            license <- save userId item option       -- 後続処理を行い
            sendMail (mailTitle userId item option)
            return (Right license)                   -- Right ライセンス

main = do
    let userId = UserId "user-id-123"

    r1 <- apply userId PersonalComputer Nothing
    print r1

    r2 <- apply userId PersonalComputer (Just Backup)
    print r2

    r3 <- apply userId PersonalComputer (Just Replacement)
    print r3

    r4 <- apply userId Keyboard (Just Backup)
    print r4

単純な行数は増えましたが、何カ所かは適切に関数とアクションが切り分けられました。
また、型定義の部分だけ抜粋すると以下の様になります。

isExist :: UserId -> IO Bool

checkCombination :: Item -> Maybe Option -> Maybe InvalidReason

save :: UserId -> Item -> Maybe Option -> IO LicenseKey

sendMail :: MailTitle -> IO ()

mailTitle :: UserId -> Item -> Maybe Option -> MailTitle

apply :: UserId -> Item -> Maybe Option -> IO (Either InvalidReason LicenseKey)

何に基づき何を返すかとか、結果があったりなかったりする(Maybe)とか、成功時と失敗時で違う型を返したい(Either)とか、副作用がある(IO)とか、型からわかる情報が大分増えたと思います。

実装はひとまずここまでとします。

始めからHaskellならどうだったか

先述の「小規模な仕様をお題としてHaskellで実装している」ですが、これはドメイン層を最初に書いて、ドメイン層が書き上がるまで他の層を一切書きませんでした。
そして単体テストやREPLを用いてドメイン層の開発だけを行い、他の層を書くことなく実装を終わらせました。
REPLも実はHaskellでドメイン層を書く際の非常に強力な利点です。単体テストやREPLについてはまた別の機会にしたいと思います。)

型だけ用意して関数の型定義までを行い、本文は所謂return null;の様な状態で関数定義だけを進めるというやりかたも出来ます。
これはチーム開発時に分担作業がしやすくなる等のメリットがあります。「設計はトップダウン、実装はボトムアップ」という考え方がピタリと合うと思いました。

いずれにせよHaskellであれば業務ロジックの中にシステムロジックを混ぜることは不可能であるし、
システムロジックの中で呼んでいる処理が関数であればドメイン層に切り出すべきだと言う事は自明であるので、
今回の様に「見直してみたら混じっていた」という状況がそもそも起きません。

純粋なドメイン層を作るという点に関しては極めて適した言語だと実感しました。

なぜHaskellを選んだか

整理すると以下の様な理由になります。

  • 関数とアクションは定義が別であること
  • その関数とアクションがぴったり業務ロジックとシステムロジックに相当すると思ったこと
  • 参照透過性を保つために関数からはアクションが呼べないため、業務ロジックにシステムロジックが混入することは絶対にないこと
  • 型の表現力が豊富なこと

現在レイヤー設計について色々と考えていますが、その根底にある発想は「業務ロジックは関数である」ことです。
ドメイン層やサービス層等にシンプルかつ強力な制限事項を設けることで、Haskellであればある程度は適切なDDDが自然と行えることを検証中です。

また必ずしも業務をHaskellで行わなくとも、関数とアクションについての感覚は他の言語で実装する際にも役に立つと実感しています。

反省点

isExistsave、つまりDBアクセスに関連するところの型定義を見ると改善の余地がありそうです。
もうひとつはapplyの中のコメントでの補足を必要としてしまっているNoUserの下りです。

参照系

参照系については今現在とても悩んでいるところですが、DBアクセスは参照する条件を詰めた型を渡し、レコードを適切な型にしてMaybeListで返すだけに留めるべきだと感じています。

例えば今回は主キー検索でしたが、「IDとステータスと申込日時をキーとして参照する」様な複雑な状況になれば話は別です。
ステータス判定も日時判定もまず大抵の場合は仕様起因です。つまり業務ロジックです。
それをひとつのアクションに押し込めてしまうのには大きな違和感を覚えます。

例えば以下の様にするべきでしょうか?

data XxxFindCond = XxxFindCond { userId :: UserId, status :: Status, appliedDate :: AppliedDate }

find :: XxxFindCond -> Maybe User

XxxFindCondは例えば「解約時における参照条件」とでも言う、仕様起因で存在するまとまりです。
この型の実装を見ることで「解約にはステータスと申込日時も参照条件に関連する」という仕様を知ることが出来ます。

これをfind :: UserId -> Maybe Userとしてしまい、例えばSQLでwhere status == 50 and date ...とハードコーディングしてしまうと、業務ロジックがシステムロジックに完全に隠れてしまいます。

更新系

更新系は今回の例が悪かったのだと思いますが、保存ついでにLicenseKeyを発行するのは不適切でした。

save :: UserId -> Item -> Maybe Option -> IO Xxx

allocate :: Xxx -> LicenseKey

の様に、保存した結果を用いて得られると言うことを関数で表現するべきだったと思います。
例えばItemも組み立てに必要であればallocate :: Xxx -> Item -> LicenseKeyとなり、saveで全て済ませてしまうより業務ロジックが見えやすくなります。

アクションからいかに業務ロジックを切り離すかは悩んでいる途中ですので、これ以上の詳細はいずれ別にまとめたいと思います。

NoUserの下り

コメントが必要である以前に、明らかに仕様起因であるロジックがシステムロジック(IOを返すapply)の中に埋もれています。

ここは先述の通り、やはりisExistではなくfindMaybe Userを手に入れ、それも一緒に使って判断する関数を書くべきでした。

checkApplicable :: Maybe User -> Item -> Maybe Option -> Maybe InvalidReason
checkApplicable user item option = case user of
    (Just _) -> checkCombination item option -- checkCombination はそのまま使う
    Nothing  -> NoUser
apply
user <- find userId

let invalidReason = checkApplicable user item option -- if が消えた

これで「申込可能か」という処理は型を見るだけで「Maybe User, Item, Maybe Optionで判断できる業務ロジック」である、と示すことが出来たはずでした。

isExistを作ったときの経験不足による判断ミスに引きずられた形になってしまいました。

今後

1年会社で見よう見まねでやりましたが、色々な思いがありずっとDDDというもの自体に対するモチベーションがありませんでした。
ですから実はここ2週間ほどで初めて「DDD始めた」という感覚になりました。

気になることは沢山あるのでもう少し考えるつもりです。
IdentityとEntityについて、DBアクセスの型定義について(反省点の部のこと)、レイヤー設計について、Aggregateについて、単体テストについて、ドメインモデルについて等々。

その際はまたまとめて見たいと思っていますが、根底にある考えは今回のHaskellを選んだ理由の部にあり、それを伝えてみようと思い長い記事になってしまいました。

何かの参考になれば幸い。
もしも指摘がもらえれば、参考にもモチベーションの向上にもなる。
2-3年して見返したら面白いかも知れないな。

なんて思い、長々とまとめてみました。

以上です。

おまけ

気を抜いて楽な感じでいくつかおまけを

他の言語で副作用を気にする

例えばJavaでやりがちな以下のコード

public class Bar {
    private String key;
    private int n;
    private Status status;

    private String message;

    public Bar(String key, int n, Status status) {
        this.key = key;
        this.n = n;
        this.status = status;

        this.message = createMessage();
    }

    private String createMessage() {
        if (status == Status.OK) {
            return key + n;
        } else {
            return key;
        }
    }
}

こういう自身のフィールドに依存する処理は僕は好きじゃあない。OOPのべき論は知らんす。好き嫌いです。

    public Bar(String key, int n, Status status) {
        this.key = key;
        this.n = n;
        this.status = status;

        this.message = createMessage(key, n, status);
    }

    private static String createMessage(String key, int n, Status status) {
        if (status == Status.OK) {
            return key + n;
        } else {
            return key;
        }
    }

statisにして中で使う値は外から全て渡したい
createMessageが別のタイミングで再利用された場合に、(この例で言えば)自身のフィールドの値が変わっていない保証がないから
同じcreateMessageでも、前後や内部を気にしないと同じ結果かわからないなんて、むしろ怖くて再利用したくなくなってしまう

他の処理結果を内包して使っていたり、利用順に注意が必要だったりすると安心できない、ので、僕はstatisが好き

(ちなみに、SpringFrameworkServiceRepositoryってシングルトンだし、こういうstaticにしたくなるロジックも普段結構あるし、
 もしかしてこのstaticってのが業務ロジックでシングルトンのやつがシステムロジックなんじゃね?
 というかじゃあむしろ状態排除そんなにしたいならOOPじゃあなくてもDDD出来るんじゃね?って思ったのが始まりの様なもの)

型の表現力

選んだ理由にて上げた表現力ですが、今回出てきていないことがいくつもあるので紹介します

List

[a]を使うだけです、簡単です

find :: UserName -> [User]

Tuple

当然あります
(a, b)を使うだけです、簡単です

authentication :: (UserId, Password) -> AuthResult

関数渡し

当たり前すぎますがあります
(a -> b)を使うだけです

map :: (a -> b) -> [a] -> [b]

標準ライブラリのmap
abにする方法とaを複数与えると複数のbになる。
abは何に読み替えても良い。Javaのジェネリクスの様な感じ。)

MaybeのTupleがListでEitherだしIO

パルスのファルシのルシがコクーンでパージみたいになってきたw

長いけどJavaよりずっと短いです
引数と戻りが格段に把握しやすい

findItems :: UserId -> IO (Either ErrorMessage [(Item, Maybe Option)])

ユーザの商品をあるかも知れないオプションとセットにしてリストで得る(副作用あり)、って一目でわかる

public Either<ErrorMessage, List<Tuple<Item, Optional<Option>>>> findItem(UserId userId) {

ListOptionalimportして、TupleEitherは自作するなりjavaslangを使うなりして用意しないと... (´Д`;)ハァ...

型エイリアス

新しい型を作るのではなく、ラベルを付ける様にエイリアスを作れます

type Contracted = (Item, Maybe Option)

findItems :: UserId -> IO (Either ErrorMessage [Contracted])

もっとやってもおk

type Contracteds = [Contracted]

findItems :: UserId -> IO (Either ErrorMessage Contracteds)

すっきりした!
(Tupleを使うか型を作るかについてはまたいつか)

丸括弧を減らす

実はあえて()を使っていたけど、$という演算の優先度を下げる方法がある

これを使うと例えば

これが
sendMail (mailTitle userId item option)
こうなる
sendMail $ mailTitle userId item option

どちらも書かないとsendMailの第一引数がmailTitleと解釈されてしまうけど、
mailTitle以降を先に評価して、その結果をsendMailに渡したいってこと、よくあるよね

これが
return (Right license)
こうなったり
return $ Right license
これも
r3 <- apply userId PersonalComputer (Just Replacement)
こうなったりする
r3 <- apply userId PersonalComputer $ Just Replacement

一番右に閉じ括弧がある開き括弧だと思えばおk
これを使うと行末が))))みたいなことにならなくて済む

おまけは以上!ノシ

61
23
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
61
23

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?