36
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

グロービスAdvent Calendar 2018

Day 11

Web FrontendでClean Architectureを試す

Posted at

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

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の型定義があるだけです。

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

Usecase

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

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アプリケーションの表示部分を司ります。

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してもらうようにしました。

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 に渡しています。

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
JavaScriptでClean Architectureを導入してみた - Vue.js・Reactのサンプルつき
Clean Architecture
Clean Architecture in Web Frontend #mixleap

36
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
36
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?