web-dev-qa-db-ja.com

すべてのタイプを定義する必要がありますか?

最近、コードの読みやすさに問題が生じました。
私は操作を行い、将来の参照のためにこの操作のIDを表す文字列を返しました(WindowsのOpenFileがハンドルを返すのと少し似ています)。ユーザーは後でこのIDを使用して操作を開始し、その終了を監視します。

相互運用性の問題から、IDはランダムな文字列である必要がありました。これにより、次のような非常に不明確な署名を持つメソッドが作成されました。

public string CreateNewThing()

これにより、戻り値の型の意図が不明確になります。この文字列を別のタイプにラップして、次のように意味を明確にすることを考えました。

public OperationIdentifier CreateNewThing()

タイプには文字列のみが含まれ、この文字列が使用される場合は常に使用されます。

この操作方法の利点は、型の安全性が高まり、意図がより明確になることは明らかですが、コードが非常に多くなり、あまり慣用的ではないコードも作成されます。一方で、安全性を追加したいのですが、多くの混乱を引き起こします。

安全上の理由から、クラスに単純型をラップすることは良い習慣だと思いますか?

144
Ziv

経験則としてscopeを使用します。そのようなvaluesの生成と消費の範囲が狭いほど、この値を表すオブジェクトを作成する必要が少なくなります。

あなたが以下を持っているとしましょうpseudocode

id = generateProcessId();
doFancyOtherstuff();
job.do(id);

その場合、スコープは非常に限定され、タイプにしても意味がありません。ただし、この値を1つのレイヤーで生成し、それを別のレイヤー(または別のオブジェクト)に渡す場合、そのための型を作成することは完全に理にかなっています。

61
Thomas Junk

静的型システムはすべて、データの不正使用を防ぐことを目的としています。

これを行うタイプの明白な例があります:

  • uUIDの月を取得できません
  • 2つの文字列を乗算することはできません。

もっと微妙な例があります

  • あなたは机の長さを使って何かを支払うことができません
  • 誰かの名前をURLとして使用してHTTPリクエストを行うことはできません。

価格と長さの両方にdoubleを使用したり、名前とURLの両方にstringを使用したくなるかもしれません。しかし、そうすることで私たちの素晴らしい型システムを破壊し、これらの誤用が言語の静的チェックに合格することを可能にします。

ポンド秒とニュートン秒を混同すると、 実行時の結果が悪い になる可能性があります。


これは特に文字列の問題です。それらは頻繁に「ユニバーサルデータタイプ」になります。

私たちは主にコンピューターとのテキストインターフェイスに慣れており、これらのヒューマンインターフェイス(UI)をプログラミングインターフェイス(API)に拡張することがよくあります。 34.25は文字34.25と見なされます。日付は文字05-03-2015と考えます。 UUIDは、文字75e945ee-f1e9-11e4-b9b2-1697f925ec7bと見なされます。

ただし、このメンタルモデルはAPIの抽象化に悪影響を及ぼします。

言葉や言語は、書かれたり話されたりしたときに、私の思考メカニズムに影響を与えていないようです。

アルバート・アインシュタイン

同様に、テキスト表現は、タイプとAPIの設計においていかなる役割も果たすべきではありません。 stringに注意してください! (および他の過度に一般的な「プリミティブ」タイプ)


Typesは「意味のある操作」を伝えます

たとえば、クライアントでHTTPに取り組んだことがあるRESTAPI。REST、適切に実行された、関連するエンティティを指すハイパーリンクを持つハイパーメディアエンティティを使用します。このクライアントでは、エンティティだけでなくタイプされた(例:ユーザー、アカウント、サブスクリプション)、それらのエンティティへのリンクもタイプされました(ユーザーリンク、アカウントリンク、サブスクリプションリンク)。リンクはUriのラッパーにすぎませんが、タイプが異なるため、試行できませんでしたAccountLinkを使用してユーザーを取得するには、すべてが単純なUris-またはさらに悪いことにstring-であると、これらのエラーは実行時にのみ検出されます。

