Software engineering from east direction

六本木一丁目で働くソフトウェアエンジニアのブログ

Go 1.15 testing パッケージの TestMain は os.Exit を呼ばなくてよくなった

TL;DR

  • Go1.15 がリリースされました
  • testing パッケージの実装の変更にて TestMain で os.Exit を呼ばなくてよくなりました
  • testing.M の実装変更及び、 go test で生成される main 関数のテンプレート修正によって実現されている

Go 1.15

2020年8月11日、 The Go Blog より、 Go 1.15 のリリースが公表されました。

blog.golang.org

Release Note を読むと、さまざまな機能が追加されています。 

golang.org

これを読んでいて、ふと Minor changes to the library から testing パッケージにいくつか変更があって、バージョンアップしたらちょっっっとテストの書き方が変わるなって思ったので深堀り確認してみました。

※ 注記: Qiitaで同じものを見たとしたら同一の記事です。 Go 1.15 testing パッケージの TestMain は os.Exit を呼ばなくてよくなった - Qiita ブログの書き先を迷子になっています。

testing の変更

testing パッケージには、いくつかマイナーな変更が加えられています。

https://golang.org/doc/go1.15#testing

  • T.Deadline の追加

The testing.T type now has a Deadline method that reports the time at which the test binary will have exceeded its timeout.

  • TestMain は os.Exit を呼ばなくてよくなった

A TestMain function is no longer required to call os.Exit. If a TestMain function returns, the test binary will call os.Exit with the value returned by m.Run.

  • T.TempDir / B.TempDir の追加

The new methods T.TempDir and B.TempDir return temporary directories that are automatically cleaned up at the end of the test.

  • go test -v の表示改善

go test -v now groups output by test name, rather than printing the test name on each line.

タイトルのとおりですが、この中の 2 つめに焦点を当ててみます。

TestMain は os.Exit を呼ばなくてよくなった

A TestMain function is no longer required to call os.Exit. If a TestMain function returns, the test binary will call os.Exit with the value returned by m.Run.

https://golang.org/doc/go1.15#testing

たとえば、TestMain を使うテストコードはGo 1.14 以前は次のようなコードだったと思います。

package main_test

import (
    "log"
    "os"
    "testing"
)

func TestMain(m *testing.M) {
    shutdown := setupDB()

    c := m.Run()

    shutdown()

    os.Exit(c)
}

func setupDB() func() {
    return func() {
        // shutdown
    }
}

testing.m.Run() は プログラムの終了コード に該当する int 値を返してくるので、それを os.Exit() に明示的に渡すようにしていました。

https://golang.org/pkg/testing/#M.Run

Go 1.15 以降は、この os.Exit が不要になりました。

package main_test

import (
    "testing"
)

func TestMain(m *testing.M) {
    shutdown := setupDB()

    m.Run()

    shutdown()
}

func setupDB() func() {
    return func() {
        // shutdown
    }
}

使い手としてはこれだけです。マイナーな変更ですね。まぁそれだけでは、へぇそうなんだねって話なので、実際中ではどういう変更があったのってところを見たいなと思います。

Dive into testing.M

Issue

この変更は、次の issue で 2019年9月6日に提案されているものです。

github.com

issue を読んでいると、よく Buggy なテストが生まれることが観測されていたと、Russ Cox氏が言及しています。

This mistake does happen a lot, and the testing package that called TestMain and prepared the m can certainly record the result of m.Run for use in its own outer os.Exit.

https://github.com/golang/go/issues/34129#issuecomment-531011027

実際にどういうミスかと言うと、こういうテストコードの場合です。

package sample_test

import (
    "os"
    "testing"
)

func TestMain(m *testing.M) {
    var exitCode int
    defer os.Exit(exitCode)

    setup()
    defer teardown()
    exitCode = m.Run()
}

func setup() {
    //
}

func teardown() {
    //
}

func TestHoge(t *testing.T) {
    t.Fail()
}

TestMain() では、deferでos.Exit をcallするようにしています。そして当該パッケージ内には失敗するテストコードがあるので、これは失敗するはずです。これを実行すると次のようになります。

$ go test -v hoge_test.go 
=== RUN   TestHoge
--- FAIL: TestHoge (0.00s)
FAIL
ok      command-line-arguments  0.369s

ok となってしまいました。終了コードがうまく反映されていません。

このような失敗していてもテストコマンドが静かにそのバグを報告しなくてなってしまうことに対して、 Accept されたのは以下でした。

  1. m.Run records its own result in an unexported field of m, and then
  2. if TestMain(m) returns, the outer test func main harness calls os.Exit with that code.

1. m.Run records its own result in an unexported field of m

この課題に対して実際に実装された結果は 下記の commit から見て取れます。

https://go-review.googlesource.com/c/go/+/219639/10/src/testing/testing.go

L220 にてコメントは次のように変更されています。

src/testing/testing.go

// and teardown is necessary around a call to m.Run. m.Run will return an exit
// status that may be passed to os.Exit. If TestMain returns, the test wrapper
// will pass the result of m.Run to os.Exit itself. When TestMain is called,
// flag.Parse has not been run. If TestMain depends on command-line flags,
// including those of the testing package, it should call flag.Parse explicitly.

If TestMain returns, the test wrapper will pass the result of m.Run to os.Exit itself.

TestMain が return したら、 the test wrapperさんがそれをos.Exitに渡してくれるというものです。 そのためにここでは、 「1. testing.M の構造体に、終了コードが追加」し、「2. testing.M.Run は M.exitCode に終了コードを設定」しています。

  1. testing.M の構造体に、終了コードが追加された。

src/testing/testing.go

// M is a type passed to a TestMain function to run the actual tests.
type M struct {
    deps       testDeps
    tests      []InternalTest
    benchmarks []InternalBenchmark
    examples   []InternalExample
    timer     *time.Timer
    afterOnce sync.Once

    numRun int

    // value to pass to os.Exit, the outer test func main
    // harness calls os.Exit with this code. See #34129.
    exitCode int // <= これが今回増えました
}
  1. testing.M.Run は M.exitCode に終了コードを設定する

return する際、これまで終了コードを返していただけでしたが、 M.exitCode に設定するようにしています。

src/testing/testing.go

   if *parallel < 1 {
        fmt.Fprintln(os.Stderr, "testing: -parallel can only be given a positive integer")
        flag.Usage()
        // return 2 これがもともと、普通に終了コードを返答していた
        m.exitCode = 2
        return
    }

※ 関数冒頭に defer func で m.exitCode を戻り値として返却するように記述しています。

src/testing/testing.go

