通常、オブジェクトまたはそのメンバー変数を関数に送信しますか?


31

これらの2つのケースの間で一般的に受け入れられている方法は次のとおりです。

function insertIntoDatabase(Account account, Otherthing thing) {
    database.insertMethod(account.getId(), thing.getId(), thing.getSomeValue());
}

または

function insertIntoDatabase(long accountId, long thingId, double someValue) {
    database.insertMethod(accountId, thingId, someValue);
}

言い換えれば、一般的にオブジェクト全体を渡すか、必要なフィールドだけを渡す方が良いでしょうか?


5
関数の目的と、問題のオブジェクトにどのように関連する(または関連しない)かに完全に依存します。
メタファイト

それが問題です。どちらを使用するかはわかりません。どちらのアプローチにも対応するために、常にコードを変更できると感じています。
AJJ

1
APIの用語で(そして実装をまったく見ていません)、前者は抽象的でドメイン指向です(これは良い)が、後者はそうではありません(悪い)。
エリックエイド

1
最初のアプローチは、より多くの3層オブジェクト指向です。しかし、メソッドから単語データベースを削除することにより、さらに重要になります。「Store」または「Persist」で、AccountまたはThingのいずれか(両方ではない)を行う必要があります。この層のクライアントとして、記憶媒体を意識するべきではありません。アカウントを取得するとき、目的のオブジェクトを識別するためにidまたはプロパティ値(フィールド値ではない)の組み合わせを渡す必要があります。または、すべてのアカウントを渡す列挙メソッドを実装します。
マーティンマート

1
通常、両方とも間違っています(または、むしろ最適ではありません)。オブジェクトをデータベースにシリアル化する方法は、通常、オブジェクトのメンバー変数に直接依存するため、オブジェクトのプロパティ(メンバー関数)である必要があります。オブジェクトのメンバーを変更する場合は、シリアル化方法も変更する必要があります。それがオブジェクトの一部である場合、よりうまく機能します
-tofro

回答:


24

一般的に、どちらも他より優れているわけではありません。ケースバイケースで行わなければならない判断の呼び出しです。

しかし実際には、あなたが実際にこの決定を下せる立場にいるとき、それはあなたがプログラム全体のアーキテクチャのどの層がオブジェクトをプリミティブに分割すべきかを決定するからです。そのため、呼び出し全体について考える必要があります。あなたが現在いるこの1つの方法だけでなく、stack。おそらく分割はどこかで行われなければならず、それを2回以上行うのは理にかなっていないでしょう(または不必要にエラーを起こしやすいでしょう)。問題は、その1つの場所がどこにあるかです。

この決定をする最も簡単な方法は、オブジェクトが変更された場合、どのコードを変更する必要があるか、または変更する必要がないかを考えることです。例を少し拡張してみましょう。

function addWidgetButtonClicked(clickEvent) {
    // get form data
    // get user's account
    insertIntoDatabase(account, data);
}
function insertIntoDatabase(Account account, Otherthing data) {
    // open database connection
    // check data doesn't already exist
    database.insertMethod(account.getId(), data.getId(), data.getSomeValue());
}

function addWidgetButtonClicked(clickEvent) {
    // get form data
    // get user's account
    insertIntoDatabase(account.getId(), data.getId(), data.getSomeValue());
}
function insertIntoDatabase(long accountId, long dataId, double someValue) {
    // open database connection
    // check data doesn't already exist
    database.insertMethod(accountId, dataId, someValue);
}

最初のバージョンでは、UIコードは盲目的にdataオブジェクトを渡していて、そこから有用なフィールドを抽出するのはデータベースコード次第です。2番目のバージョンでは、UIコードはdataオブジェクトを有用なフィールドに分割し、データベースコードはそれらがどこから来たかを知らずに直接それらを受け取ります。重要な意味はdataオブジェクトの構造が何らかの方法で変更される場合、最初のバージョンではデータベースコードのみを変更する必要があり、2番目のバージョンではUIコードのみを変更する必要があるということです。これら2つのうちどちらが正しいかは、dataオブジェクトに含まれるデータの種類に大きく依存しますが、通常は非常に明白です。たとえば、dataは「20/05/1999」のようなユーザー指定の文字列Dateです。渡す前に適切な型に変換するのはUIコード次第です。


