Talk to Code - Show the Data Journey with Fluent Interfaces (2)

Show the Data Journey with Fluent Interfaces

In the previous post, Talk to Code - Comments are Excuses (1), we discussed how to clarify intent by correctly naming variables and functions. Simply naming things well makes code much friendlier.

However, when business logic becomes complex, individual names are sometimes insufficient. When data must undergo multiple stages of processing, the flow itself—"Through what process does this data become the result?"—needs to be documented.

In this article, we will refactor complex transformation logic using the Fluent Interface (Method Chaining) pattern and explore how the structure of the code itself can replace explanations.

1. The Problem Context: Tricky Number Input Processing

Let's assume a scenario where we handle numeric strings entered by a user. Like in Excel or financial apps, the numbers users input are often not clean.

Requirements:

Thousands separator: 1234567 → 1,234,567

Decimal point handling: Inputting .5 converts to 0.5 (Adding Leading Zero)

Validation: Remove characters other than numbers and dots (.)

Initially, utility functions were created to perform each of these distinct features to solve this.

2. First Attempt: A Collection of Functions (IIFE)

Disliking that related functions were scattered, I grouped them under the name NumericStringSanitizer (Namespacing).

const NumericStringSanitizer = (() => {
  const stripNonNumeric = (v: string) => v.replace(/[^0-9.]/g, '');
  const prependLeadingZero = (v: string) => v.startsWith('.') ? `0${v}` : v;
  const addThousandsSeparator = (v: string) => Number(v).toLocaleString();

  return {
    sanitize: (v: string) => {
      let result = stripNonNumeric(v);
      result = prependLeadingZero(result);
      return result;
    },
    format: (v: string) => addThousandsSeparator(v)
  };
})();

This doesn't look bad. The features are gathered in one place, and the names are clear. However, let's look at the code that actually uses this tool.

3. Discovering the Discomfort: Broken Context

The code that uses this module to "refine input, format it, and display it on the screen" looks roughly like this:

// Usage Example
const rawInput = event.target.value; // ".5000"

// 1. Sanitize
const sanitized = NumericStringSanitizer.sanitize(rawInput);

// 2. Format
const formatted = NumericStringSanitizer.format(sanitized);

// 3. Use Result
updateDisplay(formatted);

Do you see the problem with this code?

Proliferation of intermediate variables: Variables like sanitized and formatted keep appearing. Naming variables is work, and the code gets longer.

Disconnection of flow: The process of data starting from rawInput and becoming formatted is not visible at a glance. The eye reading the code has to jump up and down.

To explain the "flow of data" via code, the functional call style feels a bit clunky.

4. Evolution: Class and Method Chaining

To solve this problem, we introduce the Fluent Interface pattern. We utilize Object-Oriented classes but build a pipeline by changing state and returning this (Method Chaining).

class DecimalString {
  private value: string;

  private constructor(value: string) {
    this.value = value;
  }

  static from(value: string) {
    return new DecimalString(value);
  }

  // 1. Keep only numbers and dots
  sanitize(): this {
    this.value = this.value.replace(/[^0-9.]/g, '');
    return this;
  }

  // 2. Convert .5 -> 0.5
  prependLeadingZero(): this {
    if (this.value.startsWith('.')) {
      this.value = `0${this.value}`;
    }
    return this;
  }

  // 3. Thousands separator
  applyThousandsSeparator(): this {
    if (!this.value) return this;
    const parts = this.value.split('.');
    parts[0] = Number(parts[0]).toLocaleString();
    this.value = parts.join('.');
    return this;
  }

  toString(): string {
    return this.value;
  }
}

Now let's look at the code using this class.

const result = DecimalString.from('.5000abc')
  .sanitize() // 1. Remove weird chars (.5000)
  .prependLeadingZero() // 2. Add leading zero (0.5000)
  .applyThousandsSeparator() // 3. Add comma (0,500)
  .toString();

5. Key Insight: Code is the Data's Journey

How does the chaining code above feel?

Left to right, top to bottom: You can see how the data changes in the exact order we read text.

Removing unnecessary noise: Temporary variables like sanitized and formatted are gone. Only "verbs (actions)" remain.

This is another form of "Self-Documenting Code".

"Method chaining narrates the journey of data in code."

If the previous method was a manual explaining "Put this value into this function," the Fluent Interface method is closer to storytelling, saying "This value is sanitized, prepended, and separated."

Conclusion: Code is a Powerful Tool to Show the Data's Journey

If Naming is the vocabulary of code, Structure is its grammar. Through Fluent Interface, we made the grammar more natural, making the code read smoothly like a sentence.

But is DecimalString alone enough for all situations? What if we need to handle "Currency"? Currency handles decimal points differently, and leading zeros (0100 → 100) might need to be removed.

In the next article, we will talk about Value Objects that maintain a similar structure but apply different domain rules.