func (m *M) Run() (code int) {
    defer func() {
        code = m.exitCode
    }()

2. if TestMain(m) returns, the outer test func main harness calls os.Exit with that code.

TestMain が終了後、外の main 関数が m.exitCode を見て、os.Exit をcallするようにしています。これは、以下のコミットにその反映を見ることが出来ます。

https://go-review.googlesource.com/c/go/+/219639/10/src/cmd/go/internal/load/test.go

この変更は、 go testによって生成される main 関数のテンプレートの修正です。

TestMain の場合は、 reflect パッケージを追加し、 構造体 M の exitCode の field をとっています。

src/cmd/go/internal/load/test.go

os.Exit(int(reflect.ValueOf(m).Elem().FieldByName("exitCode").Int()))

この変更によって、testing.M の内部の unexported な exitCode の値を使って、 os.Exit をコールしています。

以上

Go 1.15 におけるテストの書き方の変更と、その背景・内部実装の深堀りでした。 Congrats Go 1.15 release!!!

アジャイル開発におけるメンタルヘルス 〜駆け出しアジャイル記録〜

TL;DR

  • アジャイル開発における「あるあるの失敗」は、コミュニケーションを一瞬でも取り逃すと発生しうる
  • どういうときに流れに飲み込まれ、メンタルの追い込みが発生しうるかを考えた
  • 何を主眼としているかを何度もコミュニケーションし、発生させてしまいかねない副作用を避ける、流れに飲み込まれないための施策が必要だろう

背景

所属企業で、かつてからのチーム開発の課題や、WORK FROM HOME な事情もあり、アジャイルムーブメントをに取り組んでいます。そのなかで、現職での取り組みを始めてざっと3ヶ月弱となり、教科書どおりには決していかない、自分たちにとって最適なものは何かを日々レトロスペクティブなどで探っている日々。そんな発展途上な段階の思考をdumpしてしまおうという記事。

上記資料については、プロジェクトをどう進めるかについては、説明していないが、チームの取り組み方を変えた後に、プロジェクトの取り組み方自体に対して、漸進的にリズムを作っていっている最中という状況。

以下、取り除いたほうが良さそうだと感じたリスクを感覚レベルの粗末な文章で書き連ねていく。

生産性を気にしていないか?それを主眼にしていなくても視界にはいることのリスク

前提:ストーリーポイントでの見積もり、消化ポイントをメトリクスに取ることによるベロシティ計測

見積もりの手法として、いくつか他PJでも採用されている「ストーリーポイント」を使用している。これは、だいたい3日でできるといった差込なしの作業に対する見積もり(理想日による見積もりとよく表現される)のではなく、それぞれのissueを相対評価大きさで図るもの。

たとえば、「注文完了時にメールを送信する」を1ptとしたら、「注文情報を表示する」は相対的にメール送信するよりもやること多いし、技術的にも触れるバリエーションが多いから3ptくらいかしら、的な形で相対評価で、そのタスクがはいるばけつはど〜れ?って考えるやり方といえる(と認識している)。

そして、それを1週間など一定期間でどれだけの大きささばけたか、を計測していくことで、自分たちが見積もりに対してどれだけの速度でさばけるかを、ベロシティとして認識する。平均速度のようなものなので、何らかそのメトリクスに下降傾向とかがあったら「あれ?どうしたのかな?体調が悪い?」といった形で図れるといった効果も期待される。

チームのメトリクスと見ても、個々人の生産性に"感じる"リスク

個人としてチームのメトリクス内で「どれだけの大きさ」できた?みたいなのが、意識内に入ってしまうところはある、だってにんげんだもの

これに対して、小さい組織の分にはコミュニケーションの中で「なぜいまこれをしているのか」について会話をすることで何らか対策が取れるだろうが、人数が増えると大変そう。やり方をフレームワークと捉えてしまうと、即座にフレームワークの罠に陥ってしまうだろう。「前職でやってたけどつらかった」といった話はこういった経験から生まれるのかもしれないですね。

今のところ考えること: 成果(WHAT)と取り組み(HOW)の振り返りは分離する

極端なことを言うと、たくさんストーリーポイントを消化しようがしまいが、成果(WHAT)がでてフィードバックがもらえてっていうループが回せるか、が重要と心得る。

プロジェクトの成果を振り返る場では、プロダクトに対して純粋に「こうしたほうが良い?」・「ここはこう治そう」と向き合うことが出来るが、チームでの取り組みを振り返る(レトロスペクティブとよんでいる)場で仮にWHATを図ろうとした場合にできることは「消化ポイントがどれくらいだったか」になる。これがよくない。明確にWHATとHOWの振り返りを分離することで、ベロシティを上げることから視線を外す事が必要なのではないか。

また、想定した成果が出なかったよぉっていうときには、それはプロジェクトの成果として考えると良いのだろう。たとえばプロジェクトにどれだけ時間を費やしたか、といった占有率、とかそういうことが分析として考えられる。

ある種、PJ単位にすることは逃げ道を用意できる側面もあるだろう。いろいろ他のこともしていたので〜だったり。逃げるは恥だが役に立つ

そんなことを思っているので、スプリントレビュー(PJの成果レビュー)とレトロスペクティブ(チームでの取り組み振り返り)を分けることにしている。

適度なプレッシャーを保てるか

1週間っていう期間をおいて、毎週成果を出しましょうっていうのは、リリースが毎週繰り返されるみたいなお気持ちに近いところがあるので、それなりのプレッシャーは精神にかかることにはなる。だからこそメンタリング装置が各所に仕込まれているっつう話なんだろうが、このプレッシャーが適度なものになっているかは、気にしないといけない。

自分なりの努力を重ねることを元プロ野球選手のイチローが言っていたが、ゆうて自分なりに馴染む温度で仕事をすれば良いのであって、過プレッシャーはよくない。

個人にとってのHOWとWHAT

プログラマ個人にとって、HOWの理解→WHATへの焦点といった段階があるのか、とか早朝朝5時思い立ったのは、たとえば、いまからBASEの中でAPI作ってくださいねってときに、何が焦点になるかというと人によって異なっていて、3びきのこぶたのテンションでいうと、

  1. PHPを書くぞ
  2. ブラウザからアクセスされるAPIを書くぞ
  3. BASEのサービスを作るぞ

みたいな感じでそれぞれどこに焦点があたっているかで、なんとなくその人の仕事感みたいなところが出るのかな、とかおもう。

1がわりとHOWよりで、3がWHAT寄りだが、1が焦点の状態で1に取り組むと、HOWに対するプレッシャー、つまり、「PHP難しそうだけど頑張るぞぉ...」っていうプレッシャーがのってくる。

さて、そのうえでイテレーションで動くものをつくりましょう、なり、なんらか明確な言葉で成果を定義されると、その焦点は自然と2以降にずらされる。

結果的に、これまでHOWのプレッシャーが掛かっている状態で、さらにWHATのプレッシャー、つまり「今週中に自分がやりきらないといけないのはこのAPIを...」とか、「ユーザーが待っているし問い合わせも日々見るから早くリリースを...早く...」と、上乗せされる。

この事象に対して、HOWのプレッシャーに戦略的に鈍感になることで、すぐにWHATを実現しよう、みたいな話が技術的負債のあれこれに、つながっていったりするのだろう

無意識的にHOWのプレッシャーに鈍感であることは、「プロとしてレベルの低い仕事をしていることに気が付かない」可能性を含んでいるので危険だろうけども。

今のところ考えていること その人にとってのプレッシャーがどこにのっているかによって計画の立て方を変えるのが良いのかもしれない

たとえば、オンボーディング期間とかの状態で、即座に明日リリースだからシュッと全部実装してデプロイしてください、みたいなことをいうと、極端にHOW/WHAT両方のプレッシャーが押し寄せて、震度7大震災ですいやーんこわーい、って話。

そういう期間であることを加味した設計が必要なのだろう。ペアオペなどで知識を同化していく〜みたいな話も、精神の余裕があってこそ。

個人が職能横断的であることが求められる昨今

個人が職能横断的にであることが求められる昨今ホンコン。これ自分は麻痺してるんだけど、けっこう大変なんですねっていうことをおもい。

組織重力の3つの法則というみんなでアジャイルの中で語られている話だと、組織の個人は自分のサイロの中で簡単な仕事を優先する、みたいな話があるので、別に自分たちが自立してだいたいのものはカバー範囲にありコントロール下にある「簡単な仕事」にしていくことは、プロダクトの課題解決にあたってはなくては厳しいと思うが、あくまでそれはチームの話

思っていること 個人が職能横断的ではなくてもいいことは改めて言語化しないといけないのだろう。

アジャイルチームのメンバー構成の要件にはプロフェッショナルがいることを上げているが、特化した専門性は必ず必要になる。 チーム状況によっては、ある程度、専門外のことをやる必要があるが、それが強いられることはない。

専門的組織との関わり方

職能でレイヤーの切られた組織において、職能横断的であることが求められた場合、その人の志向性とミスマッチする可能性がある

PHPで世界をとりたい、みたいな人に「今からTerraform書いてAWSのインフラ作ってくれ」って言うのはその人にとって不幸せな余分な時間になるだろう。

あるいはデザインやML的な文脈でも違う専門性なので、そこに対して職能横断的であることを強いるのは精神衛生上良くない。

おもうこと プロジェクトの関係者 != チーム

チームであることを強いると、上記のミスマッチを発生させてしまう可能性がある。プロジェクトの捉え方について認識をすり合わせることは、仕事をしていく上で大事だが、それとはまた別の話。

What are you doing? 何をいましているのか・どうやっているのかをプロジェクトとしたら、チームであることを強いるのは「どこにいるのか」を規定してしまう。

たとえば、突然あなたは明日からメイドカフェのチームとして振る舞ってもらわないといけないので、猫耳をつけてくださいって言われたら、とりあえず退職届を書くことになると思うのね。 メイドカフェを立ち上げるPJをやるからそのPJにJOINしてもらいます、その中ではこういうふうに考えてやっていくの、さぁ猫耳をつけてって言われたら、いやそれでも退職届を書くことに...

例が悪かった。いい例が思いつかないが、具体的な話に戻ると、スプリントレビューに入ってもらう人と、レトロスペクティブに入ってもらう人は、明確に違う風にしていいと思っているということ。

これは、1PJ=1TEAMの1-1の関係であれば両方入るカバレッジが100%だし、社前提がアジャイルな文化です、っていうならそれもカバレッジ100%だが、世界は多種多様でよくわからないので、自身がどういう状況にいるのかを考えた上で、!= であることを改めて確認したほうが良いのだろう。

これは、組織学習の過程とも言えるかもしれない

上記の怪文書は、ある種これまで潜在的に課題だったものが発見される過程なのかもしれない。

アジャイル開発は組織学習をプロセスに取り込んでいます。アジャイルなチームとなっている組織とそうでない組織を比較して、個々のメンバーを眺めてみると、確かにアジャイルなチームのほうが優秀なメンバーが揃っているように見えるでしょう。しかし、初めから優秀なメンバーを揃えないとできないというわけではありません。おそらく、アジャイルな方法論を取り入れることができないほど優秀でないメンバーであれば、他のプロセスを採用したとしてもうまくいく可能性はありません。

広木 大地. エンジニアリング組織論への招待 ~不確実性に向き合う思考と組織のリファクタリング (Japanese Edition) (Kindle Locations 3144-3149). Kindle Edition.

生JSONを扱うのにちょっと便利な json.RawMessage と観測した実例

TL;DR

  • JSONを扱う際に、標準のjsonパッケージでは、 RawMessage という型が用意されている
  • JSON構造の文字列を取り出して、なんらか整形して出力、といったユースケースにちょっと便利

json.RawMessage

JSONを扱う際に、標準のjsonパッケージでは、 RawMessage という型が用意されている。

// RawMessage is a raw encoded JSON value.
// It implements Marshaler and Unmarshaler and can
// be used to delay JSON decoding or precompute a JSON encoding.
type RawMessage []byte

godoc.org

RawMessage is a raw encoded JSON value. It implements Marshaler and Unmarshaler and can be used to delay JSON decoding or precompute a JSON encoding.

上記の説明にある通り、 RawMessage は、 json.Marshaler interface と json.Unmarshaler interface を実装しているため、 JSON decode / encode に用いることが出来る。

https://godoc.org/encoding/json#Marshaler

type Marshaler interface {
    MarshalJSON() ([]byte, error)
}

https://godoc.org/encoding/json#Unmarshaler

type Unmarshaler interface {
    UnmarshalJSON([]byte) error
}

Marshal

たとえば、次のようにJSONを受け取り、その後それを json.MarshalIndent() で整形すると言ったことも可能。

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "os"
)

