在这种情况下,是否应始终将所需的最少数据传递给函数


81

假设我有一个IsAdmin检查用户是否为管理员的函数。我们还要说,管理员检查是通过将用户ID,名称和密码与某种规则(不重要)进行匹配来完成的。

在我的脑海中,为此有两个可能的函数签名:

public bool IsAdmin(User user);
public bool IsAdmin(int id, string name, string password);

我通常会寻求第二种签名,以为:

  • 函数签名为读者提供了更多信息
  • 函数内部包含的逻辑不必了解User
  • 通常会导致函数内部的代码少一点

但是我有时会质疑这种方法,并且意识到在某些时候它将变得笨拙。例如,如果某个函数将在十个不同的对象字段之间映射成一个结果布尔值,那么我显然会发送整个对象。但是除了这样一个鲜明的例子,我看不到传递实际对象的理由。

对于任何一种风格的论点,以及您可能提供的任何一般性意见,我将不胜感激。

我以面向对象和功能两种风格进行编程,因此应将问题视为关于任何和所有习惯用法。


40
离题:出于好奇,为什么用户的“管理员”状态取决于他们的密码?
stakx

43
@ syb0rg错误。在C#中,这是命名约定
magnattic

68
@ syb0rg是的,他没有指定语言,所以我认为告诉他他违反某种语言的约定有点奇怪。
magnattic

31
@ syb0rg关于“函数名不应该以大写字母开头”的一般说法是错误的,因为应该使用多种语言。无论如何,我并不是要冒犯您,我只是想解决这个问题。这个问题本身已经变得非常离题了。我认为没有必要,但是如果您要继续,请将辩论移至聊天室
magnattic

6
通常,提高自己的安全性通常是一件坏事(tm)。大多数语言和框架都有可用的安全性/权限系统,即使您想要更简单的东西,也有充分的理由使用它们。
詹姆斯·斯内尔

Answers:


123

我个人更喜欢第一种方法 IsAdmin(User user)

它更易于使用,并且如果稍后对IsAdmin的条件进行更改(可能基于角色或isActive),则无需在任何地方重写方法签名。

由于您不会在广告中确定哪些属性确定用户是否是管理员,也不会在任何地方传递密码属性,因此这也可能更安全。而syrion指出,当您id不匹配name/ 时会发生什么password呢?

函数中代码的长度并不重要,只要该方法能够完成工作即可,我宁愿比辅助方法代码拥有更短,更简单的应用程序代码。


3
我认为重构问题非常重要-谢谢。
Anders Arpi

1
如果没有的话,我将做方法签名参数。当其他程序员对您的方法有很多依赖性时,这特别有用。这有助于使人们彼此隔绝。+1
jmort253

1
我同意这个答案,但是让我标记两种情况下使用其他方法(“最小数据”)可能更可取:(1)您想缓存方法结果。最好不要使用整个对象作为缓存键,或者不必在每次调用时都检查对象的属性。(2)您希望方法/函数异步执行,这可能涉及序列化参数并将其存储在表中。在管理挂起的作业时,查找整数比对象blob更容易和更简单。
GladstoneKeep,2013年

原始的痴迷反模式对于单元测试可能是可怕的。
塞缪尔

82

第一个签名是优越的,因为它允许将用户相关的逻辑封装在User对象本身中。对于散布在代码库中的“ id,名称,密码”元组的构造逻辑,是没有好处的。此外,它会使isAdmin函数复杂化:例如,如果有人传入id与不匹配的name,会发生什么?如果要从知道其密码的上下文中检查给定用户是否为管理员,该怎么办?

此外,作为第三点的注释,您赞成使用带有额外参数的样式;它可能会导致“函数内的代码更少”,但是这种逻辑在哪里呢?它不能消失!相反,它散布在调用函数的每个地方。为了将五行代码保存在一个地方,您每次使用该功能都要支付五行!


45
按照您的逻辑,会不会user.IsAdmin()好很多?
Anders Arpi

13
是的,一点没错。
asthasr

6
@AndersHolmström这取决于您是否要测试用户是一般的管理员,还是只是您当前所在页面/表单的管理员。如果总的来说,那是更好的选择,但是如果您仅测试IsAdmin特定的形式或功能,那应该与用户无关
Rachel

40
@安德斯:不,不应该。用户不应该确定它是否是管理员。特权或安全系统应该。与班级紧密联系并不意味着将其纳入班级是一个好主意
Marjan Venema 2013年

11
@MarjanVenema-User类不能“决定”实例是否为管理员的想法使我感到有点厌烦。您真的不能这么强地进行概括。如果您的需求很复杂,那么将它们封装在一个单独的“系统”中可能会有所好处,但是引用KISS + YAGNI,我想做出的决定实际上是面对这种复杂性的,而不是通常的规则:-)。并请注意,即使逻辑不是用户的“组成部分”,从实用性的角度来看,对用户进行传递(仅委派给更复杂的系统)仍然有用。
Eamon Nerbonne

65

首先,但不是(仅)出于其他人给出的原因。

public bool IsAdmin(User user);

