web-dev-qa-db-ja.com

参照渡しされたオブジェクトを変更することは悪い習慣ですか?

以前は、オブジェクトの操作のほとんどは、作成/更新されているプラ​​イマリメソッド内で行ってきましたが、最近は別のアプローチをとっていることに気づき、それが悪い習慣であるかどうか知りたいと思っています。

ここに例があります。 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;
}

基本的に、別の方法でプロパティの値を設定することは悪い習慣ですか?

12
JD Davis

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

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

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

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

たとえば、2つのエンティティが必要になることをお勧めします。 Userクラスと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;
}

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

10
John Wu

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

 repo.InsertUser(user);

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

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

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

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

  • 単純にメソッドのより良い名前を見つけるを試してみてください。

したがって、PrepareAndInsertUserOrchestrateUserInsertionInsertUserWithNewNameAndPasswordなどのメソッド名、または副作用をより明確にしたい任意の名前を選択します。

もちろん、このように長いメソッド名は、メソッドが多すぎる(SRP違反)ことを示していますが、場合によってはこれを望まないか、簡単に修正できないことがあります。これは、最も煩わしくない実用的なソリューションです。

5
Doc Brown

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

どちらの場合でも、_user.UserName_および_user.Password_を設定します。パスワードアイテムについて予約がありますが、それらの予約は現在のトピックに密接に関係していません。

参照オブジェクトの変更の影響

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

古い方法と新しい方法

古い方法では、物事をテストしやすくなりました。

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

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

  • Userオブジェクトが2層の深さまで変化することはわかりません
  • その場合、Userオブジェクトへの変更はさらに驚くべきことです
  • SetUserName()は、ユーザー名を設定するだけではありません。これは広告の真実ではないため、新規開発者はアプリケーションでの動作を発見することが難しくなります。
3
Berin Loritsch

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

基本的に、別の方法でプロパティの値を設定することは悪い習慣ですか?

本質的にはありません。

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


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

例えば:

var myContract = CreateEmptyContract();

ArrrangeContractDeletedStatus(myContract);

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

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

CreateEmptyContractArrrangeContractDeletedStatusを1つのメソッドにマージした場合。削除された状態でテストするすべての異なるコントラクトに対して、このメソッドの複数のバリアントを作成する必要があります。

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

myContract = ArrrangeContractDeletedStatus(myContract);

これは冗長です(とにかくオブジェクトを変更しているため)、またはmyContractオブジェクトのディープクローンを作成するように強制しています。これは、すべてのケースをカバーしたい場合は非常に困難です(複数のレベルに相当するナビゲーションプロパティが必要かどうかを想像してください。それらすべてを複製する必要がありますか?最上位のエンティティのみをコピーする必要がありますか?...非常に多くの質問、非常に多くの暗黙の期待)

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


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

1
Flater

古いものも新しいものも問題があると思います。

古い方法:

1)InsertUser(User user)

IDEにポップアップするこのメソッド名を読んだとき、最初に頭に浮かぶのは

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

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

2)InsertUser(User user)

これは、最小の驚きの原則に明らかに違反しています。たとえば、私はこれまでに自分の作業を行い、Userの新たなインスタンスを作成してnameを与え、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);
}

最初の質問について:

参照渡しされたオブジェクトを変更することは悪い習慣ですか?

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

0
Thomas Junk