func main() {
    m := json.RawMessage(`{"hoge": false, "huga": "piyo"}`)

    c := struct {
        FlexJson *json.RawMessage `json:"json"`
    }{FlexJson: &m}

    md, err := json.MarshalIndent(&c, "", "\t")
    if err != nil {
        log.Fatalf("error: %#v\n", err)
    }
    fmt.Fprint(os.Stdout, string(md))
}

https://play.golang.org/p/FFyY4fS89Du

ここで、JSON形式として破綻している構造の文字列だった場合は、json.MarshalIndent()にて、json.SyntaxError が返答される。

error: &json.SyntaxError{msg:"invalid character ']' after object key:value pair", Offset:88}

https://godoc.org/encoding/json#SyntaxError

// A SyntaxError is a description of a JSON syntax error.
type SyntaxError struct {
    msg    string // description of error
    Offset int64  // error occurred after reading Offset bytes
}

func (e *SyntaxError) Error() string { return e.msg }

Unmarshal

たとえば、配列形式の中に異なる構造のJSON Objectsがある場合、次のように、Unmarshalしてコード内で処理することも出来る。

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "os"
)

func main() {
    res := []byte(`{
  "field1": [
    {"hoge": false, "huga": "piyo"},
    {"ponyo": 1, "piyo":  false}
  ]
}`)

    type c struct {
        Field1 []json.RawMessage `json:"field1"`
    }
    var cc c
    if err := json.Unmarshal(res, &cc); err != nil {
        log.Fatalf("error: %#v\n", err)
    }

    for _, v := range cc.Field1 {
        var dst interface{}
        if err := json.Unmarshal(v, &dst); err != nil {
            log.Fatalf("error: %#v\n", err)
        }
        fmt.Fprintf(os.Stdout, "%#v\n", dst)
    }
}

