はじめに

本書の内容

本書は@pospomeがサーバサイドアプリケーションのあれこれに関してまとめたものです。

著者

pospome。サーバサイドエンジニア。DDD、実装パターンのようなアプリケーションアーキテクチャが専門。Twitterは@pospome。ブログは「pospomeのプログラミング日記」。共にアプリケーションアーキテクチャ、システムアーキテクチャ、Go言語、GCPを中心にアウトプットしている。キャラクターは羊ではなくポメラニアン。

誤字脱字報告フォーム

誤字脱字、不自然な日本語、図やサンプルコードの参照ミスを見つけた場合は以下のフォームから報告していただけると嬉しいです。

誤字脱字報告フォームはこちら

免責事項

本書に記載された内容は、情報の提供のみを目的としています。したがって、本書を用いた開発、製作、運用は、必ずご自身の責任と判断によって行ってください。これらの情報による開発、製作、運用の結果について、著者はいかなる責任も負いません。

第1章 実装におけるSimple Or EasyとSimple And Easy

1.1 はじめに

筆者が"Easy","Simple"という概念を初めて知ったのはRails Conf 2012 Keynoteの発表がきっかけでした。Easy,Simpleという概念はプログラミングにおいて重要な概念です。本章では実装におけるEasyとSimpleについて筆者の考えを述べます。

1.2 SimpleとEasyの違い

プログラミングにおけるSimpleは"単純"という意味です。単純な実装はSimpleな実装と言えるでしょう。Simpleな実装の具体例はGo言語のdatabase/sqlパッケージです。database/sqlパッケージにはDBを操作するために必要な機能が実装されており、DBを操作するためにはリスト1.1のようにSQLを直接指定する必要があります。

リスト1.1:

//ref:https://golang.org/pkg/database/sql/#example_Tx_ExecContext
package main

import (
  "context"
  "database/sql"
  "log"
)

var (
  ctx context.Context
  db  *sql.DB
)

func main() {
  tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})
  if err != nil {
    log.Fatal(err)
  }
  id := 37
  _, execErr := tx.ExecContext(ctx, "UPDATE users SET status = ? WHERE id = ?", "paid", id)
  if execErr != nil {
    if rollbackErr := tx.Rollback(); rollbackErr != nil {
      log.Fatalf("update failed: %v, unable to rollback: %v\n", execErr, rollbackErr)
    }
    log.Fatalf("update failed: %v", execErr)
  }
  if err := tx.Commit(); err != nil {
    log.Fatal(err)
  }
}

DBのレコードを取得するコードはリスト1.2です。レコードを取得する際はdatabase/sqlパッケージのRows.Scan()を利用します。取得対象の列は1つ1つ引数に指定する必要があります。

リスト1.2:

//ref:https://golang.org/pkg/database/sql/#example_Rows
package main

import (
  "context"
  "database/sql"
  "log"
  "strings"
)

var (
  ctx context.Context
  db  *sql.DB
)

