您通常将对象或其成员变量发送到函数中吗?


31

这两种情况之间普遍接受的做法是:

function insertIntoDatabase(Account account, Otherthing thing) {
    database.insertMethod(account.getId(), thing.getId(), thing.getSomeValue());
}

要么

function insertIntoDatabase(long accountId, long thingId, double someValue) {
    database.insertMethod(accountId, thingId, someValue);
}

换句话说,通常是将整个对象传递给您,还是只传递所需的字段,这更好吗?


5
这将完全取决于函数的作用以及它与所讨论对象的关系(或不相关)。
MetaFight

那就是问题所在。我不知道何时要使用其中一个。我觉得我可以随时更改代码以适应这两种方法。
AJJ

1
用API术语(完全不看实现)而言,前者是抽象且面向领域的(这是好的),而后者则不是(这是坏的)。
埃里克·艾德

1
第一种方法是更多的3层OO。但是,通过从方法中删除单词数据库,应该更是如此。它应该是“ Store”或“ Persist”,并执行Account或Thing(不能同时执行)。作为该层的客户端,您不应该知道存储介质。检索帐户时,您需要传递ID或属性值(而非字段值)的组合来标识所需的对象。或/和无法通过所有帐户的枚举方法。
马丁·马特

1
通常,两者都是错误的(或者,不是最优的)。应该如何将对象序列化到数据库中应该是对象的属性(成员函数),因为它通常直接取决于对象的成员变量。如果更改对象的成员,则还需要更改序列化方法。如果它是对象的一部分,那就更好用了
tofro

Answers:


24

通常,两者都不比另一个更好。您必须根据具体情况作出判断。

但是在实践中,当您处于可以实际做出决定的位置时,这是因为您要决定整个程序体系结构中的哪一层应将对象分解为原语,因此您应该考虑整个调用stack,而不仅仅是您当前正在使用的一种方法。大概必须在某个地方进行分解,并且多次进行分解是没有意义的(或者不必要地容易出错)。问题是那个地方应该在哪里。

做出此决定的最简单方法是考虑如果对象被更改,应该或不应该更改哪些代码。让我们稍微扩展一下示例:

function addWidgetButtonClicked(clickEvent) {
    // get form data
    // get user's account
    insertIntoDatabase(account, data);
}
function insertIntoDatabase(Account account, Otherthing data) {
    // open database connection
    // check data doesn't already exist
    database.insertMethod(account.getId(), data.getId(), data.getSomeValue());
}

function addWidgetButtonClicked(clickEvent) {
    // get form data
    // get user's account
    insertIntoDatabase(account.getId(), data.getId(), data.getSomeValue());
}
function insertIntoDatabase(long accountId, long dataId, double someValue) {
    // open database connection
    // check data doesn't already exist
    database.insertMethod(accountId, dataId, someValue);
}

在第一个版本中,UI代码盲目地传递data对象,然后由数据库代码从中提取有用的字段。在第二个版本中,UI代码将data对象分解为有用的字段,数据库代码直接接收它们,而不知道它们来自何处。关键含义是,如果data以某种方式更改对象的结构,则第一个版本仅需要更改数据库代码,而第二个版本仅需要更改UI代码。这两个中的哪一个是正确的,很大程度上取决于data对象所包含的数据类型,但这通常是非常明显的。例如,如果data是用户提供的字符串,例如“ 20/05/1999”,应该由UI代码将其转换为正确的Date类型,然后再传递给它。


8

这不是一个详尽的列表,但是在决定是否将一个对象作为参数传递给方法时,请考虑以下因素:

对象是不变的吗?函数是“纯”的吗?

副作用是代码可维护性的重要考虑因素。当您看到遍地传递大量可变状态对象的代码时,该代码通常不那么直观(就像全局状态变量通常不那么直观)一样,并且调试通常变得更加困难且耗时-消耗。

根据经验,应尽最大可能确保传递给方法的任何对象都是不可变的。