https://play.golang.org/p/MsJFzMoLmKc

OSS内での使用例

json.RawMessageの現実世界で活用されているOSSの例として、mackerelio/mkr内で利用されています。

具体的には、mkr monitors pullというコマンドを使用した際に、Mackerelサービスに設定されている監視設定をローカルに落としてくることが可能なのですが、その際の処理に json.RawMessage が活用されていました。

func decodeMonitors(r io.Reader) ([]mackerel.Monitor, error) {
    var data struct {
        Monitors []json.RawMessage `json:"monitors"`
    }
    if err := json.NewDecoder(r).Decode(&data); err != nil {
        return nil, err
    }
    ms := make([]mackerel.Monitor, 0, len(data.Monitors))
    for _, rawmes := range data.Monitors {
        m, err := decodeMonitor(rawmes)
        if err != nil {
            return nil, err
        }
        ms = append(ms, m)
    }
    return ms, nil
}

https://github.com/mackerelio/mkr/blob/ef0237b6e6c18cab2d9ce3d0fd34e166076f5fb0/monitors.go#L109

ここでの処理は、APIレスポンス内にある monitors キー以下に、JSON Objectの配列が含まれているのを、[]json.RawMessage と定義することで取り出しています。

json.RawMessageを利用した利点として、これ配列内でさらにJSON Objectをdecodeして、中のtypeキーの情報を取得、その内容によってunmarshalする構造を切り替えるということをしています。

func decodeMonitor(mes json.RawMessage) (mackerel.Monitor, error) {
    var typeData struct {
        Type string `json:"type"`
    }
    if err := json.Unmarshal(mes, &typeData); err != nil {
        return nil, err
    }
    var m mackerel.Monitor
    switch typeData.Type {
    case "connectivity":
        m = &mackerel.MonitorConnectivity{}
    case "host":
        m = &mackerel.MonitorHostMetric{}
    case "service":
        m = &mackerel.MonitorServiceMetric{}
    case "external":
        m = &mackerel.MonitorExternalHTTP{}
    case "expression":
        m = &mackerel.MonitorExpression{}
    case "anomalyDetection":
        m = &mackerel.MonitorAnomalyDetection{}
    }
    if err := json.Unmarshal(mes, m); err != nil {
        return nil, err
    }
    return m, nil
}

発想としては、以下のブログ記事内の

engineering.linecorp.com

ですので、SDKではUnmarshal用の別typeを用意し、その中でまずは type だけ先に読み取り、それから改めて型を決定してUnmarshalする、という方法を取りました。

という方法と類似していると見れそうです(と、@budougumi0617さんに教えていただきました)。

このようなケースがひとつ、 json.RawMessage の活用例としてあげられそうです。

mkr/monitors.go at ef0237b6e6c18cab2d9ce3d0fd34e166076f5fb0 · mackerelio/mkr · GitHub

f:id:khigashigashi:20200623021642j:plain:w320

Fin

PHPコードの些末なレビューの機械化を phpcs と slevomat/coding-standard で加速しよう

TL;DR

  • PHPコードにおける、constはpublic/privateつけようね、とか、戻り値type hintingは、型名の前にspace入れようね、みたいな話、それphpcsに指摘してもらおう
  • quizlabs/php_codesniffer と slevomat/coding-standard を使用する
  • 細かくコーディングルールを拡充して、機械化を加速できる

どうすればいいか

なるべく設計的な意思決定があまり介在しないような定型的なレビューを人間がやるのは、レビュワーもレビュイーも両方疲弊してしまう。その解決案として、slevomat/coding-standardを使うのが手としてあります。これは、quizlabs/php_codesniffer のsniffs(検証ルールのようなもの)を様々提供しているライブラリです。

github.com

これを使い始めるのはかんたんで、composer require --dev したあとに、

$ composer require --dev slevomat/coding-standard

rulesetにconfigとして追加する。

ruleset.xml