func main() {
  age := 27
  rows, err := db.QueryContext(ctx, "SELECT name FROM users WHERE age=?", age)
  if err != nil {
    log.Fatal(err)
  }
  defer rows.Close()

  names := make([]string, 0)
  for rows.Next() {
    var name string
    if err := rows.Scan(&name); err != nil {
      log.Fatal(err)
    }
    names = append(names, name)
  }
  // Check for errors from iterating over rows.
  if err := rows.Err(); err != nil {
    log.Fatal(err)
  }
  log.Printf("%s are %d years old", strings.Join(names, ", "), age)

database/sqlパッケージはDBを操作するパッケージなので、SQLを直接指定したり、取得対象のテーブル列を指定するのは自然なインターフェースでしょう。"DBを操作すること"に対してシンプルなインターフェースが提供されています。シンプルであるがゆえに、DBを知っているエンジニアであれば、このパッケージの使い方に違和感を覚えることはないでしょう。

一方、プログラミングにおけるEasyとは"簡単"という意味です。"Simple = 単純"と似ているように思えますが、単純と簡単は異なる性質を持ちます。先程database/sqlパッケージを例にSimpleな実装を説明しましたが、実際にdatabase/sqlパッケージをそのまま利用して開発するかというと、そうとは限りません。おそらくORMを利用することが多いでしょう。では、なぜ実際の開発でORMを利用するのでしょうか?それはおそらくORMがdatabase/sqlパッケージよりも便利だからです。筆者は過去にGo言語のORMを調べたことがありますが、どのORMも以下の2つの機能を備えていました。

  1. SQLを直接指定しなくてもデータを取得できる機能
  2. 取得したデータを構造体に直接マッピングできる機能

例えばGo言語のORMである"GORM"はリスト1.3のようにデータを取得し、userという構造体に取得した値をマッピングすることができます。

リスト1.3:

//ref:https://gorm.io/docs/query.html#Query

// Get first record, order by primary key
db.First(&user)
//// SELECT * FROM users ORDER BY id LIMIT 1;

// Get one record, no specfied order
db.Take(&user)
//// SELECT * FROM users LIMIT 1;

// Get last record, order by primary key
db.Last(&user)
//// SELECT * FROM users ORDER BY id DESC LIMIT 1;

// Get all records
db.Find(&users)
//// SELECT * FROM users;

// Get record with primary key (only works for integer primary key)
db.First(&user, 10)
//// SELECT * FROM users WHERE id = 10;

これらの機能はGo言語に限らず他の言語のORMも備えているものでしょう。わざわざSQLを書く必要がなく、取得した値を構造体にマッピングする必要もありません。簡単なSQLだけで完結するアプリケーションであれば、SQLを知らなくても実装できてしまいます。とても便利な機能です。database/sqlよりも簡単にDB操作が行えるGORMはdatabase/sqlに対して相対的にEasyな実装であるといえるでしょう。

このような説明だけだとSimpleな実装よりもEasyな実装の方が優れているように感じるかもしれませんが、そうとは限りません。Easyな実装はSimpleな実装にはない複雑性を抱えてしまう傾向にあります。例えばモデルに対するタグの指定であったり、Associationsの種類である"Beloings To","Has One"の理解であったり、GORMが提供するEasyな実装を利用するためには、こういった概念を理解する必要があります。もし、これらが期待した通り動作しない場合、"なぜ動作しないのか?"を調査するにはさらに多くの時間をかけることになるでしょう。この他にも"実際に発行されるSQL文を考慮して利用しなければいけない"、"複雑なマッピングパータンはサポートされていない"など、Simpleな実装にはなかった複雑さを抱えることになります。

1.3 Simple Or Easy

Simpleな実装とEasyな実装はどちらが優れているのでしょうか。結論から言うと、ケースバイケースです。しかし、Simpleな実装とEasyな実装の性質を具体的に考えると、どちらが適しているのかが見えてきます。例えばリスト1.4はSimpleなメール送信の実装です。Simpleな部品を組み合わせてメール送信という目的を達成するイメージです。

リスト1.4:

to := "xxx@pospome.com"
title := "my title"

cli := email.NewClient(to, title)

body := email.NewBody("xxx")
cli.SetBody(body)
file := email.NewFile("../../xxx.pdf")
cli.AttachFile(file)

cli.Send()

Simpleな実装はSimpleがゆえに目的を果たすまでのステップが多くなる傾向にありますが、Simpleな部品を組み合わせるという性質上、利用者のニーズに幅広く対応することができる実装になります。

一方、実際のユースケースとしてメールのタイトルと本文が固定であったり、添付ファイルが不要である場合は、より目的に特化した使いやすいEasyな実装としてリスト1.5のような関数を提供するだけで十分でしょう。

リスト1.5:

//送信先を指定するだけでメールを送信できるSendMail()という関数を利用する。
to := "xxx@pospome.com"
SendMail(to)

Easyな実装はEasyがゆえに目的を最短で達成できる実装を提供しますが、ある程度ユースケースが限定される傾向にあります。Simpleな実装ほど幅広いニーズに対応できるとは限りません。

筆者はSimpleな実装は不特定多数の利用者が想定されるライブラリに向いた実装であり、Easyな実装はWebサービスなどの特定のユースケースに特化することが想定できる場合に向いた実装であると考えています。筆者はWebサービスのサーバサイドアプリケーションエンジニアとして働いていますが、Webサービスの開発では特定のユースケースに特化した実装を提供すれば十分なケースが多いので、比較的Easyな実装を提供することが多いです。別の観点だと、アプリケーションコードの場合はその都度実装に求められるニーズが変わるので、実装時に最適な実装方法を選択することが多いです。その結果、Easyな実装を選択することが多くなります。一方で、筆者は社内で共通利用するライブラリの開発をすることもあります。その際は不特定多数のユーザーに向けてSimpleな実装を提供することを心がけています。Simpleな実装が適しているのか、Easyな実装が適しているのか、というのはケースバイケースですが、このような性質の違いが適切な実装を導くヒントになるでしょう。

試し読みはここまでです。
この続きは、製品版でお楽しみください。