食の職人

「人はパンのみにて生くるものにも非ず」とはいうものの、誰でも美味しいパンを食べたいものだ。もちろん食べるためだけに生きているわけではない。食事で身体を満足させた上で別の精神的な充足感を得るために活動をする。しかし、食を追求する美食は作り手にとっても受け手にとっても生き甲斐になり得る。特に作り手のそれは、自分の腹を膨らます以上に、食材に対する探究心と「うまいものを人に食わせたい」という慈悲心がある。その純粋な友愛の心に畏敬の念を覚える。海のものや山のもの、食材だけでも数えられないほどあるのに、調味料や調理法をも考慮に入れるとそれらの組み合わせは無限に近い。複雑に分岐する暗い道筋に、名前なき先人たちの知恵で足元を照らし、言葉で武装した味覚と自らの舌を杖とし、経験と勘を合わせた想像力で狙った地点に到達することを願う。全ては目の前の人に「美味い」と言わせるために。そのようにして作られたパンを前に「人はこれだけで生きるものではない」と君は冷たく言い放てるだろうか。

(これは下書き。ちょっとだけ面白いフレーズが浮かんだので、簡単に書いて公開してみた。続きを書くかもしれないし、書かないかもしれない)

技術的な文章は一文一文ロジカルに地続きに書かないといけないが、エッセイは飛び飛びの方が面白い。最近こういう文章が書けなかった。この下書きは個人的にはリハビリに近い。一本筋を通した上で行間を膨らませる。膨らませるほど面白いが、前後の文同士が離れ離れになってはいけない。さらに文章の中には一つ、自分の中の常識が、狭い範囲だけオセロのようにひっくり返るような刺激物を入れておく。この辺りがエッセイの面白さだが、技術的な文章との往復が難しいので、モードチェンジが必要だ。段取りを考えて技術文章を書くこと、想像力を駆使してコードを書くこと、そして心の動きをエッセイに書くこと、これらの行き来は自分にとって簡単ではない。どれかのモードを選ばないといけない。そのどれも「書く」ことで共通しており、どれも好きな行為だ。好きなのに、ある程度の時期はそのどれかしか選べない。難儀なものだ。いずれかを進めると、残りのものが遠ざかる。特に技術文章の Shallow Reading に慣れすぎると、書き方が影響を受ける。思いついたことをダラダラ書いていく Shallow Writing になってしまうのだ。どちらも PC やスマホというメディアの性質のせいだ。PC を使いつつ、Shallow Writing しないように頭の使い方を訓練する必要がある。だから、この投稿はエッセイのためのリハビリのようなものだ。

プログラミングをするパンダ
プログラミングをするパンダ (@Panda_Program)
Software Engineer

弁護士ドットコムライブラリーのフロントエンドのアーキテクチャを紹介します

この記事は弁護士ドットコム Advent Calendar 2020、2日目の記事です。2020年12月に執筆された記事です。

私は弁護士ドットコムライブラリーというサービスを開発しています。これは法律書籍をネットで読める弁護士向けのサブスクリプションサービスです。

弁護士ドットコムライブラリーのトップページ

フロントエンドの採用技術はNext.js + TypeScriptで、要件定義から設計、実装は私が担当し、現在も運用しています。

この記事では、2020年5月にリリースしてから半年間、Next.jsで上記サービスを運用した知見の中から、フロントエンドでのアーキテクチャについてご紹介します。

弁護士ドットコムライブラリーの特徴は以下の通りです。

  • 画面数は10画面ほどの中規模アプリケーション(OOUIの考え方を取り入れたら画面数が減りました)
  • 基本的にバックエンドから渡されるデータを整形・表示するRead要件がメイン
  • バックエンドは認証、書籍検索(Elastic Search)、課金(Stripe)のマイクロサービス
  • ECS上でNodeコンテナとして運用しているため、VercelやNetlifyは利用していない
  • CSSについては、デザイナーさんがHTML+CSSを記述してくれるのでCSS Moduleを利用。Atomic Designは採用していない

なお、Storeの構成については、Read要件がメインのサービスでありシンプルなため、この記事では特に触れません。

サービスの利用については、現在、弁護士ドットコムに登録している弁護士の方、または弁護士事務所の事務所単位で利用する方のみ登録可能です。ただ、書籍の検索は誰でも可能なので、動かしてみたい方はトップページから検索ワードを入力してみてください。

技術スタックは以下の通りです。

- フレームワーク: Next.js(React) + TypeScript
- 状態管理: useContext(Reduxを導入する予定)
- データフェッチ: Next.js組み込みのfetch、 SWR
- CSS: CSS Modules(SCSS)
- テストフレームワーク: Jest、 @testing-library/react-hooks
- CDN: Akamai
- CI: GitLab CI
- インフラ: ECS、ECR、RDS(MySQL)
- 監視: Datadog
- その他: ESLint・Stylelint・Storybook、Renovate、Docker Compose、Stripe

普段はDiscordを使いながら、ペアプログラミングで開発しています。

レイヤードアーキテクチャを採用

結論から記述すると、フロントエンドでレイヤードアーキテクチャを採用しました。

レイヤードアーキテクチャ

(図はマーティン・ファウラーのブログ記事「PresentationDomainDataLayering」より)

「PresentationDomainDataLayering」ボブおじさんのクリーンアーキテクチャを参考にレイヤードアーキテクチャを採用し、各レイヤーがクリーンになるように設計しています。

昨今、フロントエンドでクリーンアーキテクチャを適用する試みが見られます。しかし、そもそもクリーンアーキテクチャはWebを外部のI/Oであると定義しており、デスクトップやCLIでも動作する、MVC2ではないアプリケーションを想定しています。

弁護士ドットコムライブラリーはWebアプリケーションであるため、クリーンアーキテクチャを適用しましたとは言わないものの、クリーンアーキテクチャのエッセンスを抽出した「クリーンなアーキテクチャ」を目指して設計しました。

このアーキテクチャの特徴は、以下のようなものです。

  • レイヤーごとの責務が明確であること
  • モジュールの依存の方向が制御できていること
  • モジュールがテスタブルであること(本記事執筆時点で、Unit Testのテストカバレッジは85%です)
  • 外部のものはアダプターとして使い、アプリケーション内に依存をばら撒かないこと

