「原始的な強迫観念」を許す正当な理由は「ヨーヨーの問題を回避する」ことですか?


42

よると、原始的強迫観念ではないコードのにおいがある場合は?、Stringオブジェクトの代わりに郵便番号を表すZipCodeオブジェクトを作成する必要があります。

しかし、私の経験では、私は見たいです

public class Address{
    public String zipCode;
}

の代わりに

public class Address{
    public ZipCode zipCode;
}

後者では、プログラムを理解するためにZipCodeクラスに移動する必要があると思うからです。

そして、私はすべてのプリミティブデータフィールドがあるかのように感じているクラスに置き換えた場合、私は定義を参照するために多くのクラス間を移動する必要があると考えているから苦しみヨーヨー問題(アンチパターン)。

そこで、ZipCodeメソッドを新しいクラスに移動したいと思います。例えば:

古い:

public class ZipCode{
    public boolean validate(String zipCode){
    }
}

新着:

public class ZipCodeHelper{
    public static boolean validate(String zipCode){
    }
}

そのため、郵便番号を検証する必要があるのはZipCodeHelperクラスに依存するだけです。そして、私は原始的な強迫観念を保持する別の「利点」を見つけました。たとえば、文字列列zipCodeを持つアドレステーブルなど、クラスがシリアル化された形式のように見えます。

私の質問は、「ヨーヨーの問題を回避する」(クラス定義間を移動する)が「原始的な強迫観念」を許可する正当な理由ですか?


9
@ jpmc26次に、郵便番号オブジェクトがどれほど複雑であるかを見てショックを受けます-正しいとは言いませんが、存在します
Jared Goguen

9
@ jpmc26、「複雑」から「悪いデザイン」への移行方法がわかりません。複雑なコードは、多くの場合、単純なコードが、私たちが望んでいた理想的な世界ではなく、現実の世界の複雑さと接触する結果です。 「その2ページの機能に戻ります。はい、ウィンドウを表示するだけの簡単な機能ですが、少し毛やものが増えて、誰も理由を知りません。まあ、理由を教えてください。修正します。」
キラレッサ

19
@ jpmc26-ZipCodeのようなオブジェクトをラップするポイントは、タイプセーフです。郵便番号は文字列ではなく、郵便番号です。関数が郵便番号を予期している場合、文字列ではなく郵便番号のみを渡すことができます。
DavorŽdralo

4
これは言語固有であり、異なる言語が異なることを行うと感じています。@DavorŽdralo同じストレッチで、多くの数値型も追加する必要があります。「正の整数のみ」、「偶数のみ」もすべて型にすることができます。
paul23

6
@ paul23確かに、そして、私たちがそれらを持っていない主な理由は、多くの言語がそれらを定義するエレガントな方法をサポートしていないことです。「年齢」を「摂氏温度」とは異なるタイプとして定義することは、「userAge == currentTemperature」がナンセンスとして検出される場合にのみ完全に合理的です。
IMSoP

回答:


116

Addressクラスを理解するために、ZipCodeクラスにヨーヨーする必要はないという前提です。ZipCodeが適切に設計されている場合、Addressクラスを読み取るだけで、ZipCodeが何をするかが明確になります。

プログラムはエンドツーエンドで読み取られません-通常、プログラムは非常に複雑であるため、これを実現できません。プログラム内のすべてのコードを同時に念頭に置くことはできません。したがって、抽象化とカプセル化を使用してプログラムを意味のある単位に「チャンク」するので、依存するすべてのコードを読まなくてもプログラムの一部(Addressクラスなど)を見ることができます。

たとえば、コード内で文字列に遭遇するたびに、文字列のソースコードを読むことにヨーヨーをしないでください。

クラスの名前をZipCodeからZipCodeHelperに変更すると、2つの別個の概念が存在することが示唆されます。zipコードとzipコードヘルパーです。そのため、2倍複雑です。そして今、型システムは、同じ型を持っているため、任意の文字列と有効な郵便番号を区別するのに役立ちません。これが「強迫観念」が適切な場所です。プリミティブの周りの単純なラッパータイプを避けたいという理由だけで、より複雑で安全性の低い代替案を提案しています。

