UML図?必要ねぇ!
私が関数型DDDについて講演するとき、よくこのスライドを使います(スライドの前後関係も参照してください)。
これはもちろん、あの有名なシーンをもじったものです。あ、こっちのシーンの間違いでした。
少し大げさだったかもしれません。UML図の中にも役立つものはあります(私はシーケンス図が好きです)。それに、良い図は千の言葉に匹敵する力があると思います。
しかし、多くの場合、クラス図にUMLを使う必要はないと考えています。
F#(OCamlやHaskellでも良いのですが)のような簡潔な言語なら、UMLと同じ意味を、より分かりやすく表現できます。 読み書きしやすく、そして何より、実際に動くコードに落とし込みやすいのです。
UML図では、コードに変換する必要があり、その過程で情報が失われてしまう可能性があります。 しかし、設計自体がプログラミング言語で記述されていれば、変換という手順は必要なくなり、設計は常に実装と同期することになります。
これを実際に示すために、インターネットで良いUMLクラス図と、あまり良くないUMLクラス図を探し、F#のコードに変換してみました。両者を比較してみてください。
まずは、典型的な例として正規表現を取り上げます(引用元)。
UML図はこちらです。
F#のコードはこちらです。
type RegularExpression = | Literal of string | Sequence of RegularExpression list | Alternation of RegularExpression * RegularExpression | Repetition of RegularExpression
// インタプリタは文字列と正規表現を受け取り、// 何らかの値を返します。type Interpret<'a> = string -> RegularExpression -> 'a
とても簡単ですね。
もう1つの典型的な例として、登録を取り上げます(引用元)。
UML図はこちらです。
F#のコードはこちらです。
type Student = { Name: string Address: string PhoneNumber: string EmailAddress: string AverageMark: float }
type Professor= { Name: string Address: string PhoneNumber: string EmailAddress: string Salary: int }
type Seminar = { Name: string Number: string Fees: float TaughtBy: Professor option WaitingList: Student list }
type Enrollment = { Student : Student Seminar : Seminar Marks: float list }
type EnrollmentRepository = Enrollment list
// ==================================// 処理 / ユースケース / シナリオ// ==================================
type IsElegibleToEnroll = Student -> Seminar -> booltype GetSeminarsTaken = Student -> EnrollmentRepository -> Seminar listtype AddStudentToWaitingList = Student -> Seminar -> Seminar
F#はUML図と同じ内容を表現していますが、図を描くよりも、すべての処理を関数として書き出すことで、元の要件の穴が明らかになると感じています。
たとえば、UML図のGetSeminarsTaken
メソッドでは、セミナーのリストはどこに保存されているのでしょうか?
もしそれがStudent
クラスにあるとしたら(図から暗示されるように)、Student
とSeminar
の間で相互参照が発生し、特別な処理をしない限り、
すべての学生とセミナーの情報がつながってしまい、全体を一度に読み込まないといけなくなります。
そこで、関数型バージョンでは、2つのクラスを分離するためにEnrollmentRepository
を作成しました。
同様に、登録がどのように動作するのか明確ではないので、必要な入力を明確にするためにEnrollStudent
関数を作成しました。
type EnrollStudent = Student -> Seminar -> Enrollment option
関数がoption
を返すので、登録が失敗する可能性がある(たとえば、学生が登録資格を持っていない、または誤って2回登録しようとしている)ことがすぐに分かります。
また別の例を見てみましょう(引用元)。
これをF#で書くと、以下のようになります。
type Customer = {name:string; location:string}
type NormalOrder = {date: DateTime; number: string; customer: Customer}type SpecialOrder = {date: DateTime; number: string; customer: Customer}type Order = | Normal of NormalOrder | Special of SpecialOrder
// これらの3つの操作は、どの注文にも共通です。type Confirm = Order -> Ordertype Close = Order -> Ordertype Dispatch = Order -> Order
// この操作は、SpecialOrderにのみ適用できますtype Receive = SpecialOrder -> SpecialOrder
UML図をそのままコードにしていますが、正直、この設計はあまり好きではありません。状態をもっと細かく分けた方が良いでしょう。
特に、Confirm
関数とDispatch
関数は、何を入力として受け取り、何を出力するのか、全く分かりません。
実際のコードを書くことで、要件についてより深く考えることができるようになるのです。
注文と顧客 バージョン2
Section titled “注文と顧客 バージョン2”注文と顧客の、より良いバージョンを見てみましょう(引用元)。
これをF#で書くと、以下のようになります。
type Date = System.DateTime
// == 顧客関連 ==
type Customer = { name:string address:string }
// == 商品関連 ==
type [<Measure>] grams
type Item = { shippingWeight: int<grams> description: string }
type Qty = inttype Price = decimal
// == 支払い関連 ==
type PaymentMethod = | Cash | Credit of number:string * cardType:string * expDate:Date | Check of name:string * bankID: string
type Payment = { amount: decimal paymentMethod : PaymentMethod }
// == 注文関連 ==
type TaxStatus = Taxable | NonTaxabletype Tax = decimal
type OrderDetail = { item: Item qty: int taxStatus : TaxStatus }
type OrderStatus = Open | Completed
type Order = { date: DateTime; customer: Customer status: OrderStatus lines: OrderDetail list payments: Payment list }
// ==================================// 処理 / ユースケース / シナリオ// ==================================type GetPriceForQuantity = Item -> Qty -> Price
type CalcTax = Order -> Taxtype CalcTotal = Order -> Pricetype CalcTotalWeight = Order -> int<grams>
ここでは、重さの単位を追加したり、Qty
とPrice
を表す型を作成したりするなど、少しだけ変更を加えています。
この設計も、AuthorizedPayment
型(注文の支払いは、承認された支払いのみ受け付けるようにするため)や
PaidOrder
型(同じ注文に2回支払うことを防ぐため)など、
状態をより細かく分けることで、さらに改善できる可能性があります。
たとえば、以下のような感じです。
// 支払いの承認を試みます。失敗する可能性があることに注意してください。type Authorize = UnauthorizedPayment -> AuthorizedPayment option
// 未払いの注文に対し、承認された支払いを適用します。type PayOrder = UnpaidOrder -> AuthorizedPayment -> PaidOrder
ホテルの予約
Section titled “ホテルの予約”JetBrains IntelliJのドキュメントにあった例を紹介します(引用元)。
F#で書くと、こうなります。
type Date = System.DateTime
type User = { username: string password: string name: string }
type Hotel = { id: int name: string address: string city: string state: string zip: string country: string price: decimal }
type CreditCardInfo = { card: string name: string expiryMonth: int expiryYear: int }
type Booking = { id: int user: User hotel: Hotel checkinDate: Date checkoutDate: Date creditCardInfo: CreditCardInfo smoking: bool beds: int }
// これらは一体何でしょう? なぜドメインオブジェクトに含まれているのでしょう?type EntityManager = unittype FacesMessages = unittype Events = unittype Log = unit
type BookingAction = { em: EntityManager user: User hotel: Booking booking: Booking facesMessages : FacesMessages events: Events log: Log bookingValid: bool }
type ChangePasswordAction = { user: User em: EntityManager verify: string booking: Booking changed: bool facesMessages : FacesMessages }
type RegisterAction = { user: User em: EntityManager facesMessages : FacesMessages verify: string registered: bool }
もう我慢できません。ここで終わりにします。
EntityManager
やFacesMessages
フィールドは何のためにあるのでしょう? ログは確かに重要ですが、なぜドメインオブジェクトにLog
フィールドがあるのでしょう?
誤解しないでください。私がわざとUML設計の悪い例を選んでいるのではありません。これらの図はすべて、“uml class diagram”で画像検索した上位の結果から引用したものです。
今度は、図書館のドメインです。少し良くなってきましたね(引用元)。
F# で書くと、こうなります。コードなので、UMLでは難しい、特定の型やフィールドにコメントを追加できます。
また、ISBN: string option
のように書くことで、ISBNが省略可能であることを表現できます。UMLの [0..1]
のような書き方は、少し分かりにくいですね。
type Author = { name: string biography: string }
type Book = { ISBN: string option title: string author: Author summary: string publisher: string publicationDate: Date numberOfPages: int language: string }
type Library = { name: string address: string }
// 図書館にある個々の資料 - 書籍、カセットテープ、CD、DVDなどは、それぞれ独自のアイテム番号を持つことができます。// これをサポートするために、資料にバーコードを付けることがあります。バーコードの目的は、// バーコード化された物理的な資料と、目録内の電子記録を結びつける、// 一意でスキャン可能な識別子を提供することです。// バーコードは資料に物理的に添付する必要があり、// バーコード番号は電子資料レコードの対応するフィールドに入力されます。// 図書館資料のバーコードは、RFIDタグに置き換えることができます。// RFIDタグには、資料の識別子、タイトル、資料の種類などを含めることができます。// RFIDタグはRFIDリーダーで読み取ることができ、// バーコードリーダーでスキャンするために書籍の表紙やCD/DVDケースを開ける必要はありません。type BookItem = { barcode: string option RFID: string option book: Book /// 図書館には、貸出可能な資料と閲覧のみの資料に関するルールがあります。 isReferenceOnly: bool belongsTo: Library }
type Catalogue = { belongsTo: Library records : BookItem list }
type Patron = { name: string address: string }
type AccountState = Active | Frozen | Closed
type Account = { patron: Patron library: Library number: int opened: Date
/// 利用者が何冊の本を借りることができ、 /// 何冊の本を予約できるかについてのルールも定義されています。 history: History list
state: AccountState }
and History = { book : BookItem account: Account borrowedOn: Date returnedOn: Date option }
検索インターフェースと管理インターフェースは定義されていないので、入力と出力にはプレースホルダー(unit
)を使います。
type Librarian = { name: string address: string position: string }
/// 利用者と司書の両方が検索できます。type SearchInterfaceOperator = | Patron of Patron | Librarian of Librarian
type SearchRequest = unit // to dotype SearchResult = unit // to dotype SearchInterface = SearchInterfaceOperator -> Catalogue -> SearchRequest -> SearchResult
type ManageRequest = unit // to dotype ManageResult = unit // to do
/// 司書のみが管理できます。type ManageInterface = Librarian -> Catalogue -> ManageRequest -> ManageResult
これも完璧な設計とは言えませんね。たとえば、Active
アカウントだけが本を借りられるということが、はっきりとは分かりません。F#では、以下のように表現できます。
type Account = | Active of ActiveAccount | Closed of ClosedAccount
/// ActiveAccountだけが本を借りられます。type Borrow = ActiveAccount -> BookItem -> History
CQRSとイベントソーシングを使った、このドメインのより現代的なモデリング方法を見たい場合は、この記事を参照してください。
ソフトウェアライセンス
Section titled “ソフトウェアライセンス”最後の例は、ソフトウェアのライセンスに関するものです(引用元)。
F#で書くと、以下のようになります。
open Systemtype Date = System.DateTimetype String50 = stringtype String5 = string
// ==========================// 顧客関連// ==========================
type AddressDetails = { street : string option city : string option postalCode : string option state : string option country : string option }
type CustomerIdDescription = { CRM_ID : string description : string }
type IndividualCustomer = { idAndDescription : CustomerIdDescription firstName : string lastName : string middleName : string option email : string phone : string option locale : string option // デフォルトは英語 billing : AddressDetails shipping : AddressDetails }
type Contact = { firstName : string lastName : string middleName : string option email : string locale : string option // デフォルトは英語 }
type Company = { idAndDescription : CustomerIdDescription name : string phone : string option fax : string option contact: Contact billing : AddressDetails shipping : AddressDetails }
type Customer = | Individual of IndividualCustomer | Company of Company
// ==========================// 製品関連// ==========================
/// フラグはORで組み合わせることができます[<Flags>]type LockingType = | HL | SL_AdminMode | SL_UserMode
type Rehost = | Enable | Disable | LeaveAsIs | SpecifyAtEntitlementTime
type BatchCode = { id : String5 }
type Feature = { id : int name : String50 description : string option }
type ProductInfo = { id : int name : String50 lockingType : LockingType rehost : Rehost description : string option features: Feature list bactchCode: BatchCode }
type Product = | BaseProduct of ProductInfo | ProvisionalProduct of ProductInfo * baseProduct:Product
// ==========================// 資格関連// ==========================
type EntitlementType = | HardwareKey | ProductKey | ProtectionKeyUpdate
type Entitlement = { EID : string entitlementType : EntitlementType startDate : Date endDate : Date option neverExpires: bool comments: string option customer: Customer products: Product list }
この図はデータだけで、メソッドがないので、関数の型はありません。何か重要なビジネスルールが表現できていないような気がします。
たとえば、元の資料のコメントを読むと、EntitlementType
とLockingType
に、ある興味深い制約があることが分かります。
特定のロックタイプは、特定の資格タイプでのみ使用できるのです。
これは型システムでモデル化できるかもしれませんが、今回はUMLをそのまま再現することにしました。
もうお分かりいただけたでしょうか?
UMLクラス図は、スケッチとしては良いと思います。ただし、数行のコードと比べると、少し複雑すぎる気もします。
しかし、詳細な設計を描くには、UMLクラス図は情報が足りません。コンテキストや依存関係のような重要なものが、全く表現されていないのです。 私の意見では、ここに示したUML図はどれも、コードを書くための設計としては不十分です。
さらに、UML図は、開発者以外の人を誤解させてしまう可能性があります。 UML図は「公式」に見え、実際には設計が浅く、実用できないにもかかわらず、深く考えられた設計だという印象を与えてしまうことがあるのです。
ご意見はありますか? コメントで教えてください!