Brian Will Replaces Uncle Bob, Metz OOP Code With Procedural Routines

Object-Oriented Programming is Embarrassing: 4 Short Examples — Brian Will

Brian Will

Object-oriented programming harms software development because imbuing inert data with agency creates convoluted class hierarchies that obscure straightforward procedural transformations.

AI-assisted summary of Object-Oriented Programming is Embarrassing: 4 Short Examples — original source. Check the original for full context.

Source published:

Key ideas

  • Brian Will challenges mainstream object-oriented programming by rewriting four conference talk examples under 200 lines—by Sandi Metz, Derek Banas, and Robert C. Martin—into procedural code within their original languages to prove procedural design is clearer and simpler. (0:01)
  • Will argues that OOP's fundamental flaw is imbuing inert data with intent and agency while turning actions into artificial nouns, dragging developers into abstract Platonic essentialism rather than practical engineering. (2:44)
  • The critique questions the OOP doctrine of replacing branching logic with polymorphism, noting that dynamic languages lack compile-time enforcement and real-world branching conditions are rarely parallel across system phases. (17:45)
  • Will rejects the idea that codebases should be broken into opaque object webs so developers do not have to understand the whole system, warning that giving up on understanding how software works inevitably leads to worse engineering. (26:48)

Detailed notes

## Background Published by Brian Will on YouTube on March 4, 2016, this video serves as a follow-up to his earlier presentation, 'Object-Oriented Programming is Bad.' According to the page description and introduction, Will sets out to demonstrate the practical shortcomings of object-oriented design by refactoring four real-world code examples from industry conference talks into simple procedural equivalents.

## TL;DR Brian Will argues that object-oriented programming (OOP) introduces needless complexity, overengineering, and philosophical confusion into software development. To substantiate this claim, he examines four real-world code samples under 200 lines—drawn from well-known conference talks by Sandi Metz, Derek Banas, and Robert C. Martin ('Uncle Bob')—and rewrites each in its original language using straightforward procedural paradigms. He demonstrates how replacing artificial class hierarchies, polymorphic dispatch, and single-use wrappers with plain data structures and direct functions drastically improves clarity, maintainability, and cognitive load. Ultimately, Will rejects the OOP mindset that atomizes programs into obscure webs of objects, concluding that clean software engineering requires letting data remain inert and maintaining a thorough understanding of execution flow.

## Atmosphere & style The presentation carries a calm, unapologetic, and iconoclastic tone, driven by Will's measured yet scathing critique of mainstream software dogmas. Rather than engaging in hyperbole or theatrics, Will methodically walks through actual code on screen, delivering deadpan takedowns of venerated industry figures and beloved abstractions like UML and polymorphic branching. Watching the video feels like sitting in an informal, candid code review with an experienced engineer who has grown thoroughly exhausted by industry pretension, dismantling complex object hierarchies with understated common sense.

## Bulletin - [00:01](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=1s) Brian Will challenges mainstream object-oriented programming by rewriting four conference talk examples under 200 lines—by Sandi Metz, Derek Banas, and Robert C. Martin—into procedural code within their original languages to prove procedural design is clearer and simpler. - [02:44](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=164s) Will argues that OOP's fundamental flaw is imbuing inert data with intent and agency while turning actions into artificial nouns, dragging developers into abstract Platonic essentialism rather than practical engineering. - [17:45](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=1065s) The critique questions the OOP doctrine of replacing branching logic with polymorphism, noting that dynamic languages lack compile-time enforcement and real-world branching conditions are rarely parallel across system phases. - [26:48](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=1608s) Will rejects the idea that codebases should be broken into opaque object webs so developers do not have to understand the whole system, warning that giving up on understanding how software works inevitably leads to worse engineering.

## Key takeaways - [00:01](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=1s) Will stands by his stance that object-oriented programming has harmed the software industry and analyzes unfiltered examples from OOP conference talks to demonstrate its flaws. - [02:44](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=164s) The core critique rests on the maxim 'let data be data': passive data should remain inert records, while operations should remain simple procedural functions rather than encapsulated actors. - [05:01](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=301s) The Single Responsibility Principle's rule of having 'only one reason for change' is criticized as vague and nebulous when applied to practical class design. - [05:01](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=301s) Creating stateless classes merely to namespace logic produces throwaway instances that add architectural ceremony without delivering genuine object encapsulation. - [07:21](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=441s) Decomposing single procedures into specialized classes with dynamic metaprogramming or internal dependency instantiation overengineers tasks that plain functions and hash maps solve cleanly. - [09:00](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=540s) Inlining small, single-use helper functions reduces total function count and lowers overall cognitive load across codebases. - [11:30](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=690s) Derek Banas's coin-flip simulation demonstrates how formal UML diagrams and multi-class breakdowns drastically overcomplicate trivial procedural routines. - [11:30](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=690s) Visual programming and UML fail to scale because complex entity interactions inevitably turn diagrams into unreadable tangles, whereas text-based code handles scale naturally. - [14:49](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=889s) In the Gilded Rose kata, replacing conditional logic with abstract base classes, subclasses, class lookup hashes, and factory methods introduces excessive overhead compared to a basic switch statement. - [17:45](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=1065s) The claim that polymorphism safely replaces parallel switch statements fails in dynamic languages like Ruby, which provide no compile-time enforcement for missing method overrides. - [17:45](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=1065s) Real-world program conditions are rarely neatly parallel, making polymorphic dispatch an unnatural fit for divergent branching criteria across different application stages. - [19:39](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=1179s) Robert C. Martin's argument parsing design unnecessarily fragments simple string token processing across an abstract class and three specialized, recursively structured marshaler subclasses. - [22:29](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=1349s) A procedural alternative using a single class and three typed hash maps accomplishes argument parsing with greater clarity, direct data inspection, and fewer moving parts. - [24:57](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=1497s) Thinking in terms of synthetic object personas obscures the inherent simplicity of everyday data transformation algorithms. - [26:48](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=1608s) Will rejects the software philosophy that developers do not need to understand their full codebase, asserting that atomizing systems into disconnected objects degrades software quality.

## Detailed walkthrough ### [00:01](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=1s) Following Up on “Object-Oriented Programming is Bad”

Brian Will explains that after posting an earlier video titled *Object-Oriented Programming is Bad*, where he outlined theoretical arguments against OOP and argued that developers should avoid it, some viewers complained that the title was clickbait. Will rejects that criticism entirely, standing by the framing because he believes OOP has genuinely harmed software engineering:

> “I chose the title because I think it's really true; I have nothing nice to say about object-oriented programming, I don't like it, I think it's really been a disaster for the industry.”

To ground his critique in practice, he is upping the ante with this follow-up, *Object-Oriented Programming is Embarrassing*. In this presentation, he analyzes four short, real-world code examples under 200 lines—two written in Java and two in Ruby. For every case, he rewrites the code into simple procedural code within the exact same programming language to prove that a procedural design produces a vastly clearer and superior outcome without unfair language-level bias.

Will emphasizes that he did not cherry-pick bad code. Instead, he took the very first examples he encountered on YouTube and Vimeo from speakers giving conference talks specifically explaining the virtues of object-oriented design and how it supposedly improves codebase maintainability.

Acknowledging that critics will argue that OOP’s benefits only manifest in massive software projects to protect against spaghetti code, Will states that the ultimate test would be taking a 500,000-line codebase and rewriting it between procedural and OOP paradigms. While that scale is practically impossible for one person, he invites viewers to send him complete, idiomatic OOP examples—either full programs or submodules of 1,000 to 2,000 lines, 10,000 lines, or even up to 50,000 lines. He expresses a preference for widely understood languages like JavaScript, Python, Java, or Ruby (with JavaScript being ideal for broad accessibility), provided the code embodies genuine, architectural OOP rather than merely perfunctory class syntax.

- **Examples examined:** 4 short programs under 200 lines (two Java, two Ruby). - **Core methodological rule:** Rewrites remain in the original source language to maintain a fair comparison. - **Source of examples:** Unfiltered first hits from online tech conference talks demonstrating OO design. - **Future testing target:** Open call for real-world modules between 1,000 and 10,000+ lines.

---

[02:44](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=164s) The Flawed Philosophy: Letting Data Be Data

Before jumping into the code rewrites, Will recaps his broader critique with a core maxim: “let data be data.” Object-oriented programming forces developers into a conceptual error by insisting that passive data should be modeled as an entity endowed with purpose, duties, and the power to interact with the universe:

> “We basically have to imbue our data with intent and agency, and that's just a fundamental mistake; data does not do things, data is just inert, and that's the way it should be, and you're just adding confusion if you try and conceptualize your data as some kind of actor.”

The inverse holds true for behavior: actions should simply be actions. OOP requires programmers to “nounify” verbs, packaging basic operations into rigid data structures. Will argues this unnatural fusion of action and state forces software developers to abandon practical engineering in favor of analytical philosophy and Platonic essentialism. When building a hypothetical `Chair` class, one is forced to contemplate the true essence and boundaries of “chairness.” If a developer draws those boundaries incorrectly, OOP theorists argue that design principles were violated, making the resulting spaghetti code the developer's fault rather than a flaw in OOP itself. This creates a circular, unfalsifiable justification where failure to achieve clean code merely results in gurus prescribing ten more rules and design patterns.

```mermaid flowchart TD accTitle: The Conceptual Trap of Object-Oriented Modeling accDescr: Diagram contrasting natural programming separation of data and behavior against the conflated philosophical overhead of OOP.

subgraph Natural["Natural Procedural Paradigm"] D["Inert Data<br/>(Plain values, hashes, records)"] F["Pure Actions<br/>(Verbs, procedures, routines)"] F -->|"operates on"| D end

subgraph OO_Trap["Object-Oriented Conflation"] OOP["Platonic Entity / Class"] OOP --- Intent["Imbued with Agency & Intent"] OOP --- Noun["Verbs turned into Nouns"] OOP -->|"misdrawn boundaries lead to"| Bug["Spaghetti Code & Guru Dogma"] end ```

---

[05:01](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=301s) Example 1: Sandi Metz on the Single Responsibility Principle

Will examines his first code sample, taken from Sandi Metz’s talk *SOLID Object-Oriented Design*. The core subject of Metz’s presentation is a concise Ruby class responsible for handling patents: it downloads a remote patent file, parses its contents, and updates the patent data inside a database. Metz uses this sample to demonstrate a violation of the Single Responsibility Principle (SRP)—the rule that every module, class, or method should perform exactly one task.

Will points out that defining what constitutes “one thing” remains deeply contentious. Robert C. Martin (“Uncle Bob”), who popularized the SOLID acronym, defined the Single Responsibility Principle by stating:

> “His definition is that a single class, a single unit, should have only one reason for change.”

Even after lengthy explanations, Will views the "one reason for change" metric as hopelessly vague and nebulous.

Looking at Metz's starting Ruby class, Will observes that it centers around a single `run` method that orchestrates calls to three private helper methods: `download_file`, `parse`, and `update_patents`. Metz objects to this initial implementation because downloading, parsing, and persisting data are three separate responsibilities crammed into a single unit.

Will actually concedes that grouping these disparate tasks together in one unit feels awkward, but highlights a deeper absurdity: the class contains zero fields or internal state. Because a class is supposed to define a data type, an instance of this patent processor is nothing more than an artificial namespace. To run the logic, the caller must instantiate an ephemeral object with no long-term identity or state, call `.run`, and instantly discard it.

- **Source talk:** *SOLID Object-Oriented Design* by Sandi Metz. - **Domain task:** Fetch a patent file over FTP, parse it, and commit updates to a database. - **Initial class structure:** A single `run` method orchestrating `download_file`, `parse`, and `update_patents`. - **Primary structural flaw:** Zero instance variables or persistent state, functioning merely as an instantiated wrapper around a namespace.

---

[07:21](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=441s) Deconstructing Metz's Multi-Class Solution

Metz’s remedy for the multi-responsibility class is canonical OOP: decomposing the single class into multiple specialized classes. Her first extraction is a dedicated `Config` class designed to abstract and centralize configuration values parsed from a YAML file. Its `initialize` constructor accepts an options parameter, reads the corresponding YAML file, and runs a metaprogramming routine (`define_methods_for_environment`) that dynamically generates methods on the config instance matching every configuration key.

Will argues that this abstraction is an overengineered solution to a trivial problem. Instead of instantiating an object filled with dynamically generated getter methods just to read settings, a simple standalone function could take the options, parse the YAML file, and return a plain native hash map.

Metz next isolates network concerns into an `FTPDownloader` class. Will points out the exact same architectural absurdity: the class requires an `initialize` constructor to accept and assign a `config` property, yet nobody interacting with the FTP downloader will ever access or manipulate this internal configuration reference again. It exists purely as an artificial object wrapper around a single execution step.

[09:00](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=540s) Refactoring Sandi Metz’s Patent Job to Plain Procedural Code

Continuing his critique of Sandi Metz's refactoring, Brian Will points out the absurdity of instantiating an entire class just to invoke a single method like `download_file`. In her revised `PatentJob` class, Metz removes the downloader method and instead creates a config object directly within the constructor, passing it along to instantiate a downloader object that gets stored in an instance variable for later use by the `run` method. Will finds this structure bizarre from an object-oriented design standpoint, noting that she avoids constructor injection: “As much as I don't like object-oriented programming, if you are going to do it, I think for a very good reason you do use dependency injection because that—well, that's a whole tangent, but basically it solves the problem of having to wire together all your objects, or it's a much more sane way of doing it.” Having a constructor instantiate its own dependencies internally defeats the standard benefits claimed by OOP practitioners.

However, Will argues that debating the nuances of object wiring misses the larger point entirely. The task should not involve classes at all, but rather straight procedural code using basic functions—what Ruby terms Kernel methods. Instead of an entire `Config` class, a simple `get_config` function could read the YAML file and return a plain hash map of values, ending the story without requiring constructors or throwaway instances. Will takes the simplification a step further by arguing that small helper functions called in only one place should simply be inlined into the main `process_patent` routine until future complexity genuinely demands extraction.

