修改通过引用传递的对象是不好的做法吗?


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:从根本上说,两个示例之间的唯一真正区别是,已为设置的用户名和设置的密码操作赋予了有意义的名称。
罗伯特·哈维

1
您的所有来电均在此处作为参考。您必须在C#中使用值类型来按值传递。
弗兰克·希勒曼

2
@FrankHileman:所有的调用都按值进行。这是C#中的默认设置。传递一个参考,并经过基准是不同的东西,并区分事情做。如果user是通过引用传递的,则该代码可以将其从调用者手中移出,并通过简单地说出来代替它user = null;
cHao

Answers:


10

这里的问题是a User实际上可以包含两个不同的东西:

  1. 完整的用户实体,可以将其传递到您的数据存储。

  2. 调用者需要的一组数据元素,才能开始创建User实体的过程。系统必须先添加用户名和密码,然后才能像上述#1那样是真正的有效用户。

这包括对对象模型的未记录的细微差别,而在类型系统中根本没有表达。您只需要作为开发人员“知道”它即可。那不是很好,并且会导致您遇到类似奇怪的代码模式。

我建议您需要两个实体,例如一个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;
}

呼叫者仅从注册信息开始,并在插入完成的用户后取回该用户。这样,您可以避免更改任何类,并且在插入的用户与未插入的用户之间也具有类型安全的区别。


2
名称为like的InsertUser方法可能会产生插入的副作用。在这种情况下,我不确定“ icky”是什么意思。至于使用尚未添加的“用户”,您似乎已经错过了重点。如果尚未添加,则它不是用户,而只是创建用户的请求。当然,您可以自由处理请求。
John Wu

@candiedorange这些并不是副作用,它们不是调用该方法所需的效果。如果不想立即添加,请创建另一个满足用例的方法。但这不是问题中传递的用例,所以说关注并不重要。
安迪

@candiedorange如果耦合使客户端代码更容易,我会说很好。开发人员或pos未来可能会认为是无关紧要的。如果那时候到了,您可以更改代码。没有什么比尝试构建可以处理将来任何可能的用例的代码更浪费的了。
安迪

5
@CandiedOrange我建议您不要将实现的精确定义的类型系统与业务的概念纯英语实体紧密结合。当然,“用户”的业务概念可以用两类(如果不是更多的话)来实现,以表示功能上重要的变体。未持久保留的用户不能保证拥有唯一的用户名,没有可以绑定事务的主键,甚至不能登录,因此我想说它包含一个重要的变体。当然,对于外行来说,EnrollRequest和User都是用模糊语言表示的“用户”。
John Wu,

5

只要没有意外,副作用就可以。因此,当存储库具有接受用户并更改用户内部状态的方法时,通常没有什么错。但是恕我直言,像InsertUser 这样的方法名称并不能清楚地传达这一点,这就是它容易出错的原因。使用您的存储库时,我会打个电话

 repo.InsertUser(user);

更改存储库的内部状态,而不是用户对象的状态。这两个实现中存在此问题,InsertUser内部如何完全不相关。

为了解决这个问题,您可以

  • 将初始化与插入分开进行(因此,调用方InsertUser需要提供完全初始化的User对象,或者,

  • 将初始化构建到用户对象的构建过程中(如其他一些答案所建议),或者

  • 只需尝试为该方法找到一个更好的名称,即可更清楚地表达其功能。

所以选择的方法等的名称PrepareAndInsertUserOrchestrateUserInsertionInsertUserWithNewNameAndPassword或任何你喜欢做的副作用更清晰。

当然,这么长的方法名称表明该方法可能做得“太多”(违反SRP),但有时您不希望或无法轻易解决此问题,这是最不实用的实用解决方案。


3

我正在查看您选择的两个选项,我不得不说,到目前为止,我更喜欢旧方法而不是提议的新方法。即使他们从根本上做同样的事情,也有一些原因。

无论哪种情况,都可以设置user.UserNameuser.Password。我对密码项目有保留,但这些保留与当前主题无关。

修改参考对象的含义

  • 这使并发编程更加困难-但并非所有应用程序都是多线程的
  • 这些修改可能令人惊讶,特别是如果该方法的任何内容都没有表明它将发生的话
  • 这些意外可能使维护更加困难

旧方法与新方法

旧方法使事情更容易测试:

  • GenerateUserName()可独立测试。您可以针对该方法编写测试,并确保正确生成名称
  • 如果名称需要用户对象提供的信息,则可以将签名更改为GenerateUserName(User user)并保持该可测试性

新方法隐藏了这些突变:

  • User直到深2层,您才知道对象在变化
  • User在这种情况下,对对象的更改更令人惊讶
  • SetUserName()不仅仅是设置用户名。这不是广告中的真相,这使新开发人员更难发现事情在您的应用程序中的工作方式

1

我完全同意吴宗宪的回答。他的建议是一个好建议。但这略微遗漏了您的直接问题。

基本上,从另一种方法设置属性值的做法是不好的做法吗?

不是天生的。

您不能太过分,因为您会遇到意外的行为。例如,PrintName(myPerson)不应更改person对象,因为该方法暗示它仅对读取现有值感兴趣。但这与您的情况不同,因为SetUsername(user)强烈暗示它会设置值。


实际上,这是我经常在单元/集成测试中使用的一种方法,在该方法中,我专门创建了一种方法来更改对象,以便将其值设置为要测试的特定情况。

例如:

var myContract = CreateEmptyContract();

ArrrangeContractDeletedStatus(myContract);

我明确希望该ArrrangeContractDeletedStatus方法可以更改myContract对象的状态。

主要好处是该方法允许我测试具有不同初始合同的合同删除;例如,具有很长状态历史的合同,或没有以前状态历史的合同,具有故意错误的状态历史的合同,不允许我的测试用户删除的合同。

如果我已经合并CreateEmptyContract并合并ArrrangeContractDeletedStatus为一个方法;我必须为要在删除状态下测试的每个不同合约创建此方法的多个变体。

虽然我可以做类似的事情:

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);
}

关于您的第一个问题:

修改通过引用传递的对象是不好的做法吗?

不,更多的是品味问题。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.