原始的な強迫観念がコード臭ではないのはいつですか?


22

私は最近、原始的な強迫観念をコードの匂いとして説明する記事をたくさん読みました。

原始的な強迫観念を回避することには2つの利点があります。

  1. ドメインモデルをより明確にします。たとえば、郵便番号を含む文字列ではなく郵便番号についてビジネスアナリストと話すことができます。

  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原則を破ることになります。

ただし、次のオブジェクトについてはどうですか:

  1. 生年月日:気づきよりも大きく、今日の日付よりも小さいことを確認します。

  2. 給与:ゼロ以上であることを確認します。

DateOfBirthオブジェクトとSalaryオブジェクトを作成しますか?利点は、ドメインモデルを説明するときにそれらについて話すことができることです。ただし、これは多くの検証がないため、オーバーエンジニアリングの場合です。いつ、いつ原始的な強迫観念を取り除くべきでないかを説明するルールはありますか、可能であれば常にそれを行うべきですか?

クラスの代わりに型エイリアスを作成できると思います。これは上記のポイント1に役立ちます。


8
「郵便番号が使用されるすべての場所にその検証ロジックを置くDRY原則を破ることになります。」それは真実ではありません。データがモジュールに入力されたらすぐに検証を行う必要があります。複数の「エントリポイント」がある場合、検証はにする必要があり、再利用可能なユニット、およびそれである必要はありません(もする必要があります)DTO ...
ティモシーTruckle

1
DateOfBirth確認するために、「mindate」と「today's date」をコンストラクタにどのように渡していますか?
カレス

11
カスタム型を作成するもう1つの利点は、型の安全性です。あなたが持っているSalaryDistance反対する場合は、誤ってそれらを交換して使用することはできません。それらが両方ともタイプの場合、できdoubleます。
Scroog1

3
@ w0051977 Youステートメント(私が理解したように)は、DTOコンストラクターで検証を行うこと以外はDRYに違反することを暗示しています。実際には検証が必要があり DTO ...外にある
ティモシーTruckle

2
私にとっては、それはすべて範囲の問題です。プリミティブに広い範囲を与えると、プリミティブを誤って使用したり処理したりする方法が多数あります。そのため、一般的にスコープを狭くしたいのですが、そのための1つの方法は、内部としてプライベートに保存されたプリミティブを使用して概念を表すクラスを設計し、それを実装することです。これで、プリミティブの範囲が狭くなり、誤用/誤用される可能性が低くなり、効果的に不変式を維持できます。しかし、プリミティブのスコープがそもそも狭い場合、これは行き過ぎであり、維持するために多くの余分なカップリングとコードを導入する可能性があります。

回答:


17

Primitive Obsessionは、プリミティブデータ型を使用してドメインのアイデアを表現しています。

反対は、「ドメインモデリング」または「オーバーエンジニアリング」です。

DateOfBirthオブジェクトとSalaryオブジェクトを作成しますか?

Salaryオブジェクトの導入は、次の理由から良い考えです。数値はドメインモデル内で独立していることはめったになく、ほとんどの場合、ディメンションと単位があります。通常、時間または質量に長さを追加する場合、有用なものはモデル化されません。また、メートルと足を混在させても、良い結果が得られることはほとんどありません

DateOfBirthに関しては、おそらく-考慮すべき2つの問題があります。最初に、非プリミティブな日付を作成すると、日付の計算に関するすべての奇妙な懸念を集中させることができます。多くの言語は、すぐに使えるものを提供します。DateTimejava.util.Date。これらは日付のドメインに依存しない実装ですが、プリミティブではありません

第二に、DateOfBirth実際には日付時刻ではありません。ここ米国では、「生年月日」は文化的な構成/法的フィクションです。私たちは、から誕生日を測定する傾向がある地元の人物の誕生日。カリフォルニアで生まれたボブは、彼が2人の若いにもかかわらず、ニューヨークで生まれたアリスより「早い」誕生日を持っているかもしれません。

いつ、いつ原始的強迫観念を取り除くべきか、または可能な場合は常にそれをやるべきかを説明するルールがあります。

常にそうとは限りません。境界では、アプリケーションはオブジェクト指向ではありませんtestsで動作を記述するために使用されるプリミティブを見るのはかなり一般的です。


1
上部の引用符の後の最初のコメントは、非セクチュアのようです。さらに、それは質問の主題を再説明するだけです。それ以外の場合は良い答えですが、これは本当に気が散るものです。
ジミージェームズ