<?xml version="1.0" encoding="utf-8" ?>
<ruleset name="php-pj">
  <description>ruleset based on PSR2</description>
  <exclude-pattern>vendor/*</exclude-pattern>
  <rule ref="PSR2">
  </rule>
  <config name="installed_paths" value="../../slevomat/coding-standard"/><!-- relative path from PHPCS source location -->
</ruleset>

まず、<config>により、slevomat/coding-standardが提供するルールを認識できるようにする。

<config name="installed_paths" value="../../slevomat/coding-standard"/><!-- relative path from PHPCS source location --> ですね。

以降は、 <rule> に使いたいものを指定していきましょう。

こういうのを指摘してもらえばいいんじゃない

さまざまなルールがあるが、個人的にこれは現場でも使おう、と思ったものをいくつかピックアップします。

定数定義には可視性指定しよう

SlevomatCodingStandard.Classes.ClassConstantVisibilityを使用すると、定数定義にて public または private を指定しているかどうかを指摘してくれる。

例えばこういうコードだった場合に、

    const HOGE = 'hoge';

こう指摘される

----------------------------------------------------------------------
FOUND 1 ERROR AFFECTING 1 LINE
----------------------------------------------------------------------
 31 | ERROR | Constant \Hoge\Huga::HOGE
    |       | visibility missing.
----------------------------------------------------------------------

次のようなコードに直すように、チームみんなで統一することができる。

private const HOGE = 'hoge';

default nullはnullableだよ

SlevomatCodingStandard.TypeHints.NullableTypeForNullDefaultValue を使用すると、デフォルト値をnullとしている引数の型宣言が nullable になっているかをチェックしてくれます。

    private function getDateFromFormat(string $date = null)

これは、こう指摘される。実質デフォルト null にしている引数のtype hintingは nullable なのでこちらのほうが健全な感じはある。

----------------------------------------------------------------------
FOUND 1 ERROR AFFECTING 1 LINE
----------------------------------------------------------------------
 1291 | ERROR | [x] Parameter $date has null default value, but is
      |       |     not marked as nullable.
----------------------------------------------------------------------
PHPCBF CAN FIX THE 1 MARKED SNIFF VIOLATIONS AUTOMATICALLY
----------------------------------------------------------------------

戻り値のタイプヒンティング前の space

SlevomatCodingStandard.TypeHints.ReturnTypeHintSpacing を使用すると、戻り値のタイプヒンティングの前にspaceが入っているかをチェックしてくれます。

こういうコードがあるとする、

    public function hoge():void

これは、こう指摘される

----------------------------------------------------------------------
FOUND 1 ERROR AFFECTING 1 LINE
----------------------------------------------------------------------
 907 | ERROR | [x] There must be exactly one space between return
     |       |     type hint colon and return type hint.
----------------------------------------------------------------------
PHPCBF CAN FIX THE 1 MARKED SNIFF VIOLATIONS AUTOMATICALLY
----------------------------------------------------------------------

これまでピックアップしたものを設定すると次のようなruleset.xmlになる。

<?xml version="1.0" encoding="utf-8" ?>
<ruleset name="php-pj">
  <description>ruleset based on PSR2</description>
  <exclude-pattern>vendor/*</exclude-pattern>
  <rule ref="PSR2">
  </rule>
  <config name="installed_paths" value="../../slevomat/coding-standard"/><!-- relative path from PHPCS source location -->
</ruleset>
<config name="installed_paths" value="../../slevomat/coding-standard"/><!-- relative path from PHPCS source location -->
<rule ref="SlevomatCodingStandard.TypeHints.ReturnTypeHintSpacing"/>
<rule ref="SlevomatCodingStandard.TypeHints.NullableTypeForNullDefaultValue"/>
<rule ref="SlevomatCodingStandard.Classes.ClassConstantVisibility"/>

その他

実行時の注意点として、ローカルのグローバルなphpcsで実行すると認識できないので、ちゃんと ./vendor/bin/phpcs を使用しましょう。composer scriptなりから呼ぶと自然とそのレポジトリ内のものを使用するのでよき。

PHPコードでの、`empty()`避けようねとか`===`のほうがベター、などは phpstan/phpstan-strict-rules を使って指摘しよう

TL;DR

  • PHPアプリケーションにおいて、表題のような「PHPを使う上で自制心を持とうな」みたいな温度感でstrictな書き方を推奨される昨今
  • こういうのは往々にして人間が言うのはそのうちげんなりしてくるし、逆に言われる方も同じ
  • PHPStanが提供しているカスタムルール phpstan/phpstan-strict-rules を使おう

前提

PHPStanを使うことを前提としています。

phpstan/phpstan-strict-rules

github.com

PHPStanは、「PHPStanでCustomRuleを作る」という発表でも解説されている通り、任意のカスタムルールをクラスとして定義することで、ルールを追加できます。しかし、すでにPHPStanが提供しているルールもいくつかあります。その一つが phpstan/phpstan-strict-rules です。

導入方法は

かんたんで、 composer require --dev phpstan/phpstan-strict-rules でinstallしてから、 PHPStanの設定ファイル(ex. phpstan.neon)に加える。

すべてのチェック項目を加えたい場合はincludesに追加すれば良い。

parameters:
  level: max
  paths:
    - src
    - tests
  ignoreErrors:
    - '#Dynamic call to static method PHPUnit\\Framework\\.*#'
includes:
  - vendor/phpstan/phpstan-phpunit/extension.neon
  - vendor/phpstan/phpstan-strict-rules/rules.neon

個別の項目を一つずつ加えたい場合は、servicesに追加すれば良い。

services:
  -
    class: PHPStan\Rules\DisallowedConstructs\DisallowedEmptyRule
    tags:
      - phpstan.rules.rule
  -
    class: PHPStan\Rules\StrictCalls\StrictFunctionCallsRule
    tags:
      - phpstan.rules.rule

どういう指摘をしてくれるか

たとえば、 empty() を使用したコードが有る場合は次のように指摘される

Construct empty() is not allowed. Use more strict comparison

あるいは、in_array() の第三引数をtrueにしていない場合はこう

Call to function in_array() requires parameter #3 to be set.

その他指摘される項目は、GitHubのREADME.mdにずらっと書いてあるので、自分たちにとって全て取り入れられるのか・一部のみなのかを判断するとよい。

導入の注意点

すべてのルールを適用すると、phpstan/phpstan-phpunitを入れていても、PHPUnitを用いたテストコードが次のように怒られてしまいます。

/Users/kazukihigashiguchi/src/github.com/baseinc/yellbank-php/tests/Api/ClientTest.php:54:Dynamic call to static method PHPUnit\Framework\Assert::assertSame().
/Users/kazukihigashiguchi/src/github.com/baseinc/yellbank-php/tests/Api/ClientTest.php:155:Dynamic call to static method PHPUnit\Framework\Assert::assertSame().
/Users/kazukihigashiguchi/src/github.com/baseinc/yellbank-php/tests/Api/ClientTest.php:191:Dynamic call to static method PHPUnit\Framework\Assert::fail().
/Users/kazukihigashiguchi/src/github.com/baseinc/yellbank-php/tests/Api/ClientTest.php:233:Dynamic call to static method PHPUnit\Framework\Assert::assertSame().

こういうのは、Issueにもある通り、ignore patternに追加することで対応しましょう。

  ignoreErrors:
    - '#Dynamic call to static method PHPUnit\\Framework\\.*#'

開発チームについて考えてアジャイルについて行動してみた | 書評『みんなでアジャイル ――変化に対応できる顧客中心組織のつくりかた』

TL;DR

  • 『みんなでアジャイル ――変化に対応できる顧客中心組織のつくりかた』 を読んで、実際に現実の(渡しがいる)チームの問題について考えた
  • 自分は、アジャイルのプラクティスについてなんとなく知っているが、実際に「アジャイルでのチーム開発」をやったことがない人間だった
  • そんな人間にとって、「アジャイルとはそもそも何なのか?何だったのか?どうすれば現実にうまく適用できるのか?」を考えるきっかけをくれる良書だった。

『みんなでアジャイル ――変化に対応できる顧客中心組織のつくりかた』とは

この書籍は、2020年03月に、日本語訳版が発売された。この書籍の訳者である ryuzee さんは、fukabori.fm: 32. みんなでアジャイル w/ ryuzeeに出演して直接この書籍の内容について解説してくださっている。

www.oreilly.co.jp

著者の、Matt LeMayという方は、Sudden Compassの共同設立者兼パートナーであり、同社は、 「OUR CLIENTS」にある通り、American ExpressやSpotifyなどの組織の顧客中心主義を実践する支援をされている。

f:id:khigashigashi:20200510052153p:plain

www.suddencompass.com

そんな著者の書いたこの書籍は次のように説明されている。

本書では、「顧客から始める」「早期から頻繁にコラボレーションする」「不確実性を計画する」をアジャイルの3つの原則とし、この原則を組織で共有し実践していく方法とその課題を解説します。原則を素早く実現するためのアイデアや方法、原則が適用できているかを確認する方法とうまくいかない場合の対応法などを紹介します。 アジャイルの原則を理解してゴールを定め(目標)、自分たちにあったアジャイルラクティスを見つけ(方法)、現実的な成果をもたらしているかを計測し(成果)、これらを見直しながら繰り返すことでアジャイルを継続的に強化する方法を解説します。またワークシートを用意しており、自分の環境に照らし合わせて考えることができます。 https://www.oreilly.co.jp/books/9784873119090/

つまり、この書籍は アジャイルの原則を組織で共有しいかに実践するか、それに至るまで・実践する際の課題について解説したものだ。読者として読んだ感想としては、「プランニングポーカー」とか「イテレーション」とか、個別具体的な方法というよりかは、その考え方・仕事の仕方としてのアジャイルをどう捉えて実践するかに焦点を当てている。

また、リーン開発・デザイン思考との比較についての言及しているため、近い概念との境界・輪郭をよりはっきりさせてくれる。

リーン開発やデザイ ン思考は事業企画フェーズ、アジャイルは設計から実装を経てのリリースまで のフェーズ、DevOpsは実装後期から運用のフェーズ。もちろんオーバーラップ する時期もあるが、大きく分けるとこのようなフェーズごとのアプローチと解釈できよう。 本書では、これらに別の解釈も加える。リーン開発は効率性(投資効果の最大 化)を目的とし、アジャイルは迅速性を高めることを目的とし、デザイン思考は ユーザービリティ(使い勝手)を目的とする、と筆者は定義する。

「みんなでアジャイル」 まえ書き

自分は、何らかチーム開発のプラクティスを実践することについて、たしか2年前くらいから考えていた。しかし、アジャイルでのチーム開発をやったことがある人達の話を聞くと、「うまくいった」・「うまくいかなかった」という両極端の意見を聞くことが多く、とても戸惑っていた。

そもそも「アジャイルはうまくいく・いかない」という次元の手法の話なんだっけっていう疑問を持っていたが、それに対してうまく理解を落とし込む機会がなかった。しかし、この書籍は私のこの疑問に明快な言葉で説明してくれました。

以降、書籍を読んで自分が引っかかったこと・学んだこと・考えたことを記していきます。

アジャイルソフトウェア開発宣言

アジャイルムーブメントの始まりは、History: The Agile Manifestoにて知ることができる。

On February 11-13, 2001, at The Lodge at Snowbird ski resort in the Wasatch mountains of Utah, seventeen people met to talk, ski, relax, and try to find common ground—and of course, to eat.

スノーバードというリゾート地にて集まった17人が合意し、共有しようとした価値観は次の短い文章に集約されている。

f:id:khigashigashi:20200510052218p:plain

agilemanifesto.org

ここには一切の手法・ツール・プラクティスのことが言及されていない。本書籍でもこのことを重要だと主張しており、アジャイルを単に「手法」と呼称していることについて次にように語っている。

 そのため私は、アジャイルが単に「手法」と呼ばれているのを聞くたびに少し 苛立つ。そう、たくさんの手法があるのだ。前述したスクラムやエクストリー ムプログラミング、クリスタルもそうだし、最近開発されたSAFeやLeSSもそ うだ。アジャイルの価値観を実践に移すための青写真を提供してくれる手法は 多い。だが、アジャイルをプロセスやツールとして定義することがなぜ的外れ なのか、それを理解するためにアジャイルソフトウェア開発宣言の 208 文字を そこまで目を凝らして見る必要はない。

1.1 ムーブメントとしてのアジャイルを理解する

ムーブメントとしてのアジャイル

実際に、読んでいくと、この書籍は、アジャイルをつぎの2つに分類している

その上で、それらを連携させる「ムーブメントとしてのアジャイル」を提唱している。手法としてのアジャイルラクティスに重点がおかれており、マインドとしてのアジャイルマインドセットに重点が置かれる。それに対して、ムーブメントとしてのアジャイルとは、マインドセットとプラクティスは容赦なくつなっがっている状態を目指すものである。

ムーブメントとしてのアジャイルでは、アジャイルの原則とプラクティスを明確にし、自分たちにどう適用するかを積極的に決定する、という役割が自身に求められる。言い換えると、他人が決めた価値・原則・プラクティスに従うという受動的な役割ではないことを、この言葉は示している。

そのため、思考と行動の双方に高い基準が求められ、新しい考え方と新しい働き方を必要とする。

脱線 概念が浸透していくプロセス

昨今、「流行っている」DDDなどを見ても同様に、戦術面にフォーカスがあたっている、いわゆるプラクティスに重点が置かれている状態から、(月並みな表現になるが)ビジネスに向けることといったマインドセットに重点が置かれるべきだと主張され、それらは一気通貫してマインドセットとプラクティスがつながるムーブメントとして捉える認識が広がっていく、といったふうな流れがあるように感じる。

なにか概念が広がっていくプロセスというのは、HOW的なプラクティスが注目され、なぜやるのか・その理由はなにかといったWHYに注目するように揺り戻され、最終的にWHY-HOWが一連の思考の枠組として理解される流れを踏んでいくものなのかもしれない。

スクラムやXP、LeSS、SAFeとアジャイル

正直、この辺がそれぞれお互いにどのような輪郭を持って捉えるべきなのか時系列がいまいちわかっていなかったが、歴史的経緯について非常に理解できた。

アジャイルの誕生は、たとえば印象派の芸術運動のように、何人かの実践者が独立しながら同時進行するイノベーションのもとで生まれたものである。書籍内に紹介されている通り、かんたんな年表にすると、次のように整理できる。

f:id:khigashigashi:20200510052247p:plain

自分たちの北極星を見つけること

この表現は、自分が所属企業のチームで実践するに当たり、非常に気に入って使わせていただいている。自分たちの北極星という表現は、この書籍の訳者である ryuzee さんが出演したpodcast fukabori.fm: 32. みんなでアジャイル w/ ryuzeeにて肉声で説明いただいたのが一番分かりやすかったが、自分は、季節が移り変わっても北極星の位置が変わらないように、状況が変わり時間が立っても変わらない自分たちが大事にしたい・目指したい目標と理解した。

言われてみれば言語フレームワーク等の技術選定をする際には至極当たり前の思考プロセスとして、「なにか課題なのか?」・「どのようなソフトウェアを目指したいのか?」という問いが一番最初に来るのに、それがチーム論・組織論では例外なはずがない。

書籍では、自分たちの現状を厳しくみることの重要性や、仕事のやり方をちょっと変える参考の方法にするだけではあまり効果がない、ことについて指摘している。

成功するアジャイルの適用は、常に厳しく正直に現状を見ることから始まる。 何がうまくいっていて、何がうまくいっていないのか。アジャイルを今の仕事 のやり方をちょっと変えるだけのことと思っているなら、アジャイルから得られ るメリットもちょっとだけになるだろう。今のやり方を選んだ元になっている現

2章 自分たちの北極星を見つける

これらについて フレームワークの罠 と表現しているが、それを回避するためには次の2つのステップを経る必要を説いている。

  1. チームや組織がどうなりたいか、そして何がそれを妨げているかを正直に 厳しく見ること
  2. チームや組織が掲げられたゴールにたどり着くために従う必要のあるア ジャイルの原則を選ぶ(必要なら特殊化する)こと

これは、アジャイル云々関係なく、物事全般的に言えることであり、あらゆる場所で思い出していきたい。

脱線:自分たちの北極星はどこだろう

もともと、自分自身この書籍を手にとった理由として、開発チームとしてどこに向かえばより「生き生き」と仕事できるのか、という点について漠然とした課題があった背景がある。 どういう景色かというと、だいたい全体で150人くらいの規模のサービス会社の50人弱のエンジニア組織の中で、他チームからの協力も含めピザを囲んで食べれる規模の小さなチームにいる。

そこでは、良くも悪くも個人技の集積で絶妙なバランスで顧客に対する成果となるサービス・機能を提供・運用できていた。しかし、プロジェクトの関係者の増加・期間の大きいもの、並行で考える開発プロジェクトの増加、マーケティング施策の実現やCustomer Supportからの問い合わせに迅速に対応していくこと、などが求められる中で、個人の力の集積ではなくチームとしてどう成果を出していけるか、を考える必要を感じていた。

「遠くに行くにはみんなでいけ」的な言葉があるが、月並みな表現をすると「俺たち今日もいい仕事ができるな!ちゃんと世界に価値を発揮できてるぞ!」 と言いたいぞっていう具合だった。

どうやっていきたい?っていうの実際にzoomで話したりした結果、この課題感が「アジャイル的な表現」なのかと問われると、正直わからないが、次のようになった。

1. メンバー: 個々人の達成感
2. チーム全体: 関係各所との「約束」を守る

そのために、進行において優先度高く取り組んでいきたいことは以下です。

個々人の納得感と達成感のあるマイルストーン と 重要マイルストーンに対するブレークダウンと見積もりの継続的評価

表現としては、「顧客中心」な表現とは言えないが、サービスを提供する顧客にしっかり価値を提供する、ことに対してブレークダウンしたお気持ちではある(※ 「北極星は変わらない」が、チーム内の宣言文は、より適した形でリライトされ続ける気がする)。

スプリントでの作業

アジャイル手法全体を一つのプラクティスに集約する一つの表現を書籍では、「タイムボックス化したイテレーションで作業すること」と表現していた。「スプリント」という表現がより聞き馴染みがあるかもしれない。

顧客ニーズに基づいて作業に優先順位をつけ一定期間のスプリントを実施する。スプリント後にフィードバックを得て、次のスプリントのて作業に優先順位をつける。短い期間で顧客のフィードバックを受けるフィードバックループを回すことが顧客中心主義となるヒントだ。

ここでいう「顧客」って誰になるんだっけ、を考える

自分はサービス会社にいるので、その文脈での確認になるが、自分自身のいる開発チームの動き方は大きく2種類のことを同時並行で行っている。

  1. 新規機能の開発
  2. 既存機能の改善・保守

「2. 既存機能の改善・保守」については、例えば2weekのsprintで顧客に届ける価値はそのまま本番にリリースして、顧客に使ってもらい、その後の反応をフィードバックとして得られれば次の計画に活かせるのでイメージが付きやすい。

「1. 新規機能の開発」については、2weekにおける顧客フィードバックを得るための顧客は、プロジェクトオーナーということだろうか。実際にプロジェクトオーナーと動くものを持って話すと「この機能はこうしたい」といったフィードバックが得られるので、そういうことなんだよな?

更に具体的なタスクレベルにブレークダウンしたときに、例えば、2weekで「backendのAPIを用意します」・「infra環境を作ります」といった場合は?それについてもプロジェクトオーナーと合意し、その成果について確認・説明すると、プロジェクト状況や「こういうことは対応してますか?」といった会話ができそうか。

実際の組織における課題「組織重力の3つの法則 」

ムーブメントとしてのアジャイルを実践するにしても、現実の組織は、変えることが難しいことがある。「従来の仕事の仕方」に人をつなぎとめておく力は、重力と同じように広範囲に及ぶ。筆者はそれを「組織重力の3つの法則」と呼んでいる。普段漠然と感じて考えていたことが明快な言葉で説明されていて腹落ちの度合いがすごかった。具体的には次の3つをあげている。

● 組織の個人は、日々の責任ややる気を伴わない場合、顧客対応を避ける。 ● 組織の個人は、自分のチームやサイロの居心地のよさのなかでいちばん簡単に完了できる仕事を優先する。 ● 進行中のプロジェクトは、プロジェクトを承認した最上位者の決定がない限り続く。

1章「アジャイル」とは何か?なぜ重要なのか

1. 組織の個人は、日々の責任ややる気を伴わない場合、顧客対応を避ける。

これについては、個人レベルでの組織レベルでも、心当たりがある事象だと感じた。自分のKPIや成果に焦点がいってしまい問い合わせ対応などの顧客対応の優先度が下がってしまう事象は、様々な要因で発生するように見える。

もちろん自身のKPIなるものを「全く達成できませんでした」とは言えないが、一方でサービス会社だと24時間365日サービスは顧客に使われ、そのサービスに何らか不具合や不明点があった場合には、顧客へ価値が届けられず、サービス業態によってはその人の重要な生計の流れが止まってしまう可能性がある。

「日々の責任」・「やる気」というバラメータは、それらの優先度を上げるかどうかについて、大きなバラメータになるし、組織・チームレベルでもそこに対する評価に対して一貫性が取れていない状態が危険っていうのはそうだなと。

サービス会社における「顧客対応」はタイムボックス内の計画でどう扱う?

これって、スプリント計画には当初入ってない "差し込み" タスクになると思うが、これについてどのように扱っていることが多いのだろうか。これを単に "差し込み" として扱って、スプリントレビューで「なんか成果出てなくない?」ってなるのは明らかに「顧客中心主義」の価値観とは違うよね。

これをどう扱うと、顧客対応に対して積極的であり続けて、かつチームとしてもそれを「いいじゃん!かっこいいなお前!価値出したな!」とわいわいできるんだろうか、というところに興味がある。

現時点の扱い方では、顧客対応ができた場合は、その難易度に応じた「ストーリーポイント」をつけて、当該スプリントで実施したissueに追加する、というふうにして、カンバン上で可視化している。

2. 組織の個人は、自分のチームやサイロの居心地のよさのなかでいちばん簡単に完了できる仕事を優先する。

うーあるわ〜(激しく首を縦に振りながら)だった。自分がすぐにイメージしたのは、例えばバックエンド領域のチームが、その他のレイヤ(フロントエンド・インフラ)を扱うために調整が必要なことをさけてむりやりバックエンドのレイヤだけで完結しようとする事象を思い立った(例えば、監視SaaSを使えばより運用上の有益なところに調整を嫌ってバックエンドの実装でそれを代用したり)。

後は、純粋に利害関係者が増えるとそれだけ多種多様な意見が出て、ソフトウェア開発者があんまり好きじゃないような組織間調整的なものが発生したりすると、億劫になってさけちゃいますよね。

後者については組織の文化や目指す先やプロダクトについての共通価値観の情勢などが影響するのだろうけど、前者はこの重量があるからこそ、可能な限り機能横断的である方がいいよな、と思っている。

後記

アジャイルをやろうとしているにしても、やらないにしても、この書籍は非常に一般的な組織的な課題について語っているので、自分の景色を分析するための思考の枠組みとして、いい材料になった。

この書籍を読んだからこそ、「やっぱりチーム課題を考えよう」と重い腰を上げるきっかけになった。なんらか漠然とした課題・もやもや感を感じている状況の方は、『みんなでアジャイル ――変化に対応できる顧客中心組織のつくりかた』、ぜひ手に取ることをおすすめします。

PSR-3とCakePHPから見るNull Object Pattern

TL;DR

背景

CakePHP 4.0 の変更点を追っている際に、次のような記述があった。

Cake\Database\Connection::setLogger() no longer accepts null to disable logging. Instead pass an instance of Psr\Log\NullLogger to disable logging.

https://book.cakephp.org/4/en/appendices/4-0-migration-guide.html

これまで、nullを渡してロギングを無効にする方法をとっていたが、Psr\Log\NullLoggerを渡して無効化するように案内されていた。

NullXxxと見ると、NullObjectパターンが想起されたので、Psr\Log\NullLoggerを入り口として、PSR-3Null Object Pattternの説明を試みる。

PSR

そもそも、PSRとは、PHP Standards Recommendations1の略で、PHP-FIG2というコミュニティ団体による、コミュニティにおいて(ある程度)影響が大きい標準化勧告である。PHP開発に馴染みのある方であれば、普段良く使用しているフレームワークやライブラリが、この勧告によって提示されているInterfaceをサポートしていたりする。AcceptされたものからDraft段階のものを含めてNo.19まで提案されており、冒頭に上げたCakePHPもPSR-15やPSR-16などをサポートしているフレームワークの一つです。

PSR-3 Logger Interface

その中の、No.3であるPSR-33は、すでにAcceptされているPSRで、Logger Interfaceを規定する。つまり、ロガーライブラリの標準となる共通インターフェースを定めようというものになる。

www.php-fig.org

The main goal is to allow libraries to receive a Psr\Log\LoggerInterface object and write logs to it in a simple and universal way.

基本的なインターフェースとして、RFC54254で定義された8つのレベルにログを書き込むためのメソッドを公開する。そして、9つめのメソッドとして、ログレベルを第一引数として受け入れるlogを公開する。

<?php
namespace Psr\Log;

interface LoggerInterface
{
    public function emergency($message, array $context = array());
    public function alert($message, array $context = array());
    public function critical($message, array $context = array());
    public function error($message, array $context = array());
    public function warning($message, array $context = array());
    public function notice($message, array $context = array());
    public function info($message, array $context = array());
    public function debug($message, array $context = array());
    /**
     * @throws \Psr\Log\InvalidArgumentException
     */
    public function log($level, $message, array $context = array());
}