これらの特徴を備えたアプリケーションは、メンテナンス性に優れており、仕様の追加や変更に強く、コードの処理が追いやすくなります。

ディレクトリ構成

上記の特徴を実現するために、ディレクトリ構成は次のようにしています。

app
├── pages                    # next.jsのページコンポーネント。各tsxのファイル名がURLのpathに対応している。
├── public                   # 静的ファイルを置く場所。faviconとか、サイトのロゴなど
│   └── images               # サイト内で使用する画像
└──src
    ├── assets              # pages、componentsで利用する共通のSCSS
    ├── components          # ReactのFunction Component、コンポーネント単位のSCSS
    ├── hooks               # コンポーネント間で共通のReact Hooks
    ├── interactors         # Network層。HTTPを介して外部と通信するクラスを置いている
    ├── lib                 # Adapter層。moment.jsやGoogle Analyticsのライブラリなどを呼び出している
    └── type                # アプリケーション内で共通の型を置いている
        ├── API             # RESTful APIのエンドポイントから返却されるJSONの型
        └── domain          # アプリケーション内で利用する型

Next.jsを採用しているため、ルートディレクトリにあるpagesディレクトリのファイルがルーティングに対応しています。ファイルシステムに基づいたルーティングは、素のHTMLをサーバーで配信するのと同じですね。

components内には、React Componentを記述しています。この中は、export用のindex.ts、FooコンポーネントのFooComponent.tsx、コンポーネント内で利用するSCSS(style.modules.scss)、Storybook用のコンポーネント(index.stories.tsx)、プレゼンテーションロジックを記述するpresenter.ts(後述)を配置しています。

また、interactorsというディレクトリはあまり見かけないと思います。この役割はfetcherとmapperです。つまり、APIのレスポンスデータであるJSONをJavaScriptのオブジェクトに変換し、TypeScriptでドメインの型にマッピングするための処理を記述しています。このレイヤーにより、APIの変更による影響を最小限に抑えることができます。こちらは次の章で説明します。

なお、ユーザーのリクエストからレスポンスまで、データフローは以下のような流れです。

データフロー

InteractorとMapper(データアクセス層)

Interactorの役割

Interactorはバックエンドからデータを取得するレイヤーです。interactorsディレクトリの中身は以下の通りです。

interactors
├── BaseInteractor.ts   # fetchをラップした、get, post, put, deleteメソッドを備えたクラス。
├── Books               # 書籍データを持つElastic Searchサーバーへのリクエストを担当
│   ├── Book
│   │   ├── BookInteractor.ts
│   │   └── BookMapper.ts
│   └── Search
│       ├── SearchInteractor.ts
│       └── SearchMapper.ts
├── Payment             # Stripeサーバー(決済)へのリクエストを担当
│   ├── Card
│   │   └── CardInteractor.ts
│   ├── Customer
│   │   └── CustomerInteractor.ts
│   └── Subscription
│       ├── SubscriptionInteractor.ts
│       └── SubscriptionMapper.ts
└── Session             # Sessionサーバー(ユーザー認証情報)へのリクエストを担当
    ├── SessionInteractor.ts
    └── SessionMapper.ts

Book、Payment、Sessionの3種類のInteractorは、それぞれ書籍の検索、課金、ログインセッションサーバーの各エンドポイントに対応しています。

この3種類のInteractorクラスにBaseInteractorを注入し、HTTPメソッドに応じた通信をするようにしています。

例えば、書籍サーバーからIDに応じて書籍データを取得するコードを掲載します。

BookInteractor.tsx
export default class BookInteractor {
  // ClientInterfaceはget/post/put/deleteメソッドを持つインターフェースです
  private readonly interactor: ClientInterface

  constructor() {
    // BaseInteractorを注入
    this.interactor = BaseInteractor.createBookInteractor()
  }

  findById = async (id?: string): Promise<Book | null> => {
    if (typeof id === 'undefined') {
      return null
    }

    // IDに応じた書籍データを取得する
    const res = await this.interactor.get(`${BOOK_BIBLIOGRAPHIES_PATH}/${id}`)
    try {
      const body: BookBody = await res.json()
      // 次で解説しています
      return BookMapper.bibliographyBodyToBook(body)
    } catch (e) {
      // 例外をnullで表現していますが、アンチパターンだと思うため要リファクタリングです😅
      // なお、SWRでこのクラスを利用すると、try/catchの記述は省略できます
      return null
    }
  }
}

エンドポイントごとにInteractor(fetcher)を用意しているため、エンドポイントが増えればInteractorを追加すれば仕様追加に対する変更が完了します(Open Closed Principle)。

或いは、「書籍を全件取得する」という仕様が追加された場合、BookInteractorfindAllメソッドを記述するだけでOKです(Single Responsibility Principle)。

なお、クエリストリングが必要な場合、interactor.getの第二引数に渡します。

Mapperの役割と特徴

Mapperの役割は、バックエンドが返却する値にフロントのアプリケーションを依存させないことです。Mapperの特徴は、Interactorのメソッドと1対1対応していることです。

その内容は、エンドポイントから返されるJSONをドメインの型にマッピングするためのクラスです。

BookMapper.tsx
export default class BookMapper {
  // BookBodyはレスポンスの型、Bookがアプリケーション内で利用する型です
  static bibliographyBodyToBook = (result: BookBody): Book => ({
    id: result.content_id,
    title: result.title.main,
    subTitle: result.title.sub,
    authors: result.authors || [],
    publisher: result.publisher,
    publishedAt: result.release_date,
    tableOfContents: result.toc,
    thumbnailUrl: result.thumbnail_url,
    abstract: result.abstract,
    url: result.url,
  })
}

Mapperというレイヤーを設けておくことで、バックエンドから返却される値が変わった場合出会っても、このMapperを変更するだけで済みます。このため、アプリケーション内部の変更の影響を最小限に留められます。

なお、返却されるJSONがとてもシンプルな場合は、Mapperを書かずにInteractorの中でドメインの型にマッピングすることもあります。

InteractorをReactで利用する際は、useEffect内でInteractorを呼び出すことでレスポンスデータを扱います。

type Props = { id?: string }

