これら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);
}
つまり、オブジェクト全体を渡すか、必要なフィールドだけを渡すほうが一般に良いのでしょうか。
どちらも一般的にはどちらよりも優れています。それは、ケースバイケースで行う必要がある判断の呼びかけです。
しかし実際には、この決定を実際に行うことができる位置にいるとき、それはプログラムアーキテクチャ全体のどのレイヤーがオブジェクトをプリミティブに分割すべきかを決定することができるためですあなたは考えるべきですコールスタック全体について、現在使用しているこの1つのメソッドだけではありません。おそらく分割はどこかで行われなければならず、そうすることは意味をなさない(または不必要にエラーが発生しやすい)それは複数回です。問題は、その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コード次第です。 。
これは完全なリストではありませんが、オブジェクトを引数としてメソッドに渡す必要があるかどうかを決定するときは、次の要素のいくつかを考慮してください。
副作用 は、コードの保守性にとって重要な考慮事項です。多くの変更可能なステートフルオブジェクトを含むコードがいたるところに渡されているのを見ると、そのコードは(グローバル状態変数が直感的でなくなることが多いのと同じように)多くの場合直感的でなくなり、デバッグがより困難になり、時間がかかることがよくあります。消費する。
経験則として、メソッドに渡すすべてのオブジェクトが明らかに不変であることを合理的に可能な限り保証することを目指します。
関数呼び出しの結果として引数の状態が変更されると予想される設計を(ここでも、合理的に可能な限り)回避します。このアプローチの最も強力な引数の1つは 最小の驚きの原則)です。 ;つまり、誰かがあなたのコードを読んで、関数に渡された引数を見ると、関数が戻った後にその状態が変化することを期待する可能性が低くなります。
引数リストが過度に長いメソッド(それらの引数のほとんどに「デフォルト」値がある場合でも)は、コードのにおいのように見え始めます。ただし、そのような関数が必要な場合もありますが、Parameter Objectのように機能することのみを目的としたクラスの作成を検討する場合があります。
このアプローチには、「ソース」オブジェクトからパラメーターオブジェクトへの少量の追加のボイラープレートコードマッピングが含まれる可能性がありますが、パフォーマンスと複雑さの両方の点で非常に低コストであり、デカップリングと多くの利点があります。オブジェクトの不変性。
懸念の分離(SoC) について考えてください。メソッドが存在する同じレイヤーまたはモジュールにオブジェクトが「属している」かどうか(たとえば、手巻きのAPIラッパーライブラリ、またはコアビジネスロジックレイヤーなど)は、そのオブジェクトを本当にその方法。
SoCは、クリーンで疎結合のモジュール式コードを記述するための優れた基盤です。たとえば、ORMエンティティオブジェクト(コードとデータベーススキーマの間のマッピング)は、ビジネスレイヤーで渡したり、プレゼンテーション/ UIレイヤーで渡したりしないでください。
「レイヤー」間でデータを渡す場合、メソッドにプレーンデータパラメータを渡すことは、「間違った」レイヤーからオブジェクトを渡すよりも通常は望ましい方法です。代わりにマップできる「正しい」レイヤーに存在する別のモデルを用意することはおそらく良い考えですが。
関数が多くのデータ項目を必要とする場合、その関数があまりにも多くの責任を引き受けているかどうかを検討する価値があるかもしれません。小さいオブジェクトと短くて単純な関数を使用してリファクタリングする潜在的な機会を探します。
場合によっては、データと関数の関係が密接である可能性があります。 Command ObjectまたはQuery Objectが適切な。
「Plain old data」引数の最も強力な引数は、受信クラスがすでに適切に自己完結していることであり、そのメソッドの1つにオブジェクトパラメータを追加すると、クラスが汚染されます(または、クラスがすでに汚染されている場合は、既存のエントロピーを悪化させる)
関数に関して インターフェイス分離の原則 を考慮してください。つまり、オブジェクトを渡す場合、それは、その引数(関数)が実際に必要とする引数のインターフェイスの部分にのみ依存する必要があります。
そのため、関数を作成するときは、関数を呼び出すコードを使用して、いくつかのコントラクトを暗黙的に宣言しています。 「この関数はこの情報を受け取り、それを他の情報に変換します(おそらく副作用あり)」。
したがって、契約が論理的にオブジェクト(それらが実装されている場合でも)または発生するフィールドとこれらの他の一部である必要がありますオブジェクト。どちらの方法でもカップリングを追加しますが、プログラマーとして、それがどこに属するかを決めるのはあなた次第です。
一般的に、それが不明確な場合は、関数が機能するために必要な最小のデータを優先します。関数はオブジェクトにある他のものを必要としないため、これはフィールドだけを渡すことを意味します。ただし、将来的に必然的に変更が加えられたときに影響が小さくなるため、オブジェクト全体を取得する方が正しい場合もあります。
場合によります。
詳しく説明すると、メソッドが受け入れるパラメータは、実行しようとしていることと意味的に一致している必要があります。 EmailInviter
と、invite
メソッドの次の3つの可能な実装を検討してください。
void invite(String emailAddressString) {
invite(EmailAddress.parse(emailAddressString));
}
void invite(EmailAddress emailAddress) {
...
}
void invite(User user) {
invite(user.getEmailAddress());
}
すべての文字列が電子メールアドレスであるとは限らないため、String
をEmailAddress
で渡す必要がある場所に渡すことには問題があります。 EmailAddress
クラスは、メソッドの動作と意味的によく一致します。 ただしUser
を渡すことにも問題があります。これは、いったいEmailInviter
を招待ユーザーに限定する必要があるからです。企業はどうですか?ファイルまたはコマンドラインからメールアドレスを読み取り、それらがユーザーに関連付けられていない場合はどうなりますか?メーリングリスト?リストは続く。
ここでガイダンスとして使用できる警告サインがいくつかあります。 String
やint
のような単純な値型を使用しているが、すべての文字列または整数が有効でない場合や、それらに「特別な」何かがある場合は、より意味のある型を使用する必要があります。オブジェクトを使用していて、ゲッターを呼び出すだけの場合は、代わりにオブジェクトをゲッターに直接渡す必要があります。これらのガイドラインは難しいものでも高速でもありませんが、いくつかのガイドラインは難しいものです。
保守性の観点からは、引数は、できればコンパイラレベルで互いに明確に区別できる必要があります。
// this has exactly one way to call it
insertIntoDatabase(Account ..., Otherthing ...)
// the parameter order can be confused in practice
insertIntoDatabase(long ..., long ...)
最初の設計は、バグの早期検出につながります。 2番目の設計は、テストには現れない微妙な実行時の問題を引き起こす可能性があります。したがって、最初の設計が優先されます。
2つのうち、私の好みは最初の方法です。
function insertIntoDatabase(Account account, Otherthing thing) { database.insertMethod(account.getId(), thing.getId(), thing.getSomeValue()); }
その理由は、変更がそれらのゲッターを保持する限り、変更がオブジェクトの外で透過的である限り、どちらかのオブジェクトに対して行われた変更により、変更およびテストするコードが少なくなり、アプリを中断する可能性が少なくなります。
これは私の思考プロセスにすぎません。主に、私がこの種の物事をどのように機能させ、構造化するかに基づいており、長期的に見て、かなり管理可能で保守可能であることが証明されています。
命名規則については触れませんが、この方法にはWordの「データベース」が含まれていますが、そのストレージメカニズムは将来的に変更される可能性があることを指摘しておきます。示されているコードから、使用されているデータベースストレージプラットフォームに機能を結び付けるものはありません。名前に含まれているので、単に想定しただけです。繰り返しになりますが、これらのゲッターが常に保持されていると仮定すると、これらのオブジェクトの格納方法/場所の変更は簡単です。
関数と2つのオブジェクトについて考え直しますが、これは、2つのオブジェクト構造、特に採用されているゲッターに依存する関数があるためです。また、この関数はこれらの2つのオブジェクトを1つの累積的なものに結び付け、永続化するようにも見えます。私の腸は、3番目のオブジェクトが意味をなすかもしれないと私に言っています。これらのオブジェクトについて、そしてそれらが実際にどのように関連しているか、予想されるロードマップとどのように関連しているかを知る必要があります。しかし、私の腸はその方向に傾いています。
コードが今立っているので、質問は「この関数はどこにあるべきか?それはアカウントの一部ですか、それともその他のものですか?どこに行くの?
私はすでに3番目のオブジェクト「データベース」があると思います、そして私はこの関数をそのオブジェクトに入れることに傾いています、そしてそれはそれがオブジェクトがAccountとOtherThingを処理し、変換し、そして結果を永続化できるようになるということになります。
その3番目のオブジェクトがオブジェクトリレーショナルマッピング(ORM)パターンに準拠するようにする場合は、なお良いでしょう。これで、コードを操作する人なら誰でも「ああ、これがAccountとOtherThingがぶつかり合って永続化する場所だ」と理解するのに非常に明白になります。
ただし、アカウントとOtherThingを組み合わせて変換するジョブを処理するが、永続化のメカニズムは処理しない4番目のオブジェクトを導入することも意味があります。これらの2つのオブジェクトとの間、またはそれらの間の相互作用がさらに増えると予想される場合は、永続性のみを管理するオブジェクトに永続化ビットを分解して欲しいので、そうします。
私は、Account、OtherThing、または3番目のORMオブジェクトのいずれか1つを、他の3つも変更せずに変更できるように設計を維持するために撮影します。正当な理由がない限り、AccountとOtherThingは独立していて、お互いの内部構造や構造を知る必要がないようにしたいと思います。
もちろん、これがどうなるかという完全なコンテキストを知っていれば、自分の考えを完全に変えるかもしれません。繰り返しますが、これは私がこのようなものを見たときに私がどのように考えるか、そしてどのように無駄のないものかです。
どちらのアプローチにも長所と短所があります。シナリオで何が良いかは、現在のユースケースに大きく依存します。
Pro複数のパラメーター、Conオブジェクト参照:
Proオブジェクト参照:
それで、何を使用する必要があるか、そしていつユースケースに大きく依存するか
クリーンコードでは、引数の数をできるだけ少なくすることをお勧めします。つまり、通常はObjectの方が優れたアプローチであり、ある程度の意味があると思います。なぜなら
insertIntoDatabase(new Account(id) , new Otherthing(id, "Value"));
より読みやすい呼び出しです
insertIntoDatabase(myAccount.getId(), myOtherthing.getId(), myOtherthing.getValue() );
オブジェクトの構成状態ではなく、オブジェクトを渡します。これは、カプセル化とデータ非表示のオブジェクト指向の原則をサポートしています。必要のないさまざまなメソッドインターフェイスでオブジェクトの内部を公開すると、コアOOPの原則に違反します。
Otherthing
のフィールドを変更するとどうなりますか?タイプを変更したり、フィールドを追加したり、フィールドを削除したりする場合があります。これで、質問で言及したようなすべてのメソッドを更新する必要があります。オブジェクトを渡す場合、インターフェースの変更はありません。
オブジェクトのフィールドを受け入れるメソッドを書く必要があるのは、オブジェクトを取得するメソッドを書くときだけです。
public User getUser(String primaryKey) {
return ...;
}
そのメソッドを呼び出す目的はオブジェクトを取得することなので、その呼び出しを行った時点では、呼び出し元のコードにはまだオブジェクトへの参照がありません。
一方には、アカウントとその他のオブジェクトがあります。一方、アカウントのIDとOtherthingのIDを指定して、データベースに値を挿入することができます。それが与えられた二つのことです。
AccountとOtherthingを引数に取るメソッドを記述できます。プロ側では、発信者はアカウントやその他に関する詳細を知る必要はありません。否定的な面では、呼び出し先はAccountとOtherthingのメソッドについて知る必要があります。また、Otherthingオブジェクトの値以外の何かをデータベースに挿入する方法はなく、アカウントオブジェクトのIDはあるがオブジェクト自体がない場合は、このメソッドを使用する方法はありません。
または、2つのIDと値を引数として取るメソッドを作成することもできます。否定的な面では、発信者はアカウントとその他の詳細を知る必要があります。また、データベースに挿入するIDだけでなく、アカウントやその他の詳細情報が実際に必要になる場合もあります。その場合、このソリューションはまったく役に立ちません。一方で、うまくいけば、呼び出し先ではアカウントやその他の知識は必要なく、柔軟性が高まります。
あなたの判断の呼びかけ:より柔軟性が必要ですか?多くの場合、これは1回の呼び出しではなく、すべてのソフトウェアで一貫しています。ほとんどの場合、アカウントのIDを使用するか、オブジェクトを使用します。それを混ぜると、両方の世界で最悪の事態になります。
C++では、2つのIDと値を取得するメソッドと、AccountとOtherthingを取得するインラインメソッドを使用できるため、オーバーヘッドがゼロで両方の方法を使用できます。