尽可能避免使用 getter 和 setter

2025-05-26

尽可能避免使用 getter 和 setter

不要生成 getter 和 setter!
不!!!不要点击生成 getters 和 setters 选项!!!

我喜欢这条规则:“不要使用访问器和修改器”。就像任何好的规则一样,这条规则注定会被打破。但是什么时候打破呢?

首先,让我明确一下我的观点:在考虑了一系列更好的替代方案之后,向面向对象类添加 getter 和 setter 应该是最后的选择。我相信,仔细分析后会得出这样的结论:在大多数情况下,getter 和 setter 都是有害的。

有什么危害?

首先我要指出的是,我所说的“危害”可能根本就没有任何危害。在某些情况下,以下分类是完全合理的:

// Car1.java

public class Car1 {
  public Engine engine;
}
Enter fullscreen mode Exit fullscreen mode

不过,请注意,当你看到类似的东西时,你可能会感到胃部收紧、头发竖起、肌肉紧张。

现在,我想指出的是,该类(一个带有公共类成员的公共类)与下面的类(一个带有通过 getter 和 setter 暴露的私有成员的公共类)之间没有任何有意义的功能差异。在Car1.javaCar2.java 这两个类中,我们得到的结果基本相同。

// Car2.java

public class Car2 {
  private Engine engine;

  public Engine getEngine() {
    return engine;
  }

  public void setEngine(Engine engine) {
    this.engine = engine;
  }
}
Enter fullscreen mode Exit fullscreen mode

为了说明这一点,我在Car1.javaCar2.java中读取和写入引擎

// Car1 member read and write
Car1 car1 = new Car1();
logger.debug("Car1's engine is {}.", car1.engine);
car1.engine = new HemiEngine();