const Book: React.FC<Props> = (props) => {
  const [book, setBook] = useState<BookType | null>({})

  useEffect(() => {
    async(() => {
      setBook(await new BookInteractor().findById(props.id))
    })()
  }, [props.id])

  if (book === null) {
    return <Error message={"書籍取得に失敗しました"} />
  }

  return <h1>title: {book.title}</div>
}

このInteractorはSWRでも活用できます。

type Props = { id?: string }

const Book: React.FC<Props> = (props) => {
  const { data: book, error } = useSWR<Book>(
    `${BOOK_BIBLIOGRAPHIES_PATH}/${props.id}`,
     () => new BookInteractor().findById(props.id)
   )

  if (!book) {
    return <Loading />
  }

  if (error) {
    return <Error message={"書籍取得に失敗しました"} />
  }

  return <h1>title: {book.title}</div>
}

ReactコンポーネントとPresenter層(プレゼンテーション層)

弁護士ドットコムライブラリーはバックエンドからのデータ表示がメインのアプリケーションであるため、Interactorから渡された値を表示するためのロジックを格納するPresenter層を用意します。バックエンドに例えるとMVVMのViewModel層に相当します。

Presenter層を紹介する前に、まずはクリーンなReactコンポーネントの書き方をご紹介します。

クリーンなReactコンポーネントの書き方

Greetingコンポーネントを例にReactコンポーネントの書き方を紹介します。すると、src/components/greetingは下記のような構成になります。

src/components/greeting
├── __tests__
│    ├─ useGreeting.test.ts
│    └─ presenter.test.ts
├── index.ts
├── index.stories.ts
├── Greeting.tsx
├── presenter.ts
├── useGreeting.ts
└── style.module.css

今回は、hooksと、そのテストの記述は省略します。なお、ドラッグ&ドロップなどの複雑なUIの操作は存在しないため、@testing-library/reactによるコンポーネントテストは導入していません。

CypressによるE2Eテストは導入したいと思っていますが、現在はJest@testing-library/react-hooksによるUnit Testのみ記述しています(Unit Testのコードカバレッジは85%)。

また、Reactコンポーネントの書き方は、@takepepeさんの「経年劣化に耐える ReactComponent の書き方」を参考にしています。

この記事の意義は、Vue.jsのSFC(Single File Component)の書き方をReactに導入したことです。これにより、View(JSX)をComponentに、データ表示用のロジックをContainerに記述し、責務を分離できます。詳しい説明は記事をご覧ください。

Greeting.tsx
import React, { memo } from 'react'
import css from './style.module.scss'

type ContainerProps = {
  target?: string
}

type Props = Required<ContainerProps>

// デザイナーさんはComponentのJSXを記述すれば良い
// StorybookではComponentのみをimportする
export const Component: React.FC<Props> = (props) => (
  <h1 className={css['greeting']}>
    Welcome to, <span className={css['greeting__target']}>{props.target}</span>
  </h1>
)

// フロントエンドエンジニアが書く
// propsをComponentで表示するデータ形式に書き換える
const Container: React.FC<ContainerProps> = (props) => {
  const target = props.target || 'world'

  return <Component target={target} />
}

// memo化はComponent、ContainerのどちらでもOK
export default memo(Container)

コンポーネントのmemo化については、ContainerでもComponentでもどちらでも適切な方をReact.memoでラップしましょう。

また、Storybookのコンポーネントは以下のように記述しています。

Storybookのコンポーネントはプレゼンテーションであるため、importするのはComponentです。

Containerに記述するlocal stateやデータの変換処理は不要です。Storybook上でコンポーネントのstateを操作せずとも、Container(ViewModel)の処理の結果としてComponentに渡されるデータを複数用意すれば十分です。

以下は、Storybook v6 + TypeScriptの記述方法です。

index.stories.tsx
import { Meta, Story } from '@storybook/react'
import { Component as Greeting, Props } from './Greeting'

export default {
  title: 'components/Greeting',
  component: Greeting,
  argTypes: {
    target: { control: 'text' },
  },
} as Meta<Props>

const Template: Story<Props> = ({ ...args }) => <Greeting {...args} />

export const World = Template.bind({})
World.args = {
  target: 'World'
}

export const Next = Template.bind({})
Next.args = {
  target: 'Next.js'
}

Presenterの役割

さて、Presenterについて紹介します。presenter.tsは、Container内でのロジックをテスト可能にするための関数を記述するファイルであり、コンポーネントと1対1に対応するロジックを記述します。

「コンポーネントに閉じるロジックなら、Containerに直接関数を書いてもいいのでは?」という意見もありました。しかし、クリーンなアーキテクチャを設計する観点からロジックをコンポーネントから切り出しています。理由は以下の通りです。

- Container内のロジックをテスト可能にするため
- コンポーネント内にはPresentational Componentとロジックを記述するContainerしか配置しないため
  - ファイルの見通しが悪くなるため、functionや子コンポーネントは、例え小さいものでも同一ファイルに記述しない
- Reactコンポーネントのファイルは1ファイル100行以下にしておきたいため

ただし、全ての処理をpresenter.tsに記述するわけではありません。三項演算子やStringをNumberに変換する処理など、テストをせずともバグの原因になる不安のないものは、Container内に直接記述しています。

実際のPresenterは以下のように記述しています。

下記は、ヘッダーに配置している検索欄の表示/非表示をページごとに切り替えるロジックです。なお、各ページのパスは定数に切り出しています。

presenter.ts
const canShowSearchInput = (
    pathname: string,
    keyword: string | undefined,
    hitCount: number | null
): boolean => {
  switch (pathname) {
    case SITE_SEARCH_PATH:
      // 検索結果が0件の場合は表示しない
      if (hitCount === 0) {
        return false
      }
      // キーワードが存在しないときは表示しない
      return !!keyword
    case SITE_BOOKS_ID_PATH:
      // 書籍の個別ページなら、必ず表示する
      return true
    default:
      return false
  }
}

export default canShowSearchInput

このようなロジックは要件が複雑になると記述量が増えるためContainerコンポーネントの中に書きたくありません。また、stateを使ったロジックでもないので、あえてコンポーネント内に書く必要もありません。

トップページ

検索ページ

(検索欄はトップページでは非表示ですが、検索ページでは表示しています)

