如何避免使用getter和setter方法

30
我在许多地方都读到过“getter和setter是有害的”的说法。我也明白为什么会这样。但我不知道怎样完全避免使用它们。比如说,Item是一个类,包含了关于商品名称、数量、价格等信息。而ItemList是一个类,包含了Item对象的列表。为了计算总价:

int grandTotal()
{
int total = 0;
for (Item item: itemList) total += item.getPrice();
return total; }

在上面的例子中,如何避免使用getPrice()方法呢?因为Item类提供了getName,setName等方法……

那么,我该怎么避免使用它们呢?


你的“Item”除了原始数据还有其他的吗? - Jerry Coffin
8
谁说它们是“邪恶”的?也许是那些不想在每个类中写额外40个方法的懒人。我认为将私有变量变成公共的更加“邪恶”。 - Wes
10
获取器/设置器被认为是邪恶的并且会破坏封装,这是由Allen Holub在JavaWorld论证过的一个著名案例。在文章《Why getter and setter methods are evil》中可以了解到该问题,不要轻视这个问题,它与多输入几个按键完全无关。 - Andres F.
1
@AndresF。我之前读过他的文章(特别是关于extends的那篇),虽然他提出了一些好的观点,但他用的措辞有些煽动性。无论是extends vs. contains还是getters/setters这些话题都非常重要,有很多灰色地带,所以很遗憾看到他的文章激发了这些宗教般的争论。新手程序员尤其容易看到绝对的东西并将其视为真理(可以这么说)。 - Jonathan Fingland
15个回答

47

何时应该使用getter和setter?

Getter和setter非常适合用于配置或确定类的配置,或从模型中检索数据。

获取物品价格是使用getter的完全合理用途。这是需要可用的数据,并且可能涉及通过在setter中添加验证或净化来保护数据的特殊考虑。

您还可以提供没有setter的getter。它们不一定要成对出现。

何时不应使用getter和setter?

有时对象依赖于永远不会公开的内部属性。例如,迭代器和内部集合。公开内部集合可能会产生极其负面和意外的后果。

此外,例如,假设您正在通过某个HttpURLConnection进行通信。公开HttpURLConnection的setter意味着在等待接收数据时更改连接可能会导致非常奇怪的状态。这个连接是应该在实例化时创建或完全在内部管理的东西。

总结

如果您拥有实际上是公共的数据,但需要进行管理:请使用getter和setter。

如果您需要检索数据,但在任何情况下都不应更改数据:请使用getter而不是setter。

如果您需要设置用于内部目的并且不应公开(并且不能在实例化时设置)的数据:请使用setter而不是getter(setter可能会防止第二次调用影响内部属性)

如果您有某些完全内部的东西,没有其他类需要直接访问或更改它,请不要使用getter和setter。

不要忘记,getter和setter可以是私有的,即使对于内部管理的属性,具有管理属性的setter也可能是可取的。例如,将连接字符串传递给HttpURLConnection的setter。

还要注意:

Allen Holub的文章为什么getter和setter方法是有害的似乎是OP推理的来源,但在我看来,这篇文章没有很好地解释它的观点。

编辑:添加摘要
编辑2:拼写更正


你的回答更清晰地阐明了Allen Holub的文章的主旨。我之前很难理解它的观点,以及他试图提供的替代方案。 - Elias
避免使用setter的问题在于现在你必须通过构造函数来设置数据。一旦你有一个需要超过5个参数的构造函数,查看和修改数据就变得非常麻烦。 - IcedDante

20

很遗憾看到一小部分高声喊叫的人对整个 "获取器和设置器" 是邪恶的辩论进行反击。首先,文章标题是故意挑衅性的,以吸引你的注意,就像任何博客文章一样。我之前也写过关于这个问题的博客文章几年后更新了我的观点和想法。我会在这里尽力概括。

  • 获取器和设置器(访问器)并不是邪恶的
  • 然而,它们大多数时候是“邪恶”(不必要的)
  • 封装不仅仅是在私有字段周围添加访问器来控制更改,毕竟只添加修改私有字段的get/set方法没有任何好处
  • 你应该尽可能地使用"告诉,不要询问"原则编写代码
  • 对于框架代码、DTO、序列化等,你需要使用访问器。不要试图反抗这一点。
  • 然而,你希望你的核心业务逻辑(业务对象)尽可能少地包含属性。你应该告诉对象去做事情,而不是随意检查它们的内部状态。

