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


17

Martin Fowlerの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

私は代替案を示しますが、それを提案するには、元の解決策に反対する議論が必要です。元のソリューションがアンチパターンであるという主張をするために、もしあれば、どのような議論ができますか?

そして、もしあれば、私の代替ソリューションで何が間違っていますか?



1
@DaveHillierありがとう、私はこの用語に精通していましたが、つながりはありませんでした。
ミシェルヴァンオーステルハウト

回答:


9

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

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

オブジェクトが小さく一時的でない場合(不変性の良い候補になる)、私は単純に、より多くの状態遷移をオブジェクト自体に移動する傾向があります。これにより、これらの遷移の実装の詳細を隠し、それらの状態遷移が誰/何/どこで発生するかについてより強力な要件を設定できます。


1
たとえば、「ファイル」オブジェクトがある場合、状態変更メソッドをそのオブジェクトに移動しようとはしません。これはSRPに違反します。「File」のようなライブラリクラスの代わりに独自のクラスがある場合でも有効です。すべての状態遷移ロジックをオブジェクトのクラスに入れても意味がありません。
Doc Brown

@Tetastynこれは古い答えであることは知っていますが、最後の段落の提案を具体的に視覚化するのに苦労しています。詳しく説明してください、または例を挙げてください。
AaronLS

@AaronLS-の代わりにBar(something)(およびの状態を変更してsomething)、の型のBarメンバーを作成しsomethingます。something.Bar(42)が変化する可能性が高くなりsomethingますが、オブジェクト指向ツール(プライベート状態、インターフェースなど)を使用してsomethingの状態を保護することもできます
Telastyn

14

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

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

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

または

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

または

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

または

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

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

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


そのとおりです。名前変更メソッドのリファクタリングは、副作用を明確にすることで状況を改善できます。ただし、変更によって簡潔なメソッド名が不可能になると、困難になる可能性があります。
ミシェルヴァンオーステルハウト

2
@michielvoo:簡潔なメソッドの命名ができないように思われる場合、メソッドは、タスクの機能の抽象化を構築するのではなく、間違ったものをグループ化します(そして、副作用の有無にかかわらず)。
Doc Brown

4

はい、http://codebetter.com/matthewpodwysocki/2008/04/30/side-effecting-functions-are-code-smells/を参照してください。予期しない副作用が悪いことを指摘している多くの例の1つです。

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


OPが「予想される副作用」として説明しているものを特徴付けます。たとえば、リスト内の各要素を操作する何らかのエンジンに渡すことができるデリゲート。それは基本的にForEach<T>そうです。
ロバートハーベイ

@RobertHarveyメソッドの名前が適切に指定されていないことや、副作用を把握するためにコードを読み取らなければならないことについての不満は、間違いなく予想される副作用ではありません。
btilly

それを認めます。しかし、当然の結果として、適切な名前のドキュメント化されたメソッドは、予想されるサイト効果を備えており、結局アンチパターンではないかもしれません。
ロバートハーベイ

@RobertHarvey同意します。重要なことは、重大な副作用を知ることが非常に重要であり、慎重に文書化する必要があることです(メソッドの名前で)。
btilly

予期しない副作用と非自明な副作用が混在していると思います。リンクをありがとう。
ミシエルヴァンオーステルハウト

3

まず第一に、これは「参照渡しの性質」に依存せず、可変参照型のオブジェクトに依存します。機能しない言語では、ほとんど常にそうなります。

第二に、これが問題であるかどうかは、オブジェクトと、異なるプロシージャの変更がどれだけ密接に結び付けられているかに依存します-Fooで変更を行わずにBarがクラッシュする場合、それは問題です。必ずしもコードの匂いではありませんが、FooまたはBarまたはSomethingの問題です(おそらく、入力をチェックする必要があるため、Barが無効な状態になり、それを防ぐ必要がある可能性があります)。

アンチパターンのレベルにまで上昇したとは言いませんが、注意する必要があります。


2

A.Do(Something)変更somethingsomething.Do()変更の間にはほとんど違いがないと私は主張しsomethingます。いずれかの場合、それが呼び出されたメソッドの名前から明らかであるべきでsomething修正されます。メソッド名から明確でない場合somethingは、パラメータでthisあるか、環境の一部であるかに関係なく、変更しないください


1

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

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)上記のフィルターを実行する場所。

これまでどこで出会ったのか正確には覚えていませんが、フィルタリングパイプラインと呼ばれていました。


0

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

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

ソリューションについて、いくつかの観察:

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

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


2
あなたの観察をOPの質問に関連付けた場合、これはより良い答えになります。そのままでは、答えというよりもコメントです。
ロバートハーベイ

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