検索欄の表示、非表示なのでComponent(View)はbooleanさえ渡してもらえればよく、presenter.tsに切り出すのが適切なパターンといえるでしょう。あとはContainerで処理を呼び出すだけです。

Header.tsx
import Logo from '~/src/components/logo'
import Presenter from './presenter'

type ContainerProps = {
  pathname: string
  keyword?: string
  hitCount: number | null
}

type Props = {
  canShowSearchInput: boolean
}

export const Component: React.FC<Props> = (props) => (
  <nav>
    <Logo />
    {props.canShowSearchInput && <SearchInput />}
  </nav>
)

const Container: React.FC<ContainerProps> = (props) => {
  const canShowSearchInput = Presenter.canShowSearchInput(
    props.pathname,
    props.keyword,
    props.hitCount,
  )

  return <Component canShowSearchInput={canShowSearchInput} />
}

(Headerコンポーネントは説明のため簡略化しています)

Presenterのテストを記述することにより、ユーザーに意図しない形でデータやコンポーネントが表示されているかもしれないという不安がなくなります。

型の依存の方向を制御する(Types)

一般的に、モジュールの依存の方向を整理しなければアプリケーションが複雑になります。TypeScriptでの型定義も同様です。このため、型ファイルをsrc/typesに全て配置するようにしました。

このディレクトリ内の型ファイル自体は外部の何にも依存していないため、src/componentspages配下でのみ使います。これにより、Reactコンポーネント内で使う型の依存方向を一方向にでき、依存の方向を制御できます。

ただ、特定のコンポーネントツリーでしか使わない型について、最近はsrc/componentsの各コンポーネントでtypes.tsファイルを作り、そこに書くようにしています。この型ファイルは他のコンポーネントツリーでは使用しません。2箇所以上で同じ型を使う場合、globalなものとみなしてsrc/types配下に切り出します。

また、初期では避けていましたが、今では子コンポーネントのPropsをexportして親コンポーネントで利用することもあります。こちらも、「同一コンポーネントツリー内のみで、子から親へのみimport可能」というルールを設けています。

マーティン・ファウラーのPresentationDomainDataLayeringとの対応

最後に、冒頭で紹介したファウラー氏のPresentation、Service, Domain Objects、Data Mapper、Data Accessと各レイヤーの対応をチェックします。

レイヤードアーキテクチャ

Presentation

PresentationはReact ComponentのContainerのロジックとPresenterに対応します。

Service, Domain Objects

この層に対応するレイヤーはありません。APIからjsonで取得したデータを表示させるだけであるので、Entitiy同士が相互作用する場面や、Storeから取り出した値を組み合わせて使う場面がないためです。

Data Mapper

APIから取得したJSONをアプリケーション内で使う型に変換する層であるため、InteractorのディレクトリにあるMapperに対応します。なお、Interactorの各メソッドとMapperは1対1で対応している。

Data Access

Data AccessはInteactorに対応しています。Inteactorという名前は同僚の@tenjuu99さんが開発している業務システムのコードを参考にしました。なお、業務システムはNuxt.js + BEAR.Sunday(PHP)で構築されています。

また、バックエンドのAPIはデータベースではありません。このため、Data Access層との対応は疑似的なものです。

半年間運用してみた所感

半年間運用してみた結果、感触はとても良いと思いました。

  • Presenter(Container)とComponentを分けるのは、思考がシンプルになる
  • 各レイヤーの責務が明確なので、仕様の追加・変更があっても、コードを読む箇所、書き換える箇所が狭い
  • 処理を追加する際、何をどこに書くか悩まない
  • デザイナーさんとの協業が楽(「CSSをいじるときにComponentだけ見ればいいのでわかりやすい」とのデザイナーさん評)

総じて、Next.js自体がディレクトリ構成までは指定しないフレームワークなので違和感はないです。また、新しいメンバーがジョインしても、MVCで開発した経験があれば容易に理解できると思います。

アーキテクチャを考え、テストを書き、慎重にデプロイした結果、半年間の本番で小さいバグは数個あったものの、中・大規模な障害は1度も発生せず、デプロイの切り戻しは一度もありませんでした。このため、安心して開発できます。

これからリファクタリングをしていきたいこと

以下では、これからのリファクタリング案を記載しています。現行のアプリケーションで特に問題にはなっておらず、またイテレーションの中で消化するタスクとして切り出してはいませんが、更なる品質向上のために必要だと思うことを書き出しています。

  • 初期はsrc/componentsにコンポーネントの粒度を気にせず置いていたので、下記のようにコンポーネントを整理する
    • pages/sharedで分ける
      • pagesはそのページでしか使わないコンポーネント
      • sharedは2箇所以上で使う共通コンポーネント
  • useContext/useReducerで行っているglobalな状態管理をRedux + reselect + immerに置き換える
  • Next.jsのpagesはNext.jsとstoreとの接続層とする。Next Routerもこの層でしか使わないようにする
    • Next.jsへの依存を限定するため

これらは、時間を見つけて対応していきたいです。

設計段階の狙いはかなりの部分で達成していますが、まだまだやりたいことはたくさんあります。質問などがあればtwitterまでぜひよろしくお願いします。

最後に、フロントエンドのアプリケーションを構築した経験から、結局アーキテクチャや採用技術はアプリケーションの性質・仕様・要件次第だと考えています。本記事は、write要件の少ない中規模のアプリケーションのアーキテクチャ例として一読いただければ幸いです。本記事での考え方はReactのアプリケーションに留まらず、どこか別のところでも応用できると考えています。

明日、アドベントカレンダー3日目は弁護士ドットコム本部・開発部のTech Lead @kano さんの「Polyfill.io を使って JavaScript の Polyfill を適用する」です!

追記(2022年6月)

少し補足をします。記事を書いてから2年半の時間が経ちました。自分は弁護士ドットコムを2021年4月に退職しており現在の状況はわかりません。

記事内容に今でも通用する箇所もありますが、振り返ると interactor はどうも必要なさそうだと思ったり(クラスまで作る必要なく、fetcher をラップした関数群で良い)、「redux + reselect + immer に置き換えたい」と書いていたり(結局 useReducer + useContext のまま)、型の置き場所も今読むと要検討だと思う内容だったり、内容が古くなっている箇所もあります。

