吸气剂有多少逻辑


46

我的同事告诉我,getter和setter中的逻辑应该尽可能少。

但是,我深信可以在getter和setter中隐藏很多东西,以使用户/程序员免受实现细节的影响。

我做的一个例子:

public List<Stuff> getStuff()
{
   if (stuff == null || cacheInvalid())
   {
       stuff = getStuffFromDatabase();
   }
   return stuff;
}

工作告诉我做事的一个例子(他们引述Bob叔叔的“干净代码”):

public List<Stuff> getStuff()
{
    return stuff;
}

public void loadStuff()
{
    stuff = getStuffFromDatabase();
}

设置器/获取器中适合多少逻辑?空的getter和setter的用途是什么,除了违反数据隐藏之外?


6
在我看来,这更像tryGetStuff()...
比尔·米歇尔

16
这不是一个“吸气剂”。该术语用于属性的读取访问器,而不是您在名称中意外添加“ get”的方法。
鲍里斯·扬科夫

6
我不知道第二个示例是否是您提到的这本干净的代码书的公平示例,还是有人对此一无所知,但是干净的代码不是一团糟。
乔恩·汉娜

@BorisYankov嗯... 第二种方法是。public List<Stuff> getStuff() { return stuff; }
R. Schmitz

根据确切的用例,我喜欢将缓存分成一个单独的类。创建一个StuffGetter接口,实现一个StuffComputer进行计算的接口,并将其包装在的对象中StuffCacher,该对象负责访问缓存或将调用转发给StuffComputer它包装的。
亚历山大

Answers:


71

工作告诉你做事的方式是la脚的。

根据经验,我的工作方式如下:如果获取这些东西在计算上很便宜,(或者如果很有可能在缓存中找到它),那么您的getStuff()样式就很好。如果已知获取的东西在计算上是昂贵的,那么昂贵,以至于需要在接口上公布其昂贵性,那么我就不会将其称为getStuff(),而将其称为calculateStuff()或类似的东西,以表明会有一些工作要做。

在这两种情况下,工作告诉您做事的方式都是la脚的,因为如果未提前调用loadStuff(),getStuff()将会爆炸,因此他们本质上希望您通过引入操作顺序的复杂性来使接口复杂化对它。操作顺序几乎是我能想到的最糟糕的一种复杂性。


23
+1表示操作顺序的复杂性。作为一种解决方法,也许工作会要求我始终在构造函数中调用loadStuff(),但这也很不好,因为这意味着它将始终必须被加载。在第一个示例中,仅在需要时才延迟加载数据,这是可以做到的。
laurent

6
我通常遵循“如果真的很便宜,请使用属性获取器。如果很昂贵,请使用函数”的规则。通常,这对我很有用,并且按照您的指示恰当地命名以强调它对我来说似乎也很不错。
Denis Troller

3
如果失败了-那不是吸气剂。在这种情况下,如果数据库链接断开了怎么办?
马丁·贝克特

6
+1,我对发布了多少错误答案感到震惊。存在Getters / Setters来隐藏实现细节,否则应仅将变量公开。
Izkata 2011年

2
别忘了要求在loadStuff()函数之前调用getStuff()函数也意味着该类没有正确地抽象出幕后发生的事情。
rjzii 2012年

23

吸气剂中的逻辑非常好。

但是从数据库获取数据远不止是“逻辑”。它涉及一系列非常昂贵的操作,其中很多事情都可能以不确定的方式出错。我会毫不犹豫地在吸气剂中隐式地这样做。

另一方面,大多数ORM支持集合的延迟加载,这基本上就是您正在执行的操作。


18

我认为根据“清洁代码”,应尽可能将其拆分为以下内容:

public List<Stuff> getStuff() {
   if (hasStuff()) {
       return stuff;
   }
   loadStuff();
   return stuff;
}

private boolean hasStuff() {
    if (stuff == null) {
       return false;
    }
    if (cacheInvalid()) {
       return false;        
    }
    return true;
} 

private void loadStuff() {
    stuff = getStuffFromDatabase();
}

当然,这完全是胡说八道,因为您编写的漂亮表格用任何人一眼都能理解的一小段代码就能完成正确的事情:

public List<Stuff> getStuff() {
   if (stuff == null || cacheInvalid()) {
       stuff = getStuffFromDatabase();
   }
   return stuff;
}

如何将这些东西藏在幕后应该不是呼叫者的头疼,尤其是记住以某种任意的“正确顺序”调用事物也不应该是呼叫者的头疼。


