【技術記事】その設計、変更に強いですか?単体テストできますか?...そしてクリーンアーキテクチャ

※ 本記事は、過去に Qiitaでも投稿した記事である。

はじめに

アーキテクチャや設計の書籍や記事、これまでの経験も踏まえ、学んだ事をここにまとめたい。(まだ、勉強中なので微妙なところもあるかもしれません。お気付きの点があればご指摘いただけるとありがたいです。)

参考文献や参考記事は、本当に良書、良記事で非常に参考にさせていただきました。

生意気なタイトルにしてしまいましたが、自分への戒めということもあってこのタイトルにさせていただいたので、ご容赦ください。

ある共通した話題

設計やアーキテクチャについて書かれた書籍や記事を読んでいく中で、言葉は違えどかなりの高確率で共通するテーマが存在した。 そう、それが 「変更に強くなろう」 といった趣旨のテーマだ。 アーキテクチャや設計に関する書籍や記事は様々な方法論で、これを実現しようとしていた。

今回のテーマと記事の構成

今回は、「変更に強くなろう」というテーマの中で重要だと感じた概念や考え方をまとめて、実際にそれがどう生かされているかということを考察していきたい。 「単体テスト」については、「変更に強い」コードを意識すると「単体テスト」がしやすいという副次的な効果も現れることがわかったので、サブテーマとして記述したい。 また、上記の2つを同時に満たしている思う最近何かと話題のクリーンアーキテクチャについても記述したい。

正直、「変更に強くなる」というのをテーマに書こうとしたのだが、テストもしやすくなるし、最近学んだクリーンアーキテクチャもそれらに関係があることなので、書きたいし...となって少し詰め込みすぎた感が否めないですが、ご容赦ください。

そのため、今回の記事はざっくり大きく以下の3つの編成にしている。 ・変更に強くなる編 ・単体テスト編 ・クリーンアーキテクチャ

変更に強くなる編

ここでは変更に強くなるための概念等を紹介する。 (書籍等では、他にももっとたくさん紹介されているが、ここでは基本的な一部のみを紹介する)

共通性/可変性分析

これは、『オブジェクト指向のこころ (SOFTWARE PATTERNS SERIES)』という書籍で紹介されていた概念である。

簡単にまとめると、 共通性分析とは、問題の中の変化しやすい要素(可変性分析で見つけた個々の具象)をまとめた変化しにくい抽象を見つけ出すこと。 可変性分析とは、問題の中の変化しやすい具体的な個々の要素を見つけ出すこと。

以下の一文が非常にわかりやすい。

これは問題領域のどこが流動的に要素になるのかを識別し(「共通性分析」)、そのあと、それらがどのように変化するのかを識別する(「可変性分析」)というものです。

引用元 : アラン・シャロウェイ (著), ジェームズ・R・トロット (著), 村上 雅章 (翻訳) (2014/3/11)『オブジェクト指向のこころ (SOFTWARE PATTERNS SERIES)』 丸善出版

さらに同書には、それを具体的にソフトウェアに落とし込んでいく方法が記述されている。

問題領域中の特定部分に流動的要素がある場合、共通性分析によってそれらをまとめる概念を定義できるわけです。 こういった概念は抽象クラスによって表現できます。そして可変性分析によって洗い出された流動的要素は、具象クラスになります。

引用元 : アラン・シャロウェイ (著), ジェームズ・R・トロット (著), 村上 雅章 (翻訳) (2014/3/11)『オブジェクト指向のこころ (SOFTWARE PATTERNS SERIES)』 丸善出版

自分なりに解釈すると、何かソフトウェアを設計する前には、そのソフトウェアによって解決する問題の中において、具体的な事象や物とそれらの抽象的な部分を見つけ出す。 その具体的な事象や物は似たような部分がいくつかあって、それらに共通する振る舞いを集めた概念を見つけ出す。 そして、具体的な事象や物は具象クラスに、共通する振る舞いをインターフェースや抽象クラスに落とし込んで設計していくのが大事なのだと思った。変化する具体的な問題とその問題に共通する抽象的な問題に分けるのだ。 いわば、抽象と具体に分ける。

依存関係

コードには依存関係が存在する。 例えば、AがBを呼んでおり、BがCを呼んでいるといった場合、依存関係は、A=>B=>Cといった具合になる。 この場合、=>の向きは一方向である。しかし、場合によっては、A<=>B<=>Cといった具合に、矢印が双方向に向いている場合もある。 これはAとBが互いに、BとCが互いに依存しあってしまっているのだ。これを循環依存という。 これはコードを複雑にしてしまう要因らしい。

参考: エリック・エヴァンス(著)、 今関 剛 (監修)、 和智 右桂 (翻訳) (2011/4/9)『エリック・エヴァンスのドメイン駆動設計 (IT Architects’Archive ソフトウェア開発の実践)』 翔泳社

Robert C.Martin (著)、 角 征典 (翻訳)、 高木 正弘 (翻訳) (2018/7/27)『Clean Architecture 達人に学ぶソフトウェアの構造と設計』 KADOKAWA

結合度

そのモジュールが他のモジュールにどれほど依存しているか(他のモジュールからどれほど独立しているか)ということ。 モジュール毎の結合度が低ければ、低いほど他のモジュールが変更になっても、影響を受けないので良いと考えられてる。変更に強い設計にするためにはこれを意識する必要がある。結合度が低いことを疎結合と言ったりする。

参考 : モジュール結合度とは - IT用語辞典 Weblio辞書

一旦整理

コードには依存関係があることもわかった。 依存関係は循環参照することなく、片方向の参照が好ましいという。

では、Aの具象クラスがBの具象クラスに依存し、Bの具象クラスがCの具象クラスに依存するというのはどうだろうか。 「共通性/可変性分析」のセクションで、具体的なこと(具象クラス)は変化しやすいことを説明した。 片方向とはいえ、変更されやすい具象クラスに依存するのは良いのだろうか。

A=>B=>Cという風に依存関係があった場合、どれも具象クラスなので変化しやすい。 例えば、Cに変化があったら、Bはどうなるだろうか。Bに変化があったら、Aはどうなるだろうか... Bは、Cの変更に伴って、コードを変更しなくてはならないし、AもBの変更に伴ってコードを変更しなくてはならない...辛い...

そりゃあ、数々の良書が「変更に強くなろう」というわけだ。 「変更に強くなろう」というのは、「ある変更に伴いドミノ倒しのように発生する数々のコードの変更に耐えられる精神的な強さを持とう!」と言っているのだろうか。いや違う。 ある変更があっても、他の部分に影響を(極力)生じさせない方法論を提唱してくれている。 実際の方法論や考え方を見ていこう。

インターフェースとポリモーフィズム

オブジェクト指向デザインパターンを勉強していると必ず出てくるこの2つの言葉。 変更に強くなるためには、この2つ(言語によっては抽象クラスなども含む)をうまく使うことが大事なようだ。 共通性/可変性分析の項目で変化しやすい具体的な事象や物とそれらに共通する変わらない抽象を見つけるという話をしたが、このインターフェースとポリモーフィズムというのはそれらをうまく扱うことを可能にしてくれる。

なお、この記事では、インターフェースとポリモーフィズム自体はある程度理解している前提で話を進めるので、それら自体の説明はあまりしない。 もしインターフェースやポリモーフィズムが怪しい場合は、以下の記事等を参照。

オブジェクト指向と10年戦ってわかったこと - Qiita

15分でわかる かんたんオブジェクト指向 - Qiita

インターフェースとポリモーフィズムの具体例に関しては、後述する。

共通性/可変性分析とインターフェースとポリモーフィズム

共通性分析において発見した共通的な振る舞いをまとめた抽象をインターフェースとして定義する。 可変性分析おいて発見した個別の具体的な物や事を具象クラスや構造体として定義する。

先ほどの「共通性/可変性分析」のセクションで、具体的なこと(具象クラス)は変化しやすいことがわかった。 では、他の具象クラスが依存する先を具象クラスに依存させるのではなく、インターフェースに依存させてしまえばどうか。

抽象は具象に比べると変化しにくく、具体的な物や事は変化しやすいので、抽象(つまりインターフェース)に依存させれば他の変更の影響を受けにくくなる。

クリーンアーキテクチャ本でも以下のように記述されている。

抽象インターフェースの変更は、それに対応する具象実装の変更につながる。一方、具象実装を変更してもインターフェースの変更が必要にあることはあまりない。つまりインターフェースは実装よりも変化しにくいということだ。

引用元 : Robert C.Martin (著)、 角 征典 (翻訳)、 高木 正弘 (翻訳) (2018/7/27)『Clean Architecture 達人に学ぶソフトウェアの構造と設計』 KADOKAWA

それはどういうことか。 ポリモーフィズムを使用すると、インターフェースを実装したクラスならば、そのインターフェースとして扱うことができる。 例えば、HogeInterfaceというインターフェースが存在したとして、そのインターフェースを実装しているHogeConcreteClass1HogeConcreteClass2は、全てHogeInterfaceとして扱うことができる。 いずれもHogeInterface として扱うことができるが、各々の実際の振る舞いはHogeConcreteClass1HogeConcreteClass2として振る舞う。

これを利用すると、最初に抽象(インターフェイス)を決めておきその後に具象(具象クラス)が変化した場合、抽象はそのままで具象を入れ替えることができる。 つまり、使用するクラスが抽象(インターフェイス)に依存していれば、具象(具象クラス)が変わったところで使用するクラス側の変更は行わなくとも、使用している抽象(インターフェイス)の具体的な振る舞い(具象クラスの振る舞い)が変わる。

参考: アラン・シャロウェイ (著), ジェームズ・R・トロット (著), 村上 雅章 (翻訳) (2014/3/11)『オブジェクト指向のこころ (SOFTWARE PATTERNS SERIES)』 丸善出版

DIP(依存関係逆転の法則)

ソフトウエアモジュールを疎結合に保つための特定の形式を指す用語. この原則に従うとソフトウェアの振る舞いを定義する上位レベルのモジュールから下位レベルモジュールへの従来の依存関係は逆転し、結果として下位レベルモジュールの実装の詳細から上位レベルモジュールを独立に保つことができるようになる. この原則で述べられていることは以下の2つである

  1. 上位レベルのモジュールは下位レベルのモジュールに依存すべきではない. 両方とも抽象(abstractions)に依存すべきである.

  2. 抽象は詳細に依存してはならない. 詳細が抽象に依存すべきである.

引用元 : 依存性逆転の原則 - Wikipedia

依存関係逆転の法則は、 インターフェースポリモーフィズム を用いて、モジュール間の結合度を緩やかにするためのもの。

もっと具体的にいうと、別のレイヤーのクラスなどを使用するときには、その具象クラスを直接使うのではなく、そのインターフェースを参照しようねということ。 AというクラスがBというクラスを利用するときに、Bを直接利用するのではなくて、Bの抽象(抽象クラスやインターフェイス)を利用するとBの実装に変更があっても左右されにくいので、そういう風にしましょうということ。

「共通性/可変性分析とインターフェースとポリモーフィズム」のセクションで記述した事を原則として切り出したものだ。

「変更に強くなる」とか、「単体テストをしやすくする」などの事を考えると、この原則は本当に重要なものだ。

なぜ依存関係の 逆転 というかは、この後の具体例のところでUMLぽいものを書いて説明する。

