primitive obsessionをコードのにおいとして説明する記事を最近たくさん読んだことがあります。
原始的な強迫観念を回避することの2つの利点があります。
ドメインモデルをより明示的にします。たとえば、郵便番号を含む文字列の代わりに、郵便番号についてビジネスアナリストと話すことができます。
すべての検証は、アプリケーション全体ではなく、1つの場所で行われます。
コードの匂いがいつなのかを説明する記事はたくさんあります。たとえば、次のような投稿コードの原始的な執着を削除することの利点を確認できます。
public class Address
{
public ZipCode ZipCode { get; set; }
}
ZipCodeのコンストラクタは次のとおりです。
public ZipCode(string value)
{
// Perform regex matching to verify XXXXX or XXXXX-XXXX format
_value = value;
}
[〜#〜] dry [〜#〜] の原則を破って、Zipコードが使用されているすべての場所に検証ロジックを配置します。
ただし、次のオブジェクトについてはどうでしょう。
生年月日:気にするよりも大きく、今日の日付よりも小さいことを確認します。
給与:ゼロ以上であることを確認してください。
DateOfBirthオブジェクトとSalaryオブジェクトを作成しますか?利点は、ドメインモデルを説明するときにそれらについて話すことができることです。ただし、検証があまり行われていないため、これはオーバーエンジニアリングのケースです。プリミティブオブセッションを削除するタイミングとタイミングを説明するルールはありますか、または可能であれば常に削除する必要がありますか?
クラスの代わりに型エイリアスを作成することができると思います。これは上記のポイント1に役立ちます。
プリミティブオブセッションは、プリミティブデータ型を使用してドメインのアイデアを表現しています。
反対は「ドメインモデリング」、または「オーバーエンジニアリング」です。
DateOfBirthオブジェクトとSalaryオブジェクトを作成しますか?
次の理由により、Salaryオブジェクトを導入することをお勧めします。ドメインモデルで数値が単独で存在することはめったになく、ほとんど常に次元と単位を持っています。時間または質量に長さを追加する場合、通常は有用なモデルを作成しません。 メートルとフィートを混合しても、良い結果が得られることはめったにありません。
DateOfBirthに関しては、おそらく-考慮すべき2つの問題があります。まず、プリミティブではない日付を作成すると、日付の計算に関する奇妙な懸念のすべてを集中させることができます。多くの言語がそのまま使用できます。 DateTime 、 Java.util.Date 。これらは日付に依存しないドメイン実装ですが、プリミティブではありません。
次に、DateOfBirth
は実際には日付時刻ではありません。ここ米国では、「生年月日」は文化的な構成要素/法的フィクションです。私たちは人の生年月日をlocal日付から測定する傾向があります。カリフォルニアで生まれたボブは、ニューヨークで生まれたアリスよりも若いとしても、「早い」生年月日を持っている可能性があります。
プリミティブオブセッションをいつ削除するか、いつ削除しないか、または可能であれば常に削除する必要があるかを説明するルールはありますか?.
確かに常にではない。境界では、 アプリケーションはオブジェクト指向ではありません 。 テスト で動作を説明するために使用されるプリミティブを見るのはかなり一般的です。
正直に言うと、状況によります。
コードをオーバーエンジニアリングするリスクは常にあります。 DateOfBirthとSalaryはどの程度広く使用されますか?それらを3つの密結合クラスでのみ使用するのか、それともアプリケーション全体で使用するのか?それらを独自のType/Classに「カプセル化」して、その1つの制約を強制しますか、または実際にそこに属するより多くの制約/関数を考えられますか?
たとえば、Salaryを例に考えてみましょう。「Salary」を使用した操作はありますか(たとえば、異なる通貨、またはtoString()関数を処理するなど)? Salaryを単純なプリミティブと見なさない場合、またはSalaryが何をするかを検討してください。Salaryが独自のクラスになる可能性は十分にあります。
可能な経験則は、プログラムの層によって異なります。 ドメイン(DDD)別名エンティティレイヤー(Martin、2018)の場合、これは「ドメイン/ビジネスコンセプトを表すもののプリミティブを回避する」ことにもなる可能性があります。正当化はOPで述べられているとおりです。より表現力豊かなドメインモデル、ビジネスルールの検証、暗黙的な概念の明示(Evans、2004)。
タイプエイリアスは軽量の代替物(Ghosh、2017年)にすることができ、必要に応じてエンティティクラスにリファクタリングできます。たとえば、最初にSalary
が_>=0
_であることを要求し、後で_$100.33333
_および_$10,000,000
_を超えるもの(クライアントを破産させる)を許可しないことを決定する場合があります。 Nonnegative
プリミティブを使用してSalary
および他の概念を表すと、このリファクタリングが複雑になります。
プリミティブを回避することで、過剰なエンジニアリングを回避することもできます。 SalaryとBirth of Birthを組み合わせてデータ構造にする必要があるとします。たとえば、メソッドパラメータを減らしたり、モジュール間でデータを受け渡したりするためです。次に、タイプ_(Salary, DateOfBirth)
_のタプルを使用できます。実際、プリミティブ_(Nonnegative, Nonnegative)
_を持つタプルは有益ではありませんが、一部の肥大した_class EmployeeData
_は必要なフィールドを他のフィールドに隠してしまいます。 say calcPension(d: (Salary, DateOfBirth))
のシグネチャは、calcPension(d: EmployeeData)
よりも焦点が絞られています。これは、インターフェース分離原則に違反しています。同様に、特殊な_class SalaryAndDateOfBirth
_は扱いにくいようで、おそらくやり過ぎです。後で、データクラスを定義することを選択できます。タプルと要素ドメインのタイプは、そのような決定を遅らせることができます。
外側のレイヤー(GUIなど)では、エンティティを構成要素のプリミティブに「ストリップ」する(たとえば、DAOに入れる)のが理にかなっています。これにより、Martin(2018)で提唱されているように、ドメインの抽象化が外部レイヤーにリークするのを防ぎます。
参考文献
E。エヴァンス、「ドメイン駆動設計」、2004
D。 Ghosh、「Functional and Reactive Domain Modeling」、2017年
R C.マーティン、「クリーンアーキテクチャ」、2018
Primitive ObsessionまたはArchitecture Astronautに苦しむ方が良い
どちらのケースも病理学的であり、1つのケースでは抽象化が少なすぎて、繰り返しにつながり、Appleオレンジと間違えやすい)と、もう1つのケースでは、すでにそれを止めて物事を始めるのを忘れている完了し、何かを成し遂げることを困難にします。
ほとんどいつものように、あなたは節度を望みます。
プロパティにはタイプの他に名前があることに注意してください。また、アドレスを常に同じ方法で行うと、アドレスをその構成部分に分解するのが面倒になる可能性があります。すべての世界がニューヨークのダウンタウンにあるわけではありません。
Salaryクラスがある場合は、ApplyRaiseなどのメソッドを使用できます。
一方、ZipCodeクラスは、注入できるZipCodeValidatorクラスがあればどこでも検証が重複しないように内部検証を行う必要がないため、システムが米国と英国の両方のアドレスで実行する場合は、正しいバリデーター。AUSアドレスも処理する必要がある場合は、新しいバリデーターを追加できます。
もう1つの懸念は、EntityFrameworkを介してデータベースにデータを書き込む必要がある場合、SalaryまたはZipCodeの処理方法を知る必要があることです。
インテリジェントクラスがどうあるべきかを明確にする明確な答えはありませんが、検証のようなビジネスロジックを、データクラスが純粋なデータであるビジネスロジッククラスに移動する傾向があります。 EntityFrameworkとの連携を強化します。
タイプエイリアスの使用に関しては、メンバー/プロパティ名がコンテンツに必要なすべての情報を提供する必要があるため、タイプエイリアスは使用しません。
(質問はおそらく実際には何ですか)
プリミティブ型の使用はコードの匂いではありませんか?
(Answer)
パラメータにルールがない場合-プリミティブ型を使用します。
以下のものにプリミティブ型を使用します。
htmlEntityEncode(string value)
以下のものにオブジェクトを使用します。
numberOfDaysSinceUnixEpoch(SimpleDate value)
後者の例にはルールがあります。つまり、オブジェクトSimpleDate
はYear
、Month
、およびDay
で構成されます。この場合、Objectを使用することで、SimpleDate
が有効であるという概念をオブジェクト内にカプセル化できます。
この質問の他の場所に記載されている電子メールアドレスまたは郵便番号の正規の例とは別に、プリミティブオブセッションからのリファクタリングが特に役立つ場合、エンティティIDを使用すると便利です( https://andrewlock.net/using-stronglyを参照)。 -typed-entity-ids-to-avoid-primitive-obsession-part-1 / .NETでの実行方法の例).
メソッドに次のようなシグネチャがあったため、バグが侵入した回数のカウントを失いました。
int leaveId = 12345;
int submitterId = 23456;
int approverId = 34567;
SubmitLeaveApplication(leaveId, approverId, submitterId);
public void SubmitLeaveApplication(int leaveId, int submitterId, int approverId) {
// implementation here
}
コンパイルは問題なく行われ、単体テストに厳密でない場合は、それも合格する可能性があります。ただし、これらのエンティティIDをドメイン固有のクラスにリファクタリングすると、コンパイル時のエラーが発生します。
LeaveId leaveId = 12345;
SubmitterId submitterId = 23456;
ApproverId approverId = 34567;
SubmitLeaveApplication(leaveId, approverId, submitterId);
public void SubmitLeaveApplication(LeaveId leaveId, SubmitterId submitterId, ApproverId approverId) {
// implementation here
}
そのメソッドが10以上のパラメーター、すべてのint
データ型にスケールアップすることを想像してください( Long Parameter List コードの匂いを気にしないでください)。AutoMapperのようなものを使用すると、さらに悪化しますドメインオブジェクトとDTOの間のスワップ、および自動マジックマッピングによって取得されないリファクタリングは取得されません。
DRYの原則を破って、Zipコードが使用されているすべての場所に検証ロジックを配置します。
一方、さまざまな国とそのさまざまな郵便番号システムを扱う場合、問題の国を知らなければ、郵便番号を検証できません。したがって、ZipCode
クラスには国も格納する必要があります。
しかし、Address
(郵便番号も含まれる)の一部と、郵便番号の一部(検証用)の両方として国を個別に保存しますか?
ZipCode
クラスではなくAddress
クラスです。これには、再び_string ZipCode
_が含まれます。これは、完全な円になったことを意味します。たとえば、郵便番号を含む文字列の代わりに、郵便番号についてビジネスアナリストと話すことができます。
利点は、ドメインモデルを説明するときにそれらについて話すことができることです。
情報の一部に特定の変数タイプがある場合、ビジネスアナリストと話すときはいつでもそのタイプを何らかの形で言及する義務があるという根本的な主張を理解できません。
なぜ?単に「郵便番号」について話し、特定のタイプを完全に省略できないのはなぜですか?プロパティのタイプが会話の本質であるビジネスアナリスト(テクニカルではありません!)とどのような話し合いをしていますか?
私の出身地では、郵便番号は常に数値です。したがって、選択肢があるので、int
またはstring
として保存できます。データに数学的な演算が期待されていないため、文字列を使用する傾向がありますが、neverはビジネスアナリストから文字列である必要があると言われました。その決定は、開発者(またはおそらくテクニカルアナリストですが、私の経験では、重要な問題に直接対処していません)に任されています。
ビジネスアナリストは、アプリケーションが期待どおりの動作をしている限り、データ型を気にしません。
検証は人間が期待することに依存しているため、取り組むのが難しい獣です。
まず、私は検証の議論に同意しません。これは、原始的な強迫観念を回避する必要がある理由を示す方法としては同意しません。
たとえば、これがより複雑なルックアップである場合はどうなりますか?単純なフォーマットチェックではなく、検証で外部APIに接続して応答を待機する必要がある場合はどうなりますか?インスタンス化するすべてのZipCode
オブジェクトに対して、アプリケーションにこの外部APIを強制的に呼び出しますか?
たぶんそれは厳しいビジネス要件であり、それからもちろん正当化できます。しかし、これは普遍的な真実ではありません。これが解決策よりも負担になるユースケースはたくさんあります。
2番目の例として、フォームに住所を入力する場合、国の前に郵便番号を入力するのが一般的です。 UIにすぐに検証フィードバックがあることはいいことですが、アプリケーションが「間違った」郵便番号形式について私に警告した場合、実際には(ユーザーとして)私にとって障害になります。問題の実際の原因は(たとえば)私の国はデフォルトで選択されている国ではないため、検証は間違った国に対して行われました。
これは間違ったエラーメッセージであり、ユーザーの注意をそらし、不必要な混乱を引き起こします。
永久検証が普遍的な真実ではないように、私の例もそうではありません。 contextualです。一部のアプリケーションドメインでは、何よりもデータ検証が必要です。他のドメインは、実際の優先順位と競合するため、優先順位のリストでそれほど高い検証を行いません(たとえば、ユーザーエクスペリエンス、または欠陥のあるデータを最初に保存する機能。保存)
生年月日:気にするよりも大きく、今日の日付よりも小さいことを確認します。
給与:ゼロ以上であることを確認してください。
これらの検証の問題は、それらが不完全、冗長、またははるかに大きな問題を示すであることです。
日付が精神よりも大きいことを確認することは冗長です。思いやりは文字通り、それが可能な最小の日付であることを意味します。さらに、どこに関連性の線を引きますか? _DateTime.MinDate
_を禁止し、DateTime.MinDate.AddSeconds(1)
を許可することの意味は何ですか?他の多くの値と比較して特に間違っていない特定の値を厳選しています。
私の誕生日は1978年1月2日です(そうではありませんが、そうだとしましょう)。しかし、アプリケーションのデータが間違っているとしましょう。代わりに、私の誕生日は次のとおりです。
これらの日付はすべて間違っています。それらのどれも他より「より正しい」ものではありません。しかし、あなたのバリデーションルールはこれらの3つの例のoneのみをキャッチします。
また、このデータの使用方法のコンテキストも完全に省略しています。これが、たとえば、誕生日リマインダーボットの場合、間違った日付を入力しても特に悪い影響はないため、検証は無意味です。
一方、これが政府のデータであり、誰かのIDを認証するために生年月日が必要な場合(そしてこれを行わないと、誰かの社会保障を拒否するなど、悪い結果につながります)、データの正確さが最重要ですそして、完全にデータを検証する必要があります。現在提案されている検証は適切ではありません。
給与については、マイナスにはなれないという常識があります。しかし、無意味なデータが入力されていることを現実的に期待している場合は、この無意味なデータのソースを調査することをお勧めします。センシティカルデータの入力を信頼できない場合は、正しいデータの入力を信頼できないためです。
代わりに給与がアプリケーションによって計算され、どういうわけか負の(そして正しい)数値で終わる可能性がある場合)より良いアプローチは、負の数を0に変換するためにMath.Max(myValue, 0)
を実行することです。検証に失敗します。ロジックで結果が負の数であると判断された場合、検証に失敗すると、計算をやり直す必要があり、2回目に別の数になると考える理由はありません。
そして、それが別の数である場合は、計算が一貫しておらず、したがって信頼できないと疑うようになります。
これは、検証が役に立たないと言っているのではありません。しかし、無意味な検証は悪いことです。問題を実際に解決するのではなく労力を要し、人々に誤った安心感を与えるからです。