```mermaid flowchart TD accTitle: Procedural Alternative for Patent Processing accDescr: Pipeline showing configuration parsing into a hash map and sequential patent processing. subgraph ConfigStage["Configuration"] A["YAML file"] --> B["get_config()"] B --> C["Config Hash"] end subgraph ProcessingStage["Processing Pipeline"] C --> D["process_patent()"] D --> E["Download patent file"] E --> F["Process patent data"] end ```

- When using plain functions, developers do not need to ponder the significance of classes, speculate about what their fields represent, or trace single-use objects that exist only to invoke one method. - Inlining single-use helper functions reduces cognitive load; while shrinking five functions to three seems minor in a toy example, reducing a codebase from 100 functions to 60 makes a system substantially easier to navigate.

---

[11:30](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=690s) Derek Banas, UML Modeling, and the Over-Engineered Coin Flip

Will turns to his second example: an object-oriented design tutorial series by Derek Banas. While acknowledging that Banas produces quality educational content in his more recent programming videos, Will characterizes his 11-part series on object-oriented design as disastrous due to the flawed nature of the paradigm itself. In the second video of the series, Banas demonstrates how to build a coin-flipping simulation. The program is not an elaborate interactive game; it simply takes two named players, randomly determines a winner, randomly selects heads or tails, prints a message like "so-and-so won with a coin flip of heads," and prompts the user in a loop if they would like to flip again.

To build this trivial script, Banas applies full formal modeling using UML (Unified Modeling Language). He constructs use case diagrams, an object model, a sequence diagram to map out time progression and messages between instances, and a class diagram dividing the problem into `Player`, `Coin`, and `CoinGame` classes, plus a fourth class solely to house the Java `main` method. Will demonstrates that the entire application could instead be written as a procedural loop inside `main`—optionally delegating to a single `play_game` function—that takes two strings, picks random values, and prints the result.

```mermaid flowchart LR accTitle: Derek Banas UML Complexity vs Procedural Simplicity accDescr: Comparison between Banas's multi-class UML architecture and Brian Will's minimal procedural flow. subgraph OOPApproach["Banas's OOP Architecture"] direction TB CG["CoinFlippingGame (main)"] --> GM["CoinGame"] GM --> P["Player"] GM --> C["Coin"] end subgraph ProcApproach["Brian Will's Procedural Alternative"] direction TB M["main() loop"] --> PG["play_game(player1, player2)"] PG --> R["Random winner + flip & print"] end ```

Beyond the code itself, Will mounts an attack on UML and visual programming systems in general. Diagramming tools are notoriously frustrating, but the deeper conceptual flaw is that visual representations break down as soon as a program achieves any realistic level of complexity. When multiple entities interact, diagrams devolve into an unreadable tangle of boxes and crossed lines. Written code with named functions and textual references handles scale far better than visual diagrams, which Will identifies as the primary reason visual programming environments have never succeeded broadly:

> “The lesson here is that UML is just garbage, don't ever use it, it's crazy.”

---

[14:49](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=889s) Sandi Metz’s Gilded Rose: Polymorphism vs. a Simple Switch

The third example revisits Sandi Metz, specifically her presentation titled "All the Little Things," which tackles the well-known Gilded Rose refactoring kata containing World of Warcraft-themed item names. In the talk, Metz displays a block of nested, convoluted conditional logic that updates item quality and sell-by dates over time, framing polymorphism as the elegant solution that swoops in to rescue the codebase from messy branching logic.

Metz refactors the code by introducing an enclosing `GildedRose` class containing an abstract `Item` base class with an empty `tick` method, accompanied by three specialized subclasses: `Normal`, `Brie`, and `Backstage`. She then constructs a lookup hash map that associates specific strings (such as `"Aged Brie"` or `"Backstage passes to a TAFKAL80ETC concert"`) with their respective class types. Finally, she implements a class-level factory method (`self.for`) that accepts the item name, quality, and remaining days, looks up the corresponding subclass in the hash, instantiates it, and invokes its overridden `tick` method to execute the update.

Will argues that while Metz’s solution is clever and showcases dynamic Ruby features, it introduces unnecessary architectural overhead. The flaw in the original code was not that it used conditionals, but that it failed to identify that the business logic boiled down to four mutually exclusive categories. Once those discrete cases are identified, a simple `case`/`switch` statement or an `if-elsif` ladder expresses the logic cleanly without inventing an entire hierarchy of classes:

> “The way she presents it in the talk is that polymorphism swoops in and saves the day, it makes the code much cleaner, but there's no need for it—you can just write a damn switch.”

- Metz's polymorphic architecture requires an abstract base class (`Item`), three subclasses (`Normal`, `Brie`, `Backstage`), a class-to-string registry hash, and a static factory method (`self.for`). - Will contends that creating new data types and inheritance hierarchies to replace straightforward conditional branches is "perverse" when a flat procedural switch statement achieves the same clarity with far fewer concepts.

[17:45](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=1065s) The Fallacy of Replacing Branching with Polymorphism

Transitioning from an earlier refactoring example, Brian Will explains that the procedural version could be made even cleaner. In a strictly procedural design, one does not retain instance variables inside methods of a class; instead, functions like `tick` would simply take arguments such as a name and a data object, operating directly upon its members. He addresses the widely circulated object-oriented doctrine that polymorphism can or should eliminate all branching logic. The common justification is that codebases frequently repeat parallel switch statements across multiple files; by modeling these cases polymorphically, statically typed languages like Java will enforce at compile time that every method in an interface or abstract class is implemented. This supposedly prevents developers from updating half of the switch statements when introducing a new case while forgetting the other half.

```mermaid flowchart TD accTitle: Comparison of Polymorphic Branching vs Procedural Switches accDescr: Contrasts how polymorphic classes handle dispatch across types versus how procedural code handles diverging logic.

subgraph PolymorphicApproach["Polymorphic Class Hierarchy"] Base["Base Type / Interface"] --> TypeA["Subclass A (implements method)"] Base --> TypeB["Subclass B (implements method)"] Base --> TypeC["Subclass C (implements method)"] end

subgraph ProceduralApproach["Procedural Data Inspection"] Input["Raw Input Data"] --> Switch1{"Context 1: Switch on Case"} Switch1 --> Action1["Execute Logic"] Input --> Switch2{"Context 2: Divergent Conditions"} Switch2 --> Action2["Execute Separate Logic"] end ```

Brian points out that this safety argument immediately collapses in dynamic languages such as Ruby, where subclassing an abstract class without overriding a method triggers zero language warnings. Even setting aside language typing, he fundamentally disagrees with the philosophy itself. Real-world branching logic is rarely identical across different program phases; it is typically semi-parallel, where logic switches over one set of factors in one part of the codebase, but requires completely different criteria elsewhere. Forcing code into artificial class hierarchies under the guise of eliminating switches introduces unnecessary mental friction.

> “Regardless, I still think polymorphic branching is a perverse idea mostly because I think these neatly parallel switches are actually quite rare.” — Brian Will

- In a pure procedural model, free functions accept data structures as parameters rather than executing implicitly against encapsulated instance state. - In statically typed languages like Java, unimplemented interface methods trigger compile-time errors, but dynamic languages offer no such safety nets. - Reconceptualizing regular branching logic as dispatch over types creates high conceptual overhead with negligible long-term payoff.

---

[19:39](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=1179s) Uncle Bob’s Command-Line Argument Parser

Brian introduces the final example of the presentation: a command-line argument parser demonstrated in a talk by Robert C. Martin (known as Uncle Bob), whom Brian calls “probably the preeminent object-oriented Guru.” Martin’s program parses command-line arguments into a queryable data structure so a caller can look up the value of flags such as `-b` or `-c`. The arguments are defined via a comma-delimited schema string that uses special character annotations to denote data types:

| Type Indicator | Meaning | Example Schema Token | Description | |:--- |:--- |:--- |:--- | | *(None)* | Boolean flag | `f` | Defaults to `false` if flag is omitted | | `#` | Integer flag | `j#` | Followed by an integer parameter on CLI | | `*` | String flag | `w*` | Followed by a string parameter on CLI |

For example, a schema string defining `j#`, `f`, and `w*` expects an integer flag `j`, a boolean flag `f`, and a string flag `w`. Given command-line input like `-j 35 -w blahblahblah`, the resulting data structure assigns `35` to flag `j`, `false` to flag `f` (because `-f` was omitted), and `"blahblahblah"` to flag `w`. Martin originally showed an unrefined version before presenting his cleaned-up solution. Brian skips the "bad" version entirely, remarking that it was neither worse nor better than Martin’s ostensibly superior refactored code.

In Martin's refactored architecture, an abstract `ArgumentMarshaler` class exposes two abstract methods: `set` and `get`. Brian expresses bewilderment as to why Martin chose an abstract class over a standard Java interface, given that the class contains no fields or concrete implementation. This abstract base is extended into three concrete subclasses: `BooleanArgumentMarshaler`, `StringArgumentMarshaler`, and `IntegerArgumentMarshaler`. The bodies of these classes are nearly identical, differing only in the primitive or reference type they manipulate.

```mermaid flowchart TD accTitle: Uncle Bob's Marshaler Architecture accDescr: Hierarchy of ArgumentMarshaler and its concrete subtypes feeding into the Args coordinator class.

subgraph MarshalerHierarchy["Marshaler Classes"] AM["abstract ArgumentMarshaler\n+ set()\n+ get()"] AM --> BAM["BooleanArgumentMarshaler"] AM --> SAM["StringArgumentMarshaler"] AM --> IAM["IntegerArgumentMarshaler"] end

subgraph Execution["Execution Flow"] Main["Main / Client Code"] -->|"instantiates with schema & args"| ArgsClass["Args Coordinator Class"] ArgsClass --> BAM ArgsClass --> SAM ArgsClass --> IAM Main -->|"getBoolean(), getInt(), getString()"| ArgsClass Main -->|"invokes on success"| Exec["executeApplication()"] end ```

These subclasses are then coordinated by an `Args` class containing `main`. In `main`, the consumer instantiates `Args` with the schema string and CLI inputs, receives an `Args` object, and queries it through dedicated methods like `getBoolean('l')`, `getInt(...)`, or `getString(...)`. The values are subsequently passed to a stubbed `executeApplication` function. Because the constructor and getters are declared to throw `ArgsException`, the setup must be wrapped in a broad `try`/`catch` block.

---

[22:29](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=1349s) Brian Will’s Single-Class Procedural Solution

Brian contrasts Uncle Bob’s multi-class hierarchy with his own concise alternative: a single class named `Args`. Rather than generating a family of marshaler classes, Brian's `Args` class manages state using three simple hash maps that map individual characters to values: - `HashMap<Character, Integer>` for integers - `HashMap<Character, String>` for strings - `HashMap<Character, Boolean>` for booleans

The constructor processes the input in two brief loops. First, it parses the schema string by splitting on commas, reading each token's accompanying character symbol, and inserting the flag character into the corresponding map alongside sensible defaults. Boolean entries default to `false` (since an omitted boolean flag represents falsehood), whereas integer and string entries default to `null` (since missing values have no implicit value).

```mermaid flowchart LR accTitle: Procedural Parsing Pipeline accDescr: Two-stage procedural parsing from raw schema and argument strings into three typed hash maps.

subgraph Stage1["1. Parse Schema"] Schema["Schema String\n(e.g., 'j#,f,w*')"] --> SplitSchema["Split by Comma"] SplitSchema --> PopulateMaps["Populate HashMaps with Defaults:\n- Booleans -> false\n- Integers -> null\n- Strings -> null"] end

subgraph Stage2["2. Parse CLI Tokens"] Tokens["CLI Arguments\n(e.g., '-j', '35', '-w', 'val')"] --> FilterHyphens{"Starts with '-'?"} FilterHyphens -- No --> Ignore["Ignore Token"] FilterHyphens -- Yes --> MatchMap{"Check Flag in Maps"} MatchMap --> AssignVal["Parse & Store Next Value"] MatchMap -- Unknown Flag --> Err["Throw Exception"] end ```

Second, the constructor loops over the supplied command-line arguments. Non-flag arguments lacking a leading hyphen are skipped. When a hyphenated flag is found, the constructor inspects the character against the three maps, reads the value that follows, converts it, and updates the map entry. If an unexpected character not declared in the schema is encountered, it drops into an `else` branch and throws an exception.

Brian provides equivalent `getBool`, `getString`, and `getInt` accessors to match Martin's API, but notes that in his own code, he would prefer making the hash map fields `public` and querying them directly. He also favors explicit null-checking over throwing exceptions:

> “And I prefer explicit error handling over exceptions, so the resulting code will look something like this: we get `args.ints.get`, but that might be null, and if so then that's an error, otherwise if it's not null then we can get its value.” — Brian Will

Brian acknowledges that checking for null values directly in Java can look verbose and clumsy, but explains that this clunkiness is the fault of Java's awkward divide between primitive types and boxed object types rather than an issue with procedural design.

---

[24:57](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=1497s) Complexity, Recursion, and OO Mental Models

Brian directly contrasts the architectures of both solutions. His procedural approach consists of a single `Args` data type containing a constructor, public fields or straightforward getters, and `main`. A developer inspecting the codebase knows instantly where execution begins and how data flows. Martin's solution, by contrast, explodes the problem across five separate classes, including an abstract class that Brian argues is completely unnecessary.

Most baffling to Brian is the internal design of Uncle Bob's marshaler classes: they are implemented recursively. For instance, `BooleanArgumentMarshaler` contains a field for its boolean value, but also contains an internal map associating characters with instances of `ArgumentMarshaler`. Brian observes that in practice, this internal map only ever points back to another `BooleanArgumentMarshaler`. He cannot discern any practical reason for making the marshalers recursive, describing the design as profoundly bizarre.

Brian diagnoses the root cause of this design as a cognitive failure brought on by object-oriented ideology. When programmers model everyday algorithms by assigning agency to passive data and turning actions into classes, they lose touch with the baseline simplicity of what they are building.

> “I think what happened is that because he was programming in an object-oriented style, he just didn't have the clarity to see how simple what he was doing really was and so he couldn't then devise a sensibly simple solution.” — Brian Will