参考: Robert C.Martin (著)、 角 征典 (翻訳)、 高木 正弘 (翻訳) (2018/7/27)『Clean Architecture 達人に学ぶソフトウェアの構造と設計』 KADOKAWA

クリーンアーキテクチャ(The Clean Architecture翻訳) | blog.tai2.net

オブジェクト指向設計原則とは - Qiita

依存性逆転の原則 - Wikipedia

依存関係逆転の原則について · SunriseDigital/work-shop Wiki

具体例

コードを使用して具体例を示す。コードはGoで記述する。Goにこれまで馴染みのない方もなんとなくコードを見ればわかるかと思う。

これは後ほど記述するクリーンアーキテクチャで記述したコードの一部を切り取ったものだ。 クリーンアーキテクチャやコード全体は後述する。 この例では、ユースケースであるProgrammingLangUseCaseから使用されるデータベース周りの具体的な操作を行う構造体に焦点を当てる。

ProgrammingLangUseCaseから使用され、実際に操作を行うのはProgrammingLangDAOだが、ProgrammingLangUseCaseは、ProgrammingLangDAOをそのままProgrammingLangDAOとしては使用していない。 (UseCaseやRepositoryについて、詳しくはクリーンアーキテクチャ(The Clean Architecture翻訳) | blog.tai2.netを参照) どうしているかというと、ProgrammingLangRepositoryというインターフェースを定義し、その実装としてProgrammingLangDAOを使用している。 ProgrammingLangUseCaseは、ProgrammingLangRepositoryは知っているが、ProgrammingLangDAOは知らない。

なので、その部分は ProgrammingLangRepository を実装している構造体ならば、何にでも差し替えることができる。 例えば、今回は、ProgrammingLangDAORDB(MySQL)の操作を実装しているが、ProgrammingLangRepositoryのインターフェースを満たしたNoSQLを操作する構造体に差し替えることもできるかもしれないし、メモリに保存する構造体に差し替えすることもできる。 また、単体テストの際に、モックに差し替えることができる。これは単体テストを行う際には大きなメリットとなる。(単体テストについては後述する)

クラス図ぽいものを描くと以下のようなものになる。

CleanArch.png

上記のUMLのようにProgrammingLangUseCase(上位のレイヤー)がProgrammingLangDAOや、MockProgrammingLangRepository(下位レイヤー)に直接依存するのではなく、ProgrammingLangRepository(下位レイヤーの抽象)に依存し、ProgrammingLangDAOや、MockProgrammingLangRepository(下位レイヤー)は、ProgrammingLangRepository(下位レイヤーの抽象)の実装のため、下位レイヤーから下位レイヤーの抽象へ矢印が逆向きになるため、依存関係逆転の法則というらしい。

!注意1 : なんとなくUML図ぽく描いたものである。(厳密なUML図ではない)
!注意2 : 実際のコードにはもう少しメソッドがあるが、説明のためだけの図なので、図には書かない。

ProgrammingLangUseCase(上位レイヤ)

ProgrammingLangUseCaseは、ProgrammingLangRepositoryを通して、ProgrammingLangDAOMockProgrammingLangRepositoryを使用する。 そのため、ProgrammingLangUseCaseは、直接的には、具象であるProgrammingLangDAOMockProgrammingLangRepositoryを知らない。

program_lang_usecase.go

package usecase

import (
    "context"
    "time"

    "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/model"
    "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/repository"
    "github.com/SekiguchiKai/clean-architecture-with-go/server/usecase/input"
    "github.com/pkg/errors"
)

// ProgrammingLangUseCase は、ProgrammingLangのUseCase。
type ProgrammingLangUseCase struct {
    Repo repository.ProgrammingLangRepository
}

// NewProgrammingLangUseCase は、ProgrammingLangUseCaseを生成し、返す。
func NewProgrammingLangUseCase(repo repository.ProgrammingLangRepository) input.ProgrammingLangInputPort {
    return &ProgrammingLangUseCase{
        Repo: repo,
    }
}

// List は、ProgrammingLangの一覧を返す。
func (u *ProgrammingLangUseCase) List(ctx context.Context, limit int) ([]*model.ProgrammingLang, error) {
    return u.Repo.List(ctx, limit)
}

// Get は、ProgrammingLang1件返す。
func (u *ProgrammingLangUseCase) Get(ctx context.Context, id int) (*model.ProgrammingLang, error) {
    return u.Repo.Read(ctx, id)
}

// Create は、ProgrammingLangを生成する。
func (u *ProgrammingLangUseCase) Create(ctx context.Context, param *model.ProgrammingLang) (*model.ProgrammingLang, error) {
    lang, err := u.Repo.ReadByName(ctx, param.Name)
    if lang != nil {
        return nil, &model.AlreadyExistError{
            ID:        lang.ID,
            Name:      lang.Name,
            ModelName: model.ModelNameProgrammingLang,
        }
    }

    if _, ok := errors.Cause(err).(*model.NoSuchDataError); !ok {
        return nil, errors.WithStack(err)
    }

    param.CreatedAt = time.Now().UTC()
    param.UpdatedAt = time.Now().UTC()

    lang, err = u.Repo.Create(ctx, param)
    if err != nil {
        return nil, errors.WithStack(err)
    }

    return lang, nil
}

// Update は、ProgrammingLangを更新する。
func (u *ProgrammingLangUseCase) Update(ctx context.Context, id int, param *model.ProgrammingLang) (*model.ProgrammingLang, error) {
    lang, err := u.Repo.Read(ctx, id)
    if lang == nil {
        return nil, &model.NoSuchDataError{
            ID:        id,
            Name:      param.Name,
            ModelName: model.ModelNameProgrammingLang,
        }
    } else if err != nil {
        return nil, errors.WithStack(err)
    }

    lang.ID = id
    lang.Name = param.Name
    lang.Feature = param.Feature
    lang.UpdatedAt = time.Now().UTC()

    return u.Repo.Update(ctx, lang)
}

// Delete は、ProgrammingLangを削除する。
func (u *ProgrammingLangUseCase) Delete(ctx context.Context, id int) error {
    lang, err := u.Repo.Read(ctx, id)
    if lang == nil {
        return  &model.NoSuchDataError{
            ID:        id,
            ModelName: model.ModelNameProgrammingLang,
        }
    } else if err != nil {
        return  errors.WithStack(err)
    }

    return u.Repo.Delete(ctx, id)
}

ProgrammingLangRepository(インターフェース部分)

ここでは、実際のデータベースの操作のインターフェースを定義している。 個々のデータベースの操作(例えば、MySQLPostgreSQL、あるいはそれを模したモックなど)という具体的なことに対して、ここで定義しているのは、データベースの操作をまとめた抽象的なものであることに注目して欲しい。 これは、具体的なものが共通でもつ変わりにくい抽象的な部分をインターフェースで表したものだ。

program_lang_repository.go

package repository

import (
    "context"

    "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/model"
)

// ProgrammingLangRepository は、ProgrammingLangのRepository。
type ProgrammingLangRepository interface {
    List(ctx context.Context, limit int) ([]*model.ProgrammingLang, error)
    Create(ctx context.Context, lang *model.ProgrammingLang) (*model.ProgrammingLang, error)
    Read(ctx context.Context, id int) (*model.ProgrammingLang, error)
    ReadByName(ctx context.Context, name string) (*model.ProgrammingLang, error)
    Update(ctx context.Context, lang *model.ProgrammingLang) (*model.ProgrammingLang, error)
    Delete(ctx context.Context, id int) error
}

ProgrammingLangDAO(データベース操作実装部分)

具体的なSQL型のデータベースの操作を行う構造体(Javaとかでいうところのクラスみたいなもの)。 ProgrammingLangRepositoryで定義した各メソッドを実装している。 ProgrammingLangDAOは、ProgrammingLangRepositoryを満たしているので、ProgrammingLangRepositoryとして使用することができる。 NewProgrammingLangDAOで、ProgrammingLangDAOを生成しているが、返り値の型としては ProgrammingLangDAOそのものではなく、 ProgrammingLangRepository型で返していることがわかる。 こうすることで、 ProgrammingLangDAOを使用する側は、直接ProgrammingLangDAOのことを知らなくても利用可能になる。

program_lang_dao.go

package rdb

import (
    "context"
    "fmt"

    "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/model"
    "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/repository"
    "github.com/pkg/errors"
)

// ProgrammingLangDAO は、ProgrammingLangのDAO。
type ProgrammingLangDAO struct {
    SQLManager SQLManagerInterface
}

// NewProgrammingLangDAO は、ProgrammingLangDAO生成して返す。
func NewProgrammingLangDAO(manager SQLManagerInterface) repository.ProgrammingLangRepository {
    fmt.Printf("NewProgrammingLangDAO")

    return &ProgrammingLangDAO{
        SQLManager: manager,
    }
}

// ErrorMsg は、エラー文を生成し、返す。
func (dao *ProgrammingLangDAO) ErrorMsg(method string, err error) error {
    return &model.DBError{
        ModelName: model.ModelNameProgrammingLang,
        DBMethod:  method,
        Detail:    err.Error(),
    }
}

// Create は、レコードを1件生成する。
func (dao *ProgrammingLangDAO) Create(ctx context.Context, lang *model.ProgrammingLang) (*model.ProgrammingLang, error) {
    query := "INSERT INTO programming_langs (name, feature, created_at, updated_at) VALUES (?, ?, ?, ?)"
    stmt, err := dao.SQLManager.PrepareContext(ctx, query)
    if err != nil {
        return nil, dao.ErrorMsg(model.DBMethodCreate, err)
    }
    defer stmt.Close()

    result, err := stmt.ExecContext(ctx, lang.Name, lang.Feature, lang.CreatedAt, lang.UpdatedAt)
    if err != nil {
        return nil, dao.ErrorMsg(model.DBMethodCreate, err)
    }

    affect, err := result.RowsAffected()
    if affect != 1 {
        err = fmt.Errorf("%s: %d ", TotalAffected, affect)
        return nil, dao.ErrorMsg(model.DBMethodUpdate, err)
    }

    id, err := result.LastInsertId()
    if err != nil {
        return nil, dao.ErrorMsg(model.DBMethodCreate, err)
    }

    lang.ID = int(id)

    return lang, nil
}

// List は、レコードの一覧を取得して返す。
func (dao *ProgrammingLangDAO) List(ctx context.Context, limit int) ([]*model.ProgrammingLang, error) {
    query := "SELECT id, name, feature, created_at, updated_at FROM programming_langs ORDER BY name LIMIT ?"
    langSlice, err :=   dao.list(ctx, query, limit)

    if len(langSlice) == 0 {
        return nil, &model.NoSuchDataError{
            ModelName: model.ModelNameProgrammingLang,
        }
    }

    if err != nil {
        return nil, errors.WithStack(err)
    }

    return langSlice, nil
}

// Read は、レコードを1件取得して返す。
func (dao *ProgrammingLangDAO) Read(ctx context.Context, id int) (*model.ProgrammingLang, error) {
    query := "SELECT id, name, feature, created_at, updated_at FROM programming_langs WHERE ID=?"

    langSlice, err :=  dao.list(ctx, query, id)

    if len(langSlice) == 0 {
        return nil, &model.NoSuchDataError{
            ID:     id,
            ModelName: model.ModelNameProgrammingLang,
        }
    }

    if err != nil {
        return nil, errors.WithStack(err)
    }

    return langSlice[0], nil
}

