ドメイン固有のオブジェクトとプレーンな文字列または数値を使用する場合の一般的なガイドラインまたは経験則は何ですか?
例:
ほとんどのOOP=実践者は、PhoneNumberとDomainNameの特定のクラスを間違いなく言うでしょう。それらを有効にするものとそれらを比較する方法に関するルールが多いほど、簡単なクラスをより簡単かつ安全に扱うことができます。しかし、最初の3つにはもっと議論があります。
「Age」クラスに出くわしたことはありませんが、負ではない必要があることを考えると理にかなっていると言えるかもしれません(負の年齢については議論できることは承知していますが、これはプリミティブ整数とほぼ同等であることを示す良い例です)。
文字列は「名」を表すのに一般的ですが、空の文字列は有効な文字列ですが有効な名前ではないため、完全ではありません。比較は通常、大文字と小文字を区別せずに行われます。確かに空をチェックするメソッド、大文字と小文字を区別しない比較などを行うメソッドがありますが、コンシューマがこれを行う必要があります。
答えは環境に依存しますか?私は主に、おそらく10年以上存続し、維持されるエンタープライズ/高価値ソフトウェアに関心があります。
おそらく私はこれを考えすぎていますが、クラスとプリミティブのどちらを選択するかについて誰かがルールを持っているかどうかを本当に知りたいのです。
ドメイン固有のオブジェクトとプレーンな文字列または数値を使用する場合の一般的なガイドラインまたは経験則は何ですか?
一般的なガイドラインは、ドメイン固有の言語でドメインをモデル化することです。
検討:なぜ整数を使用するのですか?すべての整数を文字列で表すのと同じくらい簡単にできます。またはバイトで。
整数および年齢のプリミティブ型を含むドメイン不可知言語でプログラミングしている場合、どちらを選択しますか?
本当に重要なのは、特定の型の「プリミティブ」な性質が、実装に使用する言語の選択を誤っていることです。
特に、数値は通常、追加のコンテキストを必要とします。年齢は単なる数値ではなく、次元(時間)、単位(年?)、丸め規則もあります!年齢を一緒に追加することは、お金に年齢を追加しない方法。
タイプを区別することで、 未確認のメールアドレスと確認済みのメールアドレス の間の違いをモデル化できます。
これらの値がメモリ内でどのように表されるかという偶然は、最も興味深い部分の1つです。ドメインモデルは、CustomerId
がint、String、UUID/GUID、JSONノードのいずれであってもかまいません。それは単にアフォーダンスを望んでいるだけです。
整数がビッグエンディアンかリトルエンディアンかを本当に気にするのでしょうか。渡されたList
が配列またはグラフの抽象化であるかどうかを気にしますか?倍精度演算が非効率的であり、浮動小数点表現に変更する必要があることを発見した場合、ドメインモデルを注意する必要があります?
パルナス、 1972年に と書いた
代わりに、難しい設計決定または変更される可能性のある設計決定のリストから始めることを提案します。各モジュールは、そのような決定を他のモジュールから隠すように設計されています。
ある意味では、私たちが紹介するドメイン固有の値タイプは、データの基礎となる表現を使用する必要があるという私たちの決定を分離するモジュールです。
したがって、メリットはモジュール性です。変更の範囲を管理しやすい設計が得られます。欠点はコストです。必要なビスポークタイプを作成するのに作業が多く、正しいタイプを選択することで、ドメインをより深く理解する必要があります。値モジュールの作成に必要な作業量は、ローカルの方言 Blub によって異なります。
方程式の他の用語には、ソリューションの予想寿命(一度実行されるスクリプトウェアの注意深いモデリングで投資収益率が低くなる)、ドメインがビジネスのコアコンピテンシーにどれだけ近いかなどが含まれます。
考慮すべき1つの特殊なケースは、通信 境界を越えて の場合です。 1つのデプロイ可能なユニットへの変更が他のデプロイ可能なユニットとの調整された変更を必要とする状況になりたくありません。したがって、messagesは、不変条件やドメイン固有の動作を考慮せずに、表現に重点を置く傾向があります。メッセージ形式で「この値は厳密に正でなければなりません」を伝えようとするのではなく、ワイヤーでその表現を伝え、ドメイン境界でその表現に検証を適用します。
あなたは抽象化について考えていて、それは素晴らしいことです。しかし、あなたのリストにあるものは、ほとんどが大きなものの個々の属性であるように見えます(もちろん、すべて同じものではありません)。
私がより重要だと思うのは、属性をグループ化し、それらにPersonクラスなどの適切なホームを提供する抽象化を提供することです。これにより、クライアントプログラマーは複数の属性ではなく、1つのより高いレベルの抽象化を扱う必要がなくなります。
この抽象化を取得したら、単なる値である属性の追加の抽象化は必ずしも必要ありません。 Personクラスは、BirthDateを(プリミティブ)日付として、PhoneNumbersを(プリミティブ)文字列のリストとして、Nameを(プリミティブ)文字列として保持できます。
書式設定コード付きの電話番号がある場合、これは2つの属性であり、電話番号のクラスに値します(番号を取得するだけでなく、書式設定された電話番号を取得するなど、いくつかの動作もあるため)。
Nameを複数の属性(たとえば、プレフィックス/タイトル、最初、最後、サフィックス)として持っている場合、クラスに値するでしょう(これで、フルネームを文字列として取得するか、小さいピースやタイトルを取得するなど、いくつかの動作があります。 。)。
必要に応じてモデル化する必要があります。単純なデータが必要な場合はプリミティブ型を使用できますが、これらのデータに対してさらに操作を実行する場合は、特定のクラスでモデル化する必要があります(たとえば、彼のコメントで@kiwironに同意します)。 Ageクラスは意味がありませんが、それをBirthdateクラスに置き換えて、これらのメソッドをそこに配置することをお勧めします。
クラス内でこれらの概念をモデル化することの利点は、モデルの豊富さを適用できることです。たとえば、代わりに任意の日付を受け入れるメソッドがあり、ユーザーは任意の日付、第二次世界大戦の開始日または冷戦の終了日を入力できます。これらの日付のうち、まだ有効な誕生日です。生年月日で置き換えることができるため、この場合、生年月日を受け入れるようにメソッドに強制し、日付を受け入れないようにします。
Firstnameの場合、あまりメリットが見られません。UniqueID、PhoneNumberも少しです。たとえば、国と地域を指定するように要求できます。たとえば、一般にPhoneNumberは国と地域を参照するため、一部のオペレーターは事前定義された番号のサフィックスを使用してモバイルを分類します。 、Office ...番号。ドメイン名と同じように、これらのメソッドをそのクラスに追加できます。
詳細については、DDD(ドメイン駆動設計)の値オブジェクトをご覧になることをお勧めします。
それが依存するのは、そのデータに関連付けられた動作(維持または変更、あるいはその両方が必要)があるかどうかです。
つまり、関連する動作がほとんどなく、データの一部である場合は、プリミティブ型を使用するか、データが少し複雑な場合は(ほとんど動作しない)データ構造(たとえば、getterとセッター)。
一方、意思決定を行うために、コード内のさまざまな場所でこのデータを確認または操作していることがわかった場合、多くの場合、互いに近接していない場所で、クラスを定義して適切なオブジェクトに変換します。関連する振る舞いをメソッドの中に入れます。何らかの理由でそのようなクラスを定義しても意味がないと感じた場合は、この動作を、このデータを操作するある種の「サービス」クラスに統合することもできます。
あなたの例は、何か他のもののプロパティや属性にずっと似ています。つまり、プリミティブだけで開始することは完全に合理的です。ある時点でプリミティブを使用する必要があるため、通常は実際に必要なときに備えて複雑さを節約します。
たとえば、ゲームを作成していて、キャラクターの老化を心配する必要がないとします。そのために整数を使用することは完全に理にかなっています。年齢は、キャラクターが何かを行うことが許可されているかどうかを確認するための簡単なチェックで使用できます。
他の人が指摘したように、地域によっては年齢が複雑なテーマになる場合があります。異なるロケールなどで年齢を調整する必要がある場合は、Age
およびDateOfBirth
をプロパティとして持つLocale
クラスを導入するのが最適です。そのAgeクラスは、任意のタイムスタンプでの現在の年齢も計算できます。
したがって、プリミティブをクラスに昇格させる理由はありますが、それらはアプリケーション固有のものです。ここではいくつかの例を示します。
基本的に、プロジェクト全体で一貫して使用したい値の処理方法に関する一連のルールがある場合は、クラスを作成します。機能にとってそれほど重要ではないために単純な値で対応できる場合は、プリミティブを使用します。
結局のところ、オブジェクトは、いくつかのプロセスが実際に実行されたことの単なる目撃者です†。
プリミティブint
は、一部のプロセスが4バイトのメモリをゼロにし、次に-2,147,483,648〜2,147,483,647の範囲の整数を表すために必要なビットを反転させたことを意味します。これは年齢の概念についての強い声明ではありません。同様に、(null以外)System.String
は、いくつかのエンコーディングに関して、Unicode文字のシーケンスを表すために、いくつかのバイトが割り当てられ、ビットが反転されたことを意味します。
したがって、ドメインオブジェクトの表現方法を決定するときは、それが証人として機能するプロセスについて考える必要があります。 FirstName
が本当に空でない文字列である必要がある場合、システムは、渡された文字のシーケンスで少なくとも1つの空白以外の文字が作成されたことを確認するプロセスを実行したことを証明する必要があります。あなたへ。
同様に、Age
オブジェクトは、いくつかのプロセスが2つの日付間の差を計算したことの証人になるはずです。 ints
は通常、2つの日付の差を計算した結果ではないため、年齢を表すには事実上不十分です。
したがって、ほとんどの場合、ドメインオブジェクトごとにクラス(単なるプログラム)を作成する必要があることは明らかです。だから問題は何ですか?問題は、C#(私が大好き)とJavaはクラスの作成に過度の儀式を要求することです。しかし、ドメインオブジェクトの定義をはるかに簡単にする代替構文を想像できます。
//hypothetical syntax
class Age = |start:date - end:date|.Years
(* ML-like syntax *)
type Age(x, y) = datediff(year, x, y)
//C#-like syntax with primary constructors and expression-bodied classes
class Age(DateTime x, DateTime y) => implicit operator int (Age a) => Abs((y - x).Years);
たとえば、F#は実際には Active Patterns の形式でこのための素晴らしい構文を持っています:
//Impossible to produce an `Age` without first computing the difference of two dates
//But, any pair (Tuple) of dates is implicitly converted to an `Age` when needed.
let (|Age|) (x, y) = (date_diff y x).Days / 365
重要なのは、プログラミング言語によってドメインオブジェクトを定義するのが面倒になるからといって、実際にそうすることが悪い考えであるということではありません。
†これらのことを 事後条件 とも呼びますが、「証人」という用語は、一部のプロセスが実行可能であったことの証拠として機能するため、好んで使用します。
クラスとプリミティブを使用して何が得られるかを考えてください。クラスを使用すると
PhoneNumber
クラスを使用すると、電話番号を期待する文字列またはランダム文字列(つまり、「きゅうり」)に割り当てることは不可能になるはずです)そしてこれらの利点はあなたの人生をより簡単にし、コードの品質と可読性を向上させますandそのようなものを使うことの痛みを上回ります、それをしてください。それ以外の場合は、プリミティブに固執します。近い場合は、ジャッジメントコールを行います。
また、適切なネーミングでそれを処理できることもあります(つまり、firstName
という名前の文字列と、文字列プロパティをラップするクラス全体を作成する)。クラスは問題を解決する唯一の方法ではありません。