When an engineer inspects Uncle Bob’s code, they must puzzle through why five separate types exist, decipher why the data structures are recursive, and trace execution jumping across numerous fragmented, tiny methods. Procedural logic may retain a switch statement or an explicit loop, but Martin’s object-oriented decomposition merely scatters that logic to the winds. Brian concludes that thinking purely in terms of data structures and data transformations produces far clearer, more maintainable code than inventing synthetic "doer" types.

[26:48](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=1608s) The Illusion of Decomposed Objects and the Necessity of Understanding Code

Reflecting on the command-line argument parsing example, Brian Will reiterates that the core task was remarkably straightforward: taking an incoming array of strings and converting it into a structured format of data—specifically using three simple hash maps—that is convenient to query and work with downstream. Stripping away the layers of object-oriented ceremony yields immediate clarity, cutting through unnecessary abstractions to focus strictly on data transformation. Brian argues: “By avoiding all of his object-oriented nonsense we can have that clarity.”

```mermaid flowchart LR accTitle: Procedural Data Transformation vs Object-Oriented Decomposition accDescr: Simple procedural pipeline transforming raw arguments into queryable hash maps versus fragmented object graphs.

subgraph Procedural ["Procedural Approach"] direction LR A["Array of Strings"] --> B["Parser Function"] B --> C["Three Hash Maps"] end

subgraph OOP ["Object-Oriented Approach"] direction LR D["System of Small Objects"] -.->|"Fragmented Interactions"| E["Opaque Call Graph"] end ```

This contrast prompts Will to recall a talk by prominent software author and speaker Sandi Metz, who openly conceded that object-oriented code is generally less comprehensible than equivalent procedural code. Metz defended object-oriented programming by arguing that developers are not actually supposed to understand the entirety of a codebase; instead, if a system is properly broken down into a web of small, isolated objects, developers can supposedly implement changes simply by introducing new objects without having to master all the complex connections and interactions beneath the surface.

Brian remains deeply skeptical that object-oriented code ever truly delivers on that promise, rejecting the notion as a fundamentally flawed engineering philosophy. In his view, attempting to build systems while deliberately avoiding comprehension of how the pieces fit together is disastrous. Developers must understand their code, their tools, and the underlying platform environment. Delivering his concluding verdict on the mindset, Brian warns: “When we give up on understanding what we're actually doing when we write code it just leads to bad software.”

- **The Pragmatic Objective**: Command-line parsing only requires taking raw input strings and reshaping them into structured, queryable data structures such as hash maps. - **Sandi Metz's Concession**: Object-oriented code is inherently harder to comprehend in its entirety than procedural code, but OOP proponents argue total system comprehension should be unnecessary if responsibilities are atomized into small objects. - **The Core Counterargument**: Abdicating the responsibility to thoroughly understand code, tools, and runtime platforms inevitably degrades software quality rather than improving maintainability.

## Notable quotes > [00:01](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=1s) I have nothing nice to say about object-oriented programming, I don't like it, I think it's really been a disaster for the industry. — Brian Will

> [26:48](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=1608s) When we give up on understanding what we're actually doing when we write code it just leads to bad software. — Brian Will

> [11:30](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=690s) UML is just garbage, don't ever use it, it's crazy. — Brian Will

## Mentioned Brian Will, Sandi Metz, Robert C. Martin, Derek Banas, SOLID Object-Oriented Design, All the Little Things, Gilded Rose, Java, Ruby, JavaScript, Python, YAML, UML, YouTube, Vimeo

## Video description A follow up to https://www.youtube.com/watch?v=QM1iUe6IofM