// ReadByName は、指定したNameを保持するレコードを1返す。
func (dao *ProgrammingLangDAO) ReadByName(ctx context.Context, name string) (*model.ProgrammingLang, error) {
    query := "SELECT id, name, feature, created_at, updated_at FROM programming_langs WHERE name=? ORDER BY name LIMIT ?"
    langSlice, err := dao.list(ctx, query, name, 1)

    if len(langSlice) == 0 {
        return nil, &model.NoSuchDataError{
            Name:      name,
            ModelName: model.ModelNameProgrammingLang,
        }
    }

    if err != nil {
        return nil, errors.WithStack(err)
    }

    return langSlice[0], nil
}

// list は、レコードの一覧を取得して返す。
func (dao *ProgrammingLangDAO) list(ctx context.Context, query string, args ...interface{}) ([]*model.ProgrammingLang, error) {
    stmt, err := dao.SQLManager.PrepareContext(ctx, query)
    if err != nil {
        return nil, dao.ErrorMsg(model.DBMethodList, err)
    }
    defer stmt.Close()

    rows, err := stmt.QueryContext(ctx, args...)
    if err != nil {
        return nil, dao.ErrorMsg(model.DBMethodList, err)
    }
    defer rows.Close()

    langSlice := make([]*model.ProgrammingLang, 0)
    for rows.Next() {
        lang := &model.ProgrammingLang{}

        err = rows.Scan(
            &lang.ID,
            &lang.Name,
            &lang.Feature,
            &lang.CreatedAt,
            &lang.UpdatedAt,
        )

        if err != nil {
            return nil, dao.ErrorMsg(model.DBMethodList, err)
        }
        langSlice = append(langSlice, lang)
    }

    return langSlice, nil
}

// Update は、レコードを1件更新する。
func (dao *ProgrammingLangDAO) Update(ctx context.Context, lang *model.ProgrammingLang) (*model.ProgrammingLang, error) {
    query := "UPDATE programming_langs SET name=?, feature=?, created_at=?, updated_at=? WHERE id=?"

    stmt, err := dao.SQLManager.PrepareContext(ctx, query)
    defer stmt.Close()

    if err != nil {
        return nil, dao.ErrorMsg(model.DBMethodUpdate, err)
    }

    result, err := stmt.ExecContext(ctx, lang.Name, lang.Feature, lang.CreatedAt, lang.UpdatedAt, lang.ID)
    if err != nil {
        return nil, dao.ErrorMsg(model.DBMethodUpdate, err)
    }

    affect, err := result.RowsAffected()
    if affect != 1 {
        err = fmt.Errorf("%s: %d ", TotalAffected, affect)
        return nil, dao.ErrorMsg(model.DBMethodUpdate, err)
    }

    return lang, nil
}

// Delete は、レコードを1件削除する。
func (dao *ProgrammingLangDAO) Delete(ctx context.Context, id int) error {
    query := "DELETE FROM programming_langs WHERE id=?"

    stmt, err := dao.SQLManager.PrepareContext(ctx, query)
    if err != nil {
        return dao.ErrorMsg(model.DBMethodDelete, err)
    }
    defer stmt.Close()

    result, err := stmt.ExecContext(ctx, id)
    if err != nil {
        return dao.ErrorMsg(model.DBMethodDelete, err)
    }

    affect, err := result.RowsAffected()
    if err != nil {
        return dao.ErrorMsg(model.DBMethodDelete, err)
    }
    if affect != 1 {
        err = fmt.Errorf("%s: %d ", TotalAffected, affect)
        return dao.ErrorMsg(model.DBMethodDelete, err)
    }

    return nil
}

MockProgrammingLangRepository(モック)

データベースの操作を模したモック。 gomockというモック生成用のツールで自動生成している。 モックの構造体もProgrammingLangRepositoryを満たしているので、ProgrammingLangRepositoryとして使用することができる。実際にProgrammingLangRepository(の実装)を使用するレイヤーのテストをする際には、ProgrammingLangRepositoryの実装としてProgrammingLangDAOではなく、このモックを使用する。

program_lang_repository_mock.go

// Code generated by MockGen. DO NOT EDIT.
// Source: domain/repository/programming_lang_repository.go

// Package mock_repository is a generated GoMock package.
package mock_repository

import (
    context "context"
    model "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/model"
    gomock "github.com/golang/mock/gomock"
    reflect "reflect"
)

// MockProgrammingLangRepository is a mock of ProgrammingLangRepository interface
type MockProgrammingLangRepository struct {
    ctrl     *gomock.Controller
    recorder *MockProgrammingLangRepositoryMockRecorder
}

// MockProgrammingLangRepositoryMockRecorder is the mock recorder for MockProgrammingLangRepository
type MockProgrammingLangRepositoryMockRecorder struct {
    mock *MockProgrammingLangRepository
}

// NewMockProgrammingLangRepository creates a new mock instance
func NewMockProgrammingLangRepository(ctrl *gomock.Controller) *MockProgrammingLangRepository {
    mock := &MockProgrammingLangRepository{ctrl: ctrl}
    mock.recorder = &MockProgrammingLangRepositoryMockRecorder{mock}
    return mock
}

// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockProgrammingLangRepository) EXPECT() *MockProgrammingLangRepositoryMockRecorder {
    return m.recorder
}

// List mocks base method
func (m *MockProgrammingLangRepository) List(ctx context.Context, limit int) ([]*model.ProgrammingLang, error) {
    ret := m.ctrl.Call(m, "List", ctx, limit)
    ret0, _ := ret[0].([]*model.ProgrammingLang)
    ret1, _ := ret[1].(error)
    return ret0, ret1
}

// List indicates an expected call of List
func (mr *MockProgrammingLangRepositoryMockRecorder) List(ctx, limit interface{}) *gomock.Call {
    return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockProgrammingLangRepository)(nil).List), ctx, limit)
}

// Create mocks base method
func (m *MockProgrammingLangRepository) Create(ctx context.Context, lang *model.ProgrammingLang) (*model.ProgrammingLang, error) {
    ret := m.ctrl.Call(m, "Create", ctx, lang)
    ret0, _ := ret[0].(*model.ProgrammingLang)
    ret1, _ := ret[1].(error)
    return ret0, ret1
}

// Create indicates an expected call of Create
func (mr *MockProgrammingLangRepositoryMockRecorder) Create(ctx, lang interface{}) *gomock.Call {
    return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockProgrammingLangRepository)(nil).Create), ctx, lang)
}

// Read mocks base method
func (m *MockProgrammingLangRepository) Read(ctx context.Context, id int) (*model.ProgrammingLang, error) {
    ret := m.ctrl.Call(m, "Read", ctx, id)
    ret0, _ := ret[0].(*model.ProgrammingLang)
    ret1, _ := ret[1].(error)
    return ret0, ret1
}

// Read indicates an expected call of Read
func (mr *MockProgrammingLangRepositoryMockRecorder) Read(ctx, id interface{}) *gomock.Call {
    return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockProgrammingLangRepository)(nil).Read), ctx, id)
}

// ReadByName mocks base method
func (m *MockProgrammingLangRepository) ReadByName(ctx context.Context, name string) (*model.ProgrammingLang, error) {
    ret := m.ctrl.Call(m, "ReadByName", ctx, name)
    ret0, _ := ret[0].(*model.ProgrammingLang)
    ret1, _ := ret[1].(error)
    return ret0, ret1
}

// ReadByName indicates an expected call of ReadByName
func (mr *MockProgrammingLangRepositoryMockRecorder) ReadByName(ctx, name interface{}) *gomock.Call {
    return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadByName", reflect.TypeOf((*MockProgrammingLangRepository)(nil).ReadByName), ctx, name)
}

// Update mocks base method
func (m *MockProgrammingLangRepository) Update(ctx context.Context, lang *model.ProgrammingLang) (*model.ProgrammingLang, error) {
    ret := m.ctrl.Call(m, "Update", ctx, lang)
    ret0, _ := ret[0].(*model.ProgrammingLang)
    ret1, _ := ret[1].(error)
    return ret0, ret1
}

// Update indicates an expected call of Update
func (mr *MockProgrammingLangRepositoryMockRecorder) Update(ctx, lang interface{}) *gomock.Call {
    return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockProgrammingLangRepository)(nil).Update), ctx, lang)
}

// Delete mocks base method
func (m *MockProgrammingLangRepository) Delete(ctx context.Context, id int) error {
    ret := m.ctrl.Call(m, "Delete", ctx, id)
    ret0, _ := ret[0].(error)
    return ret0
}

// Delete indicates an expected call of Delete
func (mr *MockProgrammingLangRepositoryMockRecorder) Delete(ctx, id interface{}) *gomock.Call {
    return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockProgrammingLangRepository)(nil).Delete), ctx, id)
}

単体テスト

このセクションは、以下の2つの記事を大変参考にさせていただいた。

mercari.go #1 で「もう一度テストパターンを整理しよう」というタイトルで登壇しました - アルパカ三銃士

Goにおけるテスト可能な設計

そもそも単体テストとは何かということを振り返る

単体テストについての説明は色々とあると思うが機能テストと比較して書かれた以下の説明がわかりやすい。

Unit test(単体テスト)
 ・単一の関数やメソッドなどをテスト
Functional test(機能テスト)
 ・リクエストからレスポンスまでのテスト

引用元 : mercari.go #1 で「もう一度テストパターンを整理しよう」というタイトルで登壇しました - アルパカ三銃士

テストダブル

よく、テストダブルという言葉を聞いたことはないだろうか。 テストダブルとは、

ソフトウェアテストにおいて、テスト対象が依存しているコンポーネントを置き換える代用品のこと。ダブルは代役、影武者を意味する。 テストを実行するには、テスト対象のシステム (SUT; System Under Test) に加えて、テスト対象が依存するコンポーネント (DOC; Depend-On Component) が必要になる。しかし、依存コンポーネントは、常に利用できるとは限らない。

こういった問題を回避するには、依存コンポーネントを、テスト用のコンポーネントと入れ替えるテクニックが利用できる。この代用のコンポーネントを、テストダブルと呼ぶ。

引用元 : テストダブル - Wikipedia

要するに、あるコンポーネントをテストする際に、そのテスト対象のコンポーネントが依存しているコンポーネントが利用できなかったりするので、それをテスト用に作ったものに置きかえようねという話。

これの総称がテストダブルで、その具体的な方法にはモックやスタブなどがある。 各々の違いについては、TDD > モック / スタブ - Qiita等で確認いただきたい。

今回は、その中でもモックを使用する。

単体テストでインターフェースをうまく利用する

先ほど、引用で単体テストは「単一の関数やメソッドなどをテスト」するということがわかった。 A=>B=>Cという依存関係がコードに存在するとする(A、B、Cは各レイヤのコード)。AはBに依存し、BはCに依存するとする。この場合、Aのテストを行おうとすると、BやCまで呼び出す必要が出て来てしまう。 先ほどの単体テストの定義だと、Aの単体テストはAのみをテストするものなはずなのに、A以外のBやCも利用することになってしまう。 これは真の意味で単体テストと言えるのだろうか...

依存関係のある中で、単体テストをうまく行うのにインターフェイスポリモーフィズムを使用するとAの単体テストを行うのに、実際のBやCを利用しなくてもよくなる。

実際の例は、先ほどのDIP(依存関係逆転の法則)のセクションで示したものを参照いただきたい。原理としては、Aの単体テストをする際に、依存しているBやCをそのまま使うのではなく、Bをモックに入れ替えている。 これは、AからBを利用する際に、Bの具象クラスをそのまま利用するのではなくて、Bの具象クラスがその実装となるインターフェイスを定義して、それをAは利用しているからなせる技だ。

