5.1 高度な例:銀行口座

この節では、次に示す最初の単純な銀行口座の定義の、改良、デバッグ、特化を通してオブジェクトと継承のほとんどの側面を明らかにしていきます( 3 章「Caml におけるオブジェクト の最後で定義した Euro モジュールを再利用します)。

#let euro = new Euro.c;;
val euro : float -> Euro.c = <fun>

#let zero = euro 0.;;
val zero : Euro.c = <obj>

#let neg x = x#times (-1.);;
val neg : < times : float -> 'a; .. > -> 'a = <fun>

#class account =
   object 
     val mutable balance = zero
     method balance = balance
     method deposit x = balance <- balance   plus x
     method withdraw x =
       if x leq balance then (balance <- balance   plus (neg x); x) else zero
   end;;
class account :
  object
    val mutable balance : Euro.c
    method balance : Euro.c
    method deposit : Euro.c -> unit
    method withdraw : Euro.c -> Euro.c
  end

#let c = new account in c # deposit (euro 100.); c # withdraw (euro 50.);;
- : Euro.c = <obj>
    

この定義を、利子を計算するメソッドで改良します。

#class account_with_interests =
   object (self)
     inherit account
     method private interest = self # deposit (self # balance # times 0.03)
   end;;
class account_with_interests :
  object
    val mutable balance : Euro.c
    method balance : Euro.c
    method deposit : Euro.c -> unit
    method private interest : unit
    method withdraw : Euro.c -> Euro.c
  end
    

interest メソッドを private にしたのは、このメソッドは明らかに外部から自由に呼ばれるべきものではないからです。 ここで、このメソッドは口座の月次もしくは年次の口座更新を扱う子クラスからのみアクセス可能とします。

現在の定義のバグを直しましょう。 deposit メソッドは負の金額を預金することでお金を引き出すことができてしまいます。 これを直接修正すると次のようになります。

#class safe_account =
   object
     inherit account
     method deposit x = if zero leq x then balance <- balance plus x
   end;;
class safe_account :
  object
    val mutable balance : Euro.c
    method balance : Euro.c
    method deposit : Euro.c -> unit
    method withdraw : Euro.c -> Euro.c
  end
    

しかしながら、このバグは次の定義でより安全に修正できるかもしれません:

#class safe_account =
   object
     inherit account as unsafe
     method deposit x =
       if zero leq x then unsafe   deposit x
       else raise (Invalid_argument "deposit")
   end;;
class safe_account :
  object
    val mutable balance : Euro.c
    method balance : Euro.c
    method deposit : Euro.c -> unit
    method withdraw : Euro.c -> Euro.c
  end
    

特に、この修正では deposit メソッドの実装に関する知識が必要ありません。

操作の履歴を保持するため、クラスを mutable なフィールド historyprivate なメソッド trace で拡張し、操作をログに追加するようにします。 トレースされるメソッドは再定義されます。

#type 'a operation = Deposit of 'a | Retrieval of 'a;;
type 'a operation = Deposit of 'a | Retrieval of 'a

#class account_with_history =
   object (self) 
     inherit safe_account as super  
     val mutable history = []
     method private trace x = history <- x :: history
     method deposit x = self trace (Deposit x);  super deposit x
     method withdraw x = self trace (Retrieval x); super withdraw x
     method history = List.rev history
   end;;
class account_with_history :
  object
    val mutable balance : Euro.c
    val mutable history : Euro.c operation list
    method balance : Euro.c
    method deposit : Euro.c -> unit
    method history : Euro.c operation list
    method private trace : Euro.c operation -> unit
    method withdraw : Euro.c -> Euro.c
  end
    

口座を開くとともに最初の金額を入金しておきたいと思う人もいるかもしれません。 最初の実装ではこの要求を扱っていませんが、これは初期化子を使うことで達成できます。

#class account_with_deposit x =
   object 
     inherit account_with_history 
     initializer balance <- x 
   end;;
class account_with_deposit :
  Euro.c ->
  object
    val mutable balance : Euro.c
    val mutable history : Euro.c operation list
    method balance : Euro.c
    method deposit : Euro.c -> unit
    method history : Euro.c operation list
    method private trace : Euro.c operation -> unit
    method withdraw : Euro.c -> Euro.c
  end
    

こうした方がよいでしょう。

#class account_with_deposit x =
   object (self)
     inherit account_with_history 
     initializer self deposit x
   end;;
class account_with_deposit :
  Euro.c ->
  object
    val mutable balance : Euro.c
    val mutable history : Euro.c operation list
    method balance : Euro.c
    method deposit : Euro.c -> unit
    method history : Euro.c operation list
    method private trace : Euro.c operation -> unit
    method withdraw : Euro.c -> Euro.c
  end
    