https://github.com/php-fig/log/blob/4aa23b5e211a712047847857e5a3c330a263feea/Psr/Log/LoggerInterface.php#L3

logメソッドのPHPDoc5を見ると明らかですが、ログレベルがRFC5425[^4]で規定されたレベル以外の場合は、\Psr\Log\InvalidArgumentExceptionを返すことが指定されている。

Psr\Log\NullLogger

この、PSR-3[^3]のLogger Interfaceの「1.4 Helper classes and interfaces[^5]」にて、Psr\Log\NullLoggerがInterfaceと一緒に提供されている点について説明されている。

The Psr\Log\NullLogger is provided together with the interface. It MAY be used by users of the interface to provide a fall-back “black hole” implementation if no logger is given to them. However, conditional logging may be a better approach if context data creation is expensive.

フォールバックの実装を提供するために使用することができるblack hole実装となっています。

<?php

namespace Psr\Log;

/**
 * This Logger can be used to avoid conditional log calls.
 *
 * Logging should always be optional, and if no logger is provided to your
 * library creating a NullLogger instance to have something to throw logs at
 * is a good way to avoid littering your code with `if ($this->logger) { }`
 * blocks.
 */
class NullLogger extends AbstractLogger
{
    /**
     * Logs with an arbitrary level.
     *
     * @param mixed  $level
     * @param string $message
     * @param array  $context
     *
     * @return void
     *
     * @throws \Psr\Log\InvalidArgumentException
     */
    public function log($level, $message, array $context = array())
    {
        // noop
    }
}