[Source](https://www.youtube.com/watch?v=IRTfhkiAqPw)

中文简报

Brian Will 用过程式例程重写 Uncle Bob 与 Metz 的面向对象代码 面向对象编程会损害软件开发,因为赋予惰性数据能动性会产生错综复杂的类继承层级,从而掩盖直截了当的过程式转换。 Brian Will 认为,面向对象编程(OOP)给软件开发带来了不必要的复杂性、过度工程和哲学层面的混乱。为了证实这一观点,他分析了来自 Sandi Metz、Derek Banas 以及 Robert C. Martin(“Uncle Bob”)知名会议演讲中四个 200 行以内的真实代码范例,并在保留各自原始语言的前提下,采用直截了当的过程式范式进行了重写。他论证了用纯粹的数据结构和直接函数来取代人造的类继承层级、多态分发以及一次性包装器,如何显著提高清晰度与可维护性,并降低认知负荷。归根结底,Will 摒弃了将程序拆解为晦涩对象网络的 OOP 思维方式,他总结道,整洁的软件工程要求让数据保持惰性,并对执行流保持彻底的理解。 Brian Will 对主流面向对象编程发起挑战,他将来自 Sandi Metz、Derek Banas 和 Robert C. Martin 会议演讲中四个 200 行以内的范例,用其原有语言重写为过程式代码,以证明过程式设计更加清晰、简单。 (0:01) Will 认为,OOP 的根本缺陷在于赋予惰性数据意图与能动性,同时将行为转化为人造名词,从而将开发者拖入抽象的柏拉图式本质主义,脱离了实际工程。 (2:44) 该批评质疑了 OOP 中用多态取代分支逻辑的教条,指出动态语言缺乏编译期强制约束,且现实世界中的分支条件很少在系统各阶段保持平行。 (17:45) Will 反对为了让开发者无需理解整个系统而将代码库拆分为不透明对象网络的观点,并警告称,放弃理解软件的运作方式必然会导致更糟糕的工程实践。 (26:48)

Reading summary

Software developer Brian Will argues that object-oriented programming damages software quality by forcing developers to treat inert data as active agents, turning basic procedural operations into bloated, fragmented abstractions. Treating Passive Data as Active Agents (2:44) Will argues that the fundamental flaw of object-oriented programming is a conceptual trap: programmers imbue passive data with agency, intent, and duties, while turning basic actions into rigid nouns. This conflation pushes developers away from practical engineering toward analytical philosophy and Platonic essentialism, where defining the exact essence and boundaries of an entity takes precedence over writing functional software. When misdrawn boundaries inevitably produce convoluted code, object-oriented doctrine treats the resulting failure as a personal defect of the programmer rather than a defect of the paradigm, prescribing ever-increasing layers of design patterns and rules. Deconstructing SRP and Artificial Single-Method Classes (5:01) Analyzing a Ruby refactoring presented by Sandi Metz to demonstrate the Single Responsibility Principle, Will notes that the initial patent processing class contained zero internal state or fields, acting merely as an artificial namespace that required instantiation solely to execute a single run method. Metz's proposed solution further fragmented the routine into specialized classes, including an FTPDownloader that accepted configuration references through a constructor only to execute a single network download. Will contends that this approach introduces needless complexity. Rather than instantiating disposable objects, using metaprogramming to generate getter methods, or debating internal dependency wiring, the identical task could be solved procedurally with plain functions and native hash maps. Inlining single-use helper functions into a primary execution routine simplifies navigation and reduces cognitive load far more effectively than splitting trivial behavior across multiple classes. The Pretense of Eliminating Branching Logic (14:49) Using Metz's refactoring of the Gilded Rose kata and Derek Banas's UML-heavy coin-flip tutorial, Will challenges the object-oriented claim that polymorphism should replace conditional branching. Metz addressed nested updates by defining an abstract Item base class, three subclasses, a class registry hash map, and a static factory method. Will argues this design adds excessive overhead to four mutually exclusive business rules that can be expressed cleanly with a single switch statement or if-elsif ladder. While statically typed languages enforce interface implementations at compile time, Will points out that this safety net is entirely absent in dynamic languages like Ruby. Furthermore, he views the entire pursuit as misguided because real-world logic rarely branches over neatly parallel conditions across different execution contexts. Forcing divergent logic into class hierarchies creates synthetic types and diagrammatic bloat without practical engineering benefits. Parser Architecture and the Cost of Fragmented Objects (19:39) Contrasting his procedural design against an argument parser authored by Robert C. Martin (Uncle Bob), Will highlights the stark difference in architectural clarity. Martin separated the task into five distinct types, including an abstract base class, three concrete marshaler subclasses with recursive internal maps, and a coordinating Args class wrapped in exception-handling blocks. Will achieved identical functionality within a single Java class that parses inputs into three basic hash maps—one each for booleans, integers, and strings—using straightforward loops and explicit null checks. Will diagnoses Martin's elaborate structure as a cognitive byproduct of object-oriented ideology, which obscures the baseline reality that argument parsing is merely reshaping an array of strings into convenient key-value lookups. While proponents like Metz concede that object-oriented graphs are harder to comprehend in their entirety but argue developers need not understand the whole system, Will rejects this abdication of understanding as a recipe for fragile software. Will concedes that fully demonstrating the superiority of procedural architecture over object-oriented design requires comparing large systems, an effort he cannot execute alone. To test the hypothesis at scale, he invites developers to submit complete, idiomatic OOP modules ranging from 1,000 to 50,000 lines in accessible languages like JavaScript, Python, Java, or Ruby.

精炼阅读摘要

Brian Will 认为面向对象编程(OOP)给软件工程带来了负面影响,主张回归过程式编程以恢复代码的直接性与可理解性。他通过重写业界知名布道者的四个少于 200 行的真实案例,指出 OOP 强制将数据与行为捆绑、滥用抽象和类型系统,反而人为制造了认知负担与结构复杂性。 哲学根源:数据代理化与概念拟人陷阱 (2:44) Brian Will 提出“让数据回归数据”(let data be data)的核心原则。他认为 OOP 在概念层面存在根本缺陷:它迫使开发者将本应被动的惰性数据(inert data)塑造成具备意图、职责与主观行动能力的实体,同时将纯粹的操作与动作“名词化”,打包进死板的类结构中。这种融合使软件工程偏离了务实的工程实现,转向类似于柏拉图本质主义的概念思辨——例如在定义类似“椅子”的类时,被迫去界定“椅子本质”的边界。Will 认为,一旦对象边界划分出现纰漏,OOP 理论家往往将其归咎于开发者未遵循设计原则,随后开出更多设计模式或抽象规则,形成了不可证伪的教条循环。 职责拆解偏执:无状态类与过度解耦的低效 (5:01) 在分析 Sandi Metz 针对单一职责原则(SRP)所展示的专利处理 Ruby 案例时,Will 批判了那种为了隔离职责而不断生成微型类的做法。原始代码包含下载文件、解析与更新数据库三项操作,虽然塞在一个单元中略显别扭,但整类完全没有内部字段或持久状态,实质上只是一个瞬态的人工命名空间,调用者必须实例化一个毫无状态标识的对象仅为了运行一次方法。Metz 随后的重构进一步拆分出独立的 Config 类与 FTPDownloader 类,甚至借助元编程动态生成 getter 方法,并在构造函数中直接硬编码依赖。Will 反驳称,解析 YAML 配置完全可以用返回原生哈希表的独立函数解决,单一用途的小型辅助函数也完全可以直接内联到主流程中,无需引入一次性对象来徒增调用栈和认知成本。 形式主义建模:UML失效与多态对条件分支的滥用 (11:30) Will 针对 Derek Banas 抛硬币教学用例与 Metz 的 Gilded Rose 重构提出批评,揭露了面向对象建模的形式主义弊端。在 Banas 的案例中,一个仅需读取两名玩家、随机掷币并打印胜者的微型脚本,被套上了用例图、时序图以及切分成四个类的 UML 体系。Will 直言统一建模语言(UML)是无效负担,在现实软件规模化时必定沦为混乱的线框交织,无法替代命名清晰的文本代码。而在 Gilded Rose 的案例中,Metz 引入抽象基类、三个具体子类、字符串查找哈希表以及类工厂方法来替代嵌套条件判断。Will 指出,真实业务逻辑很少是整齐平行的类型派发,在 Ruby 这种缺乏编译期类型检查的动态语言中,用庞大的类继承和多态机制来替代直截了当的 switch/case 分支,属于本末倒置的过度设计。 数据转换迷失:Uncle Bob 参数解析器的对象膨胀 (19:39) 面对 Robert C. Martin(Uncle Bob)编写的 Java 命令行参数解析器,Will 对比了两种实现路径。Martin 的重构方案设计了一个抽象的 ArgumentMarshaler 类及其派生出的布尔、整型、字符串三个具体子类,甚至在子类内部使用了让 Will 感到匪夷所思的递归映射结构,并由 Args 协调类包裹大量异常捕获。Will 指出该程序的核心仅仅是将字符串数组转换为便于下游查询的数据,他改用单个包含三个类型化哈希表的 Args 类,仅通过两次线性循环(一次解析 schema 生成默认值,一次解析输入参数填充映射)便完成了相同功能。Will 认为,OOP 范式蒙蔽了开发者的判断力,使其无法看透简单算法的本质,把直观的数据结构转换撕裂为多类协同的碎片化逻辑。 针对 Sandi Metz 提出的“开发者无需理解系统全貌、只需在局部添加小对象即可工作”的 OOP 防御论调,Brian Will 予以坚决驳斥,强调放弃对底层代码和平台运作的透彻理解必将导致劣质软件。受限于单人测试规模,Will 未能在数十万行级的大型项目上对比过程式与 OOP 的维护差异,但他向外界公开征集 1,000 至 50,000 行的真实工程模块,希望通过后续实践进一步验证其论点。

Detailed notes — zh

## Background 该视频由 Brian Will 于 2016 年 3 月 4 日发布在 YouTube 频道上,是其此前引发广泛争议的理论探讨视频《面向对象编程很糟糕》(Object-Oriented Programming is Bad)的实战续作。作者在视频简介中附上了前作链接,并旨在通过 4 个 200 行以内的具体代码重构示例,直接展示面向对象编程范式在实际落地中的尴尬与弊端。 ## TL;DR Brian Will 通过剖析 Sandi Metz、Derek Banas 和 Robert C. Martin(Uncle Bob)等面向对象代表人物的 4 个真实教学案例(涵盖专利处理、抛硬币模拟、Gilded Rose 和 CLI 参数解析),对比了面向对象重构与纯面向过程重构的代码设计。他尖锐指出,面向对象强行将惰性数据拟人化为“行为实体”,并为了追求所谓的单一职责、多态和 UML 建模,制造了大量冗余类、动态派发和自引用结构,导致代码严重碎片化。相反,回归“数据做数据、动作做动作”的过程式思维,仅用直观的函数、条件分支与扁平数据结构就能以更低的心智负荷优雅解决问题,彻底击碎了 OOP 大师们宣称的设计神话。 ## Atmosphere & style 整个视频洋溢着一种务实、犀利且毫不妥协的批判基调。Brian Will 带着技术现实主义者的冷峻与直率,剥离了面向对象社区中常见的玄学话术与权威光环,对 industry gurus 奉为圭臬的架构模式进行了毫不留情的“解剖”。听他抽丝剥茧地重构代码,既有一种看穿过度设计后的荒谬与快意,又充满着对朴素工程逻辑与全盘掌控力的呼唤,极易引发饱受样板代码折磨的工程师的强烈共鸣。 ## Bulletin - [00:01](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=1s) Brian Will 通过 4 个 200 行以内的真实公开范例(Java 与 Ruby 各半),在不更换语言的前提下重构为面向过程风格,力证面向对象编程(OOP)不仅过度复杂而且给软件工业带来了灾难性后果。 - [02:47](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=167s) 批判核心哲学:数据本身是惰性的,OOP 强行赋予数据“意图与能动性”并将所有动词名词化,迫使程序员陷入分析哲学与本质主义的循环论证困境。 - [05:01](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=301s) 案例分析直指业内权威:拆解 Sandi Metz 的专利处理类与 Gilded Rose 重构,以及 Uncle Bob 的命令行解析器,指出所谓的单一职责与多态常导致无意义的类膨胀和认知负荷剧增。 - [27:07](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=1627s) 坚决驳斥“微小对象使得程序员无需理解全局系统即可开发”的论调,强调放弃理解代码与底层平台只会不可避免地导致劣质软件。 ## Key takeaways - [02:47](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=167s) Brian Will 秉持“让数据归数据,动作归动作”的哲学,反对 OOP 强行把单纯的数据想象为具有职责与能动性的实体。 - [02:47](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=167s) OOP 辩护常陷入循环论证:一旦设计落地困难或变成意面代码,宣导者总会归咎于开发者“做错了”,并抛出更多繁复规则。 - [05:01](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=301s) Sandi Metz 的专利处理类本质上没有任何字段,仅仅充当了命名空间,却被迫实例化并引入更多中间类来满足单一职责原则(SRP)。 - [07:21](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=441s) Config 类与 FTPDownloader 类展示了典型的类膨胀:仅仅为了读取 YAML 返回配置或执行一次下载,便无端创建了包装类和生命周期管理负担。 - [09:00](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=540s) 面向过程方案仅需返回简单哈希表(Hash)的纯函数即可替代整套对象装配,内联单次使用的辅助逻辑能显著降低系统认知负荷。 - [11:30](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=690s) Derek Banas 的抛硬币游戏演示揭示了 UML 建模的形式主义破产,基于框线箭头的可视化在稍复杂的调用拓扑中极难扩展,远不如直接的文本函数调用。 - [14:49](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=889s) Gilded Rose 范例的原代码混乱源自互斥业务条件划分不清,而非分支结构本身有原罪,引入基类和子类多态派发完全是过度设计。 - [17:45](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=1065s) “用多态消灭所有分支”的辩护在动态语言(如 Ruby)中破产,因为缺少编译器强制检查,且现实中工整对称的并行 switch 极为罕见。 - [20:43](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=1243s) Uncle Bob 的命令行参数解析器为了处理三种数据类型,生造出 ArgumentMarshaler 抽象类、三个派生类及递归字典结构,严重碎片化了控制流。 - [22:35](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=1355s) 面向过程的 Args 替代方案仅用一个类和三个标准 HashMap,在构造函数中以两步扁平扫描完成解析,大幅缩减了系统的对外暴露面。 - [26:48](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=1608s) 命令行解析的本质是“将字符串数组转换为便于查询的数据结构”,OOP 的封装仪式感遮蔽了这一朴素数据流转的本质。 - [27:07](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=1627s) 驳斥 Sandi Metz 关于“无需理解系统全局、仅需堆砌微小对象即可扩展”的主张,指出放弃对代码及底层环境的透彻理解必将导致劣质软件。 ## Detailed walkthrough ### [00:01](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=1s) 缘起与面向过程的重构实验 Brian Will 提到自己此前发布了一期题为《面向对象编程很糟糕》(Object-Oriented Programming is Bad)的视频,在理论层面上剖析了为何不应采用面向对象编程(OOP)。当时不少观众批评这一标题过于“标题党”(clickbaity),但他坚决反对这种说法,认为事实确实如此:他对面向对象编程毫无好感,并认定该范式对软件工业造成了灾难性的后果。为了进一步推进并落实这一论点,他决定推出后续视频,直接拿出实际代码作为例证,并将视频命名为更加尖锐的《面向对象编程令人尴尬》(Object-Oriented Programming is Embarrassing)。 在这期视频中,Brian Will 准备了四个篇幅极短的代码示例,每个都在 200 行以内。其中两个采用 Java 编写,另外两个采用 Ruby 编写。他的对比方式十分克制且公平:完全不更换编程语言,Java 代码依旧用 Java 重写,Ruby 代码依旧用 Ruby 重写,只是将范式从面向对象全部转为纯粹的面向过程(procedural)风格。他强调这些示例绝非恶意挑选,而是他在 YouTube 和 Vimeo 上直接搜索面向对象设计演讲时,随手找到的最靠前的几个实例。演讲者们原本将这些代码当作范例,用来展示面向对象设计如何规范运作并改善代码质量。Brian Will 坦言,这正是他着手寻找时最先撞见的例子。 他也清醒地意识到,仅靠 200 行以内的微型示例很难彻底说服所有人。OOP 的拥护者往往会辩解称,面向对象编程的优势只有在大型代码库中才会显现,能够防止代码随着规模膨胀而沦为“意面式代码”(spaghetti code)。对此,Brian Will 认为理想的验证方式是将一个 50 万行的系统在 OOP 和面向过程之间双向重写并对比结果,但出于工作量的考量,现实中没人能完成这种工程。不过,他计划后续逐步拓展到 1,000 至 2,000 行、10,000 行乃至 50,000 行规模的代码案例。他向观众公开征集开源仓库或子模块代码,语言偏向主流的 Java、JavaScript、Python 或 Ruby(尤其是 JavaScript,因其受众最广),唯一的核心前提是该代码必须是真正贯彻面向对象设计理念的代码,而非徒具形式的表面封装。 - 视频包含 4 个代码示例,单例代码量均控制在 200 行以内。 - 语言分布:2 个 Java 示例,2 个 Ruby 示例,重构前后严格保持语言不变。 - 示例来源:直接取自 YouTube 和 Vimeo 上公开讲解面向对象设计的技术演讲。 - 后续规划:寻求 1,000 至 2,000 行、10,000 行左右的真实开源项目进行更大尺度的重构验证。 --- ### [02:47](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=167s) 核心哲学冲突:让数据归数据,动作归动作 在深入分析具体代码之前,Brian Will 重新概括了他反思面向对象编程的核心哲学:那就是“让数据做数据,让动作做动作”(let data be data, let action just be actions)。他指出,OOP 强迫程序员将原本单纯的数据脑补为具有某种职责、能在现实世界中采取行动的实体,从而在本质上给死气沉沉的数据强行注入意图与能动性(intent and agency)。在 Brian Will 看来,这是一个根本性的错误,因为数据本身就是惰性且毫无生气的,强行将其构想为“行动者”只会徒增系统混乱。 与此相对,系统中的操作与动作也应当独立存在,程序员根本没有必要把所有想执行的代码行为都挂载到某种数据结构上,也就是所谓的“把所有动词都名词化”(nounify all our verbs)。正是这种将动作与数据混为一谈的做法,让面向对象编程变得极其晦涩。程序员若是听信了 OOP 大师(gurus)的布道,写代码的过程就会荒诞地蜕变为求解分析哲学与柏拉图本质主义(analytical philosophy and platonic essentialism)的难题。 以设计一个“椅子类”(`Chair`)为例,开发者不得不反复揣摩椅子的所谓“真实本质”与其核心属性。大师们会警告你:一旦实体边界划分出现偏差,你就破坏了面向对象的某条黄金准则;而一旦代码日后滑向混乱的意面状态,大师们曾经许诺的种种设计收益就全部作废,并且错全在你,因为你没有完全按照他们意图的规则来划分抽象。Brian Will 犀利地指出,面向对象编程的整套辩护逻辑完全是循环论证(circular reasoning):每当你发现面向对象设计难以落地且收益不明时,大师给出的解释永远是“那是因为你做得不对”,随即再甩给你十条新的设计准则与指导方针。 > “I think it's really true I have nothing nice to say about object-oriented programming I don't like it I think it's really been a disaster for the industry.” —— Brian Will > “What object-oriented programming demands is that we're supposed to be conceptualizing our data as not just data but as some sort of thing with responsibilities and something which acts in the world we basically have to imbue our data with intent and agency and that's just a fundamental mistake data does not do things data is just inert and that's the way it should be.” —— Brian Will --- ### [05:01](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=301s) 案例一解析:Sandi Metz 的专利处理类与单一职责原则之惑 Brian Will 剖析的第一个案例源自著名 Ruby 开发者 Sandi Metz 的演讲《SOLID 面向对象设计》(SOLID Object-Oriented Design)。该演讲的核心演示是一个极简的专利处理类,负责执行一个完整的工作流:下载专利文件、解析该文件,最后更新数据库中的专利信息。Sandi Metz 借此示例演示由 Robert C. Martin(Uncle Bob)提出的 SOLID 原则中的“单一职责原则”(Single Responsibility Principle, SRP),即一个类或方法应该只做一件事。 但 Brian Will 提出质疑:到底什么才叫“只做一件事”?Uncle Bob 给出的经典定义是“一个类应当仅有一个引起它变化的原因”(only one reason to change)。Brian Will 认为哪怕经过了长篇大论的诠释,这个定义依然极度模糊。回到 Sandi Metz 给出的原始 Ruby 类中,整个类仅包含 4 个方法:入口核心方法是 `run`,它按序调度其余 3 个私有辅助方法:`download_file`、`parse` 以及 `update_patents`。 ```mermaid flowchart TD accTitle: Sandi Metz 原始专利处理类结构 accDescr: 展示原始 Patent 类中 run 方法调度其余三个无状态辅助方法的调用流程 subgraph PatentClass["Patent 类(无任何实例字段)"] R["run()"] D["download_file()"] P["parse()"] U["update_patents()"] R --> D R --> P R --> U end ``` Sandi Metz 对这种设计很不满意,理由是该类承担了过多的职责——既负责下载文件,又负责解析,还负责更新数据库。Brian Will 在这一表层现象上表示赞同,将文件 I/O、格式解析和数据库更新混在同一块代码里确实很怪异。然而,从更深层的代码本质来看,这个类内部根本没有任何成员变量(fields)。它名义上是个类,本质上却只是一个简陋的命名空间(namespace)。 更荒唐的是,由于身处面向对象的世界,哪怕只是为了调用一次它的逻辑,你也必须先将其实例化(`instantiate`)。程序员不得不无端去管理一个生命周期毫无持久价值的对象实例,在仅仅调用一次 `run` 方法后就立刻将其丢弃。面对这样一个本身就不合理的类,Sandi Metz 给出的药方不仅没有回归简单,反而是典型的面向对象式方案:引入更多的类。 > “The definition given by Robert Martin AKA Uncle Bob who coined the SOLID principles and SRP the single responsibility principle his definition is that a single class a single unit should have only one reason for change now what exactly is one reason for change well it's something he explains at length I think it's even after explanation I think it's a very nebulous idea which is part of the problem.” —— Brian Will --- ### [07:21](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=441s) 类膨胀与伪封装:配置类与下载器类的荒谬抽象 为了拆分原有的专利处理类,Sandi Metz 在重构中首先引入了一个专门的 `Config` 类,用以剥离并泛化程序的配置逻辑。虽然分离配置项本身是一项合理的工程考量,但该类的实现方式却充斥着 OOP 特有的冗余。`Config` 类的构造函数(即 Ruby 中的 `initialize` 方法)接收一个可选参数包 `options`,根据这些参数去寻找并加载指定的 YAML 配置文件。 在此基础之上,Sandi Metz 运用了 Ruby 的元编程(metaprogramming)机制:在 `define_methods_for_environment` 这类方法中,代码遍历从 YAML 读取出来的键值对,动态地在 `Config` 实例上挂载与配置项同名的方法。这意味着整个类的最终目的,仅仅是为了让调用者能以调用方法的形式去获取配置值。对此,Brian Will 提出了尖锐的反问:为什么要为此专门造一个类?直接用一个函数读取参数、解析 YAML 文件,并返回一个普普通通的哈希表(hashmap)不好吗?为键值映射套上一层类的外壳到底创造了什么实际价值? ```mermaid flowchart LR accTitle: Config 与 FTPDownloader 的对象化流转 accDescr: 展示从 options 经由元编程生成 Config 实例,并注入 FTPDownloader 的过程 subgraph ConfigGen["Config 实例化"] OPT["options 选项"] --> INIT["initialize() 读取 YAML"] INIT --> META["元编程动态挂载方法"] META --> CFG["Config 实例"] end subgraph Downloader["FTPDownloader 实例化"] CFG --> D_INIT["initialize(config) 保存为内部属性"] D_INIT --> D_RUN["download() 执行下载"] end ``` 不仅如此,Sandi Metz 还将文件下载逻辑剥离到了另一个独立的类 `FTPDownloader` 中。在 Brian Will 看来,这里暴露出同样的荒谬性:为了使用下载功能,调用者必须实例化 `FTPDownloader`,并在构造函数中传入之前生成的 `Config` 实例。这个内部保存的 `config` 属性对于任何使用下载器的外部代码来说完全不可见,外部也绝不需要访问它。这不仅造成了不必要的对象生命周期管理负担,也证明了面向对象范式为了追求所谓的“单一职责”,是如何在没有数据实体支撑的前提下,生造出大量毫无必要的中间类与样板封装。 - `Config` 类:解析 YAML 配置文件,借助元编程为每个配置键生成对应的方法调用,实质就是一个哈希表包装器。 - `FTPDownloader` 类:构造函数强行接收并封存一个 `Config` 对象,外部仅调用其单一行为,存在典型的“为了面向对象而面向对象”的类膨胀。 ### [09:00](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=540s) 过程式代码的救赎:消除过度设计的类与依赖 在审视经过重构的 `PatentJob` 类时,Brian Will 指出其构造函数内部直接创建了 `Config` 对象并将其传递给 `Downloader` 对象,随后将该下载器实例保存在一个字段中,仅为了稍后在 `run` 方法中调用一次。他对此感到十分困惑并提出质疑:“为了调用一个 `download_file` 方法,你居然要创建一个完整的类实例,就为了调用这一个方法,为什么我们需要一个类?为什么它不能就只是一个函数?”即便站在面向对象编程的角度,他也惊讶于原作者居然没有采用构造函数注入(Constructor Injection)或依赖注入(Dependency Injection),而是让 `PatentJob` 直接在内部实例化这些依赖;尽管他个人反感面向对象,但他承认依赖注入至少提供了一种更理智的对象组装方式。 ```mermaid flowchart TD accTitle: 面向对象重构与过程式设计的对比 accDescr: 展示了面向对象复杂的对象嵌套与方法调用链路,对比过程式单向直接的数据流。 subgraph OO["面向对象方式(过度设计)"] PJ["PatentJob 构造函数"] -->|"new"| CFG["Config 实例"] PJ -->|"new(config)"| DL["Downloader 实例"] DL -->|"保存为字段"| FIELD["@downloader"] FIELD -->|"run() 中调用"| METH["download_file()"] end subgraph Procedural["过程式方案(清晰直接)"] GC["get_config()"] -->|"返回 Hash 字典"| PARSED["解析后的 YAML 配置"] PARSED --> PP["process_patent()"] PP -->|"直接执行"| DONE["完成文件下载与处理"] end ``` 然而,对 Brian Will 而言,争论面向对象的组装方式根本就是多余的,因为这段逻辑完全可以用直接的过程式代码替代。在 Ruby 中,只需编写一组顶层内核方法(Kernel methods)——也就是纯粹的函数。通过一个 `get_config` 函数解析 YAML 文件并返回一个包含字段的 Hash 字典,所有类与构造函数的包袱便烟消云散。他进一步主张,如果某些微型辅助函数除了在 `process_patent` 中被调用一次之外别无他用,完全应当直接内联进去。当一个新手接手代码库时,理解 3 个函数比理解 5 个函数轻松得多;如果在大型项目中能将函数总量从 100 个缩减到五分之三,系统的认知负荷将显著降低,开发者再也无需无休止地推测那些无用类和孤立函数的深层意图。 - 批评核心:创建一个生命周期极短的对象仅为了调用其单一方法,是面向对象设计中最典型的荒谬形式。 - 架构对比:直接返回哈希表(Hash)的过程式函数,比定义专有的配置数据类更加直观且无状态负担。 - 认知负荷管理:减少代码库中独立符号与函数的数量,避免读者去凭空猜测某些辅助抽象是否会在其他地方被复用。 ### [11:30](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=690s) UML 的破产:以抛硬币游戏为例 Brian Will 转向了 Derek Banas 早期关于面向对象设计的系列视频。尽管 Derek Banas 近期的编程教学视频相当优秀,但该系列第二期演示的抛硬币游戏模拟程序却堪称灾难。这个程序的业务需求极其简单:模拟两人抛硬币,随机决定正反面并选出获胜者,最后打印出某某以正面或反面获胜,并用循环询问玩家是否再来一局。然而,原作者为了展示基于统一建模语言(UML)的正式建模技术,先后构建了用例图、包含三个类(`Player`、`Coin`、`CoinGame`)的类图、对象模型以及时序图,最终加上包含 `main` 入口类的 `CoinFlippingGame`,共拼凑出四个类。 ```mermaid flowchart LR accTitle: 抛硬币模拟程序中 UML 与面向对象的过度工程 accDescr: 说明了一个仅需打印随机结果的简单逻辑,被拆解为用例、类图、时序图以及四个不同类的繁琐过程。 subgraph Design["无谓的建模过程"] UC["用例分析"] --> CD["类图 (Player, Coin, CoinGame)"] CD --> OM["对象模型"] OM --> SD["时序图 (时间流与消息传递)"] end subgraph Impl["臃肿的 Java 实现"] SD --> C1["Coin 类"] SD --> C2["Player 类"] SD --> C3["CoinGame 业务类"] SD --> C4["CoinFlippingGame (main 容器类)"] end ``` Brian Will 对这种层层建模嗤之以鼻,直言“这里的教训就是:UML 完全是垃圾,永远不要去用它,这简直太疯狂了”。在实际实现中,抛开 Java 必须用类包裹 `main` 方法的技术限制不谈,核心逻辑只需要一个接收两个字符串并随机选取胜者与硬币朝向的普通过程式函数。他痛批各种画框和箭头的建模工具不仅极度难用,而且存在底层认知缺陷:一旦软件复杂度稍有上升,实体之间的连线就会变成混乱的乱麻,根本无法阅读。文本代码允许函数通过命名引用自由组合,而可视化建模语言之所以从未成为主流,正是因为这种图形化表征在可扩展性上存在致命硬伤。 - 需求本质:程序根本不具备真正的交互操作,纯粹是打印一段由随机数生成的预设格式文本。 - 工具批判:UML 建模软件繁琐低效,绘制箭头和矩形框并不能帮助理清逻辑,反而在形式主义上浪费精力。 - 表达优势:基于文本的函数命名与直接调用,在处理复杂调用拓扑时远远优于连线交错的视觉图表。 ### [14:49](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=889s) 多态的幻觉:Gilded Rose 重构的过度复杂化 随后,Brian Will 剖析了 Sandi Metz 在其知名演讲《All the Little Things》中使用的 Gilded Rose(镀金玫瑰)重构案例。原代码充斥着大量嵌套晦涩的 `if-else` 分支与《魔兽世界》(World of Warcraft)的彩蛋术语,属于公认需要清理的糟糕代码。Sandi Metz 提出的面向对象救赎方案是将所有东西包裹在 `GildedRose` 类中,定义包含空 `tick` 方法的抽象 `Item` 基类,派生出 `Normal`、`Brie`、`Backstage` 三个特化子类,并建立一个由字符串映射到具体类的哈希表,最后通过一个类似静态工厂方法的类方法 `GildedRose.for` 实例化对象并调用其 `tick` 方法,从而利用多态消除分支。 ```mermaid flowchart TD accTitle: Sandi Metz 的多态方案与直接分支语句的方案对比 accDescr: 对比了引入基类、子类工厂和多态派发,与直接使用条件分支处理四种业务情形的复杂度差异。 subgraph Sandi["Sandi Metz 的多态重构"] GR["GildedRose.for 工厂方法"] --> LOOKUP["Hash 映射表查找"] LOOKUP -->|"创建"| SUB["子类 (Normal / Brie / Backstage)"] SUB -->|"继承自"| BASE["Item 抽象基类"] SUB -->|"多态调用"| TICK["各自重写的 tick()"] end subgraph Simple["过程式直接实现"] COND{"识别四种互斥情形"} COND -->|"case 'normal'"| A1["处理普通逻辑"] COND -->|"case 'Aged Brie'"| A2["处理干酪逻辑"] COND -->|"case 'Backstage...'"| A3["处理门票逻辑"] COND -->|"default"| A4["无操作"] end ``` Brian Will 承认这种做法极具巧思且充分利用了 Ruby 的元编程与动态语言特性,但他一针见血地指出,原代码之所以丑陋,根源在于它没有正确识别出系统中那四个真正互斥的业务场景,而不是因为缺少了多态。只要梳理出“无操作”、“普通物品”、“陈年干酪(Aged Brie)”以及“后台门票(Backstage Passes)”这四种情况,用一个简单的 `switch` 语句或 `if-else` 阶梯就能清晰表达。针对这种言必称多态的风气,他反驳道:“当你的代码根本不需要它们时,你为什么非要引入更多的数据类型概念?这简直是本末倒置。”过程式代码完全可以用更少的心智概念达到甚至超越多态重构后的整洁度。 - 根因分析:老代码结构混乱的本质是对业务互斥条件划分不清,而非分支结构本身有原罪。 - 抽象过载:为了消灭几个分支而引入一个抽象基类、三个具体子类以及一个动态查找分发映射,属于本末倒置。 - 过程式反击:直截了当的 `switch-case` 逻辑不仅行数更少,而且能把所有业务规则平铺直叙地呈现在眼前,免去了在多态继承树中穿梭跳转的痛苦。 ### [17:45](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=1065s) 过程化替代与多态分支的虚妄优势 回顾前面的例子,Brian Will 指出代码里仍然带有面向对象的残余——比如将某些操作写成类的方法并使用实例变量。在纯粹的过程式编程视角下,根本不需要这些概念;可以直接写一个 `tick` 函数,将名称和具体的数据对象传进去,然后直接操作其成员即可。谈及“用多态替代所有分支(branching)”的主张,Brian Will 梳理了支持者通常给出的辩护:在代码库中,往往会针对某项数据使用 `switch` 语句,并且类似的并行 `switch` 会散落在很多地方;如果用多态来表达这些逻辑,在静态类型语言(如 Java)中,编译器会强制要求实现接口的所有方法,这样添加新类型时就不容易遗漏某些分支处理,避免了普通 `switch` 容易漏改某处的问题。 ```mermaid flowchart TD accTitle: 静态语言多态与动态语言对比 accDescr: 展示静态类型语言中接口编译检查与动态语言中重写缺失无报错的对比 subgraph Static["静态语言 (如 Java)"] A["定义接口与抽象方法"] --> B["派生新类实现接口"] B --> C{"是否漏写方法?"} C -- "是" --> D["编译报错 (强制补全)"] C -- "否" --> E["安全运行"] end subgraph Dynamic["动态语言 (如 Ruby)"] F["继承抽象类"] --> G{"是否遗漏重写?"} G -- "是" --> H["无任何警告/运行时报错"] end ``` 然而,Brian Will 尖锐地反驳了这种观点,指出该论点在动态语言(如 Ruby)中彻底站不住脚。在动态语言中,子类就算漏掉了重写父类的方法,也不会获得任何语言层面的编译警告。更核心的是,现实开发中这种工整对称的“并行 switch”极为罕见。现实业务往往只是半并行的(semi-parallel),代码某处可能根据一组属性判断,而另一处分支逻辑所依赖的条件截然不同。费尽心机将普通分支重构为基于类型的多态派发,不仅增加了极大的概念认知负担,长远来看往往得不偿失。 > “I still think polymorphic branching is a perverse idea mostly because I think these neatly parallel switches are actually quite rare.” —— Brian Will - 支持者观点:静态类型语言通过接口或抽象类强制子类实现所有方法,降低添加新分支时疏漏的概率。 - 动态语言反例:在 Ruby 等语言中,继承后未覆盖方法不会产生任何语言级警告,多态防遗漏的论据不攻自破。 - 业务实际:绝大多数条件分支只是部分重合(semi-parallel),不同位置的分支考量差异极大,强行抽象为“类型多态”只会增加心智负担。 --- ### [19:39](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=1179s) 示例四:Uncle Bob 的命令行参数解析器 第四个例子来自面向对象领域最具代表性的权威之一——Robert C. Martin(常称 Uncle Bob)。Uncle Bob 在一次演讲中展示了一个用于解析命令行参数的程序,其目标是将原始参数字符串解析为方便查询的数据结构,比如查询某个标志(flag,如 `-b` 或 `-c`)的具体取值。参数规范通过一个格式化的 schema 字符串定义:以逗号分隔,无特殊标记的字符代表布尔值(Boolean),带 `#` 的代表整数(Integer),带 `*` 的代表字符串(String)。 假设输入的 schema 字符串声明了整数标志 `j`、布尔标志 `f` 和字符串标志 `w`,当传入命令行参数 `-j 35 -w blahblah` 时,解析器应当得出:`j` 的值为 35;由于没有提供 `-f` 参数,`f` 自动默认为 `false`;而 `w` 的值则为 `"blahblah"`。Uncle Bob 在讲座中先给出了一个自认为不够好的版本,随后进行了“清理重构”。Brian Will 直言,那个所谓的原始糟糕版本和重构后的“更好版本”相比半斤八两,因此他直接展示 Uncle Bob 重构后的面向对象方案。 ```mermaid flowchart LR accTitle: 命令行 Schema 解析映射 accDescr: 展示根据 schema 规则解析命令行参数到对应值的映射过程 subgraph Input["输入参数"] S["Schema: j#, f, w*"] A["CLI Args: -j 35 -w blahblah"] end subgraph Output["解析结果"] J["j: 35 (Integer)"] F["f: false (默认 Boolean)"] W["w: 'blahblah' (String)"] end Input --> Output ``` - Schema 规则: - 无符号后缀:布尔标志(Boolean) - `#` 后缀:整数标志(Integer) - `*` 后缀:字符串标志(String) - 解析示例:在 `j#, f, w*` 规则下,输入 `-j 35 -w blahblah` 会将 `j` 设为 35、`f` 设为 `false`(默认值)、`w` 设为字符串 `"blahblah"`。 --- ### [20:43](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=1243s) Uncle Bob 的 OO 解法:臃肿的抽象与递归 Marshaler Uncle Bob 的解决方案构建了繁复的类继承体系。首先是一个名为 `ArgumentMarshaler` 的抽象类,内部声明了抽象方法 `set` 和 `get`。Brian Will 疑惑地吐槽:既然没有任何成员字段,为什么不用 `interface` 而要写一个抽象类?接着,该抽象类派生出三个具体类:`BooleanArgumentMarshaler`、`IntegerArgumentMarshaler` 和 `StringArgumentMarshaler`。这三个类的内部结构与行为几乎完全一致,唯一的区别仅在于分别处理布尔、整数和字符串类型。 这些 Marshaler 被汇总到一个主控制类 `Args` 中。在 `Args` 的 `main` 函数里,程序实例化 `Args` 并传入 schema 与命令行实参,随后通过 `getBoolean`、`getInt`、`getString` 等方法按键查询,最后调用一个未实现的 `executeApplication` 方法开启主流程。更关键的是,这一连串查询与实例化都必须包裹在一个庞大的 `try-catch` 块中,因为它们随时可能抛出自定义的 `ArgsException`。 ```mermaid flowchart TD accTitle: Uncle Bob 的面向对象类继承体系 accDescr: 展示 Uncle Bob 方案中抽象类、派生类与主调用类之间的继承与依赖关系 AM["abstract class ArgumentMarshaler\n(+set, +get)"] AM --> BAM["BooleanArgumentMarshaler"] AM --> IAM["IntegerArgumentMarshaler"] AM --> SAM["StringArgumentMarshaler"] BAM --> Args["class Args\n(包含 main 入口)"] IAM --> Args SAM --> Args Args --> App["executeApplication(...)"] ``` 更令人匪夷所思的是这些类的字段设计。以 `BooleanArgumentMarshaler` 为例,它不仅包含一个用于存值的布尔字段,竟然还包含一个名为 `booleanArgs` 的 `Map` 字段,用于将字符映射到其他的 `ArgumentMarshaler`。Brian Will 表示完全无法理解为何这里的类型必须设计成自引用的递归结构,甚至在实际运行中该 Map 存放的全都是具体的 `BooleanArgumentMarshaler`。 > “I don't know why he's using an abstract class rather than an interface because there's no you know there's no actual fields here so why why have an abstract class and not an interface?” —— Brian Will - 类结构膨胀:包含 `ArgumentMarshaler` 抽象类及三个具体派生类(Boolean、String、Integer),逻辑重复度极高。 - 异常驱动控制流:`Args` 构造与取值方法全部可能抛出 `ArgsException`,客户端调用必须强行嵌套 `try-catch`。 - 奇怪的递归设计:每个具体 Marshaler 内部还持有映射到父类 Marshaler 的字典,结构莫名复杂且缺乏合理解释。 --- ### [22:35](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=1355s) 过程化对比:单类三表与直接的数据转换 随后,Brian Will 展示了自己的替代方案。他没有拆分出 5 个类,而是只保留了一个单一的 `Args` 类。类的内部仅有三个核心字段,分别是针对整数、字符串和布尔值的标准映射表:`HashMap<Character, Integer>`、`HashMap<Character, String>` 以及 `HashMap<Character, Boolean>`。 所有的解析工作集中在 `Args` 的构造函数中完成,分为简洁的两步: 1. **解析 Schema 字符串**:按逗号切分字符串,遍历每个元素,根据字符后的特殊标识(`#`、`*` 或无后缀)将其归入对应的 HashMap 中。此时存入默认初始值——布尔型默认设为 `false`(符合未指定标志即为假的一般常理),整数和字符串则统一设为 `null`。 2. **遍历处理实参列表**:依次读取实际传入的参数字符串,若不是以 `-` 开头则直接忽略;若是,则取出紧跟的字符,检查该字符落在哪个类型的 Map 中,并把紧接着的值赋给该 Map。如果遇到了从未在 Schema 中声明的非法字符,则在 `else` 分支直接抛出异常。 ```mermaid flowchart TD accTitle: Brian Will 的扁平化解析流程 accDescr: 展示在单一构造函数中顺序解析 schema 和实参的过程 subgraph Step1["步骤 1: 解析 Schema"] S1["按逗号切分 Schema 字符串"] --> S2["识别类型后缀 (#, *, 无)"] S2 --> S3["填充三张 HashMap 并赋默认值\n(Bool: false, Int/Str: null)"] end subgraph Step2["步骤 2: 解析命令行参数"] A1["遍历命令行实参"] --> A2{"是否以 '-' 开头?"} A2 -- "否" --> A3["直接忽略"] A2 -- "是" --> A4{"判断字符属于哪张 Map"} A4 -- "在 Map 中" --> A5["解析参数并写入该 Map"] A4 -- "未在 Schema 声明" --> A6["抛出异常"] end Step1 --> Step2 ``` 在数据读取方面,Brian Will 指出虽然可以保留 `getBool`、`getString` 和 `getInt` 包装方法以匹配原有签名,但他更倾向于直接将那三个 HashMap 暴露为 `public`,并采用显式的错误处理替代异常控制流。比如通过 `args.ints.get('j')` 获取数值,显式判断其是否为 `null`;尽管由于 Java 苛刻的基本类型(Primitives)与包装对象类型(Object Types)分离机制,使得空指针检查代码略显啰嗦,但比起将控制流交给隐式异常,显式处理要清晰得多。 > “In my version of the code we had an args data type it had its constructor and there was the main method and that's the entire surface level of the code.” —— Brian Will - 单一职责类:仅维护一个 `Args` 类和三张映射表(`ints`、`strings`、`booleans`)。 - 集中直观的构造解析:第一步初始化并赋初值,第二步线性扫入实参设值,遇到非法字符报错,流程完全扁平。 - 显式访问与报错:倾向于直接读取公开的 Map 字段并做非空判断,避免冗余的包装方法和异常机制裹挟主流程。 --- ### [24:57](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=1497s) 复杂性的根源:面向对象遮蔽了朴素的逻辑 对比两套方案,代码的认知成本存在天壤之别。在过程化的解法中,整个代码的对外暴露面只有 `Args` 数据类型、其构造函数和 `main` 入口,任何人阅读代码时都能一眼看清流程的起点与脉络。而在 Uncle Bob 的面向对象方案中,平白无故生出了 5 个类、怪异的抽象类定义、令人费解的“Marshaler”隐喻,以及不合常理的递归数据结构。读者为了理解一个简单的参数解析功能,必须在散落的细碎方法和类继承关系之间反复横跳。 Brian Will 总结道,Uncle Bob 之所以写出这样的代码,根本原因在于其深受面向对象范式的束缚。过度执念于将现实逻辑抽象为对象,导致程序员失去了洞察问题本质简单性的清醒视野,因而无法构思出常识性的简约方案。虽然过程化代码中包含了循环和分支判断,但在视觉和逻辑上它们是连续、透明的;而面向对象版本并没有消除这些复杂性,只是将其切得细碎后随意抛向空中,让逻辑变得支离破碎。只要回归以“数据以及数据的流动转换”为核心的思维,摆脱把数据硬套成“执行者(doers)”或把纯操作升格为独立数据类型的概念混乱,软件开发就能重新收获直截了当的清晰解法。 > “I think what happened is that because he was programming in an object-oriented style he just didn't have the clarity to see how simple what he was doing really was and so he couldn't then devise a sensibly simple solution.” —— Brian Will - 代码暴露面剧增:面向对象版本由 5 个相互纠缠的类构成,严重模糊了代码执行路径。 - 伪简单性与碎片化:面向对象并没有真正简化逻辑,只是把原本简单的流程肢解并分散到各个微小方法中。 - 核心哲学启示:关注“数据与数据变换(data and data transformation)”,而非将数据拟人化为“行为主体(doers)”或将动作强行具象化为类型。 ### [26:48](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=1608s) 剥离面向对象包袱:数据转换的本质与清晰度 在回顾处理命令行参数的示例时,Brian Will 指出,使用带有三个哈希表(HashMaps)的 Args 类其实就把核心诉求解决得一清二楚。整项任务的初衷极其纯粹:眼前有一个字符串数组,开发者唯一要做的事情就是把它转换成一种在后续流程中更容易查询、使用起来更顺手的数据形式。然而,在面向对象的繁文缛节介入之后,这种直截了当的转换被包裹上了厚重的仪式感和复杂的抽象层。Brian Will 强调,只要抛弃那些多余的包袱,程序的意图就能直接浮现出来,他总结道:“By avoiding all of his object-oriented nonsense we can have that clarity.”(只要避开他那些面向对象的无稽之谈,我们就能获得那份清晰度。) ```mermaid flowchart LR accTitle: 数据转换的本质路径对比 accDescr: 对比面向过程的直接数据转换与面向对象的复杂封装流程 subgraph Direct["面向过程 / 务实方式"] A["字符串数组 (Array of Strings)"] --> B["转换为便于查询的数据 (3个 HashMaps)"] B --> C["清晰直接的下游调用"] end subgraph OO["过度面向对象方式"] D["字符串数组"] --> E["复杂的对象继承与层级体系"] E --> F["难以理清的隐式交互与样板代码"] end ``` ### [27:07](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=1627s) 驳斥 Sandi Metz:放弃对代码的全局理解只会带来劣质软件 这种为了模式而牺牲可读性的现象,让 Brian Will 联想到了著名软件工程师 Sandi Metz 的另一场演讲。在那场分享中,Sandi Metz 曾公开承认面向对象的代码在通常情况下确实不如面向过程(Procedural)的代码易于理解。但她随即为面向对象辩护,提出了一个观点:程序员本来就不需要、也不应该去理解整个系统的全部代码;只要将一切合理地拆解并分散到由许多微小对象组成的系统中,后续想要修改或扩展功能时,就只需要往系统里添加更多小对象即可,完全不必弄懂它们之间所有错综复杂的连接和交互。 对于 Sandi Metz 的这套论调,Brian Will 表达了强烈的质疑与反驳。他首先对这种“无需理解代码就能顺利推进”的所谓优势是否真实存在持极度怀疑的态度;退一步讲,即便真能做到,这种思维从根基上也是对编程本质的背离。在他看来,开发者必须理解自己的代码,而且认知范围还必须进一步延伸:必须理解自己所使用的工具,必须理解运行代码的环境与底层平台。如果开发者连自己写下的代码究竟在做什么都不再尝试弄懂,软件质量的崩塌将是不可避免的结局。正如 Brian Will 所指出的:“When we give up on understanding what we're actually doing when we write code it just leads to bad software.”(当我们放弃去理解写代码时自己究竟在做什么,这只会导向糟糕的软件。) - **Sandi Metz 的核心辩护逻辑**:承认面向对象的可读性劣于过程式代码,但主张利用微小对象的系统拆解,使开发者能在不掌握整体调用关系的前提下,仅凭“堆砌新对象”来完成改动。 - **Brian Will 的反驳要点**: - 这种宣称不用全面理解即可无痛扩展的承诺在现实中根本站不住脚。 - “放弃理解全局”在编程方法论上是根本性的错误。 - 优秀的工程实践要求程序员不仅对业务代码了如指掌,还必须透彻掌握开发工具、运行环境以及底层平台机制。 ## Notable quotes > [02:47](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=167s) I think it's really true I have nothing nice to say about object-oriented programming I don't like it I think it's really been a disaster for the industry. — Brian Will > [27:07](https://www.youtube.com/watch?v=IRTfhkiAqPw&t=1627s) When we give up on understanding what we're actually doing when we write code it just leads to bad software. — Brian Will ## Mentioned Brian Will, Sandi Metz, Robert C. Martin, Derek Banas, YouTube, Vimeo, Java, Ruby, JavaScript, Python, SOLID, UML, Gilded Rose, World of Warcraft, YAML ## Video description A follow up to https://www.youtube.com/watch?v=QM1iUe6IofM [Source](https://www.youtube.com/watch?v=IRTfhkiAqPw)

Source quotes

[2:47] Brian Will: I think it's really true I have nothing nice to say about object-oriented programming I don't like it I think it's really been a disaster for the industry. [27:07] Brian Will: When we give up on understanding what we're actually doing when we write code it just leads to bad software.

Source quotes

[0:01] Brian Will: I have nothing nice to say about object-oriented programming, I don't like it, I think it's really been a disaster for the industry. [26:48] Brian Will: When we give up on understanding what we're actually doing when we write code it just leads to bad software. [11:30] Brian Will: UML is just garbage, don't ever use it, it's crazy.

Original transcript

[0:01] I recently posted a video called [0:02] object-oriented programming is bad in [0:04] which I made the theoretical case [0:06] against object-oriented programming and [0:07] why you should really never do it and a [0:09] number of people complained about the [0:11] title suggesting that it was somehow [0:12] click baity but I disagree I chose the [0:15] title because I I think it's really true [0:17] I have nothing nice to say about obje [0:19] programming I don't like it I think it's [0:20] really been a disaster for the industry [0:23] and so to follow up that argument and [0:25] see it through I wanted to present some [0:27] concrete examples and this one's up in [0:29] the an so it's called objectoriented [0:31] programming is [0:32] embarrassing in this video I'll have [0:34] four examples but they'll be really [0:36] short uh less than 200 lines in each [0:38] case uh two of them are in Java and two [0:40] of them are in Ruby and I'm going to [0:42] show how if you rewrite them into just a [0:45] simple procedural style the resulting [0:46] code is much better and I'll try and [0:48] discuss exactly why and just to make the [0:51] comparison Fair uh for each example I [0:53] won't change a language so from java [0:55] I'll rewrite it in Java and if it's in [0:56] Ruby I'll rewrite it in Ruby be clear I [0:59] did not TR pick these examples at all I [1:01] just basically took the first examples I [1:03] found on YouTube or Vimeo of some uh [1:06] speaker giving a talk explaining how [1:08] objectoriented design is supposed to [1:09] work and how it's going to make your [1:11] code better these are literally the very [1:13] first code examples I came across when I [1:15] started looking for them so if you think [1:17] the examples are unfair I do send in [1:19] suggestions uh also I would want to [1:21] follow us up with even longer examples [1:23] because I know people are not going to [1:24] be convinced with just 200 under lining [1:26] examples they're going to say oh well [1:27] object R programing virtues don't really [1:29] come out until you're talking about much [1:30] larger samples of code because it [1:31] protects your uh code from becoming [1:33] spaghetti when it gets larger and larger [1:36] ideally we would actually put that [1:37] theory to the test and take a 500,000 [1:39] line program and rewrite it from [1:41] objector to code into procedural and [1:42] vice versa and see what we get for [1:44] obvious reasons though no one ever does [1:46] that but what I would like to do at [1:48] least is next take an example of say a [1:50] th000 or 2,000 lines and then next maybe [1:53] some example that say 10,000 lines and [1:56] ideally I don't know rewrite in like [1:59] 50,000 lines is a lot of work and I [2:01] don't know if there's an ideal candidate [2:02] for it but maybe one could be found that [2:04] that would be a long-term project [2:06] probably in the meantime if you have [2:08] some suggestions of some code example [2:09] you found some some gith her repo of a [2:11] complete program or maybe even just some [2:13] subm module of a program that's like [2:15] 1,000 to 2,000 lines or 10,000 lines I'd [2:18] very appreciate if you sent me some [2:20] suggestions preferably it would be [2:22] something in Java or JavaScript or [2:24] python or Ruby even though I prefer [2:26] python it's not a strict requirement but [2:29] it's if it's in language that everyone [2:31] knows because I I want to have it be [2:33] accessible to as many people as possible [2:34] so Pro probably preferably JavaScript [2:36] though if it make sure it's an example [2:38] that is actually in an objectoriented [2:40] style not perfunctory objectoriented [2:41] style but actual objectoriented [2:44] style so before getting into the [2:47] examples very briefly I just want to [2:48] recap very very broadly my argument [2:50] against object-oriented programming this [2:52] is a new formulation I suppose and what [2:54] I originally stated in the video but [2:55] it's another simple way of thinking [2:57] about it and that is just let data be [2:59] data [3:00] what object-oriented programming demands [3:02] is that we're supposed to be [3:03] conceptualizing our data as not just [3:06] data but as some sort of thing with [3:08] responsibilities and something which [3:09] acts in the world we basically have to [3:12] um imbue our data with intent and agency [3:16] and that's just a fundamental mistake [3:18] data does not do things data is just [3:19] inert and that's the way it should be [3:21] and you're just adding confusion if you [3:23] try and conceptualize your data as some [3:25] kind of [3:26] actor the other side of the coin is that [3:28] we should let our action just be actions [3:31] we shouldn't have to conceptualize all [3:33] the things we want to do in code in [3:35] terms of some kind of data we shouldn't [3:37] have to noun ify all our verbs it is [3:40] this conflation of action with data and [3:43] vice versa that I think makes [3:45] objectoriented programming so mystifying [3:47] if you go down that path if you follow [3:49] the advice of the object-oriented [3:51] programming gurus I think what happens [3:53] inevitably is that writing code then [3:56] begins to feel like you're having to [3:57] solve the problems of analytical [3:59] philosophy and platonic essentialism you [4:02] have to constantly question the nature [4:04] of the entities you are creating in your [4:06] code if you create a a chair class for [4:09] example you have to consider the true [4:10] nature of a chair you have to uh [4:13] determine its essential aspects because [4:15] if you get that wrong according to these [4:17] gurus if you draw the boundaries around [4:20] this entity incorrectly you're violating [4:23] some some principle or another of [4:24] objectoriented code it's going to bite [4:26] you in the ass in one way or another and [4:28] your code's going to become spaghetti [4:29] and all the guarantees they promised [4:31] about what object oriented programming [4:32] is going to deliver for you all their [4:34] guarantees are void and you can't hold [4:36] them responsible because you didn't [4:38] follow one supposed principle exactly as [4:40] they intended notice then the [4:42] justification for object-oriented [4:44] programming is circular you try and do [4:46] objectoriented designs and they don't [4:47] seem to really quite work and you don't [4:49] understand why things are supposed to be [4:50] arranged in this way and what benefits [4:51] you're supposed to get well it's because [4:53] you're just not doing it right so here [4:55] are 10 more principles and guidelines in [4:57] other words [5:01] so the first example is from a talk [5:03] given by Sandy Mets called solid [5:06] object-oriented design you can find it [5:07] on YouTube the centerpiece of the talk [5:10] is a really short class that processes a [5:12] patent it uh downloads a patent file it [5:15] parses the file and then it updates that [5:17] patent information in a database and the [5:20] core thrust of her talk is she's [5:21] attempting to demonstrate the so-called [5:23] single responsibility principle this [5:25] idea that your classes and generally [5:28] also your methods should only be doing [5:29] one thing what exactly does it mean for [5:32] something to be doing just one thing and [5:34] one thing only well that's a disputed [5:37] point the definition given by Robert [5:39] Martin AKA Uncle Bob who coined the [5:41] solid principles and SRP the single [5:44] responsibility principle his definition [5:46] is that a single class a single unit [5:48] should have only one reason for change [5:52] now what exactly is one reason for [5:54] change well it's something he explains [5:56] at length I think it's even after [5:58] explanation I think it's a very nebulous [6:00] idea which is part of the problem but [6:02] putting that aside for the moment let's [6:03] look at her example class as you can see [6:06] it's written in Ruby and it has four [6:08] methods starting with run which is [6:10] really where all the action is uh that [6:12] invokes the other three methods uh [6:14] download file parse and then update [6:17] patents Sandy however is not satisfied [6:19] with this design and says that you [6:21] shouldn't be either because what's going [6:23] on here is we just have too many [6:24] responsibilities in one class I actually [6:27] broadly agree on that point it's very [6:30] strange that this one unit of code is [6:32] both downloading files but then also [6:34] parsing and updating the database it's [6:37] mixing a lot of disperate stuff into [6:39] this one unit of code that yes do all [6:42] concern patents I suppose but there is [6:44] something really weird going on here as [6:46] evidenced by the simple fact that you [6:48] know classes are supposed to be data [6:49] types and yet look there are no fields [6:51] in this class so what the hell is this [6:53] thing really the real answer is that [6:55] it's just this tiny namespace and that's [6:57] all that's going on here that's all this [6:59] class really is except it's worse than [7:01] that because in object code to use a [7:03] class you have to instantiate it so we [7:05] have to manage this instance thing in a [7:07] totally nonsensical way we're managing [7:09] this instance that has no persistent [7:12] value whatsoever and so it's you [7:14] immediately dispose of it after invoking [7:16] the run meth what what is this thing why [7:18] does it have to be a thing it serves no [7:19] purpose being a [7:21] thing Sand's prescription is given this [7:23] nonsensical class the solution is as you [7:25] might expect more classes in her rewrite [7:28] she ends up with first this config class [7:31] which separates out and effectively [7:33] generalizes the configuration concerns [7:35] of this program which which is a sound [7:37] idea I I think but we have this [7:39] Constructor which the initialized method [7:41] which takes in some set of options [7:43] potentially uh I don't know exactly what [7:46] purpose they serve but anyway so the [7:47] options tell us where do we get the file [7:49] name I believe for this yaml file that [7:51] has configuration stuff that's loaded uh [7:54] and from the yl file we get this data [7:57] which is then uh there's some weird made [7:59] a business going on here in Ruby in her [8:01] defined methods for environment method [8:03] the names are turned into effectively [8:07] properties of the config class itself or [8:09] rather actually methods of the config [8:10] class itself that return values uh [8:13] corresponding to the key value pairs of [8:15] the configuration file effectively in [8:17] the end we get a config instance that [8:19] has a method for all the configuration [8:22] property names and you just invoke the [8:24] method to get its Associated [8:26] value so then my objection to this class [8:29] is why do we need it at all why can't we [8:30] just have a hashmap instead of having a [8:32] Constructor we have some function taking [8:34] in the options doing all the same [8:36] business uh parsing this yaml file but [8:39] why not just return a hashmap why do we [8:40] need this [8:42] class she also moves all of the [8:44] downloading business into its own class [8:46] called FTP downloader and a similar [8:48] absurdity leaps out here why is this a [8:51] class we're initializing this config [8:54] property which we're never going to use [8:55] again any user of the FTP download is [8:57] never going to access this config [8:59] propert property it's just they're going [9:00] to call this download file method it's [9:02] you're creating a whole instance of a [9:03] class just to call this one method why [9:06] do we have a class why is this not just [9:08] a [9:09] function lastly here's her rewrite of [9:11] the patent job class the difference is [9:14] there's now no downloader method instead [9:16] in the Constructor we create a config [9:19] object and then pass that to create a [9:21] downloader object and this downloader [9:23] object is preserved in a downloader [9:25] field which is then later used when we [9:27] called the run method and I'll note here [9:30] I'm surprised she doesn't use [9:31] Constructor injection it's odd that she [9:32] has her patent job actually creating [9:34] these config objects and downloader [9:37] objects um as much as I don't like [9:39] object do programming if you are going [9:40] to do it I think for a very good reason [9:42] you do use dependency injection CU that [9:45] well that's a whole tangent but [9:46] basically it solves the problem of [9:48] having to uh wire together all your [9:50] objects or it's a much more sane way of [9:52] doing it so I'm surprised she doesn't do [9:54] that here it seems wrong that she's [9:55] instantiating these objects inside this [9:57] other Constructor and anyway that's a [9:59] Mot point because the code should just [10:01] be this it should just be straight [10:03] procedural code just a bunch of Kernel [10:04] methods as they're called in Ruby [10:06] basically just functions we don't have [10:08] to ponder the significance of any [10:10] classes and and speculate as to what [10:12] their fields might signify why they [10:14] exist and what purpose they serve and so [10:17] say instead of a config class we just [10:19] have this get config function which [10:22] returns a hash with all the fields [10:24] parsed out of the yaml file and a story [10:27] we don't have to think about any [10:28] Constructor [10:29] and we don't have to do silly things [10:31] like creating objects immediately [10:32] calling one method and then never using [10:34] the object again we don't have to do [10:35] silly things like that in fact I would [10:38] go a little further here I would take [10:39] these little tiny functions that don't [10:41] get C anywhere else except in this one [10:43] place in the process patent function and [10:45] just put them in line there maybe [10:48] speculatively in the future as the [10:49] program evolves those might become [10:51] larger more involved operations where [10:54] maybe at some point it'll make sense to [10:56] split them off into their own functions [10:58] because they get really involved but as [10:59] it stands it seems really silly you know [11:02] if I can come across your code base and [11:04] encounter just three functions I have to [11:06] understand instead of five well of [11:07] course at this tiny scale it's no big [11:09] deal either way so it doesn't really [11:11] matter but if you can show me a code [11:12] base where instead of 100 functions I [11:15] have 3- fifths of that that's just way [11:17] more tractable it's just a lot easier to [11:18] get a foothold and start understanding [11:20] the code because I don't have to [11:22] contemplate the potential uses of these [11:24] functions and where they might get [11:25] called [11:30] the next example comes from a video by [11:32] Derek Banis from a few years ago it's [11:34] this whole series he has on [11:35] object-oriented design there are 11 or [11:38] so videos but this code comes from the [11:39] second one of those uh to be fair to [11:41] Derek his other videos on programming [11:43] his more recent ones I've seen are [11:45] actually quite good uh but the series is [11:47] terrible because just the nature of what [11:49] he's trying to teach it's just [11:51] disastrous so in this example he is uh [11:54] writing a coin flipping game yeah you [11:56] know one person throws a coin into the [11:58] air and then someone else calls it heads [12:00] or tails and there's a winner uh it's [12:02] not going to actually really be an [12:03] interactive program it's just we're [12:05] simulating the result to get the end [12:07] result you know so one of two named [12:09] players wins and randomly it's either [12:12] heads or tails and that's it and we just [12:14] print out so and so one with a coin flip [12:16] of heads or so and so one with a coin [12:18] flip of Tails and that's all we're going [12:19] for here in this video he's trying to [12:22] demonstrate how to do formal modeling uh [12:24] using uml techniques uml what does it [12:27] stand for unified modeling language [12:29] Universal model something like that who [12:30] cares it's garbage as you can [12:33] see by this this is his use case he has [12:36] his class diagram he has there's three [12:37] classes there's a player and a coin and [12:39] a coin game and you know the class [12:41] diagram is one thing but then there's [12:42] the actual composition of of instances [12:45] so you have an object model which is [12:47] different and then we have a sequence [12:49] diagram which is basically just sort of [12:50] like a A visual representation of of [12:53] progression of time and and what talks [12:55] to what and then from all of that he [12:57] writes his actual code so we have four [12:59] classes here the the fourth class is [13:00] just a coin flipping game in the bottom [13:02] right which is just a container for main [13:04] because you know it's Java and [13:05] everything has to be a class I'm not [13:08] even going to walk through this code it [13:10] it's totally confused even for something [13:11] so simple because just here this is all [13:14] he needs he doesn't really need classes [13:15] of any kind except for container from [13:17] Main and then in this main okay we we'll [13:20] split out play game to another method I [13:21] suppose doesn't really matter in this [13:23] case we just take in two strings we [13:25] randomly pick one of them and then we [13:27] randomly pick heads or tails and print [13:29] out so and so one with a flip of [13:31] whatever and that's all we need to do [13:33] and then we have this in a a loop [13:35] because that's what he did uh he the [13:37] user is prompted do you want to play [13:39] again and you hit something and and it [13:40] just goes again that's all I don't think [13:43] I have to actually make the argument [13:44] about why is cod is so absurd just look [13:46] at it and then look at what you actually [13:47] just need it to do at a minimum the [13:49] lesson here is that uml is just [13:51] garbage don't ever use it it's crazy if [13:54] you want to plan out your code ahead of [13:55] time you know do so to a degree of [13:58] course just to plan out your data [14:00] structures write a little bit of pseudo [14:01] code don't try and diagram anything that [14:04] never works it's just pain in the ass to [14:05] deal with all the tools for modeling the [14:08] boxes and the arrows and everything [14:09] they're all terrible and even if they [14:11] were good it just what's stupid about [14:13] visual representation of code is that [14:16] past a very tiny amount of complexity [14:18] you have all these boxes and then you [14:20] need to draw a bunch of lines between [14:21] them and very quickly you have so many [14:24] lines that it's really hard to visually [14:26] parse and and why didn't you just write [14:28] code to begin with cuz code has the the [14:30] nice virtue of you know you have a bunch [14:31] of functions and they can all just refer [14:32] to each other by name it's past a [14:34] certain point of complexity of things [14:35] all tied together it's better [14:38] represented as just text and named [14:40] references that's the main reason why [14:42] visual languages have never really gone [14:44] anywhere it's just a fundamental [14:49] problem the next example is another one [14:51] from Sandy Mets called all the little [14:53] things I I don't mean to pick on Sandy [14:55] it's just I found one of her talks and [14:56] it was easy to find another in this talk [14:59] she presents an example of some ugly [15:01] code some ugly uh branching logic and [15:04] she demonstrates how this ugly branching [15:06] can be more cleanly represented with [15:08] polymorphism so looking at this actual [15:11] code um I this was not a real code [15:13] example clearly I think it was an [15:14] arbitary example um and these are like [15:16] random World of Warcraft references [15:19] apparently and looking at this I think [15:21] we can all agree that this is ugly code [15:23] you don't want to see code like this if [15:25] you're going to have a lot of complex [15:26] logic it should be complex for a reason [15:28] and and you can very quickly see in a [15:30] few places that this doesn't need to be [15:31] so complicated this can be cleaned up [15:33] definitely but here's her solution she [15:36] puts everything in this class gilded [15:37] rows I don't know I don't understand [15:40] what that's supposed to signify but [15:41] whatever and inside we have this class [15:44] item with a tick method and sort of an [15:46] abstract class item with a an empty tick [15:48] method and then we create three [15:50] specializations of item we have normal [15:53] Bri and backstage each with their own [15:54] tick methods because what she determined [15:57] with the original branching code is that [15:59] we really have four mutually exclusive [16:01] cases we have the case where we do [16:02] nothing we had one case where the string [16:04] was equal to normal another where it's [16:06] equal to a BRI and another where it's [16:08] equal to backstage passes to a tefl 80 [16:11] Etc concert again some kind of World of [16:13] Warcraft reference there so at the [16:16] bottom here she has default classes item [16:18] but then there's the specific cases she [16:20] has this hashmap normal Maps the normal [16:23] class H Bri to the Bri class and [16:25] backstage yada yada to the backstage [16:27] class and and then at the bottom we have [16:29] the self. for method I think the [16:32] significance of self here is that this [16:33] makes it a basically a static method of [16:36] guilded row it's a it's a class method [16:38] rather than an actual instance method so [16:39] you write gilded rows. four and you pass [16:42] in the name which is going to be a [16:43] string like normal and then the the [16:45] quality and days remaining and you get [16:47] back an item object upon which you call [16:49] its tick method and you get the same [16:51] branching Behavior we saw in the [16:52] original [16:53] code so all of this is clever I suppose [16:57] it's certainly Mak full use of Ruby [17:00] language features but if you don't care [17:02] about doing that why didn't you just [17:03] write this code the problem with the [17:06] original code the reason was so ugly is [17:08] because it didn't correctly first [17:09] Identify the four major mutually [17:12] exclusive cases and if it had there's no [17:14] reason it can't be expressed as a switch [17:16] or in a Felts ladder the way she [17:17] presents it in the talk is that [17:19] polymorphism swoops in and saves the day [17:20] it makes the code much cleaner but the [17:22] there's no need for it you can just [17:24] write a damn switch if you can figure [17:26] out that the major cases are the string [17:28] being normal or age3 or backstage yada [17:30] yada then you can just as easily write [17:33] this code you don't have to involve [17:34] polymorphism why would you want to [17:36] introduce more concepts of data types [17:38] into your code when you don't need them [17:40] that's just perverse the procedural code [17:42] version is just as clean and actually [17:44] much cleaner because it doesn't involve [17:45] extraneous Concepts in fact I would [17:48] actually make this a little bit more [17:49] procedural here we're still using uh [17:51] instance variables as if this is the [17:52] method of some class but in a straight [17:54] procedural version we don't really have [17:56] classes so we'd pass into tick we'd pass [17:58] in name name and then some object and [18:00] then operate upon its [18:03] members now I've seen this polymorphism [18:06] can replace all branching idea in other [18:08] places and the larger argument generally [18:10] given is okay so in your code you tend [18:12] to have a switch over one piece of data [18:15] and you're going to have parallel [18:17] switches in many other places in code [18:19] and so it's better to express your [18:21] switch logic polymorphically because [18:23] then especially in a in a static [18:24] language at least you would be required [18:27] by the language to make sure that you [18:28] have all the methods implemented in Java [18:30] for example if you implement a interface [18:32] and you don't Implement one of the [18:34] methods that's an error so you're less [18:36] likely to forget to cover all the [18:38] parallel cases when you introduce a new [18:41] case uh with straight switches you might [18:43] update half the switches and then forget [18:45] that to the other half but of course [18:47] that's in a static language in a dynamic [18:49] language like Ruby if you subass some [18:52] abstract class and you failed to [18:53] override one of its uh its methods [18:56] you're not going to get any language [18:57] warning whatsoever [18:59] so it seems like a really weak argument [19:01] to me in a dynamic language regardless I [19:03] still think polymorphic branching is a [19:05] perverse idea mostly because I think [19:07] these neatly parallel switches are [19:09] actually quite rare much more commonly I [19:11] think you have scenarios where the [19:13] switches are semi- parallel like they [19:16] they switch over one set of factors in [19:18] one part of code but then the branching [19:20] logic needs to be quite different in [19:22] other parts of code so going through all [19:24] this trouble to reconceptualize certain [19:27] switches as being switching in over [19:29] types when you don't necessarily have [19:31] different types that's a lot of bother [19:33] and conceptual overhead for something [19:34] that's quite likely to not pay off in [19:36] the long [19:39] run the last example is from a talk by [19:42] Robert C Martin AKA Uncle Bob who is [19:45] probably the preeminent object-oriented [19:47] Guru and in this talk he presents a [19:49] program that parses command line [19:50] arguments into a more convenient data [19:52] structure such that say you can query [19:55] hey uh what is the value of the flag [19:58] that starts with hyphen b or hyphen C [20:00] some hyphen some character right and [20:03] these arguments are specified with a [20:04] schema string uh in this format here [20:07] they're separated by commas and if it's [20:10] a Boolean it's not marked by any special [20:12] character but if it's an integer it's [20:13] marked with a number sign and if it's a [20:16] string then it's marked with an asterisk [20:18] and so here for example this this schem [20:20] of string expects an INF flag j a bll [20:23] flag F and a string flag W so given the [20:27] schema if say we then had command line [20:29] arguments of um hyphen J 35 and then [20:32] hyphen W blah blah blah then the flag J [20:35] will have the value 35 the flag F will [20:37] have the value false because there was [20:38] no F flag and then flag W will have the [20:41] value blah blah [20:43] blah so first he presents a version of [20:45] his code which he thinks is not so great [20:48] but then he cleans it up and this is [20:50] that version I won't I won't bother [20:51] showing the bad version it didn't seem [20:53] any worse or better than this better [20:55] version this ostensibly better version [20:57] so I'll just show the better version [20:59] so what does he have here first he has [21:01] this abstract argument marshaler class [21:04] with two abstract methods set and get I [21:07] don't know why he's using an abstract [21:09] class rather than an interface because [21:11] there's no you know there's no actual [21:12] Fields here so why why have an abstract [21:14] class and not an interface uh whatever [21:17] that then gets extended into three [21:19] concrete classes there's Boolean [21:21] argument marshaler there's also then [21:24] string argument Marshal and integer [21:26] argument Marshal and their bodies are [21:28] really similar they're doing the same [21:30] stuff except um they're dealing with [21:32] Boolean string and integer values and [21:34] that's the main difference [21:36] there and then those three concrete [21:38] classes are utilized in this args class [21:41] which is where he has his main uh so I'm [21:43] not going to step through any of the [21:44] code so just look at the bottom right [21:46] where we have Main and so we instantiate [21:48] args we pass in a schema string and then [21:51] also the actual arguments to the main [21:53] function that gets us back this args [21:56] data structure which we can then query [21:57] with methods get Boolean get int and get [21:59] string and so here we're asking hey what [22:02] is the value of the Boolean flag L and [22:05] we get back a Boolean value and so forth [22:07] and then so we parcel of our arguments [22:09] and get their values and then we pass [22:10] them off to this uh execute application [22:12] method which he doesn't actually it's [22:14] not Implement it's just hypothetically [22:16] here's your actual Program start and [22:19] notice that the args instantiation and [22:20] it's get buo and get int and get string [22:22] Methods are all called inside this Tri [22:24] block because they can all throw args [22:27] exceptions now if you want take a few [22:29] moments and study his code and try and [22:30] figure out what's going on he does a few [22:32] really odd things because all he really [22:35] needs to do is this there's my solution [22:37] I just have a single class args with the [22:40] the same main function uh down at the [22:42] bottom I'll show in the next slide uh [22:44] but first args has three Fields it has a [22:46] hashmap for characters to integers a [22:49] hashmap for characters to Strings and a [22:50] hashmap for characters to booleans and [22:53] then in the Constructor we pass in just [22:54] like his args we pass in the schema [22:56] string and the actual ARG strings and [22:59] first what we do is we parse the schema [23:01] in this Loop we we split the schema [23:03] string by comma and then for each [23:05] element of the schema we add that [23:07] character to the appropriate hashmap [23:09] given the the symbol character that [23:11] might accompany it uh and for now we're [23:14] just giving the integer and string and [23:16] Boolean values in these hash Maps [23:17] default values for for Boolean the [23:19] default value is false but then for [23:20] string and integer the default value is [23:22] null because it it makes sense for a [23:23] Boolean flag if you omit the flag then [23:25] presumably then it should be false but [23:27] if you omit a string or integer flag uh [23:30] who knows what that should be so there's [23:31] not really a default value there in any [23:34] case our ARS Constructor then continues [23:36] and we Loop through all the actual [23:37] argument strings and we check first if [23:40] they begin with a hyphen because if they [23:41] don't begin with a hyphen we don't care [23:43] about them we just ignore them but if we [23:44] do then we check the next letter and we [23:47] see well is that in the boolean's map or [23:49] the strings map or the ins map and [23:51] whichever one it's in we want to take [23:53] the specified value and assign it to [23:55] that character in the hashmap [23:58] understanding the default case here in [23:59] the else Clause if we encounter a [24:01] character which we didn't see in the [24:03] schema then that's an exception you're [24:04] not supposed to we're not supposed to [24:06] have any such Flags if they weren't in [24:08] the [24:09] schema so that Constructor gets us our [24:11] fully parsed data structure and then [24:13] just to use a thing we have the same [24:14] methods that he has in his code we have [24:15] get bull get string and get int and the [24:17] main function here is exactly what he [24:19] had but actually what I would do is [24:21] probably I wouldn't have get bull get [24:23] string and get int I would just make the [24:25] hashmap fields public and then access [24:27] them directly and I prefer explicit [24:29] error handling over exceptions so the [24:31] resulting code will look something like [24:32] this um we get ar. ins. get but that [24:36] might be null and if so then we that's [24:37] an error otherwise if it's not null then [24:39] we can get us value and yeah this is a [24:41] little ugly in verbose but that's mainly [24:43] just because Java has a noxious uh [24:45] distinction between Primitives and and [24:47] object types so that's really the root [24:49] problem here so yeah this is a little [24:52] verbose being explicit about errors but [24:54] I still think it's better to be [24:57] explicit so let's step back and look at [24:59] the differences here in my version of [25:01] the code we had an args data type it had [25:04] its Constructor and there was the main [25:06] method and that's the entire surface [25:08] level of the code if you want to [25:09] understand what's going in my code no [25:11] question whatsoever where you need to [25:13] start in Uncle Bob's code we have not [25:16] just one class we have five one of them [25:18] is abstract which itself is misdefined [25:20] in its own special way he has this [25:23] strange notion of marshallers I still [25:25] can't figure out what that's really [25:26] supposed to mean and what's oddest of [25:29] all is that these marshaler classes are [25:30] all recursive Boolean argument marshaler [25:33] has a Boolean value field but then also [25:35] it's Boolean ARS field as a map of [25:37] characters to other argument marshallers [25:39] I don't know why he made it the the [25:41] parent class argument marshaler because [25:42] it turns out in practice it's always [25:44] specifically Boolean argument marshaler [25:46] so I don't know what's going on there [25:48] but I don't know why it's recursive to [25:49] begin with it's very very strange I [25:52] think what happened is that because he [25:54] was programming in an object-oriented [25:55] style he just didn't have the clarity to [25:58] see how simple what he was doing really [26:00] was and so he couldn't then devise a [26:03] sensibly Simple Solution when someone [26:05] comes to his code and they want to make [26:06] heads or tals of it they have to ponder [26:09] why he has these five different data [26:11] types and what purposes they really [26:12] serve and then looking at the fields why [26:14] are these data structures recursive and [26:16] then you have to look at all the methods [26:17] and everything split up into tiny little [26:19] methods so yes in the straightforward [26:21] procedural version there's a little bit [26:23] of complex logic as you can see in the [26:25] loop and branching but his version just [26:27] chops up up and obscures it it scatters [26:30] it all to the winds if you think simply [26:32] in terms of data and data transformation [26:35] rather than trying to conceptualize your [26:37] data as doers and your your actions as [26:40] somehow also being data types if you [26:42] avoid that confusion it's much much [26:45] easier to arrive at straightforward [26:46] Solutions like I did where I just have [26:48] this ARS class with the three hashmaps [26:51] because what was the task at hand I had [26:52] this array of strings and I just wanted [26:54] to convert it into a form of data that [26:56] was more convenient to query just a form [26:59] of data that was more convenient to use [27:00] down the line that's all I was doing by [27:03] avoiding all of his object-oriented [27:04] nonsense we can have that [27:07] Clarity this brings to mind another talk [27:09] by Sandy Mets where she explicitly [27:11] concedes that object-oriented code is [27:13] generally less comprehensible than [27:14] procedural code but she defends [27:16] object-oriented code with the idea that [27:18] you're not supposed to understand the [27:20] entirety of your code if you properly [27:21] decompose everything into this the [27:23] system of little objects then you can [27:25] make changes to your code by just adding [27:28] more objects without having to fully [27:29] understand all the connections between [27:31] them and all the [27:32] interactions aside from being skeptical [27:34] that this is actually true that you [27:36] actually get that benefit in object ring [27:38] code that you can proceed without really [27:39] understanding your code I think this is [27:41] fundamentally just the wrong way to [27:42] program yes you do need to understand [27:44] your code and beyond that you need to [27:46] understand your tools and you need to [27:47] understand your environment meaning [27:48] generally your platform when we give up [27:51] on understanding what we're actually [27:52] doing when we write code it just leads [27:54] to bad software