同様に、あなたの状況では、Operationを識別するという1つの目的にのみ使用されるデータがあります。それ以外のものには使用しないでください。また、作成したランダムな文字列でOperationsを識別しようとするべきではありません。別のクラスを作成すると、コードに可読性と安全性が追加されます。


もちろん、良いものはすべて過剰に使うことができます。検討する

  1. それがあなたのコードにどれだけ明確になるか

  2. 使用頻度

データの「タイプ」(抽象的な意味で)が明確な目的のために、およびコードインターフェイス間で頻繁に使用される場合、冗長性を犠牲にして、別のクラスになるための非常に良い候補です。

34
Paul Draper

一般に、プリミティブと文字列のタイプを作成する必要があることがよくありますが、上記の回答ではほとんどの場合タイプを作成することが推奨されているため、以下の理由/理由をいくつか挙げます。

  • パフォーマンス。ここで実際の言語を参照する必要があります。 C#では、クライアントIDが短く、それをクラスでラップする場合、多くのオーバーヘッドが発生しました。64ビットシステムでは8バイトになり、ヒープに割り当てられるため、速度が向上するためです。
  • 型を仮定するとき。クライアントIDが短く、何らかの方法でそれをパックするロジックがある場合-通常、タイプはすでに想定されています。これらのすべての場所で、抽象化を解除する必要があります。それが1つのスポットである場合、それは大した問題ではありません。それがいたるところにある場合、プリミティブを使用している時間の半分を見つけるかもしれません。
  • すべての言語にtypedefがあるわけではないからです。そして、そうでない言語やすでに書かれているコードの場合、そのような変更を加えることは、バグを引き起こすことさえあるかもしれない大きな仕事になるかもしれません(しかし、誰もが適切なカバレッジのテストスイートを持っていますよね?)。
  • 場合によっては減少読みやすさ。 これをどのように印刷しますか?これをどのように検証しますか?nullをチェックする必要がありますか?型の定義にドリルダウンする必要があるすべての質問です。
10
ytoledano

安全上の理由から、クラスに単純型をラップすることは良い習慣だと思いますか?

時々

これは、より具体的なstringではなくOperationIdentifierを使用することで発生する可能性がある問題を比較検討する必要がある場合の1つです。それらの重大度は何ですか?彼らの可能性は何ですか?

次に、他のタイプを使用するコストを考慮する必要があります。使用するのはどれくらい大変ですか?どれくらいの労力が必要ですか?

場合によっては、Nice具象型を使用することで、時間と労力を節約できます。他の人では、それは問題の価値がありません。

一般的には、こういうことは今以上にすべきだと思います。ドメインに何かを意味するエンティティがある場合、そのエンティティはビジネスとともに変化/成長する可能性が高いため、それを独自のタイプとして持つことをお勧めします。

10
Telastyn

いいえ、「すべて」のタイプ(クラス)を定義しないでください。

しかし、他の回答が述べているように、はしばしばそうするのに役立ちます。あなたは、可能な限り意識的に、適切な型またはクラスの不在により、摩擦が多すぎるという感覚または感覚を発達させなければなりません。あなたのコード。私にとって、摩擦が多すぎるのは、複数のプリミティブ値を単一の値として統合する場合、または値を検証する必要がある場合です(つまり、プリミティブ型のすべての可能な値のうち、どれがプリミティブの有効な値に対応するかを決定します) 「暗黙のタイプ」)。

あなたの質問であなたが提起したものなどの考慮事項が、私のコードの多すぎる設計の原因であることがわかりました。私は、必要以上にコードを書くことを意図的に避ける習慣を身につけました。うまくいけば、コードの適切な(自動化された)テストを作成できます。そうであれば、コードを簡単にリファクタリングし、型またはクラスを追加できます。そうすることで、進行中の開発とコードのメンテナンス