这是类型安全的。尤其是由于User是您自己定义的类型,因此几乎没有或没有机会转置或切换参数。显然,它在Users上运行,不是接受任何int和两个字符串的通用函数。这是使用类型安全的语言(例如Java或C#)的重要意义,您的代码看起来像这样。如果您要询问特定的语言,则可能要在问题中添加该语言的标签。

public bool IsAdmin(int id, string name, string password);

在这里,任何int和两个字符串都可以。是什么阻止您转移名称和密码?第二个是要使用更多的工作,并允许更多的出错机会。

大概两个函数都要求在函数内部(在第一种情况下)或在调用函数(在第二种情况下)之前调用user.getId(),user.getName()和user.getPassword()。耦合量是相同的。实际上,由于此函数仅对用户有效,因此在Java中,我将消除所有参数并将其作为User对象的实例方法:

user.isAdmin();

由于此功能已经与用户紧密耦合,因此使其成为用户身份的一部分是有意义的。

附言:我敢肯定这只是一个例子,但看来您正在存储密码。相反,您应该只存储加密安全的密码哈希。登录期间,应以相同的方式对提供的密码进行哈希处理,并将其与存储的哈希进行比较。应避免以纯文本形式发送密码。如果违反此规则,并且isAdmin(int Str Str)将记录用户名,则如果在代码中转置了名称和密码,则可能会记录该密码,这会导致安全问题(请勿将密码写入日志) ),它是另一个赞成传递对象而不是其组成部分(或使用类方法)的参数。


11
用户不应该确定它是否是管理员。特权或安全系统应该。与课程紧密联系并不意味着将其纳入课程是一个好主意。
Marjan Venema 2013年

6
@MarjanVenema用户没有决定任何事情。用户对象/表存储有关每个用户的数据。实际用户不会改变自己的一切。即使用户更改了允许的内容,如果他们更改了电子邮件地址或用户ID或密码,它也可能会触发电子邮件等安全警报。您是否正在使用一个系统,允许用户神奇地更改有关某些对象类型的所有内容?我对这样的系统不熟悉。
GlenPeterson

2
@GlenPeterson:你是故意误会我了吗?当我说用户时,我当然是指用户类别。
Marjan Venema

2
@GlenPeterson完全没有话题,但是多轮哈希和加密功能会削弱数据。
Gusdor

3
@MarjanVenema:我不是故意要困难。我认为我们的观点截然不同,我想更好地了解您的观点。:我已经翻开了新的问题,其中这可以更好地讨论programmers.stackexchange.com/questions/216443/...
GlenPeterson

6

如果您选择的语言没有按值传递结构,那么(User user)变体似乎更好。如果某天您决定取消用户名并仅使用ID /密码来标识用户,该怎么办?

同样,这种方法不可避免地导致长时间且过分的方法调用或不一致。传递3个参数可以吗?5呢?还是6?如果在资源上传递对象便宜(可能比传递3个参数便宜),为什么还要考虑呢?

我(有点)不同意第二种方法为读者提供了更多信息-直观地说,方法调用询问“用户是否具有管理员权限”,而不是“给定的ID /名称/密码组合是否具有管理员权限”。

因此,除非传递User对象会导致复制大型结构,否则第一种方法对我来说更干净,更合逻辑。


3

我可能会两者兼有。第一个作为外部API,希望能够及时稳定。第二个是由外部API调用的私有实现。

如果以后需要更改检查规则,则只需编写一个新的私有函数,并带有一个由外部函数调用的新签名。

这具有易于更改内部实现的优点。同样很平常,有时您可能需要同时使用这两个功能,并根据某些外部上下文更改来调用一个或另一个功能(例如,您可能具有不同字段的旧用户和新用户共存)。

关于问题的标题,在两种情况下,您都提供了最少的必要信息。用户似乎是包含该信息的最小的与应用程序相关的数据结构。id,password和name这三个字段似乎是实际需要的最小实现数据,但是这些并不是真正的应用程序级对象。

换句话说,如果您正在处理数据库,则“用户”将是一条记录,而“ id”,“密码”和“登录名”将是该记录中的字段。


我看不到使用私有方法的原因。为什么要同时维护两者?如果私有方法需要不同的参数,则仍然必须修改公共参数。您将通过公开测试。不,在以后的某个时间,您通常都不需要这两者。您在这里猜测-YAGNI。
2013年

例如,您假设User数据结构从一开始就没有任何其他字段。这通常是不正确的。第二个功能可以清楚地检查用户的哪个内部部分,以查看它是否是管理员。典型的用例可能是这样的:如果用户创建时间在某个日期之前,则调用旧规则,如果不调用新规则(这可能取决于User的其他部分,或者取决于同一部分,但使用另一个规则)。通过这种方式拆分代码,您将获得两个最小的功能:最小的概念性API和最小的实现功能。
克里斯,

换句话说,通过这种方式,可以从内部函数签名中立即看到是否使用了User的哪些部分。在生产代码中,这并不总是显而易见的。我目前正在使用具有多年维护历史的大型代码库,这种拆分对于长期维护非常有用。如果您不打算维护代码,那么它实际上是无用的(但是即使提供稳定的概念性API也可能无用)。
克里斯,

