この記事では、F#の重要な利点の1つを見ていきます。それは型システムを使って「不正な状態を表現不可能にする」ことです。この表現はYaron Minskyから借りました。

まず、 Contact (連絡先)型を見てみましょう。前回のリファクタリングのおかげで、かなりシンプルになっています。

type Contact = 
    {
    Name: Name;
    EmailContactInfo: EmailContactInfo;
    PostalContactInfo: PostalContactInfo;
    }

ここで、「連絡先にはメールアドレスまたは郵便住所のどちらかが必要」という簡単なビジネスルールがあるとします。この型は、ルールに適合しているでしょうか。

答えは「いいえ」です。このビジネスルールは、連絡先がメールアドレスだけの場合や、郵便住所だけの場合があることを示しています。しかし、現状の型では、連絡先が常に 両方の 情報を持つことを求めています。

解決方法は明らかに思えます。アドレスをどちらもオプション型にしてみましょう。

type Contact = 
    {
    Name: PersonalName;
    EmailContactInfo: EmailContactInfo option;
    PostalContactInfo: PostalContactInfo option;
    }

しかし、今度は行き過ぎました。この設計では、連絡先がどちらの種類のアドレスも持たない可能性があります。しかし、ビジネスルールでは、少なくともどちらかの情報が 必須 だと言っています。

では、どう解決すればいいのでしょうか。

不正な状態を表現不可能にする

ビジネスルールをよく考えると、3つの可能性があることがわかります。

  • 連絡先にメールアドレスだけがある
  • 連絡先に郵便住所だけがある
  • 連絡先にメールアドレスと郵便住所の両方がある

このように考えると、解決方法は明らかです。各可能性に対応するケースを持つ共用体型を使いましょう。

type ContactInfo = 
    | EmailOnly of EmailContactInfo
    | PostOnly of PostalContactInfo
    | EmailAndPost of EmailContactInfo * PostalContactInfo

type Contact = 
    {
    Name: Name;
    ContactInfo: ContactInfo;
    }

この設計は完璧に要件を満たしています。3つのケースを明示的に表現し、4つ目の可能性(メールアドレスも郵便住所も持たない)は許していません。

なお、「メールと郵便住所の両方」のケースでは、とりあえずタプル型を使いました。ニーズは十分満たしています。

ContactInfoの構築

では、これを実際にどう使うか見てみましょう。まず、新しい連絡先を作ります。

let contactFromEmail name emailStr = 
    let emailOpt = EmailAddress.create emailStr
    // メールが有効か無効かのケースを処理する
    match emailOpt with
    | Some email -> 
        let emailContactInfo = 
            {EmailAddress=email; IsEmailVerified=false}
        let contactInfo = EmailOnly emailContactInfo 
        Some {Name=name; ContactInfo=contactInfo}
    | None -> None

let name = {FirstName = "A"; MiddleInitial=None; LastName="Smith"}
let contactOpt = contactFromEmail name "abc@example.com"

このコードでは、名前とメールアドレスを渡して新しい連絡先を作る簡単なヘルパー関数 contactFromEmail を作りました。 ただし、メールアドレスが有効でない可能性があるので、関数は両方のケースを処理する必要があります。これは Contact option を返すことで行っています( Contact をそのまま返すのは適切ではありません)。

ContactInfoの更新

既存の ContactInfo に郵便住所を追加する必要がある場合、3つの可能性すべてを処理しなければなりません。

  • 連絡先に以前はメールアドレスしか存在しなかった場合、現在はメールアドレスと郵便住所の両方があることになります。そのため、 EmailAndPost ケースを使って連絡先を返します。
  • 連絡先に以前は郵便住所しか存在しなかった場合、 PostOnly ケースを使って連絡先を返し、既存の連絡先を置き換えます。
  • 連絡先に以前はメールアドレスと郵便住所の両方が存在していた場合、 EmailAndPost ケースを使って連絡先を返し、既存の連絡先を置き換えます。

以下は、郵便住所を更新するヘルパーメソッドです。各ケースを明示的に処理していることがわかります。

let updatePostalAddress contact newPostalAddress = 
    let {Name=name; ContactInfo=contactInfo} = contact
    let newContactInfo =
        match contactInfo with
        | EmailOnly email ->
            EmailAndPost (email,newPostalAddress) 
        | PostOnly _ -> // 既存のアドレスを無視する
            PostOnly newPostalAddress 
        | EmailAndPost (email,_) -> // 既存のアドレスを無視する
            EmailAndPost (email,newPostalAddress) 
    // 新しい連絡先を作る
    {Name=name; ContactInfo=newContactInfo}

そして、コードの使用例がこちらです。

let contact = contactOpt.Value   // option.Valueの使用に関する警告は以下を参照
let newPostalAddress = 
    let state = StateCode.create "CA"
    let zip = ZipCode.create "97210"
    {   
        Address = 
            {
            Address1= "123 Main";
            Address2="";
            City="Beverly Hills";
            State=state.Value; // option.Valueの使用に関する警告は以下を参照
            Zip=zip.Value;     // option.Valueの使用に関する警告は以下を参照
            }; 
        IsAddressValid=false
    }
let newContact = updatePostalAddress contact newPostalAddress

警告。このコードでは、オプションの内容を取り出すのに option.Value を使っています。 インタラクティブな操作中には問題ありませんが、本番コードではとても悪い習慣です。常にマッチングを使って、オプション型の両方のケースを処理するようにしましょう。

なぜこんな複雑な型を作る必要があるのか

この時点で、必要以上に複雑にしたのではないかと思うかもしれません。これに対しては、次のように答えたいと思います。

まず、ビジネスロジック 自体 が複雑なのです。これを避ける簡単な方法はありません。もしコードがこれほど複雑でないなら、すべてのケースを適切に処理していない可能性があります。

次に、ロジックを型で表現すれば、自動的に自己文書化されます。以下の共用体ケースを見るだけで、ビジネスルールが何かすぐにわかります。他のコードを分析するのに時間を費やす必要はありません。

type ContactInfo = 
    | EmailOnly of EmailContactInfo
    | PostOnly of PostalContactInfo
    | EmailAndPost of EmailContactInfo * PostalContactInfo

最後に、ロジックを型で表現すれば、ビジネスルールの変更はすぐに破壊的な変更を引き起こします。これは一般的に良いことです。

次の投稿では、最後の点についてさらに深く掘り下げます。型を使ってビジネスロジックを表現しようとすると、突然、ドメインに関する全く新しい洞察を得られることがあります。

results matching ""

    No results matching ""