具体的にいうとProgrammingLangRepositoryというインターフェースを定義し、製品コードではこのProgrammingLangRepositoryの実装であるProgrammingLangDAOを使用してDBの操作を行い、ProgrammingLangUseCaseのテストでは、ProgrammingLangRepository実装であるMockProgrammingLangRepositoryに差し替えているのだ。 モックもインターフェースを実装した具象クラスの1つであるというわけだ。

ProgrammingLangDAO is a ProgrammingLanRepository であり、
MockProgrammingLangRepository is a ProgrammingLanRepository でもあるという事だ。

具体的なコードは、以下に記述(コード全体) https://github.com/SekiguchiKai/clean-architecture-with-go

ちなみにこのインスタンスを差し替える方法は、 DI(依存性の注入)という名前がついている。 依存性の注入自体の説明は以下がわかりやすい。 依存性注入(DI)の解説とやり方 - Qiita

クリーンアーキテクチャ

変更に強く、テストがしやすいということで(もちろん他にも利点はたくさんある)最近何かと話題に上がることの多いクリーンアーキテクチャ。 これの何が優れているのかということをこれまでの説明に沿って考えてみたい。 ただし、既にクリーンアーキテクチャの優れた部分は他の記事等でも紹介されているので、ここでは、これまでの記事の内容に沿ったものだけに焦点を当てたい。

このセクションでは以下の記事を非常に参考にさせていただいた。
Clean ArchitectureでAPI Serverを構築してみる - Qiita

Goでクリーンアーキテクチャを試す | POSTD

Goのサーバサイド実装におけるレイヤ設計とレイヤ内実装について考える

クリーンアーキテクチャ(The Clean Architecture翻訳) | blog.tai2.net

変更に強くなる編に合致する点

依存の方向性

このアーキテクチャを機能させる重要なルールが、依存ルールだ。
このルールにおいては、ソースコードは、内側に向かってのみ依存することができる

引用元 : クリーンアーキテクチャ(The Clean Architecture翻訳) | blog.tai2.net

というようにクリーンアーキテクチャは、依存関係を片方向にのみにするとしている

詳しくは、クリーンアーキテクチャ(The Clean Architecture翻訳) | blog.tai2.netを参照。

レイヤーとDIP

レイヤーの境界をまたがる時には、DIPを利用する。 つまり、あるレイヤーから別のレイヤーを使用する時に、直接使用するレイヤーの具象に依存させるのではなく、その抽象に依存させるようにしている。 詳しくはクリーンアーキテクチャ(The Clean Architecture翻訳) | blog.tai2.netを参照いただきたいが、レイヤーを分けて、レイヤ間の境界をまたがるときには、疎結合になるようにDIPを用いることが多い。これを行うことで、あるレイヤのコードが変更になったときに、別のレイヤーに影響を及ぼしにくい。

単体テスト編に合致する点

モックにできる

レイヤを分けて、レイヤ間の境界をまたがるときには、疎結合になるようにDIPを用いるので、依存している他のレイヤはモックに差し替えることができるため、単体テストがしやすい。

実際のコード

実際にコードを書いてみた。
https://github.com/SekiguchiKai/clean-architecture-with-go

参考文献

エリック・エヴァンス(著)、 今関 剛 (監修)、 和智 右桂 (翻訳) (2011/4/9)『エリック・エヴァンスのドメイン駆動設計 (IT Architects’Archive ソフトウェア開発の実践)』 翔泳社

Robert C.Martin (著)、 角 征典 (翻訳)、 高木 正弘 (翻訳) (2018/7/27)『Clean Architecture 達人に学ぶソフトウェアの構造と設計』 KADOKAWA

アラン・シャロウェイ (著)、 ジェームズ・R・トロット (著)、 村上 雅章 (翻訳) (2014/3/11)『オブジェクト指向のこころ (SOFTWARE PATTERNS SERIES)』 丸善出版

結城 浩 (2004/6/19)『増補改訂版Java言語で学ぶデザインパターン入門』 ソフトバンククリエイティブ

InfoQ.com、徳武 聡(翻訳) (2009年6月7日) 『Domain Driven Design(ドメイン駆動設計) Quickly 日本語版』 InfoQ.com Domain Driven Design(ドメイン駆動設計) Quickly 日本語版

中山 清喬、国本 大悟 (2014/8/7)『スッキリわかるJava入門 第2版 スッキリわかるシリーズ』 インプレス

参考にさせていただいたサイト

変更に強くなる編

実践DDD本の第4章「アーキテクチャ」 ~レイヤからヘキサゴナルへ~ (2/4):CodeZine(コードジン)

オブジェクト指向設計原則とは - Qiita

依存性逆転の原則 - Wikipedia

依存関係逆転の原則について · SunriseDigital/work-shop Wiki

オブジェクト指向と10年戦ってわかったこと - Qiita

15分でわかる かんたんオブジェクト指向 - Qiita

モジュール結合度とは - IT用語辞典 Weblio辞書

単体テスト

mercari.go #1 で「もう一度テストパターンを整理しよう」というタイトルで登壇しました - アルパカ三銃士

Goにおけるテスト可能な設計

テストダブル - Wikipedia

TDD > モック / スタブ - Qiita

依存性注入(DI)の解説とやり方 - Qiita

クリーンアーキテクチャ

The Clean Architecture | 8th Light

クリーンアーキテクチャ(The Clean Architecture翻訳) | blog.tai2.net

Clean Architecture │ nrslib

Goのサーバサイド実装におけるレイヤ設計とレイヤ内実装について考える

Clean ArchitectureでAPI Serverを構築してみる - Qiita

Goでクリーンアーキテクチャを試す | POSTD

持続可能な開発を目指す ~ ドメイン・ユースケース駆動(クリーンアーキテクチャ) + 単方向に制限した処理 + FRP - Qiita

【技術記事】Goで学ぶポインタとアドレス

※ 本記事は、過去に Qiitaでも投稿した記事である。

Goってシンプルで書きやすいですよね。 しかし、シンプルなGoでもいくつか躓きやすいポイントがあると思っています。 その最初のポイントがポインタではないでしょうか。特に、ポインタの概念が存在しない言語から始めた人にとっては、なかなかとっつきにくいものだと思います。そこで今回は、なんとなく使っていたポインタを、ちゃんと理解するためのエントリを書きました。ポインタをちゃんと理解しようとすると、その前提として知らなければならないことが多々あり、そこから説明するので、やや遠回りをした説明になっています。 「これちげえじゃねえか」とか、「ここわかりにくいぜ」っていうのがあったら、ご教授ください。

※ 技術的な話は「です、ます」調よりも「である、だ」調の方が書きやすいので、以降は「である、だ」調で書きます。

前提知識Part

先ほど述べたとおり、ポインタを理解しようとすると、前提知識が必要になってくる。 まずは、その前提知識を説明したいと思う。

プログラムのコンパイルから実行までの流れ

何かしらの高級言語(GoとかJavaとか)で書かれたソースコードはそのままではそのプログラムをPCで実行することはできない。 ではどうするかというと、高級言語で書かれたソースコードコンパイラコンパイルし、コンピュータがプログラムを実行できるような形にする。 この「実行できるような形」は、バイナリーコードになった実行ファイルである。

変数とメモリとアドレス

ポインタを理解するには、まず変数とメモリとアドレスの関係を理解する必要がある。 ここで整理したいと思う。

  • メモリは、1バイト毎に番号がつけられ、区別されている
  • 変数は実行ファイルになると、番号が割り当てられる
  • 変数は、メモリ上の該当の番号の区分に格納され、記憶される
  • この変数に付与されるメモリの区分番号をアドレスという

図にするとこんな感じ メモリと変数.png

ここでいうメモリ1番地とかがアドレスで、実際にはあとで説明するが、0x1040a0d0 みたいな感じの16進数で表される。

参考 : 変数とメモリの関係 - 苦しんで覚えるC言語

例えば、以下の様にする。

name := "太郎"

そうすると、コンパイルした時に、メモリ上のある場所に変数の値が格納される。 この メモリ上のある場所 が上記で説明した アドレス というものである。 メモリ上に変数が格納される場所がアドレスである。

実際に格納されたアドレスを16進数で表示させることもできる。 詳しくはここを参照。

package main

import "fmt"

// Person は人間を表す構造体。
type Person struct {
    Name string
    Age  int
}

func main() {
    // ポインタ型の変数を宣言する
    // pがポインタ変数
    // *Personポインタ型
    var p *Person

    p = &Person{
        Name: "太郎",
        Age:  20,
    }
    fmt.Printf("変数pに格納されているアドレス :%p", p)
}

実行結果

変数pに格納されているアドレス :0x1040a0d0

参考 : メモリの仕組み - 苦しんで覚えるC言語

ポインタPart

ポインタ型とポインタ変数

ポインタという概念を学ぶ時に、よく以下のような説明を目にする。

  • ポインタってのはメモリのアドレス情報のことだよ
  • ポインタってのはアドレス情報を格納するための変数のことだよ

これらの説明はわかりやすいのだが、実際にコードを見た時には「結局どれがポインタなの?」ってなりがちだ。 その疑問ついて以下の記事が非常にわかりやすかったので、一読されるといいと思う。 C言語のポインタきらい - Qiita

上記の記事によれば、以下のコードの pが ポインタ変数 で、 *Person がポインタ型になる。

コード例

package main

import "fmt"

// Person は人間を表す構造体。
type Person struct {
    Name string
    Age  int
}

func main() {
    // ポインタ型の変数を宣言する
    // pがポインタ型変数
    // *Personポインタ型
    var p *Person

    p = &Person{
        Name: "太郎",
        Age:  20,
    }
    fmt.Printf("p :%+v\n", p)
    fmt.Printf("変数pに格納されているアドレス :%p", p)
}

実行結果

p :&{Name:太郎 Age:20}
変数pに格納されているアドレス :0x1040a0d0

pを表示すると、 &{Name:太郎 Age:20} となることを覚えておいて欲しい。 & については後ほど説明する。

ポインタ変数とは

メモリ上のアドレスを値として入れられる変数のこと

引用元 : ポインタ変数とは|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典

上記のコードでは、変数pがポインタ変数となり、実際にpにはアドレスが格納されている。(詳細は後述)

デリファレンス

& を使うことで、ポインタ型を生成することができる。 Person型の変数pを &p とすると、Personへのポインタである *Person型 の値を生み出すことができる。 &p は、pのアドレスという。

package main

import "fmt"

// Person は人間を表す構造体。
type Person struct {
    Name string
    Age  int
}

func main() {

    // 値として、pに代入
    p := Person{
        Name: "太郎",
        Age:  20,
    }

    fmt.Printf("最初のp :%+v\n", p)

    p2 := p
    p2.Name = "二郎"
    p2.Age = 21
    // pではなく
    fmt.Printf("p2で二郎に書き換えを行なったはずのp :%+v\n", p)

    // &pで*Person(Personのポインタ型)を生成する
    // p3はpのアドレスが格納されている状態になる
    p3 := &p
    p3.Name = "二郎"
    p3.Age = 21

    fmt.Printf("p3で二郎に書き換えを行なったp :%+v\n", p)
}

実行結果