キーをスネークケースからキャメルケースに変更するのも camelcase-keys のようなライブラリに任せれば自前で書かなくて済むし、そもそもこのプロジェクトはバックエンドのスキーマを Open API を使って定義していたので今から考えると fetcher を自動生成できました。

エラーハンドリングは記事内で TODO としていますが、もし今やるなら { data, error, status} の形式で返すように設計すると思います。

この記事は内容に古いところはあるものの、それでもフロントエンドでユニットテストを書いているという実例として価値があると考えています。また、Presenter(Container)と Component に分けるのは、当時ベストだと考えていたものの、実際に他の現場ではあまり見ないですし、自分も個人開発ではもうやっていません。ただ、弁護士ドットコムではデザイナーさん全員が HTML と CSS を書けるため、コンポーネントを修正する際にデザイナーさんからは「最初は React だと聞いて全くわからないなと抵抗感があったが、直すべき箇所が Component だとわかるので修正するのに心理的負担がなくなった」と評判でした。そういった特殊事情もあったことを追記します。

フロントエンドの変化のスピードは早いです。記事の内容を鵜呑みにせず、現在の技術トレンドと世間で定着した技術、読んでくださる方のアプリケーションの要求・仕様を勘案して適宜読み替えて頂けると幸いです。

Next.jsReactTypeScript
プログラミングをするパンダ
プログラミングをするパンダ (@Panda_Program)
Software Engineer

書籍の読み方には2種類ある

書籍の読み方には大別して2通りある。最初から最後まで読む通読と、必要な箇所をピックアップして読む参照だ。 通読は小説を、参照は辞書を想像してもらうと良い。

技術書は実用書であり、後者の参照に向いている。 パターンのカタログのような書籍は特にそうだ。PoEAA 、GoFのデザインパターンはもちろん、リファクタリング、レガシーコード改善ガイドなどは、通読するより必要に応じて参照する方が良い。著者もそのことを知っていて、実は序文や第1章に通読より参照してほしいと書かれているケースがある。

参照方法は簡単だ。まず解決したい疑問を頭に浮かべて目次、もしくは巻末の索引からキーワード探す。自分の疑問に答えてくれそうな箇所があればそのページに飛んで読む。 その中で知らない単語があったり、参考文献が紹介されていればそれを辿って読む。知らない単語がある場合、大抵同じ書籍内の別の箇所で解説されている。答えや思索を深める記述にたどり着けなければ、本を替えて同じことをする。

書籍を横断して一つのテーマを読むこの方法をヨコ読みと私は呼んでいる。 頭から最後まで読む方法はタテ読みだ。通読から熟読、さらに韋編三絶とまで行けば良いのだが、長い文章の中から目的の、あるいは自分が面白いと思う情報を探すような浅い読み方なら「通読」よりもタテ読みの方が似つかわしいように思う。もちろん自分独自の言葉ではなく、本のタテ読みヨコ読みナナメ読みは昔からある言葉だ。

そのヨコ読みといえども、やっていることは Web 検索と変わらない。 Google が書籍の目次になり、リンクが知らない単語や別の参考書籍になり、検索結果に上がってくる別の Web ページが他の書籍になるだけだ。

ヨコ読みを実践する

エッセンシャルスクラム

最近、この読み方で業務中に生まれた1つの疑問を解消できた。それは、「toC サービスにおいて、あるプロダクトにおける新規機能のレビュー者は誰が適切なのか」というものだ。

この疑問を持った前提として、プロダクトオーナーはチーム内にいてユーザーのことをチーム内で一番よく知っているものの、純粋なプロダクトのユーザーではない。このため、プロダクトオーナーがよしと言っても、ユーザーが本当に喜ぶかがわからない。よって、レビュー者を外部ユーザーに依頼するべきではないか、しかし単発ならまだしも、定期的なレビューであれば協力者を探すのが大変なため簡単にはできない、さてどうしたものかとチーム内で議論が膠着した。

そこで、スクラムではどのように考えられているのだろうと、エッセンシャルスクラムのスプリントレビューの章を読んでみた。すると、計10ページほどの記述の中で、まさに上記の議論が既にされていた。要約すると「プロダクトのレビューは、内部ステークホルダーで完結するならそれで良いが、定期的に外部ステークホルダーにも参加してもらうと良い」とのこと。

さらに「誰かを招待する時には、常識だけではなく熱意と人格を考慮すると良いだろう」という有益なアドバイスも書かれている。 このように具体的な解決方法の指南のみならず、著者のエピソードが豊富だったり、単なる Q&A 以上のお節介なことが書かれているというのは、エッセンシャルスクラムに限らない分厚い技術書の特徴の一つだ。

レビューについてもう一冊読んだ気もするが忘れてしまった。「プロダクトのレビュー者を誰にするか」という同じ疑問を持ってリーンスタートアップを読むと良いかも知れない。それでは良い読書体験を。

エクストリーム・プログラミング

と、ここで筆を置こうと思ったが、流石に締まりが悪いのでもう一冊実際に読んでみる。 エクストリーム・プログラミング(第2版)の「本物の顧客参加」というプラクティスから引用する。

顧客参加のポイントは、ニーズをもつ人とそれを満たす人が直接やりとりをして、ムダな労力を減らすことである(エクストリーム・プログラミング 2nd Edition、p. 59)

本物の顧客がいなければ、あるいは本物の顧客の「プロキシ」しかいなければ、使われないフィーチャーを開発したり、本物の受け入れ条件を反映していないテストを仕様化したり、(中略)、さまざまなムダを招いてしまう(同 p.60)

前後の文脈から、XP の想定する顧客は toB の受託開発のように読めるが、上記の警告は toC にも当てはまる。

このように、同じテーマの疑問を持って複数の書籍に当たるヨコ読みは、疑問を解決するだけでなくそのテーマの立体的な理解に役立つ。

上記のようにエッセンシャル・スクラムとエクストリーム・プログラミングという2冊の著者が異なる書籍を開くことで、「toC のプロダクト開発で機能のレビューは誰がするべきか」という問いの一定の答えがわかるだけでなく、ソフトウェア開発におけるプロダクトのレビューの目的とアンチパターンも語れるようになった。 つまり、ヨコ読みは学習効率が良いのだ。