この特定のタイプに依存する検証やその他のロジックがない場合、プリミティブを使用することは正当です。しかし、ロジックを追加するとすぐに、このロジックがタイプでカプセル化されていると、はるかに簡単になります。

シリアル化に関しては、使用しているフレームワークの制限のように思えます。確かに、ZipCodeを文字列にシリアル化するか、データベースの列にマップできるはずです。


2
「意味のあるユニット」(メイン)の部分には同意しますが、郵便番号と郵便番号の検証が同じ概念であることにはあまり同意しません。ZipCodeHelper(これを呼び出す方がいいでしょうZipCodeValidator)は、Webサービスへの接続を確立して、それを行うことができます。これは、「郵便番号データを保持する」という単一の責任の一部ではありません。型システムが無効な郵便番号を許可しないようにすることは、ZipCodeコンストラクターをJavaのpackage-privateと同等にし、ZipCodeFactory常にバリデーターを呼び出すa でコンストラクターを呼び出すことで達成できます。
R.シュミッツ

16
@ R.Schmitz:それは、単一の責任原則という意味での「責任」の意味ではありません。ただし、いずれにしても、郵便番号とその検証をカプセル化する限り、もちろん必要な数のクラスを使用する必要があります。OPは、郵便番号カプセル化する代わりにヘルパー提案しますが、これは悪い考えです。
JacquesB

1
私は敬意を持って同意しません。SRPは、クラスに「変更する理由が1つだけある」ことを意味します(「zipcodeの構成」と「検証方法」の変更)。ここでは、この特定のケースは、さらに、本の中で詳述されてクリーンコード:「オブジェクトは、抽象化の後ろに自分のデータを非表示にし、そのデータを操作する関数を公開するデータ構造は、そのデータを公開し、意味のある機能を持っていません。」 - ZipCode「データ構造」になりますそしてZipCodeHelper、「オブジェクトのいずれの場合も、私たちは私たちは郵便番号のコンストラクタへのWeb接続を渡す必要はありませんことに同意と思います。。
R.シュミッツ

9
この特定のタイプに依存する検証やその他のロジックがない場合、プリミティブを使用することは正当です。=>同意しない。すべての値が有効であっても、プリミティブを使用するのではなく、セマンティクスを言語に伝達することを好みます。関数が現在のセマンティック使用法にとって無意味なプリミティブ型で呼び出せる場合、その関数はプリミティブ型であってはならず、賢明な関数のみが定義された適切な型でなければなりません。(例として、intIDとして使用すると、IDにIDを掛けることができます...)
Matthieu M.

@ R.Schmitz郵便番号は、あなたが行っている区別の悪い例だと思います。多くの場合、変化するものは、個別のクラスの候補になる可能性がFooありFooValidatorます。我々は可能性が持っているZipCodeフォーマットと検証クラスZipCodeValidatorに正しくフォーマットされたことを確認するために、いくつかのWebサービスを打つZipCode実際に現在あるが。郵便番号が変わることはわかっています。しかし実際には、有効な郵便番号のリストをにカプセル化するZipCodeか、ローカルデータベースに入れます。
いいえ、

55

できる場合:

new ZipCode("totally invalid zip code");

また、ZipCodeのコンストラクターは次のことを行います。

ZipCodeHelper.validate("totally invalid zip code");

次に、カプセル化を解除し、ZipCodeクラスにかなり愚かな依存関係を追加しました。コンストラクター呼び出さない場合、ZipCodeHelper.validate(...)実際に強制することなく、独自のアイランドでロジックを分離しています。無効な郵便番号を作成できます。

validateこの方法は、郵便番号のクラスの静的メソッドである必要があります。これで、「有効な」郵便番号の知識がZipCodeクラスにバンドルされます。コード例がJavaのように見えることを考えると、誤った形式が指定された場合、ZipCodeのコンストラクターは例外をスローする必要があります。

public class ZipCode {
    private String zipCode;

    public ZipCode(string zipCode) {
        if (!validate(zipCode))
            throw new IllegalFormatException("Invalid zip code");

        this.zipCode = zipCode;
    }

    public static bool validate(String zipCode) {
        // logic to check format
    }