避免(尽可能在合理范围内)通过参数调用来期望改变参数状态的任何设计-这种方法最强的参数之一是“最小惊讶原则”;例如,有人在阅读您的代码并看到传递给函数的参数时,“不太可能”期望函数返回后状态会发生变化。

该方法已经有多少个参数?

参数列表过长的方法(即使其中大多数参数都具有“默认”值)也开始看起来像代码味道。但是,有时此类功能是必需的,您可以考虑创建一个类的唯一目的是充当Parameter Object的类

这种方法可能涉及从“源”对象到参数对象的少量附加样板代码映射,但是就性能和复杂性而言,这是相当低的成本,但是,在解耦方面有很多好处和对象不变性。

传递的对象是否仅属于应用程序中的“层”(例如,ViewModel或ORM实体?)?

考虑问题分离(SoC)。有时问自己对象是否属于您的方法所在的同一层或模块(例如,手推式API包装器库或核心业务逻辑层等)可以告知该对象是否确实应该传递给该对象方法。

SoC是编写干净的,松散耦合的模块化代码的良好基础。例如,理想情况下,不应在业务层中传递ORM实体对象(在代码与数据库模式之间进行映射),更不要在演示文稿/ UI层中传递。

在“层”之间传递数据的情况下,将普通数据参数传递给方法通常比从“错误”层传递对象更可取。尽管在“正确”层上存在单独的模型可能是一个好主意,您可以将其映射到。

函数本身是否太大和/或太复杂?

当一个功能需要大量数据项时,可能值得考虑该功能是否承担了太多责任;寻找使用较小对象和更短,更简单的功能进行重构的潜在机会。

函数应该是命令/查询对象吗?

在某些情况下,数据与功能之间的关系可能很紧密。在这些情况下,请考虑使用命令对象还是查询对象是否合适。

向方法中添加对象参数是否会强制包含类采用新的依赖关系?

有时,“普通数据”参数的最强参数只是接收类已经完全独立,并且在其方法之一中添加对象参数会污染该类(或者,如果该类已被污染,则它将污染该类)。使现有的熵更糟)

您是否真的需要传递一个完整的对象,还是只需要该对象界面的一小部分?

考虑与您的函数有关的接口隔离原理 -即,在传递对象时,它仅应取决于参数(函数)实际需要的参数接口的一部分。


5

因此,在创建函数时,您会隐式声明一些带有调用它的代码的协定。“此功能获取此信息,并将其转换为另一件事(可能带有副作用)”。

因此,您的合同应该在逻辑上与对象(无论它们是如何实现)一起使用,还是与恰好是这些其他对象的一部分的字段一起使用。您可以通过任何一种方式添加耦合,但是作为程序员,由您决定耦合的位置。

通常,如果不清楚,则支持该功能正常工作所需的最小数据。这通常意味着仅传递字段,因为该函数不需要在对象中找到其他内容。但是有时候,拿走整个物体会更正确,因为当将来不可避免地发生变化时,它会减少影响。


为什么不将方法名称更改为insertAccountIntoDatabase,还是要传递任何其他类型?在使用obj的特定数量的参数的情况下,代码很容易阅读。在您的情况下,我宁愿考虑方法名称是否清除了我要插入的内容,而不是即时消息将如何执行。
2016年

3

这取决于。

详细说来,您的方法接受的参数应在语义上与您要执行的操作匹配。考虑一种方法EmailInviter以及这三种可能的实现invite

void invite(String emailAddressString) {
  invite(EmailAddress.parse(emailAddressString));
}
void invite(EmailAddress emailAddress) {
  ...
}
void invite(User user) {
  invite(user.getEmailAddress());
}