github.com

継承しているPsr\Log\AbstractLoggerにて、8つのログレベルの公開メソッドが実装されているため、LoggerInterfaceを満たすものとなっています。

フレームワークにおける使用例

このPsr\Log\NullLoggerの使用例をCakePHPの内部実装で見てみます。PSR-3の\Psr\Log\LoggerInterfaceへの依存へ変更しているPRから、見ていきます。

-    public function setLogger(?QueryLogger $logger)
+    public function setLogger(LoggerInterface $logger)

github.com

CakePHP3.x以前では、ロギングを無効にするために、Cake\Database\Connection::setLogger()nullを渡す方法をとっていました。

        $connection = ConnectionManager::get('test');
        $connection->setLogger(null); // 無効

この場合、以降ロガーを呼び出す際に、if ($this->_logger === null)といった条件分岐などを使用してチェックするようなコードを用意することになります。

それに対して、NullLoggerを利用する場合は、そのようなチェックをせずとも、同一のインターフェースを利用することができます。

        $connection = ConnectionManager::get('test');
        $connection->setLogger(new NullLogger()); // 無効

このようなメリットを享受できるコード設計パターンは、Null Object Patternというパターンの特徴として見ることができます。

Null Object Pattern

Null Object Pattern6とは、オブジェクト指向言語おけるパターンで、参照される値がないか、定義されたニュートラル(null)動作を持つオブジェクトを指します。「プログラムデザインのためのパターン言語―Pattern Languages of Program Design選集7」や、「リファクタリング 既存のコードを安全に改善する(第2版)8」にて、本として初めて紹介されたパターンとなります。(※ 「リファクタリング 既存のコードを安全に改善する(第2版)[^8]」では、Special Case Patternとも表現されています。)