最初のp :{Name:太郎 Age:20}
p2で二郎に書き換えを行なったはずのp :{Name:太郎 Age:20}
p3で二郎に書き換えを行なったp :{Name:二郎 Age:21}

pはポインタではなく、Person型の値である。 p2 := p は、Person型の値コピーしてp2に格納しているので、p2で書き換えを行っても、それがpに反映されることはない。これを値渡しという。

逆に、p3 := &p は、Person型(Personへのポインタである Person型)をp3に格納しているので、p3はpのアドレス(Personへのポインタである *Person型)を持っていることになる。 従って、p3で書き換えを行うと、その変更はpに反映される。これを参照渡しという。

Goでは、構造体内のメソッド内で、構造体のフィールドの情報を変更するときには、この参照渡しをよく利用する。こことかが参考になる。

*Hoge型が格納された変数

& を使うことで、ポインタ型を生成することができた。 では、& を使って生成されたポインタ型を格納した変数はどう扱うか。 まずは、& の復習もかねて、以下のコードを見てみよう。

package main

import "fmt"

func main() {
    name := "太郎"
    fmt.Printf("name :%v\n", name)

    namePoint := &name

    // namePointは、&nameが格納されているだけなので、stringへのポインタである *string型の値が格納されている。
    fmt.Printf("namePoint :%v\n", namePoint)

    // namePointが指している変数は、"*namePoint"という感じで、"*"をつけて表す。
    fmt.Printf("namePoint :%v\n", *namePoint)
}

実行結果

name :太郎
namePoint :0x1040c128
namePoint :太郎

コードに示したように namePoint には &name が格納されている。 & は、ポインタ型を生成するので&name は、stringへのポインタである *string型 の値(アドレス)が格納されている。 よって、 namePoint を表示すると *string型 の値である name のアドレスが格納されていることがわかる。

では、namePoint の元となっている name の変数に格納されている値(ここでは「太郎」)は、どのように取得すれば良いか。 そのような場合は、 *namePoint のように変数名の前に * をつければ良い。 なお、ここが紛らわしいところなのだが、 *namePoint 自体も変数なので、これに代入することもできる。 例えば、以下のようなコードだ。

package main

import "fmt"

func main() {
    name := "太郎"
    fmt.Printf("name :%v\n", name)

    namePoint := &name

    // namePointは、&nameが格納されているだけなので、stringへのポインタである*string型の値が格納されている。
    fmt.Printf("namePoint :%v\n", namePoint)

    // namePointが指している変数は、"*namePoint"という感じで、"*"をつけて表す。
    fmt.Printf("namePoint :%v\n", *namePoint)

    *namePoint = "二郎"

    // *namePointに値を代入することもできる。
    fmt.Printf("*namePointに二郎を代入後の*namePoint :%v\n", *namePoint)

    // 再代入したところで、namePointに格納されている*string型の値(アドレス)自体は、変わらない
    fmt.Printf("*namePointに二郎を代入後のnamePoint :%v\n", namePoint)

    // stringへのポインタである*string型の値(nameに格納されている値)を書き換えたので、nameの値も変更される。
    fmt.Printf("*namePointに二郎を代入後のname :%v\n", name)
}

実行結果

name :太郎
namePoint :0x1040c128
namePoint :太郎
*namePointに二郎を代入後の*namePoint :二郎
*namePointに二郎を代入後のnamePoint :0x1040c128
*namePointに二郎を代入後のname :二郎

ここで注意すべきことは、 *namePoint に値を代入すると、nameの値も書き変わるということだ。 これはなぜか? *namePoint には、 &name (stringへのポインタである*string型の値が格納されているからであり、それを *namePoint = "二郎" で書き換えているので、当然 name の値も書き変わるということである。

まとめ

ポインタは確かにとっつきにくいかもしれないですが、Goを使用する上では必須ですし、使い方によっては非常に便利なものなので、ちゃんと理解して使っていきましょう。

参考

参考文献

松尾 愛賀 (2016/4/15)『スターティングGo言語』 翔泳社

Alan A.A. Donovan (著), Brian W. Kernighan (著), 柴田 芳樹 (翻訳)(2016/6/20)『プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)』丸善出版

参考にさせていただいたサイト

バイナリーコード(ばいなりーこーど)とは - コトバンク

変数とメモリの関係 - 苦しんで覚えるC言語

メモリの仕組み - 苦しんで覚えるC言語

C言語のポインタきらい - Qiita

ポインタ変数とは|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典

Part4 誰もがつまずくポインタを完璧理解 | 日経 xTECH(クロステック)

C言語ポインタの基礎 - Qiita

Goのポインタ - はじめてのGo言語

【C言語入門】ポインタのわかりやすい使い方(配列、関数、構造体) | 侍エンジニア塾ブログ | プログラミング入門者向け学習情報サイト

もう一度基礎からC言語 第38回 プログラミングの周辺事項(1)~Cで書いたプログラムの仕組みと構造 Cプログラムの構造

Go言語の構造体の値渡しとポインタ渡しの動作を確認してみる - Qiita

はてなブログを再開する

はてなブログオンリーに投稿→はてなブログとQiita両方に投稿→Qiitaオンリーに投稿 と言う感じで技術記事を投稿してきたが、またはてなブログにも投稿しようかなと言う話。 技術以外のことも書こうかなと思う。

Qiitaにしか書いてなかったいくつかの記事については、こっちにも投稿しようかと思う。

【技術記事】Apache Beam with Google Cloud Dataflow(over 2.0.x系)入門~Combine~

Apache Beam with Google Cloud Dataflow(over 2.0.x系)入門~Combine~

Apache Beamの5つのCore Transformの内の1つ、Combineの基本的な使い方について記す。
他のCore TransformやそもそものApache Beam 2.0.xの基本的な話は以下に記述している。

IntelliJとGradleで始めるApache Beam 2.0.x with Google Cloud Dataflow - Qiita

Apache Beam with Cloud Dataflow(over 2.0.0系)入門~基本部分~ParDoまで~ - Qiita

Apache Beam with Google Cloud Dataflow(over 2.0.x系)入門~基本的なGroupByKey編~ - Qiita

なお、本記事は以下の2つの公式ドキュメントを参考に記述している。

Beam Programming Guide

コレクションと値の結合 | Cloud Dataflow のドキュメント | Google Cloud Platform

Combineの2つの役割

Combineは、PCollection内に存在する各要素(各データ)を結合したり、マージする。
Map/Shuffle/ReduceでいうところのReduceのようなものだと認識している。

Combineの仕方は大きく分けて2つ存在する。
「1つのPCollection内に存在する要素を結合して、1つの値を生成する方法」と「KeyによってGroup化されたPCollectionのValue部分の各要素を結合して、1つの値を生成する方法」である。
以下、各々の方法を記したい。

1つのPCollection内に存在する要素を結合して、1つの値を生成する方法

1つのPCollection内に存在する要素を結合して、1つの値を生成する方法とは

PCollection内の各要素を結合する。
=>これはParDoとの違いに注意する必要がある。
ParDoは、PCollection内の各々の要素に対して何らかの処理を行う。
Combineは、PCollection内の各要素を結合する。

例えば、PCollection内に存在する要素を結合して、1つの値を生成する場合がこれ。

PCollection<Integer> sum = pCollection.apply(Sum.integersGlobally());

一見、Combineが存在しないように見えるが、Sum.integersGlobally()が、 Combine.globallyをwrapしている。実際のSum.integersGlobally()は以下。

public static Combine.Globally<Integer, Integer> integersGlobally() {
  return Combine.globally(Sum.ofIntegers());}

参考 API リファレンス

withoutDefaults()

空のPCollectionがinoutとして与えられた場合に、emptyを返したいなら withoutDefaults()をつける。

PCollection<Integer> sum = integerPCollection.apply(Sum.integersGlobally().withoutDefaults());

Global Windowの場合と非Global Windowの場合の動作の違い

Global Windowの場合には、1 つの項目を含んだ PCollection を返すことがデフォルトの動作になっている。

一方、非Global Windowの場合、上記のようなデフォルトの動作はしない。
Combineを使用する際に、Optionを指定する。
公式がわかりやすかったので、以下引用。(本投稿執筆時には、Apache Beam 2.0.xの方のDocumentにはまだこの記載が存在しなかったため、Google Cloud Dataflow1.9の方の公式ドキュメントから引用させていただいている)

.withoutDefaults を指定する。この場合、入力 PCollection 内の空のウィンドウは、出力>コレクションでも空になります。

.asSingletonView を指定する。この場合、出力は直ちに PCollectionView へと変換されます。これは、それぞれの空ウィンドウが副入力として使用される場合のデフォルト値になります。通常、このオプションは、パイプラインの Combine の結果が後にパイプライン内で副入力として使用される場合にのみ、使用する必要があります。

引用元 : コレクションと値の結合 | Cloud Dataflow のドキュメント | Google Cloud Platform

実際にコードを書いてみた

各処理は、コードにコメントとして記載している。
理解を優先するため、メソッドチェーンを極力使用していない。
そのため、冗長なコードになっている。

package com.company;

import org.apache.beam.sdk.Pipeline;
import org.apache.beam.sdk.io.TextIO;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.transforms.DoFn;
import org.apache.beam.sdk.transforms.ParDo;
import org.apache.beam.sdk.transforms.Sum;
import org.apache.beam.sdk.values.PCollection;

/**
 * メインクラス
 */
public class Main {
    /**
     * 関数型オブジェクト
     * String => Integerの型変換を行う
     */
    static class TransformTypeFromStringToInteger extends DoFn<String, Integer> {
        @ProcessElement
        public void processElement(ProcessContext c) {
            // 要素をString=>Integerに変換して、output
            c.output(Integer.parseInt(c.element()));
        }
    }

    /**
     * 関数型オブジェクト
     * Integer =>Stringの型変換を行う
     */
    static class TransformTypeFromIntegerToString extends DoFn<Integer, String> {
        @ProcessElement
        public void processElement(ProcessContext c) {
            // 要素をString=>Integerに変換して、output
            System.out.println(c.element());
            c.output(String.valueOf(c.element()));
        }
    }


    /**
     * インプットデータのパス
     */
    private static final String INPUT_FILE_PATH = "./sample.txt";
    /**
     * アウトデータのパス
     */
    private static final String OUTPUT_FILE_PATH = "./result.txt";

    /**
     * 理解のためにメソッドチェーンは極力使用しない
     * そのため冗長な箇所がある
     * メインメソッド
     *
     * @param args
     */
    public static void main(String[] args) {
        // optionを指定して、Pipelineを生成する
        Pipeline pipeline = Pipeline.create(PipelineOptionsFactory.create());

        System.out.println("a");
        // ファイルから読み込み
        PCollection<String> lines = pipeline.apply(TextIO.read().from(INPUT_FILE_PATH));
        // 読み込んだ各データをString => Integerに変換
        PCollection<Integer> integerPCollection = lines.apply(ParDo.of(new TransformTypeFromStringToInteger()));
        // Combine.GloballyでPCollectionの各要素を合計
        // 空のPCollectionの場合、emptyを返したいなら => PCollection<Integer> sum = integerPCollection.apply(Sum.integersGlobally().withoutDefaults());
        PCollection<Integer> sum = integerPCollection.apply(Sum.integersGlobally().withoutDefaults());
        // PCollection<Integer> sumをInteger => Stringに変換
        PCollection<String> sumString = sum.apply(ParDo.of(new TransformTypeFromIntegerToString()));
        // ファイルに書き込み
        sumString.apply(TextIO.write().to(OUTPUT_FILE_PATH));

        // 実行
        pipeline.run().waitUntilFinish();
    }
}

