Web FrontendでClean Architectureを試す

こんにちは、スープです。
EdTechビジネスのプラットフォームチームでエンジニアをしています。

Clean ArchitectureでTodoアプリのFrontendを試作しました。
良ければ実装をみてください。

https://github.com/souppower/clean-architecture-todo-frontend

## 概要

Clean Architectureでは、UIやDB、FWといったアプリケーションの詳細を、ビジネスルールから切り離し、プラグインとして実装します。
これには、テスタビリティやコードのリーダビリティが高く保たれる等のメリットがあります。
とはいえ最大のメリットは、**詳細の決定を遅延**できることかもしれません。

### 詳細の決定を遅延できるということ

開発初期には、すべてのユースケースを把握できないため、重大な決定は遅延できるのが望ましいです。
開発が進むにつれて、適切な決定を下すための情報が数多く手に入るため、後回しにできるメリットは大きい。

また、数多くの実験をする余裕を獲得するというメリットもあります。

このメリットについては、Robert C. Martin 氏が度々強調しています。
https://blog.cleancoder.com/uncle-bob/2011/11/22/Clean-Architecture.html

## 実装

TODOアプリの実装を手短に見ていきます。

### Domain

TODOの型定義があるだけです。

```ts
export interface Todo {
  id: number;
  title: string;
}
```

### Usecase

アプリケーション固有のビジネスルールを書いています。
渡された `todoRepo` にデータのやりとりの処理を委譲します。
特定の実装に依存せず、interfaceに依存させることにより、ビジネスルールが外部の詳細を知らずにすみます。

```ts
import { Todo } from "domain";
import { TodoRepository } from "./repository";

export default class TodoUsecase {
  constructor(private todoRepo: TodoRepository) {}

  findAll(): Todo[] {
    return this.todoRepo.findAll();
  }

  add(text: string) {
    this.todoRepo.add(text);
  }

  edit(id: string, text: string) {
    this.todoRepo.edit(id, text);
  }

  finish(id: string) {
    this.todoRepo.delete(id);
  }

  finishAll() {
    this.todoRepo.deleteAll();
  }
}

```

### Interface

#### Presenter

TODOアプリケーションの表示部分を司ります。

```tsx
import React from "react";

import Usecase from "usecase/todo";
import { Todo } from "domain";

import { Input, Item, ClearButton } from "./todos";

import "./app.css";

interface Props {
  usecase: Usecase;
}

interface State {
  input: string;
  todos: Todo[];
}

export default class App extends React.Component<Props, State> {
  private usecase: Usecase;

  constructor(props: Props) {
    super(props);
    this.usecase = this.props.usecase;
    this.state = {
      input: "",
      todos: []
    };
  }

  componentDidMount() {
    const todos = this.usecase.findAll();
    this.setState({ todos });
  }

  handleChange(e: any) {
    this.setState({ input: e.target.value });
  }

  onKeyPress(e: any) {
    if (e.key !== "Enter") {
      return;
    }

    const input = e.target.value;
    this.usecase.add(input);
    this.setState({ input: "" });
    const todos = this.usecase.findAll();
    this.setState({ todos });
  }

  finishTodo(id: string) {
    this.usecase.finish(id);
    const todos = this.usecase.findAll();
    this.setState({ todos });
  }

  clearTodos() {
    this.usecase.finishAll();
    this.setState({ todos: [] });
  }

  render() {
    const { todos } = this.state;
    return (
      <section className="todoapp">
        <Input
          value={this.state.input}
          handleChange={this.handleChange.bind(this)}
          onKeyPress={this.onKeyPress.bind(this)}
        />

        <section>
          <ul className="list-group">
            {this.state.todos.map(todo => (
              <Item
                key={todo.id}
                todo={todo}
                onFinish={this.finishTodo.bind(this)}
              />
            ))}
          </ul>
          {todos.length > 0 && (
            <div className="right-align">
              <ClearButton onClick={this.clearTodos.bind(this)} />
            </div>
          )}
        </section>
      </section>
    );
  }
}
```

### Infrastructure

TODOの保存処理を書きました。
Memory か LocalStorage 保存か選択できるように、 `store` は interface に依存させ、詳細ロジックはDIしてもらうようにしました。

```ts
import Persistor from "interface/repository/persistor";

export default class Store {
  constructor(private store: Persistor) {}

  get(key: string) {
    return this.store.get(key);
  }

  getAll() {
    return this.store.getAll();
  }

  save(value: any) {
    this.store.save(value);
  }

  delete(id: string) {
    this.store.delete(id);
  }

  clear() {
    this.store.clear();
  }
}
```

### エントリーポイント

そして最後にエントリーポイントです。
`usecase` を組み立てて、 `Presenter` に渡しています。

```ts
import React from "react";
import ReactDOM from "react-dom";

import { LocalStorage, Persistor } from "infrastructure";
import { TodoRepository } from "interface/repository";
import App from "interface/presenter";
import { TodoUsecase } from "usecase";

const storage = new LocalStorage("clean-architecure-todo");
const persistor = new Persistor(storage);
const todoRepository = new TodoRepository(persistor);
const usecase = new TodoUsecase(todoRepository);

ReactDOM.render(<App usecase={usecase} />, document.getElementById("app"));
```

### 感想

これからもやっていきたい。

## 参考
[Front End Architecture — Making rebuild from scratch not so painful](https://medium.com/car2godevs/front-end-architecture-making-rebuild-from-scratch-not-so-painful-7b2232dc1666)
[JavaScriptでClean Architectureを導入してみた - Vue.js・Reactのサンプルつき](https://qiita.com/RikutoYamaguchi/items/7d265c58cb1a37969921)
[Clean Architecture](https://blog.cleancoder.com/uncle-bob/2011/11/22/Clean-Architecture.html)
[Clean Architecture in Web Frontend #mixleap](https://speakerdeck.com/pirosikick/clean-architecture-in-web-frontend-number-mixleap)