これは、GoF (Gang of Four)の23個のデザイン・パターンではありませんが、現代のプログラミングの現場において広く知られる「デザイン・パターン」といえます。

先程のCakePHPの例では、次のようなクラス関係を表現しています。

f:id:khigashigashi:20200102191704p:plain

デザイン・パターンは、オブジェクト指向ソフトウェアを設計する際の経験を記録、カタログ化したものです。繰り返し現れる構造をパターンとしてまとめたものです。「問題」・「解決」・「コンテクスト」の大きく3つの要素をそのパターンから見出すことができます。

まとめ

PSR-3の標準的なインターフェースとともに提供されているクラスから、NullObjectというパターンを紹介しました。


  1. PHP Standards Recommendations (https://www.php-fig.org/psr/)

  2. PHP-FIG (https://www.php-fig.org/)

  3. PSR-3 (https://www.php-fig.org/psr/psr-3/)

  4. RFC5425 (https://tools.ietf.org/html/rfc5424)

  5. phpDocumentor (https://www.phpdoc.org/)

  6. Null Object Pattern (https://en.m.wikipedia.org/wiki/Null_object_pattern)

  7. プログラムデザインのためのパターン言語―Pattern Languages of Program Design選集 (https://www.amazon.co.jp/dp/4797314397)

  8. リファクタリング 既存のコードを安全に改善する(第2版) (https://www.amazon.co.jp/dp/B0827R4BDW)