なお、上記で答えは答えでも「一定の答え」と書いた。それは、科学的なプロセスを経て辿り着いた真実ではないが、現場で実践するには十分なアイデアとプラクティスではあるというニュアンスを含めているからだ。偉人の経験が詰まった書籍も万能ではない。できれば信頼に足る論文に当たるのがベストだという注意書きを持って本記事の締めくくりとしたい。

essay
プログラミングをするパンダ
プログラミングをするパンダ (@Panda_Program)
Software Engineer

PoEAA を通して DDD の半分を理解する

マーティン・ファウラーの PoEAA を読んでから、DDD のことを考え続けている。今まで DDD の話題はあえて避けてきた。分厚く難解な書籍、増えるコード量、教祖とその信徒たち(MV)、全てをその視点から解釈しようとする試み、少しでも間違えたら求められる自己批判、無知な者に対する SNS 上のオルグ、いつまでも出てこない総括、それでも信じるものは救われる。「一匹の亡霊がIT界隈を徘徊してる。DDDという亡霊が…」

まあ早まらないでほしい。何も DDD こき下ろそうというわけではない。自分の実力不足が主な原因と思い、深入りする前から「わからないもの」と決めつけていた DDD は、PoEAA というライトに照らされてその姿を私の前に姿を表し始めた。それは亡霊ではなく、確固たる手触りのある実体(Entity)だったのである。

PoEAA は 1990年代の開発現場でのオブジェクト指向の実装パターンをまとめた書籍だ。この書籍のある一文に「エリック・エヴァンスがオブジェクト指向の本を書いている。原稿を読ませてもらったがいい本になるだろう」と書かれている。それが世にも名高いDDD本である。

PoEAA には、「Value Object」「サービスレイヤ」「ドメインモデル」「レポジトリ」など、OOP の実装パターンが掲載されている。現代では当たり前になったこれらのパターンを学び直すには最適だなと思いつつ、ふと思い出した。DDD でもこれらのパターンを使っていたではないかと。

手元にある「ドメイン駆動設計 モデリング/実装ガイド」の目次を開くと、第6章の見出しには「エンティティ」「値オブジェクト」「ドメインサービス」「リポジトリ」「ファクトリー」とある。

マーティン・ファウラーは、「自分はパターンを考案したのではなく、当時の開発で使われていたパターンを PoEAA にまとめただけだ」と書いていた。そして、冒頭のエヴァンスの本に対する感想から、DDD 本は PoEAA の後に出版されていることがわかる。つまり、DDD は PoEAA に含まれているような当時の OOP 開発のベストプラクティスを活用しているのだ。

さらに、DDD Reference の日本語版の謝辞(有志による翻訳) の末尾に、「本の出版から数年後に、ウォード・カニンガム が パターンレポジトリ(Portland Pattern Repository)の仕事の一環として、 DDD 本の要約をクリエイティブ・コモンズで公開することを提案した」との記述がある。

つまり、DDD の半分は実装パターン集だったのだ!もちろん知ってる人は知っている。ただ、自分がこの数年抱き続けていた「DDDとは何か」という疑問が解消されたプロセスを記しておきたい。

ではもう半分は何か。それはドメインに対するモデリングである。実は、冒頭で書いた総括がないというのは嘘だ。PIXTA のラジオ texta.fmの第1回 「 Software Development in 2003 」で、@t_wada 氏が「DDD 本が出版されたのは2003年。だから実装方法は古くなってしまった。しかし、この本は開発者の意識を業務ドメインに向かわせ、ソースコードをインプットにして開発後にもモデルを洗練させるフィードバックループを提示したことに意義がある」と話している(29:53 ~ 34:09)。

このラジオはもっと前に聞いて覚えていた。だから、「DDD の半分はパターン。もう半分はモデリングの話」とわかったのだった。DDD の半分は実装パターンであるということについて、DDD 本の要約である DDD Reference と DDD Quickly を参照しながら紹介する。

DDD の半分は実装パターンである

DDD の図の実装パターンを表したしたところ

これは、DDD Reference に掲載されている図だ。赤い囲みは私が書いている。この囲みの中の要素こそが、DDD における実装パターン集なのである。DDD Reference では、実装というものはこれらの「ビルディングブロック」を組み合わせだと書かれている。そして、OOP の熟練の開発者にはこのような感覚がある。

https://twitter.com/tanakahisateru/status/1533268037700583424?s=20&t=aNaUB0FTD7uCa_2WMFVeRg

また、TDD で開発していると「OOP とは、まず信頼できるシンプルなオブジェクトを作り、それらを組み合わせてやりたいことを実現する手段だ」と理解できる。ブロック(オブジェクト)をビルド(組み立て)するのが開発なのだ。

しかし、パターン集だけでは複雑なドメインに立ち向かうソフトウェアを構築することはできない。パターンはある課題とそのコンテキストを考慮しないと、丸い穴に四角い杭を打つように不適切な使い方になってしまう。盲目的にパターンを使って組み上げても、出来上がったものが砂上の楼閣であれば意味がない。

DDD Quickly では、このことを以下のように表現している。少し長いが重要な箇所なので引用する。

ソフトウエアの設計は家を構成するのに似ています。 いわば全体的な展望を作る作業です。一方、コードの設計は詳細部についての作業であり、どの壁にどの絵画を掛けるのかを決めるような作業です。 もちろんコードの設計はとても重要な作業ですが、ソフトウエアの設計のような基礎となる作業ではありません。ほとんどの場合、コードの設計上のミスは簡単に修正できますが、ソフトウエアの設計上の失敗を修正するには、多くのコストがかかります。絵画をもっと左側に掛けるのと、家の側面を取り壊して今までとは違うように改築するのはまったく違う作業です。しかし、優れたコード設計なしによい製品は完成しません。だからこそコード設計のパターン集を手元に置き、必要になったら適用します。 優れたコーディング技術は、きれいで保守しやすいコードを記述する手助けをしてくれます。(DDD Quickly, p.5-6。太字は筆者)

つまり、ソフトウェア設計は全体に関わる物である一方、コード設計は細部の作業である。コード設計は細部のものといえども、疎かにしてはならない。このため、細部の表現であるコードを設計する優れたパターンを使おう。それは、Entity や Value Object などのDDD で紹介している OOP の実装パターンなのだという論法だ。