C#DateTimeもjava.util.Dateも、DateOfBirthの適切な基本型ではありません。
ケビンクライン

たぶん置き換えるjava.util.Datejava.time.LocalDate
Koray Tugay

7

正直に言うと、状況次第です。

コードをオーバーエンジニアリングするリスクは常にあります。DateOfBirthとSalaryはどの程度使用されますか?3つの密結合クラスでのみ使用しますか、それともアプリケーション全体で使用しますか?それらを1つの制約を強制するために独自のタイプ/クラスに「ちょうど」カプセル化しますか、それとも実際にそこに属する制約/関数をもっと考えることができますか?

たとえば、Salaryを考えてみましょう。「Salary」を使用した操作(異なる通貨の処理、またはtoString()関数など)はありますか?Salaryを単純なプリミティブと見なさない場合、Salaryが何であるかを検討してください。Salaryが独自のクラスになる可能性は十分にあります。


型エイリアスは良い代替手段ですか?
w0051977

@ w0051977 charonxに同意し、タイプエイリアスが代わりになる可能性があります
-techagrammer

@ w0051977主な目的が厳密な型指定を強制し、特定の値(給与)を明示的に述べて、「フロートドル」(時間ごと、週ごと、月ごと)の偶発的な割り当てを避ける場合「フロート給与」(月ごと?年ごと?)。それは本当にあなたのニーズに依存します。
-CharonX

@CharonX、小数は浮動小数点数ではなく給与に使用されるべきだと思います。同意しますか?
w0051977

@ w0051977適切な10進数型があれば、その方が望ましいでしょう、はい。(私は現在C ++プロジェクトに取り組んでいるので、ブール値、整数、浮動小数点数が頭の中で最前線にあります)
CharonX

5

可能な経験則は、プログラムのレイヤーに依存する場合があります。以下のためにドメイン別名(DDD)エンティティレイヤ(マーティン、2018)、このかもしれないが同様に、「ドメイン/ビジネスコンセプトを表現する何のためにプリミティブを避けるために」こと。正当化は、OPで述べられているとおりです。より表現力のあるドメインモデル、ビジネスルールの検証、暗黙の概念の明示化(Evans、2004)。

型エイリアスは軽量の代替手段であり(Ghosh、2017)、必要に応じてエンティティクラスにリファクタリングできます。例えば、我々は最初にその必要とするかもしれないSalaryBEを>=0、後に禁止することを決定 $100.33333し、上記のもの$10,000,000(クライアントを破産でしょう)。NonnegativeプリミティブSalaryやその他の概念を使用すると、 このリファクタリングが複雑になります。

プリミティブを回避すると、過剰なエンジニアリングを回避するのにも役立ちます。給与生年月日を組み合わせてデータ構造にする必要があると します。たとえば、メソッドのパラメーターを減らしたり、モジュール間でデータを渡したりします。次に、typeのタプルを使用できます(Salary, DateOfBirth)。実際、プリミティブを持つタプル(Nonnegative, Nonnegative)、は有益でclass EmployeeDataはありませんが、肥大化したものの 中には必要なフィールドを隠すものがあります。たとえば、署名はにcalcPension(d: (Salary, DateOfBirth))比べてより焦点が絞られてcalcPension(d: EmployeeData)おり、インターフェイス分離の原則に違反しています。同様に、専門家class SalaryAndDateOfBirthは扱いにくいようで、 おそらくやり過ぎです。後で、データクラスを定義することを選択できます。タプルと要素ドメインタイプにより、このような決定を延期できます。

外側のレイヤー(GUIなど)では、エンティティを構成要素のプリミティブに「ストリップ」することが理にかなっています(DAOに入れるなど)。これにより、Martin(2018)で提唱されているように、ドメインの抽象化が外部層に漏れることが防止されます。

参照
E. Evans、「ドメイン駆動設計」、2004
D. Ghosh、「機能的および反応性ドメインモデリング」、2017
RC Martin、「クリーンアーキテクチャ」、2018


すべての参照に対して+1。
w0051977

4

原始的強迫観念建築宇宙飛行士に苦しむ方が良いですか?

どちらの場合も病理学的で、ある場合には抽象化が少なすぎて繰り返しになり、リンゴをオレンジと間違えてしまいます。また、別の場合には、それですでにやめ、物事を成し遂げることを忘れてしまい、何かを成し遂げることが難しくなります。

ほぼいつものように、あなたは節度を望みます。うまくいけば、よく考えられた中道です。