8

これは完全なリストではありませんが、オブジェクトを引数としてメソッドに渡す必要があるかどうかを決定する際には、次の要因のいくつかを考慮してください。

オブジェクトは不変ですか?関数は「純粋」ですか?

副作用は、コードの保守性に関する重要な考慮事項です。多くの可変ステートフルオブジェクトがいたるところに渡されているコードを見ると、そのコードは直観的ではないことがよくあり(グローバルステート変数が直観的でないことが多いのと同じように)、デバッグはより困難で時間がかかります。消費する。

経験則として、合理的に可能な限り、メソッドに渡すオブジェクトは明らかに不変であることを保証することを目指します。

関数の呼び出しの結果として引数の状態が変更されることが予想される設計を(ここでも、合理的に可能な限り)回避します。このアプローチの最も強力な引数の1つは、Least Astonishmentの原則です。つまり、誰かがあなたのコードを読んで、関数に渡された引数を見ると、関数が返された後に状態が変化することを期待する可能性は「低く」なります。

メソッドにはすでにいくつの引数がありますか?

過度に長い引数リストを持つメソッドは(それらの引数のほとんどが「デフォルト」値を持っているとしても)コード臭のように見え始めます。ただし、そのような関数が必要な場合があり、パラメーターオブジェクトのように動作することを唯一の目的とするクラスの作成を検討することもできます。

このアプローチには、「ソース」オブジェクトからパラメーターオブジェクトへの少量の追加ボイラープレートコードマッピングが含まれますが、パフォーマンスと複雑さの両方の点で非常に低コストですが、デカップリングに関して多くの利点がありますおよびオブジェクトの不変性。

渡されたオブジェクトは、アプリケーション内の「レイヤー」にのみ属していますか(たとえば、ViewModel、またはORMエンティティ?)

懸念の分離(SoC)について考えてください。オブジェクトがメソッドが存在するのと同じレイヤーまたはモジュール(たとえば、手巻きAPIラッパーライブラリ、コアビジネスロジックレイヤーなど)に「属する」かどうかを自問して、そのオブジェクトを本当に渡す必要があるかどうかを知ることができます。方法。

SoCは、クリーンで疎結合のモジュラーコードを記述するための優れた基盤です。たとえば、ORMエンティティオブジェクト(コードとデータベーススキーマ間のマッピング)は、ビジネスレイヤーやプレゼンテーション/ UIレイヤーで渡すのが理想的ではありません。

「レイヤー」間でデータを渡す場合は、通常、「間違った」レイヤーからオブジェクトを渡すよりも、プレーンデータパラメーターをメソッドに渡す方が適しています。ただし、代わりにマップできる「右」レイヤーに存在する個別のモデルを用意することをお勧めします。

関数自体が大きすぎたり複雑すぎたりしていますか?

関数が多くのデータ項目を必要とする場合、その関数が多くの責任を負っているかどうかを検討する価値があります。より小さなオブジェクトとより短くシンプルな関数を使用してリファクタリングする潜在的な機会を探します。

関数はコマンド/クエリオブジェクトである必要がありますか?

場合によっては、データと関数の関係が密接になることがあります。これらの場合、コマンドオブジェクトまたはクエリオブジェクトのどちらが適切かを検討してください。

オブジェクトパラメータをメソッドに追加すると、それを含むクラスに新しい依存関係が強制されますか?

「Plain old data」引数の最も強力な引数は、単に受信クラスがすでに自己完結していることであり、メソッドの1つにオブジェクトパラメータを追加すると、クラスが汚染されます(または、クラスが既に汚染されている場合、既存のエントロピーを悪化させる)