如果你有大量的访问器,实际上你违反了封装的原则。例如:

class Employee
{
   public decimal Salary { get; set; }

   // Methods with behaviour...
}

这是一个糟糕的领域对象,因为我可以做到这一点:

me.Salary = 100000000.00;

这可能是一个简单的例子,但任何在专业环境中工作的人都可以证明,如果有一些公共代码,人们会利用它。开发者看到这个例子后,不妨开始在代码库周围添加大量检查来使用薪资决定如何处理雇员,这并不是错误的做法。
更好的对象应该是:
class Employee
{
   private decimal salary;

   public void GivePayRise()
   {
        // Should this employee get a pay rise.
        // Apply business logic - get value etc...
        // Give raise
   }

   // More methods with behaviour
}

现在我们不能依赖工资是公开的信息。任何想给员工加薪的人都必须通过这种方式来做。这很好,因为这个业务逻辑被包含在一个地方。我们可以改变这一个地方,影响到使用Employee的所有地方。

1
不过有一个问题:如果加薪取决于其他因素,比如公司员工数量或经济状况,你会告诉员工这些信息吗?还是会将其委派给另一个类来处理(至少我会这样做)?但是如果你要委派,那么该类了解这些信息的唯一方法就是从其他类中获取信息? - Stefan Hendriks
1
我要诚实地说,你的例子并没有支持你的观点。有很多情况下获取和设置薪资是必要的。最好检查约束条件而不是简单地消除选项。请求的用户或对象是否有权限这么做?在你的例子中,你使用了空的getter和setter,这对于打桩计划扩展的函数来说是可以的,但这完全不是getter和setter的用途。当需要数据访问或操作以及需要进行一些管理、验证或净化时,请使用getter和setter。 - Jonathan Fingland
@JonathanFingland 第一个示例没有方法,但假设与类相关联的逻辑。换句话说,该示例将具有行为。带有逻辑的Getter / Setter,例如对象不能将其设置为特定值,通常暗示比仅获取/设置值更复杂的逻辑,因此应将其作为方法。 - Finglas
@StefanHendriks 这个例子只是一时兴起,但遵循DDD思想的话,涉及到一个或多个服务。关键是“某些东西”会告诉每个员工获得加薪,然后每个员工/服务都会做正确的事情。他们会知道自己工作了多长时间,级别等等。 - Finglas
setSalary(salary) { //获取会话凭据; //检查权限; //更新薪资值; //记录更新 } 这只是一个例子,但我认为它能够传达要点。有时候你只想以一种受控的方式设置值。给予加薪并不一定是一个坏函数,但它与设置薪资不同。 - Jonathan Fingland
2
@JonathanFingland 是的,它们并不相同。我的观点是为了演示如何增加/减少员工的薪水,而__不是__获取/设置他们的薪水。在员工管理领域中,任意设置员工的薪水是没有意义的。 - Finglas

16
以下示例展示了模板代码中出色的 setter 和 getter 示例。
class Item{
  private double price;

  public void setPrice(final double price){
    this.price = price;
  }
  public double getPrice(){
    return this.price;
  }
}

有些程序员认为这被称为封装,但实际上这段代码与以下代码完全等效:

class Item{
  public double price;
}

两个类中都没有保护或封装 price,但第二个类更易于阅读。

 class Item{
    private double price;

    public void setPrice(final double price){
      if(isValidPrice(price))
        this.price = price;
      else throw new IllegalArgumentException(price+" is not valid!");
    }

    public double getPrice(){
      return this.price;
    }
  }

这是真正的封装,类的不变式由setPrice保护。我的建议- 不要编写虚拟的getter和setter,只有在它们保护类不变式时才使用getter和setter


1
代理模式还可以用于权限控制。 - Matt Gibson
8
唯一的问题是,在编写代码时,您假设已经知道要执行所有验证和错误检查。编写简单情况下的setter方法的目的是,您可以稍后添加参数转换、验证等操作,而无需更改对someitem.setPrice(price)的调用方式。显然,添加异常会产生连锁反应,但这对任何情况都是适用的。 - Jonathan Fingland
2
@Jonathan,使用现代IDE,重命名或创建getter/setter只需要几秒钟的时间。而且getter/setter允许更精确的调试。我的观点是,虚拟的getter/setter会使代码更难读懂。 - Kiril Kirilov
4
你不能在几秒钟内改变使用你的类的其他人的代码。 - dan04
但是,为什么会有人设置无效的价格呢?如果是来自用户,那么验证应该在读取用户表单后立即进行。 - Giumo
显示剩余5条评论

7
我在很多地方看到过“getter和setter是邪恶的”的说法。真的吗?这听起来对我来说很疯狂。许多?给我们展示一个。我们会把它撕成碎片。
我理解为什么会有人这样说,但我并不认同。这似乎很荒谬。要么你误解了,但认为你已经明白,要么原始来源只是疯狂。
但是我不知道如何完全避免使用他们。
你不应该完全避免使用他们。
如何避免 getPrice?
你为什么想要避免它?否则,您将如何从对象中获取数据呢?
如何避免它们?
不要。停止阅读疯狂的言论。

3
这篇文章似乎是典范性的文章,但是确实有些言过其实。 - SWeko
6
因为没有努力去理解问题,所以被批评。确实,Allen Holub的文章是经典资料,但这并不是无稽之谈,而是一个合理的担忧。我们被教导盲目地编写大量的getter和setter,并不意味着这是编写Java代码的最佳方式。 - Andres F.
3
@Andres F.:请原谅,我的意思是:1.提供上下文的责任在于OP;我在我的回答中告诉OP提供上下文,以便我们可以分析它。2.OP没有标记为Java。3.OP没有链接到Holub文章,因此因未阅读该文章而投反对票是愚蠢的;如果我能的话,我会投你的评论反对票。4.没有明智的人会盲目地教授编写getter和setter。 - jason
8
暴露对象内部细节是不良的面向对象编程风格,无论是哪种编程语言。你必须提供一个高质量的答案,而你没有——我不是因为不同意而对你进行了贬低评价,而是因为你没有费心研究这个问题,把它视为“疯狂的言论”。一个经过深入研究的回答应该注意到设计问题,并解释在某些情况下打破封装是不可避免的原因。 - Andres F.
2
@Jason,我想为Holub的文章辩护的一点是,有时候类会有一些变量需要跟踪,但只用于内部使用,不需要暴露出来(例如迭代器及其内部集合)。对于这些情况,getter或setter是不可取的。Holub只是在解释这一点上做得很糟糕。 - Jonathan Fingland
显示剩余9条评论

5

当有人告诉你getter和setter是邪恶的时候,请思考一下他们为什么这样说。

Getter

它们是邪恶的吗?在代码中不存在所谓的“邪恶”。代码只是代码,既不好也不坏。问题只在于阅读和调试的难度。

在您的情况下,我认为使用getter计算最终价格是完全可以的。

“邪恶”之处

用例:您想在购买某物时获取其价格。

有时人们会像这样使用getter:

if(item.getPrice() <= my_balance) {
   myBank.buyItem(item);
}

这段代码没有问题,但它并不像它本可以那样直截了当。看看这个更实用的方法:

myBank.buyItem(item); //throws NotEnoughBalanceException

当购买物品时,买家或收银员无需检查商品价格。这实际上是银行的工作。想象一下,客户 A 有一个名为 SimpleBank.java 的项目。

public class SimpleBank implements Transaction {

  public void buyItem(Item item){
     if(getCustomer().getBalance() >= item.getPrice()){
        transactionId = doTransaction(item.getPrice());
        sendTransactionOK(transactionId);
     }
  }
}

第一种方法在这里看起来很好。但是如果客户B有一个NewAndImprovedBank.java文件呢?
public class NewAndImprovedBank implements Transaction {

  public void buyItem(Item item){
     int difference = getCustomer().getBalance() - item.getPrice();
     if (difference >= 0) {
        transactionId = doTransaction(item.getPrice());
        sendTransactionOK(transactionId);
     } else if (difference <= getCustomer().getCreditLimit()){
        transactionId = doTransactionWithCredit(item.getPrice());
        sendTransactionOK(transactionId);
     }
  }
}

你可能认为当使用第一种方法时你正在进行防御,但实际上你是在限制你系统的能力。

结论

不要请求授权,而是请求宽恕,也就是使用 NotEnoughBalanceException 而不是 item.getPrice()


3

我猜测getPrice()正在访问一个私有变量。

直接回答你的问题,将价格变量设置为public,并编写类似以下代码(语法可能因语言、指针使用等而异):

total += item.price;

然而,这通常被认为是不良风格。类变量通常应保持私有。请参见我对问题的评论。

1
如果没有阅读Allen Holub的文章,就给出-1分吧。使用getter/setter的替代方案不是暴露内部属性,而是重新考虑你的设计。 - Andres F.
5
请原谅,你能再说一遍吗?我没看过某个网站上的某篇文章并不重要。网络上到处都是垃圾文章。他询问了“如何做”,而我向他展示了一种“如何”的方法。在我看来值得一试。+1 - Wes
5
@AndresF。在发布此答案时,根本没有引用那篇文章。问题甚至没有提及它,只有您在发布此答案4分钟后的评论中首次提到了它。因此,期望读过它是不现实的,而对此进行负评定绝对是荒谬的。 - GaryJL

2
如何避免使用getter和setter?设计类以便实际操作它们所持有的数据。
getter方法会误导关于数据的信息。在例子中的Item.getPrice(),我可以看到我得到了一个int类型的值。但是这个价格是以美元还是分为单位?是否包括税费?如果我想知道在不同的国家或州的价格,我还能使用getPrice()吗?
是的,这可能超出了系统设计的范围,而且你可能只是从方法中返回变量的值,但是通过使用getter方法来公开这种实现细节会削弱你的API。

2
迄今为止缺少的不同观点是:getter和setter会违反“Tell Don't Ask”原则!
想象一下,您在超市购物。最后,收银员需要向您要钱。getter/setter方法是:您把钱包交给收银员,收银员数一下您钱包里的钱,拿走您欠的钱,然后把钱包还给您。
现实中您是这样做的吗?完全不是。在现实世界中,您通常不关心“自治”其他“对象”的内部状态。收银员告诉您:“您的账单是5.85美元”。然后您付款。您如何付款由您决定,唯一需要的是收银员从您那里收到相应金额的钱。
因此:通过以行为为思考方式而不是以状态为思考方式,你可以避免使用getter和setter。Getter/setter从“外部”操作状态(通过执行avail = purse.getAvailableMoney()purse.setAvailableMoney(avail - 5.85)),而你想要做的是调用person.makePayment(5.85)

2

'邪恶'作为.getAttention()

这个问题经常被讨论,甚至有时因对话中使用了贬义词"邪恶"而引起轰动。当然,在某些情况下你需要它们。但问题在于正确使用它们。你看,Holub教授的抱怨并不是关于你的代码现在做了什么,而是关于将自己限制住,以致未来的改变会变得痛苦和容易出错。

事实上,我读过他所有的东西都有这个主题。

这个主题如何适用于类 Item

Item的未来展望

这里是虚构的物品类:

class Item{
    private double price;

    public void setPrice(final double price){
      if(isValidPrice(price))
        this.price = price;
      else throw new IllegalArgumentException(price+" is not valid!");
    }

    public double getPrice(){
      return this.price;
    }
  }

这很好,但从某种意义上说,它仍然是“邪恶”的,因为它可能会在未来给你带来很多麻烦。

这种麻烦可能来自于有一天,“价格”可能必须考虑不同的货币(甚至更复杂的易货交换计划)。通过将价格设置为double类型,在“启示录”(毕竟我们在谈论邪恶)之前和之后编写的任何代码都将连接到double类型的价格。

最好的做法(甚至可以说是好的做法)是传递一个Price对象而不是一个double类型。这样做可以轻松实现对“价格”含义的更改,而不会破坏现有接口。

关于getter和setter的要点

如果您发现自己在简单类型上使用getter和setter,请确保考虑接口的可能未来变化。有很大的机会表明您不应该这么做。您是否正在使用setName(String name)?您应该考虑使用setName(IdentityObject id)或甚至setIdentity(IdentityObject id),以防其他身份验证模型出现(头像、密钥等)。当然,您始终可以四处走动并在所有内容上使用setAvatarsetKey,但是通过在方法签名中使用对象,您可以更轻松地将其扩展到可以使用新身份属性的对象,而不会破坏遗留对象。


1

Cloudanger的答案是“1”,但您还必须意识到,物品清单可能包含许多带有订购数量的物品对象。

解决方案:在它们中间创建另一个类,该类将您的物品存储在物品列表中,并存储该物品的订购数量(假设该类称为OrderLine)。

OrderLine将具有Item和qty字段。

之后,在Item中编写类似于calculateTotal(int qty)的代码,返回price * qty。在OrderLine中创建一个调用calculateTotal(qtyOrdered)的方法。

将返回值传递给itemList。

这样,您可以避免使用getter。ItemList只会知道总价格。您的代码应该与数据一起运行。询问具有数据的对象来计算totalPrice,而不是要求该对象提供原始数据来计算totalPrice。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接