web-dev-qa-db-ja.com

オブジェクトを変更するメソッドにオブジェクトを渡すと、それは一般的な(アンチ)パターンですか?

Martin Fowler's Refactoring book で一般的なコードのにおいについて読んでいます。その文脈では、コードベースで見られるパターンについて疑問に思っていました。また、客観的にそれをアンチパターンと見なすことができるかどうかについて考えていました。

パターンとは、オブジェクトが引数として1つ以上のメソッドに渡され、すべてがオブジェクトの状態を変更するパターンですが、オブジェクトを返すものはありません。したがって、(この場合は)C#/。NETの参照渡しの性質に依存しています。

var something = new Thing();
// ...
Foo(something);
int result = Bar(something, 42);
Baz(something);

(特にメソッドに適切な名前が付けられていない場合は特に)オブジェクトの状態が変化したかどうかを理解するために、そのようなメソッドを調べる必要があることがわかりました。複数レベルの呼び出しスタックを追跡する必要があるため、コードの理解がより複雑になります。

そのようなコードを改善して、新しい状態の別の(複製された)オブジェクト、または呼び出しサイトでオブジェクトを変更するために必要なものを返すように提案したいと思います。

var something1 =  new Thing();
// ...

// Let's return a new instance of Thing
var something2 = Foo(something1);

// Let's use out param to 'return' other info about the operation
int result;
var something3 = Bar(something2, out result);

// If necessary, let's capture and make explicit complex changes
var changes = Baz(something3)
something3.Apply(changes);

私にとっては、最初のパターンは仮定に基づいて選択されているようです

  • 作業が少ない、または必要なコード行が少ない
  • オブジェクトの変更、の両方で他の情報を返すことができること
  • Thingのインスタンスが少ないため、より効率的です。

私は代替案を説明しますが、それを提案するには、元の解決策に対する議論を持つ必要があります。元の解決策がアンチパターンであると主張するために、もしあれば、どのような議論をすることができますか?

そして、もしあれば、私の代替ソリューションの何が問題になっていますか?

はい、元のソリューションはあなたが説明する理由からアンチパターンです:それは何が起こっているのかを推論するのを難しくします、オブジェクトはそれ自身の状態/実装(カプセル化の破壊)に責任がありません。また、これらの状態変化はすべてメソッドの暗黙の規約であり、要件の変化に直面してもそのメソッドが脆弱になることも付け加えておきます。

とはいえ、ソリューションにはいくつかの欠点がありますが、最も明らかなのは、オブジェクトのクローン作成が優れていないことです。大きなオブジェクトの場合は遅くなる可能性があります。コードの他の部分が古い参照を保持している場合にエラーが発生する可能性があります(これは、説明するコードベースの場合がそうです)。これらのオブジェクトを明示的に不変にすると、これらの問題の少なくともいくつかは解決されますが、より劇的な変更になります。

オブジェクトが小さく、一時的でない場合(それがオブジェクトを不変性の候補として適切にする場合)は、状態遷移の多くを単にオブジェクト自体に移動する傾向があります。これにより、これらの遷移の実装の詳細を非表示にして、それらの状態遷移が発生する人/内容/場所についてより強い要件を設定できます。

9
Telastyn

メソッドの名前が適切でない場合

実際、thatは実際のコードの匂いです。変更可能なオブジェクトがある場合は、状態を変更するメソッドが提供されます。さらにいくつかのステートメントのタスクに埋め込まれたそのようなメソッドへの呼び出しがある場合、そのタスクを独自のメソッドにリファクタリングしても問題ありません。これにより、説明されている状況が正確に残ります。しかし、FooBarのようなメソッド名がなく、オブジェクトを変更することを明確にするメソッドがある場合、ここでは問題は発生しません。のことを考える

void AddMessageToLog(Logger logger, string msg)
{
    //...
}

または

void StripInvalidCharsFromName(Person p)
{
// ...
}

または

void AddValueToRepo(Repository repo,int val)
{
// ...
}

または

void TransferMoneyBetweenAccounts(Account source, Account destination, decimal amount)
{
// ...
}