Telastynの答えThomas Junkの答え はどちらも、関連するコードのコンテキストと使用法について非常に優れています。コードの単一ブロック内で値を使用している場合(メソッド、ループ、usingブロックなど)、プリミティブ型を使用するだけで問題ありません。値のセットを繰り返し、他の多くの場所で使用している場合は、プリミティブ型を使用しても問題ありません。ただし、値のセットを使用する頻度が高く、広くなるほど、また、値のセットがプリミティブ型によって表される値に対応する間隔が狭くなるほど、クラスまたは型で値をカプセル化することを検討する必要があります。

7
Kenny Evitt

プリミティブ型のラップの背後にあるアイデア、

  • ドメイン固有の言語を確立するには
  • 誤った値を渡すことにより、ユーザーが行う間違いを制限する

明らかにそれをどこでも行うのは非常に難しく実用的ではありませんが、必要に応じて型をラップすることが重要です。

たとえば、クラスOrderがある場合、

Order
{
   long OrderId { get; set; }
   string InvoiceNumber { get; set; }
   string Description { get; set; }
   double Amount { get; set; }
   string CurrencyCode { get; set; }
}

Orderを検索するための重要なプロパティは、主にOrderIdとInvoiceNumberです。また、AmountとCurrencyCodeは密接に関連しており、Amountを変更せずにCurrencyCodeを変更した場合、注文は有効と見なされなくなります。

したがって、このシナリオではOrderId、InvoiceNumberをラップし、通貨のコンポジットを導入するだけで意味があり、おそらく説明をラップすることは意味がありません。したがって、好ましい結果は次のようになります。

    Order
    {
       OrderIdType OrderId { get; set; }
       InvoiceNumberType InvoiceNumber { get; set; }
       string Description { get; set; }
       Currency Amount { get; set; }
    }

したがって、すべてをラップする理由はありませんが、実際に重要なことです。

5

表面的には、操作を特定するだけでよいように見えます。

さらに、あなたは操作が何をすることになっているのかを言う:

to操作を開始します[および] to終了を監視します

これは「識別情報の使用方法」と同じように言いますが、これらは操作を説明するプロパティだと思います。それは型定義のように聞こえます 「コマンドパターン」と呼ばれるパターンが非常によく適合します。

それは言う

コマンドパターン[...]は、アクションを実行するために必要なすべての情報をカプセル化するために使用されますまたはイベントをトリガーします後で

これは、業務でやりたいことと非常に似ていると思います。 (私が両方の引用符で太字にしたフレーズを比較してください)文字列を返す代わりに、抽象的な意味でのOperationの識別子、たとえばoop内のそのクラスのオブジェクトへのポインターを返します。

コメントについて

このクラスをコマンドと呼び、ロジックを持たないのはwtf-yです。

いいえ、そうではありません。 パターンは非常に抽象的であることを忘れないでくださいので、実際には抽象的であり、多少メタです。つまり、実際の概念ではなく、プログラミング自体を抽象化することがよくあります。コマンドパターンは、関数呼び出しを抽象化したものです。 (またはメソッド)パラメータの値を渡した直後と実行の直前に誰かが一時停止ボタンを押したかのように、後で再開します。

以下はoopを検討していますが、その背後にある動機はどのパラダイムにも当てはまるはずです。ロジックをコマンドに配置することが悪いことと考えられる理由をいくつか挙げたいと思います。

  1. コマンドにそのすべてのロジックがある場合、それは肥大化し、乱雑になります。あなたは「まあ、すべてのこの機能は別のクラスにあるべきだ」と考えているでしょう。コマンドからロジックをrefactorします。
  2. コマンドにそのようなロジックがすべて含まれていると、テストを行ってテストの邪魔になることは困難です。 「なんてこった、なぜこのコマンドをナンセンスに使用しなければならないのですか?この関数が1を出力するかどうかをテストしたいだけです、私はしません後で電話したい、今すぐテストしたい!」
  3. コマンドにそのようなロジックがすべて含まれている場合、完了時にレポートを返すことはあまり意味がありません。デフォルトで同期的に実行される関数について考えた場合、実行が終了したときに通知されても意味がありません。多分あなたはあなたの操作が実際にアバターを映画形式にレンダリングすること(Raspberry Piで追加のスレッドが必要かもしれません)であるかもしれないので別のスレッドを開始しているかもしれません終了時にレポートを返す場合、何らかの非同期性(つまり、単語ですか?)スレッドを実行したり、サーバーに接続したりすることは、コマンドに含めるべきではないロジックだと思います。これはいくぶんメタ的な議論であり、おそらく議論の余地があります。意味が無いと思う方はコメント欄で教えてください。