実施にコードを書いてみた(メソッドチェーンを使ったver)

だいぶすっきりした

package com.company;

import org.apache.beam.sdk.Pipeline;
import org.apache.beam.sdk.io.TextIO;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.transforms.DoFn;
import org.apache.beam.sdk.transforms.ParDo;
import org.apache.beam.sdk.transforms.Sum;


/**
 * メインクラス
 */
public class Main {
    /**
     * 関数型オブジェクト
     * String => Integerの型変換を行う
     */
    static class TransformTypeFromStringToInteger extends DoFn<String, Integer> {
        @ProcessElement
        public void processElement(ProcessContext c) {
            // 要素をString=>Integerに変換して、output
            c.output(Integer.parseInt(c.element()));
        }
    }

    /**
     * 関数型オブジェクト
     * Integer =>Stringの型変換を行う
     */
    static class TransformTypeFromIntegerToString extends DoFn<Integer, String> {
        @ProcessElement
        public void processElement(ProcessContext c) {
            // 要素をString=>Integerに変換して、output
            System.out.println(c.element());
            c.output(String.valueOf(c.element()));
        }
    }


    /**
     * インプットデータのパス
     */
    private static final String INPUT_FILE_PATH = "./sample.txt";
    /**
     * アウトデータのパス
     */
    private static final String OUTPUT_FILE_PATH = "./result.txt";

    /**
     * メインメソッド
     *
     * @param args
     */
    public static void main(String[] args) {
        // Pipeline生成
        Pipeline pipeline = Pipeline.create(PipelineOptionsFactory.create());

        // 処理部分
        pipeline.apply(TextIO.read().from(INPUT_FILE_PATH))
                .apply(ParDo.of(new TransformTypeFromStringToInteger()))
                .apply(Sum.integersGlobally().withoutDefaults())
                .apply(ParDo.of(new TransformTypeFromIntegerToString()))
                .apply(TextIO.write().to(OUTPUT_FILE_PATH));

        // 実行
        pipeline.run().waitUntilFinish();
    }
}

読み込んだファイル

1
2
3
4
5
6
7
8
9
10

実行結果

result.txt-00000-of-00001 が出力される
result.txt-00000-of-00001の中身は

55

やっていることは、

10
Σk
k = 1

みたいなもん。

PerKey

GroupByKeyを行うと K,V(IterableなCollection)になる。
例えば、以下のようになる。

Java [1, 2, 3]

CombineのPerKeyは、このK,V[IterableなCollection]のV[IterableなCollection]部分をKey毎に結合する。なので、例えば上記のGroupByKey後のK,V(IterableなCollection)をCombine PerKeyを行うと以下のようになる。

Java [6]

K,V(IterableなCollection)の,V(IterableなCollection)の要素がすべて結合された。

実際にコードを書いてみた

各処理は、コードにコメントとして記載している。
理解を優先するため、メソッドチェーンを極力使用していない。
そのため、冗長なコードになっている。

package com.company;

import org.apache.beam.sdk.Pipeline;
import org.apache.beam.sdk.io.TextIO;
import org.apache.beam.sdk.options.PipelineOptions;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.transforms.DoFn;
import org.apache.beam.sdk.transforms.ParDo;
import org.apache.beam.sdk.transforms.Sum;
import org.apache.beam.sdk.values.KV;
import org.apache.beam.sdk.values.PCollection;

/**
 * メイン
 */
public class Main {
    /**
     * 関数オブジェクト
     * 与えられたString str, String numを","で分割し、
     * numをInteger型に変更して、KV<String, Integer>型にする
     */
    static class SplitWordsAndMakeKVFn extends DoFn<String, KV<String, Integer>> {
        @ProcessElement
        // ProcessContextは、inputを表すobject
        // 自分で定義しなくてもBeam SDKが勝手に取ってきてくれる
        public void processElement(ProcessContext c) {
            // ","で分割
            String[] words = c.element().split(",");
            // 分割したword[0]をKに、words[1]をIntegerに変換してVにする
            c.output(KV.of(words[0], Integer.parseInt(words[1])));
        }
    }


    /**
     * 関数オブジェクト
     * KV<String, Iterable<Integer>型をString型に変更する
     */
    static class TransTypeFromKVAndMakeStringFn extends DoFn<KV<String, Integer>, String> {
        @ProcessElement
        public void processElement(ProcessContext c) {
            // inputをString型に変換する
            c.output(String.valueOf(c.element()));

        }

    }


    /**
     * インプットデータのパス
     */
    private static final String INPUT_FILE_PATH = "./sample.txt";
    /**
     * アウトデータのパス
     */
    private static final String COMBINE_OUTPUT_FILE_PATH = "./src/main/resources/combine_result/result.csv";

    /**
     * メイン
     * 理解のため、メソッドチェーンを極力使用していない
     * そのため、冗長なコードになっている
     *
     * @param args 引数
     */
    public static void main(String[] args) {
        // まずPipelineに設定するOptionを作成する
        // 今回は、ローカルで起動するため、DirectRunnerを指定する
        // ローカルモードでは、DirectRunnerがすでにデフォルトになっているため、ランナーを設定する必要はない
        PipelineOptions options = PipelineOptionsFactory.create();

        // Optionを元にPipelineを生成する
        Pipeline pipeline = Pipeline.create(options);

        // inout dataを読み込んで、そこからPCollection(パイプライン内の一連のデータ)を作成する
        PCollection<String> lines = pipeline.apply(TextIO.read().from(INPUT_FILE_PATH));

        // 与えられたString str, String numを","で分割し、numをInteger型に変更して、KV<String, Integer>型にする
        PCollection<KV<String, Integer>> kvCounter = lines.apply(ParDo.of(new SplitWordsAndMakeKVFn()));

        // Combine PerKey は、オペレーションの一部として GroupByKey 変換を実行する
        PCollection<KV<String, Integer>> sumPerKey = kvCounter
                .apply(Sum.integersPerKey());
        
        // PCollectionをファイル出力可能な形に変換する
        PCollection<String> output = sumPerKey.apply(ParDo.of(new TransTypeFromKVAndMakeStringFn()));

        // 書き込む
        output.apply(TextIO.write().to(COMBINE_OUTPUT_FILE_PATH));

        // run : PipeLine optionで指定したRunnerで実行
        // waitUntilFinish : PipeLineが終了するまで待って、最終的な状態を返す
        pipeline.run().waitUntilFinish();
    }


}

実施にコードを書いてみた(メソッドチェーンを使ったver)

だいぶすっきりした

package com.company;

import org.apache.beam.sdk.Pipeline;
import org.apache.beam.sdk.io.TextIO;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.transforms.DoFn;
import org.apache.beam.sdk.transforms.ParDo;
import org.apache.beam.sdk.transforms.Sum;
import org.apache.beam.sdk.values.KV;

/**
 * メイン
 */
public class Main {
    /**
     * 関数オブジェクト
     * 与えられたString str, String numを","で分割し、
     * numをInteger型に変更して、KV<String, Integer>型にする
     */
    static class SplitWordsAndMakeKVFn extends DoFn<String, KV<String, Integer>> {
        @ProcessElement
        // ProcessContextは、inputを表すobject
        // 自分で定義しなくてもBeam SDKが勝手に取ってきてくれる
        public void processElement(ProcessContext c) {
            // ","で分割
            String[] words = c.element().split(",");
            // 分割したword[0]をKに、words[1]をIntegerに変換してVにする
            c.output(KV.of(words[0], Integer.parseInt(words[1])));
        }
    }


    /**
     * 関数オブジェクト
     * KV<String, Iterable<Integer>型をString型に変更する
     */
    static class TransTypeFromKVAndMakeStringFn extends DoFn<KV<String, Integer>, String> {
        @ProcessElement
        public void processElement(ProcessContext c) {
            // inputをString型に変換する
            c.output(String.valueOf(c.element()));

        }

    }


    /**
     * インプットデータのパス
     */
    private static final String INPUT_FILE_PATH = "./sample.txt";
    /**
     * アウトデータのパス
     */
    private static final String COMBINE_OUTPUT_FILE_PATH = "./src/main/resources/combine_result/result.csv";

    /**
     * メイン
     * @param args 引数
     */
    public static void main(String[] args) {
        Pipeline pipeline = Pipeline.create(PipelineOptionsFactory.create());
        pipeline
                .apply(TextIO.read().from(INPUT_FILE_PATH))
                .apply(ParDo.of(new SplitWordsAndMakeKVFn()))
                .apply(Sum.integersPerKey())
                .apply(ParDo.of(new TransTypeFromKVAndMakeStringFn()))
                .apply(TextIO.write().to(COMBINE_OUTPUT_FILE_PATH));
        pipeline.run().waitUntilFinish();
    }


}

読み込んだファイル

Java,1
Python,5
Go,1
Java,3
Java,2
Go,5
Python,2
Go,2
Go,9
Python,6

実行結果

以下の3つのファイルが生成される。
result.csv-00000-of-00003
result.csv-00001-of-00003
result.csv-00002-of-00003

それぞれのファイルの中身は、以下。
分散並列処理で処理が行われているので、どの内容がどのファイルに出力されるかは毎回ランダムである。

result.csv-00000-of-00003

KV{Python, 13}

result.csv-00001-of-00003

KV{Java, 6}

result.csv-00002-of-00003

KV{Go, 17}

関連記事

IntelliJとGradleで始めるApache Beam 2.0.x with Google Cloud Dataflow - Qiita

Apache Beam with Cloud Dataflow(over 2.0.0系)入門~基本部分~ParDoまで~ - Qiita

Apache Beam with Google Cloud Dataflow(over 2.0.x系)入門~基本的なGroupByKey編~ - Qiita

参考にさせていただいたサイト

Beam Programming Guide コレクションと値の結合 | Cloud Dataflow のドキュメント | Google Cloud Platform API リファレンス

【技術記事】Apache Beam with Google Cloud Dataflow(over 2.0.x系)入門~基本的なGroupByKey編~

Apache Beam with Google Cloud Dataflow(over 2.0.x系)入門~基本的なGroupByKey編~

Apache Beamの5つのCore Transformの内の1つ、GroupByKeyの基本的な使い方について記す。
CoGroupByKeyなどについては別の機会に書けたらなと思う。

Apache Beam や Cloud Dataflowの基本についてはこちら

公式のBeam Programming Guideを参考に書かせていただいている。

GroupByKeyとは

並列なreduction操作。
Map/Shuffle/Reduce-styleでいうところのShuffle。
GroupByKeyは、簡単に言うとその名の通り「KeyによってCollectionをGroup化する」Core Transform。
Keyは同じだが、valueが異なるペアが複数存在するKey-ValueなCollectionを結合して新しいCollectionを生成する。
共通なKeyを持っているデータを集約するのに役に立つ。

multimapとuni-map

multimap

例えば、Java, Python, GoというKeyがあったとする。
その複数の各Keyに各々Valueが数字で割り当てられている。
変換前のこのMapをmultimapという。

Java,1
Python,5
Go,1
Java,3
Java,2
Go,5
Python,2
Go,2
Go,9
Python,6

uni-map

上記のKey-ValueなmultimapのCollectionに対してGroupByKeyを適用すると以下のような結果が得られる。

Java [1, 6, 8]
Python [2, 7]
Go[7, 8]

