0%

外部性与程序设计

经济学里面有一个名为“外部性“的概念。外部性是指一个人或企业的行为对其他人或企业产生的影响。

外部性可以是正的,也可以是负的。比如,一个企业的生产活动可能会产生污染,这就是一种负的外部性,对周围的环境和居民造成了伤害。相反,一个企业的生产活动也可能带来正面的外部性,比如提高周围地区的就业机会或改善周围地区的基础设施。

外部性对于组织职责划分的启示

利用这个概念,我们可以用来分析组织的职责划分。

什么样的职责应该由下级组织负责?我们说,当某一件事没有产生外部性或产生了正的外部性,那么下级组织应该负责。此时如果上级组织非得介入,则反而会增加下级组织的沟通成本,降低效率。因为上级组织往往由于掌握不了足够的细节,而要求下级组织频繁的汇报信息。

但是,当某一件事产生了负的外部性,那么上级组织应该负责。此时,下级组织往往没有驱动力去解决这个问题,因为这会增加下级组织的成本,但是却不会增加下级组织的收益。

举个例子。比如,企业在生产过程中赚到了钱,顺便改善了周边的经济环境,创造了正的外部性,很高兴。但是对于产生污染这样的负外部性,企业就不太会在意,且没有驱动力去解决,因为这会增加企业的成本。这时,作为上级组织的政府就应该介入,协调企业和周围居民的矛盾,并负责督促企业处理污染,避免对周围环境和居民造成伤害。

外部性对于程序设计的启示

在了解了外部性及其应用之后,我发现它对程序设计也有很大的启发。

无外部性或负外部性与高内聚

其一是在类或模块的职责划分上。如果一个类或模块的行为不会对其他类或模块产生影响,那这个行为就应该让这个类或模块自己处理。我们常常说的内聚性就是这个道理。如果一个类可以基于自己管理的数据独立完成某个功能,那么这个功能就应该由这个类自己实现,而不是由调用它的类来插手。

当我们看到在某一个类的方法中直接修改另一个对象的内部状态时,就应该警惕,因为维护这个状态可能是另一个对象自己的职责。越俎代庖很可能破坏了另一个对象的内聚性,并增加了系统的耦合度。

而当一个类或者一个模块的行为对其他类或者模块产生了影响,就产生了外部性。

如果这种外部性是正的,那么我们可以说这个函数或者模块是“无害的”,无需处理,并应极力鼓励。比如,某一个类优化了内部的算法,使得整个系统的性能提高了,这就是一种正的外部性。我们应该经常鼓励这样的优化。

但是,如果这种外部性是负的,那么我们就需要特别警惕,并考虑如何处理这种负外部性。这样的负外部性常常隐藏较深难以发现。

负外部性与上层协调

举一个大家经常碰到的例子。在基于数据库的后端程序开发中,我们常常需要从数据库中读取数据构建领域对象。JPA 可以帮我们自动完成这个过程。但是,如果我们不注意,JPA 可能会产生一种负的外部性,即 N+1 问题。

N+1 问题是指,当我们从数据库中读取一个领域对象时,JPA 会自动为这个领域对象的每一个关联对象发送一个额外的查询。如果这个领域对象有 N 个关联对象,那么就会发送 N+1 个查询。这将导致性能问题。

JPA 默认在非所有者端默认使用一种叫做“惰性加载”的模式来处理关联对象。惰性加载是指,当我们从数据库中读取一个领域对象时,不会立即查询关联对象,而是等到我们真正需要使用关联对象时,再发送查询。大多时候,这种模式是有效的,因为我们可能并不需要使用所有的关联对象。但是,如果我们需要使用所有的关联对象,那么就会产生 N+1 问题。

这种负外部性很难在领域对象内部自己解决,因为它不知道调用者何时会访问到哪些关联对象。

为了解决这个问题,我们可以在对应的 repository 中显示地定义一个方法,用于一次性加载所有关联对象(或通过参数指定需要加载哪些关联对象)。这样,我们就可以在需要的时候,在调用方(上级组织)显式地加载所有关联对象。在实现时,我们可以使用 JPA 的 fetch join 特性,在查询领域对象时,同时查询关联对象。这样,就可以避免 N+1 问题。

下面是一个示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@Entity
public class Order {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private Date orderDate;

@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
private List<OrderItem> items;

}

@Entity
public class OrderItem {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String productName;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id")
private Order order;

}

@Repository
public class OrderRepository {

@PersistenceContext
private EntityManager entityManager;

public Optional<Order> findOrderByIDWithItems(Long id) {
TypedQuery<Order> query = entityManager.createQuery(
"SELECT o FROM Order o LEFT JOIN FETCH o.items WHERE o.id = :id",
Order.class)
.setParameter("id", id);

List<Order> resultList = query.getResultList();
return resultList.isEmpty() ? Optional.empty() : Optional.of(resultList.get(0));
}

}

上述代码 findOrderByIDWithItems 被设计为供上层调用,其实现过程中:

  • SELECT o FROM Order o: 指定了我们要查询的主实体是 Order,并将其别名命名为 o
  • LEFT JOIN FETCH o.items: 这是 fetch join 的关键部分。这里我们执行了一次 左连接 (LEFT JOIN) 来包括所有订单。这意味着当你遍历结果列表中的每个订单并访问其 items 集合时,不会触发额外的数据库查询,因为所有必要的数据都已经在初始查询中被加载了。
    在发现 N+1 问题时,如果我们想在领域对象内部解决这个问题就很困难。此时应该改变思路,通过提供接口让上层组织中显式地调用,这个问题就迎刃而解了。

总结

外部性这一经济学概念在软件设计中也有鲜活的体现。通过识别和分析系统中的外部性,我们有以下启示:

  • 无外部性或正外部性:类或者模块的行为对其他部分没有影响(无外部性)或产生积极影响(正外部性),则其职责应尽量内聚,由自己处理,并鼓励正外部性优化。
  • 负外部性:行为对其他部分产生了负面影响,需要通过上层组织进行协调、解决。通过显式地提供清晰的接口,上层组织就可以灵活处理问题。

欢迎关注我的其它发布渠道