参照によって渡されたオブジェクトの変更は悪い習慣ですか?


12

過去には、通常、作成/更新されるプライマリメソッド内でオブジェクトのほとんどの操作を行ってきましたが、最近は別のアプローチを取っていることがわかりました。

以下に例を示します。Userエンティティを受け入れるリポジトリがあるとしますが、エンティティを挿入する前に、いくつかのメソッドを呼び出して、すべてのフィールドが必要なものに設定されていることを確認します。ここで、メソッドを呼び出してInsertメソッド内からフィールド値を設定するのではなく、挿入前にオブジェクトを整形する一連の準備メソッドを呼び出します。

古い方法:

public void InsertUser(User user) {
    user.Username = GenerateUsername(user);
    user.Password = GeneratePassword(user);

    context.Users.Add(user);
}

新しいメソッド:

public void InsertUser(User user) {
    SetUsername(user);
    SetPassword(user);

    context.Users.Add(user);
}

private void SetUsername(User user) {
    var username = "random business logic";

    user.Username = username;
}

private void SetPassword(User user) {
    var password = "more business logic";

    user.Password = password;
}

基本的に、別のメソッドからプロパティの値を設定する習慣は悪い習慣ですか?


6
ちょうどそれが言ったように...あなたはここで参照によって何も渡していない。値によって参照を渡すことは、まったく同じことではありません。
cHao

4
@cHao:それは違いのない区別です。コードの動作は関係なく同じです。
ロバートハーベイ

2
@JDDavis:基本的に、2つの例の唯一の本当の違いは、ユーザー名の設定アクションとパスワードの設定アクションに意味のある名前を付けたことです。
ロバートハーヴェイ

1
すべての呼び出しはここに参照によってあります。値で渡すには、C#で値型を使用する必要があります。
フランクヒルマン

2
@FrankHileman:すべての呼び出しはここでは値によるものです。これがC#のデフォルトです。渡す参照をして渡すことで参照が異なる獣であり、区別は問題ありません。参照によって渡された場合、コードは呼び出し元の手からそれをヤンクし、単に言うなどで置き換えることができます。useruser = null;
cHao

回答:


10

ここでの問題は、User実際に2つの異なるものを含むことができるということです。

  1. 完全なUserエンティティ。データストアに渡すことができます。

  2. ユーザーエンティティの作成プロセスを開始するために呼び出し元から必要なデータ要素のセット。システムは、上記の#1のように、実際に有効なユーザーになる前にユーザー名とパスワードを追加する必要があります。

これは、オブジェクトモデルに対する文書化されていないニュアンスで構成されており、タイプシステムではまったく表現されていません。開発者としてそれを「知る」だけです。それは素晴らしいことではなく、あなたが遭遇しているような奇妙なコードパターンにつながります。

Userクラスとクラスなど、2つのエンティティが必要であることをお勧めしますEnrollRequest。後者には、ユーザーを作成するために知っておく必要があるすべてのものを含めることができます。Userクラスのように見えますが、ユーザー名とパスワードはありません。次に、これを行うことができます:

public User InsertUser(EnrollRequest request) {
    var userName = GenerateUserName();
    var password = GeneratePassword();

    //You might want to replace this with a factory call, but "new" works here as an example
    var newUser = new User
    (
        request.Name, 
        request.Email, 
        userName, 
        password
    );
    context.Users.Add(user);
    return newUser;
}

発信者は登録情報のみで開始し、挿入された後、完了したユーザーを取得します。この方法により、クラスの変更を回避し、挿入されたユーザーと挿入されていないユーザーとの間でタイプセーフな区別もできます。


2
次のような名前のメソッドには、InsertUser挿入の副作用がある可能性があります。この文脈で「気難しい」が何を意味するのかわかりません。追加されていない「ユーザー」の使用に関しては、ポイントを見逃しているようです。追加されていない場合、それはユーザーではなく、ユーザーを作成するためのリクエストです。もちろん、リクエストを自由に処理できます。
ジョン・ウー