変換後このMapをuni-mapと呼ぶ。 一意のJava, Python, GoというKeyに対して、数字のCollectionのMapが割り当てられている。

Beam SDK for Java特有のKey-Valueの表し方

Beam SDK for Javaでは、通常のJavaとは異なるKey-Valueの表し方をする。
KV<K, V>という型でkey-valueのオブジェクトを表す。

実際にコードを書いてみた

読み込むファイル

Java,1
Python,5
Go,1
Java,3
Java,2
Go,5
Python,2
Go,2
Go,9
Python,6

実際のJavaのコード

各処理は、コードにコメントとして記載している。
理解を優先するため、メソッドチェーンを極力使用していない。
そのため、冗長なコードになっている。

import org.apache.beam.sdk.Pipeline;
import org.apache.beam.sdk.io.TextIO;
import org.apache.beam.sdk.options.PipelineOptions;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.transforms.DoFn;
import org.apache.beam.sdk.transforms.GroupByKey;
import org.apache.beam.sdk.transforms.ParDo;
import org.apache.beam.sdk.values.KV;
import org.apache.beam.sdk.values.PCollection;


/**
 * メイン
 * Created by sekiguchikai on 2017/07/12.
 */
public class Main {
    /**
     * 関数オブジェクト
     * 与えられたString str, String numを","で分割し、
     * numをInteger型に変更して、KV<String, Integer>型にする
     */
    static class SplitWordsAndMakeKVFn extends DoFn<String, KV<String, Integer>> {
        @ProcessElement
        // ProcessContextは、inputを表すobject
        // 自分で定義しなくてもBeam SDKが勝手に取ってきてくれる
        public void processElement(ProcessContext c) {
            // ","で分割
            String[] words = c.element().split(",");
            // 分割したword[0]をKに、words[1]をIntegerに変換してVにする
            c.output(KV.of(words[0], Integer.parseInt(words[1])));
        }
    }

    /**
     * 関数オブジェクト
     * KV<String, Iterable<Integer>型をString型に変更する
     */
    static class TransTypeFromKVAndMakeStringFn extends DoFn<KV<String, Iterable<Integer>>, String> {
        @ProcessElement
        public void processElement(ProcessContext c) {
            // inputをString型に変換する
            c.output(String.valueOf(c.element()));

        }

    }


    /**
     * インプットデータのパス
     */
    private static final String INPUT_FILE_PATH = "./sample.txt";
    /**
     * アウトデータのパス
     */
    private static final String OUTPUT_FILE_PATH = "./result.csv";

    /**
     * メイン
     * 理解のため、メソッドチェーンを極力使用していない
     * そのため、冗長なコードになっている
     *
     * @param args 引数
     */
    public static void main(String[] args) {
        // まずPipelineに設定するOptionを作成する
        // 今回は、ローカルで起動するため、DirectRunnerを指定する
        // ローカルモードでは、DirectRunnerがすでにデフォルトになっているため、ランナーを設定する必要はない
        PipelineOptions options = PipelineOptionsFactory.create();

        // Optionを元にPipelineを生成する
        Pipeline pipeline = Pipeline.create(options);

        // inout dataを読み込んで、そこからPCollection(パイプライン内の一連のデータ)を作成する
        PCollection<String> lines = pipeline.apply(TextIO.read().from(INPUT_FILE_PATH));

        // 与えられたString str, String numを","で分割し、numをInteger型に変更して、KV<String, Integer>型にする
        PCollection<KV<String, Integer>> kvCounter = lines.apply(ParDo.of(new SplitWordsAndMakeKVFn()));

        // GroupByKeyで、{Go, [2, 9, 1, 5]}のような形にする
               // GroupByKey.<K, V>create())でGroupByKey<K, V>を生成している
        PCollection<KV<String, Iterable<Integer>>> groupedWords = kvCounter.apply(
                GroupByKey.<String, Integer>create());

        // 出力のため、<KV<String, Iterable<Integer>>>型からString型に変換している
        PCollection<String> output = groupedWords.apply(ParDo.of(new TransTypeFromKVAndMakeStringFn()));

        // 書き込む
        output.apply(TextIO.write().to(OUTPUT_FILE_PATH));

        // run : PipeLine optionで指定したRunnerで実行
        // waitUntilFinish : PipeLineが終了するまで待って、最終的な状態を返す
        pipeline.run().waitUntilFinish();
    }
}

ちなみにメソッドチェーンを使うとこんな感じ。
だいぶすっきりした。

import org.apache.beam.sdk.Pipeline;
import org.apache.beam.sdk.io.TextIO;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.transforms.DoFn;
import org.apache.beam.sdk.transforms.GroupByKey;
import org.apache.beam.sdk.transforms.ParDo;
import org.apache.beam.sdk.values.KV;


/**
 * メイン
 * Created by sekiguchikai on 2017/07/12.
 */
public class Main {
    /**
     * 関数オブジェクト
     * 与えられたString str, String numを","で分割し、
     * numをInteger型に変更して、KV<String, Integer>型にする
     */
    static class SplitWordsAndMakeKVFn extends DoFn<String, KV<String, Integer>> {
        @ProcessElement
        // ProcessContextは、inputを表すobject
        // 自分で定義しなくてもBeam SDKが勝手に取ってきてくれる
        public void processElement(ProcessContext c) {
            // ","で分割
            String[] words = c.element().split(",");
            // 分割したword[0]をKに、words[1]をIntegerに変換してVにする
            c.output(KV.of(words[0], Integer.parseInt(words[1])));
        }
    }

    /**
     * 関数オブジェクト
     * KV<String, Iterable<Integer>型をString型に変更する
     */
    static class TransTypeFromKVAndMakeStringFn extends DoFn<KV<String, Iterable<Integer>>, String> {
        @ProcessElement
        public void processElement(ProcessContext c) {
            // inputをString型に変換する
            c.output(String.valueOf(c.element()));

        }

    }


    /**
     * インプットデータのパス
     */
    private static final String INPUT_FILE_PATH = "./sample.txt";
    /**
     * アウトデータのパス
     */
    private static final String OUTPUT_FILE_PATH = "./result.csv";

    /**
     * メイン
     * 理解のため、メソッドチェーンを極力使用していない
     * そのため、冗長なコードになっている
     *
     * @param args 引数
     */
    public static void main(String[] args) {
        Pipeline pipeline = Pipeline.create(PipelineOptionsFactory.create());

        // メソッドチェーンを使った書き方
        pipeline.apply(TextIO.read().from(INPUT_FILE_PATH))
                .apply(ParDo.of(new SplitWordsAndMakeKVFn()))
                .apply(GroupByKey.<String, Integer>create())
                .apply(ParDo.of(new TransTypeFromKVAndMakeStringFn()))
                .apply(TextIO.write().to(OUTPUT_FILE_PATH));

        // run : PipeLine optionで指定したRunnerで実行
        // waitUntilFinish : PipeLineが終了するまで待って、最終的な状態を返す
        pipeline.run().waitUntilFinish();
    }
}

実行結果

以下の3つのファイルが生成される。
result.csv-00000-of-00003
result.csv-00001-of-00003
result.csv-00002-of-00003

それぞれのファイルの中身は、以下。
分散並列処理で処理が行われているので、中身が空白のファイルや、中身が1つ、2つのものがあったりとバラバラである。
また、どの内容がどのファイルに出力されるかは毎回ランダムである。

result.csv-00000-of-00003
中身なし

result.csv-00001-of-00003

KV{Java, [1, 3, 2]}

result.csv-00002-of-00003

KV{Go, [5, 2, 9, 1]}
KV{Python, [5, 2, 6]}

関連記事

Apache Beam with Cloud Dataflow(over 2.0.0系)入門~基本部分~ParDoまで~ - Qiita

IntelliJとGradleで始めるApache Beam 2.0.x with Google Cloud Dataflow - Qiita

参考にさせていただいたサイト

Beam Programming Guide

GroupByKey と結合  |  Cloud Dataflow のドキュメント  |  Google Cloud Platform

※ Qiitaでも同一の投稿を行っている

【技術記事】Circle CI 2.0の基礎的な設定まとめてみた(GAE/Goのサンプル付き)

Circle CI 2.0の基礎的な設定まとめてみた(GAE/Goのサンプル付き)

今回の記事について

Circle CI2.0用の設定の基礎的な部分のメモ。

今回の記事は、単純な設定をCircle CI 2.0 で行うことを目的としているため、基礎的な設定のみを行い、Workflow等の設定は行わない。
※ Workflowの記事は別で書きたいと思っている。
また、サンプルとして、GAE/Goの設定を行う。
別にCircle CI1.0のことを知らなくても読めるはず。
Circle CI 1.0とCircle CI 2.0の機能的な違いなどについては、ここでは行わないので、以下を参考にするといいと思われる。

CircleCI 2.0に移行して新機能を活用したらCIの実行時間が半分になった話 - クラウドワークス エンジニアブログ

CircleCI 2.0を使うようにするだけで、こんなに速くなるとは夢にも思わなかった! | Tokyo Otaku Mode Blog

基本的な設定の項目はまとめるが、全部ではないので足りないところについては公式ドキュメントを参照されたい。

公式ドキュメントをかなり参考にさせていただいた。

実際の設定

まずは、実際の設定ファイルを見ていく方がわかりやすいと思うので、まずは設定ファイルを以下に記述する。

version: 2 # バージョン2を指定する
jobs:
 build: # Goのbuildとテストを行う
  docker: # ベースとなるDocker imageを指定
   - image: circleci/golang:1.8 # Dockefileのパスを指定(Go1.8を指定)
  environment:
   TZ: /usr/share/zoneinfo/Asia/Tokyo # Time Zoneを指定
  working_directory: /home/circleci/go/src/project # コード実行場所 以下のstepsはworking_directoryで実行される
  steps: # ローカルでも必要なものはshell scriptと言う感じで行う
   - checkout # working_directoryにcheckout
   - run: # command lineのプログラムを発動させる
      name: Set PATH to .bashrc. # runには名前をつけることができる
      command: | # 実際のコマンド 複数行に場合は、 `|` をつける
       echo 'export PATH=$HOME/go/bin:$HOME/go_appengine:$PATH' >> $BASH_ENV  # $BASH_ENVはデフォルトで入っている
       source /home/circleci/.bashrc
   - run:
      name: Make GOPATH directory. # GOPATHを指定するディレクトリを作成
      command: mkdir -p $HOME/go/src
   - run:
      name: Set GOPATH to .bashrc. # .bashrcにGOPATHを追加
      command: |
       echo 'export GOPATH=$HOME/go' >> $BASH_ENV  # $BASH_ENVはデフォルトで入っている
       source /home/circleci/.bashrc
   - run:
      name: Install appengine sdk. # appengine SDKをインストール
      command: |
       wget https://storage.googleapis.com/appengine-sdks/featured/go_appengine_sdk_linux_amd64-1.9.58.zip
       unzip go_appengine_sdk_linux_amd64-1.9.58.zip -d $HOME
   - run:
      name: Execute setup. # ここでセットアップ用のshellを実行
      command: PATH/TO/setup.sh
   - run:
      name: Run Server Unit Tests. # ここでユニットテストを実行するshellを実行
      command: PATH/TO/test.sh

Circle CI 2.0の各項目の説明

設定ファイル

Circle CI 2.0では、設定ファイルを従来の circle.yml ではなく、 .circleci/config.yml に記述することになっている。

version

