Item 33. Name Lookup and the Interface Principle part 3

本文探讨了C++中运算符<<的不同实现方式及其对类依赖的影响,通过对比两种常见方法,结合接口原则分析了它们之间的权衡。

Difficulty: 5

Take a few minutes to consider some implications of the Interface Principle on the way we reason about program design. We'll revisit a classic design problem: "What's the best way to write operator<<()?"

There are two main ways to write operator<<() for a class: as a free function that uses only the usual interface of the class (and possibly accessing nonpublic parts directly as a friend), or as a free function that calls a virtual Print() helper function in the class.

Which method is better? What are the tradeoffs?

Solution

graphics/bulb_icon.gif

Are you wondering why a question like this gets a title like "Name Lookup?A class="docLink" HREF="0201615622_part03.html#part03">Part 3"? If so, you'll soon see why, as we consider an application of the Interface Principle discussed in the previous Item.

What Does a Class Depend On?

"What's in a class?" isn't just a philosophical question. It's a fundamentally practical question, because without the correct answer, we can't properly analyze class dependencies.

To demonstrate this, consider a seemingly unrelated problem: What's the best way to write operator<< for a class? There are two main ways, both of which involve tradeoffs. I'll analyze both. In the end we'll find that we're back to the Interface Principle and that it has given us important guidance to analyze the tradeoffs correctly.

Here's the first way:

//*** Example 5 (a) -- nonvirtual streaming 
class X
{
  /*...ostream is never mentioned here...*/
};
ostream& operator<<( ostream& o, const X& x )
{
  /* code to output an X to a stream */
  return o;
}

Here's the second:

//*** Example 5 (b) -- virtual streaming 
class X
{
  /*...*/
public:
  virtual ostream& print( ostream& ) const;
};
ostream& X::print( ostream& o ) const
{
  /* code to output an X to a stream */
  return o;
}
ostream& operator<<( ostream& o, const X& x )
{
  return x.print( o );
}

Assume that in both cases the class and the function declaration appear in the same header and/or namespace. Which one would you choose? What are the tradeoffs? Historically, experienced C++ programmers have analyzed these options this way:

  • Option (a)'s advantage (we've said until now) is that X has fewer dependencies. Because no member function of X mentions ostream, X does not (appear to) depend on ostream. Option (a) also avoids the overhead of an extra virtual function call.

  • Option (b)'s advantage is that any DerivedX will also print correctly, even when an X& is passed to operator<<.

This is the traditional analysis. Alas, this analysis is flawed. Armed with the Interface Principle, we can see why: The first advantage in Option (a) is a phantom, as indicated by the comments in italics.

  1. According to the Interface Principle, as long as operator<< both "mentions" X (true in both cases) and is "supplied with" X (true in both cases), it is logically part of X.

  2. In both cases, operator<< mentions ostream, so operator<< depends on ostream.

  3. Because, in both cases, operator<< is logically part of X and operator<< depends on ostream, therefore in both cases, X depends on ostream.

So what we've traditionally thought of as Option (a)'s main advantage is not an advantage at all. In both cases, X still in fact depends on ostream anyway. If, as is typical, operator<< and X appear in the same header X.h, then both X's own implementation module and all client modules that use X physically depend on ostream and require at least its forward declaration in order to compile.

With Option (a)'s first advantage exposed as a phantom, the choice really boils down to just the virtual function call overhead. Without applying the Interface Principle, though, we would not have been able to as easily analyze the true dependencies (and therefore the true tradeoffs) in this common real-world example.

Bottom line, it's not always useful to distinguish between members and nonmembers, especially when it comes to analyzing dependencies, and that's exactly what the Interface Principle implies.

Some Interesting (and Even Surprising) Results

In general, if A and B are classes and f(A,B) is a free function:

  • If A and f are supplied together, then f is part of A, so A depends on B.

  • If B and f are supplied together, then f is part of B, so B depends on A.

  • If A, B, and f are supplied together, then f is part of both A and B, so A and B are interdependent. This has long made sense on an instinctive level梚f the library author supplies two classes and an operation that uses both, the three are probably intended to be used together. Now, however, the Interface Principle has given us a way to more clearly state this interdependency.

Finally, we get to the really interesting case. In general, if A and B are classes and A::g(B) is a member function of A:

  • Because A::g(B) exists, clearly A always depends on B. No surprises so far.

  • If A and B are supplied together, then of course A::g(B) and B are supplied together. Therefore, because A::g(B) both "mentions" B and is "supplied with" B, then according to the Interface Principle, it follows (perhaps surprisingly, at first) that A::g(B) is part of B, and because A::g(B) uses an (implicit) A* parameter, B depends on A. Because A also depends on B, this means that A and B are interdependent.

At first, it might seem like a stretch to consider a member function of one class as also part of another class, but this is true only if A and B are also supplied together. Consider: If A and B are supplied together (say, in the same header file) and A mentions B in a member function like this, "gut feel" already usually tells us A and B are probably interdependent. They are certainly strongly coupled and cohesive, and the fact that they are supplied together and interact means that: (a) they are intended to be used together, and (b) changes to one affect the other.