@candiedorangeこれらは副作用ではなく、メソッド呼び出しの望ましい効果です。すぐに追加したくない場合は、ユースケースを満たす別のメソッドを作成します。しかし、それは質問で渡されたユースケースではないので、懸念は重要ではないということです。
アンディ

@candiedorangeカップリングによりクライアントコードが簡単になったら、それでいいと思います。開発者やPOSが将来どのように考えるかは無関係です。その時が来たら、コードを変更します。将来のユースケースを処理できるコードを構築しようとするほど無駄はありません。
アンディ

5
@CandiedOrange実装の正確に定義された型システムを、ビジネスの概念的でわかりやすい英語のエンティティと緊密に結合しないようにすることをお勧めします。「ユーザー」のビジネスコンセプトは、機能的に重要なバリアントを表すために、2つ以上のクラスに実装できます。永続化されていないユーザーには、一意のユーザー名が保証されておらず、トランザクションを関連付けることができるプライマリキーがなく、サインオンすらできないため、重要なバリアントであると言えます。素人にとっては、もちろん、EnrollRequestとUserの両方があいまいな言語での「ユーザー」です。
ジョン・ウー

5

副作用は、予想外の結果が出ない限り問題ありません。そのため、リポジトリにユーザーを受け入れるメソッドがあり、ユーザーの内部状態を変更する場合、一般的に問題はありません。しかし、IMHOのようなメソッド名InsertUser はこれを明確に伝えていないため、エラーが発生しやすくなっています。あなたのレポを使用するとき、私は次のような呼び出しを期待します

 repo.InsertUser(user);

ユーザーオブジェクトの状態ではなく、リポジトリの内部状態を変更します。この問題は両方の実装に存在しますInsertUserが、これは内部的にはまったく関係ありません。