Circle CI 2.0を使用する場合は、versionと言うkeyを .circleci/config.yml の先頭に記述し、valueとして、2を指定する。

jobs

mapで表現する。以降で説明する各jobの集合。

docker

dockerの設定を行う。
色々な項目があり、全部は記述しないので、その他の項目や詳しいことは公式ドキュメントを参照されたい。

image

Circle CIで使用するCustom Docker Image。
複数指定することが出来るが、最初に設定したDocker ImageがDocker executorを使用して、各jobを実行するのに使用されるPrimary Containerとなる。

environment

各変数はここで宣言する。

TZ

Docker imageのTime Zoneは、environmentにTZと言う変数で指定する。

working_directory

後述するstepsを実行する場所を指定する。

branches

Git hubなどのbranchのルールを指定する。
onlyとignoreを指定することができる。
onlyとignoreが同一 .circleci/config.yml に存在する場合に関しては、ignoreのみが適用される。

only

onlyを宣言した後のbranch名のリストにあるbranchのみCircle CIが実行されるようになる。

ignore

ignoreを宣言した後のbranch名のリストにあるbranchを無視してCircle CIが実行されるようになる。

Workflowを使用する場合は、個別のjobにbranchを記述しない。

steps

実行されるstepのリスト。
key/valueのmapで表現する。
色々な項目があり、全部は記述しないので、その他の項目や詳しいことは公式ドキュメントを参照されたい。

checkout

設定されたpathにソースコードをcheck outする。
デフォルトでは、 working_directory がそのpathになる。

run

command lineプログラムを実行する。
以下のような方法が基本的な書き方。

-run 
  command: コマンド

以下のように name としてcommandに名前をつけることもできる。

-run 
    name:
  command: コマンド

この場合は、CircleCI UIで表示される時にcommandがこの名前で表示される。
そうじゃない場合はフルのcommandが表示れるので、 name をつけた方がわかりやすくていいと思う。

複数行に渡る command| をつけて、以下のように記述する。

-run 
    name:
  command: |
  コマンド1行目
  コマンド2行目

その他の注意点

Circle CI 1.0では、 environment に指定すればCircle CIがよしなにやってくれていたが、Circle CI 2.0ではそうはいかない。
Circle CI 2.0では、自分で command で、.bashrc に設定する必要がある。
サンプルにある BASH_ENV というのはCircle CI 2.0がデフォルトでexportしているため、自前でenvironmentで宣言する必要はない。
以下を参考にさせていただいた。
How to add a path to PATH in Circle 2.0? - CircleCI 2.0 / 2.0 Support - CircleCI Community Discussion

GAE/Goのサンプルの流れの説明

GAE/Goの流れは以下のようになっている。(stepsの部分のみ説明)
1. .bashrcにPATHを設定する
2. GOPATH用のディレクトリを作成する
3. 作成したGOPATH用のディレクトリを.bashrcにGOPATHとして設定
4. appengine SDKをインストールして、解凍
5. その他の設定用のshellを実行
6. テスト用のshellを実行

Circle CIから別の環境に乗り換えることも考慮して、appengine SDK以外の設定に関しては、shellで行うようにしている。

所感

書きやすくなった。

参考にさせていただいたサイト

公式ドキュメント

Circle CI2.0

circleci2.0でおもにgoをCIする

CircleCI 2.0に移行して新機能を活用したらCIの実行時間が半分になった話 - クラウドワークス エンジニアブログ

How to add a path to PATH in Circle 2.0? - CircleCI 2.0 / 2.0 Support - CircleCI Community Discussion

CircleCI 2.0を使うようにするだけで、こんなに速くなるとは夢にも思わなかった! | Tokyo Otaku Mode Blog

Qiitaでも同一の投稿を行っている。

【技術記事】CSSの設計方法をまとめてみた~SUIT CCS編~(Angularによるサンプル付き)

CSSの設計方法をまとめてみた~SUIT CCS編~

SUIT CSSについて、簡単にまとめる。
また、Angularを使用した簡単なサンプルも書いてみる。
なお、本記事は、公式ドキュメントをかなり参考にさせていただいている。

詳細な部分に関しては以下の記事がわかりやすかったので、参考にされると良いと思う。
suit/design-principles.md at master · suitcss/suit suit/naming-conventions.md at master · suitcss/suit

SUIT CSSとは

Componentベースのcssの方法論。
Componentベースなので、AngularやReactなどのComponent指向なJavaScriptフレームワークやライブラリと相性が良いそう。

SUIT CSSのメリット

各々のユニットの結びつきを緩くして、独立したものにすることができる。

SUIT CSS の設計原則

ここで使用している英文は以下から引用させていただいている。
suit/design-principles.md at master · suitcss/suit

Modularity(モジュール性)

Each component should have a single focus and contain everything necessary to realise a specific part of the UI.

【意訳】
各Componentは、1つの事柄に集中して、また、UIの特定のパーツを実現するのに必要なすべてのものを含んでいるべきである。

Componentは、HTML、CSSJavaScriptを含んでいる。

=> AngularのComponentのようなもの。

Cohesion(結束)

The functionality and presentation defined by a component must be semantically related.
Components do not have direct influence over each other.

【意訳】
Componentによって、定義される機能性と表現は、意味的に結びついていなければならない。
Componentは、違いに直接的な影響を与えない。

Composable and configurable(組み立て可能で、設定可能である)

必要に応じて、Componentを組み合わせ可能であること。

Configuration is done via interfaces that are provided and used by components.

【意訳】
Componentによって提供され、利用されるinterfaceを通して、設定が行われる。

Loose coupling(弱い結びつき)

Component間の依存関係は、直接Component同士であれこれするのではなく、interfaceとeventでどうにかしましょう。

Soft encapsulation(ソフトなカプセル化)

Componentを他のComponentからカプセル化して、他のComponentから直接Component内のコードを利用できなくする必要がある。

Documentation(文書化)

各Componentの役割やCSSプロパティがなぜ必要かを丁寧に文書化する。

SUIT CSS命名規則

SUIT CSSでは、意味のあるハイフンと構造化したclassを使用する。
SUIT CSSでは、大きく分けて、UtilitiesとComponentsと概念が存在する。
繰り返し使うようなCSSのプロパティに対して、適用すると何回もCSSを記述しなくても良くなる。

Utilities

Utilitiesは、低レベルの構造的また位置の特徴を表す。
Componentの中で、どの要素にも直接適用できる。
繰り返し使うようなCSSのプロパティに対して、適用すると何回もCSSを記述しなくても良くなる。

記述方法

基本的には u-utilityName で記述する。
utilityNamecamel caseで記述する。
レスポンシブデザインを考慮した書き方もでき、その場合は u-size-utilityName で記述する。
sizeは sm = small、 md = medium、 lg = large Mediaのいずれかを適用する。

記述例

以下のように記述する。

<p class="u-floatLeft">hoge</p>

Components

記述方法

namespace-ComponentName-descendentName--modifierName というふうに記述する。

namespace(任意)

必要に応じて、namespaceというプレフィックスをつけることができる。
これによって、自作の部分とライブラリの区別がつきやすくなったりする。
<div class="namespace-Component">hoge<div> という形で記述する。
.namespace-Component{...} という形でstyleを適用する。

ComponentName

Componentの名前。
pascal case(アッパーキャメルケース)で記述する。
<div class="ComponentName">hoge<div> という形で記述する。
.ComponentName{...} という形でstyleを適用する。

ComponentName--modifierName

ベースとなるComponentの表現を変更するためのclass。
camel caseで記述する。
ComponentName--modifierNameは、ベースとなるに追加してHTML上に書く。
<div class="ComponentName ComponentName--modifierName">hoge<div> という形で記述する。
以下の形でstyleを適用する。

.ComponentName{...}
.ComponentName--modifierName{...}
ComponentName-descendentName

Componentの子要素のようなもの。
Componentの子要素に直接、cssの表現を適用する。
<div class="ComponentName-descendentName">hoge<div> という形で記述する。
.ComponentName-descendentName{...} という形でstyleを適用する。

ComponentName.is-stateOfComponent

Componentの状態の変更を反映するのに使用する。
camel caseで記述する。
state nameはいろんなところで使い回すけども、各Componentによってstateに当てるstyleは異なるので、直接このstate nameにstyleを当ててはいけない。
<div class="ComponentName is-stateOfComponent">hoge<div> という形で記述する。
以下の形でstyleを適用する。

.Compomnent{}
.Compomnent.is-stateOfComponent{}

という感じでstyleを適用する。

実際に書いてみた

実際にComponent指向のjavaScriptフレームワークであるAngularで実装してみた。

実際のコード

app.component.ts

import {Component} from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss', './utilities.scss']
})
export class AppComponent {
  contentsTitle = 'SUIT CSS!';
  contentsHeading = 'design principles';
  designPrinciples = ['Modularity', 'Cohesion', 'Composition and configuration', 'Loose coupling', 'Soft encapsulation', 'Documentation'];
  clazz = 'AppComponent is-coloredBlack';

  /**
   * 文字を赤くする
   */
  changeColorToRed() {
    this.clazz = 'AppComponent is-coloredRed';
  }

}

app.component.html

<!--Component Name-->
<article class="AppComponent">
  <!--ComponentName-descendentName-->
  <header class="AppComponent-header">
    <h1>{{contentsTitle}}</h1>
  </header>
  <!--ComponentName-descendentName-->
  <div class="AppComponent-content">
    <!--ComponentName.is-stateOfComponent-->
    <h1 [class]="clazz" (click)="changeColorToRed()">{{contentsHeading}}</h1>
    <!--utilities-->
    <ul class=" u-listStyleNone">
      <li *ngFor="let p of designPrinciples">
        {{p}}
      </li>
    </ul>
  </div>
</article>

app.component.scss

/*AppComponentに対するstyle*/
.AppComponent {
  width: 100%;
  .AppComponent-header {
    color: #0000ed;
  }
  .AppComponent-content {
    color: #3A3A3A;
  }
  .is-coloredBlack {
    color: black;
  }
  .is-coloredRed {
    color: red;
  }
}

app.component.scssからemitされたapp.component.css

@charset "UTF-8";
/*AppComponentに対するstyle*/
.AppComponent {
  width: 100%;
}

.AppComponent .AppComponent-header {
  color: #0000ed;
}

.AppComponent .AppComponent-content {
  color: #3A3A3A;
}

.AppComponent .is-coloredBlack {
  color: black;
}

.AppComponent.is-coloredRed {
  color: red;
}

utilities.scss

/*listの点を消す*/
.u-listStyleNone {
  list-style: none;
}

utilities.scssからemitされたutilities.css

@charset "UTF-8";
/*listの点を消す*/
.u-listStyleNone {
  list-style: none;
}

実装結果

クリック前
suitccs-before-click.png

クリック後

suitccs-after-click.png

関連記事

CSSの設計方法をまとめてみた~BEM編~ - Qiita

所感

Component指向が根底にある設計方法なので、Angularなどの現代的なComponentベースのフレームワークと相性がよくて、使いやすそう。
Component指向のフレームワークを使っている人にはすごく馴染みやすいと思う。
また、システマチックな感じに考えられているところがよかった。

参考にさせていただいたサイト

suit/doc at master · suitcss/suit

キャメルケース - Wikipedia

HTML5のお勉強 articleとsectionとか - Qiita

[HTML5] 新要素まとめ【2014/2/14版勧告候補】 - Qiita

Qiitaでも同一の投稿を行っている。