由于您并非必须将所有字符串都作为电子邮件地址String,因此将传递到您应该传递的位置EmailAddress是有缺陷的。该EmailAddress级较好语义匹配方法的行为。但是,传递a User也是有缺陷的,因为为什么a 到底应该EmailInviter仅限于邀请用户?那企业呢?如果您正在从文件或命令行中读取电子邮件地址,但未与用户关联,该怎么办?邮件列表?清单继续。

您可以在此处使用一些警告标志进行指导。如果您使用的是简单值类型,例如Stringint但不是所有的字符串或整数都有效,或者它们有某种“特殊”含义,则应使用更有意义的类型。如果您使用的是对象,并且唯一要做的就是调用getter,那么您应该直接在getter中传递对象。这些准则既不严格也不很快,但很少有准则。


0

干净代码建议使用尽可能少的参数,这意味着通常使用Object是更好的方法,我认为这是有道理的。因为

insertIntoDatabase(new Account(id) , new Otherthing(id, "Value"));

比起更具可读性的通话

insertIntoDatabase(myAccount.getId(), myOtherthing.getId(), myOtherthing.getValue() );

不能同意。两者不是同义词。创建2个新的对象实例只是将它们传递给方法是不好的。我会使用insertIntoDatabase(myAccount,myOtherthing)代替任何一个选项。
jwenting

0

绕过对象,而不是其构成状态。这支持封装和数据隐藏的面向对象原理。在不必要的各种方法接口中公开对象的内部行为违反了核心OOP原则。

如果更改其中的字段会Otherthing怎样?也许您更改类型,添加字段或删除字段。现在,所有方法(如您在问题中提到的方法)都需要更新。如果您绕过对象,则不会更改界面。

您唯一应该编写一种方法来接受对象上的字段的时间是在编写用于检索对象的方法时:

public User getUser(String primaryKey) {
  return ...;
}

在进行该调用时,调用代码还没有对该对象的引用,因为调用该方法的目的是获取该对象。


1
“如果更改其中的字段会发生什么Otherthing?” (1)这将违反开放/封闭原则。(2)即使您传入整个对象,该对象中的代码也可以访问该对象的成员(如果没有,为什么要传入该对象?)仍然会中断...
David Arno

@DavidArno我的回答的重点不是什么都不会破坏,而是更少会破坏。也不要忘记第一段:无论发生什么中断,都应该使用对象的接口抽象对象的内部状态。绕过其内部状态违反了OOP原则。

0

从可维护性的角度来看,自变量应该彼此清晰地区分,最好是在编译器级别。

// this has exactly one way to call it
insertIntoDatabase(Account ..., Otherthing ...)

// the parameter order can be confused in practice
insertIntoDatabase(long ..., long ...)

第一个设计导致早期发现错误。第二种设计可能导致细微的运行时问题,这些问题不会在测试中出现。因此,第一种设计是首选。


0

在这两种方法中,我的首选方法是第一种方法:

function insertIntoDatabase(Account account, Otherthing thing) { database.insertMethod(account.getId(), thing.getId(), thing.getSomeValue()); }

原因是只要对这些对象进行更改,只要这些更改保留了这些吸气剂,因此更改在对象外部是透明的,那么您进行更改和测试的代码就更少了,中断应用程序的机会也就更少。

这只是我的思考过程,主要是基于我喜欢如何工作和构造具有这种性质的事物,并且从长远来看,事实证明该过程非常易于管理和维护。

我不打算讨论命名约定,但会指出,尽管此方法中包含“数据库”一词,但这种存储机制可以改变。根据显示的代码,没有将功能与使用的数据库存储平台绑定的内容,即使它是数据库也是如此。我们只是假设了它的名字。同样,假设那些吸气剂始终被保存,改变这些对象的存储方式/位置将很容易。

我会重新考虑函数和两个对象,因为您有一个函数依赖于两个对象结构,尤其是所使用的吸气剂。看起来该函数将这两个对象绑定到一个累积的事物中,该事物将持久化。我的直觉告诉我,第三个对象可能有意义。我需要更多地了解这些对象以及它们在现实中与预期路线图之间的关系。但是我的直觉正朝那个方向倾斜。