プロパティには、タイプに加えて名前もあることに注意してください。また、アドレスをその構成部分に分解することは、常に同じ方法で行われた場合、制約が厳しすぎる可能性があります。すべての世界がニューヨークのダウンタウンにあるわけではありません。


3

Salaryクラスがある場合は、ApplyRaiseなどのメソッドを使用できます。

一方で、ZipCodeValidatorクラスを挿入できるすべての場所で検証の重複を回避するために、ZipCodeクラスに内部検証が必要ないため、システムを米国と英国の両方のアドレスで実行する場合は、正しいバリデーターを使用し、AUSアドレスも処理する必要がある場合は、新しいバリデーターを追加するだけです。

もう1つの懸念は、EntityFrameworkを介してデータベースにデータを書き込む必要がある場合、SalaryまたはZipCodeの処理方法を知る必要があることです。

インテリジェントなクラスがどのようにあるべきかを明確に示す答えは明確ではありませんが、検証などのビジネスロジックを、データクラスが純粋なデータであるビジネスロジッククラスに移動する傾向があると言えますEntityFrameworkとの連携を改善します。

タイプエイリアスの使用に関しては、メンバー/プロパティ名がコンテンツに必要なすべての情報を提供する必要があるため、タイプエイリアスは使用しません。


型エイリアスは良い代替手段ですか?
w0051977

2

(質問はおそらく本当に何ですか)

プリミティブ型の使用がコード臭ではないのはいつですか?

(回答)

パラメーターにルールがない場合-プリミティブ型を使用します。

次のような場合にプリミティブ型を使用します。

htmlEntityEncode(string value)

次のようなオブジェクトを使用します。

numberOfDaysSinceUnixEpoch(SimpleDate value)

後者の例では、その中にルールを有する、すなわち、オブジェクトがSimpleDate構成されYearMonthおよびDay。この場合のオブジェクトの使用により、SimpleDate有効であるという概念をオブジェクト内にカプセル化できます。


1

この質問の他の場所に記載されている電子メールアドレスまたは郵便番号の標準的な例とは別に、私がプリミティブオブセッションから離れたリファクタリングが特に役立つのは、エンティティ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コードの匂いは気にしないでください)。 automagicマッピングによって選択されません。


0

郵便番号が使用されるすべての場所にその検証ロジックを置くDRY原則を破ることになります。

一方、多くの国やそれらの異なる郵便番号システムを扱う場合、問題の国を知らない限り郵便番号を検証することはできません。したがって、ZipCodeクラスには国も保存する必要があります。

しかし、その後、国をAddress郵便番号の一部(郵便番号も含む)と郵便番号の一部(検証用)の両方として別々に保存しますか?

  • そうした場合、DRYにも違反していることになります。DRY違反と呼ばない場合でも(各インスタンスは異なる目的を果たすため)、2つの国の値が異なる場合にバグへの扉を開くことに加えて、不必要に余分なメモリを占有します(論理的には決してすべきではありません) )。
    • または、2つのデータポイントを常に同じにするために同期する必要があります。これは、いずれにしても、このデータを実際に単一のポイントに保存する必要があることを意味します。
  • そうでない場合、それはZipCodeクラスではなくAddressクラスであり、これには再び含まれますstring ZipCode

たとえば、郵便番号を含む文字列ではなく郵便番号についてビジネスアナリストと話すことができます。

利点は、ドメインモデルを説明するときにそれらについて話すことができることです。

情報に特定の変数タイプがある場合、ビジネスアナリストと話すときは常にそのタイプに言及する義務があるというあなたの根底にある主張を理解していません。

どうして?「zipcode」について単に話せず、特定のタイプを完全に省略することができないのはなぜですか?プロパティのタイプが会話に典型的なビジネスアナリスト(テクニカルではありません!)とどのような議論をしていますか?

私の出身地では、郵便番号は常に数値です。そのため、選択肢があり、intまたはとして保存できstringます。私たちは、データの数学的な操作のない期待はありませんので、文字列を使用する傾向があるが、決してビジネスアナリストは、それが文字列であるために必要なことを私に言っていません。その決定は開発者に任せられます(またはおそらくテクニカルアナリストですが、私の経験では、彼らは核心を直接扱っていません)。

アプリケーションが期待どおりに動作する限り、ビジネスアナリストはデータ型を気にしません。


検証は、人間が期待するものに依存しているため、取り組むのが難しい獣です。

1つは、(普遍的な真実として)データを常に検証する必要があることに同意しないためです。

