こんにちは、スープです。
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