本当に完全なオブジェクトを渡す必要がありますか、それともそのオブジェクトのインターフェースのごく一部だけが必要ですか?

考えてみましょうインタフェース棲み分け原理をあなたの機能に関して-すなわちオブジェクトを渡したときに、それが唯一のそれ(機能)が実際に必要とその引数のインターフェイスの部分に依存しなければなりません。


5

したがって、関数を作成するときは、それを呼び出しているコードでいくつかのコントラクトを暗黙的に宣言しています。「この関数はこの情報を取得し、この情報を他の情報に変換します(おそらく副作用を伴う)。」

そのため、契約が論理的にオブジェクト(実装されている場合でも)であるのか、またはたまたまこれらの他のオブジェクトの一部であるフィールドであるのか。どちらの方法でもカップリングを追加しますが、プログラマーとして、それがどこに属するかを決めるのはあなた次第です。

一般に、不明な場合は、関数が機能するために必要な最小のデータを優先します。関数はオブジェクトにある他のものを必要としないため、フィールドのみを渡すことを意味することがよくあります。ただし、オブジェクト全体を取得する方が正しい場合があります。これは、将来物事が必然的に変化した場合の影響が小さくなるためです。


メソッド名をinsertAccountIntoDatabaseに変更しないのか、他の型を渡すのですか?objを使用する特定の数の引数では、コードが読みやすくなります。あなたの場合、メソッド名がどうやってそれをやるのではなく、私が挿入しようとしているものをクリアするかどうかを考えます。
ライヴ

3

場合によります。

詳しく説明すると、メソッドが受け入れるパラメーターは、実行しようとしているものと意味的に一致する必要があります。メソッドのEmailInviter3つの可能な実装を検討してくださいinvite

void invite(String emailAddressString) {
  invite(EmailAddress.parse(emailAddressString));
}
void invite(EmailAddress emailAddress) {
  ...
}
void invite(User user) {
  invite(user.getEmailAddress());
}

すべての文字列が電子メールアドレスであるとは限らないためString、を渡す必要がある場所に渡すことにEmailAddressは欠陥があります。このEmailAddressクラスは、メソッドの動作により意味的にマッチします。しかしUser、なぜEmailInviterユーザーを招待することに限定する必要があるのか​​という理由で、aを渡すことにも問題があります。ビジネスはどうですか?ファイルまたはコマンドラインから電子メールアドレスを読み取り、それらがユーザーに関連付けられていない場合はどうなりますか?メーリングリスト?リストは続きます。

ここでガイダンスとして使用できる警告サインがいくつかあります。あなたのような単純な値のタイプを使用している場合String、またはintそれらについてではなく、すべての文字列またはint型が有効であるか、そこの何か「特別な」、あなたはより意味のあるタイプを使用する必要があります。オブジェクトを使用していて、ゲッターを呼び出すだけの場合は、代わりにゲッターでオブジェクトを直接渡す必要があります。これらのガイドラインは難しいものでも速いものでもありませんが、ほとんどありません。


0

Clean Codeは、できるだけ少ない引数を持つことを推奨しています。つまり、通常はObjectの方がより良いアプローチであり、意味があると思います。なぜなら

insertIntoDatabase(new Account(id) , new Otherthing(id, "Value"));

より読みやすい呼び出しです

insertIntoDatabase(myAccount.getId(), myOtherthing.getId(), myOtherthing.getValue() );

そこに同意することはできません。2つは同義語ではありません。メソッドに渡すためだけに2つの新しいオブジェクトインスタンスを作成するのはよくありません。どちらかのオプションの代わりにinsertIntoDatabase(myAccount、myOtherthing)を使用します。
jwenting

0

オブジェクトを構成する状態ではなく、オブジェクトの周りを渡します。これは、カプセル化とデータ隠蔽のオブジェクト指向の原則をサポートします。必要のないさまざまなメソッドインターフェイスでオブジェクトの内部を公開すると、OOPのコア原則に違反します。