たとえば、これがより複雑なルックアップの場合はどうなりますか?単純な形式チェックではなく、検証で外部APIに連絡して応答を待つ必要がある場合はどうでしょうか?ZipCodeインスタンス化するすべてのオブジェクトに対して、この外部APIをアプリケーションに強制的に呼び出しますか?
たぶん、それは厳しいビジネス要件であり、それから当然正当化できます。しかし、これは普遍的な真実ではありません。これが解決策よりも負担となるユースケースがたくさんあります。

2番目の例として、フォームに住所を入力する場合、国の前に郵便番号を入力するのが一般的です。UIですぐに検証フィードバックを行うのは良いことですが、実際の問題の原因は(たとえば)次のような理由で、アプリケーションが「間違った」zipcode形式を警告した場合、実際には(ユーザーとして)邪魔になります。私の国はデフォルトで選択されている国ではないため、検証は間違った国に対して行われました。
それは間違ったエラーメッセージであり、ユーザーを混乱させ、不必要な混乱を引き起こします。

永続的な検証が普遍的な真実ではないように、どちらも私の例ではありません。それはだ文脈。一部のアプリケーションドメインでは、何よりもデータの検証が必要です。他のドメインは、それがもたらす煩わしさが実際の優先順位と矛盾するため、優先順位のリストに高い検証を行いません(たとえば、ユーザーエクスペリエンス、または最初に欠陥のあるデータを保存して、決して許可するのではなく修正できるようにするため)保存済み)

生年月日:気づきよりも大きく、今日の日付よりも小さいことを確認します。
給与:ゼロ以上であることを確認します。

これらの検証の問題は、それらが不完全、冗長、またははるかに大きな問題を示していることです。

日付が思考よりも大きいことを確認することは冗長です。精神は文字通り、それが可能な限り最小の日付であることを意味します。また、どこに関連性の線を引きますか?防止するDateTime.MinDateが許可することのポイントは何DateTime.MinDate.AddSeconds(1)ですか?他の多くの値と比較して、特に間違っていない特定の値を選択しています。

私の誕生日は1978年1月2日です(そうではありませんが、そうだと仮定しましょう)。しかし、アプリケーションのデータが間違っているとしましょう。代わりに、私の誕生日は次のようになります。

  • 1978年1月1日
  • 1722年1月1日
  • 2355年1月1日

これらの日付はすべて間違っています。それらのいずれも、他のものよりも「より適切」ではありません。ただし、検証ルールはこれら3つの例のうちの1つだけをキャッチします。

また、このデータの使用方法のコンテキストも完全に省略しました。これが誕生日リマインダーボットなどで使用されている場合、間違った日付を入力しても特に悪い結果はないため、検証は無意味だと思います。
一方、これが政府のデータであり、誰かの身元を認証するために生年月日が必要な場合(およびそうしないと、たとえば社会保障の拒否などの悪い結果につながります)、データの正確性が最重要であり、完全に必要ですデータを検証します。現在、提案されている検証は適切ではありません。

給与については、マイナスになることはできないという常識があります。しかし、無意味なデータが入力されていると現実的に予想する場合は、この無意味なデータのソースを調査することをお勧めします。官能的なデータを入力することを信頼できない場合、正しいデータを入力することも信頼できないためです。

代わりに給与がアプリケーションによって計算され、どういうわけか負の(そして正しい)数値で終わる可能性がある場合、より良いアプローチはMath.Max(myValue, 0)、検証に失敗するのではなく、負の数値を0に変えることです。論理が結果が負の数であると判断した場合、検証に失敗すると計算をやり直す必要があり、2回目に異なる数になると考える理由がないためです。
そして、それが別の数になった場合、計算が一貫しておらず、したがって信頼できないと疑うことになります。

これは、検証が役に立たないと言っているわけではありません。しかし、無意味な検証は、問題を実際に解決するのではなく手間がかかり、人々に誤った安心感を与えるため、どちらも悪いです。


すでに次の日にスキップしたタイムゾーンで赤ちゃんが生まれた場合、誰かの生年月日は実際には現在の日付を過ぎている可能性があります。また、病院では、「生年月日」をデータベースに保存できますが、これは数か月先になる可能性があります。そのために別のタイプが必要ですか?
gnasher729

@ gnasher729:私が従うかどうかは定かではありませんが、あなたは私に同意しているようです(検証は文脈的であり、普遍的に正しいわけではありません)が、あなたのコメントのフレージングは​​、私が同意しないと思うことを示唆しています または私は誤解していますか?
フラット
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.