この節では、次に示す最初の単純な銀行口座の定義の、改良、デバッグ、特化を通してオブジェクトと継承のほとんどの側面を明らかにしていきます( 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
なフィールド history
と private
なメソッド 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.);;
これにより、顧客は自身の口座の balance
や history
に直接のアクセスをもつことはありません。 残高を変える唯一の方法はお金を預けるか引き出すことです。顧客に口座(たとえば販促のためのディスカウント口座)を作る能力だけでなく、クラスを与え、顧客が自分の口座をパーソナライズできるようにしているのが重要です。 例えば、顧客は deposit
と withdraw
メソッドを改良すれば自動的に彼自身の財務簿記ができるようになります。 一方、 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;;