构造函数还是静态工厂方法?

位置:首页>文章>详情   分类: Java教程 > 编程技术   阅读(247)   2024-04-03 06:20:37

我相信 Joshua Bloch 在他的好书 “Effective Java” 中首先说过:与构造函数相比,静态工厂方法是实例化对象的首选方式。我不同意。不仅因为我相信静态方法是纯粹的邪恶,而且主要是因为在这种特殊情况下,它们假装是好的,让我们认为我们必须爱它们。

Mike Judge 摘录 (2009)

让我们从面向对象的角度来分析推理,看看为什么它是错误的。

这是一个具有一个主要构造函数和两个辅助构造函数的类:

class Color {
  private final int hex;
  Color(String rgb) {
    this(Integer.parseInt(rgb, 16));
  }
  Color(int red, int green, int blue) {
    this(red << 16 + green << 8 + blue);
  }
  Color(int h) {
    this.hex = h;
  }
}

这是一个具有三个静态工厂方法的类似类:

class Color {
  private final int hex;
  static Color makeFromRGB(String rgb) {
    return new Color(Integer.parseInt(rgb, 16));
  }
  static Color makeFromPalette(int red, int green, int blue) {
    return new Color(red << 16 + green << 8 + blue);
  }
  static Color makeFromHex(int h) {
    return new Color(h);
  }
  private Color(int h) {
    return new Color(h);
  }
}

你更喜欢哪一个?

根据 Joshua Bloch 的说法,使用静态工厂方法而不是构造函数有三个基本优势(实际上有四个,但第四个不再适用于 Java ):

  • 他们有名字。
  • 他们可以缓存。
  • 他们可以分类型。

我相信这三者都非常有道理……如果设计有误的话。它们是变通办法的好借口。让我们一一介绍。

他们有名字

这是使用构造函数创建 red tomato 颜色对象的方法:

Color tomato = new Color(255, 99, 71);

这是使用静态工厂方法的方式:

Color tomato = Color.makeFromPalette(255, 99, 71);

似乎 makeFromPalette() 在语义上比 new Color() 更丰富,对吧?嗯,是。如果我们只是将它们传递给构造函数,谁知道这三个数字意味着什么。但是“调色板”这个词可以帮助我们立即弄清楚一切。

真的。

然而,正确的解决方案是使用多态和封装,将问题分解为几个语义丰富的类:

interface Color {
}
class HexColor implements Color {
  private final int hex;
  HexColor(int h) {
    this.hex = h;
  }
}
class RGBColor implements Color {
  private final Color origin;
  RGBColor(int red, int green, int blue) {
    this.origin = new HexColor(
      red << 16 + green << 8 + blue
    );
  }
}

现在,我们使用正确类的正确构造函数:

Color tomato = new RGBColor(255, 99, 71);

看到了吗,约书亚?

他们可以缓存

假设我需要在应用程序的多个位置使用红番茄色:

Color tomato = new Color(255, 99, 71);
// ... sometime later
Color red = new Color(255, 99, 71);

将创建两个对象,这显然是低效的,因为它们是相同的。最好将第一个实例保留在内存中的某个位置,并在第二个调用到达时返回它。静态工厂方法可以解决这个问题:

Color tomato = Color.makeFromPalette(255, 99, 71);
// ... sometime later
Color red = Color.makeFromPalette(255, 99, 71);

然后在 Color 的某个地方,我们保留了一个私有静态 Map ,其中包含所有已经实例化的对象:

class Color {
  private static final Map<Integer, Color> CACHE =
    new HashMap<>();
  private final int hex;
  static Color makeFromPalette(int red, int green, int blue) {
    final int hex = red << 16 + green << 8 + blue;
    return Color.CACHE.computeIfAbsent(
      hex, h -> new Color(h)
    );
  }
  private Color(int h) {
    return new Color(h);
  }
}

它在性能方面非常有效。对于像我们的 Color 这样的小对象,问题可能不会那么明显,但是当对象更大时,它们的实例化和垃圾收集可能会浪费很多时间。

真的。

然而,有一种面向对象的方法可以解决这个问题。我们刚刚引入了一个新类 Palette,它成为颜色的存储:

class Palette {
  private final Map<Integer, Color> colors =
    new HashMap<>();
  Color take(int red, int green, int blue) {
    final int hex = red << 16 + green << 8 + blue;
    return this.computerIfAbsent(
      hex, h -> new Color(h)
    );
  }
}

现在,我们创建一个 Palette 的实例,并要求它在我们每次需要它时返回一种颜色给我们:

Color tomato = palette.take(255, 99, 71);
// Later we will get the same instance:
Color red = palette.take(255, 99, 71);

看,Joshua,没有静态方法,没有静态属性。

他们可以分型

假设我们的类 Color 有一个方法 lighter(),它应该将颜色转移到下一个可用的较浅的颜色:

class Color {
  protected final int hex;
  Color(int h) {
    this.hex = h;
  }
  public Color lighter() {
    return new Color(hex + 0x111);
  }
}

然而,有时更希望通过一组可用的 Pantone 颜色选择下一个较浅的颜色:

class PantoneColor extends Color {
  private final PantoneName pantone;
  PantoneColor(String name) {
    this(new PantoneName(name));
  }
  PantoneColor(PantoneName name) {
    this.pantone = name;
  }
  @Override
  public Color lighter() {
    return new PantoneColor(this.pantone.up());
  }
}

然后,我们创建一个静态工厂方法,它将决定哪种 Color 实现最适合我们:

class Color {
  private final String code;
  static Color make(int h) {
    if (h == 0xBF1932) {
      return new PantoneColor("19-1664 TPX");
    }
    return new RGBColor(h);
  }
}

如果请求 true red 颜色,我们将返回 PantoneColor 的实例。在所有其他情况下,它只是一个标准的 RGBColor。由静态工厂方法做出决定。这就是我们如何称呼它:

Color color = Color.make(0xBF1932);

不可能对构造函数进行相同的“分叉”,因为它只能返回声明它的类。静态方法具有返回 Color 的任何子类型的所有必要自由。

真的。

然而,在面向对象的世界中,我们可以而且必须以不同的方式来做这件事。首先,我们会让 Color 成为一个接口:

interface Color {
  Color lighter();
}

接下来,我们将把这个决策过程移到它自己的类 Colors 中,就像我们在前面的例子中所做的那样:

class Colors {
  Color make(int h) {
    if (h == 0xBF1932) {
      return new PantoneColor("19-1664-TPX");
    }
    return new RGBColor(h);
  }
}

我们将在 Colors 中使用类 Color 的实例而不是静态工厂方法:

colors.make(0xBF1932);

然而,这仍然不是真正的面向对象的思维方式,因为我们正在将决策权从它所属的对象上移开。通过静态工厂方法 make() 或新类 Colors——如何实现并不重要——我们将对象分成两部分。第一部分是对象本身,第二部分是留在其他地方的决策算法。

一个更面向对象的设计是将逻辑放入类 PantoneColor 的对象中,它会装饰原始的 RGBColor

class PantoneColor {
  private final Color origin;
  PantoneColor(Color color) {
    this.origin = color;
  }
  @Override
  public Color lighter() {
    final Color next;
    if (this.origin.hex() == 0xBF1932) {
      next = new RGBColor(0xD12631);
    } else {
      next = this.origin.lighter();
    }
    return new PantoneColor(next);
  }
)

然后,我们创建一个 RGBColor 的实例并用 PantoneColor 装饰它:

Color red = new PantoneColor(
  new RGBColor(0xBF1932)
);

我们要求 red 返回一种较浅的颜色,它返回 Pantone 调色板中的颜色,而不是仅在 RGB 坐标中较浅的颜色:

Color lighter = red.lighter(); // 0xD12631

当然,这个例子相当原始,如果我们真的希望它适用于所有 Pantone 颜色,还需要进一步改进,但我希望你能理解。逻辑必须留在类的内部,而不是外部的某个地方,不能在静态工厂方法中,甚至不能在其他一些补充类中。当然,我说的是属于这个特定类的逻辑。如果是类实例管理相关的东西,就可以有容器和存储,就像上面的例子一样。

总而言之,我强烈建议您永远不要使用静态方法,尤其是当它们要替换对象构造函数时。通过对象的构造函数诞生一个对象,是任何面向对象软件中最“神圣”的时刻,千万不要错过它的美妙之处。

您可能还会发现这些相关的帖子很有趣:Each Private Static Method Is a Candidate for a New Class; 你的架构师越好,你的图表就越简单; 只能有一个主构造函数; 为什么InputStream的设计是错误的; 为什么许多返回语句在 OOP 中不是一个好主意;

标签2: Java教程
地址:https://www.cundage.com/article/jcg-constructors-static-factory-methods.html

相关阅读

Java HashSet 教程展示了如何使用 Java HashSet 集合。 Java哈希集 HashSet 是一个不包含重复元素的集合。此类为基本操作(添加、删除、包含和大小)提供恒定时间性...
SpringApplicationBuilder 教程展示了如何使用 SpringApplicationBuilder 创建一个简单的 Spring Boot 应用程序。 春天 是用于创建企业应...
通道是继 buffers 之后 java.nio 的第二个主要新增内容,我们在之前的教程中已经详细了解了这一点。通道提供与 I/O 服务的直接连接。 通道是一种在字节缓冲区和通道另一端的实体(通...
课程大纲 Elasticsearch 是一个基于 Lucene 的搜索引擎。它提供了一个分布式的、支持多租户的全文搜索引擎,带有 HTTP Web 界面和无模式的 JSON 文档。 Elasti...
解析器是强大的工具,使用 ANTLR 可以编写可用于多种不同语言的各种解析器。 在这个完整的教程中,我们将: 解释基础:什么是解析器,它可以用来做什么 查看如何设置 ANTLR 以便在 Java...
Java 是用于开发各种桌面应用程序、Web 应用程序和移动应用程序的最流行的编程语言之一。以下文章将帮助您快速熟悉 Java 语言,并迈向 API 和云开发等更复杂的概念。 1. Java语言...
Java中的继承是指子类继承或获取父类的所有非私有属性和行为的能力。继承是面向对象编程的四大支柱之一,用于提高层次结构中类之间的代码可重用性。 在本教程中,我们将了解 Java 支持的继承类型,...
Java Message Service 是一种支持正式通信的 API,称为 网络上计算机之间的消息传递。 JMS 为支持 Java 程序的标准消息协议和消息服务提供了一个通用接口。 JMS 提...
Java 项目中的一项常见任务是将日期格式化或解析为字符串,反之亦然。解析日期意味着你有一个代表日期的字符串,例如“2017-08-3”,你想把它转换成一个代表 Java 中日期的对象,例如Ja...
之前,我介绍了spring 3 + hibernate 集成 示例和struts 2 hello world 示例。在本教程中,我将讨论在将 spring 框架与 struts 与 hibern...