8
-1。真正令人头疼的是,当调用者被卡住时,弄清楚为什么简单的getter调用会导致缓慢的地狱数据库访问。
Domenic

14
@Domenic:无论如何都必须完成数据库访问,如果不这样做,就不会节省任何人的性能。如果您需要它List<Stuff>,只有一种方法可以得到它。
DeadMG 2011年

4
@lukas:谢谢,我不记得'Clean'代码中使用的所有技巧,可以使琐碎的代码再增加一行;-)现在修复。
Joonas Pulakka 2011年

2
你在诽谤罗伯特·马丁。他永远不会将简单的布尔取和扩展为九行函数。您的功能hasStuff与干净代码相反。
凯文·克莱恩

2
我读了这个答案的开头,我打算绕过它,以为“还有另一本书崇拜者”,然后“当然,这完全是胡说八道”部分引起了我的注意。说得好!C-:=
Mike Nakis

8

他们告诉我,getter和setter中的逻辑应该尽可能少。

为了满足班级的需求,需要有尽可能多的逻辑。我个人的喜好是尽可能地少,但是在维护代码时,通常必须将原始接口保留给现有的getter / setter,而要在其中添加许多逻辑以纠正较新的业务逻辑(例如,“客户” 911后环境中的吸气剂必须满足“了解您的客户”和OFAC法规,并结合公司政策禁止某些国家(例如古巴或伊朗)的客户出现。

在您的示例中,我更喜欢您的示例,并且不喜欢“ uncle bob”示例,因为“ uncle bob”版本要求用户/维护人员在致电loadStuff()之前记得先打电话getStuff()-如果您的维护人员中的任何一个忘记​​(或更糟的是,永远不会知道)。我在过去十年中工作过的大多数地方仍在使用已使用了十年以上的代码,因此易于维护是需要考虑的关键因素。


6

你是对的,你的同事是错的。

忘记每个人关于get方法应该或不应该做什么的经验法则。一个类应该呈现某种事物的抽象。您的课程具有可读性stuff。在Java中,通常使用“ get”方法读取属性。已经编写了数十亿行框架,期望stuff通过调用来阅读getStuff。如果您命名函数fetchStuff或除之外的其他名称getStuff,则您的类将与所有这些框架不兼容。

您可能将它们指向Hibernate,其中的“ getStuff()”可以执行一些非常复杂的操作,并在失败时引发RuntimeException。


Hibernate是一个ORM,因此程序包本身表达了意图。如果包本身不是ORM,则很难理解此意图。
FMJaguar 2014年

@FMJaguar:完全容易理解。Hibernate提取数据库操作以呈现对象网络。OP正在抽象化数据库操作以呈现具有名为的属性的对象stuff。两者都隐藏了细节,使编写调用代码更加容易。
凯文·克莱恩

如果该类是ORM类,则在其他情况下已经表达了意图:问题仍然存在:“另一个程序员如何知道调用getter的副作用?”。如果程序包含1k类和10k getter,则允许在其中任何一个中进行数据库调用的策略可能会很麻烦
FMJaguar 2014年

4

听起来像是纯粹主义者与应用程序的争论,可能会受到您更喜欢控制函数名称的方式的影响。从应用的角度来看,我宁愿看到:

List<String> names = clientRoster.getNames();
List<String> emails = clientRoster.getEmails();

相对于:

myObject.load();
List<String> names = clientRoster.getNames();
List<String> emails = clientRoster.getEmails();

甚至更糟:

myObject.loadNames();
List<String> names = clientRoster.getNames();
myOjbect.loadEmails();
List<String> emails = clientRoster.getEmails();

这只会使其他代码变得更加冗余且更难以阅读,因为您必须开始经历所有类似的调用。另外,调用加载器函数或类似的函数甚至使使用OOP的全部目的中断,因为您不再需要从正在使用的对象的实现细节中抽象出来。如果您有一个clientRoster对象,则不必getNames像如何调用那样去关心它的工作原理loadNames,您应该只知道getNames给您一个List<String>带有客户名称的名称。

因此,听起来问题似乎更多是关于语义和用于获取数据的函数的最佳名称。如果公司(和其他公司)的getand set前缀有问题,那么如何调用类似的函数retrieveNames呢?它说明了正在发生的事情,但并不意味着该操作将是get方法预期的瞬时操作。

就访问器方法中的逻辑而言,应将其最小化,因为通常隐含它们是瞬时的,并且仅与变量发生名义上的相互作用。但是,这通常也只适用于简单类型,复杂数据类型(即List),我发现很难将其正确封装在属性中,并且与严格的mutator和accessor相对,通常使用其他方法与它们进行交互。


2

调用getter应该表现出与读取字段相同的行为:

  • 找回价值应该便宜
  • 如果使用setter设置值,然后使用getter读取它,则该值应相同
  • 获得价值应该没有副作用
  • 它不应引发异常

2
我对此并不完全同意。我同意它应该没有副作用,但是我认为以区别于现场的方式实施它是完全可以的。在查看.Net BCL时,在查看吸气剂时,InvalidOperationException被广泛使用。另外,请参阅MikeNakis关于操作顺序的答案。
马克斯

同意除最后一项以外的所有要点。当然,获取值可能涉及执行计算或其他操作,这取决于未设置的其他值或资源。在那些情况下,我希望吸气剂会抛出某种异常。
TMN

1
@TMN:在最佳情况下,类的组织方式应使吸气剂不需要运行能够考虑异常的操作。尽量减少可能引发异常的地方,从而减少意外的惊喜。
hugomg 2011年

8
我将通过一个具体的示例来反对第二点foo.setAngle(361); bar = foo.getAngle()bar可能是361,但是1角度是否绑定到范围也可能合法。
zzzzBov 2011年

1
-1。(1)在此示例中便宜-延迟加载后。(2)示例中当前没有“ setter”,但是如果有人在其后加一个,并且只设置了stuff,则getter 返回相同的值。(3)如示例中所示,延迟加载不会产生“可见的”副作用。(4)是有争议的,也许是正确的一点,因为事后引入“延迟加载” 可以更改以前的API合同-但必须查看该合同才能做出决定。
布朗

2

调用其他属性和方法以计算其自身值的吸气剂也意味着依赖。例如,如果您的属性必须能够计算自身,并且这样做需要设置另一个成员,那么如果您的属性是在初始化代码中访问的,而不必设置所有成员,那么您就不得不担心意外的空引用。

这并不意味着“永远不要访问不是getter内的属性支持字段的其他成员”,而是意味着要注意您所隐含的关于​​对象所需状态的含义,以及是否与您期望的上下文匹配要访问的属性。

但是,在您给出的两个具体示例中,我选择一个而不是另一个的原因完全不同。您的getter会在首次访问时进行初始化,例如Lazy Initialization。假定第二个示例在某个先验点(例如,显式初始化)处进行了初始化

确切的初始化发生时间可能不重要。

例如,它可能非常慢,并且需要在用户期望延迟的加载步骤中完成,而不是在用户首次触发访问时(例如,用户右键单击,出现上下文菜单,用户已经再次右键单击)。

同样,有时在执行过程中有一个明显的地方,所有可能影响/弄脏缓存属性值的事物都会发生。您甚至可能正在验证所有依赖项都没有更改,并且稍后会引发异常。在这种情况下,即使没有特别昂贵的计算,也应该在那一刻也缓存该值,只是为了避免使代码执行更复杂并且更难于遵循。

就是说,在许多其他情况下,惰性初始化很有意义。因此,就像在编程中经常遇到的困难一样,它可以归结为具体的代码。


0

就像@MikeNakis所说的那样做...如果您只是得到东西,那很好...如果您做其他事情,请创建一个新函数来完成工作并将其公开。

如果您的属性/功能仅执行其名称所说明的操作,那么就没有太多的并发症了。凝聚力是IMO的关键


1
注意这一点,您可能会暴露太多的内部状态。您不希望仅由于类的初始实现需要它们而产生大量的empty loadFoo()preloadDummyReferences()or createDefaultValuesForUninitializedFields()方法。
TMN

当然...我只是告诉你,如果您按照名字所说的就不会有很多问题...但是您说的是绝对正确的……
Ivan CrojachKaračić2011年

0

就个人而言,我将通过构造函数中的参数来显示Stuff的要求,并允许实例化任何东西的任何类进行确定它应来自何处的工作。如果stuff为null,则应返回null。我不想尝试像OP原始版本那样的聪明解决方案,因为这是一种将错误隐藏在实现内部的简便方法,在发生故障时根本看不出什么地方出了问题。


0

这里还有更多重要的问题,而不仅仅是“适当性” ,您应该根据这些做出决定。主要是,这里的重大决定是您是否要让人们绕过缓存。

  1. 首先,考虑是否有一种方法可以重新组织代码,以便所有必需的加载调用和缓存管理都在构造函数/初始化程序中完成。如果可能的话,您可以创建一个类,该类的不变性允许您使用第1部分中的简单getter和第1部分中的复杂getter的安全性。(双赢方案)

  2. 如果无法创建此类,请确定是否需要权衡,还需要确定是否要允许使用者跳过缓存检查代码。

    1. 如果让消费者永远不要跳过缓存检查很重要并且您不介意性能损失,那么将检查加到吸气剂内部,使消费者不可能做错事。

    2. 如果可以跳过缓存检查,或者确保在getter中获得O(1)性能非常重要,那么请使用单独的调用。

正如您可能已经注意到的那样,我不是“干净代码”,“将所有内容拆分为微小功能”的忠实拥护者。如果您有一堆可以按任意顺序调用的正交函数,则拆分它们将以较低的成本为您提供更多的表达能力。但是,如果您的函数具有顺序依赖性(或仅在特定顺序中确实有用),则拆分它们只会增加您做错事的方式,而几乎没有好处。


-1,构造函数应构造而不是初始化。将数据库逻辑放在构造函数中将使该类完全不可测试,并且如果您只有少数几个,则应用程序启动时间将变得很长。那只是初学者。
Domenic

@Domenic:这是一个语义和语言相关的问题。一个对象适合使用并在完全构建之后(只有在此之后)才提供适当的不变性的观点。
hugomg 2011年

0

我认为,Getters中应该没有太多逻辑。它们不应该有副作用,并且您永远都不应从中获得任何例外。当然,除非您知道自己在做什么。我的大多数吸气剂没有逻辑,只能去一个领域。但是最明显的例外是公共API,它必须使用起来尽可能简单。因此,我有一个如果没有调用另一个吸气剂就会失败。解决方案?像var throwaway=MyGetter;getter中那样依赖它的一行代码。我并不为此感到骄傲,但是我仍然看不到更干净的方法


0

这看起来像是从具有延迟加载的缓存中读取的内容。正如其他人指出的那样,检查和加载可能属于其他方法。加载可能需要同步,因此您不会同时获得二十个线程。

getCachedStuff()为getter 使用名称可能是适当的,因为它的执行时间不一致。

根据cacheInvalid()例程的工作方式,可能不需要检查null。除非stuff从数据库中填充缓存,否则我不希望缓存有效。


0

我期望在返回列表的getter中看到的主要逻辑是确保列表不可修改的逻辑。就目前而言,您的两个示例都可能破坏封装。

就像是:

public List<Stuff> getStuff()
{
    return Collections.unmodifiableList(stuff);
}

至于在getter中进行缓存,我认为可以,但是如果构建缓存要花费大量时间,我可能会倾向于移出缓存逻辑。即取决于。


0

根据确切的用例,我喜欢将缓存分成一个单独的类。创建一个StuffGetter接口,实现一个StuffComputer进行计算的接口,并将其包装在的对象中StuffCacher,该对象负责访问缓存或将调用转发给StuffComputer它包装的。

interface StuffGetter {
     public List<Stuff> getStuff();
}

class StuffComputer implements StuffGetter {
     public List<Stuff> getStuff() {
         getStuffFromDatabase()
     }
}

class StuffCacher implements StuffGetter {
     private stuffComputer; // DI this
     private Cache<List<Stuff>> cache = new Cache<>();

     public List<Stuff> getStuff() {
         if cache.hasStuff() {
             return cache.getStuff();
         }

         List<Stuffs> stuffs = stuffComputer.getStuff();
         cache.store(stuffs);
         return stuffs;
     }
}

这种设计使您可以轻松地添加缓存,删除缓存,更改基础派生逻辑(例如,访问数据库与返回模拟数据)等。虽然有些罗word,但对于足够高级的项目来说还是值得的。


-1

恕我直言,如果您使用合同设计,这非常简单。确定您的getter应该提供什么,并相应地进行编码(可能涉及或委托某处的简单代码或某些复杂逻辑)。


+1:我同意你的观点!如果该对象仅用于保存一些数据,则获取器应仅返回该对象的当前内容。在这种情况下,加载数据是其他对象的责任。如果合同说该对象是数据库记录的代理,则getter应该即时获取数据。如果数据已加载但不是最新的,则情况可能变得更加复杂:是否应将数据库中的更改通知给对象?我认为这个问题没有唯一的答案。
乔治
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.