    @Override
    public String toString() {
        return zipCode;
    }
}

コンストラクターは形式をチェックして例外をスローし、それによって無効な郵便番号の作成を防ぎます。また、静的validateメソッドは他のコードで使用できるため、形式をチェックするロジックはZipCodeクラスにカプセル化されます。

ZipCodeクラスのこのバリアントには「ヨーヨー」はありません。適切なオブジェクト指向プログラミングと呼ばれています。


また、ここでは国際化を無視します。これにより、ZipCodeFormatまたはPostalService(PostalService.isValidPostalCode(...)、PostalService.parsePostalCode(...)など)と呼ばれる別のクラスが必要になる場合があります。


28
注:ここでの@Greg Burkhardtのアプローチの主な利点は、誰かがZipCodeオブジェクトを提供した場合、そのタイプとそれが正常に構築されたという事実により、ZipCodeオブジェクトを再度チェックすることなく、信頼できることです。その保証。代わりに文字列を渡す場合、有効な郵便番号があることを確認するためだけにコードのさまざまな場所で「validate(zipCode)」をアサートする必要があると感じるかもしれませんが、正常に構築されたZipCodeオブジェクトで、それを信頼できますその内容は、再度チェックすることなく有効です。
一部のガイ

3
@ R.Schmitz:このZipCode.validateメソッドは、例外をスローするコンストラクターを呼び出す前に実行できる事前チェックです。
グレッグブルガート

10
@ R.Schmitz:厄介な例外について心配している場合、構築の代替アプローチは、ZipCodeコンストラクターをプライベートにし、渡されたパラメーターの検証を実行するパブリック静的ファクトリー関数(Zipcode.create?)を提供することです。失敗した場合はnullを返し、そうでない場合はZipCodeオブジェクトを構築して返します。もちろん、呼び出し元は常にnullの戻り値をチェックする必要があります。一方、たとえば、ZipCodeを構築する前に常に検証(正規表現、検証、など)を行う習慣がある場合、例外は実際にはそれほど厄介ではない場合があります。
ある男

11
Optional <ZipCode>を返すファクトリ関数も可能です。その場合、呼び出し元には、ファクトリ関数の起こりうる障害を明示的に処理する以外に選択肢はありません。とにかく、どちらの場合でも、エラーは、おそらくずっと後でなく、元の問題から遠く離れたクライアントコードによって、作成された場所の近くで発見されます。
ある男

6
ZipCodeを個別に検証することはできませんので、しないでください。ZipCode / PostCode検証ルールを検索するには、Countryオブジェクトが本当に必要です。
ジョシュア

11

この質問に何度も取り組んでいるなら、おそらくあなたが使用する言語は仕事にふさわしいツールではないでしょうか?この種の「ドメイン型のプリミティブ」は、たとえばF#で簡単に表現できます。

たとえば、次のように書くことができます。

type ZipCode = ZipCode of string
type Town = Town of string

type Adress = {
  zipCode: ZipCode
  town: Town
  //etc
}

let adress1 = {
  zipCode = ZipCode "90210"
  town = Town "Beverly Hills"
}

let faultyAdress = {
  zipCode = "12345"  // <-Compiler error
  town = adress1.zipCode // <- Compiler error
}

これは、異なるエンティティのIDを比較するなど、よくある間違いを回避するのに非常に便利です。そして、これらの型指定されたプリミティブはC#やJavaクラスよりもはるかに軽量なので、実際にそれらを使用することになります。


おもしろい-の検証を強制する場合、どのようになりますZipCodeか?
ハルク

4
@Hulk F#でオブジェクト指向スタイルを記述し、型をクラスにすることができます。ただし、ZipCode = stringというプライベートZipCode型の型を宣言し、作成関数を使用してZipCodeモジュールを追加する機能的なスタイルを好みます。いくつかの例がここにありますgist.github.com/swlaschin/54cfff886669ccab895a
Guran

@ Bent-Tranberg編集ありがとうございます。あなたは正しいです、単純な型の省略はコンパイル時の型セキュリティを与えません。
グラン