要約すると、コマンドパターンを使用すると、機能をオブジェクトにラップして、後で実行することができます。モジュール性(コマンドを介して実行されるかどうかに関係なく機能が存在する)、テスト容易性(機能はコマンドなしでテスト可能でなければならない)、および本質的に適切なコードを記述する要求を表す他のすべての流行語のため、実際のロジックは入れませんコマンドに。


パターンは抽象的であるため、優れた現実世界のメタファーを思い付くのは難しい場合があります。ここに試みがあります:

「おばあちゃん、テレビの12時に録画ボタンを押して、チャンネル1のシンプソンズを見逃さないようにしてくれませんか。」

私の祖母は、レコードボタンを押したときに技術的に何が起こるかわかりません。ロジックは別の場所(テレビ内)にあります。そしてそれは良いことです。機能はカプセル化されており、情報はコマンドから隠されています。これはAPIのユーザーであり、必ずしもロジックの一部ではありません。

5
null

不人気な意見:

通常、新しいタイプを定義しないでください!

プリミティブまたは基本クラスをラップするための新しいタイプの定義は、疑似タイプの定義と呼ばれることがあります。 IBMはこれを悪い習慣と説明しています here (この場合、ジェネリックの誤用に特に焦点を当てています)。

疑似型により、一般的なライブラリ機能が役に立たなくなります。

Javaの数学関数は、すべてのプリミティブで機能します。しかし、新しいクラスPercentageを定義する場合(0から1の範囲にある可能性のあるdoubleをラップする)これらの関数はすべて役に立たないため、Percentageクラスの内部表現について知る必要がある(さらに悪い)クラスでラップする必要があります。 。

疑似型がバイラルになる

複数のライブラリを作成するとき、これらの疑似タイプがウイルスに感染することがよくあります。上記のパーセンテージクラスを1つのライブラリで使用する場合は、ライブラリの境界で変換する(すべての安全性/意味/ロジック/その型を作成した理由が失われる)か、これらのクラスを作成する必要があります。他のライブラリにもアクセスできます。単純なdoubleで十分な可能性があるタイプで新しいライブラリを感染させる。

メッセージを持ち帰る

ラップする型が多くのビジネスロジックを必要としない限り、疑似クラスでラップしないことをお勧めします。深刻なビジネス上の制約がある場合のみ、クラスをラップする必要があります。他の場合では、変数に正しく名前を付けることは、意味を伝える上で大きな役割を果たすはずです。

例:

uintはUserIdを完全に表すことができ、uints(==など)にJavaの組み込み演算子を引き続き使用できます。また、ビジネスロジックは必要なく、ユーザーID。

4
Roy T.

すべてのヒントと同様に、ルールをいつ適用するかを知ることはスキルです。型駆動型言語で独自の型を作成すると、型チェックが行われます。したがって、一般的には、これは良い計画です。

NamingConventionは可読性の鍵です。 2つを組み合わせると、意図を明確に伝えることができます。
しかし、///は依然として有用です。

だからはい、私はそれらの寿命がクラスの境界を超えているときに多くの独自のタイプを作成すると言います。また、常に[Class]ではなく、StructClassの両方の使用も検討してください。

3
phil soady