やはり DDD の半分は実装パターン集なのだ。

「戦術的 DDD」「軽量 DDD」の名前の由来について

なお、DDD が紹介しているパターンを用いて実装をすることは、「戦術的DDD」「軽量DDD」と呼ばれている。確かに、DDD Reference には 1ページ目に Strategy(戦略) と Tactics(戦術) という単語が使われている(Tactics and strategy must be combined to succeed, and DDD addresses both tactical and strategic design.)。

戦略は目的を持った計画立案であり、戦術はその実行に相当する。戦術は戦略より下位の概念であり、DDD ではパターンを用いた実装を戦術、ドメインに対するモデリングを戦略と対応させているようだ。そこから「戦術的DDD」という単語はそのあたりから来ているように思う(おそらく原著の DDD 本にも戦略や戦術といった単語が登場するはずだ)。

そして、専門家との対話を必要とし、絶え間ない洗練を必要とするモデリングより、開発者に閉じて手を動かせばいい実装の方が軽いという感覚が「軽量DDD」という単語を生み出したのだろう。

DDD のもう半分はモデリングである

さて、DDD のもう半分はモデリングについてである。この辺りはまだ自分の中で消化しきれていないため、簡単にだけ今現在の自分の理解を紹介する。

DDD の図のモデリングを表したしたところ

ドメインとはある複雑な業務領域である。その領域で仕事をする人は、ドメインについて知っている。その人をドメインエキスパートやドメインの実践者(domain practitioner)と呼ぶ。

そして、ドメインに対する理解、つまりモデルはドメインエキスパートの頭の中にある。しかし、あるドメイン実践者の頭の中にモデルがあっても、他の人には伝わらない。特にソフトウェアの開発者(software practitioner)がドメインのことを理解しないと、ソフトウェアを設計できない。

特定の業務領域には特有の概念を表す専門用語がある。開発者はドメインエキスパートに寄り添い、彼らと共通言語を使う(ユビキタス言語)。これによりドメインエキスパートと開発者によるコミュニケーションの基盤を確立し、意味の一貫性を保証する。コードのクラス名や変数名にもユビキタス言語を使う。

さらにこのユビキタス言語を用いて、ドメインに関する重要なことを形式化する。形式化の方法は、UML でユースケース図やドメインモデル図を書いたり、文章でもいいし、ポンチ絵でもいい。

最初はドメインに対する理解が浅く、大したモデルにならない。しかし、専門家との対話を重ねることで、ドメインに対する理解が深まる。結果、モデルが洗練されていく。開発、対話、モデルの洗練、開発、モデルのコードへの反映、また対話、モデルの洗練、開発、モデルのコードへの反映。この繰り返し(イテレーション、フィードバックサイクル)がより良いモデルを探求することにつながる。このあたりはアジャイル開発と親和性がある。

モデルを起点として開発は進んでいく。モデル駆動設計というワードが図の中心部にあることから、業務ドメインのモデルとその洗練こそが DDD の本質といえるだろう。

DDD の図のモデル駆動開発の要素

この辺りの説明はまだ読み終えていないので、今書けるのはここまで。

なお、DDD という単語こそ使っていないものの、請求書のソフトウェアを開発している LayerX 社ではエンジニアが簿記を学んでいるそうだ。これこそまさにエヴァンス氏がエンジニアに対して求めていた姿勢と言えるだろう。

https://twitter.com/naobit_/status/1533249637057847296?s=20&t=lVgoYzrYkT4WdnnHGowv2Q

本記事の内容は知ってる人にとっては当たり前のこと

ここまで「DDD の正体」と大仰に書いてきたが、何て事はない。知ってる人にとっては当たり前のことだ。だって「ドメイン駆動設計 サンプルコード&FAQ」の冒頭にこう書いている。

DDDの目的は、「ソフトウェアの機能性と保守性の両方を高めること」です。(中略)

そのために、DDDでは次のアプローチを行います。

①ドメインエキスパートと共に行うモデリング

②頻繁なモデルの更新に耐えられる実装パターン

(中略)

2つのアプローチは「ドメインモデル」でつながっており、一緒に適用するとより大きな価値を発揮するようになっています。

まさにDDDを端的に言い表している。

さて、ここまで辿ってきて一つ素朴な疑問が湧いてくる。それは「使う人はドメインの専門家だ。それなら、その人が作ればいいのでは」というものだ。だが、話題があちこちに飛んでしまうので、この疑問の深掘りは別の記事に譲りたい。

なお、この記事の公開3日前に書かれた「ドメイン駆動設計の源流のPofEAAを読んでみる」という記事も面白い。同じ時期に同じ過程で似たようなことを全く別の場所で考えている人がいることに驚いたので最後に紹介しておく。

DDD を学ぶための資料

まず DDD の原著に当たるのではなく、DDD Reference と DDD Quickly を読むとそのエッセンスを感じ取ることができるだろう。IDDD 本は評判が良い(が私は未読)。

実装で困ったら下記の2冊をおすすめする。

DDD Quickly は友人の @TSUCHIYA_Naoki に教えてもらった。松岡さんの本は同僚が勧めてくれた。感謝。

essayddd
プログラミングをするパンダ
プログラミングをするパンダ (@Panda_Program)
Software Engineer

PoEAA から始めるパターン集

2年ほど積読していた PoEAA を手に取ってみると、これが意外と面白い。PoEAA は20年ほど前の書籍であるものの現代でも通用するところがある。というより、PoEAA は当時の実装パターン集であるため、現代でも活かせることがあるのは当然とも言える。

それは、パターンが現場で直面する課題に対して、繰り返し活用できる解決方法だからだ。 ソフトウェアがデータベースにアクセスしたり、オブジェクトのコレクションを扱ったり、同じインターフェースを備えた複数のオブジェクトを差し替えて動き方を変えることは20年以上前から今に至るまで変わらない。これからも同じことを相変わらず行うだろう。

ただ、厄介なことに PoEAA の実装コードは古くなっている。 本書の序文によると、著者であるマーティン・ファウラーが書籍内のパターンに出会ったのは80年代後半から90年代初頭であり、それらはSmalltalk、C++、CORBA などで書かれていたとのことだから当然だ。