私が削除した最初のコメントを郵送された場合、その理由は最初にあなたのソースを誤解したからです。私はそれを十分に注意深く読みませんでした。私がそれをコンパイルしようとしたとき、あなたは実際にあなたが実際にこれを正確に実証しようとしていることに気付いたので、それを正しくするために編集することにしました。
ベントトランバーグ

うん。私の元のソースは大丈夫でしたが、残念ながら無効であると思われる例を含んでいます。ど!自分自身ではなく、入力コードのWlaschinに:)リンクHAVする必要がありfsharpforfunandprofit.com/posts/...
Guran

6

答えは、郵便番号で実際に何をしたいかによって完全に異なります。2つの極端な可能性があります。

(1)すべての住所は、単一の国にあることが保証されています。例外はありません。(たとえば、外国の顧客、または外国の顧客のために働いている間、個人住所が海外にある従業員はいません。)この国には郵便番号があり、深刻な問題が発生することはありません(つまり、自由形式の入力は不要です) 「現在はD4B 6N2ですが、これは2週間ごとに変わります」など)。郵便番号は、宛先指定だけでなく、支払い情報の検証などの目的にも使用されます。-これらの状況では、郵便番号クラスは非常に理にかなっています。

(2)住所はほぼすべての国に存在する可能性があるため、ZIPコードの有無にかかわらず(および数千の奇妙な例外や特殊なケースを含む)数十または数百のアドレス指定スキームが関連します。「ZIP」コードは、ZIPコードが使用されている国の人々に、忘れずに提供することを思い出させるためにのみ求められます。アドレスは、誰かがアカウントへのアクセスを失い、名前とアドレスを証明できる場合にのみ使用されるため、アクセスが復元されます。-これらの状況では、関連するすべての国の郵便番号クラスは多大な労力を費やします。幸いなことに、それらはまったく必要ありません。


3

他の回答では、OOドメインモデリングと、より豊富な型を使用して価値を表現する方法について説明しました。

特にあなたが投稿したコード例を考えると、私は同意しません。

しかし、それが実際にあなたの質問のタイトルに答えるかどうかも疑問に思います。

次のシナリオを検討してください(私が取り組んでいる実際のプロジェクトから抜粋):

中央サーバーと通信するフィールドデバイス上にリモートアプリケーションがあります。デバイスエントリのDBフィールドの1つは、フィールドデバイスが存在する住所の郵便番号です。あなたは郵便番号(またはその問題の残りのアドレスのいずれか)を気にしません。それを気にするすべての人々はHTTP境界の反対側にいます:あなたはたまたまデータの唯一の真実の源です。ドメインモデリングには場所がありません。それを記録し、検証し、保存し、要求に応じてJSON blobでシャッフルして別の場所に移動します。

このシナリオでは、SQLの正規表現の制約(またはそのORMに相当)で、インサートの検証を超えて何の多くを行うことで、おそらく行き過ぎYAGNI品種の。


6
SQL正規表現の制約は、修飾された型と見なすことができます。データベース内では、Zipコードは「VarChar」としてではなく、「この規則によって制約されたVarChar」として保存されます。一部のDBMSでは、そのtype + constraintに再利用可能な「ドメインタイプ」として簡単に名前を付けることができ、データに意味のあるタイプを与える推奨場所に戻りました。私は原則としてあなたの答えに同意しますが、その例が一致するとは思わない。より良い例は、データが「生のセンサーデータ」であり、データの意味がわからないため、最も意味のあるタイプが「バイト配列」である場合です。
IMSoP

@IMSoP興味深い点。ただし、Java(またはその他の言語)の文字列郵便番号を正規表現で検証することはできますが、より豊富な型ではなく文字列として処理することができます。ドメインロジックによっては、さらに操作が必要になる場合があります(たとえば、郵便番号が正規表現で検証するのが困難/不可能な状態に一致することを確認するなど)。
ジャレッドスミス

することができます、そうするとすぐに、ドメイン固有の振る舞いを指定します。引用された記事がまさにそれがカスタム型の作成につながるはずだと言っています。問題は、あるスタイルでそれを行うことができるかどうかではなく、プログラミング言語で選択できると仮定して、すべきかどうかです。
IMSoP