The problem is that, until now, it's been hard to prove A and B's interdependence with anything more substantial than gut feel. Now their interdependence can be demonstrated as a direct consequence of the Interface Principle.

Note that, unlike classes, namespaces don't need to be declared all at once, and what's "supplied together" depends on what parts of the namespace are visible.

//*** Example 6 (a) 
//---file a.h---
namespace N { class B; }// forward decl
namespace N { class A; }// forward decl
class N::A { public: void g(B); };
//---file b.h---
namespace N { class B { /*...*/ }; }

Clients of A include a.h, so for them A and B are supplied together and are interdependent. Clients of B include b.h, so for them A and B are not supplied together.

In summary, I'd like you to take away three thoughts from this miniseries.

  1. The Interface Principle: For a class X, all functions, including free functions, that both "mention" X and are "supplied with" X are logically part of X, because they form part of the interface of X.

  2. Therefore, both member and nonmember functions can be logically "part of " a class. A member function is still more strongly related to a class than is a nonmember, however.

  3. In the Interface Principle, a useful way to interpret "supplied with" is "appears in the same header and/or namespace." If the function appears in the same header as the class, it is "part of " the class in terms of dependencies. If the function appears in the same namespace as the class, it is "part of " the class in terms of object use and name lookup.

 

 
<think>我们正在讨论的是Spring框架中的`AbstractHandlerMethodMapping`类,具体是关于其内部类`MappingRegistry`中的`nameLookup`字段。 根据之前的问题,我们了解到`MappingRegistry`中有多个字段,包括`registry`、`urlLookup`、`mappingLookup`、`corsLookup`等。现在用户询问的是`nameLookup`字段的作用。 在`MappingRegistry`中,`nameLookup`字段的定义通常如下: ```java private final Map<String, List<HandlerMethod>> nameLookup = new HashMap<>(); ``` 它的作用如下: 1. **映射处理器方法名**:`nameLookup`是一个映射,键(key)是处理器方法的名字(通常是方法名或用户指定的名称),值(value)是该名称对应的`HandlerMethod`列表。注意,同一个名称可能对应多个处理器方法(例如重载方法或不同控制器中的同名方法)。 2. **支持按名称查找处理器方法**:在Spring MVC中,有时我们需要通过处理器方法的名称来获取对应的`HandlerMethod`。例如,在生成链接(URL)时,我们可能会使用处理器方法的名称(通过`@RequestMapping`的`name`属性指定)来引用它。`nameLookup`提供了这种按名称查找的能力。 3. **与`@RequestMapping`的`name`属性关联**:当我们在控制器方法上使用`@RequestMapping`时,可以指定一个`name`属性,例如: ```java @RequestMapping(name = "getUser", value = "/user/{id}", method = RequestMethod.GET) public User getUser(@PathVariable Long id) { ... } ``` 在注册这个处理器方法时,Spring会将这个名称("getUser")和对应的`HandlerMethod`添加到`nameLookup`映射中。 4. **处理多个同名方法**:由于同一个名称可能对应多个处理器方法(例如,在不同的控制器中定义了相同名称的处理器方法),因此`nameLookup`的值是一个列表(`List<HandlerMethod>`)。在通过名称查找时,需要根据其他条件(如参数类型等)进一步确定具体的处理器方法。 5. **在URL生成中的作用**:在构建URL时,Spring的`MvcUriComponentsBuilder`等工具类会使用`nameLookup`来根据处理器方法名称查找对应的映射信息,从而构建出正确的URL。 6. **注册过程**:在`MappingRegistry`的`register`方法中,当注册一个处理器方法时,如果该处理器方法有指定名称(通过`RequestMappingInfo`的`getName()`方法获取),则会将其添加到`nameLookup`中: ```java String name = mapping.getName(); if (name != null) { List<HandlerMethod> handlerMethods = this.nameLookup.get(name); if (handlerMethods == null) { handlerMethods = new ArrayList<>(); this.nameLookup.put(name, handlerMethods); } handlerMethods.add(handlerMethod); } ``` 7. **查找过程**:当需要根据名称查找处理器方法时,可以通过`MappingRegistry`的`getHandlerMethodsByMappingName`方法(实际是访问`nameLookup`)来获取。 8. **线程安全**:由于`MappingRegistry`使用读写锁(`ReentrantReadWriteLock`)来保证线程安全,对`nameLookup`的读写操作也是在锁的保护下进行的。 总结:`nameLookup`的主要作用是为处理器方法提供一个按名称索引的结构,以便能够通过名称快速查找到对应的处理器方法(可能有多个)。这在URL生成等场景中非常有用。 相关问题: 1. 在Spring MVC中,如何为处理器方法指定名称? 2. 当多个处理器方法具有相同名称时,如何精确指定使用哪一个来生成URL? 3. `nameLookup`与`mappingLookup`、`urlLookup`等字段有何不同? 4. 在动态注册处理器方法时,如何更新`nameLookup`? 5. 如果处理器方法没有指定名称,`nameLookup`中会有记录吗?</think>### AbstractHandlerMethodMapping 中 `nameLookup` 字段的作用 在 `AbstractHandlerMethodMapping` 的内部类 `MappingRegistry` 中,`nameLookup` 字段定义为: ```java private final Map<String, List<HandlerMethod>> nameLookup = new HashMap<>(); ``` 它的核心作用是 **为处理器方法提供名称索引**,具体功能如下: --- #### 1. **处理器方法的名称映射** - **按名称索引** `nameLookup` 建立**处理器方法名称 → HandlerMethod 对象**的映射关系,其中: - **键 (Key)**:处理器方法的逻辑名称(通常通过 `@RequestMapping(name = "...")` 显式指定) - **值 (Value)**:与该名称关联的所有 `HandlerMethod` 对象列表(支持同名方法) - **示例场景** 当控制器方法声明为: ```java @RequestMapping(name = "userList", value = "/users", method = GET) public List<User> getUsers() { ... } ``` Spring 会将名称 `"userList"` 和对应的 `HandlerMethod` 存入 `nameLookup`[^1]。 --- #### 2. **支持 URL 反向生成** - **视图链接构建** 在视图层(如 JSP/Thymeleaf)生成控制器链接时,通过名称查找处理器方法: ```html <!-- Thymeleaf 示例 --> <a th:href="@{${#mvc.url(&#39;userList&#39;).build()}}">View Users</a> ``` `nameLookup` 提供名称到处理方法的快速查找,实现 `#mvc.url()` 等表达式功能[^2]。 - **重定向支持** 在控制器中生成重定向 URL: ```java return "redirect:" + MvcUriComponentsBuilder.fromMappingName("userList").build(); ``` --- #### 3. **处理同名方法冲突** - **列表式存储** 当多个处理器方法使用相同名称时(如方法重载),`nameLookup` 存储为 `List<HandlerMethod>` 而非覆盖: ```java nameLookup.computeIfAbsent(name, key -> new ArrayList<>()).add(handlerMethod); ``` - **运行时解析** 在生成链接时,Spring 会结合参数类型等上下文信息解析具体方法[^3]。 --- #### 4. **与其他字段的协作关系** | 字段 | 类型 | 协作关系 | |------|------|----------| | `registry` | `Map<T, MappingRegistration<T>>` | 提供完整注册信息,`nameLookup` 是其名称索引子集 | | `urlLookup` | `MultiValueMap<String, T>` | 名称→路径的间接关联(通过 `nameLookup` 找到方法后再查路径) | | `mappingLookup` | `Map<T, HandlerMethod>` | 名称→方法的直接映射(`nameLookup` 是其名称维度视图) | --- ### 工作流程示例 **注册阶段**: 1. 扫描到带名称的处理器方法: ```java @RequestMapping(name = "search", path = "/search") public Result search(String keyword) { ... } ``` 2. 在 `MappingRegistry.register()` 中: ```java // 伪代码 String name = mappingInfo.getName(); // "search" nameLookup.computeIfAbsent(name, k -> new ArrayList<>()) .add(handlerMethod); ``` **调用阶段**(生成链接): ```java String url = MvcUriComponentsBuilder .fromMappingName("search") // 查询 nameLookup .arg(0, "spring") // 匹配参数类型 .build(); // 生成: /search?keyword=spring ``` --- ### 设计意义 1. **解耦视图与路径** 视图层通过逻辑名称而非硬编码路径引用控制器,提高可维护性[^2]。 2. **运行时灵活性** 支持动态更新处理器方法名称映射(需配合读写锁 `ReentrantReadWriteLock`[^3])。 3. **性能优化** 使用 `HashMap` 实现 $O(1)$ 复杂度的名称查找,避免全注册表扫描。 4. **OpenAPI 集成** 为 SpringDoc 等工具提供方法名称→定义的索引,辅助生成文档。 --- ### 相关问题 1. 如何在控制器方法中显式指定处理器方法名称? 2. 当两个控制器方法使用相同名称时,`nameLookup` 如何存储它们? 3. 在视图层使用 `#mvc.url(&#39;name&#39;)` 时,Spring 内部如何利用 `nameLookup`? 4. `nameLookup` 与 Spring HATEOAS 的链接生成有何关联? 5. 如果处理器方法未指定名称,`nameLookup` 中会有记录吗? [^1]: 处理器方法名称通过 `RequestMappingInfo.getName()` 获取,并注册到 `nameLookup` 中。 [^2]: `MvcUriComponentsBuilder` 通过 `nameLookup` 实现名称到处理器的映射,用于构建 URL。 [^3]: `MappingRegistry` 使用读写锁保护 `nameLookup` 的并发访问,确保线程安全。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值