// Car2 member read and write
Car2 car2 = new Car2();
logger.debug("Car2's engine is {}.", car2.getEngine());
car2.setEngine(new HemiEngine();
Enter fullscreen mode Exit fullscreen mode

重点是,我能用Car2.java 做的任何事,都能用Car1.java 做,反之亦然。这一点很重要,因为我们被教导说,看到Car1.java
就得小心翼翼。看到那个公共成员,我们就会说,不安全!引擎没有任何保护!任何人都可以为所欲为!啊啊啊啊啊啊啊啊啊啊啊啊啊

然而,当我们看到Car2.java时,不知为何,我们松了一口气。抱歉,我个人觉得这挺有意思的,因为这两件事的保护措施其实是一样的(根本不存在)。

可能出现什么问题?

以下是直接公开单个私有成员、具有相同名称且不提供其他功能的公共 getter 和 setter 的一些缺点。

Getter 和 Setter 是孤立变化的虚假保险政策

getter 和 setter 的一个优点是,如果需要更改类成员的类型,则可以通过将现有的 getter 简单地从内部类型转换为先前公开的类型,将更改限制在类内部。

// Car2.java, engine changed to motor

public class Car2 {
  private Motor motor;

  public Engine getEngine() {
    return convertToEngine(motor);
  }

  public void setEngine(Engine engine) {
    this.motor = convertToMotor(engine);
  }
}
Enter fullscreen mode Exit fullscreen mode

我的问题是,程序员有多少次需要这样做?我从事软件行业这么多年,从来没做过这种事。我一次也没能利用 getter 和 setter 提供的“假保险”政策。

engine此外,如果从一开始就没有暴露(假设它是私有的或包私有的),那么这个论点就毫无意义了。只需暴露行为,而不是状态,你就永远不需要担心修改实现的灵活性。

这种认为私有成员不应该被暴露的认识,引发了另一个认识:这个论点是同义反复的。Getter和 Setter 暴露了私有成员,并且它们的存在理由就建立在私有成员被暴露这一事实之上。

Getter 和 setter 公开实现细节

假设我只向您提供了我的 Car 对象的以下 API:

 _________________________________
| Car                             |
|---------------------------------|
| + getGasAmount(): Liters        |
| + setGasAmount(liters: Liters)  |
|_________________________________|
Enter fullscreen mode Exit fullscreen mode

如果你假设这是一辆汽油驱动的汽车,内部会以升为单位记录汽油量,那么你的预测 99.999% 都是正确的。这真的很糟糕,这就是为什么 getter 和 setter 暴露了实现/违反了封装性。现在这段代码很脆弱,很难修改。如果我们想要一辆氢燃料汽车怎么办?我们现在必须把整个 Car 类都扔掉。如果只使用像这样的行为方法就更好了fillUp(Fuel fuel)

这就是为什么一些著名的库会留下糟糕的遗留类。你有没有注意到,大多数语言都有一个数据结构,但在 Java 中Dictionary却被这样调用?实际上是 JDK 1.0 中引入的一个接口,但它存在问题,最终不得不被取代MapDictionaryMap

Getter 和 Setter 实际上可能很危险

我给你讲个关于我朋友的故事好吗?我说的是朋友!!!

有一天,这位朋友上班,发现全球几十个知名网站的标题和导航都和母公司主网站(而非自己的网站)一样,而且用的都是英式英语。运营团队疯狂地重启了全球数百台服务器,因为这些服务器在最初半个小时左右运行正常。然后(砰!)突然间,事情发生了变化,让整个系统都乱了套。

罪魁祸首是所有这些不同网站都在使用的共享平台深处的一个setter方法。恰好,在这次灾难发生前不久,一小段按计划运行的代码进行了更新,通过调用这个setter方法,改变了决定网站标题和语言的底层值。

如果只有一个 getter,情况可能同样糟糕。至少在 Java 中,从 getter 返回引用类型会将该引用提供给调用者,而调用者可能会以意想不到的方式对其进行操作。让我来演示一下。

public class Debts {
  private List<Debt> debts;

  public List<Debt> getDebts() {
    return debts;
  }
}
Enter fullscreen mode Exit fullscreen mode

好吧,这似乎很合理。我需要查看一个人的债务才能给他们一份账单。啊?你说什么?现在就可以添加债务了?妈的!怎么会这样!

Debts scottsDebts = DebtTracker.lookupDebts(scott);
List<Debt> debts = scottsDebts.getDebts();

// add the debt outside scotts debts, outside the debt tracker even
debts.add(new Debt(new BigDecimal(1000000))); 

// prints a new entry with one million dollars
DebtTracker.lookupDebts(scott).printReport();
Enter fullscreen mode Exit fullscreen mode

唷!

防止这种情况发生的一种方法是返回一个副本。另一种方法是使用不可变成员。不过,最好的方法是完全不以任何方式暴露该成员,而是将操作该成员的行为放在类内部。这样可以实现完全的实现隔离,并且只需在一个地方进行修改。

当 getter 有意义时

等一下!如果访问器和修改器有这么多缺点,为什么还要使用它们呢?

我确信,仅仅返回类成员的 getter 和 setter 几乎毫无意义。不过,只要你在该方法中确实做了一些事情,你就可以编写一些类似getter/setter 功能的代码。

两个例子:

  • 在 setter 中,在根据某些输入更新对象的状态之前,我们会验证输入。输入验证是附加功能。

  • getter 的返回类型是接口。因此,我们将实现与暴露的接口解耦。

瞧,我真正提倡的是对 getter 和 setter 的不同立场和理念。与其说是说永远不要使用访问器和修改器,不如给你列出一些我在使用它们之前尝试过的各种选项:

  • 我的“默认”设置是从一个私有 final 成员开始,仅由构造函数设置。没有 getter 或 setter!

  • 如果另一个类确实需要访问这个成员,我会思考原因。我会尝试寻找可以公开的行为,并为该行为创建一个方法。

  • 如果由于某种原因绝对有必要,那么我会放宽到包私有(在 Java 中)并且仅将成员暴露给同一包中的其他类,但不会进一步暴露。

  • 好的,那么数据用例呢?实际上,我可能需要一个对象来跨某种接口边界传递数据(比如传递到文件系统、数据库、Web 服务或其他对象)。我仍然不会费力地处理 getter 和 setter。我创建了一个包含所有包私有成员的类,并将其视为一个属性包并使用它。我尝试将它们限制在应用程序边界的各自的包和层中。

  • 我会考虑在公共 API 中为数据用例创建 getter 和 setter,比如说,我正在编写一个库,打算将其用作许多其他应用程序的依赖项。但我只会在用尽所有这些列表项之后才会考虑。

大师的智慧

简短的后记。显然,世界上关于 getter 和 setter 的争论一直存在。重要的是要知道,像罗伯特·C·马丁(绰号“鲍勃叔叔”)这样的“大师”显然是主张避免使用 getter 和 setter 的。在《代码整洁之道》一书的第六章中,马丁写道:

Bean 拥有由 getter 和 setter 操作的私有变量。Bean 的准封装性似乎让一些面向对象纯粹主义者感觉良好,但通常不会带来其他好处。

Josh Bloch 在《Effective Java》 第 14 条中持一种微妙的立场,他略微赞成在公共类中使用 getter 和 setter,而略微反对其他类。他最终基本上说,他真正关心的是可变性,这一点我在上面提到过:

总而言之,公共类永远不应该暴露可变字段。公共类暴露不可变字段虽然危害较小,但仍然值得商榷。然而,有时包私有或私有嵌套类暴露字段(无论是可变的还是不可变的)是可取的。

进一步阅读

以下是一些来自比我聪明的人的有益想法。

告诉,不要问

为什么 getter 和 setter 方法是邪恶的

访问器是邪恶的

文章来源:https://dev.to/scottshipp/avoid-getters-and-setters-whenever-possible-c8m
PREV
编程语言的名字由来
NEXT
使用 Markdown 和 Git 的出色笔记系统(第 2 部分)