换句话说:我当然不会通过外部方法对内部方法进行单元测试。那是不好的测试习惯。
克里斯,

1
这是一个好习惯,并且有个名字。这就是所谓的“单一责任原则”。它可以应用于类和方法。具有单一职责的方法是制作更具模块化和可维护性的代码的好方法。在这种情况下,一项任务是从用户对象中选择基本数据集,而另一项任务是检查该基本数据集以查看用户是否为管理员。通过遵循这一原理,您将能够编写方法并避免代码重复。如@kriss所述,这也意味着您将获得更好的单元测试。
diegoreymendez 2014年

3

将类(引用类型)发送给方法有一个缺点,那就是质量分配漏洞。想象一下IsAdmin(User user),我有UpdateUser(User user)方法,而不是方法。

如果Userclass具有一个称为的布尔属性IsAdmin,并且如果我在执行方法期间不进行检查,则很容易发生批量分配。GitHub在2012年就受到这种签名的攻击。


2

我会采用这种方法:

public bool IsAdmin(this IUser user) { /* ... */ }

public class User: IUser { /* ... */ }

public interface IUser
{
    string Username {get;}
    string Password {get;}
    IID Id {get;}
}
  • 使用接口类型作为此函数的参数,可以传递用户对象,定义模拟用户对象以进行测试,并为使用和维护该函数提供更大的灵活性。

  • 我还添加了关键字this以使该函数成为所有IUser类的扩展方法。这样,您可以以更加面向对象的方式编写代码,并在Linq查询中使用此功能。更简单的方法是在IUser和User中定义此函数,但是我认为您出于某种原因决定将此方法放在该类之外?

  • 我使用的IID不是自定义界面,int因为它允许您重新定义ID的属性;例如,你应该永远需要从改变intlongGuid或别的东西。这可能是过大了,但是尝试使事情变得灵活总是好事,这样您就不会陷入早期决策中。

  • 注意:您的IUser对象可以位于与User类不同的名称空间和/或程序集中,因此您可以选择将User代码与IsAdmin代码完全分开,它们仅共享一个引用的库。确切地说,您在做什么就可以让您怀疑如何使用这些物品。


3
IUser在这里没有用,只会增加复杂性。我不明白为什么您不能在测试中使用真实用户。
2013年

1
它以很少的努力就增加了未来的灵活性,因此尽管当前不增加价值,也不会受到损害。我个人比较喜欢这样做,因为它避免了必须考虑所有情况的未来可能性(考虑到需求的不可预测性,这几乎是不可能的,而且要花费部分时间甚至比解决问题还要花更多时间停留在界面上)。
JohnLBevan

@Peri一个真正的User类创建起来可能很复杂,采用一个接口可以模拟它,以便使测试保持简单
jk。

@JohnLBevan:YAGNI !!!
霹雳霹雳州

@jk .:如果很难建立用户,则出了点问题
Piotr Perak

1

免责声明:

在我的答复中,我将重点讨论样式问题,而从安全的角度来看,ID,登录名和普通密码是可以使用的一组好参数。您应该可以将我的答案推论到用来确定用户特权的任何基本数据集...

我也认为user.isAdmin()等同于IsAdmin(User user)。这是您的选择。

回复:

我的建议是仅向任一用户:

public bool IsAdmin(User user);

或同时使用以下两者:

public bool IsAdmin(User user); // calls the private method below
private bool IsAdmin(int id, string name, string password);

公开方法的原因:

在编写公共方法时,通常最好考虑一下如何使用它们。

对于这种特殊情况,通常要调用此方法的原因是要弄清楚某个用户是否是管理员。这是您需要的方法。您确实不希望每个调用者都必须选择正确的参数来发送(并且由于代码重复而冒错误的风险)。

采用私有方法的原因:

有时,仅接收最少一组必需参数的私有方法可能是一件好事。有时没有那么多。

拆分这些方法的好处之一是,您可以从同一类中的多个公共方法中调用私有版本。例如,如果您希望(由于某种原因)对Localuser和的逻辑略有不同RemoteUser(也许有一个设置可以打开/关闭远程管理员?):

public bool IsAdmin(Localuser user); // Just call private method
public bool IsAdmin(RemoteUser user); // Check if setting is on, then call private method
private bool IsAdmin(int id, string name, string password);

此外,如果出于任何原因需要将私有方法公开...则可以。就像更改private为一样简单public。在这种情况下,优势似乎并不大,但有时确实如此。

另外,在进行单元测试时,您将可以进行更多的原子测试。通常不必创建完整的用户对象即可对此方法运行测试,这非常好。而且,如果您想全面覆盖,则可以测试两个呼叫。


0
public bool IsAdmin(int id, string name, string password);

由于列出的原因而不好。那不代表

public bool IsAdmin(User user);

是自动的。特别地,如果用户是具有像方法的对象:getOrders()getFriends()getAdresses(),...

您可以考虑使用仅包含ID,名称,密码的UserCredentials类型重构域,然后将其传递给IsAdmin而不是所有不需要的User数据。

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.