これを解決するには、次のいずれかを行うことができます

  • 挿入とは別の初期化(したがって、呼び出し元はInsertUser完全に初期化されたUserオブジェクトを提供する必要があります。または、

  • ユーザーオブジェクトの構築プロセスに初期化を組み込みます(他のいくつかの回答で示唆されているように)。または

  • メソッドのより明確な名前見つけるようにしてください。

だから、のようなメソッド名を選択しPrepareAndInsertUserOrchestrateUserInsertionInsertUserWithNewNameAndPasswordまたはあなたは副作用がより明確にすることを好むものは何でも。

もちろん、このような長いメソッド名は、メソッドが多すぎる(SRP違反)ことを示している可能性がありますが、これを必要としないか、簡単に修正できない場合があります。


3

私はあなたが選んだ2つのオプションを見ています、そして私は提案された新しい方法よりはるかに古い方法を好むと言わなければなりません。基本的に同じことをしているにもかかわらず、それにはいくつかの理由があります。

いずれの場合も、user.UserNameとを設定していuser.Passwordます。パスワードアイテムについては予約していますが、これらの予約は目下のトピックと密接な関係はありません。

参照オブジェクトの変更の意味

  • 並行プログラミングがより困難になりますが、すべてのアプリケーションがマルチスレッドではありません
  • 特にメソッドについて何も示唆されていない場合、これらの変更は驚くべきことです。
  • これらの驚きはメンテナンスをより困難にする可能性があります

古い方法と新しい方法

古い方法により、テストが簡単になりました。

  • GenerateUserName()独立してテスト可能です。そのメソッドに対してテストを作成し、名前が正しく生成されることを確認できます
  • 名前にユーザーオブジェクトからの情報が必要な場合は、署名を変更してGenerateUserName(User user)そのテスト容易性を維持できます。

新しいメソッドは突然変異を隠します:

  • User2層の深さになるまでオブジェクトが変化していることを知らない
  • Userその場合、オブジェクトの変更はより驚くべきことです
  • SetUserName()ユーザー名を設定するだけではありません。それは広告では真実ではなく、新しい開発者がアプリケーションで物事がどのように機能するかを発見することを難しくします

1

私はジョン・ウーの答えに完全に同意します。彼の提案は良いものです。しかし、それはあなたの直接の質問をわずかに逃しています。

基本的に、別のメソッドからプロパティの値を設定する習慣は悪い習慣ですか?

本質的ではありません。

予期しない動作が発生するため、これをあまりに深く理解することはできません。例えばPrintName(myPerson)、メソッドは既存の値の読み取りにのみ関心があることを示唆しているため、人物オブジェクトを変更すべきではありません。ただし、SetUsername(user)値を設定することを強く示唆しているため、これはあなたの場合とは異なる引数です。


これは、実際にユニット/統合テストでよく使用するアプローチであり、テストする特定の状況に値を設定するためにオブジェクトを変更するメソッドを作成します。

例えば:

var myContract = CreateEmptyContract();

ArrrangeContractDeletedStatus(myContract);

ArrrangeContractDeletedStatusメソッドがmyContractオブジェクトの状態を変更することを明示的に期待しています。

主な利点は、このメソッドを使用すると、異なる初期契約で契約の削除をテストできることです。たとえば、ステータス履歴が長い契約、または以前のステータス履歴がない契約、意図的に誤ったステータス履歴がある契約、テストユーザーが削除できない契約。

単一のメソッドにマージCreateEmptyContractArrrangeContractDeletedStatusていた場合; 削除された状態でテストする契約ごとに、このメソッドの複数のバリアントを作成する必要があります。

そして、私は次のようなことができます:

myContract = ArrrangeContractDeletedStatus(myContract);

これは冗長です(とにかくオブジェクトを変更しているため)、またはmyContractオブジェクトの詳細なクローンを作成するように強制しています。すべてのケースをカバーしたい場合、これは非常に困難です(いくつかのレベルのナビゲーションプロパティが必要かどうかを想像してください。すべてのクローンを作成する必要がありますか?最上位エンティティのみですか?... )

オブジェクトを変更することは、原則としてオブジェクトを変更しないようにするためだけに、より多くの作業を行うことなく、必要なものを取得する最も簡単な方法です。


したがって、あなたの質問に対する直接的な答えは、メソッドが渡されたオブジェクトを変更する責任があることを難読化しない限り、本質的に悪い習慣ではないということです。現在の状況では、メソッド名によってそれが十分に明らかにされています。


0

古いものと新しいものに問題があることがわかりました。

古い方法:

1) InsertUser(User user)

IDEでこのメソッド名がポップアップ表示されると、まず頭に浮かぶのは

ユーザーはどこに挿入されますか?

メソッド名はである必要がありますAddUserToContext。少なくとも、これはメソッドが最終的に行うことです。

2) InsertUser(User user)

これは、最も驚きの少ない原則に明らかに違反しています。たとえば、私はこれまでに自分の作業を行い、aの新たなインスタンスを作成し、User彼にa nameを設定してa を設定しpasswordました。

a)これはユーザーを何かに挿入するだけではありません

b)また、ユーザーをそのまま挿入するという私の意図を覆します。名前とパスワードは上書きされました。

c)これは、単一責任原則の違反でもあることを示していますか。

新しい方法:

1) InsertUser(User user)

依然としてSRPに違反しており、最小限の驚きの原則

2) SetUsername(User user)

質問

ユーザー名を設定しますか?何に?

より良い:SetRandomNameまたは意図を反映するもの。

3) SetPassword(User user)

質問

パスワードを設定してください?何に?

より良い:SetRandomPasswordまたは意図を反映するもの。


提案:

私が読むことを好むのは次のようなものです:

public User GenerateRandomUser(UserFactory Uf){
    User u = Uf.GetUser();
    u.Name = GenerateRandomUserName();
    u.Password = GenerateRandomUserPassword();
    return u;
}

...

public void AddUserToContext(User u){
    this.context.Users.Add(u);
}

最初の質問について:

参照によって渡されたオブジェクトの変更は悪い習慣ですか?

いいえ、それは好みの問題です。

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