# Duplicate Observed Data

> Move domain data out of a UI class into a domain object, and keep the UI in sync with it through observation.

- kind: technique
- category: organizing-data
- canonical: https://aipatterns.dev/refactoring/techniques/duplicate-observed-data
- inspired-by: https://refactoring.guru/duplicate-observed-data

## Problem

A UI component stores business data in its own fields and computes business rules in its event handlers. The rules can't be tested without rendering, can't be reused by an API or a background job, and every UI change risks breaking them.

## Solution

**Create a domain class that owns the data and rules. Have the UI hold a copy of what it displays and subscribe to the domain object for changes.** The UI keeps its fields, but the domain object is the source of truth.

## Before

```ts
class CartWidget {
  private items: { sku: string; price: number; qty: number }[] = [];
  private totalLabel = "";

  onAdd(sku: string, price: number): void {
    const existing = this.items.find((i) => i.sku === sku);
    if (existing) existing.qty += 1;
    else this.items.push({ sku, price, qty: 1 });
    const subtotal = this.items.reduce((s, i) => s + i.price * i.qty, 0);
    const discount = subtotal > 100 ? subtotal * 0.1 : 0;
    this.totalLabel = `$${(subtotal - discount).toFixed(2)}`;
    this.render();
  }

  render(): void { /* draws items and totalLabel */ }
}
```

## After

```ts
type Listener = () => void;

class Cart {
  private items = new Map<string, { price: number; qty: number }>();
  private listeners: Listener[] = [];

  subscribe(fn: Listener): void { this.listeners.push(fn); }

  add(sku: string, price: number): void {
    const item = this.items.get(sku) ?? { price, qty: 0 };
    item.qty += 1;
    this.items.set(sku, item);
    this.listeners.forEach((fn) => fn());
  }

  total(): number {
    const subtotal = [...this.items.values()].reduce((s, i) => s + i.price * i.qty, 0);
    return subtotal > 100 ? subtotal * 0.9 : subtotal;
  }
}

class CartWidget {
  private totalLabel = "";

  constructor(private cart: Cart) {
    cart.subscribe(() => this.update());
  }

  onAdd(sku: string, price: number): void { this.cart.add(sku, price); }

  private update(): void {
    this.totalLabel = `$${this.cart.total().toFixed(2)}`;
    this.render();
  }

  render(): void { /* draws items and totalLabel */ }
}
```

## Why

- The discount rule is testable with no UI.
- An API endpoint or a second view can use `Cart` directly.
- UI and domain change for different reasons; now they live in different classes.

## Steps

1. Create the domain class, empty, and give the UI a reference to it.
2. Add a subscription mechanism to the domain class (callback list, event emitter, or your framework's store).
3. For one field, add it to the domain class. Make the UI's handler write to the domain object, and make the UI's update callback copy the value back into its own field. Run the tests.
4. Move the business logic that computes from that field into the domain class with [Move Method](https://aipatterns.dev/refactoring/techniques/move-method.md). Run the tests.
5. Repeat field by field. Finally, remove UI fields that no longer need a local copy.

## Watch out

- Modern UI frameworks already separate state from view: React state and stores, Vue reactivity, signals, MVVM bindings. Use the framework's mechanism instead of writing an observer by hand.
- Unsubscribe when the UI is destroyed, or the domain object keeps it alive and calls into dead views.
- Notifications fired inside notifications can loop. Keep update callbacks read-only toward the domain.
- Don't apply when the "data" is purely presentational (which tab is open, scroll position). That belongs in the UI.
- No direct inverse; collapsing domain back into a UI class is the smell.

## Related

- Fixes [Large Class](https://aipatterns.dev/refactoring/smells/large-class.md) and [Divergent Change](https://aipatterns.dev/refactoring/smells/divergent-change.md) in UI classes.
- The sync mechanism is [Observer](https://aipatterns.dev/design-patterns/observer.md).
- Built from [Move Field](https://aipatterns.dev/refactoring/techniques/move-field.md) and [Move Method](https://aipatterns.dev/refactoring/techniques/move-method.md).

## Agent checklist

- [ ] Business rules are in the domain class and have tests without rendering.
- [ ] The UI writes through the domain object and reads back via notifications.
- [ ] Subscriptions are removed when the UI component is disposed.
- [ ] The framework's own state mechanism was preferred where one exists.
- [ ] Tests pass.