またはそのようなもの-これらのメソッドのクローンオブジェクトを返す理由はここにはありません。また、渡されたオブジェクトの状態を変更することを理解するためにそれらの実装を調べる理由もありません。

副作用が必要ない場合は、オブジェクトを不変にしてください。元のオブジェクトを変更せずに、変更された(複製された)オブジェクトを返すように、上記のようなメソッドを強制します。

14
Doc Brown

はい、予期しない側面を指摘している人々の多くの例の1つについては、 http://codebetter.com/matthewpodwysocki/2008/04/30/side-effecting-functions-are-code-smells/ を参照してください効果は悪いです。

一般に、基本的な原則は、ソフトウェアはレイヤーに組み込まれており、各レイヤーは次のレイヤーに可能な限り最もクリーンな抽象化を提示する必要があるということです。そして、明確な抽象化とは、それを使用するために可能な限り心に留めておかなければならないものです。これはモジュール性と呼ばれ、単一の機能からネットワーク化されたプロトコルまですべてに適用されます。

4
btilly

まず、これは「参照渡し」の性質に依存せず、オブジェクトが変更可能な参照型であることによって異なります。非関数型言語では、ほとんどの場合そうなります。

次に、これが問題であるかどうかは、オブジェクトと、さまざまな手順での変更がどの程度緊密に結び付いているかに依存します。Fooでの変更に失敗し、それによってBarがクラッシュする場合は、問題です。コードの匂いは必ずしも必要ではありませんが、Foo、Bar、または何かに問題があります(おそらく、入力をチェックしているはずのBarですが、何かが無効な状態になっている可能性があるため、防止する必要があります)。

それがアンチパターンのレベルに上がるとは言いませんが、むしろ気をつけるべきことです。

3
jmoreno

somethingを変更するA.Do(Something)somethingを変更するsomething.Do()にはほとんど違いがないと私は思います。 どちらかの場合、呼び出されたメソッドの名前からsomethingが変更されることを明確にする必要があります。メソッド名から明確でない場合は、somethingがパラメーター、this、または環境の一部であるかどうかに関係なく、すべきではありませんを変更します。

2
svidgen

いくつかのシナリオではオブジェクトの状態を変更しても問題ないと思います。たとえば、ユーザーのリストがあり、クライアントに返す前にリストにさまざまなフィルターを適用したいとします。

_var users = Dependency.Resolve<IGetUsersQuery>().GetAll();

var excludeAdminUsersFilter = new ExcludeAdminUsersFilter();
var filterByAnotherCriteria = new AnotherCriteriaFilter();

excludeAdminUsersFilter.Apply(users);
filterByAnotherCriteria.Apply(users); 
_

そして、はい、フィルタリングを別のメソッドに移動することでこれをきれいにすることができるので、次のようなものになります。

_var users = Dependency.Resolve<IGetUsersQuery>().GetAll();
Filter(users);
_

Filter(users)は、上記のフィルターで実行されます。

私がこれに出くわした正確な場所を前に覚えていませんが、それはフィルタリングパイプラインと呼ばれたと思います。

1
CodeART

(オブジェクトをコピーする)提案された新しいソリューションがパターンであるかどうかはわかりません。あなたが指摘したように、問題は機能の悪い命名法です。

複雑な数学演算を関数として書いてみましょう f()。私はそれを文書化します f() は、NXNNにマップする関数と、その背後にあるアルゴリズムです。関数の名前が不適切で、文書化されておらず、テストケースもない場合、コードを理解する必要があります。その場合、コードは役に立ちません。

あなたの解決策について、いくつかの観察:

  • アプリケーションはさまざまな側面から設計されています。オブジェクトが値を保持するためだけに使用される場合、またはコンポーネントの境界を越えて渡される場合は、変更方法の詳細を入力するのではなく、オブジェクトの内部を外部から変更する方が賢明です。
  • オブジェクトのクローンを作成すると、メモリ要件が肥大化し、多くの場合、互換性のない状態で同等のオブジェクトが存在します(Xf()の後にYになりましたが、Xは実際にはY、)であり、時間的な不整合がある可能性があります。

対処しようとしている問題は有効です。しかし、膨大なオーバーエンジニアリングが行われても、問題は回避され、解決されません。

0
CMR