RegexValidatedString文字列自体と、それを検証するために使用される正規表現を含むのようなものをモデル化できます。しかし、すべてのインスタンスに一意の正規表現がある場合(これは可能ですが可能性は低いですが)、これは少し愚かで無駄なメモリ(およびおそらく正規表現のコンパイル時間)のようです。したがって、正規表現を別のテーブルに入れて、各インスタンスにルックアップキーを残してそれを見つけるか(間接的なために間違いなく悪い)、またはそのルールを共有する値の一般的なタイプごとに一度保存する方法を見つけます- -例 IMSoPが述べたように、ドメインタイプの静的フィールド、または同等のメソッド。
ミラル

2

ZipCodeあなたの場合は抽象化にのみ意味を作ることができるAddressクラスも持っていなかったTownNameプロパティを。それ以外の場合は、抽象化が半分になります。郵便番号は町を指定しますが、これらの2つの関連情報は異なるクラスにあります。あまり意味がありません。

しかし、それでも、それはまだ正しいアプリケーション(またはむしろソリューション)ではありません。私が理解しているように、これは主に2つのことに焦点を当てています:

  1. 特にプリミティブのコレクションが必要な場合、メソッドの入力(または出力)値としてプリミティブを使用します。
  2. これらの一部を独自のサブクラスにグループ化する必要があるかどうかを再検討せずに、時間とともに余分なプロパティを成長させるクラス。

あなたの場合もそうではありません。住所は、明確に必要なプロパティ(通り、番号、郵便番号、町、州、国、...)を含む明確に定義された概念です。地球上の場所を指定するという単一の責任があるため、このデータを分割する理由はほとんどありません。アドレスが意味を持つためには、これらすべてのフィールドが必要です。アドレスの半分は無意味です。

これは、さらに細分化する必要がないことを知る方法です。それ以上細分化すると、Addressクラスの機能的な意図が損なわれます。同様に、ドメインで(人が接続NameされていPersonないName)意味のある概念でない限り、クラスでサブクラスを使用する必要はありません。それは(通常)そうではありません。名前は人を識別するために使用され、通常、それ自体には価値がありません。


1
@RikD:答えから:名前(人がいない場合がドメインで意味のある概念でない限りNamePersonクラスでサブクラスを使用する必要はありません。」名前のカスタム検証がある場合、名前はドメインで意味のある概念になります。サブタイプを使用するための有効なユースケースとして明示的に言及しました。第二に、郵便番号の検証のために、特定の国の形式に従う必要がある郵便番号など、追加の前提条件を導入しています。あなたはOPの質問の意図よりもはるかに広いトピックをブローチしています。
1

5
住所は、明確に必要なプロパティ(通り、番号、郵便番号、町、州、国)を含む明確に定義された概念です。」- それはまさしく間違っています。これに対処する良い方法については、Amazonの住所フォームをご覧ください。
R.シュミッツ

4
@Flaterまあ、偽りの完全なリストを読んでいないからといって非難するつもりはありません。なぜなら、それは文字通り「住所には通りがあります」、「住所には都市と国の両方が必要」、「住所「郵便番号」などがありますが、これは引用文の内容とは逆です。
R.シュミッツ

8
@GregBurghardt「郵便番号は米国郵政公社を想定しており、郵便番号から町の名前を導き出すことができます。都市は複数の郵便番号を持つことができますが、各郵便番号は1つの都市にのみ結び付けられます。」これは一般に正しくありません。主に近隣の都市で使用される郵便番号を持っていますが、私の居住地はそこにありません。郵便番号は必ずしも政府の境界に沿っているとは限りません。たとえば、42223にはTNとKYの両方の郡が含まれます
ジミージェームズ

2
オーストリアには、ドイツからしかアクセスできない谷があります(en.wikipedia.org/wiki/Kleinwalsertal)。この地域には特別な条約が存在し、特にこの地域の住所にはオーストリアとドイツの両方の郵便番号があることも含まれています。そのため、一般に、住所に有効な郵便番号が1つしかないことさえ想定できません;)
ハルク

1

記事から:

より一般的には、ヨーヨーの問題は、人が概念を理解するために異なる情報源をめくり続けなければならない状況を指すこともあります。