実際、 deposit の呼び出し時に自動的に安全性チェックとトレース出力が得られるため、後者のほうがより安全だといえます。 テストしてみましょう。

#let ccp = new account_with_deposit (euro 100.) in 
 let balance = ccp#withdraw (euro 50.) in
 ccp#history;;
Warning Y: unused variable balance.
- : Euro.c operation list = [Deposit <obj>; Retrieval <obj>]
    

次の多相関数で口座を閉じることができます:

#let close c = c#withdraw (c#balance);;
val close : < balance : 'a; withdraw : 'a -> 'b; .. > -> 'b = <fun>
    

もちろん、これは全ての種類の口座に適用できます。

最後に、 口座のいくつかのバージョンを、通貨について抽象化されたモジュール Account にまとめます。

#let today () = (01,01,2000) (* an approximation *)
 module Account (M:MONEY) =
   struct
     type m = M.c
     let m = new M.c
     let zero = m 0. 
         
     class bank =
       object (self) 
         val mutable balance = zero
         method balance = balance
         val mutable history = []
         method private trace x = history <- x::history
         method deposit x =
           self trace (Deposit x);
           if zero leq x then balance <- balance   plus x
           else raise (Invalid_argument "deposit")
         method withdraw x =
           if x leq balance then
             (balance <- balance   plus (neg x); self trace (Retrieval x); x)
           else zero
         method history = List.rev history
       end
         
     class type client_view = 
       object
         method deposit : m -> unit
         method history : m operation list
         method withdraw : m -> m
         method balance : m
       end
           
     class virtual check_client x = 
       let y = if (m 100.) leq x then x
       else raise (Failure "Insufficient initial deposit") in
       object (self) initializer self deposit y end
         
     module Client (B : sig class bank : client_view end) =
       struct
         class account x : client_view =
           object
             inherit B.bank
             inherit check_client x
           end
             
         let discount x =
           let c = new account x in
           if today() < (1998,10,30) then c   deposit (m 100.); c
       end
   end;;
    

これはモジュールを使ってひとつの単位とみなすことができるいくつかのクラス定義をグループ化する方法を示しています。 この単位は内部と外部両方で使うよう、銀行から提供されます。 この実装は通貨を抽象化したファンクタとして実装されているので、同じコードを異なる通貨の口座を提供するために使うことができます。

クラス bank は銀行口座の実際の実装です (インライン化することもできました) (FIXME:どういう意味だ?)。 これをさらなる拡張や改良等のために使うことになります。 逆に、顧客には顧客ビューのみが与えられます。

#module Euro_account = Account(Euro);;

#module Client = Euro_account.Client (Euro_account);;

#new Client.account (new Euro.c 100.);;
    

これにより、顧客は自身の口座の balancehistory に直接のアクセスをもつことはありません。 残高を変える唯一の方法はお金を預けるか引き出すことです。顧客に口座(たとえば販促のためのディスカウント口座)を作る能力だけでなく、クラスを与え、顧客が自分の口座をパーソナライズできるようにしているのが重要です。 例えば、顧客は depositwithdraw メソッドを改良すれば自動的に彼自身の財務簿記ができるようになります。 一方、 discount 関数は上のように、これ以上のパーソナライズの余地がないようになっています。

クライアントのビューを Client ファンクタとして提供し、顧客の口座を、 bank の特化の後に構築することができるようにしているのも重要です。拡張された口座の顧客ビューを初期化するにも、 Client ファンクタを変更せずに、新しい定義を渡せばよいのです。

#module Investment_account (M : MONEY) = 
   struct
     type m = M.c
     module A = Account(M)
         
     class bank =
       object
         inherit A.bank as super
         method deposit x =
           if (new M.c 1000.) leq x then
             print_string "Would you like to invest?";
           super deposit x
       end
         
     module Client = A.Client
   end;;
    

Client ファンクタは、顧客に口座の新しい機能を与えることができるようになったときに再定義してもよいでしょう。

#module Internet_account (M : MONEY) = 
   struct
     type m = M.c
     module A = Account(M)

     class bank =
       object
         inherit A.bank 
         method mail s = print_string s
       end
         
     class type client_view = 
       object
         method deposit : m -> unit
         method history : m operation list
         method withdraw : m -> m
         method balance : m
         method mail : string -> unit
       end
           
     module Client (B : sig class bank : client_view end) =
       struct
         class account x : client_view =
           object
             inherit B.bank
             inherit A.check_client x
           end
       end
   end;;