のフィールドを変更するとどうなりますOtherthingか?タイプを変更したり、フィールドを追加したり、フィールドを削除したりするかもしれません。ここで、質問で言及したようなすべてのメソッドを更新する必要があります。オブジェクトを迂回する場合、インターフェースの変更はありません。

オブジェクトのフィールドを受け入れるメソッドを記述する必要があるのは、オブジェクトを取得するメソッドを記述するときだけです。

public User getUser(String primaryKey) {
  return ...;
}

その呼び出しを行う時点では、そのメソッドを呼び出すポイントはオブジェクトを取得することであるため、呼び出し元のコードにはまだオブジェクトへの参照がありません。


1
「フィールドを変更するとどうなりますOtherthingか?」(1)それは、オープン/クローズド原則に違反します。(2)オブジェクト全体を渡しても、その中のコードはそのオブジェクトのメンバーにアクセスします(そうでない場合、なぜオブジェクトを渡すのでしょうか?)
David Arno

@DavidArno私の答えのポイントは、何も壊れないということではありませが、壊れることは少ないでしょう。また、最初の段落も忘れないでください。どのブレークが発生しても、オブジェクトの内部状態はオブジェクトのインターフェイスを使用して抽象化する必要があります。内部状態を渡すことは、OOPの原則に違反しています。

0

保守性の観点からは、引数はコンパイラレベルで互いに明確に区別できる必要があります。

// this has exactly one way to call it
insertIntoDatabase(Account ..., Otherthing ...)

// the parameter order can be confused in practice
insertIntoDatabase(long ..., long ...)

最初の設計は、バグの早期発見につながります。2番目の設計では、テストには現れない微妙なランタイムの問題が発生する可能性があります。したがって、最初の設計が優先されます。


0

2つのうち、私の好みは最初の方法です。

function insertIntoDatabase(Account account, Otherthing thing) { database.insertMethod(account.getId(), thing.getId(), thing.getSomeValue()); }

その理由は、変更がそれらのゲッターを保持し、変更がオブジェクトの外部で透過的である限り、いずれかのオブジェクトに変更が加えられるため、変更およびテストするコードが少なくなり、アプリを中断する可能性が少なくなるためです。

これは単なる思考プロセスであり、主に私がこのような性質のものをどのように動作させ、構築するのが好きで、長期的には非常に管理しやすく維持できることが証明されています。

命名規則には触れませんが、この方法には「データベース」という単語が含まれていますが、そのストレージメカニズムは将来変更される可能性があることを指摘します。示されたコードから、使用されているデータベースストレージプラットフォームに関数を結び付けるものはありません。それがデータベースであってもです。それは名前にあるからだと思いました。繰り返しますが、これらのゲッターが常に保持されると仮定すると、これらのオブジェクトの保存方法/場所の変更は簡単です。

ただし、2つのオブジェクト構造、特に採用されているゲッターに依存する関数があるため、関数と2つのオブジェクトを再考します。また、この関数は、これらの2つのオブジェクトを1つの累積的なものに結び付けて永続化するように見えます。私の腸は、3番目のオブジェクトが意味をなすかもしれないと言っています。これらのオブジェクトと、それらが実際にどのように関連しているか、および予想されるロードマップについてさらに知る必要があります。しかし、私の腸はその方向に傾いています。

現在のコードでは、「この関数はどこにあるべきですか?」アカウントの一部ですか、それともその他のものですか?どこに行くの?

すでに3番目のオブジェクト「データベース」があり、この関数をそのオブジェクトに入れることに傾いていると思います。それから、そのオブジェクトがAccountとOtherThingを処理し、変換し、結果を永続化できるようになります。

3番目のオブジェクトがオブジェクトリレーショナルマッピング(ORM)パターンに準拠するようにした場合、さらに良い結果が得られます。これにより、コードで作業している人なら誰でも、「ああ、これはAccountとOtherThingが一緒に壊されて永続化される場所」を理解することが非常に明白になります。

