Bloch Warns Flawed APIs Trap Enterprise Codebases for Decades
Google TechTalks
Because public software interfaces cannot be altered once adopted without breaking client systems, designers must aggressively minimize feature scope and enforce strict contracts from the outset.
Public APIs represent permanent engineering contracts that cannot be broken once adopted, turning design errors into permanent organizational and performance liabilities. (1:39)
Initial API specifications should be constrained to a single page to encourage rapid stakeholder feedback, short iteration cycles, and early client-side prototyping. (7:42)
APIs should follow the rule 'When in doubt, leave it out' to minimize conceptual weight and preserve long-term implementation freedom. (24:19)
Data exposed by an API in string form must always provide programmatic, structured access to prevent callers from writing brittle parsers. (51:49)
Detailed notes
## Background
Published by the Google TechTalks channel on YouTube on October 9, 2007, this presentation features Joshua Bloch speaking on January 24, 2007, as part of Google's internal Advanced Topics in Programming Languages series. According to the page description, nearly all software developers function as API designers, and well-crafted APIs serve as vital organizational assets that elevate programmer productivity, code quality, and the corporate bottom line.
## TL;DR
Joshua Bloch outlines the core principles of designing robust, maintainable application programming interfaces, framing API architecture as an enduring discipline identical to language design. He emphasizes starting with concise one-page specifications, testing client code before writing implementations, validating service provider interfaces against multiple providers, and strictly adhering to the maxim 'when in doubt, leave it out.' Drawing directly on historical lessons and missteps across the Java platform—such as performance pitfalls in AWT, broken inheritance in Stack and Properties, and side effects in Thread.interrupted—Bloch highlights the importance of immutability, symmetric naming, clear documentation, and failing fast.
## Atmosphere & style
The presentation carries the pragmatic, authoritative tone of a seasoned software architect delivering candid, battle-tested wisdom to fellow engineers. Bloch balances technical rigor with dry wit and self-deprecating humor, freely critiquing both industry anti-patterns and his own past platform mistakes. The session feels like an engaging, unpretentious masterclass, transforming complex software engineering tradeoffs into intuitive, memorable design principles.
## Bulletin
- [01:39](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=99s) Public APIs represent permanent engineering contracts that cannot be broken once adopted, turning design errors into permanent organizational and performance liabilities.
- [07:42](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=462s) Initial API specifications should be constrained to a single page to encourage rapid stakeholder feedback, short iteration cycles, and early client-side prototyping.
- [24:19](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=1459s) APIs should follow the rule 'When in doubt, leave it out' to minimize conceptual weight and preserve long-term implementation freedom.
- [51:49](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=3109s) Data exposed by an API in string form must always provide programmatic, structured access to prevent callers from writing brittle parsers.
## Key takeaways
- [03:26](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=206s) Every programmer is an API designer because modular programming inherently establishes interface boundaries between components.
- [05:47](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=347s) A good API is easy to learn, difficult to misuse, and precisely powerful enough to fulfill its intended requirements without excess complexity.
- [07:42](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=462s) Requirements should be gathered through real operational use cases rather than premature solutions or monolithic design documents.
- [15:03](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=903s) Prototyping client-side code before writing the underlying implementation ensures the API is natural to use and yields reliable sample code.
- [17:24](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=1044s) Service Provider Interfaces (SPIs) must be validated against multiple implementations—ideally three—to prevent baking provider-specific assumptions into the interface.
- [21:45](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=1305s) API naming communicates architectural integrity; difficulty finding a clean, intuitive name often indicates an overloaded or misplaced abstraction.
- [26:19](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=1579s) Implementation details such as low-level exception types or internal data layouts must never leak into public contracts.
- [32:22](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=1942s) Comprehensive documentation defining contracts, preconditions, and side effects is strictly necessary to prevent clients from relying on incidental implementation details.
- [35:31](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=2131s) APIs impose hard performance limits that cannot be solved by runtime optimizations, as illustrated by the allocation overhead of java.awt.Component.getSize().
- [39:41](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=2381s) Classes should be immutable by default to ensure thread safety and shareability, and mutable state spaces should be strictly bounded.
- [42:01](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=2521s) Inheritance breaks encapsulation and should only be used where a true subtype relationship exists, or else classes should be explicitly designed for inheritance or prohibited from it.
- [48:31](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=2911s) APIs must adhere to the Principle of Least Astonishment and fail fast as close to the source of an error as possible.
- [53:12](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=3192s) Method overloading should be used sparingly, especially when overloaded signatures accept similar types but trigger different semantic behaviors.
- [54:11](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=3251s) Methods should favor interface types, maintain consistent parameter orderings, and strictly avoid floating-point primitives for exact domain calculations.
- [58:08](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=3488s) Methods returning collections or arrays should return zero-length collections or arrays instead of null to eliminate boilerplate null checks.
## Detailed walkthrough
### [00:23](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=23s) Introduction and Jeremy Manson’s Over-the-Top Welcome
Jeremy Manson introduces the session as the latest installment in Google’s internal series on advanced topics in programming languages. The explicit goal of the series is to surface and circulate the deep domain expertise scattered across the company. Addressing the room with self-deprecating humor, Manson remarks that while he discounts himself, everyone else in the audience is a genius working at Google and ought to step forward to present. He invites attendees to email or message him with proposed topics, emphasizing that one does not need to be an established book author to participate.
Turning to the speaker, Manson notes that the event is already running a bit behind schedule. He explains that Joshua Bloch explicitly instructed him not to deliver an exaggerated buildup that might give him an inflated ego. Ignoring that instruction completely, Manson launches into hyperbolic praise, declaring that in a company teeming with intellect, Bloch's star shines the brightest, and jokes that he is not even worthy to lick the heel of Bloch’s boot. From off-mic, Bloch fires back dryly, “I'll get you for that.” Taking the podium to laughter and applause, Joshua Bloch responds in kind to Manson’s theatrical praise: “Normally at this point, I thank my introducer for the introduction but today I think I'll dispense with that formality.”
- The talk was hosted at Google as part of the internal Advanced Topics in Programming Languages lecture series.
- Manson encouraged peer contributions regardless of publishing prestige or seniority.
[01:39](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=99s) Why API Design Matters: Massive Assets and Permanent Liabilities
Bloch warns the audience that the lecture will be unusually packed and long, noting that while he typically prefers concise presentations organized around one or two core ideas, API design simply does not lend itself to brevity. As a result, he asks the audience to hold all questions until the very end, offering to stay as long as necessary afterward to answer them. He then poses the foundational question of the presentation: why does API design matter? APIs, he argues, can easily become a company's single greatest asset. Customers and developers invest in them heavily, both through obvious mechanisms—such as building commercial software products on top of them—and through less obvious investments, primarily the immense personal effort required to learn an interface. Developers spend months mastering an API, and once that knowledge is ingrained, they resist switching because doing so requires unlearning muscle memory and replacing entire conceptual models.
When an API becomes deeply wired into an organization's foundational infrastructure, a successful design can establish an enterprise franchise that endures for a quarter of a century. Conversely, a flawed API becomes a devastating corporate liability. Poor designs generate an endless stream of expensive, frustrating support calls from engineers who cannot force the system to perform expected behaviors. Worse, bad interfaces cripple a company's ability to innovate because public interfaces cannot simply be modified or rewritten without breaking downstream clients. Emphasizing the unforgiving finality of releasing public code, Joshua Bloch warns: “You're pretty much stuck with it forever. You have one chance to get it right.”
[03:26](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=206s) Modular Code, Accidental APIs, and Language Design
Bloch addresses the common misconception among engineers that API design is a niche discipline reserved only for library authors or framework teams. In reality, every single computer programmer functions as an API designer. Because high-quality software engineering is inherently modular, the boundaries separating internal modules are themselves APIs. When an engineer builds a clean, effective module to solve an internal problem, a teammate will inevitably encounter that exact problem and adopt the code. Once another developer relies on that interface, the author loses the freedom to arbitrarily rewrite or mutate it without breaking other systems. As Bloch quips, if that colleague shares the code with ten more friends who all start depending on it, the original author is “really hosed.” Furthermore, adopting an API designer’s mindset forces developers to write better, more decoupled programs, preventing them from simply hacking disjointed scripts together and encouraging the creation of composable, reusable units.
Addressing why an API design lecture belongs in a series titled "Advanced Topics in Programming Languages," Bloch jokes that the superficial reason is simply that his former friend Jeremy Manson asked him to do it. The substantive reason, however, is that API design and programming language design are fundamentally identical disciplines. The primary distinction is syntactic: language designers possess the liberty to invent arbitrary syntax, whereas API designers operate strictly within the syntactic constraints of the host language. In both cases, the designer's true role is creating an expressive vehicle that allows humans to communicate intent both to the runtime machine and to other human engineers who must read, debug, and maintain the system over time. Modern languages cannot be divorced from their core libraries; together, language and standard library constitute an inseparable platform, making the underlying design skill sets identical. Highlighting this shared purpose, Joshua Bloch observes: “Whether you're designing a language or an API, you are creating a tool for programmers to express their intent to the machine and to other programmers, who read the program, maintain it, modify it and so forth.”
```mermaid
flowchart TD
accTitle: Language Design versus API Design Scope
accDescr: Diagram contrasting language design syntactic freedom against API design constraints while highlighting their shared core purpose.
subgraph APIDesign["API Design"]
C["Host Language Syntax Constraints"]
D["Class, Method & Type Boundaries"]
end
subgraph SharedCore["Shared Core Objective"]
E["Express Programmer Intent to the Machine"]
F["Communicate Meaning to Human Maintainers"]
G["Deliver Unified Platform (Language + Core Libraries)"]
end
A --> SharedCore
B --> SharedCore
C --> SharedCore
D --> SharedCore
```
[05:47](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=347s) Characteristics of a Good API and the Talk Outline
Bloch outlines the essential traits that distinguish good APIs from bad ones. A well-crafted API must be remarkably easy to learn, understand, and use—ideally intuitive enough that developers can utilize it correctly without constantly consulting documentation. It must make immediate intuitive sense and be easy to commit to memory. Critically, usability is only half the equation; an interface must also be actively difficult or impossible to misuse. A superior interface acts as a set of guardrails that steers the programmer toward correct implementations while preventing illegal states. Code written against the API should read clearly, remain simple to maintain, and possess sufficient capability to resolve its target domain problems. Bloch cautions against equating design quality with raw power: an API should not aim to be maximally complex or excessively feature-rich, but merely powerful enough to accomplish its objectives while retaining the structural flexibility to evolve smoothly as future requirements emerge.
Finally, an API must speak the native vocabulary of its intended audience. An interface designed for Wall Street financial analysts requires different abstractions, naming conventions, and domain assumptions than one built for quantum physicists. Bloch introduces the structural roadmap for the remainder of the talk, which covers five distinct areas: the process of API design, general design principles, and how those foundational principles specifically apply to classes, methods, and exception handling, concluding with real-world refactoring case studies if time allows. Emphasizing restraint in system capability, Joshua Bloch explains: “It is not the case that the more powerful an API is, the better it is. It should basically be just powerful enough to do what it needs to do.”
- Core criteria of an effective API:
- Easy to learn, memorize, and use even without documentation.
- Hard or impossible to misuse, steering users to correct behavior.
- Clean to read, review, and maintain over long lifecycles.
- Sufficiently powerful without over-engineering unnecessary complexity.
- Adaptable to changing future requirements.
- Tailored precisely to the mental models and terminology of its target audience.
[07:42](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=462s) Gathering Requirements with Skepticism and the One-Page Spec
Bloch turns to the design process itself, clarifying that while he is not a dogmatic "process weenie," successful API designs share specific procedural habits. The first critical stage is requirements gathering, which must be approached with aggressive skepticism. When designers ask prospective users what an API needs to accomplish, stakeholders rarely articulate root requirements; instead, they prescribe premature, overly complex implementation solutions that obscure simpler alternatives. For instance, when a team demands fine-grained knobs to dictate precise garbage collection intervals and pause times, their real requirement is not GC scheduling—it is simply that their server must process requests smoothly without disruptive pauses. A designer’s responsibility is to interrogate stakeholder feedback, strip away proposed mechanisms, and extract concrete use cases representing the actual operational problems the software must solve. These use cases form the benchmark against which every future proposal must be judged.
```mermaid
flowchart LR
accTitle: Requirements Gathering to Agile Specification Workflow
accDescr: Progression from raw stakeholder requests through skeptical filtering and use case extraction to an agile one-page specification.
subgraph Intake["Stakeholder Input"]
R1["Proposed Solutions<br/>(e.g., GC knobs, rigid RPCs)"]
end
subgraph Analysis["Analytical Filtering"]
F1["Healthy Skepticism & Interrogation"]
U1["Concrete Use Cases & True Needs<br/>(Smooth server operation, flexible data format)"]
end
R1 --> F1
F1 --> U1
U1 --> S1
U1 -.->|"Anti-pattern trap"| S2
```
Bloch shares an example from his earlier tenure at Transarc, where engineers requested an entirely new set of data structures and RPC interfaces stamped with "Version 2" attributes. Recognizing that creating bespoke wire protocols and disk formats for every minor attribute expansion would create an unmaintainable maintenance burden, Bloch uncovered the genuine underlying requirement: the system needed a flexible, dynamic data format that could seamlessly accommodate future evolution. Delivering that dynamic solution eliminated the need to revisit wire interfaces again.
Bloch warns designers against jumping into over-generalized frameworks, while simultaneously noting that generalized solutions can occasionally prove easier and more elegant to implement than overly narrow, brittle requests. To avoid catastrophic design traps, Bloch insists that designers must begin with a compact specification—ideally fitting on a single page. In early development, agility completely outweighs exhaustiveness. Joshua Bloch warns against traditional bureaucratic planning: “The worst thing you can do is to send six smart guys off into a room and have them sit there with the door closed for six months and come out with a 240 specification document.” Such documents lead teams to defend bad ideas because their egos are heavily invested, while the sheer bulk makes the spec impossible to pivot once fundamental design flaws are discovered. A one-page spec can instead be circulated, debated, and refined rapidly among all primary stakeholders.
[10:55](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=655s) Listening to Feedback and the 1-Page Spec: The Retry Policy Example
When presenting an early design to prospective users, the API designer must listen attentively and take objections seriously. If a developer objects that the proposed interface makes it impossible to write a specific program, the designer might initially suspect the user wants to write bad code, but more frequently, it uncovers an unanticipated requirement: “Oh gee, I didn’t think of that. This really is important. What if we structure it this way?” Keeping the initial specification extremely brief—roughly a single page—allows major architectural refactorings to happen in ten minutes without emotional or temporal friction. The designer should only flesh out the specification into full documentation as confidence builds, a validation process that demands writing code that calls the proposed interface, pretending it already exists, long before anyone attempts to implement it.
To illustrate this, the speaker presents a sketch prepared months earlier for an OOPSLA presentation. A user had requested the ability to retry failed operations, which naturally fits alongside the existing Java Executor framework. Rather than building a sprawling subsystem, the speaker reduced the core abstraction to a minimal `RetryPolicy` interface containing just two methods. The first method evaluates whether a failure is recoverable by taking an exception and returning a boolean. The second computes the next delay interval based on the initial start time and the total count of previous attempts. Passing all contextual state directly into the method arguments allows the implementation to remain completely stateless. Consequently, policies like exponential backoff can exist as lightweight, immutable singletons without storing per-operation state. While insufficient for Javadoc or implementation work, this rough sketch is precisely detailed enough for users to confirm whether it meets their operational needs.
- The interface passes the exception into an initial method returning `boolean` to decide if recovery should be attempted.
- Delay computation is calculated from initial start time and prior retry count, keeping policy instances stateless singletons.
- As Joshua Bloch explains: “If you keep the spec short, it’s easy to modify.”
Moving to the utility layer, the design uses static utility methods to integrate retry policies seamlessly with standard platform idioms. A primary factory accepts an `ExecutorService` alongside a `RetryPolicy` and returns a retrying `ExecutorService`. Reusing standard abstractions drastically reduces the learning curve of an API: anyone already familiar with an `ExecutorService` immediately understands how to execute tasks with retries. The speaker also provides wrapper utilities that convert a standard `Callable` or `Runnable` into their retrying equivalents, alongside static factory methods configured for common use cases like exponential backoff.
Examining the factory parameters on his own slide—such as initial delay, time unit (e.g., 10 seconds or 100 milliseconds), and an ambiguous `timeout` parameter—the speaker pauses candidly. He admits he had not reviewed the slide recently and realizes that the meaning of `timeout` is unclear. It most likely represents the overall duration before abandoning retries entirely, but the slide failed to state that clearly. This minor defect illustrates the exact value of early, concise specifications: even a flawed or slightly ambiguous draft fitting on one or two slides communicates enough architectural intent to surface design oversights and clarify parameters long before any implementation work begins.
```mermaid
flowchart TD
accTitle: Retrying Executor Wrapper Architecture
accDescr: Diagram showing how a standard ExecutorService and RetryPolicy combine into a Retrying ExecutorService.
subgraph Inputs["Standard Abstractions & Policies"]
ES["ExecutorService"]
RP["RetryPolicy\n(e.g., Exponential Backoff)"]
end
subgraph Utility["Static Utilities"]
Factory["RetryingExecutors.newRetryingExecutorService(ES, RP)"]
end
subgraph Output["Decorated Interface"]
RES["Retrying ExecutorService\n(Implements ExecutorService)"]
end
ES --> Factory
RP --> Factory
Factory --> RES
```
- Static factories wrap `Callable` and `Runnable` into retry-aware counterparts.
- Reusing standard platform interfaces prevents API bloat by keeping "conceptual weight" to a minimum.
- As Joshua Bloch emphasizes: “Use interfaces that have already been designed—defined, in this case, executor service.”
[15:03](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=903s) Writing to the API Early and Often: Why Example Programs Must Be Exemplary
Writing client code against a provisional API should start well before implementation begins, preventing developers from throwing away wasted implementation code when an API inevitably turns out to be flawed. In fact, coding should start before the formal specification is even finished to avoid laboring over exhaustive specifications for broken designs. Designers must avoid abandoning this exercise midway through development. Teams that halt client coding to plunge into a "death march to implementation" routinely face nasty surprises days before shipping, suddenly discovering that the interface cannot accommodate a vital, real-world scenario.
Far from being wasted effort, early client code serves a foundational long-term purpose: it invariably morphs into the public example code distributed in documentation, tutorials, and samples. Because programmers copy and paste example code across the software ecosystem, flawed examples seed systemic bugs across the web for decades. The speaker references a paper presented at OOPSLA titled *Design Fragments* by George Fairbanks, David Garlan, and William Scherlis from Carnegie Mellon University (CMU). The authors tracked race conditions and threading mistakes in the initial applet code shipped with the earliest releases of Java directly into thousands of broken, concurrent programs still circulating across the web more than a decade later. For this reason, example code demands rigorous craftsmanship.
- The CMU study traced persistent, modern web errors directly back to flawed concurrent applets shipped in Java 1.0.
- Example code should be treated with far higher scrutiny than routine internal application logic.
- As Joshua Bloch insists: “So, you know, my rule of thumb is you should spend ten times as much time on every line of example code as you do production code.”
[17:24](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=1044s) Service Provider Interfaces (SPIs) and the Rule of Threes
Service Provider Interfaces (SPIs) require an even higher standard of validation through early consumer implementation. Unlike general-use client APIs, an SPI is an internal plug-in interface designed to allow third parties—such as RSA Data Security, Inc. (RSADSI) or Sun Microsystems—to supply swappable back-end implementations (e.g., cryptographic algorithms) behind a uniform, high-level client façade. The overarching purpose of an SPI is to cleanly hide radically different internal mechanics behind an identical contract.
Designing an SPI against only a single provider is virtually guaranteed to fail. If only one provider is implemented, the interface inevitably leaks idiosyncrasies of that specific implementation, permanently cementing assumptions that make future alternate providers impossible. Implementing two providers improves matters, though accommodating subsequent ones will still involve painful friction. Validating against three distinct providers almost completely ensures long-term extensibility, as a fourth implementation is unlikely to present edge cases not already covered by the first three. This observation matches the "Rule of Threes" formulated by Will Tracz in his book *Confessions of a Used Program Salesman: Institutionalizing Software Reuse*.
- The color coding throughout the lecture uses green to denote best practices and red to flag anti-patterns.
- An SPI allows diverse plug-ins to dispatch under a consistent high-level user API.
- As Joshua Bloch explains: “Once you try to do the second, you’ll find that there’s something about that SPI that ties it forever to the only implementation you actually thought about.”
[19:14](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=1154s) Managing Expectations: Aiming to Displease Everyone Equally
Because most API design projects are inherently overconstrained by conflicting customer desires, designers must accept that compromises are inevitable. Attempting to grant every user request produces an bloated monstrosity—a giant, unusable "pig." Instead of striving for universal satisfaction, the designer's goal should be counterintuitive: aim to displease everyone equally. If one stakeholder is thoroughly overjoyed while another is completely alienated, the API has likely failed an essential requirement. If every constituency feels somewhat dissatisfied but can still achieve their core technical requirements, the designer has struck the right balance.
Balancing trade-offs must never devolve into design by committee, which simply mushes competing philosophies into an incoherent compromise. Every successful API requires a strong design lead who ensures that the final result remains cohesive, elegant, and clearly reflective of a single guiding mind or unified team. Ultimately, API design is extraordinarily difficult, and subtle flaws will only become obvious after years of widespread production use. While finding mistakes years later is too late to fix the deployed API, it provides the exact hard-earned material needed to educate the engineering community on how to avoid repeating them.
- Overconstrained design requirements make trade-offs and compromises mandatory.
- APIs designed by loose committees degenerate into disjointed, bloated collections of conflicting features.
- As Joshua Bloch warns: “If you try to please everyone, you come up with a pig.”
[20:46](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=1246s) Extensibility and the Lessons of Java Collections
Given that designers inevitably make mistakes and will remain stuck with an initial API, Josh Bloch emphasizes the importance of designing interfaces so they can be grown over time to circumvent early design flaws. Reflecting on his own experience designing the Java Collections Framework around 1997–1998, he notes that there were real, known shortcomings in the original `SortedSet` and `SortedMap` interfaces. Specifically, they were asymmetric—traversing elements forward was considerably easier than navigating backward—along with several other subtle design defects. Bloch was aware of these flaws at the time of authoring, but did not yet know how to solve them cleanly.
Because the core architecture allowed for extension, the design team was later able to resolve these issues in Java 6 by introducing `NavigableSet` and `NavigableMap`. These new interfaces extended `SortedSet` and `SortedMap`, adding the necessary navigational methods to remediate the earlier asymmetric limitations. While this approach did not instantly fix third-party legacy classes implementing the original interfaces, it allowed all standard implementations bundled inside the Sun platform to be updated and cured cleanly without breaking backward compatibility.
[21:45](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=1305s) Do One Thing Well and Listen to Your Names
Bloch turns to the foundational principles of API design, starting with the classic maxim that an API should do one thing and do it well. Acknowledging that this principle often sounds like generic "motherhood and apple pie," he emphasizes that it offers concrete, actionable guidance: an API's functionality must be straightforward to explain. If a class or method cannot be explained simply, it is doing too much and is a design mess. A clear indicator of this failure is struggling to assign an element a clear, intuitive name.
```mermaid
flowchart TD
accTitle: API Naming as a Feedback Loop
accDescr: Diagram illustrating how naming difficulties signal underlying architectural design problems.
subgraph Feedback["Naming Feedback Loop"]
A["Attempt to name class or method"] --> B{"Is the name clear & simple?"}
B -->|"Yes"| C["Sound abstraction:\nclear purpose & clean boundaries"]
B -->|"No"| D["Architectural warning:\nlisten to the API talking back"]
end
subgraph Resolution["Remediation Options"]
D --> E["Split module:\ntoo much crammed into one place"]
D --> F["Merge modules:\nexposing internal details that should be hidden"]
end
```
Bloch views names as a diagnostic tool, warning developers: “The names are the API talking back to you, so listen to them.” When an identifier turns into a monstrosity, the design itself is flawed. To prove he is not inventing hypotheticals, he cites egregious examples taken straight from actual Java platform packages (such as Java CORBA): `DynAnyFactoryOperations`, `_BindingIteratorImplBase` (which even violates platform naming conventions by starting with an underscore), `ENCODING_CDR_ENCAPS`, and `OMGVMCID`. He jokes about the last one: “I know OMG is, 'Oh, my god,' but I can't—I can't figure out the rest of it.” By contrast, good names—such as `Font`, `Set`, `PrivateKey`, `Lock`, or `ThreadFactory`—instantly communicate their responsibilities. For example, once an abstraction is named `Set`, its core operations (inserting, removing, containment testing) follow intuitively. When names do not work out, developers should remain open to splitting overloaded modules or consolidating multiple leaky modules to properly hide internal details.
- Real Java platform counterexamples cited: `DynAnyFactoryOperations`, `_BindingIteratorImplBase`, `ENCODING_CDR_ENCAPS`, and `OMGVMCID`.
- Exemplary self-describing names cited: `Font`, `Set`, `PrivateKey`, `Lock`, and `ThreadFactory`.
[24:19](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=1459s) "When in Doubt, Leave It Out"
Turning to module sizing, Bloch cites the familiar aphorism that an API should be as small as possible, but no smaller. While this rule is conventionally attributed to Albert Einstein, Bloch notes that after looking into it thoroughly, he found no evidence Einstein ever said it, even if he may have agreed with the sentiment. The cardinal takeaway for all API design boils down to an essential rule:
> “When in doubt, leave it out.”
This rule applies universally across every layer of design: functionality, classes, methods, and individual parameters. Josh Bloch stresses that if designers take only one piece of advice away from his lecture, this should be it: “You'll probably be able to add it later, but you most certainly will not be able to take it out once you put it into an API. Once you put it in people will be using it, if you take it out they will scream bloody murder.”
Furthermore, API size should be evaluated by conceptual weight rather than superficial bulk. Bulk measures raw numbers—how many methods, classes, or parameters exist—whereas conceptual weight measures how many distinct mental concepts a programmer must master to use the system. A powerful technique for maximizing the power-to-weight ratio is reusing existing interfaces. In the Java Collections Framework, developers learn the single `Set` interface once; when Sun introduced implementations like `HashSet` and `TreeSet`, followed later by `LinkedHashSet` and various concurrent sets, programmers could exploit rich functionality without having to learn a completely new API surface.
[26:19](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=1579s) Preventing Implementation Details from Leaking
Bloch warns that implementation details must never affect the public API because they confuse users and permanently rob both users and maintainers of implementation freedom. If an API about phone numbers throws a `SQLException`, it becomes nearly impossible to later migrate the underlying storage to an in-house proprietary datastore without breaking clients who are catching `SQLException`. In such cases, developers are forced into the absurd task of emulating SQL exceptions on top of non-relational storage. Exceptions must always exist at the same level of abstraction as the enclosing API rather than mindlessly bubbling up from underlying dependencies.
Another major historical failure mode in Java is over-specifying method contracts, such as hash functions. Unless one is building a persistent, on-disk data store where hashes must remain immutable, a `hashCode()` contract should only guarantee that it returns an integer, has a low probability of collision across distinct objects, and is cheap to compute. Specifying exact mathematical return values permanently freezes the implementation, preventing improvements when better algorithms are discovered. Bloch laments that early Java releases specified explicit formulas for hash functions across the platform, establishing a rigid tradition that became standard.
Bloch also singles out Java object serialization as an egregious vector for implementation leaks. Declaring `implements Serializable` causes an object's internal implementation—including all private fields—to instantly become part of its permanent public API via its serial form. Unless the serialized form is intentionally designed and decoupled from internal representations, any future refactoring of internal private variables risks breaking external data streams.
[29:26](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=1766s) Information Hiding and the Weight of Names
To prevent coupling and preserve flexibility, designers should strictly minimize the accessibility of everything by making classes, members, and fields as private as possible. Public classes should never expose public fields, with the sole exception of immutable constants. Citing David Parnas, the pioneer who coined the term "information hiding," Bloch points out that strong modular boundaries allow different components to be developed, understood, optimized, tested, and debugged concurrently by isolated teams. Conversely, if components expose their internals and intermingle, changing any single module triggers cascading breakage across neighboring systems.
Bloch strongly rejects the idea that naming discussions are bikeshedding or a waste of team time. When team members argue that an imperfect name is "good enough" and urge the group to just move on, they fail to grasp that an API functions as a miniature domain-specific language. Clear, precise names are essential because these vocabularies endure for years and must communicate their intent clearly to external users and to the original authors months down the road.
[30:54](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=1854s) Naming Consistency, Symmetry, and Code That Reads Like Prose
An API defines a little language that programmers must learn and speak, meaning names should be largely self-explanatory and free of cryptic abbreviations—an area where the original Unix command and library names failed miserably. Consistency is paramount: the same word should always mean the same thing across an API, and multiple synonyms should never be introduced to describe the identical operation. Having both `remove` and `delete` in the same API is almost always a mistake because users have no intuition for how they differ. If they do mean the same thing, pick one; if they truly differ, the names must clearly convey the distinction—for example, using `delete` alongside `expunge` implies that the latter is a more drastic, permanent form of removal.
Beyond naming consistency, designers should strive for structural symmetry. If an API exposes two verbs (`add`, `remove`) and two nouns (`entry`, `key`), developers naturally expect all four combinations to exist: `addEntry`, `addKey`, `removeEntry`, and `removeKey`. If one of those combinations is omitted, there must be a compelling reason for its absence. When naming, consistency, and symmetry align, the calling code reads cleanly and naturally. As Joshua Bloch summarizes:
> “And if you get it right, the code will read like prose.”
- Cryptic abbreviations make APIs hard to learn; names should describe their intent directly.
- Avoid synonymous operations with different names; if verbs differ (such as `delete` versus `expunge`), ensure the names explicitly signal the semantic difference.
- High symmetry makes APIs predictable: pairing verbs and nouns across types allows users to infer method names without constantly consulting reference manuals.
- Prose-like readability serves as the ultimate litmus test for API ergonomics, such as an alert condition reading: `car.getSpeed() > 2 * speedLimit` leading to `speaker.generateAlert("watch out for cops")`.
---
[32:22](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=1942s) Documenting Religiously: Specifications vs. Implementations
API documentation is not optional; without it, software reuse remains an unfulfilled promise. To emphasize the gravity of documentation, Joshua Bloch cites software engineering pioneer David Parnas:
> “Reuse is something that is far easier to say than to do. Doing it requires both good design and very good documentation. Even when we see good design, which is still infrequently, we won't see the components reused without good documentation.”
Reading Parnas inspires what Bloch describes as "getting religion" about documentation, compelling a designer to document every public class, interface, method, constructor, parameter, and exception. When APIs omit doc comments, users are forced into one of two bad paths: they either guess how the code works and write buggy programs, or they inspect the underlying implementation. Reading the source code is equally dangerous because the internal implementation inadvertently becomes the de facto specification. The API becomes over-specified, locking maintainers into implementation quirks and destroying the freedom to optimize or refactor later.
```mermaid
flowchart TD
accTitle: The Cost of Undocumented APIs
accDescr: Flowchart contrasting documented APIs against the failure modes of guessing or reading source code.
subgraph Problem["Undocumented API"]
NoDoc["No Specification Provided"]
end
subgraph FailureModes["Client Developer Options"]
Guess["Guess Behavior<br/>(Software breaks in production)"]
Inspect["Inspect Source Code<br/>(Implementation becomes specification)"]
end
subgraph Consequence["Result"]
OverSpec["API is Over-Specified<br/>(Maintainers lose freedom to refactor)"]
end
Proper documentation requires clear contracts for each component:
- **Classes:** Explain what an instance of the class conceptually represents.
- **Methods:** Define the exact contract with clients, including preconditions (what must hold before calling), postconditions (what is guaranteed after return), and any side effects. Bloch warns: “If you had side effect and you don't document them, people will shoot themselves in the foot.”
- **Parameters:** Specify units explicitly (e.g., block sizes in bytes versus megabytes), input formats (such as specific string or XML schemas), and object ownership transfer (whether the caller retains the right to modify an argument or surrenders ownership to the callee).
- **Mutable State Spaces:** Mutable objects require meticulous state space documentation so callers know which methods are valid in each lifecycle state. Bloch points to Java's `java.util.Calendar` and `Date` APIs as notorious examples where poorly documented, complex state spaces caused endless bugs, though Sun had recently initiated a JSR (eventually leading toward JSR 310 / `java.time`, influenced by Joda-Time) to replace the legacy system.
---
[35:31](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=2131s) Performance Consequences of API Design and the AWT `Dimension` Blunder
While developers are rightly warned that premature optimization is evil, ignoring performance entirely during API design is a grave error. Jon Bentley emphasized that architectural and API choices impose hard ceilings on performance. Choices such as making a type mutable instead of immutable, exposing constructors instead of static factory methods, or accepting concrete implementation classes instead of interface types permanently restrict optimization opportunities and force callers into inefficient patterns. Conversely, an API should never be warped to work around a temporary runtime inefficiency, because runtimes eventually get optimized, leaving the API warped forever:
> “The thing that used to get slow becomes fast but your API is still broken, you know. So, design your APIs for the long haul.”
A classic real-world example of an API decision that crippled performance occurred in the original Abstract Window Toolkit (AWT). Calling `Component.getSize()` returned a `java.awt.Dimension` object containing two public, mutable fields (`width` and `height`). Because `Dimension` was mutable, the component could not return its internal instance directly; doing so would allow one caller or thread to mutate the object and corrupt another component's size. Consequently, every single invocation of `getSize()` forced a defensive heap allocation of a new `Dimension` instance.
Although allocating a small two-field wrapper is dirt cheap in isolation, GUI layout managers and render loops call `getSize()` millions of times during user interactions. This generated megabytes of unnecessary garbage, putting severe pressure on the Java garbage collector. Java 1.2 eventually remedied the design by introducing individual primitive getters (`getWidth()` and `getHeight()`), since primitives are intrinsically immutable and need no defensive copying. However, legacy code invoking the 1.1 `getSize()` API remains permanently burdened by the original allocation overhead.
---
[38:06](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=2286s) Platform Conventions and the Perils of Transliteration
APIs must coexist peacefully with the platform on which they run by obeying idiomatic naming conventions, respecting established design idioms, and steering clear of platform-specific anti-patterns. Bloch jokes that developers should read the books documenting traps and pitfalls for their environment, playfully hinting at his own writings. Mimicking the design patterns of a platform's core libraries reduces the cognitive friction of adoption: because developers already understand standard platform APIs, a library that mirrors those patterns feels instantly familiar and requires virtually no ramp-up time.
The most dangerous pitfall when bridging systems is direct API transliteration:
> “Transliterated APIs are almost always broken.”
When porting a C++ library to Java, designers must resist the temptation to map each C++ class and method directly to a counterpart Java class and method. Design decisions, memory management conventions, and idiom sets that make complete sense in C++ are frequently nonsensical or unsafe in Java, and vice versa. Rather than transliterating code line by line, designers must step back, evaluate the underlying problem the class solves, and ask how that functionality should naturally be expressed in the target language.
---
[39:41](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=2381s) Class Design: Minimizing Mutability and Controlling State
Turning to class design with fifteen minutes remaining in the talk, Bloch delivers a foundational rule: classes should be immutable unless there is an overwhelming reason to allow mutability. Immutable classes offer massive advantages:
- They are simple to understand and reason about.
- They are inherently thread-safe without requiring explicit synchronization.
- Their instances can be freely shared, cached, and reused indefinitely without defensive copying.
The lone disadvantage of immutability is that every distinct value requires a separate object allocation. For massive data structures—such as a `BigInteger` containing one million digits—flipping a single bit requires allocating a new instance and copying an entire megabit of data, whereas a mutable structure could modify the bit in place at negligible cost.
When mutability cannot be avoided, designers must still constrain it as much as possible, keeping the mutable state space compact and precisely defined. Callers must always understand exactly what state transitions occur and when specific operations are legal. The legacy `java.util.Calendar` API exemplifies the chaos of unconstrained mutability: complex methods like `roll()` manipulate obscure internal state machinery, making it unclear what state the calendar ends up in or whether components like date formatters can be safely shared across concurrent threads.
[41:05](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=2465s) Minimizing Mutability and `TimerTask`
When an object exposes hidden mutable state, subtle concurrency bugs inevitably arise. A date formatter is a prime cautionary tale: if one thread is using it and another thread attempts to use it simultaneously, both threads are completely broken. The class gives callers the superficial impression of being immutable, yet it conceals internal state that cannot be read directly. In contrast, while some objects cannot be strictly immutable because they represent dynamic computations, an API designer should strive to minimize their mutability as much as possible.
```mermaid
flowchart LR
accTitle: TimerTask Lifecycle
accDescr: Linear lifecycle of a TimerTask showing creation, execution, and termination without reuse.
A["Create Task"] --> B["Schedule Task"]
B --> C["Run Execution(s)"]
C --> D["Task Dies"]
```
Joshua Bloch points to `java.util.TimerTask` as an exemplary design in this regard. Inherently, a `TimerTask` must mutate because it represents an active computation. Its entire lifecycle, however, is strictly linear: you instantiate it, schedule it, let it run as many times as specified, and then it terminates. During the design of the timer API, developers repeatedly requested the ability to reset and reuse tasks under the assumption that re-instantiation was overly expensive. The design team firmly rejected this request:
> “And the answer is, ‘No, let it die in peace.’”
Eliminating cyclical transitions from the state space radically simplifies an API and makes it far less bug-prone.
---
[42:01](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=2521s) Inheritance, the Liskov Substitution Principle, and the Fragile Base Class
API designers should employ subclassing only when genuine subtyping exists under the Liskov Substitution Principle. In practical terms, when deciding whether class `Foo` should extend class `Bar`, the designer must ask: "Is every `Foo` a `Bar`?" If that question cannot be answered affirmatively with a straight face, inheritance should be rejected in favor of composition. Java contains notorious historical counterexamples:
- `Properties` extends `Hashtable`: A properties table maps specific strings to specific strings; it merely *has* a hash table under the hood.
- `Stack` extends `Vector`: Stacks only support push, pop, peek, and size operations, whereas `Vector` exposes arbitrary indexed random access. Extending `Vector` not only broke the abstraction but permanently squandered prime naming real estate in the core platform:
> “We can't use the name stack for a class that actually does implement stack anymore because the name has been taken.”
Conversely, `Set` extending `Collection` is a proper use of inheritance, because a set genuinely is a specialized collection that prohibits duplicate elements.
Bloch also emphasizes a controversial but essential rule regarding inheritance:
> “But I believe that you should design and document a class for inheritance or else prohibit it outright, to make the class final or have no publicly accessible constructor.”
Because subclassing breaks encapsulation in a way method invocation does not—known as the fragile base class problem—an overridden method can unintentionally disrupt internal self-use patterns in a superclass. If a superclass later refactors internal dispatches among its methods, existing subclasses break. To make inheritance safe, an API must explicitly document all self-use patterns. Abstract skeleton classes like `AbstractSet` and `AbstractMap` correctly follow this discipline, whereas non-final concrete classes like `HashMap` and `HashSet` fail to document their internal self-calls and remain inherently fragile.
---
[45:11](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=2711s) Avoid Boilerplate: Don't Make the Client Do Work the Module Can Do
Method design hinges on two fundamental axioms. The first rule, recalled from earlier in the lecture, is: "When in doubt, leave it out." The second fundamental rule is equally critical:
> “The second one is, don't make the client do anything the module could do.”
An API must not force callers into repetitive ping-pong calls where intermediate values are passed from one method to another. This anti-pattern forces developers to write boilerplate code, which invariably breeds defects through copy-paste-and-modify programming. If a copied snippet is not completely tailored to the new context, it will still compile while producing erroneous runtime behavior.
Bloch illustrates this failure with an egregious real-world example from the W3C DOM and TrAX APIs: serializing an in-memory XML document to an output stream. Conceptually, this operation requires a single method call: `writeDoc(OutputStream out)`. Instead, the standard library forces developers through an absurd maze of factory lookups, property configurations, and adapters:
```mermaid
flowchart TD
accTitle: W3C XML Document Output Ceremony
accDescr: Multi-step factory and adapter pipeline required simply to write an XML document to a stream.
subgraph Setup["1. Factory & Instance"]
A["TransformerFactory.newInstance()"] --> B["TransformerFactory"]
B --> C["newTransformer()"]
C --> D["Transformer"]
end
subgraph Config["2. Output Properties"]
D --> E["setOutputProperty(doctype-system,...)"]
end
subgraph Transform["3. Adapters & Execution"]
E --> F["Wrap Document in DOMSource"]
E --> G["Wrap OutputStream in StreamResult"]
F --> H["transformer.transform(DOMSource, StreamResult)"]
G --> H
end
subgraph Catch["4. Useless Catch"]
H --> I["catch TransformerException -> AssertionError"]
end
```
Developers must import `javax.xml.transform`, `javax.xml.transform.dom`, and `javax.xml.transform.stream`. They must invoke `TransformerFactory.newInstance()`, construct a `Transformer`, retrieve system identifiers from the document type to configure obscure output properties, wrap the document in a `DOMSource`, wrap the destination in a `StreamResult`, call `transform()`, and finally wrap the invocation in a try-catch block for a checked `TransformerException` that can never actually occur at runtime. This six-line pile of unreadable boilerplate exists solely because the API designers prioritized design patterns over concrete use cases.
---
[48:31](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=2911s) The Principle of Least Astonishment and Failing Fast
Methods must honor the Principle of Least Astonishment: API callers should never be surprised by a method's behavior. Investing implementation effort or even accepting minor performance overhead is entirely justified to keep caller expectations aligned with reality:
> “The user of an API should never be surprised by its behavior.”
A classic violation occurs in `java.lang.Thread`: the static method `Thread.interrupted()`. When developers call it to query whether the current thread has been interrupted, the method quietly clears the thread's interrupt status as a side effect. By concealing its primary mutation behind an inquiry name, the method has cost developers countless debugging hours. Had the method been called `clearInterruptStatus()` and documented as returning the previous state as a convenience, the surprise would vanish.
APIs must also fail fast by exposing programming errors immediately. Catching defects at compile time is vastly preferable to exposing them in production:
> “Whenever there's an error, you should tell the user of your API as soon as possible after the error has happened.”
Bloch notes that while static typing remains controversial among some language communities, introducing generics into Java proved its value firsthand by uncovering latent bugs in existing codebases through stricter type specifications. If an error cannot be caught at compile time, the runtime system must signal failure the exact instant improper data is supplied, rather than tolerating corrupted state that explodes minutes later. Bloch introduces `Properties` once again as the quintessential example of failing this principle.
[50:59](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=3059s) The Perils of Weak Typing: `java.util.Properties`
```mermaid
flowchart TD
accTitle: Failure Mode of java.util.Properties
accDescr: Diagram showing how invalid objects inserted via put are silently accepted and only cause a ClassCastException much later during save or store.
subgraph Insertion ["At Insertion Time"]
A["Client calls properties.put(key, nonStringVal)"] --> B["Accepted silently: Object, Object signature"]
end
subgraph Failure ["10 Minutes Later"]
B --> C["Client calls properties.save(outputStream)"]
C --> D["Translates to legacy file format"]
D --> E["💥 ClassCastException thrown"]
end
```
Examining the Java specification reveals that a `Properties` instance is meant to map strings to strings. However, because it inherits from `Hashtable`, its inherited `put` method accepts an `Object` key and an `Object` value. Rather than failing immediately when handed something other than a string, the class silently accepts any arbitrary object. The failure is deferred until much later—often ten minutes down the line when the program attempts to invoke `save` (or `store`) to serialize the contents to an output stream into a format that, as Joshua Bloch dryly observes, “isn’t quite XML.”
Only at that delayed moment does the method blow up with a `ClassCastException`. By that point, diagnosing the problem is an absolute nightmare because the stack trace points only to the serialization call, leaving no record of where the invalid object was originally placed into the properties map.
- `Properties` should map `String` to `String`, but exposed `put(Object, Object)` due to inheritance from `Hashtable`.
- Fails late during serialization (`save` / `store`) rather than failing fast on insertion.
---
[51:49](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=3109s) Provide Programmatic Access to String Data
Bloch emphasizes a critical rule for API design: whenever an API returns information in string form, it must also provide a programmatic way to access that exact same data. If an API fails to provide programmatic access, callers are forced to write custom string-parsing logic. Doing so turns the human-readable string into an accidental, de facto part of the public contract. Once clients parse that string, any future addition of fields or alteration of formatting breaks client code in production.
“You can never add information to that string because there's code out there that [is parsing] that string,” Bloch points out. The correct approach is to supply structured accessor methods alongside the printable representation, explicitly warning in the specification that the string’s text format is unspecified and subject to change without notice. Bloch illustrates this mistake with Java's initial exception handling: originally, developers could only inspect a stack trace via `Throwable.printStackTrace()`, forcing developers to parse stdout strings. Java 1.4 finally resolved this by introducing `Throwable.getStackTrace()`, returning structured `StackTraceElement` objects containing the file name, line number, class name, and method name, but by then “the horse had already left the barn.”
---
[53:12](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=3192s) Overload With Care: The `TreeSet` Constructor Mistake
Method overloading is easily overused and becomes dangerous when it creates ambiguous behavior—situations where two overloads produce completely different side effects when supplied with the same underlying data. Bloch holds up one of his own design mistakes from the Java Collections Framework as a prime cautionary tale: `TreeSet` provides two constructors, one taking a `Collection` and the other taking a `SortedSet`. The `Collection` constructor ignores any preexisting ordering, whereas the `SortedSet` constructor assumes it should mirror the ordering of the passed set.
This creates an insidious trap: if a client holds a `SortedSet` but passes it typed or cast as a plain `Collection`, the constructor yields a `TreeSet` with completely different ordering behavior than if passed uncast. “The basic rule here is just because you can doesn't mean you should,” Bloch reflects, noting that he should have used a dynamic `instanceof SortedSet` check inside a single constructor or provided explicitly named factory methods instead of overloading.
---
[54:11](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=3251s) Choose Appropriate Parameter and Return Types
API designers should favor interface types over concrete classes, and input parameters should be as specific as possible. If a method accepts a generic `Collection` but inevitably crashes at runtime unless passed a `Set`, the API is broken: it has unnecessarily traded a compile-time safety check for a runtime exception. Similarly, designers must avoid string abuse. In the modern era of web services and XML, data frequently enters an application as text, but developers must convert strings into rich domain types immediately rather than propagating them throughout the codebase. Bloch recalls encountering a program that shuttled around a string containing either `"yes"` or `"no"` across its entire execution, completely ignoring the `boolean` primitive.
Floating-point types require extreme caution. Float and double primitives must never be used for monetary calculations because base-10 decimals like $0.10$ cannot be represented exactly as binary floating-point fractions. Financial figures should always be modeled using `BigDecimal`, `long`, or `BigInteger`. Furthermore, when a genuine need for floating-point calculations arises, developers should almost always choose `double` over `float`, since modern hardware processes `double` just as fast while avoiding severe loss of numeric precision.
---
[55:55](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=3355s) Maintain Consistent Parameter Ordering Across Methods
Parameter ordering must remain consistent across an entire API surface. When order varies arbitrarily between related methods, callers will mix them up, and if the swapped parameters share the same type, the compiler cannot catch the mistake. Bloch points to a notorious historical blunder from Unix: the memory copy routine `bcopy` accepts `(source, destination, size)`, whereas the string copy routine `strncpy` expects `(destination, source, size)`. Programmers swapping the two routines inevitably overwrite and corrupt their source data with garbage from the destination buffer, creating bugs that take an extraordinary amount of time to track down.
```mermaid
flowchart LR
accTitle: Parameter Ordering Discrepancy in C / Unix APIs
accDescr: Flowchart contrasting the parameter orders between bcopy and strncpy that lead to silent data corruption.
subgraph UnixInconsistency ["Historical Unix Inconsistency"]
direction TB
B["bcopy: source, destination, size"]
S["strncpy: destination, source, size"]
end
UnixInconsistency --> C["Developer assumes uniform order"]
C --> D["💥 Source memory clobbered with destination garbage"]
```
By contrast, the Java platform enforces strict structural conventions:
- In `java.util.Collections`, the first parameter is always the collection being mutated or manipulated.
- In `java.util.concurrent`, time parameters are universally specified as a duration followed by a `TimeUnit` (for example, `long timeout, TimeUnit unit`), never the reverse. Strong typing ensures that even an accidental swap is caught immediately at compile time.
---
[57:03](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=3423s) Avoid Long Parameter Lists
Long parameter lists should be avoided, ideally limited to three parameters or fewer, because memorizing sequences beyond that is difficult for developers. Long runs of identically typed parameters are especially perilous since transposed arguments compile cleanly without warning. Bloch shows a classic anti-pattern from the Win32 `CreateWindowEx` API, which requires around 15 parameters, including runs of integers like `int x, int y, int nWidth, int nHeight` mixed with other parameters that are secretly typedef'd aliases for integers. Using such an API without heavy IDE assistance is virtually impossible.
To eliminate sprawling argument lists, designers can decompose a bloated method into multiple focused methods or bundle arguments into helper classes. In particular, when a constructor or static factory takes numerous parameters—most of which have reasonable default values—designers should apply the Builder pattern. A builder allows callers to fluently configure only the fields they care about before invoking `build()`, producing code that is straightforward to read and write.
---
[58:08](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=3488s) Never Return `null` Instead of Empty Arrays or Collections
APIs should avoid return values that demand exceptional handling by the client. Specifically, methods returning arrays or collections should never return `null` to indicate the absence of items; they should return a zero-length array or an empty collection. Bloch highlights a defect in Java's 2D imaging API: `BufferedImageOp.getRenderingHints()` returns a `RenderingHints` object or `null`. Because the method rarely returns `null`, developers routinely forget to write a null check, resulting in surprise `NullPointerException`s in production. Returning an empty collection costs virtually nothing and guarantees that standard iteration loops execute cleanly without special-case branching.
---
[58:50](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=3530s) Wrapping Up: "API Design by Bumper Sticker"
Running out of time, Bloch wraps up the session. Rather than rushing through the remaining slides, he offers to stick around to answer individual questions. He then hands out copies of a companion document from his earlier OOPSLA presentation. Faced with a strict two-page limit for an extended abstract in the conference proceedings, Bloch decided to condense his principles into "API design by bumper sticker"—a collection of roughly 50 punchy maxims.
“I tried to basically take this entire talk and break it down into 50 little maxims, like, 'When in doubt, leave it out' or, you know, 'All programmers are API designers,' each with a sort of a sentence describing, you know, in a bit more detail,” Bloch explains as he distributes the handouts to the audience. He offers to post the document internally to the Google tech mailing list (JJB) for attendees who missed out on the physical paper, and closes the talk to audience applause.
## Notable quotes
> [48:31](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=2911s) The user of an API should never be surprised by its behavior. — Joshua Bloch
> [24:19](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=1459s) You'll probably be able to add it later, but you most certainly will not be able to take it out once you put it into an API. — Joshua Bloch
> [21:45](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=1305s) The names are the API talking back to you, so listen to them. — Joshua Bloch
> [05:47](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=347s) It is not the case that the more powerful an API is, the better it is. It should basically be just powerful enough to do what it needs to do. — Joshua Bloch
> [42:01](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=2521s) I believe that you should design and document a class for inheritance or else prohibit it outright, to make the class final or have no publicly accessible constructor. — Joshua Bloch
> [38:06](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=2286s) Transliterated APIs are almost always broken. — Joshua Bloch
> [03:26](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=206s) Whether you're designing a language or an API, you are creating a tool for programmers to express their intent to the machine and to other programmers, who read the program, maintain it, modify it and so forth. — Joshua Bloch
> [48:31](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=2911s) Whenever there's an error, you should tell the user of your API as soon as possible after the error has happened. — Joshua Bloch
## Mentioned
Albert Einstein, AWT, BufferedImageOp, Carnegie Mellon University, Confessions of a Used Program Salesman: Institutionalizing Software Reuse, CORBA, David Garlan, David Parnas, Design Fragments, DOM, DOMSource, George Fairbanks, Google, java.awt.Component, java.awt.Dimension, java.util.AbstractMap, java.util.AbstractSet, java.util.Calendar, java.util.Collection, java.util.Collections, java.util.concurrent, java.util.Date, java.util.HashMap, java.util.HashSet, java.util.Hashtable, java.util.Properties, java.util.Set, java.util.Stack, java.util.TimerTask, java.util.TreeSet, java.util.Vector, Java, Java Collections Framework, Jeremy Manson, Joda-Time, Jon Bentley, Joshua Bloch, JSR 310, OOPSLA, RenderingHints, RSA Data Security, Inc., StackTraceElement, StreamResult, Sun Microsystems, Thread, Throwable, TimeUnit, TrAX, TransformerFactory, Transarc, W3C, Will Tracz, William Scherlis, Win32
## Video description
Google Tech Talks
January 24, 2007
ABSTRACT
Every day around the world, software developers spend much of their time working with a variety of Application Programming Interfaces (APIs). Some are integral to the core platform, some provide access to widely distributed frameworks, and some are written in-house for use by a few developers. Nearly all programmers occasionally function as API designers, whether they know it or not. A well-designed API can be a great asset to the organization that wrote it and to all who use it. Good APIs increase the pleasure and productivity of the developers who use them, the quality of the software they produce, and ultimately, the corporate bottom line....
In a Google TechTalk, Joshua Bloch argues that API design is an enduring software engineering discipline equivalent to programming language design, where interfaces must be kept minimal, intuitive, and strictly insulated from underlying implementation details.
Why API Design Matters as an Irreversible Asset (1:39)
Bloch contends that every modular programmer acts as an API designer because internal boundaries become interfaces that colleagues inevitably adopt. APIs represent massive long-term corporate assets that can anchor developer ecosystems for decades, but flawed designs turn into permanent liabilities. Because downstream clients rely on public contracts, publicly released interfaces cannot simply be modified without breaking client systems, meaning designers realistically have only one opportunity to get an interface right.
Requirements Gathering and Early Specification Workflows (7:42)
API requirements should be met with skepticism because stakeholders typically prescribe narrow technical mechanisms—such as fine-grained garbage collection parameters—rather than explaining their true operational problems. Designers should distill concrete use cases and draft minimal, one-page specifications that can be iterated and debated rapidly, avoiding massive waterfall documents that entrench bad decisions due to invested egos.
Bloch advises developers to write client code calling provisional APIs before beginning any implementation or finalizing formal documentation. This early consumer code prevents wasted engineering effort and ultimately serves as public tutorial examples. He cites a CMU study demonstrating that concurrency flaws in initial Java 1.0 applet examples persisted in web codebases for over a decade, underscoring the rule that example code deserves ten times the scrutiny of routine application logic.
Information Hiding and the Rule of Minimality (24:19)
Bloch identifies 'when in doubt, leave it out' as his primary design rule, urging engineers to minimize conceptual weight rather than just counting superficial classes or methods. Features can easily be introduced later, but removing an established method causes immediate downstream breakage. APIs must also enforce rigorous information hiding; implementation details like internal data stores or exact `hashCode` formulas should never leak into public contracts.
Leaking implementation details strips away future optimization freedom. Declaring Java serialization inadvertently exposes all private fields as permanent public contracts, while returning mutable objects compromises safety. For example, the legacy AWT `Component.getSize()` returned a mutable `Dimension` object, forcing defensive heap allocations on every call and generating massive garbage collection overhead in render loops until primitive getters like `getWidth()` were introduced in Java 1.2.
Inheritance Pitfalls and Minimizing Mutability (39:41)
Classes should remain immutable whenever possible to ensure thread safety and eliminate defensive copying. If mutability is necessary, the state space must be kept compact and strictly linear; Bloch cites `java.util.TimerTask` as a positive example where attempts to reset and reuse tasks were rejected to keep its lifecycle simple.
Inheritance should be restricted strictly to genuine subtyping obeying the Liskov Substitution Principle. Bloch criticizes historical Java missteps where `Properties` extended `Hashtable` and `Stack` extended `Vector`, which broke encapsulation and squandered platform names. Furthermore, classes must either be explicitly designed and documented for inheritance—accounting for internal method self-use—or prohibited from subclassing by making them final.
Method Ergonomics and Preventing Client Errors (45:11)
Methods should adhere to the Principle of Least Astonishment and spare clients from performing repetitive boilerplate conversions. Bloch highlights how serialization in W3C DOM and TrAX forced developers through an unreadable six-line sequence of factories, adapters, and property configurations to perform a single logical write operation. He also warns against misleading inquiries that conceal mutations, such as `Thread.interrupted()`, which unexpectedly clears interrupt status.
Interfaces must fail fast, favoring strong compile-time types over weak strings or generic objects. Methods returning collections or arrays should return empty collections rather than `null` to prevent unexpected runtime `NullPointerException`s. Bloch cautions against dangerous overloading that relies on runtime types—as occurred in the `TreeSet` constructor overloads—and urges designers to limit parameter lists to three or fewer arguments, applying the Builder pattern when constructors grow complex.
Bloch concluded by noting that API design inevitably involves overconstrained trade-offs where designers must aim to displease competing stakeholders equally rather than succumbing to design by committee. Because time prevented covering every topic, he distributed a companion summary condensing his guidelines into roughly 50 aphorisms.
精炼阅读摘要
Joshua Bloch 在本次 Google 技术讲座中主张,API 设计本质上与编程语言设计无异,它不仅关乎工具易用性,更是决定软件长期维护成本与企业资产价值的核心架构活动。优秀的 API 必须做到易学、易用且极难误用,设计者应秉持审慎精简的原则,在需求挖掘、模块边界与契约制定中全面隐藏实现细节并尽早暴露错误。
API 的战略价值与设计本质 (1:39)
Bloch 指出,每一位编写模块化代码的程序员本质上都是 API 设计者,因为模块边界即为内部接口。一旦代码被其他团队或开发者依赖,作者便丧失了随意修改接口的自由。公开发布的 API 若设计得当,能够成为支撑企业业务长达四分之一个世纪的核心资产;反之,缺陷接口不仅会带来高昂的技术支持成本,还会因向后兼容的限制而永久锁死后续创新的空间。API 设计与编程语言设计的终极目标一致,即为开发者提供一套能够向底层机器和后续维护人员清晰表达意图的工具,区别仅在于 API 设计必须在宿主语言的既有语法约束下运作。
需求求证与轻量规范迭代 (7:42)
在设计流程上,Bloch 警告设计者必须对各方提出的需求保持高度怀疑。用户通常倾向于提出直接的实现方案(例如要求精细的垃圾回收控制参数),而设计者的职责是剥离表象,挖掘其底层的真实业务场景(例如确保服务器稳定无卡顿运行)。为避免团队陷入耗时数月、产出冗长规范文档却最终难堪大用的瀑布陷阱,设计初期应仅维持约一页纸的轻量级敏捷规范。这种轻量规范不仅便于在十分钟内完成架构重构,还能以低摩擦的方式接纳各方反馈。此外,在 SPI(服务提供者接口)设计中,必须严格遵循“三度法则”(Rule of Threes),即至少对接三种互不相同的后端实现以彻底消除对单一实现特性的隐式依赖,同时设计者要做好权衡,以“让所有利益相关方同等程度地不满意”为目标来抑制接口膨胀。
以精简契约抵御实现泄露 (24:19)
针对 API 边界划分,Bloch 强调“存疑之时,宁缺毋滥”(When in doubt, leave it out)。后期追加特性十分简单,但一旦公开便几乎无法收回。评估接口规模不应只看方法数量等表层体积,而应考量概念复杂度(Conceptual Weight),通过复用既有核心抽象(如 Java 的 `Set` 体系)能显著提升功能与学习成本的比例。更重要的是,底层实现细节绝不能渗入公开契约。例如,电话号码服务若抛出底层的 `SQLException`,日后迁移到底层非关系型存储时便会引发灾难;在 Java 中随意声明 `implements Serializable` 也会将所有私有字段暴露为持久化的公开契约。此外,对于无状态或不可变设计难以完全覆盖的类,必须严格控制其可变状态空间,如 `TimerTask` 采用单向线性的生命周期,坚决拒绝引入重置与循环复用机制,从而杜绝复杂的隐式状态同步缺陷。
防御性类型设计与错误及时暴露 (48:31)
在方法与类型签名层面,接口设计必须践行“最小惊奇原则”并坚持“尽早报错”(Fail-Fast)。弱类型与不当继承往往会掩盖严重错误,典型反例是 `Properties` 继承自 `Hashtable`,导致本应映射字符串的接口允许插入任意 `Object`,直到数分钟后调用序列化方法时才抛出 `ClassCastException`,极大增加了排查难度。在参数设计上,应优先使用特定接口而非宽泛对象,避免使用 `float` 或 `double` 处理货币数值,并保持跨方法的参数顺序一致,避免如 Unix 中 `bcopy` 与 `strncpy` 颠倒源与目标参数而导致的数据破坏。为了防止客户端编写脆弱的字符串解析逻辑,任何返回文本信息的方法都应提供对应的结构化程序访问入口,同时集合类或数组返回值在无数据时应返回空集合或零长度数组,坚决不能返回 `null`。
由于演讲时长所限,Bloch 未能展开全部重构案例,但他指出成熟的 API 必须经得起漫长时间与多样化场景的检验。他将长篇演讲提炼为包含约 50 条短句的“保险杠贴纸式”经验法则,以此提醒工程师将规范编写、示例代码打磨(投入精力应达生产代码十倍)以及继承限制视作日常工程纪律,而不仅是孤立的理论探讨。
Detailed notes — zh
## Background
该视频由 Google TechTalks 频道于 2007 年 10 月 9 日在 YouTube 平台发布,记录了 2007 年 1 月 24 日举行的一场技术讲座。视频简介指出,无论是否有自觉,软件开发者在日常工作中都在扮演 API 设计师的角色,而一套设计优良的 API 能显著提升开发效率与软件质量,成为组织的关键资产。
## TL;DR
Java 核心架构师乔舒亚·布洛克(Joshua Bloch)在讲座中系统阐述了优秀 API 设计的核心哲学与工程实践。他指出 API 设计在本质上等同于编程语言设计,决定了接口是成为公司的长期资产还是难以摆脱的技术负债。布洛克提出应以“带着怀疑审视需求”和“一页纸敏捷规范”起步,恪守“存疑即弃”(When in doubt, leave it out)以及“绝不强迫客户端做模块自身能完成的事”两大核心原则。随后,他结合 Java 标准库与 W3C DOM 等典型软硬件历史教训,详细剖析了在类、方法、参数及异常设计中最小化可变性、严防实现泄露、保持一致命名与提供结构化访问的具体准则。
## Atmosphere & style
整场讲座洋溢着典型的技术极客氛围,开场主持人的幽默捧杀与布洛克的机智回击迅速活跃了现场气氛。布洛克的讲授风格犀利直率又极具感染力,既有对工业界常见设计反模式与委员会作风的辛辣嘲弄,也有毫不避讳剖析自己过往失误的严谨自省。观众能深切感受到一位工程宗师对代码优雅度与健壮性“宗教般的虔诚”,整场分享兼具发人深省的警示与酣畅淋漓的启发感。
## Bulletin
- [01:29](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=89s) 乔舒亚·布洛克(Joshua Bloch)指出,API 具有极强的资产属性与高昂的迁移成本,糟糕的 API 一旦发布并被广泛依赖就会演变为无法轻易更改的永久技术负债。
- [24:19](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=1459s) 布洛克提出了 API 设计的核心铁律“存疑即弃”(When in doubt, leave it out)以及“绝不让客户端做模块本该完成的事”,强调通过最小化概念负担和消除样板代码来降低使用成本。
- [17:24](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=1044s) 针对服务提供者接口(SPI)的验证,布洛克引用了 Will Tracz 的“三原则”(The Rule of Threes),即必须至少基于三个独立实现进行验证,才能避免接口与单一实现产生隐式绑定。
- [15:03](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=903s) 面向 API 的示例代码必须作为典范来编写,布洛克提出示例代码的打磨时间应当是编写普通生产代码的十倍,避免因示例中的设计缺陷导致全球开发者长期复制错误实践。
## Key takeaways
- [03:26](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=206s) 每个程序员在编写模块化与可复用代码时本质上都是 API 设计师,良好的 API 思维能从根源上提升整体软件质量。
- [04:32](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=272s) API 设计在底层逻辑上与编程语言设计完全相通,两者的根本使命都是让开发者能够清晰地向机器和协作者表达意图。
- [07:15](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=435s) 需求收集应带着怀疑态度深挖业务方的真实底层诉求,避免直接将其主观设想的具体技术方案当作 API 需求。
- [09:55](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=595s) 设计初期应保持一页纸的精简规范以确保敏捷迭代,避免因撰写冗长规格书而绑定工程师自尊并丧失架构调整的灵活性。
- [15:03](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=903s) 必须尽早且频繁地面向未实现的 API 编写调用代码,示例程序应具有典范性(Exemplary),其投入时间应为普通生产代码的十倍。
- [17:24](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=1044s) 设计 SPI(服务提供者接口)必须遵循“三原则”,仅支持单一实现的 SPI 几乎注定无法在日后平滑支持第二个扩展。
- [19:14](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=1154s) API 设计通常过度受限,应追求“让每个人受到同等程度的不满”,由强有力的主导者把控架构统一性,杜绝委员会式拼凑折中。
- [24:19](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=1459s) “存疑即弃”(When in doubt, leave it out)是 API 设计的核心准则,通过复用通用接口降低概念权重比缩减表层方法数更为关键。
- [26:19](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=1579s) 严防底层实现细节(如特定数据存储抛出的 SQLException、序列化内部字段或具体哈希算法)渗漏到公共接口契约中。
- [29:26](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=1766s) 遵循信息隐藏原则将可访问性降至最低,除真正常量外公开类严禁暴露可变公有字段,以降低模块间耦合度。
- [30:54](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=1854s) 命名构成了领域专用语言,应避免晦涩缩写与同义异名,维持正交对称性以实现“代码即散文”的自解释性。
- [32:22](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=1942s) 文档即规范,必须详尽记录方法的前置条件、后置条件、副作用、参数度量单位、对象所有权转移以及可变类型的状态空间。
- [35:31](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=2131s) 避免因不良 API 决策锁死性能上限(如 AWT 可变 Dimension 导致频繁分配引发 GC 压力),但绝不能为临时瓶颈破坏 API 架构。
- [39:42](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=2382s) 类设计应追求“最小化可变性”,除非有极其充分的理由否则应优先设计为不可变类,或像 TimerTask 一样采用极简的单向生命周期。
- [42:01](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=2521s) 公有类继承必须严格遵守里氏替换原则,面向继承时应遵循“专门为继承设计并文档化自使用模式,否则彻底禁止继承”。
- [45:11](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=2711s) 绝不要强迫客户端去做模块自身就能完成的事情,避免像 W3C DOM 输出 XML 那样产生充斥工厂与适配器的繁琐样板代码。
- [48:31](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=2911s) 践行“最小惊奇原则”与“快速失败”,避免方法名隐藏重大副作用(如 Thread.interrupted 清除标记),并优先利用强静态类型在编译期拦截错误。
- [50:59](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=3059s) 任何以字符串形式返回的数据都必须同时提供结构化的程序化访问接口,防止字符串格式被强制升格为事实上的 API 契约。
- [53:12](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=3192s) 谨慎使用重载并避免参数语义歧义,涉及金额绝对禁止使用二进制浮点数,且返回数组或集合时严禁返回 null 代替空集合。
## Detailed walkthrough
### [00:23](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=23s) 开场介绍:谷歌编程语言系列讲座与对布洛克的调侃
讲座主持人杰里米·曼森(Jeremy Manson)首先向在场观众表示欢迎,并介绍这是谷歌“编程语言高级主题”(Advanced Topics in Programming Languages)系列讲座的最新一场。他解释举办这一系列活动的目的,是为了发掘并展示谷歌内部在编程语言领域拥有的庞大专业知识。曼森半开玩笑地对台下表示,这意味着在座的各位——除了他自己之外,所有在谷歌工作的“天才们”——都应该主动站出来做分享。他呼吁大家不论是通过发邮件、发即时通讯消息还是其他方式,都可以直接联系他并提交分享主题,他会协助安排日程;大家不必像即将登场的乔舒亚·布洛克(Joshua Bloch)或后续的主讲嘉宾那样是成名作者,只要有想法都可以参与。
由于活动时间有些推迟,曼森迅速转向对布洛克的介绍。他打趣道,布洛克之前特意叮嘱不要把他捧得太高,免得让他“冲昏头脑”(swell head)。因此曼森决定“克制”一点,调侃称在这个满是天才的公司里,布洛克是“那颗闪耀得最为明亮的星”,甚至极尽夸张地自嘲自己“连给他提鞋都不配”。台下的布洛克听后笑着插话回击:“这笔账我回头再跟你算。”(I'll get you for that.)随后在全场的掌声与笑声中,曼森正式请出了主讲人乔舒亚·布洛克。
### [01:29](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=89s) 为什么 API 设计至关重要:资产还是长久负债
登台后,布洛克幽默地表示通常自己会向介绍人致谢,但今天他决定省去这套客套。他随即向观众打了一剂预防针:这篇演讲内容非常长,通常他喜欢做只有一到两个核心观点的小型分享,但 API 设计这一主题内容过于庞杂,无法草草带过。他请求大家把疑问留到最后,由于整场演讲很可能会占满一个小时,他承诺演讲结束后会一直留在现场,直到回答完大家的所有问题。
进入正题后,布洛克抛出了核心问题:为什么 API 设计如此重要?他指出,API 可以成为一家公司最宝贵的资产之一。人们会在优秀的 API 上投入巨资,这种投入既有显而易见的方式(比如围绕 API 构建产品、直接针对其编写业务代码),也有隐蔽的方式——那就是开发者为了学习和熟练掌握该 API 所花费的长达数月的时间成本。一旦开发者熟悉了某套 API,他们就极度抗拒学习新方案,因为这意味着他们必须全盘推翻已有的知识储备并重新来过;加之 API 会深深植根并交织在公司的整体技术基础设施当中,因此一个成功的 API 能为公司奠定长盛不衰的商业壁垒。
反过来,糟糕的 API 则会成为公司最沉重的负债。首先,用户无法让该接口正常运行预期的功能,这会导致无休无止的支持电话和运维压力;其次,它会严重束缚公司向前演进的能力,因为糟糕的 API 一旦发布并被广泛依赖,你就失去了随意更改它的自由,只能被永远套牢。正如乔舒亚·布洛克所断言:“你只有一次把事情做对的机会。”(You have one chance to get it right.)鉴于这种覆水难收的残酷现实,设计出经得起时间考验的 API 就显得尤为迫切。
### [03:26](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=206s) 每个人都是 API 设计师:模块化与代码复用的隐性边界
布洛克接着指出,台下很多工程师可能觉得自己只是普通程序员,根本算不上“API 设计师”,但事实恰恰相反——只要你在编写计算机程序,你就是一名 API 设计师。良好编程实践的本质是模块化,而模块与模块之间的交互边界,其本质就是 API。
优秀的代码模块往往会被复用。当你在日常工作中写了一个表现优秀的模块,迟早有一天你的同事会面临完全相同的需求。一旦同事开始调用你的代码模块,你就再也不能随心所欲地重构该接口了,因为任何细微的改动都会直接破坏同事的程序;假如这位同事又有十个朋友也引入了这段代码,那么你的修改空间就会被彻底封死。布洛克强调,始终带着 API 设计的思维来编写代码,能从根本上提升程序的整体质量,它会防止开发者随手拼凑意大利面条式的散乱逻辑,促使大家写出高度可组合、可复用且清晰合理的高质量软件单元。
### [04:32](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=272s) API 设计与编程语言设计的相通性
针对为何在一个名为“语言设计系列”的讲座上大谈 API 设计的疑问,布洛克先是给出了一个轻佻的答案:因为曼森叫他来讲,而曼森“曾经”是他的朋友,所以他随口答应了。但回归严肃的本质,布洛克解释道,API 设计与编程语言设计在底层逻辑上几乎毫无二致。两者唯一的实质区别在于自由度:API 设计受限于其宿主语言既有的语法规则,而设计编程语言时你则可以任意定义自己想要的语法形式。
然而,无论是在设计一门语言还是在设计一个 API,其核心使命都是完全相同的——创造一种工具,使程序员能够向计算机以及后续阅读、维护、修改代码的其他开发者清晰表达自己的意图。在现代软件工程中,编程语言与类库早已密不可分,两者共同构成了一个完整的软件平台;设计语言的过程必然伴随着核心类库的同步设计。因此,设计优秀 API 所需的技能体系与设计编程语言几乎是完全重叠的。
```mermaid
flowchart LR
accTitle: Language Design vs API Design
accDescr: 比较语言设计与 API 设计在表达意图上的共同本质与语法约束上的差异
subgraph Goal["核心目标:表达意图"]
A["开发者意图"] --> B["向计算机表达执行逻辑"]
A --> C["向协作者表达代码设计"]
end
subgraph Scope["实现形式与约束"]
D["编程语言设计"] -->|"语法规则完全自由"| E["平台 (Platform)"]
F["API 库设计"] -->|"受宿主语言语法约束"| E
end
Goal --- Scope
```
### [05:47](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=347s) 优秀 API 的核心特征及本次演讲的大纲
在定义什么样的 API 才是“好 API”时,布洛克列举了若干关键标准:
- **易学易用,甚至无需文档**:它应该极其符合直觉,易于记忆,让人看一眼就觉得合情合理。
- **难以甚至无法误用**:优秀的 API 会通过设计机制主动引导甚至强迫开发者“做正确的事”,避免产生错误调用。
- **高可读性与可维护性**:使用该 API 编写的业务代码必须清晰易懂、易于后续维护。
- **恰到好处的表达能力**:布洛克特别指出,并非能力越强 API 就越好,它不该为了盲目追求强大而变得复杂,只要“足够完成任务”即可。
- **平滑演进的能力**:能够满足当前业务需求,并留有在未来需求变更时平滑扩展的演进空间。
- **契合目标受众**:API 必须说使用者的语言,例如为华尔街金融分析师设计的 API 与为理论物理学家设计的 API,在术语体系和思维方式上应当有截然不同的取向。
针对整个演讲的组织架构,布洛克宣布演讲共分为五个核心板块:首先是 API 的设计流程;其次是通用的 API 设计原则;随后将这些原则具体细化落地到类(classes)、方法(methods)以及异常(exceptions)的定义规范;最后若时间允许,他还将通过几组具体的代码重构案例,直观展示如何将糟糕的设计改良为优雅的 API。
### [07:15](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=435s) API 设计流程:带着怀疑态度收集真实需求而非方案
在流程方面,布洛克坦言自己不是死板的“流程狂热分子”(process weenie),但他发现所有优秀 API 的诞生过程都遵循一些不可或缺的规律。第一步就是收集需求,但他强调必须“带着健康的怀疑态度”去审视所有收集到的意见。这是因为当询问业务方“你需要这个 API 做什么”时,对方脱口而出的往往不是真正的业务需求,而是一套他们主观设想的具体技术解决方案,而此时往往存在更好、更通用的解决途径。
为了说明这一点,布洛克列举了一个垃圾回收的案例:如果业务方告诉你“我们需要精确控制垃圾回收的触发时间间隔以及每次垃圾回收的最大停顿时间”,这并不是真实需求;真正的底层需求其实是“我们需要服务器在进行垃圾回收时依然能平稳顺畅地运行业务”。如何实现这一平稳目标应当由 API 设计师来权衡定夺,设计者的核心职责就是通过不断与利益相关者拉锯沟通,将真实需求剥离出来,并将其转化为具体的“用例”(Use Cases)——即该 API 必须解决的具体业务场景,以此作为衡量一切备选方案的基准。
布洛克分享了自己在 Transarc 公司工作时的真实教训:
- 当时系统进行升级,业务方提出的要求是:“我们需要一套全新的数据结构以及支持版本 2(Version 2)属性的远程过程调用(RPC)接口。”
- 但经过深挖,他们真正的意图其实是:“我们需要一种全新的数据格式,以便能够容纳未来内部数据结构的所有后续演进。”
- 业务方并不想每当服务器增加几个新属性时,就不得不重写一套底层数据结构、重塑磁盘存储接口与网络传输协议。理解了这一点后,布洛克通过引入动态机制重新设计了底层格式,彻底一劳永逸地解决了扩展性问题。
此外,布洛克提醒大家注意:在很多情况下,构建一个比被要求的方案略微更通用的解决方案,往往反而比编写高度定制的代码更简单、更有回报。这并不意味着动辄就要造一套庞大的框架,而是说明高度定制的特异性逻辑有时实现起来既繁琐又不具备扩展能力。
### [09:55](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=595s) 从一页纸规范起步:敏捷性压倒完备性
在确立了用例之后,进入规格说明书(Spec)编写阶段时,布洛克给出的核心建议是:规范必须尽可能简短,单页纸是最理想的状态。在 API 设计的最初期阶段,敏捷迭代的重要性完全压倒对完备性的追求。
布洛克痛斥了一种常见的行业错误:组织六个聪明绝顶的工程师关起门来闭门造车六个月,最后拿出厚达 240 页的庞大技术规格说明书。乔舒亚·布洛克对此尖锐地批评道:“这是一件极其糟糕的事,因为到了那个地步,首先,他们的个人自尊已经深深绑定在刚刚完成的东西上了;哪怕做出来的是一堆垃圾,他们也会硬着头皮把它建出来。”(It's an awful idea because at that point, first of all, their ego is invested in what they've just done. They're going to build it even if it's a piece of crap.)
这种大而全的笨重规格说明书存在致命缺陷:
- 丧失敏捷性:面对 240 页的复杂文档,根本没人能在编码前看清其整体优劣。
- 修改代价巨大:一旦发现底层设想存在根本性错误,就必须推翻重写整整 240 页的内容。
- 脱离核心诉求:由于早期没有得到充分验证,它极有可能在一开始就遗漏了某些关键业务需求。
因此,API 规范应当在一开始压缩到一页纸的体量,只有这样,设计者才能以极低的成本将接口草案快速推送给尽可能多的利益相关者并获得真实反馈。
### [10:55](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=655s) 保持规范精简并尽早面向 API 编写客户端代码
在设计 API 时,倾听反馈并严肃对待至关重要。当有用户指出某种方案行不通、无法编写特定程序时,设计者固然可能会下意识觉得“你根本就不该写那种程序,那是个糟糕的想法”,但更常见的情况往往是:“天哪,我之前完全没考虑到这一点,这确实非常重要,如果我们换一种结构来组织呢?”如果最初的规范只有一页纸长,你完全可以在十分钟内完成一次重大的架构重构。规范越简短,修改起来就越容易;只有当你确信方向正确时,才逐步充实规范的细节。
为了验证方向是否正确,必须尽早编写代码——特别强调的是,这是面向你正在定义的 API 编写调用代码,而不是编写 API 本身的底层实现。此时完全可以假装该 API 已经被实现好了。Joshua Bloch 分享了他在筹备 OOPSLA 演讲时编写的一个真实用例:当时有人希望在计算发生失败时拥有自动重试的能力。由于 Java 中已经有了现成的 Executor 框架,因此核心诉求只是一个告知在失败时如何进行重试的“重试策略”(Retry Policy)。在这个微型接口中只定义了两个方法:一个方法接收异常并返回布尔值,用以判断特定故障是否可恢复;另一个方法根据初始启动时间和此前的重试次数计算下一次延迟。通过传入这些必要数据,重试策略本身可以保持完全无状态(Stateless),从而能够以单例的形式存在,例如定义一个名为指数退避(Exponential Backoff)的单例策略,而无需在每个实例内部存储任何状态。
虽然这份草案还不够完整,既无法直接生成 Javadoc,也不足以让开发人员直接着手实现底层逻辑,但它已经足够让别人看一眼就清楚判断:“是的,这能满足我的需求”或者“不行,它满足不了”。配套的静态工具类进一步展示了如何使用这些重试策略:用户可以传入一个现有的 `ExecutorService` 和重试策略,从而获取一个包装后的 `RetryingExecutorService`。由于返回的实例仍然实现了 `ExecutorService` 接口,任何熟悉该接口的人都能零学习成本地上手,这正是降低 API 概念负担(Conceptual Weight)的极佳方式。Joshua Bloch 看着幻灯片上的静态工厂方法自我调侃道,对于指数退避参数中的初始延迟、时间单位以及 `timeout` 参数,他当时甚至没在幻灯片上写清 `timeout` 到底是指单次超时还是彻底放弃重试的总超时时间:“这告诉我这份草案其实还不够好,原本我想展示一份极其简单、能放进一两张幻灯片里、足以传达设计意图并检验好坏的 API,而这里的答案是它‘几乎够好,但还没完全做好’。”不过这种精简形式的核心优势依然十分明显:它用极小的体量验证了功能可行性,且一旦发现问题极易修改。
```mermaid
flowchart TD
accTitle: 基于现有接口降低 API 概念负担的设计流程
accDescr: 展示将 ExecutorService 与 RetryPolicy 组合为 RetryingExecutorService 的包装流
subgraph Inputs["输入组件"]
ES["现有 ExecutorService\n(开发者已熟悉)"]
RP["无状态 RetryPolicy\n(如 Exponential Backoff)"]
end
subgraph Factory["静态工具与包装"]
Util["RetryingExecutors 静态工厂"]
end
subgraph Output["输出接口"]
RES["RetryingExecutorService\n(仍实现 ExecutorService 接口)"]
end
ES --> Util
RP --> Util
Util --> RES
```
- 精简规范的核心优势:只有一页纸的规范可以在十分钟内完成重构,随着信心增长再逐步细化。
- 概念负担的缩减机制:尽量复用已经定义良好的核心接口(如 `ExecutorService`),让使用者凭借已有知识直接上手。
- 无状态设计优势:重试策略方法通过形参接收所有状态(如重试次数、起始时间),使具体策略对象可以作为无状态单例复用。
---
### [15:03](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=903s) 尽早且频繁地编写使用示例:示例代码必须是典范
编写面向 API 的调用代码必须贯彻“尽早且频繁”的原则。必须在具体实现 API 之前就开始写代码,这能避免因 API 结构设计拙劣而被迫把写好的底层实现全盘扔掉;同样,在完整编写详细规范之前也要开始写代码,以防在错误的 API 上浪费精力撰写冗长的规格说明。更为关键的是,在逐步完善 API 的全过程中,绝不能中断调用代码的编写。许多人往往在设计进行到一半时就停止编写调用代码,随后一头扎进漫长痛苦的“实现死亡行军”(Death march to implementation),结果往往在交付前一周遭遇可怕的意外:当重新尝试调用时,才猛然发现该 API 根本无法解决最初设想的关键场景。
有些人会担忧,把时间花在面向 API 编写测试或调用代码上,是在浪费本该用于实现 API 的工时。针对这一顾虑,Joshua Bloch 极其明确地反驳道:“Nothing could be further from the truth. Those initial pieces of code that you write to any API are among the most important pieces of code that you'll ever write to it.”(没有什么比这种想法更远离真理了。你为任何 API 所写下的最初那些代码,都是你将为它写下的最重要的代码之一。)这些最初的代码最终会沉淀为你对外发布的官方示例,并被广大开发者疯狂模仿。如果写对了,你就向市场播撒了正确使用 API 的良种;若写错了,则意味着未来十年间整个网络上都会充斥着基于该错误的残缺程序。
Joshua Bloch 提到,他过去一直发自内心地坚信这一点,而如今学术界已经给出了实质性的证据:在 OOPSLA 大会上,来自卡耐基梅隆大学(CMU)的 Fairbanks、Garlan 和 Scherlis 发表了一篇题为《Design Fragments》的论文。该论文通过严谨的追溯发现,最初伴随 Java 第一版发布的 Applet 官方示例代码中的并发错误,直接导致了至今仍散落在网络各处的数以千计的错误并发程序。因此,Joshua Bloch 总结并强调:“Example programs should be exemplary.”(示例程序必须具有典范性。)他提出了一条经验法则:在编写示例代码的每一行上所花费的时间,应该是编写普通生产代码的十倍。
- 死亡行军的风险:中途停止使用场景验证会导致临近发布时才暴露致命缺陷,使得重要用例彻底无法支持。
- 示例代码的长期扩散效应:官方示例会被全球开发者直接复制和借鉴,错误范例的生命周期往往长达数十年。
- 时间投入经验准则:为确保代码的典范性,示例程序的编写与打磨时间投入应达到生产代码的 10 倍。
---
### [17:24](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=1044s) 面向 SPI 编写与“三原则”
在各种接口中,针对 SPI(Service Provider Interface,服务提供者接口)编写代码比面向普通 API 更加关键。SPI 并不是给普通业务程序员直接调用的接口,而是一种供插件机制接入的系统扩展接口。例如,在安全加密体系中,RSADSI 公司或 Sun Microsystems 可以分别提供各自的加密算法实现,而终端用户只需调用更高层级的统一 API,底层系统会自动分发至具体的 SPI 实现上。
SPI 的核心使命在于将截然不同的底层实现细节隐藏在抽象之后。然而,设计 SPI 时最容易陷入的致命陷阱就是只基于一个提供者进行设计。如果一套 SPI 在发布时仅有一个底层实现,在实际工程中它几乎注定无法支持第二个实现;当你尝试引入第二个提供者时,会痛苦地发现该 SPI 早已在不知不觉中与唯一的那个实现紧紧绑定在了一起。相对而言,如果最初就针对两个实现来设计与验证,通常能勉强通用,后续扩展可能会带有一些困难;但若能成功支持三个实现,该接口通常就能完美适配任意数量的扩展。正如 Joshua Bloch 所说,当接口能顺利承载三个截然不同的提供者时,第四个实现通常不会超出前三个实现的考量范畴。
```mermaid
flowchart LR
accTitle: 服务提供者接口(SPI)分层调用与实现验证
accDescr: 高层 API 面向用户,底层通过 SPI 抽象分发至多个不同的具体实现
User["上层应用程序"] --> HighAPI["高层统一 API\n(如通用加密接口)"]
HighAPI --> SPI["服务提供者接口 (SPI)"]
subgraph Providers["底层实现验证(三原则)"]
P1["实现一: RSA 算法库"]
P2["实现二: Sun 官方算法库"]
P3["实现三: 第三方提供者"]
end
SPI -.-> P1
SPI -.-> P2
SPI -.-> P3
```
Joshua Bloch 表示这一规律是自己在实践中摸索出来的,随后在 Will Tracz 的著作《Confessions of a Used Program Salesman: Institutionalizing Software Reuse》(二手程序销售员的自白:软件复用的制度化)中也发现了完全相同的论述,书中将其正式命名为“三原则”(The Rule of Threes)。在此处,Joshua Bloch 还顺便向听众解释了演讲胶片中的颜色标识规范:绿色代表最佳实践、应当照做;红色则代表反模式、务必避免。
- SPI 的本质定位:面向驱动/插件提供者的扩展接口,由系统通过高层 API 统一对外分发。
- 单一实现的局限:仅基于单一提供者构建的 SPI 往往包含大量隐式假设,导致后续几乎无法扩展。
- “三原则”(The Rule of Threes):针对三个不同的实现进行编写与验证,是确保接口具备真正通用性与可复用性的黄金门槛。
---
### [19:14](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=1154s) 保持现实期望:在妥协中追求统一与克制
在整个 API 设计周期中,设计者必须时刻保持现实的期望,因为绝大多数 API 设计本质上都是“过度受约束的”(Over-constrained)。各方利益相关者总是希望它能承载超出其承受极限的功能,因此设计者必须学会妥协,不可能让所有人同时满意。正如 Joshua Bloch 所警告的那样:“If you try to please everyone, you come up with a pig. You come with big, nasty APIs that no one will ever be able to use properly.”(如果你试图取悦所有人,最终只会搞出一头蠢猪,制造出庞大丑陋、谁也无法正常使用的糟糕 API。)
面对这种困境,一个听起来或许有些奇怪的正确策略是:力求让每个人都受到同等程度的不满(Aim to displease everyone equally)。如果核心利益相关者中有一方极度不满,而其他人欢天喜地,这往往说明 API 遗漏了某项不可或缺的关键能力;反之,若每个人的满意度都略低于 100%,但大家都能勉强接受并达成目标,通常说明折中方案是合理的。但绝不能将这种折中误解为推崇“委员会设计”(Design by committee)或把所有人的想法揉成一团大杂烩。相反,API 项目必须拥有一个强有力的架构负责人(Design lead),以确保产出的设计风格连贯、精巧且富有美感,让人一眼看出它出自单一大脑或具备统一思想的团队之手。在满足广泛客户诉求与维持设计优雅统一之间取得平衡,始终是一门微妙的权衡艺术。
最后,设计者必须做好犯错的心理准备,因为 API 设计本身极具挑战。Joshua Bloch 幽默地指出,好在几年的实际生产使用一定会把隐藏的错误全部暴露出来;但不幸的是,到了那个时候,你通常已经无法对 API 做任何破坏性修改了。当然,你依然可以写出精彩的演讲胶片,向大家倾囊相授这些血泪教训以防后人重蹈覆辙——而这正是他今天站在这里演讲的真正目的。
- 权衡法则:避免为了满足所有需求而使 API 臃肿不堪,适度且均衡的妥协优于单一角色的绝对满意。
- 强设计主导制:坚决反对委员会式拼凑,确保由单一意志或统一思想的主导者把控架构的内聚与美感。
- 历史包袱的不可逆性:API 错误一旦在生产环境沉淀数年便极难修正,早期的克制与审查因而变得无可替代。
### [20:46](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=1246s) 预留扩展空间与集合框架的教训
在设计 API 时,犯错几乎是不可避免的,而一旦发布,设计者就会被最初的 API 牢牢套牢。因此,API 的设计必须允许未来的扩展,以便后人能够绕过最初设计的缺陷。Joshua Bloch 以自己在 1997 至 1998 年间设计的 Java 集合框架(Collections Framework)为例:当时的 `SortedSet` 和 `SortedMap` 实现存在真正的缺陷,尤其是接口表现出非对称性,正向遍历远比逆向遍历容易得多。Joshua 当时虽意识到这些缺陷,却不知道该如何修复。好在原有的 API 具备可扩展性,到了 Java 6 发布时,团队引入了继承自 `SortedSet` 和 `SortedMap` 的 `NavigableSet` 与 `NavigableMap`,通过添加新方法解决了这些历史难题。虽然外部旧实现尚未完全更新,但至少 Sun 公司官方提供的标准集合实现都得到了修复。
随后,Joshua 转向探讨良好 API 设计的通用原则,并强调接下来的内容虽听上去像是“母亲与苹果派”(motherhood and apple pie,意指人人都赞成却流于空泛的陈词滥调),但他会给出切实可行的实践建议。首要原则便是“一个 API 应该只做一件事,并且做好”(do one thing and do it well)。检验这一点的核心标准是功能是否易于解释:“If it's not easy to explain, then it's not doing one thing and doing it well.”如果无法用清晰简洁的语言说清楚,或者想不出一个好名字,那这个设计往往就是一团糟。
```mermaid
flowchart TD
accTitle: Java集合框架演进
accDescr: 展示Java集合框架如何通过扩展SortedSet与SortedMap引入NavigableSet与NavigableMap以修复历史缺陷
subgraph Legacy["Java 1.2 历史接口 (1997-1998)"]
SS["SortedSet<br/>(存在遍历不对称等缺陷)"]
SM["SortedMap<br/>(存在遍历不对称等缺陷)"]
end
subgraph Extended["Java 6 扩展接口"]
NS["NavigableSet<br/>(新增双向导航等方法)"]
NM["NavigableMap<br/>(新增双向导航等方法)"]
end
SS -->|"继承与扩展"| NS
SM -->|"继承与扩展"| NM
```
---
### [22:24](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=1344s) 命名是 API 的心声:好名字驱动好设计
命名是 API 在向设计者“回话”(The names are the API talking back to you, so listen to them)。如果在为类或方法命名时,憋出的是冗长晦涩的怪胎——例如 `DynAnyFactoryOperations`、违反平台命名规范的 `_BindingIteratorImplBase`、`ENCODING_CDR_ENCAPS`,或者让人啼笑皆非的 `OMGVMCID`(Joshua 调侃道:“我知道 OMG 是‘Oh my god’,但剩下的部分我就完全看不懂了”),这就明确表明设计出了大问题。这些反面案例全都是 Java 官方平台真实存在的 API(来自 CORBA 相关包)。
相比之下,优秀的命名清晰直观,就像字号或字体(Font)、集合(Set)、私钥(PrivateKey)、锁(Lock)或线程工厂(ThreadFactory)一样,使用者一眼就能意会其用途。更重要的是,优秀的命名能够自发驱动良好的软件设计:一旦将某个组件命名为 `Set`,它的操作集(插入元素、移除元素、检查包含关系)就变得顺理成章、自然而然。相反,糟糕的命名往往是糟糕设计的直接警讯。当名字怎么起都不顺畅时,通常意味着当前的抽象职责不清,设计者应当随时做好准备:若一个模块塞入了过多职责,就果断拆分;若一个模块暴露了本应隐藏的内部实现细节,就应当将多个模块合并为一个更大的模块以封装这些细节。
---
### [24:19](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=1459s) 保持精简与“存疑即弃”(When in Doubt, Leave it Out)
API 应当“尽可能精简,但不能过于简陋”(as small as possible but no smaller)。虽然这句话通常被归于爱因斯坦名下,但 Joshua 调侃自己掘地三尺也没找到出处,认为爱因斯坦可能心里这么想过,但从未真正说过。无论如何,API 必须满足其功能需求,而在这段分享中,Joshua 给出了全场最关键的一条忠告:“If you only remember one thing from the talk today, please remember this: When in doubt, leave it out.”
这条准则适用于一切要素,包括功能、类、方法乃至单个参数。只要对是否应该引入某项特性心存疑虑,就坚决把它拿掉。因为未加入的东西以后随时可以再加,但一旦放入公共 API,外部开发者便会开始依赖它;日后若想移除,使用者必将大发雷霆(scream bloody murder)。在评估 API 的大小时,概念权重(conceptual weight)远比表面的代码体量(bulk,即方法数、类数或参数量)更关键。决定学习成本的是开发者需要理解多少个全新的核心概念。
降低概念权重最有效的手段是复用通用接口。以集合框架为例,尽管拥有 `HashSet`、`TreeSet`、`LinkedHashSet` 以及后来大批并发集合等繁多的底层实现,但由于它们全部遵循统一的 `Set` 接口,使用者掌握了 `Set` 的概念后便能无缝操作所有具体实现,无需学习新的 API。这种做法极大地提升了“权能比”(power to weight ratio)——让用户以极低的学习成本完成极其丰富的工作。
---
### [26:19](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=1579s) 严防实现细节泄露到公共接口
“实现细节绝不应影响 API”是另一条至理名言。一旦实现细节渗漏到 API 中,不仅会困扰调用者,更会严重束缚实现者未来重构和迭代的自由。Joshua 举了一个典型的负面例子:假设设计一个电话号码查询相关的 API,方法签名中却抛出了 `SQLException`;未来如果团队希望将底层数据库替换为某种私有专有数据存储系统,由于现有客户端全部编写了捕获 `SQLException` 的代码,API 便彻底被套牢,甚至不得不荒谬地去模拟旧的 SQL 异常。异常的抽象层次必须与 API 本身所在的抽象层次严格保持一致。
类似的设计失误还体现在对实现细节的过度规范(over-specification)上:
- **哈希函数规范化问题**:方法的规格说明不应固定具体哈希算法。规格说明只需承诺 `hashCode()` 返回一个整数、两个不同对象产生该整数的冲突概率极低、且计算成本廉价即可。具体计算公式应当作为实现细节,允许版本更迭时持续优化。除非是用于持久化磁盘存储的专用哈希函数,否则固定返回值只会锁死未来的优化空间。Java 早期版本在此处犯下了严重失误,将几乎所有类的哈希算法写入了规范。
- **异常直接透传**:开发者在编写方法时,往往因为底层调用抛出了某种底层异常,便不假思索地在自身方法签名中声明抛出该异常,导致实现泄漏。
- **简单实现 `Serializable`**:在 Java 中仅仅声明 `implements Serializable` 是一场灾难。对象的序列化形式默认会把类的所有私有字段(private fields)暴露为公共 API 的一部分,彻底摧毁了封装性。正确的做法是精心设计自定义序列化形式。
---
### [29:26](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=1766s) 最小化可访问性与命名的语言属性
为了实现彻底的信息隐藏(Information Hiding,由计算机科学家 David Parnas 提出),设计者必须将类、成员和字段的可访问性降到最低(尽量声明为 private)。公共类除常量(编译期常量本质并非普通字段)外,绝对不应包含任何公共字段。最小化可访问性能够最大程度降低模块间的耦合度。当细节被妥善隐藏在模块边界之后时,各个模块便能够由不同的团队独立且并行地进行理解、使用、构建、优化、调试与测试。反之,若各模块都在直接读写对方暴露的细节,牵一发而动全身,系统的任何局部改动都将引发灾难。
最后,Joshua 再次强调了命名不可忽视的极端重要性。针对那些认为“名字不重要、差不多就行、别浪费时间赶紧推进”的观点,他进行了严厉的反驳。在公共 API(包括几个月后自己也要维护的代码)中,命名至关重要,因为每个 API 本质上都是设计者为使用者量身发明的一套“小型领域语言”(a little language)。
- **信息隐藏的核心价值**:由 David Parnas 奠定理论基础,解耦模块以允许并行开发、独立测试与自由重构。
- **公共字段禁令**:公开类严禁暴露可变 public 字段,仅允许暴露真正的不可变常量。
- **API 的语言属性**:API 即语言,接口中的类型与方法构成了该领域的词汇与语法,命名的严谨度直接决定了该语言的表达力与易用性。
### [30:54](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=1854s) 命名原则:一致性、对称性与“代码即散文”
在设计 API 时,用户必须先学习该 API 的语言,然后用这门语言进行思考与交流,因此命名必须在很大程度上做到“顾名思义”(self-explanatory),坚决避免晦涩难懂的缩写——Joshua Bloch 认为早期的 Unix 命名在这方面表现得极为糟糕。比命名清晰更重要的是保持 API 内部的一致性:同一个词在整个 API 中反复出现时必须始终指代相同的概念,绝不能用多个同义词去表达同一件事。例如,如果在同一个 API 中同时出现 `remove` 和 `delete`,这几乎总是错误的,因为普通开发者根本无法从字面区分两者的差异;如果它们含义相同,就必须统一名称,而如果它们确实不同,就必须在命名上体现出显著区别,例如命名为 `delete` 与 `expunge`,用户就能立刻意会到 `expunge` 意味着更为彻底或永久的清除。
除了内部一致性,API 还应当极力追求对称性(symmetry)。如果 API 中提供了两个动词 `add` 和 `remove`,以及两个名词 `entry` 和 `key`,那么理想情况下就应该完整提供 `addEntry`、`addKey`、`removeEntry` 和 `removeKey`。如果缺失了其中某一个组合,设计者必须有非常站得住脚的理由。Joshua Bloch 指出,虽然并非所有 API 都能做到绝对对称,但绝大多数都应该如此。当命名与对称性做到极致时,最终写出的调用代码就会像优美的散文一样自然顺畅。例如代码读起来就像是:“如果汽车速度超过限速的两倍,扬声器应当发出警报提示小心警察”(If the car's speed is more than twice the speed limit, the speaker should generate an alert that says watch out for cops)。Joshua Bloch 总结道:“And if you get it right, the code will read like prose. That's the prize.”
- **避免生涩缩写**:原始 Unix 命名是糟糕的反面教材,API 词汇应当自解释。
- **消灭同义异名**:不要在同一个 API 混用同义动词(如 `remove` 与 `delete`),除非通过词义强度差异(如 `delete` 与 `expunge`)明确区分行为。
- **维持概念对称**:在动词与名词的组合上尽量保持正交对称,防止功能缺失让调用者产生突兀感。
---
### [32:22](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=1942s) 文档即规范:为什么必须虔诚地为每一处公共 API 编写文档
文档与 API 设计同等重要,软件工程先驱 David Parnas 早在 1994 年就一针见血地指出:“复用是一件说起来容易做起来难的事情。要实现复用,既需要良好的设计,也需要极其优秀的文档。即使我们看到了良好的设计(这种情况本就罕见),如果没有好的文档,这些组件也绝不可能被复用。”Joshua Bloch 坦言,读到 Parnas 这段话后,自己就有了“宗教般的虔诚”,从此近乎强迫性地为公共 API 中的每一个类、接口、方法、构造器、参数以及异常编写文档。
现实中很多开发者并未做到这一点。如果没有文档注释来阐述规范,所谓的规范究竟是什么?Joshua Bloch 指出开发者此时只有两种极其糟糕的选择:要么全凭猜测,这会导致程序极可能出现 Bug 且无法正常运行;要么被迫去翻阅底层源代码,而一旦调用者通过阅读源码来理解行为,“实现本身就成了规范”(the implementation becomes the specification)。这会导致实现细节被严重过度指定(over-specified),API 的维护者将彻底失去在未来重构或更改底层实现的自由。Joshua Bloch 严正警告道:“If you had side effects and you don't document them, people will shoot themselves in the foot.”
针对不同 API 元素的文档撰写,Joshua Bloch 给出了具体指导要求:
- **类(Class)**:清晰说明该类的实例究竟代表什么现实抽象或概念。
- **方法(Method)**:明确说明方法与客户端之间的契约(Contract),即调用前必须满足的前置条件(preconditions)、调用返回后保证为真的后置条件(postconditions),以及极其关键的**副作用**(side effects)。
- **参数(Parameter)**:绝不能只写一句“数据块的大小”,必须写明具体度量单位(字节还是兆字节)、字符串的格式要求(例如是否必须为符合特定 Schema 的 XML),以及至关重要的**所有权转移**(ownership)——将对象传入后,调用方是否仍拥有该对象?后续是否还能修改它?还是所有权已经彻底转移给了接收对象?
- **可变状态空间(Mutable State Space)**:如果定义的类型是可变的,必须极其详尽地文档化其状态空间(state space),否则用户根本无法知晓在何时调用什么方法是合法的,调用后对象又会进入什么状态。Java 原生自带的 `java.util.Date` 和 `java.util.Calendar` 就是状态空间未被妥善文档化的典型恶劣范例,多年来引发了无数 Bug。不过 Joshua Bloch 欣慰地透露,Sun 公司在几天前刚刚决定推进一项新的 JSR(即后来的 JSR 310,引入了 Joda-Time 的设计思想),终于有望彻底摆脱 `Date` 和 `Calendar` 留下的历史烂摊子。
---
### [35:31](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=2131s) 性能考量与平台共生:避免 AWT Dimension 式的惨痛代价
API 设计必须审慎权衡性能后果。虽然开发者常说“过早优化是万恶之源”(Premature optimization is evil),但这绝不代表可以忽视性能。Jon Bentley 曾强调不良的 API 决策会彻底锁死性能的上限:例如在应当设计为不可变类型时错误地做成可变类型(或反之)、在接口本该抽象的地方暴露了具体实现类型(导致调用者被死锁在特定实现上无法享受未来更好的算法)、或是使用公有构造器而不是静态工厂方法。与此同时,绝对不能为了迎合暂时的性能问题而去刻意扭曲破坏 API 的结构——如果为了避开某个临时存在的性能瓶颈而把 API 改丑,一旦底层问题修复变快了,丑陋损坏的 API 却要永远遗留在生态中。正如 Joshua Bloch 所说:“Design your APIs for the long haul. Luckily, good API design usually coincides with good performance.”
```mermaid
flowchart TD
accTitle: AWT getSize 性能缺陷流转示意图
accDescr: 展示可变 Dimension 对象如何迫使 getSize 每次分配全新对象并导致 GC 压力
subgraph AWT11["Java 1.1 的设计缺陷"]
Call11["调用 Component.getSize()"]
Alloc["每次调用必须 new Dimension(w, h)<br/>(防范调用方修改内部引用)"]
GC["高频 GUI 渲染中被调用数百万次<br/>产生数兆字节垃圾对象并加剧 GC 压力"]
Call11 --> Alloc --> GC
end
subgraph AWT12["Java 1.2 的弥补方案"]
Call12["调用 Component.getWidth() / getHeight()"]
Prim["直接返回不可变的基本数据类型 (primitive types)"]
Clean["零额外对象分配,彻底消除垃圾收集压力"]
Call12 --> Prim --> Clean
end
```
关于不良 API 决策限制性能的经典案例,莫过于早期 Java AWT 中的 `java.awt.Dimension`。当开发者调用某个组件的 `getSize()` 时,它会返回一个包含两组坐标的 `Dimension` 对象。由于 `Dimension` 被设计成了包含公开可变字段的**可变对象**,为了防止不同线程或调用者之间拿到对象引用后肆意修改互相干扰,`getSize()` 每次被调用时,底层都必须重新在堆上分配一个全新的 `Dimension` 实例。尽管分配一个只包含两组数值的微型对象本身极快,但在复杂的 GUI 循环中,`getSize()` 会在瞬间被调用数百万次,瞬间生成数兆字节的短命垃圾,给垃圾收集器(GC)带来沉重的分配压力。虽然 Sun 在 Java 1.2 中通过新增直接返回原始数值类型(如 `getWidth()`/`getHeight()`)的方法进行了弥补,但那些依然依赖 Java 1.1 旧 API 的遗留代码却注定永远承受这种缓慢。
此外,编写的 API 必须与所在平台“和平共存”(coexist peacefully):
1. **遵循平台惯例**:严格遵守目标平台的命名约定与核心习惯,主动避开该语言平台中公认的禁忌(verboten)与陷阱。
2. **模仿核心 API 模式**:调用者都熟悉目标平台的核心标准库,如果你的 API 风格与核心库如出一辙,开发者就能无师自通。
3. **切忌机械直译(Transliteration)**:如果打算把现有的 C++ API 移植到 Java,绝不能把 C++ 的类和方法机械地一一对应翻译成 Java 类。在 C++ 中完全合情合理的设计,照搬到 Java 中往往荒谬绝伦。设计者必须抽离一步,重新思考:“这个类原本要解决的核心问题是什么?在 Java 的语境下应当如何地道地表达?”机械直译出来的 API 几乎无一例外是残缺损坏的。
---
### [39:42](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=2382s) 类设计原则:最小化可变性(Minimize Mutability)
进入类设计(Class Design)环节,Joshua Bloch 提炼出的首要铁律就是“最小化可变性”(Minimize mutability)。除非有极为充分且不可抗拒的理由需要使其可变,否则类应当一律设计成不可变类(Immutable classes)。不可变类的优势极其巨大:它们不仅结构极其简单,天生具备线程安全性,而且对象实例可以被自由共享与无限制复用,调用方完全无需为了防御性拷贝而频繁生成新实例。
不可变类唯一的缺点在于:每一个不同的值都必须对应一个独立的对象实例。例如一个长达上百万位的超大整数(`BigInteger`),如果它是可变的,修改最后一位数值只需以极低的代价在原地进行位翻转;但因为不可变性,系统不得不将这一兆位的数据全部拷贝一份,修改末位后再废弃掉旧的一兆位内存,这在特定极端场景下确实存在开销。但 Joshua Bloch 强调,即使业务场景强迫你必须使用可变类,也必须尽可能地限制其可变范围,让它“越不可变越好”。必须为可变类设计一个极其精简且定义严密的状态空间,让调用方能清楚预判何时调用何种方法才是合法的。Joshua Bloch 再次提及 Java 的 `Date` 和 `Calendar`(例如 `Calendar.roll()` 方法),以及多线程环境下并发使用未文档化状态的日期格式化工具类(`DateFormat`)时引发的数据混淆,将其斥为由于内部状态空间混乱且不可变性缺失而导致调用者屡屡踩坑的典型反面教材。
Joshua Bloch 用最精炼的一句话总结了类的可变性设计哲学:“Classes should be immutable unless there's a very good reason to make them mutable.”
- **不可变类的核心价值**:天然线程安全、代码简单直接、实例自由安全共享,大幅降低心智负担。
- **不可变类的折中代价**:无法进行就地修改(in-place modification),微小变更可能带来整对象重新创建的内存拷贝开销。
- **可变类的退守策略**:若必须可变,状态空间必须收敛得极小且定义完备,禁止留下模糊或隐式的方法调用顺序依赖。
### [41:05](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=2465s) 最小化可变性与 TimerTask 的生命周期设计
对象如果包含了隐藏的可变状态,往往会给并发环境埋下巨大的安全隐患。Joshua Bloch 指出,Java 中的日期格式化类(如 `SimpleDateFormat`)表面上让人感觉像是一个不可变对象,但其内部实际上维护着可读不到的状态;一旦两个线程试图同时使用同一个实例,两个线程都会彻底崩溃。这种类就属于“可变性超出了合理限度”的典型负面案例。
与之形成鲜明对比的是 `java.util.TimerTask`。Bloch 解释道,`TimerTask` 并不是纯粹的不可变对象,但它极为成功地“最小化了可变性”(minimizes mutability)。由于它代表一次正在进行或即将进行的具体计算,内部状态变化在所难免。然而,它的生命周期模型异常干净简单:创建任务、排期执行、按照既定次数运行完毕,随后彻底销毁。“它死了,消失了,故事到此结束。” Bloch 回忆起当年设计 Timer API 时的真实争论:
“Now, there were people, who asked us, when we were designing the timer API, 'We want to reuse them, you know. It's like expensive to make a TimerTask.' And the answer is, 'No, let it die in peace.'”
通过果断拒绝重用 `TimerTask` 实例的诉求,设计团队从根本上斩断了状态图中的循环回溯逻辑。Bloch 强调,如果后续还需要执行相同的任务,直接再 `new` 一个即可;正是通过消除这种状态机闭环,使得整套 API 变得极其简洁且远离并发缺陷。
```mermaid
flowchart LR
accTitle: TimerTask 生命周期状态图
accDescr: 展示 TimerTask 从创建、调度、运行到终结的单向无环生命周期。
subgraph LifeCycle["TimerTask 极简单向生命周期"]
A["创建任务 (Create)"] --> B["调度排期 (Schedule)"]
B --> C["执行任务 (Run)"]
C --> D["彻底终结 (Dead)"]
end
```
### [42:01](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=2521s) 审慎继承:里氏替换原则与 Java 历史设计失误
在处理公有 API 的类设计时,必须严格遵守面向对象中的里氏替换原则(Liskov Substitution Principle,简称 LSP)。Bloch 给出了一个极为朴素实用的自查标尺:当你在公有 API 中打算让类 `Foo` 继承类 `Bar` 时,务必反问自己一句:“每一个 `Foo` 真的都是一个 `Bar` 吗?”(Is every foo a bar?)。只有能够不假思索且问心无愧地回答“是”的时候,才允许使用继承;否则绝不要继承。
Bloch 列举了 Java 标准库中因违背该原则而遗留的灾难性案例:
1. **`Properties` 继承自 `Hashtable`**:每个 `Properties` 对象难道是 `Hashtable` 吗?“天哪,绝对不是。” Bloch 吐槽道。属性列表是一个将特定字符串映射到另一字符串的专用载体,它充其量是底层利用哈希表来实现,即“拥有”(has-a)一个哈希表,绝非“是一个”(is-a)哈希表。
2. **`Stack` 继承自 `Vector`**:栈本应只支持 `push`、`pop` 以及少许 `peek` 或 `size` 操作,而 `Vector` 却允许根据索引进行任意的随机访问。让 `Stack` 继承 `Vector` 是彻头彻尾的错误,并且造成了永久性的命名空间浪费——它白白霸占了 `Stack` 这个绝佳的类名,导致后续即使想提供真正遵循栈契约的类,也无法再命名为 `Stack` 了。
相反,良性继承的代表是 `Set` 继承(实现)`Collection`。每一个 Set 确实都是一个 Collection,它只是在语义上加了一条强约束——不允许包含重复元素。
### [43:23](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=2603s) 继承与封装的破坏:要么专为继承设计并文档化,要么彻底禁止
关于面向对象中的继承,Bloch 提出了一个颇具争议但被他深信不疑的设计准则:“专门为继承而设计并提供文档,否则就直接禁止继承”(design and document a class for inheritance or else prohibit it outright)。禁止的方式包括将类声明为 `final`,或者将其构造器私有化以杜绝公有访问。其深层逻辑在于:继承在潜移默化中破坏了封装性,这种破坏力远甚于普通的方法调用。
这就是业界常说的“脆弱基类问题”(Fragile Base Class Problem)。若子类重写了父类的一个方法,由于父类内部的其他方法可能相互依赖并在底层调用了这个被重写的方法,子类的行为就会被意外篡改。更严重的是,当基类未来进行版本迭代重构时,原本方法间的自调用链路一旦发生改变(比如改为统一委托给第三个辅助方法),子类原有的逻辑就会在毫无征兆的情况下彻底崩溃。
避免这种隐患的唯一途径是公开自使用模式(self-use patterns),在 Javadoc 中把每个方法在内部如何调用其它方法交代得一清二楚。Bloch 坦言,Java 早期库在这一点上做得不够好:
- **反面典型**:`HashMap`、`HashSet` 等大多数具体的集合类都是非 final 的,且均未明确文档化自身的自调用规律,因此具有潜在的脆弱性。
- **正面典型**:`AbstractSet`、`AbstractMap` 等抽象类则真正做到了为继承而生,提供了周密的继承契约与自使用说明。
### [45:11](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=2711s) 绝不要让客户端做模块本该完成的事:W3C DOM 的荒谬设计
谈到方法级别的 API 设计时,Bloch 提醒现场听众牢记整场演讲最关键的两大核心法则。第一条是前半部分讲过的“如有疑虑,宁缺毋滥”(When in doubt, leave it out);而第二条则是:“不要强迫客户端去做模块自身就能完成的事情。”
“The second one is, don't make the client do anything the module could do. Those two things are like the fundamental rules of API design.”
Bloch 情绪激动地指出,最糟糕的 API 莫过于迫使调用者来回反复调用多个接口,机械地把上一个调用的输出当作垃圾塞进下一个调用的输入。这种设计会催生大量令人厌恶的模板样板代码(boilerplate code)。样板代码往往依赖复制粘贴再微调,一旦开发者漏改了某处,代码依然可以通过编译,但运行时却会产生隐蔽的 Bug。
为了直观证明这种反面模式有多荒唐,Bloch 当场剖析了 W3C DOM API 中将 XML 文档写入输出流的真实反面案例:
```mermaid
flowchart TD
accTitle: W3C DOM 打印 XML 的复杂调用流程
accDescr: 展示从 TransformerFactory 开始,层层组装包装器最终执行转换的繁琐样板流程。
subgraph FactoryLayer["工厂与实例获取"]
A["TransformerFactory.newInstance()"] --> B["factory.newTransformer()"]
end
subgraph ConfigLayer["属性配置"]
B --> C["transformer.setOutputProperty(...)"]
C --> D["设置 DocType 与 System ID"]
end
subgraph AdaptLayer["适配包装与调用"]
D --> E["new DOMSource(doc)"]
D --> F["new StreamResult(outputStream)"]
E --> G["transformer.transform(source, result)"]
F --> G
end
subgraph ExceptionLayer["异常处理"]
G --> H["catch (TransformerException)"]
H --> I["throw new AssertionError()"]
end
```
在理想设计中,开发者只需要一个简单明了的方法(例如 `doc.write(outputStream)`)。但在 W3C API 中,开发者不仅需要导入一堆如 `xmltransform.dom` 和 `xmltransform.stream` 等令人费解的包,还必须经历如下折磨:
- 调用 `TransformerFactory.newInstance()` 获取工厂对象;
- 从工厂中再 `newTransformer()`;Bloch 带着明显的讽刺调侃道:“如你所见,他们读过《设计模式》,太棒了,代码里到处都是‘模式’!”;
- 在 Transformer 上手动调用 `setOutputProperty` 设置文档类型及各种系统标识符;
- 使用适配器模式把 Document 手动包成 `DOMSource`,把 OutputStream 手动包装成 `StreamResult`;
- 执行 `transform` 方法,并被迫使用 try-catch 去捕获一个实际上永远不可能发生(checked)的 `TransformerException`,然后在 catch 块中抛出断言错误。
Bloch 感叹,足足六行晦涩难懂的垃圾代码,仅仅是为了做一件打印 XML 的最基本小事。如果设计者当初能够以实际用例为导向,这一切荒谬本都可以避免。
### [48:31](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=2911s) 最小惊奇原则与快速失败原则
Bloch 强调的下一条核心原则是“最小惊奇原则”(Principle of Least Astonishment)——API 的使用者绝不应该对其运行行为感到意外。为了不让用户困惑,哪怕多耗费实现的精力、甚至是做出一定的性能牺牲,都是完全值得的。一旦 API 产生预期之外的行为,开发者就会写出充满 Bug 的程序。
在这方面,Java 早期并发体系中的 `Thread.interrupted()` 是一个臭名昭著的反面教材。该方法名看起来仅仅像是一个只读的询问状态的方法,用于检测当前线程是否被中断;然而它却附带了一个致命的副作用:**它会同时清除当前线程的中断状态标记**。
- 很多开发者为了定位该副作用导致的中断信号丢失 Bug,耗费了数小时乃至数天的时间排查。
- Bloch 尖锐地指出,清除中断状态其实是该调用产生的第一主效应,检查状态反而是顺带的。该方法本应命名为 `clearInterruptStatus()`,并将其原先的状态作为返回值顺带返回。以次要特性作为方法名,完全违背了最小惊奇原则。
随后 Bloch 引出了“快速失败”(Fail Fast)理念:一旦程序出现错误,API 必须在第一时间向调用者告警。
- **编译期失败优于运行时失败**:错误最好在编译阶段就暴露出来,这意味着在编码阶段就能把 Bug 拦截在实验室,而不是漏到生产环境。Bloch 明确表达了自己对强静态类型系统的支持——尽管静态类型具有争议,但他在为 Java 引入泛型后,成功在已有代码库中筛出了大量潜藏的历史类型 Bug。
- **运行时尽早报错**:若错误只能在运行时被发现,那么在脏数据传入的第一瞬间就该抛出异常,而不是让这个不合法的脏数据在系统内部默默流通了十分钟后,在某个无关的角落引发灾难。演讲在此处引申到了 Properties 类在运行时对错误输入处理不当的具体缺陷上。
### [50:59](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=3059s) 属性映射与延迟崩溃:提供程序化访问的重要性
Joshua Bloch 指出,Java 中的 `Properties` 类在规范中声称是将字符串映射到字符串(maps strings to strings),然而其继承自 `Hashtable` 的 `put` 方法接收的却是 `Object key` 和 `Object value`。直觉上,如果传入非字符串,API 应当立刻报错,但事实并非如此。它允许开发者塞入任何垃圾数据,并在十分钟后,当调用 `save`(或类似输出流方法,将属性序列化为某种看似 XML 的格式)时,才突然抛出 `ClassCastException` 崩溃。此时由于时间滞后,调试变得极其困难,开发者根本无从排查究竟是哪一行代码在十分钟前存入了错误的对象。
紧接着,Bloch 强调了一个核心设计法则:任何以字符串形式返回的数据,都必须同时提供程序化访问(programmatic access)的 API。如果只给字符串,调用方就不得不去解析(parse)该字符串。这不仅给使用者带来极大的痛苦,更严重的是会让字符串的格式事实上升格为 API 的一部分(de facto API)。一旦以后更改字符串格式或添加新信息,就会直接破坏调用方的代码。因此,规范中必须明确声明不要依赖该字符串的格式,并向使用者提供提取具体字段的结构化接口。
```mermaid
flowchart TD
accTitle: 缺少程序化 API 导致的接口脆弱性
accDescr: 流程展示了仅返回字符串如何迫使客户端解析,并最终导致修改字符串格式时破坏现有代码。
A["API 仅提供字符串形式输出"] --> B["客户端编写自定义代码解析字符串"]
B --> C["字符串格式变成事实上的 API 契约"]
C --> D["开发者后续向字符串新增或调整信息"]
D --> E["客户端解析逻辑崩溃,破坏向后兼容性"]
A -.->|"正确做法"| F["同时提供结构化访问方法 (如 getStackTrace)"]
F --> G["客户端通过对象直接获取字段,解耦格式依赖"]
```
- **反面案例**:在早期 Java 中,获取堆栈轨迹的唯一途径是调用 `printStackTrace`,导致很多开发者被迫去解析打印出来的文本;直到 Java 1.4 才终于补充了 `getStackTrace` API,返回包含文件名、行号和类名的 `StackTraceElement`,但这已经是“马离开马厩之后才关门”(the horse had already left the barn)。
### [53:12](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=3192s) 慎用重载与类型选取陷阱
对于方法重载(overloading),Bloch 给出的忠告是必须格外谨慎。虽然重载有其价值,但极易被滥用。尤其要避免产生歧义的重载(ambiguous overloading)——即传入相同的对象却根据不同的编译时静态类型产生截然不同的行为。Bloch 甚至坦白承认了自己在 Java 标准库中的一次失误:“And a bad example, which I am guilty of here myself, is TreeSet has two constructors; one that takes Collection, one that takes a SortedSet.” 接收 `Collection` 的构造函数会忽略传入集合的排序方式,而接收 `SortedSet` 的则会保留原排序。如果开发者将一个 `SortedSet` 向上转型为 `Collection` 传入,得到的结果就完全不同。他反思道,当年应该使用动态测试(`instanceof SortedSet`)来统一维护排序规则,并总结道:“So, you know, the basic rule here is just because you can doesn't mean you should.” 很多时候与其重载,不如换一个清晰的方法名。
在参数和返回类型的选取上,Bloch 主张:
1. **优先使用接口而非具体实现类**。
2. **输入参数要尽可能具体**:如果方法名义上接收 `Collection`,但在运行时遇到非 `Set` 类型就会崩溃,那就是把编译期可以捕获的错误推迟到了运行时。
3. **不要使用字符串替代更优的类型**:Web 服务和 XML 让所有数据最初都以字符串形式流入,但绝不能让它们在系统里一直作为字符串流动。Bloch 回忆自己多年前见过一个程序,全生命周期都在传递内容为 `"yes"` 或 `"no"` 的字符串,而该场景显然应当使用 `boolean`。
4. **禁止使用浮点数表示金额**:由于二进制浮点数无法精确表示形如 1/10 的十进制小数,在涉及货币计算时绝对不能使用 `float` 或 `double`,而应当使用 `BigDecimal`、`long` 或 `BigInteger`。在浮点数之间进行选择时,几乎总应该倾向于选择 `double`,因为两者的执行速度通常相当,但 `float` 会损失至关重要的精度。
### [55:52](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=3352s) 参数列表设计:一致性、短小与规避特殊返回值
参数列表的设计直接关系到 API 的易用性与容错能力。Bloch 指出,跨方法的一致参数顺序(consistent parameter ordering)至关重要。他举了 Unix 系统中复制数据的经典反面教材:`strcpy` 接收 `(dest, src)`,而 `bcopy` 却接收 `(src, dest, size)`。一旦开发者记混,就会用目标数组中的垃圾数据覆盖源数据,且排查这种 bug 往往要耗费巨额时间。特别是当相邻参数具有相同类型时,顺序颠倒在编译阶段完全无法被察觉。
相反,良好的 API 会保持严苛的一致性:
- `java.util.Collections` 中被修改或操作的集合永远作为第一个参数。
- `java.util.concurrent` 中表示时间跨度时,永远是数值延迟在前、时间单位在后(如 `delay, TimeUnit`)。由于两者类型完全不兼容(`long` 与枚举),即便顺序写反,编译器也会立即拦截。
Bloch 提倡应尽量避免过长的参数列表,理想状态下应将其限制在三个以内。以 Win32 API 中创建窗口的函数为例,中间夹杂着长达 15 个参数,其中连续出现 `int x, int y, int nWidth, int nHeight` 以及大量包装为其他别名的整型。没有 IDE 强力提示,这种 API 几乎无法使用。缩短参数列表的手段包括拆分方法、引入辅助参数对象,或是采用建造者模式(Builder Pattern)——当构造函数或静态工厂需要传入多达十个参数且大多数参数具有默认值时,Builder 模式不仅书写简单,更具有极佳的可读性。
最后,Bloch 警告不要设计需要特殊异常处理的返回值:“In particular, you should never return a, 'null,' instead of a zero-length array or collections.” 他列举了 Java 内部的一个教训:`BufferedImageOp` 接口中的 `getRenderingHints` 方法在某些情况下返回集合,另外一些情况下返回 `null`。结果导致几乎所有调用该方法的代码都存在漏洞,因为返回 `null` 的几率极低,开发者往往忘记处理特殊情况,直到偶尔触发 `NullPointerException`。如果直接返回一个长度为零的集合,所有的繁琐检查和潜在空指针崩溃都将迎刃而解。
### [58:50](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=3530s) 总结与 OOPSLA 纪念卡片赠送
由于演讲时间已超额使用五分钟,Joshua Bloch 宣布结束幻灯片部分的讲解,并表示自己会在会后停留一段时间,欢迎台下观众随时上前交流讨论。
在结束前,Bloch 向在场听众分享了一份特别的赠品。他提到,当年在面向对象编程顶级会议 OOPSLA 发表此篇论文时,论文集(proceedings)允许他提交两页的扩展摘要(extended abstract)。当时他决定采用“汽车保险杠贴纸”(bumper sticker)的方式来总结 API 设计理念,将整场讲座的内容浓缩为大约 50 条短小精悍的技术箴言(例如“拿不准时,宁缺勿滥”/“When in doubt, leave it out”以及“所有程序员都是 API 设计师”/“All programmers are API designers”),并为每条原则配上一句简短的解释。他将打印出来的卡片分发给现场听众互相传阅,并表示会将电子版本上传至内部系统,方便大家下载。演讲在现场观众的掌声中圆满落幕。
## Notable quotes
> [24:19](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=1459s) When in doubt, leave it out. — Joshua Bloch
> [15:03](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=903s) Example programs should be exemplary. — Joshua Bloch
> [39:42](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=2382s) Classes should be immutable unless there's a very good reason to make them mutable. — Joshua Bloch
> [19:14](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=1154s) If you try to please everyone, you come up with a pig. You come with big, nasty APIs that no one will ever be able to use properly. — Joshua Bloch
> [20:46](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=1246s) If it's not easy to explain, then it's not doing one thing and doing it well. — Joshua Bloch
> [32:22](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=1942s) If you had side effects and you don't document them, people will shoot themselves in the foot. — Joshua Bloch
> [55:52](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=3352s) In particular, you should never return a, 'null,' instead of a zero-length array or collections. — Joshua Bloch
> [35:31](https://www.youtube.com/watch?v=aAb7hSCtvGw&t=2131s) Design your APIs for the long haul. Luckily, good API design usually coincides with good performance. — Joshua Bloch
## Mentioned
Joshua Bloch, Jeremy Manson, Google, Sun Microsystems, Transarc, OOPSLA, David Parnas, Jon Bentley, Will Tracz, Confessions of a Used Program Salesman: Institutionalizing Software Reuse, Design Fragments, Fairbanks, Garlan, Scherlis, RSADSI, W3C DOM, Java Collections Framework, JSR 310, Joda-Time, Win32 API
## Video description
Google Tech Talks
January 24, 2007
ABSTRACT
Every day around the world, software developers spend much of their time working with a variety of Application Programming Interfaces (APIs). Some are integral to the core platform, some provide access to widely distributed frameworks, and some are written in-house for use by a few developers. Nearly all programmers occasionally function as API designers, whether they know it or not. A well-designed API can be a great asset to the organization that wrote it and to all who use it. Good APIs increase the pleasure and productivity of the developers who use them, the quality of the software they produce, and ultimately, the corporate bottom line....
[Source](https://www.youtube.com/watch?v=aAb7hSCtvGw)
Source quotes
[24:19] Joshua Bloch: When in doubt, leave it out.
[15:03] Joshua Bloch: Example programs should be exemplary.
[39:42] Joshua Bloch: Classes should be immutable unless there's a very good reason to make them mutable.
[19:14] Joshua Bloch: If you try to please everyone, you come up with a pig. You come with big, nasty APIs that no one will ever be able to use properly.
[20:46] Joshua Bloch: If it's not easy to explain, then it's not doing one thing and doing it well.
[32:22] Joshua Bloch: If you had side effects and you don't document them, people will shoot themselves in the foot.
[55:52] Joshua Bloch: In particular, you should never return a, 'null,' instead of a zero-length array or collections.
[35:31] Joshua Bloch: Design your APIs for the long haul. Luckily, good API design usually coincides with good performance.
Source quotes
[48:31] Joshua Bloch: The user of an API should never be surprised by its behavior.
[24:19] Joshua Bloch: You'll probably be able to add it later, but you most certainly will not be able to take it out once you put it into an API.
[21:45] Joshua Bloch: The names are the API talking back to you, so listen to them.
[5:47] Joshua Bloch: It is not the case that the more powerful an API is, the better it is. It should basically be just powerful enough to do what it needs to do.
[42:01] Joshua Bloch: I believe that you should design and document a class for inheritance or else prohibit it outright, to make the class final or have no publicly accessible constructor.
[38:06] Joshua Bloch: Transliterated APIs are almost always broken.
[3:26] Joshua Bloch: Whether you're designing a language or an API, you are creating a tool for programmers to express their intent to the machine and to other programmers, who read the program, maintain it, modify it and so forth.
[48:31] Joshua Bloch: Whenever there's an error, you should tell the user of your API as soon as possible after the error has happened.
Original transcript
[0:23] >> First, I would like to thank you and welcome
you all to the latest in our series of talks
[0:27] on advance topics and programming languages.
[0:29] The purpose of these series of talks is to
expose all of the great domain knowledge in
[0:33] programming languages that we have at Google.
[0:36] So that--what that means is that everybody
in this audience, all of you geniuses who
[0:39] work for Google and myself discounted, should
give a talk.
[0:44] So please, come to me, my name Jeremy Manson
email me, IM me whatever and tell me what
[0:50] talk you are going to give and I will set
it up and give you a talk.
[0:52] You don't have to be an author like Josh or
some of the people we have coming up in order
[0:57] to do so.
[1:00] Josh--and we're running late.
[1:01] Josh didn't want me to build him up too much
and give him a swell head.
[1:05] So, I want to introduce him.
[1:07] I'll just say that in a company full of geniuses,
it's his star that shines probably the most
[1:12] brightly.
[1:14] And I just want to...
[1:16] >> BLOCH: I'll get you for that.
[1:18] >> I just want to introduce this man whose
boot heel I am not worthy to lick, Joshua
[1:25] Bloch ladies and gentlemen.
[1:29] >> BLOCH: Normally at this point, I thank
my introducer for the introduction but today
[1:37] I think I'll dispense with that formality.
[1:39] So, I should--I should also say by the way
that this is unfortunately rather a long talk.
[1:46] I try to have short talks with one or two
key ideas but this subject matter just doesn't
[1:51] lend itself to that.
[1:52] So, please hold your questions till the end
and in fact, we'll probably use up the hour
[1:56] without questions but then I'll hang around
for as long as you want and answer all the
[2:00] questions that you have.
[2:02] So, why is API design important?
[2:05] Well, APIs can be among a company's greatest
assets.
[2:10] A good API is something that people invest
heavily in.
[2:14] They do this in obvious ways and in less obvious
ways.
[2:17] Obvious ways are may be a by product built
around the APIs.
[2:22] They write to the API.
[2:23] But the less obvious ways is they learn it.
[2:25] They spend hours and hours--actually, they
spend months learning the APIs.
[2:29] And once they've have done that, you know,
they don't want to learn a new one because
[2:33] they have to unlearn everything they know
and replace it with something else.
[2:37] And furthermore, the API is just wired throughout
the infrastructure at a company.
[2:42] So, a successful API can make a company, can
give a franchise that last.
[2:46] So--I guess it's about twenty-five years now
and similarly a bad API can be among a company's
[2:55] greatest liabilities.
[2:56] And there are a couple of reasons for this.
[2:59] First of all, a bad API can cause an unending
stream of, you know, support phone calls because
[3:05] people cannot make the thing do what it ought
to do and it can inhibit a company's ability
[3:11] to move forward because once you have a bad
API you cannot change it at will.
[3:15] You're pretty much stuck with it forever.
[3:18] You have one chance to get it right.
[3:20] So, that's pretty scary and with that in mind,
you want to learn how to make APIs that will
[3:24] stand the test of time.
[3:26] So, now you know why API design is important
but why is it important to you?
[3:31] Not all of you may think of yourselves as
API designers.
[3:34] Well, it turns out that all of you are API
designers.
[3:38] Anyone who programs a computer is an API designer.
[3:42] And the reason is that good programming is
inherently modular and these inter-modular
[3:47] boundaries are themselves APIs.
[3:51] Furthermore, good APIs tend to get reused.
[3:54] If you've written a module and it's good at
doing something, you now, one of these days
[3:59] one of your co-worker is going to need to
do the same thing.
[4:01] And gosh, you know you've already got this
module that does it but once he's using that
[4:05] API, you are no longer free to change it at
will because if you change it you'll break
[4:09] his program.
[4:10] And if he has ten friends and they all start
using it then you're really hosed.
[4:15] Finally, thinking in terms of API design tends
to improve the quality of the programs that
[4:21] you write.
[4:22] It tends to sort of keep you from just hacking
things together.
[4:25] It tends to make you want to write nice units,
you now, that are--that are composable, that
[4:30] are reusable and that are sensible.
[4:32] Now, one other question we've got to get out
of the way at the beginning is, why am I talking
[4:37] about API at what is billed as a language
design series?
[4:42] And the glib answer of course is that Jeremy
asked me to do it.
[4:45] And Jeremy used to be friend, so, of course,
I said, "Yeah, of course I'll do it."
[4:51] But in fact the real answer is that API design
and language design are very, very similar.
[4:55] The only real difference is that API design
is constrained by the syntactic--sorry, the
[5:01] syntax of the language for which you're writing
the API.
[5:04] Whereas, when you're designing a language
you have the flexibility to do anything you
[5:07] like with the syntax.
[5:09] But in fact, whether you're designing a language
or an API, you are creating a tool for programmers
[5:16] to express their intent to the machine and
to other programmers, who read the program,
[5:21] maintain it, modify it and so forth.
[5:24] Finally, these days you don't really think
in terms of a language or a library alone.
[5:31] A language and a library together comprise
a platform and when you're designing a language,
[5:37] you design the core libraries hand in hand
with the language.
[5:42] So, really the skill set for designing good
APIs and for designing languages is pretty
[5:46] much the same.
[5:47] So, what are the characteristics of a good
API?
[5:51] First of all, it's easy to learn and it's
easy to use, even without documentation.
[5:56] So, a good API should be easy to memorize.
[5:59] It should just plain make sense.
[6:02] And the flip side of that is, not only should
it be easy to use a good API but it should
[6:06] hard to misuse a good API.
[6:09] It should be hard or impossible to use--misuse
a good API.
[6:14] That is basically, a good API should simply
force you to do the right thing.
[6:18] It should be easy to read and to maintain
code written to that API.
[6:22] The API should be sufficiently powerful to
do what it has to do.
[6:27] Note that I didn't say the API should be powerful.
[6:30] It is not the case that the more powerful
an API is, the better it is.
[6:35] It should basically be just powerful enough
to do what it needs to do.
[6:39] But it should be easy to evolve the API over
time because there will be new needs later
[6:44] on.
[6:45] So, what you want to do is want to write an
API that meets its requirements and that can
[6:48] evolve to meet future requirements.
[6:50] And finally, the API has to be appropriate
to the audience.
[6:53] What is a good API for let's say a Wall Street
analyst is probably not a good API for a physicist
[7:03] because they have different terminology.
[7:05] They think differently.
[7:06] So their API has to be aimed at its audience.
[7:10] So now, we know what the characteristics are.
[7:12] How do we achieve them?
[7:13] And that's what's the rest of this talk is
about.
[7:15] The talk is divided into five sections.
[7:18] The first one is on the process of the API
design.
[7:20] I'm not a big process weenie but I found over
the years that there are certain things that
[7:25] all good API designs have in common in terms
of the process used to create them.
[7:29] So, I'll try to get over that.
[7:30] Then, the general principles of API design,
then those principles as they apply to classes,
[7:35] as they apply to methods, and as they apply
to exceptions.
[7:37] And finally, if I have time I'll show a couple
of refactorings where we improve API designs.
[7:42] So, what is the process of API design like?
[7:46] Well, the first thing you got to do is you
got to gather the requirements but do it with
[7:50] a healthy dose of speck--of skepticism.
[7:54] Because often when you ask people, you know,
"Well, what does this API have to do for you?"
[7:59] What you'll get won't be a real set of requirements,
it'll be a set of proposed solutions and a
[8:05] better solution may exist.
[8:06] So, you know, if someone tells you, let's
say, "We need to precisely control the garbage
[8:12] collection intervals and the maximum time
that each garbage collection can take."
[8:17] You know, that's not really a requirement.
[8:19] I mean, the requirement is, you know, we need
to be able to run a server smoothly while
[8:24] any garbage collection takes place.
[8:26] How you choose to achieve that is up to you.
[8:30] Your job is to extract the requirements from
the stakeholders in your API and often it's
[8:37] real give and take process.
[8:39] Once you've got the requirements, they should
take the form of used cases, and by used cases,
[8:44] I simply mean, the problems that your API
should be able to solve.
[8:48] And these are extremely important because
they provide the benchmark against which you
[8:52] can measure any proposed solution.
[8:55] One thing you should keep in mind is that
it can be easier and more rewarding to build
[9:00] a more general solution than what you've been
asked to do.
[9:02] This doesn't mean you should just say, "Oh,
I'm going to build a framework," every time
[9:05] someone asks you to do something.
[9:07] But sometimes the more specific thing is more
difficult to build as well as being less powerful.
[9:13] So, always keep an open when you're--when
you're looking at those initial requirements.
[9:17] Oh, let's see, I guess I have another example
here of what they say and what they mean.
[9:22] When they say, "We need new data structures
and RPCs with the Version 2 attributes," this
[9:25] actually happened to me at a company called
Transarc, you know, when we were kind of upgrading.
[9:29] They said, you know, "Make a whole new set
of data structures and a whole new set of
[9:32] APIs."
[9:33] What they really meant was, "We need a new
data format that will accommodate all further
[9:38] evolution in the internal data structures."
[9:41] You know, because you don't have to want to
have to make a whole new set of data structures
[9:44] and a whole new set of on the wire and on
disk interfaces every time you decide to add
[9:49] a few attributes to your server.
[9:50] So in fact, I made the system much more dynamic
and we never had to do that again.
[9:55] So, you should start with a really short spec,
one page is ideal.
[10:01] At this stage in an API design, agility definitely
trumps completeness.
[10:05] The worst thing you can do is to send six
smart guys off into a room and have them sit
[10:11] there with the door closed for six months
and come out with a 240 specification document.
[10:17] And believe me this has been done many, many
times.
[10:19] It's an awful idea because at that point,
first of all, their ego is invested in what
[10:23] they've just done.
[10:24] They're going to build it even if it's a piece
of crap.
[10:27] And second of all, you know, how do you know
if it's any good?
[10:32] It's like this big, long, hairy spec.
[10:34] It's no longer agile.
[10:35] If they made a fundamental mistake, then you've
got to change all 240 pages of it.
[10:40] But it may fail to satisfy some sort of key
requirement that they didn't really understand
[10:45] before they started.
[10:46] So, what you want to do in the beginning is
have the entire API spec on one page.
[10:51] In this way, you can bounce that spec off
as many stakeholders as possible.
[10:55] Listen to what they have to say and take it
seriously.
[10:57] If they say, "No, I'm sorry, this won't do
for me because I cannot write such and such
[11:01] a program," think about it.
[11:03] You know, you may say, "Well, you shouldn't
be writing that kind of program.
[11:06] It's a really bad idea."
[11:07] But more likely, you may say, "Oh gee, I didn't
think of that.
[11:11] This really is important.
[11:12] What if we structure it this way?"
[11:13] The whole thing is only a page long.
[11:15] You can do major refactorings in ten minutes.
[11:17] If you keep the spec short, it's easy to modify.
[11:20] And you flesh that spec out only as you gain
confidence that you're on the right track.
[11:25] And this necessarily involves coding.
[11:28] In particular, it involves coding to the API
that you are defining.
[11:33] It doesn't involve implementing the API.
[11:35] It involves pretending it's already been implemented.
[11:38] So, what does it look like when you do it
right?
[11:40] Well, here's an example that I was writing
at about the time that I was putting this
[11:44] talk together for OOPSLA some number of months
ago.
[11:49] Someone wanted the ability to retry a computation
in the face of failure.
[11:53] And I said, "Oh well, you know, we have this
executor framework, otherwise known as the
[11:57] executor framework."
[11:59] And really, all we want here is a retry policy
that tells you how you might choose to retry
[12:06] the thing in the face of failure.
[12:07] So, you know, here's a little interface and
it's got a couple of methods.
[12:12] One tells you, if a given failure is recoverable.
[12:15] You pass in the exception and it just gives
you true or false.
[12:18] We should try to recover from this one or
we shouldn't.
[12:21] And the second one computes the next delay
in terms of, the initial start time and the
[12:27] number of previous retries and by passing
in all these data, the actual retry policy
[12:33] can be stateless.
[12:34] So, you can have singletons.
[12:36] You can have a retry policy called exponential
backoff.
[12:38] But you're going to have to store any data
in each exponential back-off instance.
[12:42] It really is just a retry policy and it's
called exponential backoff.
[12:46] And that's kind of all there is to that one.
[12:48] And this isn't really very complete.
[12:49] This is not, you know, a spec of a quality
that I could use for Java doc.
[12:54] It's not a spec that someone could use to
implement to but it's a spec that's good enough
[12:58] for someone to look at it and say, "Yes.
[12:59] It does do what I need or it doesn't."
[13:01] The rest of it is on another slide here.
[13:04] This is a set of static utilities that lets
you actually use retry policies.
[13:09] So, what can you do?
[13:11] Well, the first thing you can is you can pass
in an executor service and retry policy and
[13:16] get a retrying executor service.
[13:17] It implements the same interface, which is
executor service.
[13:20] So, if you already know how to use an executor
service, you know how to use the retrying
[13:24] executor service and that's great by the way.
[13:26] That's a good way to keep the sort of the
conceptual weight of an API small.
[13:30] Use interfaces that have already been designed--defined,
in this case, executor service.
[13:36] And what else do we have?
[13:37] We have another kind of a retrying executor
service.
[13:41] What are--what are the difference between
the first two?
[13:42] I haven't looked at this for a while so, I
apologize for that.
[13:46] Anyway, it doesn't--it doesn't matter.
[13:50] And then, we have a couple of wrappers, one
of which takes a callable and returns a retrying
[13:54] callable and takes a runnable, returns to
retrying runnable.
[13:57] And then, we have a couple examples of the
retry policies themselves.
[14:01] These are static factories, we have, if you
want an exponential backoff, you get one.
[14:05] And these are the parameters that describe
your exponential backoff.
[14:09] What's the initial delay?
[14:11] In what unit, you know, 10 seconds, a hundred
milliseconds, whatever and then, the timeout,
[14:21] which I'm not sure what that means.
[14:22] So, by the way, this is actually interesting.
[14:24] This tells me this wasn't quite good enough,
you know.
[14:26] What I wanted to show you is that something
that's really simple.
[14:29] It fits on the slide or two, is enough to
communicate your intent and enough to sort
[14:32] of, figure out whether it's good enough and
you know, try it out.
[14:37] And I think the answer here is it's almost
good enough but not quite good enough.
[14:41] I guess, the timeout probably, is the overall
timeout.
[14:44] Like after how long of trying and retrying
do you finally give up.
[14:49] But I think it should have said that somewhere
on this.
[14:52] But anyway, you get the idea.
[14:53] The idea is that this is a very small description
of an API.
[14:57] But it's big enough to find out if it's good
enough to do what needs to be done.
[15:00] And if it's not, it's easy to modify it.
[15:03] You should write to the API early and often.
[15:05] Of course, you should start before you've
implemented the API because this saves you
[15:09] from having to throw away an implementation
of a bad API.
[15:13] You know if you--if you first specify it,
then implement it, then try the implementation
[15:17] and decide that the API was garbage, well,
you've wasted lots of time implementing it.
[15:21] And as I said, you should start before you've
even specified it properly because that saves
[15:25] you from having to throw away detailed specifications
for broken APIs.
[15:30] You should continue writing to the API as
you flesh it out and this is important.
[15:35] Some people, sort of stop writing to the API
about halfway through the process and just
[15:40] go on this death march to implementation.
[15:42] The problem with that is you get nasty surprises
about a week before you ship when you try
[15:46] writing to it again and you find that, you
know, "Oh, my gosh.
[15:49] It actually doesn't solve this important case
that we thought it solved."
[15:53] And some people worry.
[15:54] They worry that, you know, that all these
coding to the API is a waste of time when
[15:58] they should be implementing it.
[16:00] But that nothing could be further from the
truth.
[16:02] Those initial pieces of code that you write
to any API are among the most important pieces
[16:07] of code that you'll ever write to it.
[16:09] The code lives on in the examples that you
publish for how to use that API.
[16:17] And those examples tend to get emulated heavily.
[16:20] If you get them right, you seeded the market
with good uses of your API.
[16:24] If you get them wrong, conversely, you know,
you've ensured that there will be broken programs
[16:29] floating around the web for the next ten years.
[16:32] And I used to, you know believe this with
all my heart and soul.
[16:35] But now, I actually can point to a proof of
it.
[16:37] It turns out in the last OOPSLA, there was
a paper published called Design Fragments
[16:42] by Fairbanks, Garlan and Scherlis from CMU.
[16:44] And they actually traced mistakes in the original
applets that were shipped out with, you know,
[16:51] the first release of Java into broken concurrent
programs, thousands of them that still exist
[16:57] on the web today.
[16:58] So, you know, the way I put this is, example
program should be exemplary.
[17:03] There's a reason they call them example programs.
[17:05] And the programs that--the first programs
that you write to an API, as you are fleshing
[17:09] it out, invariably become the example programs.
[17:13] You know, so, you know, my rule of thumb is
you should spend ten times as much time on
[17:17] every line of example code as you do production
code.
[17:20] That may sound backwards to you but I really
believe it.
[17:24] Writing to an SPI, that is a Service Provider
Interface is even more important than writing
[17:29] to any other kind of API.
[17:31] You all probably what SPIs are or maybe you
don't.
[17:34] If you know an SPI is, raise your hand.
[17:37] Okay.
[17:38] So, an SPI is a special kind of an API which
you use to provide a new means of doing something.
[17:45] It's not the API which the programmers write
to rather, it's the plug-in API that, let's
[17:51] say, lets--RSADSI Incorporated provides their
encryption methods and Sun Microsystems provide
[17:57] their encryption methods.
[17:58] So, the user of this encryption API can use
a higher level API, which then dispatches
[18:03] to these encryption methods.
[18:04] And the thing about SPIs is that you're supposed
to be able to hide very, very different implementations
[18:11] underneath them.
[18:12] And if you, you know, write an SPI and you
only have one provider it turns out that is
[18:17] a practical matter, you will probably never
be able to support another.
[18:21] Once you try to do the second, you'll find
that there's something about that SPI that
[18:25] ties it forever to the only implementation
you actually thought about.
[18:29] If on the other hand, you do two implementations
rather than one.
[18:32] Then, you'll probably be good enough.
[18:34] You'll probably be able to support subsequent
ones with some difficulty.
[18:38] But if you do three, it will probably work
fine for any number.
[18:41] If it works for three, the fourth won't all
be that different from each of the first three.
[18:46] I found this out myself and then, I saw it
in a book, Will Tracz discovered this and
[18:50] called it The Rule of Threes in a book, which
was subtitled Confessions of a Used Program
[18:55] Salesman because it was a book about a software
reuse.
[18:58] And by the way, here we explain the subtle
coding that is used throughout this talk.
[19:03] Whenever you see something green, it means
this is good, do it this way.
[19:07] And when you see something in red, it means
this is bad, don't do it that way.
[19:14] You should maintain realistic expectations
throughout the process of API design.
[19:17] It turns out that most API designs are over
constrained.
[19:21] People want them to do more than they can
possibly do.
[19:23] So, you are going to have to make compromises.
[19:26] You cannot please everyone.
[19:28] If you try to please everyone, you come up
with a pig.
[19:31] You come with big, nasty APIs that no one
will ever be able to use properly.
[19:35] So, what you should do?
[19:36] And this may sound strange, is you should
aim to displease everyone equally.
[19:42] The idea is that, you know if one of your
important stakeholders is very displeased
[19:48] and the others are really pleased, that's
probably a problem because your API isn't
[19:52] doing something it has to do.
[19:54] If everyone is like less than a 100% happy
but they're all happy enough then, you've
[19:58] probably done the right thing.
[20:00] But do not misinterpret this as saying I favor
design by committee and you should take everyone's
[20:04] ideas and mush them altogether.
[20:06] You do need one sort of strong design lead
that can ensure that the API that you're design
[20:11] is cohesive and pretty and clearly the work
of, you know, one single mind or at least
[20:19] a single minded body.
[20:21] And that's always a little bit of a trade
off, being able to satisfy the needs of many
[20:24] costumers and yet produce something that is,
you know, beautiful and cohesive.
[20:30] Expect to make mistakes.
[20:31] API design is hard.
[20:32] Now luckily, a few years of real world use
will always flush out the mistakes.
[20:37] Unfortunately, by that time it's like too
late to do anything about them.
[20:40] Although, you can write nice talks and tell
people about the mistakes so they don't make
[20:43] them again and it's kind of what I'm doing
here today.
[20:46] So, given that you're going to make mistakes
and you're going to be stuck with the original
[20:50] API, write the API so that at least you can
sort of add to it and produce something that
[20:54] will help you get around the shortcomings
of your original designs.
[20:58] The recent example of that in my life is in
the collections API which I did around 1997,
[21:04] 1998.
[21:05] There were some real flaws in the sorted sets
and sort of map implementations.
[21:09] In particular, you know, they're a little
bit asymmetric.
[21:11] It's much easier to a forward than backward
and there are couple other things.
[21:16] I knew about these flaws at that time but
I didn't know how to fix them.
[21:19] However, we were able to extend that API.
[21:21] So, if you look at the most recent release
of Java 6.
[21:24] You'll see something called the Navigable
Set and Navigable Map, which extends sorted
[21:28] set and sorted map and provide, you know,
additional methods that basically fix those
[21:34] difficulties.
[21:35] You know and it's not a perfect solution because
there are some things that implement the old--the
[21:38] older interfaces and they're not fixed yet.
[21:41] But at least all the standard collection implementations
from Sun now are fixed.
[21:45] So, what are the general principles of good
API design?
[21:49] First all, an API should do one thing and
do it well.
[21:52] And I should say that almost all of what I'm
going to say for the next five minutes may
[21:57] just sound like motherhood and apple pie.
[22:00] But, there's more to it than that.
[22:02] I'm going to try to give you actionable advice.
[22:04] I'm going to try to take the sort of the standard
old souls like in API should do one thing
[22:08] and do it well and see what it really means
and tell you how to achieve it.
[22:12] So, you know, in this case, the functionality
should be easy to explain.
[22:18] If it's not easy to explain, then it's not
doing one thing and doing it well.
[22:23] It's a mess.
[22:24] If you can't come up with a good name for
it, then it's a mess.
[22:29] The names are the API talking back to you,
so listen to them.
[22:33] When you try to name those methods and those
classes, you know, if you come up with the
[22:37] really complicated name like DynAnyFactoryOperations
or underscore BindingIteratorImplBase switch,
[22:44] you know, actually violates the naming conventions
of platen--platform, sorry.
[22:49] ENCODING_CDR_ENCAPS, you know, clearly you've
got problems.
[22:54] Any API that contains this sort of stuff is
a mess.
[22:57] Oh, what about this, OMGVMCID?
[22:59] I know OMG is, "Oh, my god," but I can't--I
can't figure out the rest of it.
[23:04] And by the way least you think I'm just making
this stuff up, all of these comes from an
[23:08] actual Java platform API.
[23:10] And I won't tell what it is except to say
that it's club Good API names are like a font.
[23:18] Yeah, I know what a font is.
[23:20] Sure, you know, it's like, it's italic or
bold or whatever.
[23:23] You know, a set, I know what a set is.
[23:25] A private key, a lock, a ThreadFactory, these
things are, you know, they instantly communicate
[23:32] what they are.
[23:33] And the methods, you know the classes all
of them should be like that.
[23:37] Looking at them, you know, it should be clear
what they are.
[23:39] And good names drive good designs.
[23:42] You know, once you have something that's called
a set.
[23:44] You know what the operations are.
[23:46] You insert things from sets, you remove, you
test for containments.
[23:50] So, good names drive good designs and bad
names are an indication of bad designs.
[23:56] So, listen to those names speaking to you.
[23:58] And if you just you can't get it to work out
right, then you probably not trying to build
[24:02] something reasonable.
[24:03] So, always remain amenable to splitting a
module up if you're trying to cramp too much
[24:08] into a module, or to putting multiple modules
together, if you're trying to expose sort
[24:13] of internal details that ought to be hidden.
[24:15] Maybe you should just make a bigger module
that hides some of those details.
[24:19] An API should be as small as possible but
no smaller.
[24:23] This principle is usually attributed to Einstein.
[24:25] Although I looked really, really hard and
I don't believe he ever said it.
[24:29] I believe that it's, you know--he probably
believed it but he didn't say it.
[24:33] At any rate the API should satisfy its requirements
and if you only remember one thing from the
[24:39] talk today, please remember this.
[24:42] When it doubt, leave it out.
[24:45] That applies to everything.
[24:46] It applies to functionality, to classes, to
methods, to parameters within a method, anything.
[24:51] If you have any doubts about whether to include
something, leave it out.
[24:57] You'll probably be able to add it later, but
you most certainly will not be able to take
[25:00] it out once you put it into an API.
[25:03] Once you put it in people will be using it,
if you take it out they will scream bloody
[25:06] murder.
[25:07] So, if you ever have any doubts about whether
to include something, leave it out.
[25:11] And if that's all you take away with you from
today's talk, then, I think it's been worth
[25:15] your hour.
[25:16] Of course, that's just my judgment.
[25:18] Anyway, the--when you're thinking about the
size of an API, the conceptual weight is more
[25:23] important than the bulk.
[25:24] By bulk, I mean the number of methods, the
number of class, the number parameters.
[25:29] What's really important is the number of concepts.
[25:32] When I'm learning this API, how many different
things do I have to learn about?
[25:36] And there are a number of ways to decrease
the conceptual weight of an API.
[25:41] The most important one is reusing interfaces.
[25:44] So, for example, if you look at the collection's
framework, there are many implementations
[25:48] of the set interface, you know, whether it's
TreeSet.
[25:51] The original ones were, you know, a HashSet,
TreeSet and then we add a LinkedHashSet and
[25:56] more recently a whole slew of concurrent set
implementations.
[25:59] You don't have to learn any new APIs.
[26:01] You learn the Set API and we can add new functionality,
you know, we can richness without making you
[26:07] learn any new API.
[26:08] So, that's one of the great ways to increase
the power to weight ratio.
[26:12] That's the important thing.
[26:13] You want to be able to do a lot without learning
a lot.
[26:19] The implementation shouldn't impact the API.
[26:21] Once again this is motherhood but what does
it really mean?
[26:25] It means don't put any implementation details
into the API.
[26:29] They confuse the users and they inhibit their
freedom to decide.
[26:32] They inhibit the implementer's freedom to
change the API later.
[26:35] So, you know, for example, let us say, if
you have some API that's about phone numbers
[26:41] but it throw a sequel exceptions and now you
want to re-implement it on top of some proprietary
[26:48] data store rather than a sequel data store.
[26:52] But your clients are already trying to catch
the sequel exceptions.
[26:55] You know, what do you do?
[26:56] Well, you can emulate those olds sequel exceptions
but that's crazy.
[26:59] So that means that you should make sure that
the exceptions that you throw are kind of
[27:03] at the same layer of abstraction as the rest
of the API is.
[27:09] That's just one example where implementation
details kind of leak into APIs.
[27:13] The important thing is you have to be aware
of what actually is an implementation detail.
[27:18] You don't want to over specify your methods.
[27:21] You don't want the specification of a method
to involve something that is in the implementation
[27:27] detail and that you would like to be able
to change later on.
[27:30] So, here's an example where we did that.
[27:33] Don't specify your hash functions.
[27:36] You might think that, you know, exactly what
value is returned by the hash code method
[27:40] is a proper part of a spec but it isn't.
[27:42] It's an implementation detail.
[27:43] The spec should simply say it returns an integer
and, you know, with high probability the integer
[27:48] will differ for two different objects and
furthermore it should be cheap to calculate
[27:52] the thing.
[27:53] But exactly what number is returned you should
have the flexibility to change that from release
[27:56] to release as you learn about flaws in your
old hash functions and as the technology improves
[28:03] and hash functions improve.
[28:04] Of course, you cannot do that if you're writing
a persistent data store.
[28:07] If those hash functions are going to be use
to store data on disk then they can't change.
[28:11] But that's a very special kind of hash function.
[28:14] Those ones must be specified but the great
majority of hash functions out there shouldn't
[28:18] be.
[28:19] And we got this very wrong in initial releases
of Java and unfortunately that tradition has
[28:23] stuck to the point where almost all of these
hash functions are specified.
[28:27] But they really shouldn't be.
[28:28] Finally, you shouldn't let the implementation
details kind of just leak into the API.
[28:32] The example I gave you before with an exception
is an example where, you know, you didn't
[28:38] really think hard about them and say, "Oh
yeah, you know, we should maybe throw a sequel
[28:41] exception here."
[28:42] You probably just wrote it and you realized,
"Gee, I'm calling something that throws a
[28:45] sequel exception, so I have to propagate it
out."
[28:48] That's a case where an implementation detail
is just sort of leaking.
[28:52] Another example, a really notorious example
in Java is, if you simply say, "Implements
[28:58] serializable."
[29:00] Once you've done that, your entire implementation
has just sort of leaked out as part of your
[29:06] API because the serial form consists of the
entire field that comprise your object even
[29:13] your private fields, so all of the sudden
the private fields are part of the public
[29:17] API and that's really, really bad.
[29:20] And the way around that by the way is to design
your serial forms carefully.
[29:23] Don't just say implement serializable.
[29:26] You should minimize the accessibility of everything.
[29:30] That means you should make your classes, your
members, your fields, all, as private as possible.
[29:36] One specific case is that public classes should
have no public fields with the exception of
[29:40] their constants, which aren't really fields.
[29:44] This maximizes what they call information
hiding.
[29:47] Parnas is the guy who came up with that term.
[29:49] And it minimizes--minimizes the coupling between
APIs.
[29:53] You know, if things are kind of hidden behind
inter-modular boundaries, they can be change
[29:59] freely.
[30:00] And this allows modules to be understood,
to be used, to be built, to be optimized,
[30:05] debugged, tested, and what have you individually
and in parallel.
[30:09] So you can have multiple teams, you know,
dealing with multiple APIs concurrently.
[30:14] If on the other hand the APIs sort of expose
everything and each, you know, module is sort
[30:19] of messing around with other modules, then
there is very little that you can do to any
[30:23] module without affecting a whole slew of modules
around it.
[30:28] Names matter a lot.
[30:29] There are some people who think that names
don't matter and, you know, when sit down
[30:33] and you say, "Well, this isn't named right."
[30:35] They say, "Don't waste your time.
[30:36] Let's just move on.
[30:37] It's good enough."
[30:38] Names in an API that are going to be used
by anyone else and that includes yourself
[30:44] in a few months, matter an awful lot.
[30:48] The idea is that every API is kind of a little
language and people who are going to use your
[30:54] API have to learn that language and then speak
in that language and that means names should
[30:58] be largely self explanatory.
[31:00] You should avoid cryptic abbreviations.
[31:02] So the original Unix names I think, failed
this one miserably.
[31:07] You should be consistent.
[31:08] It's very important that same word means the
same things when you used repeatedly in your
[31:12] API.
[31:13] And that you don't have multiple words meaning
that same thing.
[31:15] So let us say you have a remove and a delete
in the same API.
[31:19] That's almost always wrong.
[31:20] You know, what's the difference between remove
and delete?
[31:22] Well, I don't know.
[31:23] When I listen to those two things, they seem
to mean the same thing.
[31:26] If they do mean the same thing, then call
them both the same thing.
[31:30] If they don't, then make the names different
enough to tell you how they differ.
[31:33] If they were called, let's say delete and
expunge, I would know that expunge was a more--a
[31:38] permanent kind of removal or something like
that.
[31:41] Not only should you strive for consistency
but you should strive for symmetry.
[31:44] So, if your API has, let's say two verbs,
add and remove, and two nouns, entry and key,
[31:52] I'd like to see, you know, add entry, add
key, remove entry and remove key.
[31:56] If one of them is missing, there should be
a very good reason for it.
[31:59] I'm not saying that all API should be symmetric
but the great bulk of them should.
[32:02] And if you get it right, the code will read
like prose.
[32:06] That's the prize.
[32:07] So, you know, in this case the code reads,
"If the car's speed is more than twice a speed
[32:11] limit and the speaker should generate in alert
that says watch out for cops."
[32:15] That's pretty much English.
[32:16] It reads like prose and that's an indication
that API is reasonably decent.
[32:22] Documentation matters as well.
[32:23] And Parnas, the aforementioned Parnas said
it much better than I could, so I'm simply
[32:27] going to read what he had to say.
[32:29] He said, "Reuse is something that is far easier
to say than to do.
[32:34] Doing it requires both good design and very
good documentation.
[32:39] Even when we see good design, which is still
infrequently, we won't see the components
[32:42] reused without good documentation."
[32:45] He said that in 1994.
[32:47] And I don't know about you but, you know,
when I--when I read that I get religion.
[32:51] And the only thing I can do then is to document
religiously.
[32:55] Document every single class, every interface,
every method, every constructor, every parameter
[32:59] and every exception in my public API.
[33:02] Of course all of you do that, right?
[33:05] And when you go out on the web, whenever you
look Javadoc, it's always the case that every
[33:10] public or protected method has a Javadoc comment,
right?
[33:14] No.
[33:15] You know, it's really terrible because if
you don't have a comment telling you what
[33:19] the specification is, what is the specification?
[33:22] Who knows?
[33:24] You have two choices.
[33:25] Either you guess, in much case your program
probably doesn't work, or you read the code
[33:30] in which case, the implementation becomes
the specification and it's over specified
[33:35] and you no longer have the freedom to change
that implementation at all.
[33:39] So document everything and there are--you
know, what does this mean for classes?
[33:43] Just tell me what an instance of that class
represents.
[33:46] From methods, tell me the contract between
the method and its client.
[33:50] That is what must be true before I call it.
[33:52] What will be true after it returns and any
side effects.
[33:55] Those are particularly important.
[33:56] If you had side effect and you don't document
them, people will shoot themselves in the
[34:00] foot.
[34:01] I'll give you an example of this later.
[34:02] For parameters, people often forget.
[34:05] Don't just say, "Hmm, the size of the block."
[34:09] It's the size of the block in bytes or in
megabytes or whatever.
[34:13] You've got to tell me what the units are,
the form, if it's a string, especially.
[34:17] I've got to know.
[34:18] Is it XML?
[34:19] You know what form is this string in and finally,
the ownership.
[34:23] If I'm passing an object into an API, do I
still only object?
[34:27] Am I free to modify it after I passed it in
or have I transferred ownership from my self
[34:33] to the object to which I passed that other
object?
[34:37] If the thing that you're defining is mutable,
that is, can be modified, then you must document
[34:44] the state space very carefully.
[34:46] If you have a badly documented space--state
space, then you have no hope of being able
[34:52] to use that API properly.
[34:53] Because people won't know when it's legal
to call what and what will happen after the
[34:57] call is made.
[34:59] I may discuss this elsewhere but an example
of how to do this very badly, our date and
[35:04] calendar, in particular the calendar API in
Java.
[35:07] The state space is almost undocumented and
it caused numerous bugs over time.
[35:12] I'm happy to say by the way, that just days
ago, Sun decided that we would be pursuing
[35:20] any JSR to improve the date and calendar APIs
based in part on [INDISTINCT] time and who
[35:25] knows what or else.
[35:27] But to me--we may finally be, you know, free
of that mess.
[35:31] You should consider performance consequences
of API design decisions.
[35:34] And this is funny because this tends to, you
know, contradict the advice.
[35:37] You've all heard that, you know, a premature
optimization is evil.
[35:40] In fact, I have an old essay about that in
effective Java.
[35:44] However, that doesn't mean that you can just
ignore performance.
[35:47] Jon Bentley was big on this fact.
[35:49] And in particular, it turns out that bad API
decisions can limit performance.
[35:55] Examples of things that can limit it are making
the type mutable when it should be immutable
[35:59] or vice versa, providing a constructor instead
of a static factory, using implementation
[36:04] type in an API instead of an interface, which
means that people will always have to use
[36:08] that particular implementation even if a better
one comes along later.
[36:13] But the converse of this is, never work an
API to gain performance.
[36:17] Every once in a while you have something that
sort of temporarily broken.
[36:21] You know, this thing is slow and in order
to avoid this slow thing, you break your API.
[36:27] The thing that used to get slow becomes fast
but your API is still broken, you know.
[36:32] So, design your APIs for the long haul.
[36:35] Luckily, good API design usually coincides
with good performance.
[36:40] Here's an example of an API design decision
that led to bad performance.
[36:46] In the original AWT, there was something called
a dimension.
[36:50] If you had a component and you ask for its
size, you got back a dimension object, which
[36:54] contained two coordinates.
[36:56] It's simply couple of longs that were wrapped.
[36:59] The problem is that the dimension object was
mutable.
[37:02] Those longs weren't really wrapped.
[37:04] You know, they were--they were publicly visible,
mutable fields.
[37:08] And what that meant was every time you called
getSize, you had to allocate a new dimension
[37:14] object because otherwise I might, you know,
get the dimension object, give it to you,
[37:19] you might ask for it and then you might modify
it modifying his copy as well.
[37:23] And that would be really bad.
[37:24] Now these two, you know, independent threads
or computations will be tied together in nasty
[37:30] ways.
[37:31] Is that bad?
[37:32] You know, is it really expensive to allocate
a little object containing two longs?
[37:36] No, it's dirt cheap but unfortunately this
thing gets called millions, literally millions
[37:41] of times in a goyap So, you know, all of a
sudden, you're basically allocating megabytes
[37:45] of garbage and that really does cost you.
[37:48] It's garbage collector pressure that you just
don't need.
[37:50] And it was fixed in 1.2 by adding methods
that return each dimension individually as
[37:57] a primitive type which is in fact immutable.
[37:59] But, you know unfortunately, old codes that
used the 1.1 APIs is still slow and will always
[38:04] be slow.
[38:06] The APIs that you write have to coexist peacefully
with the platform.
[38:10] So, that means that you have to do what's
customary for that platform.
[38:14] You have to obey its naming conventions.
[38:16] You have to avoid anything that's just verboten
in that platform.
[38:20] You know, whatever it is, if you're in java,
you know, if you are in C++, everybody knows
[38:24] that there are certain things that you just
shouldn't do or shouldn't use.
[38:27] So, learn what those things are and then avoid
them.
[38:30] And there are--there are generally books that
tell you the traps and pitfalls for every
[38:33] platform.
[38:34] I could recommend one for Java but, you know.
[38:37] Anyway, the thing you've got to do is mimic
the patterns and the core APIs in the platform,
[38:45] because everyone who uses a platform knows
its core APIs.
[38:48] So, if your API feel just like one of the
core APIs, then everyone, because they already
[38:53] know how to use core API will already know
how to use your API.
[38:57] It's as simple as that.
[38:58] And the real trap here is you should never
simply transliterate APIs.
[39:06] That is the worst way to design an API.
[39:08] And what do I mean by that?
[39:10] Suppose you have a C++ API, and you want a
Java version of the same facility.
[39:15] What you should not do is take every C++ class
and make a complementary Java class that contains
[39:21] all the same methods as the C++ class because
what was reasonable in C++ will almost certainly
[39:26] be unreasonable in Java and vice versa.
[39:29] So, you have to basically take a step back.
[39:31] You have to say, "What is this class doing
and how would I do this in Java?"
[39:36] That's the right way to do it.
[39:38] Transliterated APIs are almost always broken.
[39:41] Okay.
[39:42] On to class design, I have 15 minutes to finish
the whole--rest of the talk.
[39:47] So that gives me five minutes each for the
next three sections and zero minutes for the
[39:50] last section.
[39:52] So, first of all minimize mutability.
[39:56] Classes should be immutable unless there's
a very good reason to make them mutable.
[40:00] And the advantages are that the class you
get are simple, they are thread safe, they
[40:05] are--instances of reusable.
[40:07] You never have to generate a new one.
[40:10] The only disadvantage is that you need a separate
object for each value.
[40:14] So, if you a huge object, let's say a big
integer that's a million digits long and you
[40:19] want to throw it away but you just want to
change a last bit, if it were mutable, you
[40:25] could do that in place, you know, at virtually
no cost.
[40:27] But because they're immutable, you have to
basically copy a megabit of data and then
[40:32] throw away the old megabit, which is a little
bit unpleasant.
[40:35] But if you do have to make things mutable,
and you often do, you should still make them
[40:39] as immutable as possible.
[40:41] You should give them a nice small, well defined
state space.
[40:44] You should make it clear--you should make
it clear when it's legal to call which method.
[40:49] So, bad examples, as I mentioned before, are
date and calendar.
[40:51] Calendar has this, you know, like the roll
method and when you, you know, you put date
[40:55] into it, who knows what state is behind it?
[40:58] When it's legal to call what?
[40:59] What state the calendar is in after if you've
used it?
[41:02] And, you know, did you know that when you're
using a date formatter, if one thread uses
[41:05] a date formatter and other thread tries to
use at the same time, both threads are hosed.
[41:10] You know, it kind of feels like an immutable
object, except that it has state inside it.
[41:14] You can't read that state but it's there.
[41:16] You know, so, these are--these are things
that are more mutable than they should be.
[41:19] A good example is TimerTask.
[41:20] It's not immutable, but it minimizes mutability.
[41:24] In particular, a TimerTask is inherently mutable
because it represents an actual or current
[41:29] computation and that mutates.
[41:30] But, you know, a TimerTask, what you do?
[41:34] You create it, you schedule it, it runs as
many times as it has to run, and then it's
[41:39] dead.
[41:40] It's gone.
[41:41] End of story.
[41:42] Now, there were people, who asked us, when
we were designing the timer API, "We want
[41:46] to reuse them, you know.
[41:47] It's like expensive to make a TimerTask."
[41:49] And the answer is, "No, let it die in peace."
[41:51] If you need another one, make another one
but by eliminating that sort of loop from
[41:55] the state space, you make the API much simpler
and much less bug-prone.
[42:01] You should subclass only when it makes to
do so.
[42:03] This is the Liskov Substitution Principle.
[42:05] And it's actually very simple.
[42:06] You just have to ask yourself a question.
[42:08] When you have two classes, in a public API,
and you're thinking of making one of subclass
[42:13] to another, like through a subclass of bar,
ask yourself, "Is every foo a bar?"
[42:18] If you can answer that with a straight face,
then make it a subclass.
[42:22] If not, don't.
[42:24] So, a bad example is, in Java properties extends
Hash table.
[42:28] Is every property's object a Hash table?
[42:31] Heavens, no.
[42:32] A property's object is a special thing that
map certain strings to certain other string.
[42:37] So, every property's object has a Hash table,
perhaps.
[42:41] Typically, it's implemented a top Hash table
but you can't answer that answer--that is
[42:45] a question with a straight face or what about
Stack extends Vector?
[42:48] Is every stack a vector?
[42:50] No.
[42:51] You push and pump on stacks and that's pretty
much all you do them.
[42:53] You might also have a peek method and a size
method.
[42:57] But, you know a vector allows random access,
accessing by index, a stack doesn't.
[43:01] So, it was really wrong to have Stack extends
Vector.
[43:04] And the really bad thing about it was they
took away great piece of real estate.
[43:07] We can't use the name stack for a class that
actually does implement stack anymore because
[43:12] the name has been taken.
[43:13] Good is Set extends Collections.
[43:15] Is every set a collection?
[43:16] Yes, it really is.
[43:17] Set is just a special kind of a collection.
[43:19] It's a collection that does not allow duplicate
elements.
[43:23] This is a fairly controversial one.
[43:25] But I believe that you should design and document
a class for inheritance or else prohibit it
[43:30] outright, to make the class final or have
no publicly accessible constructor.
[43:34] The reason for that is, is that in heritance
violates in encapsulation in a--in a subtle
[43:39] way.
[43:40] And that is to say that subclassing violates
encapsulation in a way that mere method invocation
[43:46] does not.
[43:47] This is sometimes called the fragile base
class problem, where basically if you have
[43:51] one class, it's implemented atop second class.
[43:55] And you override a method in the first class.
[43:59] It may modify the behavior of other methods
in the subclass because the original implementations
[44:05] of those methods dispatched to the method
that you just overrode.
[44:10] And then if in the future implementation of
the super class, they re-implement all of
[44:14] these methods, so that this method no longer
is implemented in terms of this one but both
[44:19] of them are implemented in terms of some third
method.
[44:22] Then, you know, this new version of the super
class will break the subclass.
[44:30] And the way to avoid that is either come clean
on exactly how every method uses every other
[44:37] method.
[44:38] That is document the self-use patterns of
a class.
[44:41] If you've done that, then you documented it
and designed it for inheritance.
[44:44] So, it's okay to make it non-final.
[44:46] Otherwise, it could just be final.
[44:47] Now, if you look at the Java API, we got this
wrong in many places.
[44:52] So, most of the concrete class in the J2SE
libraries, in particular, collection classes
[44:56] like, HashMap and HashSet.
[44:58] They're all non-final, but they don't exactly
define their self-use patterns.
[45:02] So, they're a bit fragile.
[45:04] Abstract set and abstract map and the other
abstract xxx classes are good.
[45:08] They really are designed and documented for
inheritance.
[45:11] Okay, onto methods.
[45:14] So, this is--if you remember only two things
from the talk today, this is the second thing.
[45:19] By the way what was the first thing I told
you to remember?
[45:22] >> [INDISTINCT]
>> BLOCH: Excellent.
[45:23] When in doubt, leave it out.
[45:24] The second one is, don't make the client do
anything the module could do.
[45:30] Those two things are like the fundamental
rules of API design.
[45:33] So, the worst thing you can do is write an
API that just requires the client to call
[45:38] in and out, and in and out just doing repeated
calls, passing junk from the first call to
[45:42] the second call.
[45:44] These causes boilerplate code, which as you
can see it's red.
[45:47] It's really, really bad.
[45:48] Why is boilerplate code bad?
[45:50] Because it's an opportunity for bugs, you
make boilerplate code by doing cut-and-paste
[45:55] and then modify.
[45:56] But if you don't do all the modification that
you should, it may still compile but it won't
[45:59] do the right thing.
[46:00] It is ugly, it is annoying, and it's error
prone.
[46:03] And here is a real live example from the W3Cs
DOM API.
[46:09] Suppose you have in hand an XML document and
you want to print it to an output stream.
[46:14] That's a very reasonable thing to do.
[46:16] It should take one call, you know, print,
it takes an output stream and you're done.
[46:21] But it doesn't.
[46:22] Here is what you actually have to do.
[46:24] First you have to import, in addition to w3c.dom,
that's fine and java.io, you also have to
[46:30] import XML Transform, xmltransform.dom and
xmltransform.stream.
[46:35] Why?
[46:37] I don't know but I do know you have to do
it.
[46:39] And then here's how the call should've looked.
[46:41] It should've been called Right Doc and, you
know, the document perhaps should have been
[46:44] the receiver and you pass in an output stream
and it would throw an IO exception.
[46:48] But here's what you actually have to do.
[46:49] First, you get a transformer and what is a
transformer?
[46:52] I don't know, but I know you need one.
[46:53] How do you get one?
[46:54] Well, you take the new instance method on
the transformer factory and then that gives
[47:03] you a transformer factory and once you get
the factory you ask for a new transformer.
[47:07] So, as you can see, they read design patterns.
[47:09] It's great.
[47:10] It's like filled with patterns.
[47:11] So now, you've got your transformer out of
your transformer factory and then you have
[47:17] to set an output properly on that transformer.
[47:20] You have to set its doc type system to be
the doc type from the doc.
[47:26] I don't know what any of these means but I
know you have to do all of it.
[47:29] And then, you have to get the system I.D.
and you have to--that's part of this output
[47:37] property that you're setting.
[47:38] And then when you're all done, you can actually
do the output.
[47:42] The way you do that is you call transform
on the transformer.
[47:45] And you just don't pass in the doc source
because that doesn't implement the right thing.
[47:48] You use the adaptor pattern here to take your
doc and you turn it into a DOM source and
[47:54] then also you can't quite just pass in your
output stream.
[47:58] You wrap it in a stream result and now you
get your output, right?
[48:03] Except for one tiny problem, which is it can
throw a transformer exception?
[48:06] When can it throw it?
[48:07] Well, never actually but the API says it can
and it's a checked exception, so we have to
[48:13] catch it if it gets thrown and then we throw
an insertion there because it can never happen.
[48:16] So, you know, you've got like, whatever it
is, six lines of just unreadable garbage code
[48:21] give you something very simple.
[48:22] If they had started with a used case, people
might want to print their xml documents.
[48:29] You get the idea.
[48:31] Another general rule when you're designing
class--a method is, don't violate the Principle
[48:35] of Least Astonishments.
[48:36] The user of an API should never be surprised
by its behavior.
[48:40] It's worth extra implementation effort.
[48:42] It's even sometimes worth a reduced performance
not to surprise the users of your API because
[48:47] if you surprise them, what will happen?
[48:49] They'll simply do the wrong thing.
[48:51] They'll think it does something, it'll actually
do something else and their program will be
[48:53] broken.
[48:54] Here's a real example from the thread API
in Java.
[48:58] So, we have this method called interrupted.
[49:02] You got a thread and you want to check if
it's interrupted.
[49:04] You call thread.interrupted.
[49:07] And what does it do?
[49:08] Well, it tests whether the current thread
has been interrupted.
[49:10] Oh, and by the way, it clears the interrupted
status of the current thread.
[49:16] That's just like a little side effect.
[49:17] It clears the interrupted status of the current
thread.
[49:20] Looking at the name, you know, thread.interrupted,
there'll be no way to guess that it does this.
[49:24] But it does this and many people have, you
now, spent hours chasing bugs because of it.
[49:28] You know what is the primary thing that this
call does?
[49:32] It clears the interrupted status.
[49:33] It's not an unreasonable call but it should've
been named clear interrupt status and by the
[49:38] way, it could've returned the old interrupt
status as a favor to you.
[49:41] But they named it based on--so, the second
most important thing it did, instead of the
[49:46] first most important thing it did.
[49:48] And in doing so, they violated this Principle
of Least Astonishment.
[49:52] You should fail fast.
[49:53] Whenever there's an error, you should tell
the user of your API as soon as possible after
[49:57] the error has happened.
[49:59] Ideally, you should tell him at compile time,
you know, because this way it happens in the
[50:03] lab or in the, you know, here where the program
is being written, instead of out in the field
[50:09] where the program is being run.
[50:11] And that means that I believe that static
typing is a very good thing.
[50:16] It moves errors from runtime to compile time.
[50:18] I understand that this is another highly controversial
topic.
[50:23] But I do believe it.
[50:24] You know, I've seen it happened.
[50:26] You know, for example, when increased the
static typing in Java by adding generics.
[50:30] I found bugs in preexisting code, you know,
because it forced to me to be more specific
[50:35] about the types that were expected and they
told me where things were wrong.
[50:39] If you are only going to be able to find out
an error at runtime, you want to find out
[50:44] the first time you do something wrong.
[50:47] Like, if you pass some garbage into something
else, you should find out as soon as you pass
[50:51] the garbage in, not ten minutes later.
[50:53] Here's the example of how not to do it.
[50:56] In the aforementioned properties class that
extends hash table.
[50:59] If you look at the spec, it has properties
instance maps strings to strings.
[51:04] But if you look at this put call, it takes
an object key and an object value.
[51:09] Any object, maybe a string, maybe something
else, but it tells you right away if you pass
[51:14] in something that's not a string, right?
[51:17] If only it were so.
[51:19] It doesn't, in fact, tell you right away.
[51:20] It lets you put in any garbage you want and
then, ten minutes later when you try--when
[51:27] you call the save call to an output stream,
which basically takes his property's object
[51:31] and translate into some garbage that isn't
quite XML.
[51:35] Then and only then does it blow up with a
class cast exception because you put something
[51:38] wrong into it ten minutes before.
[51:41] But by that time, debugging becomes almost
impossible.
[51:43] You don't know where the call was that put
the garbage into your properties object.
[51:49] You should provide programmatic access to
all data that is also available in string
[51:53] form.
[51:54] This is really important.
[51:55] Whenever you have a method that return something
as a string you should also have a method
[51:59] that returns the same stuff in programmatic
form.
[52:01] If you don't do that, then clients will have
to parse the string.
[52:06] Not only is that a pain in the butt but it
turns the string into a de facto part of the
[52:13] API.
[52:14] You can never add information to that string
because there's code out there that pausing
[52:18] that string.
[52:19] And if you change the format of the string,
you break that code.
[52:22] So, what you should do is along with the API
that gives you the printable string, you should
[52:26] have other APIs that give you access to--excuse
me, to the actual information and this way
[52:31] you can add more information to the string
later.
[52:33] And in fact, the spec should say that you
are not specifying the format of the string
[52:37] and that anyone who writes code to parse the
string is taking their lives into their hands.
[52:42] So, you know, a bad example is, initially
the only way to get the stack trace in Java
[52:48] was to call this gets stack--sorry, prints
stack trace API and people actually did go
[52:56] parse those things.
[52:57] In 1.4, we finally added get stack trace API
that gives you all the same information, a
[53:02] stack trace element consisting of the filename,
a line number, class name and so forth.
[53:06] But this was a case of sort of the horse had
already left the barn.
[53:12] You should overload with care.
[53:15] Method overloading can be a good thing but
it tends overused.
[53:19] You should avoid ambiguous overloading.
[53:21] That is multiple overloading that can do different
things when passed the same values.
[53:26] And a bad example, which I am guilty of here
myself, is TreeSet has two constructors; one
[53:32] that takes collection, one that takes a sorted
set.
[53:35] The first one ignores the order of the thing
that was passed in.
[53:37] The second one says, "Gee, I'm making a tree
set out of another sorted set, I might as
[53:41] well order it in the same way."
[53:43] Well, here's the problem.
[53:44] If you have a sorted set that's cast to a
collection, then you're calling this constructor
[53:48] and you get one result.
[53:50] Whereas, if you don't cast it, if you just
pass it in, you get another result, so I really
[53:54] should not have done that way.
[53:55] I should've done a dynamic test.
[53:56] If the thing was the instance of sorted of
set, then I should've preserved the order.
[54:00] So, you know, the basic rule here is just
because you can doesn't mean you should.
[54:05] Often, it's better to simply give something
another name rather than overloading.
[54:10] Overloading can be a real trap.
[54:11] If you use appropriate parameter and return
types that means you should favor interfaces
[54:18] that were specific classes.
[54:19] You should use the most specific possible
input parameter type.
[54:22] If you accept, let's say, a collection but
you'll blow up unless somebody passes you
[54:27] a set that's broken, you've just taken something
that could've been caught at compile time
[54:32] and instead you're catching it at runtime.
[54:34] Here's a really, sort of, another trap, don't
use a string if a better type exist.
[54:38] In these days of, you know, XML and web services,
people always start off with strings.
[54:43] Strings come in over the web.
[54:44] Just because it started as a string doesn't
mean it should stay as string.
[54:47] You should turn it into something more reasonable
and leave it there.
[54:50] A really bad example of this that I saw in
a program years ago, was a program that passed
[54:56] around a string for its whole duration of
its execution, that was either yes or no.
[55:02] We have a good data type for that it's called
boolean.
[55:05] You should never use floating point types
at Google.
[55:08] You guys already know all these but never
use the floating point types, float or double
[55:13] for monetary values.
[55:15] They're not good enough to represent money.
[55:16] You cannot do exact computations base 10 using
floating point numbers--binary numbers because
[55:23] of the fact that 1/10th is not representable
as a binary fraction.
[55:27] So, don't do it.
[55:28] If you have you know, an amount of money,
use big decimal.
[55:32] Perhaps, use long big integer or what have
you but do not use float or double.
[55:37] And when you are faced with the choice of
using float or double, you should almost use
[55:40] double rather than float because, you know,
typically double will run just as fast and,
[55:48] you know, you lose real and important precision
by going down to float.
[55:52] Let see, I'm going to have to just run through
the rest of this.
[55:55] This one is really important.
[55:57] Use consistent parameter ordering across methods.
[56:00] So, you know, here is an example of what not
to do.
[56:02] A real example from Unix.
[56:04] We have two methods to copy data.
[56:06] One is called store and copy.
[56:07] One is called b-copy.
[56:09] The first one takes a destination, a source
and a size.
[56:12] The second takes a source, a destination and
a size.
[56:16] So, what happens, you know, if somebody assumes
one ordering when they call the other method?
[56:22] They clobber their source data with whatever
garbage was in their destination array.
[56:26] And how long does that take to find that bug?
[56:29] Probably a really, really long time.
[56:31] This is particularly important when the types
of the two parameters are identical because
[56:35] if you switch them around, you will not know
at compile time.
[56:38] A good example here is in Java.util.Collections,
the first value is always the collection being
[56:44] mutated or manipulated.
[56:46] Similarly, in util concurrent, when you have
an amount of time, you always specify it as
[56:52] a delay followed by time unit never other
a way around.
[56:54] And even if it were the other a way around,
the compiler would tell you because these
[56:57] are strong types that are incompatible.
[57:00] A long and the time unit are two different
things.
[57:03] You should avoid long parameter list.
[57:05] Ideally, you should limit them to three.
[57:07] It's really easy to remember three things
especially, you should avoid long list of
[57:11] identically typed parameters because of the
problems that I've told you before.
[57:14] If you get the order wrong, you're hosed.
[57:16] So here's an example of what not to do.
[57:18] This is from the Win3 to API, to create a
window.
[57:22] You know, if you look in the middle of these,
whatever 15 parameters, you see Int x, Int
[57:26] y, Int end with, Int height.
[57:28] So, here's, you know, a whole string of Ints
and by the way some of these other things
[57:31] are also Ints, just Int by another name.
[57:33] So, you know, without support from an IDE
it's pretty much impossible to use this API.
[57:37] Luckily, there are number of great techniques
for shorting parameter list.
[57:41] One thing you can do is break up a method
into a multiple methods, or you can create
[57:44] a helper classes to hold the parameters.
[57:46] A specific example of the helper class is
the builder pattern, where if you got a constructor
[57:51] or a static factory that naturally would take
10 parameters, most of which you don't have
[57:56] to specify, most of which have good defaults.
[57:58] Instead, make a builder and then just plug-in
the ones you actually care about and then
[58:03] call a build method.
[58:04] And that code will be much easier to write
and to read.
[58:08] You should avoid return values that demand
exceptional processing.
[58:11] In particular, you should never return a,
"No," instead of a zero-length array or collections.
[58:17] Here is an example of something we got wrong.
[58:20] The--in the buffered image op-class, we have
a method called get rendering hints.
[58:25] And either, it returns a rendering hints collection
or it returns, "No."
[58:31] And what's the consequence of this?
[58:32] Almost all code that calls this thing is wrong
because it rarely returns, "No."
[58:37] So, people forget to code for that special
case.
[58:39] If they do get a no, what happens?
[58:41] No point or exception and it's completely
unnecessary.
[58:45] If they just returned a zero-length collection,
then they wouldn't got into any trouble.
[58:50] So, I think I'm actually going to skip the
rest of the talk because I'm out of time.
[58:55] In fact, I've already used up five minutes
more time than I have.
[58:58] But if you have any other collection--try
again.
[59:00] If you have any other questions, I'll be around
for a while afterwards and you can ask me
[59:04] anything you'd like.
[59:05] So, thanks for coming.
[59:09] Oh, one other thing, which is I have a--I
have a give-away for you, which is--when I
[59:17] gave this at OOPSLA in the proceedings, they
gave me two pages to put in a--what do you
[59:25] call that?
[59:26] Like an extended abstract.
[59:27] And instead of doing that, I tried to do API
design by bumper sticker.
[59:30] I try to basically take this entire talk and
break it down into 50 little maxims, like,
[59:36] when in doubt, leave it out or, you know,
all programmers are API designers, each with
[59:41] a sort of a sentence describing, you know,
in a bit more detail.
[59:43] So here, pass this out amongst yourselves,
as best you can.
[59:47] And I'll put it up on, you know--and tell
the JJB in case there are more of you than
[59:53] there are piece of paper.
[59:55] So thanks again for coming.