書籍内のコードは Java で記述されているため今でも読めないこともない。 ただし、そのコードをそのまま実務で記述してもコードレビューは通らないだろう。設計が過剰である上にコードが冗長という指摘を受けるに違いない。また、現代ではメジャーな MVC フレームワークの備えている機能を活用すれば十分な箇所もある。

しかし、それでも PoEAA は現代でも読むべき価値のある書籍だと痛感している。なぜなら、この書籍のおかげでバックエンドエンジニアとのコミュニケーションがスムーズになり、さらに自分が前から持っていた 「DDD 忌避症」が解消されたからだ。 PoEAA を読むことで、DDD の半分はパターン集であり、そのパターンも PoEAA が執筆された当時のものを参考にしているとわかったためだ。

本記事では PoEAA を例に、パターン集を読むための私見を紹介する。ここでいうパターン集は PoEAA の他に、GoFのデザインパターンリファクタリングDomain Driven DesignSQLアンチパターンあたりを想定している。

パターン集を読む時に注意していること

パターン集を読む際、個人的に注意していることは「現代では通用しない箇所を差し引きつつ、場所と時を超えて普遍的なことを頭に入れておく」ことだ。

実装詳細は差し引いて読む

まず、差し引いて読むところはどこか。それは実装詳細だ。 例えば、PoEAA に出てくる JAVA のコードも、Kotlin で書き直すともっと洗練されたコードになるだろう。「リファクタリング」は初版のサンプルコードは Java だったが、第2版で JavaScript に変えられたので読みやすくなった。ただし、このようにコードが書き直されることは稀である。

実装は、その本が書かれた時に流行しているプログラミング言語や当時のベストプラクティスによって変わる。このため、パターン集を読むにあたって実装を鵜呑みにしないことが重要だ。

普遍的な記述に注目する

反対に、頭に入れるべきは現代でも通用する普遍的な箇所だ。 もちろん、あるパターン集の記述の全部が今でも活用できる知識ではない。このため、深い洞察がある記述を探すのもパターン集を読む醍醐味の一つである。

PoEAA は2部構成であり、第2部では様々な実装パターンのカタログだ。さらに実装パターンの紹介も「パターンの定義」「動作方法」「使用するタイミング」「実装例」という一種のパターンになっている。そして、「パターンの定義」「動作方法」「使用するタイミング」 には普遍的な内容が書かれている可能性が高い。

パターンはその定義と使うタイミングを覚えておく

特にパターンの定義とそれを使用するタイミングを覚えておくと良い。 すると、そのパターンが解決する課題に直面した時(使用するタイミング)に、パターン名の実装方法を検索できるからである。検索方法は、書籍内の PoEAA の該当の記述でも良いし Google でも良い。 もちろん、人とのコミュニケーションに役立つ。 CTO やテックリードに聞いたりや Stack Overflow で質問しても良い。

その際、「このパターンが適用できそうなのですが、そもそも今回の課題に当てはまりますか。また、そうであればこの言語・フレームワークだとどう実装するのがいいでしょうか」と尋ねることができる。質問された方もそのパターンを知っていると「ああ、あれね」とコミュニケーションがスムーズになる。 メジャーなパターンはコミュニケーションの共通言語になるのだ。

特に PoEAA では、現代の開発者にとって当たり前の単語がパターンとして解説されている。 このため、プログラミングの基礎を学び終え、綺麗なコードを書こうとするプログラマにとって重宝する。「アクティブレコード」「レポジトリ」「サービスレイヤー」「ドメインモデル(今では一般的にドメインオブジェクトと呼ばれている)」「データ・トランスファー・オブジェクト(DTO)」「データ・アクセス・オブジェクト(DAO)」「トランザクションスクリプト」「クエリーオブジェクト」などという単語を理解しておくとこの先もずっと使える知識となる。

そして、パターン名とそれを使うケースを読んでおくと、「目の前の課題を解決するためのとっかかり」が頭の中にできる。すると、目の前にある課題があったとき、その文脈・背景を知っている自分、もしくはチームが、普遍的な課題解決策であるパターンの視点から課題を捉え直すことができる。

パターンの注意点

ただし、パターンは銀の弾丸ではない。このため、書籍にあるようなパターンは大抵そのまま適用できない。

このため、パターン集において、その実装は鵜呑みにしてはならない。それよりも、パターンの定義とそれを活用する場面、コンテキストを頭に入れておくこと。 これは PoEAA だけではなく、もちろん GoF のデザインパターンにも当てはまる。

また、何でもマイクロサービス化しようとしたり何でも DDDを適用しようとする態度も、結局それらは一種のパターンだという観点が抜け落ちている。 マイクロサービスは数あるアーキテクチャパターンの一つに過ぎず(その他のパターンは「ソフトウェアアーキテクチャの基礎」に数多く掲載されている)、DDD もその半分は実装パターン集である。

ソフトウェアアーキテクチャであれ、アプリケーションアーキテクチャであれ、パターンの活用原則通りに、そのパターンが課題に対する適切な解決策になっているか、コンテキストを考慮しながら慎重に判断する必要がある。

簡単にまとめると以下のようになる。

パターン学習の原則

  1. パターンの定義を理解する
  2. 実装詳細より、パターンのユースケースを重視する
  3. パターンは専門家の共通言語である

パターンを適用する原則

  1. パターンは、課題解決の出発点である
  2. パターンは、教科書通りに適用できるとは限らない
  3. パターンは、無理に適用するものではなく、後から発見するものである

読書を楽しむ

この記事では、技術書のパターン集の読み方を解説してきた。色々書いてきたが、古いパターン集だから役に立たないだろうし読みにくそうと避けずに、まずは手に取ってみて読書そのものを楽しむことだ。

普遍的な記述の箇所だけに価値があるのではない。古くなっている記述は、当時の技術の流行や雰囲気を今に伝えている。パターン集は古典である。古典を読んだ後は、視点が相対化され、同じものを見ていても昨日と今日で捉え方が変わる。古典と向き合うことは、まさに自分の世界を広げることに繋がるのだ。

最後に、田中ひさてるさんの 「GoF デザインパターン チートシート」 を紹介する。この記事にはデザインパターンの乱用について釘を刺すベテランならではのアドバイスが書かれている。

essay
プログラミングをするパンダ
プログラミングをするパンダ (@Panda_Program)
Software Engineer