就目前的代码而言,问题开始于“此函数将在哪里或应该在哪里?” 它是帐户的一部分,还是其他?去哪儿了?

我猜已经有第三个对象“数据库”,并且我倾向于将此函数放到该对象中,然后它成为该对象的工作,能够处理Account和OtherThing,进行转换,然后还保留结果。

如果您要使第三个对象符合对象关系映射(ORM)模式,那就更好了。对于使用该代码的任何人来说,理解“啊,这就是Account和OtherThing一起粉碎并持久存在的地方”,这将非常明显。

但是引入第四个对象也可能是有意义的,该对象处理组合和转换Account和OtherThing的工作,而不处理持久化的机制。如果您期望与这两个对象进行更多交互,或者这样做,我会这样做,因为随着时间的增长,我希望将持久性因素分解成一个仅管理持久性的对象。

我会努力保持设计,以便可以更改Account,OtherThing或第三个ORM对象中的任何一个,而不必同时更改其他三个对象。除非有充分的理由,否则我希望Account和OtherThing是独立的,并且不必了解彼此的内部工作原理和结构。

当然,如果我知道将要发生的全部情况,那么我可能会完全改变我的想法。再说一遍,这就是我看到这样的事情时的想法,以及多么苗条。


0

两种方法各有优缺点。在哪种情况下更好的选择很大程度上取决于手头的用例。


专业版多重参数,Con Object参考:

  • 调用者未绑定到特定的类,它可以完全传递来自不同来源的值
  • 对象状态可以避免在方法执行过程中意外修改

Pro Object参考:

  • 清楚地表明该方法已绑定到Object引用类型,这使得很难意外地传递不相关 /无效的值
  • 重命名字段/获取程序需要在方法的所有调用上进行更改,而不仅仅是在其实现上进行更改
  • 如果添加新属性并且需要传递新属性,则无需在方法签名中进行任何更改
  • 方法可以 改变对象状态
  • 传递太多相似类型的变量会使调用者在顺序方面感到困惑(Builder模式问题)

因此,需要使用什么以及何时使用取决于使用情况

  1. 传递单个参数:通常,如果该方法具有与对象类型无关,则最好传递单个参数列表,以使其适用于更广泛的受众。
  2. 介绍新的模型对象:如果参数列表变大(超过3个),最好引入属于被调用API的新模型对象(首选构建器模式)
  3. 传递对象引用:如果该方法与域对象相关,则从可维护性和可读性的角度来看,传递对象引用更好。

0

一方面,您有一个Account和Otherthing对象。另一方面,给定帐户的ID和“其他”的ID,您可以将值插入数据库。这就是两个给定的东西。

您可以编写一个以Account和Otherthing为参数的方法。在专业方面,呼叫者无需了解有关“帐户”和“其他”的任何详细信息。消极的一面,被呼叫者需要了解“帐户”和“其他”的方法。而且,除了“其他”对象的值之外,没有其他方法可以将任何其他内容插入数据库,并且如果您具有帐户对象的ID,但是没有对象本身,则无法使用此方法。

或者,您可以编写一个使用两个id和一个值作为参数的方法。消极的一面,呼叫者需要知道“帐户”和“其他”的详细信息。在某些情况下,您可能实际上需要一个帐户或其他详细信息,而不仅仅是将ID插入数据库中,在这种情况下,此解决方案完全没有用。另一方面,希望在被叫方中不需要帐户和其他方面的知识,并且有更大的灵活性。

您的判断力:是否需要更大的灵活性?通常这不是一个电话的问题,而是在所有软件中都保持一致:要么大部分时间使用Account的ID,要么使用对象。混合使用会使您两全其美。

在C ++中,您可以有一个采用两个id加值的方法,以及一个采用Account和Otherthing的内联方法,因此两种方式的开销都为零。

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.