ソースコードは、書かれているよりもはるかに頻繁に読み取られます。したがって、多くのファイルを切り替える必要があるというヨーヨーの問題が懸念されます。

しかし、なし(互いの間で前後に呼び出す)深く相互に依存するモジュールやクラスを扱うとき、ヨーヨー問題は、はるかに関連した感じ。これらは読むべき特別な種類の悪夢であり、ヨーヨーの問題の考案者が念頭に置いていたものと思われます。

ただし、はい、抽象化の層が多すぎないようにすることが重要です!

すべての非自明な抽象化は、ある程度、漏れやすいです。-漏れのある抽象化の法則。

たとえば、mmmaaaの答えにある「Addressクラスを理解するためにZipCodeクラスに[(visit)]ヨーヨーする必要はない」という仮定に同意しません。私の経験では、あなたはそうしている -少なくとも最初の数回はコードを読んだ。他の人が指摘しているようしかし、そこにある時はZipCode、クラスが適切です。

YAGNI(Ya Ai n't Gonna Need It)は、ラザニアコード(レイヤーが多すぎるコード)を避けるために従うべき優れたパターンです-型やクラスなどの抽象化はプログラマーを支援するために存在し、そうでない場合使用しないでください援助。

私は個人的に「コードの行を保存する」ことを目指しています(そしてもちろん関連する「ファイル/モジュール/クラスを保存する」など)。私は「プリミティブに取りつかれた」という言い回しを適用する人がいると確信しています- ラベル、パターン、アンチパターンを心配するよりも、推論しやすいコードを持つことが重要です。関数、モジュール/ファイル/クラスを作成するとき、または関数を共通の場所に配置するときの正しい選択は、非常に状況的です。私は、3-100行関数、80-500行ファイル、および再利用可能なライブラリコード(SLOC-コメントや定型文を含まない。通常、必須の行ごとに少なくとも1つの追加SLOCが必要です。ボイラープレート)。

最もポジティブなパターンは、開発者が必要なときにまさにそれを行っている開発者から生じています。同じ問題を解決せずにパターンを適用しようとするよりも、読み取り可能なコードを記述する方法を学ぶことがはるかに重要です。優れた開発者であれば、ファクトリパターンを実装することができます。これは、問題に最適な珍しいケースでは、これまで見たことがないものです。私はファクトリー・パターン、オブザーバー・パターン、そしておそらく何百もの名前を知らずに使用しました(つまり、「変数割り当てパターン」はありますか?)。楽しい実験のために、JS 言語にいくつのGoFパターンが組み込まれているのかを確認します。2009年に12〜15後にカウントを停止しました。Factoryパターンは、たとえばJSコンストラクターからオブジェクトを返すだけです。 WidgetFactory。

そう- はい時には ZipCode良いクラスです。しかし、いや、ヨーヨーの問題は厳密には関係ありません。


0

ヨーヨーの問題は、前後に反転する必要がある場合にのみ関係します。これは、1つまたは2つのもの(時には両方)によって引き起こされます。

  1. 悪いネーミング。ZipCodeは問題ないように見えますが、ZoneImprovementPlanCodeでは、ほとんどの人(および感心しない人)による外観が必要になります。
  2. 不適切なカップリング。ZipCodeクラスに市外局番のルックアップがあるとします。便利だから理にかなっていると思うかもしれませんが、実際にはZipCodeとは関係ありません。

名前を見て、それが何をするのかについて合理的な考えを持ち、メソッドとプロパティが合理的に明白なことをすることができるなら、コードを見に行く必要はなく、ただそれを使うことができます。それがそもそもクラスの重要なポイントです。これらは、単独で使用および開発できるモジュール式のコードです。クラスのAPI以外を調べて、クラスの機能を確認する必要がある場合、せいぜい部分的な障害です。


-1

覚えておいて、特効薬はありません。高速でクロールする必要がある非常にシンプルなアプリを作成している場合は、シンプルな文字列で十分です。ただし、98%の時間で、DDDでEric Evansが説明したValue Objectが最適です。値オブジェクトが提供するすべての利点を簡単に見ることができます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.