しかし、AccountとOtherThingを組み合わせて変換するジョブを処理するが、永続化の仕組みを処理しない4番目のオブジェクトを導入することも意味があります。これらの2つのオブジェクトとの間で、またはこれらの間でさらに多くの相互作用が予想される場合は、それが大きくなると、永続性ビットを永続性のみを管理するオブジェクトに含めることを望みます。

Account、OtherThing、または3番目のORMオブジェクトのいずれか1つを、他の3つも変更せずに変更できるように設計を維持するために撮影します。正当な理由がない限り、AccountとOtherThingは独立していて、お互いの内部の仕組みや構造を知る必要はありません。

もちろん、これがどうなるかについて完全な文脈を知っていれば、私の考えを完全に変えるかもしれません。繰り返しますが、これは私がこのようなものを見たときの私が考える方法であり、どのように無駄がありません。


0

どちらのアプローチにも長所と短所があります。シナリオで優れているのは、手元のユースケースに大きく依存します。


Pro複数のパラメーター、Conオブジェクト参照:

  • 特定のクラスにバインドされていない呼び出し元は、異なるソースから値を完全に渡すことができます
  • オブジェクトの状態は、メソッドの実行中に予期せず変更されることはありません

プロオブジェクトリファレンス:

  • メソッドがオブジェクト参照型にバインドされていることを明確にインターフェイスし、無関係な /無効な値を誤って渡すことを困難にします
  • フィールド/ゲッターの名前を変更するには、その実装だけでなく、メソッドのすべての呼び出しで変更が必要です
  • 場合は、新しいプロパティが追加されるとニーズが渡され、変更はメソッドのシグネチャでは必要ありません
  • メソッドはオブジェクトの状態を変更できます
  • 同様のプリミティブ型の変数を渡しすぎると、順序に関して呼び出し元が混乱します(Builderパターンの問題)

したがって、何を使用する必要があり、いつ使用ケースに依存するか

  1. 個々のパラメーターを渡す:一般に、メソッドがオブジェクトタイプとは関係ない場合は、より多くのユーザーに適用できるように、個々のパラメーターリストを渡す方が適切です。
  2. 新しいモデルオブジェクトの導入:パラメーターのリストが大きくなる場合(3つ以上)、呼び出されるAPIに属する新しいモデルオブジェクトを導入することをお勧めします(ビルダーパターンが望ましい)
  3. オブジェクト参照を渡す:メソッドがドメインオブジェクトに関連している場合、オブジェクト参照を渡す方が保守性と読みやすさの観点から優れています。

0

一方には、AccountオブジェクトとOtherthingオブジェクトがあります。一方、アカウントのIDとその他のIDを指定すると、データベースに値を挿入できます。それが2つの与えられたものです。

AccountおよびOtherthingを引数として取るメソッドを作成できます。プロ側では、発信者はアカウントやその他に関する詳細を知る必要はありません。マイナス面では、呼び出し先はAccountおよびOtherthingのメソッドを知る必要があります。また、Otherthingオブジェクトの値以外にデータベースに何かを挿入する方法はなく、アカウントオブジェクトのIDを持っているがオブジェクト自体は持っていない場合、このメソッドを使用する方法もありません。

または、2つのIDと値を引数として取るメソッドを作成できます。マイナス面では、発信者はアカウントとその他の詳細を知る必要があります。また、データベースに挿入するためにidだけでなくAccountやOtherthingの詳細が実際に必要な場合もあります。その場合、このソリューションはまったく役に立ちません。一方で、できれば、呼び出し先でAccountやOtherthingの知識が必要でなく、より柔軟性があることを願っています。

あなたの判断:より柔軟性が必要ですか?多くの場合、これは1回の呼び出しの問題ではありませんが、すべてのソフトウェアで一貫しています。ほとんどの場合、アカウントのIDを使用するか、オブジェクトを使用します。それを混ぜると、両方の世界で最悪の事態になります。

C ++では、2つのidと値を取得するメソッドと、AccountとOtherthingを取得するインラインメソッドを使用できるため、両方の方法でオーバーヘッドがゼロになります。

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