UZABASE Tech Blog

〜迷ったら挑戦する道を選ぶ〜 株式会社ユーザベースの技術チームブログです。

Smalltalkで『オブジェクト指向設計実践ガイド』の「第2章 単一責任のクラスを設計する」をハンズオンしたら快適で楽しかった

今日は。 SPEEDA を開発している濱口です。

前回はテスト駆動開発(TDD)を習得するためのハンズオンを行いましたが、今回はオブジェクト指向設計(OOD)です。

SPEEDA は複雑なビジネスロジックから成っており、開発では複雑で多くのコミュニケーションが発生します。
開発チームでは gauge などのツールを使い e2e テストが仕様を語るようにしたり、ペアプロを徹底したり、すべてが流れるようにしたり...等々の様々な工夫を行っています。
コードや設計もコミュニケーションのひとつだと捉えると、実際に開発で発生するコニュニケーションの比重の中では大きな割合を占めるものだということを感じています。
SPEEDA 開発者は優れた OOD をすることが必要となっていると思います。

有意義なハンズオンを、より有意義にしたい

今回も前回と同様、 Smalltalk でやります。
環境も Pharo (イメージのテンプレートは Pharo 7.0 )です。
『オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方』は、副題にもある通りコードは Ruby で書かれていますが、プログラミング言語翻訳プロセスを介することで、ハンズオンがアクティブ・ラーニング化することを狙っています。

古くてあたらしい言語(環境)、 Smalltalk にダイブしたい

また、 Smalltalk では自分のコードと既存のコードは全く区別無く閲覧、編集が可能であるため、よいお手本の宝庫としてのクラスライブラリが本当に身近にあります。
シンプルで完成度の高い直感的な IDE により、クラスライブラリを快適に回遊しながら、その中でアプリケーションをつくることができます。
探索的プログラミングを助けるツールとしては、 System Browser 、 Finder 、 Playground をよく使いました。
また、コードは Iceberg でバージョン管理します。(プロジェクト作成時、リポジトリ一覧で「No Project Found」というステータスに出くわした場合、『Manage Your Code with Iceberg』の「1.4 Add a new project to Iceberg」以降の手順で解決できました。)

f:id:yhamaro:20191209054645p:plain
Let's 写経!

Smalltalkでは、やはりわりと忠実な写経が可能でした

わりと忠実な写経は以下です。(プログラムの進化の過程が見えるようにコミットを分けました)

「やはり」と書いたのは、言語として近いことはなんとなく認識していて、書籍の表現でもメソッドの実行を「メッセージを送る」と表現していたためです。
ただ、もちろん違いはありました。 途中、特に気になった差異をいくつか挙げていきます。

全てのインスタンス変数を隠蔽せよ、ということ

本書の「2.3 変更を歓迎するコードを書く」の"データではなく、振る舞いに依存する"という部分の結論ではそうなっています。

変数はそれらを定義しているクラスからでさえも隠蔽しましょう。
『オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方』

この点において、Smalltalk開発者では意見が分かれているようです。

Smalltalk開発者の中には、アクセサを通してのみインスタンス変数にアクセスすることを好む人もいます。このプラクティスは価値あるものですが、クラスのインターフェースを乱雑にする側面があり、下手をすると外部の世界にプライベートな状態を晒すことにもなります。
Pharo by Example(日本語版)

『ケント・ベックのSmalltalkベストプラクティス・パターン』(SBPP)でも以下の相反するパターンが取り上げられています。

  • Direct Variable Access パターン
  • Indirect Variable Access パターン

一方が読みやすさを、一方が柔軟性を実現するとしています。
この問題が一筋縄ではいかないことを示すエピソードも添えられています。

初めて Smalltalk を始めた時(テクトロニクス社にいた80年代半ばのことですが)、状態のアクセスについて熱い議論を戦わせていました。両サイドに派閥ができ、自分勝手な主張を繰り広げたものです(研究所はあまり好きではないですか?)。

たいていは Indirect Variable Access を支持するグループが、 Smalltalk コミュニティの心をつかんでいました。なぜなら、大手のトレーニング会社が間接アクセスを教えていたからだと思います。この問題は「直接アクセスは悪で、間接アクセスは善である」といった具合に、非常に単純化されてしまっているきらいがあります。

...(中略)...
私は間接アクセスを主張するクライアントと仕事をしました。プロのプログラマはどんなスタイルでも思いのままにかけるものです。私は、良き一兵卒となって Getting Method と Setting Method をひたすら書きました。

そのクライアントとの仕事が終わったあと、家でちょっとしたコードを書いてみました。前の仕事と比べてそのコードがなんと読みやすかったことでしょう。スタイルの違いはまさに、直接アクセスか間接アクセスかということにあったのです。

『ケント・ベックのSmalltalkベストプラクティス・パターン』

ただ、ひとつの基準も示されています。

もし、継承のニーズがある場合には、 Indirect Variable Access を用いるべきでしょう。
次に使う人はきっと感謝します。

『ケント・ベックのSmalltalkベストプラクティス・パターン』

結論として、今やっているのはハンズオンなので読みやすさを重視すべきですし、未だ継承の概念が出てきていないため、一旦 Direct Variable Access パターンを採用しています。

Smalltalk には Ruby の Struct クラスにあたるものがない

Gear クラスをつくっている最中に Wheel の概念が必要になるくだりがあります。
そこでの想定は以下となっています。

もし Wheel クラスを別につくれる状況にあるならば、おそらくつくるべきでしょう。
しかしいまは、一時的でなくずっと使い続けるような、アプリケーション全体で利用可能な新しいクラスはつくらないと選択したとしましょう。
何らかの制約が課されているのかもしれませんし、もしくは、どこに向かっているのかまったくわからなく、考えが変わる恐れがあるので、 だれかが依存をつくってしまいかねない新しいクラスはつくりたくない、といった状況が考えられます。
『オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方』

Ruby では上記の想定のなかで、 Struct クラスを用います。

A Struct is a convenient way to bundle a number of attributes together, using accessor methods, without having to write an explicit class.
(明示的にクラスを書くことなく、いくつもの属性を1カ所に束ねるための便利な方法。アクセサメソッドが用いられる。)
Rubyの公式ドキュメント

これは上記の想定に完璧に合った用途のもので、さらに、

Wheel を Gear 内に埋め込むことで、 Wheel は Gear のコンテキストにおいてのみ存在すると設計者が想定していることがわかります。
『オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方』

というように設計意図を伝えることもします。
私は上記の想定のなかで同様の構造で設計意図を伝えられる方法を探しましたが、結論から言えば見つけることが出来ませんでした。
私の試した方法は以下です。

Dictionary を使う

これが一番、 Struct での実現方法に近いと思います。

{ #category : #calculating }
RevealingReferences >> diameters [
    ^ wheels
        collect: [ :wheel | (wheel at: #rim) + ((wheel at: #tire) * 2) ]
]

{ #category : #private }
RevealingReferences >> setData: aData [
    wheels := aData
        collect: [ :cell | 
            {(#rim -> (cell at: 1)).
            (#tire -> (cell at: 2))} asDictionary ]
]

Struct の機能である「明示的にクラスを書くことなく、いくつもの属性を1カ所に束ねる」ことを実現しています。 一方、「アクセサメソッドが用いられる」ことに関しては diameters メソッドの中でアクセサでダイレクトにではなく、キーアクセスになってしてしまっているので微妙です。(ブロッククロージャを用いたやり方もできそうですが、これと同じ問題に遭遇すると考え試していません。)

Wheel クラスをつくっちゃう

元も子もないですが。。

{ #category : #calculating }
RevealingReferences >> diameters [
    ^ wheels collect: [ :wheel | wheel diameter ]
]

{ #category : #private }
RevealingReferences >> setData: aData [
    wheels := aData
        collect: [ :cell | Wheel rim: (cell at: 1) tire: (cell at: 2) ]
]

しかし、 diameters メソッドが書籍のコードと同じかたちになるので、ハンズオンでは一旦この方法を取っています。

まとめ

前回同様、違いはありましたが、このハンズオンが意図する本筋から外れるものはなく、ステップバイステップで進めていけました。(ちなみにちゃんと TDD でやっています。)
個人的には Pharo が手に馴染んで、クラスライブラリに関する知識も少しずつ増えてきた感触があります。 この調子で第9章までやりきりたいと思います。

ElixirライブラリのFlowを使ってAPIを優しく呼び出す

こんにちは。SPEEDA開発チームの上村です。

先日同僚が書いたElixirのコードを眺めていた際に、とあるバッチ処理内でback-pressureを用いてAPIサーバを優しく呼び出している処理を見かけました。 そのコード内で用いられていたライブラリを調べてみるとなかなかに面白かったので折角なので今回まとめてみました。

本エントリでは、APIサーバを優しく呼び出すことを「APIサーバが過負荷にならないくらいの負荷で呼び出す」と定義し、それを実現する方法の1つとしてElixir製のライブラリであるFlowを用いたback-pressureの手法について述べていきます。

Flowとは

FlowはElixir製のコレクションを並列分散処理することに特化したライブラリです。 Flowを用いて、あるテキストファイルに含まれる単語の出現頻度を数え上げるコードを書くと以下のようになります。

File.stream!("path/to/some/file")
|> Flow.from_enumerable()
|> Flow.flat_map(&String.split(&1, " "))
|> Flow.partition()
|> Flow.reduce(fn -> %{} end, fn word, acc ->
  Map.update(acc, word, 1, & &1 + 1)
end)
|> Enum.to_list()

細かなFlowのAPIについて特に知らなくても、コードを見るとこのコードが何をしたいのかの雰囲気が伝わってくると思います。

Flowは正確にはback-pressureをよしなに扱えるGenStageラッパーのことを指し、コレクション操作に特化したものとなっています (GenStageについては後述)。 また、GenStageのラッパーには、MessageQueueをGenStageに連携させることに特化しているBroadwayも存在します。

GenStageとは

GenStageのリポジトリを見ると、GenStageはproducerとconsumer間でイベントを交換するための仕様と述べられています。 イベントと言われるとちょっとふわっとしている印象を受けますが、データ処理のトリガーというニュアンスで捉えても大丈夫だと思います。

GenStageでは、イベントの処理を行う単位をステージと呼び、ステージはproducer、producer_consumer、consumerのいずれかに属するものとして扱います。 これらのステージを組み合わせてパイプラインを構成し、イベントを処理する仕組みを、GenStageは提供します。

各ステージの役割を把握するために下記のようなパイプラインを考えてみます。

[A] -> [B] -> [C]

この例において、Aはproducer、Bはproducer_consumer、Cはconsumerに対応します。 producer/producer_consumer/consumerはGenStageでは以下のような役割を持つものと説明されています。

  • producerは、イベントの送り元であるため、イベントをconsumerへと受け渡します(つまり、sourceとなります)
  • producerは、イベントの受け手であるため、イベントをprocuerから受け取ります(つまり、sinkとなります)
  • producer_consumerは、producerでもありconsumerでもあります(つまり、前後のステージからの見方次第で異なる役割に見えることになります)

イベントを処理するためにはconsumerがproducerを購読しなければなりません。 そのため、producerはconsumerからの催促を待ち受けていると表現されたりもします。

パイプライン内の各ステージにイベントが伝搬する際のトリガーは、consumerの役割を持つステージがproducerの役割を持つステージに対して処理対象のイベントの需要がどれくらいあるかを伝えることです。 producerはその需要ときっちり同じ量のイベントをconsumerに対して送出することをきっかけに、先のパイプラインのフローのようなイベントの処理が行われます。 このようなconsumer側が処理可能なイベント量のみをproducerに伝えることで処理中のイベント量を管理するような方法は一般的にback-pressureと呼ばれています。

GenStageにおいて、back-pressureはconsumerに対して :max_demand:min_demand のオプションを設定することでコントロールされます。 consumerは:max_demand で対応するproducerと連結しているフロー内で処理されるイベントの最大量を設定し、:max_demand で対応するproducerと連結しているフロー内に存在するイベントの数が少なくなりすぎないように、フロー内にイベントを補充する閾値を設定します。 つまり、パイプラインの起動時には :max_demand 個のイベントがproducer Aから送出され、producer Aを購読するconsumer Bがイベントを処理します。その結果、パイプライン内のイベントの総数が :min_demand 個になったら、:max_demand - :min_demand 個のイベントの需要を、consumer Bがprocucer Aに対して新たに伝えて処理を続けることになります。

:max_demand:min_demandのパラメータのイメージは下記のようになります。 (:max_demand=3, :min_demand=1でconsumer Bがproducer Aを購読している場合を想定しています)

まず、:max_demand相当のイベントが送出されます。

[A]-[B]
   -> 
   -> :max_demand個
   ->
(->が各イベントに対応。:max_demandの数だけイベントが存在)

処理が進むと、パイプラインのイベントは:min_demand個まで捌けた状態になります。

[A]-[B]
   -> :min_demand個 

パイプライン内のイベントの総量が減ったので、:max_demand - :mix_demand個のイベントの送出需要を伝えて、パイプライン内のイベント総量が増えた状態にします。つまり、最初の状態に近しい状態になります。

[A]-[B]
   -> 
   -> :max_demand個
   ->

以上のようなイメージで:max_demand:min_demandのパラメータは作用します。

ちなみに、FlowはGenStageがベースとなっているため、このようなback-pressureの仕組みが背後で動いていることを理解しておくとパラメータチューニングの際に役に立ちます。

Flowの利用例

Flowを使ってAPIを優しく呼び出すサンプルコードを示します。 利用するAPIはランダムに200のステータスコードか503のステータスコードを返します。 サンプルコード内では、503のステータスコードを受け取った際は最大3回リトライする処理を簡素ながらリトライ処理として実装しています。

サンプルコード

こちらに置いてあります。 リポジトリ内のディレクトリツリーは以下のようになっています。

.
├── fragile_api
└── fragile_api_client

ランダムにステータスコード200かステータスコード503をレスポンスとして返すAPI(fragile_api)、そのAPIをFlowを使って呼び出すクライアント(fragile_api_client)の2つのディレクトリから構成されます。 本エントリがElixir関連のものなので、折角なのでfragile_apiはElixirのWebフレームワーク群の中で著名なPhoenixを用いて実装しました。

Flowの依存関係

fragile_api_client内のmix.exsに以下のような依存関係を追加しています。

defp deps do
  [
    {:flow, "~> 0.15.0"}
  ]
end

Flowを用いたコード例

Flowを利用しているコードを先述のサンプルコード(fragile_api_client/lib/fragile_api_client.ex )から抜粋します。

  def main(_args) do
    1..30
    |> Flow.from_enumerable(stages: 3, min_demand: 0, max_demand: 1)
    |> Flow.map(fn _ -> call_api() end)
    |> Enum.to_list()
    |> Enum.reduce(%{}, fn status_code, acc ->
      Map.update(acc, status_code, 1, &(&1 + 1))
    end)
    |> IO.inspect
  end

APIを呼び出す総回数は何回でも良いのですが、この例では30回としています。 1..30で先のGenStageの説明におけるproducerの役割となるステージを1つ作成します。

次に、|> Flow.from_enumerable(stages: 3, min_demand: 0, max_demand: 1) でconsumerの役割のステージを3つ作成します。 それぞれのconsumerは同時に1つの処理のみを処理するようにmin_demand: 0, max_demand: 1と設定します。

Flowを使った処理はここまでで、最後にEnum.to_listEnum.reduceを使って処理結果のサマリーを作成しています。

上記のコードを実行した結果は以下となります。

%{attempts: 0, reason: "success", status_code: 200}
%{attempts: 0, reason: "success", status_code: 200}
%{attempts: 0, reason: "success", status_code: 200}
%{attempts: 0, reason: "success", status_code: 200}
%{attempts: 0, reason: "success", status_code: 200}
%{attempts: 0, reason: "success", status_code: 200}
%{attempts: 0, reason: "success", status_code: 200}
%{attempts: 0, reason: "success", status_code: 200}
%{attempts: 1, reason: "success", status_code: 200}
%{attempts: 0, reason: "success", status_code: 200}
%{attempts: 1, reason: "success", status_code: 200}
%{attempts: 0, reason: "success", status_code: 200}
%{attempts: 0, reason: "success", status_code: 200}
%{attempts: 0, reason: "success", status_code: 200}
%{attempts: 0, reason: "success", status_code: 200}
%{attempts: 0, reason: "success", status_code: 200}
%{attempts: 1, reason: "success", status_code: 200}
%{attempts: 0, reason: "success", status_code: 200}
%{attempts: 1, reason: "success", status_code: 200}
%{attempts: 1, reason: "success", status_code: 200}
%{attempts: 0, reason: "success", status_code: 200}
%{attempts: 0, reason: "success", status_code: 200}
%{attempts: 3, reason: "max_retry_exceeds", status_code: 503}
%{attempts: 0, reason: "success", status_code: 200}
%{attempts: 0, reason: "success", status_code: 200}
%{attempts: 0, reason: "success", status_code: 200}
%{attempts: 0, reason: "success", status_code: 200}
%{attempts: 0, reason: "success", status_code: 200}
%{attempts: 3, reason: "max_retry_exceeds", status_code: 503}
%{attempts: 2, reason: "success", status_code: 200}
%{200 => 28, 503 => 2}

APIの利用状況について可視化することができればFlowでの処理状況がより分かりやすかったのですが、筆者の調べ方が浅いせいか、 Phoenixアプリのモニタリング状況の可視化までは至りませんでした…。

まとめと感想

本エントリではFlow経由でGenStageのback-presssureを用いてAPIを優しく呼び出す方法について述べました。 FlowはGenStageが抽象化したback-pressureを更に抽象化することで、クライアントコード側の処理をより直感的に書けるようにしてくれます。 consumer/producerの購読関係を明示的に一つ一つ設定することなく、サッとイベントのパイプラインを書くことができるのは非常に魅力的です。 バッチ処理などでデータを多段に処理する際の選択肢として、Flowの採用を考えてみるのも面白いのではないかと思います。

Smalltalkで『テスト駆動開発』の「第I部 多国通貨」をハンズオンしたら快適で楽しかった

今日は。 SPEEDAの開発をやっている濱口です。

SPEEDA開発チームではテスト駆動開発(TDD)、ペアプログラミングを徹底しています。
だからなのか、『テスト駆動開発』はすごく楽しく読めました。
今回ハンズオンを行った「第I部 多国通貨」でも、ペアプロをしながら著者が語りかけてくるような感じで、 読者側も、著者の意図をひとつずつ理解しながら読み進めていけるようになっています。

有意義なハンズオンを、より有意義にしたい

ただ、そうは言っても、 読むだけよりも手を動かしたほうがよいと思いますし、 さらに、書籍で使用されているJava言語で書き写すだけよりも他のプログラミング言語でコーディングを行うことで、 ハンズオンがよりアクティブ・ラーニング化されると考えました。
プログラミング言語の翻訳プロセスを挟むことで、より能動的なハンズオンになるはずです。

古くてあたらしい言語(環境)、Smalltalkにふれたい

今回、私はプログラミング言語にSmalltalkを選択しました。
まったく違うパラダイムの言語でやってみても面白いかもしれませんが、 今回はより純粋なTDD、およびオブジェクト指向プログラミング(OOP)体験を求めてそうしました。
著者も最初はSmalltalkでTDDを行っていたはずですし、 Smalltalkは開発環境も内包していることから、IDEの源流みたいなものにも触れられると考えたからです。

環境はPharoを使いました。(イメージのテンプレートはPharo 7.0)
IcebergというGitリポジトリと連携するツールも付属しますので このハンズオンを行い、ソースコードを管理する限りでは、Pharo単体で完結しました。

f:id:yhamaro:20191209054113p:plain
Pharoの開発環境。ここですべてが完結します。

Smalltalkの文法は非常にシンプルなので、『Pharo by Example』の「Part I Getting Started」にひと通り目を通せば書き始められました。
ただ、Pharo自身を含むすべてのソースコードと自分の書いたコードは全く同じ扱いになるため、「自分の設計・実装が、そのまま既存のクラスライブラリの拡張になる」という意識づけが自然になされます。
なので、既存のクラスライブラリに分け入って、自分の設計を環境になじませる必要性を常に感じることになります。
開発環境の詳細な解説は上述のドキュメントに譲りますが、System Browser、Finderなどの各種ツールが上記のような探索的開発をサポートしてくれます。
今回は、メッセージの命名やプロトコルの整理などの作法を中心に既存のコードを参考にしました。

f:id:yhamaro:20191209054645p:plain
Let's 写経!

Smalltalkでは、わりと忠実な写経が可能だった

一通りハンズオンをこなしてみて、思ったより忠実な写経が出来上がった印象です。
とはいえ、そもそも言語が異なるのでやはり違いはありました。
途中、特に気になった差異をいくつか挙げます。

Javaのコンストラクタのような特別なメソッドが無い

例えば、5ドルを現すオブジェクトのインスタンスが欲しい場合、以下のようにします。

Money dollar: 5

これを実現(コンストラクト)するコードは以下になります。

{ #category : #'instance creation' }
Money class >> dollar: anInteger [
    ^ Money amount: anInteger currency: 'USD'
]

{ #category : #'instance creation' }
Money class >> amount: anInteger currency: aString [
    ^ self new setAmount: anInteger currency: aString
]

{ #category : #'initialization - private' }
Money >> setAmount: anObject currency: aString [
    amount := anObject.
    currency := aString.
    ^ self
]

『Smalltalk Best Practice Patterns』(『ケント・ベックのSmalltalkベストプラクティス・パターン―シンプル・デザインへの宝石集』)を参考にしましたが、
一番上のクラスサイドのメソッド(Shortcut Constructor Methodパターン)を入り口として、
汎用的なクラスメソッドの中でインスタンスを生成(new)し、必要なインスタンス変数初期化のためのメッセージを呼び出します(Constructor Parameter Methodパターン)。
インスタンスサイドに初期化時のみに呼び出すSetting Methodを作り、それを正しいプロトコルinitializetion -privateにカテゴライズするところが肝だと考えています。

インスタンス変数のスコープと意図を伝えるためのプロトコル

上記でも登場したMoneyクラスの定義は以下になります。

Class {
    #name : #Money,
    #superclass : #Object,
    #instVars : [
        'amount',
        'currency'
    ],
    #category : #'Example-TDD'
}

Smalltalkではインスタンス変数のスコープはすべてJavaでいうprivate、またはprotectedになります。
なので、インスタンス変数にアクセスするにはGetting/Setting Methodを介すことになります。

{ #category : #private }
Money >> amount [
    ^ amount
]

ただ、そのメソッド自体はアクセス制限されません(publicになります)。
privateプロトコルにカテゴライズすることでこのクラス以外から使用されない意図を伝えます。
「第4章 意図を語るテスト」でamountインスタンス変数をprivateにするタスクを行う際、 上記のような言語仕様の違いはありますが、Smalltalkでもテストコードでamountセレクタを使用しなくなるので、
「テスト対象オブジェクトの新しい機能を使い、テストコードとプロダクトコードの間の結合度を下げた。」という目的に適うタスクになります。

型が無い(untyped)

一番の違いはこれかと思います。
Smalltalkでは型が無く、かつ処理を全てメッセージ送信で表現するため、メッセージのシグニチャ(もちろん、それに型は含まれない)だけで多態を実現します。
なので、Javaのインタフェースのようなものが無い、というか必要無いです。
したがって、「第12章 設計とメタファー」から登場するExpressionインタフェースは必要無くなり、
「第13章 実装を導くテスト」で「Expression に reduce(String) メソッドの定義を引き上げる」というタスクを行いますが、 以下のようにSumとMoneyが等しくreduceTo:メッセージに応答するようにするだけです。

{ #category : #enumerating }
Money >> reduceTo: aCurrency at: aBank [
    | rate |
    rate := aBank rateFrom: self currency to: aCurrency.
    ^ self class amount: self amount / rate currency: aCurrency
]

{ #category : #enumerating }
Sum >> reduceTo: aCurrency at: aBank [
    | sum |
    sum := (augend reduceTo: aCurrency at: aBank) amount
        + (addend reduceTo: aCurrency at: aBank) amount.
    ^ Money amount: sum currency: aCurrency
]

上記の違いはありましたが、このハンズオンが意図する本筋から外れるものはなく、1章ずつステップバイステップで進めていけました。 (章ごとに出来上がったコードのスナップショットはコミット単位で分けています

個人的にはSmalltalkのシンプルが気に入ったので、
この本をさらに読み進めるのと、今まで読んだOOP関連の本(『実装パターン』など)をSmalltalk視点でもう一度読み直すなどし、
SmalltalkでOOPやTDDをしばらく学んでみたいと考えています。

Go言語でオブジェクト指向プログラミングの基本(型編)

こんにちは、SPEEDA開発チームの @tkitsunai です。

最近リリースされたプロダクションコードにようやくGo言語が採用されました。嬉しい。

今回はGo言語でオブジェクト指向プログラミングで型表現についてのテクニックや考え方の基礎を紹介します。もっとGopherが増えて欲しい!

対象読者

  • Go言語をこれから始める人
  • Go言語の型宣言で表現力を高めたい人
  • オブジェクト指向プログラミングに向き合いたい人

型を定義する

オブジェクト指向プログラミングの基本として「型」で表現を体現します。structはGoの値であり型です。

例題:

  • エンジニアだけが所属できる、SPEEDAプロダクトというユニットに、tkitsunaiという名前のエンジニアが所属している。
  • デザイナーだけが所属できる、SPEEDAデザインというユニットに、hiranotomokiという名前のデザイナーが所属している。

例題を悪い例として実装します。

type (
    Unit struct {
        Name string
        Members []string
    }
)

利用する実装コードは以下です。

   tkitsunai := "tkitsunai"
    hiranotomoki := "hiranotomoki"

    speedaProductUnitName := "SPEEDAプロダクトユニット"
    speedaDesignUnitName := "SPEEDAデザインユニット"

    speedaProductUnit := Unit{
        Name:    speedaProductUnitName,
        Members: []string{
            tkitsunai,
        },
    }

    speedaDesignUnit := Unit{
        Name:    speedaDesignUnitName,
        Members: []string{
            hiranotomoki,
        },
    }

型で厳格さを作り、その型を洗練させる

上の構造体定義はプリミティブな型で表現されています。例題に対して型でほとんど表現できておらず悪い例といえます。

  • ユニットが変数名だけで表現されており、デザインユニットにエンジニアが入ってしまう可能性がある
  • tkitsunaiがそもそも人ではない可能性がある
  • tkitsunaiかhiranotomokiという文字だけでは、エンジニアかデザイナーか判断できない
  • etc...

プリミティブ型を多用することによって起きる問題を、型の厳格さを加えることで排除します。

改良1: プリミティブ型をなくす

Go言語には型宣言によって、基本型もしくは定義された構造体を別名で再定義することが可能です。改良を加えてみます。

type (
    UnitName string
    PersonName string

    Person struct {
        Name PersonName
    }

    Unit struct {
        Name UnitName
        Members []Person
    }
)

利用する実装コードは以下です。

   tkitsunai := Person{
        Name: PersonName("tkitsunai"),
    }
    hiranotomoki := Person{
        Name: PersonName("hiranotomoki"),
    }

    speedaProductUnitName := UnitName("SPEEDAプロダクトユニット")
    speedaDesignUnitName := UnitName("SPEEDAデザインユニット")

    speedaProductUnit := Unit{
        Name: speedaProductUnitName,
        Members: []Person{
            tkitsunai,
        },
    }

    speedaDesignUnit := Unit{
        Name: speedaDesignUnitName,
        Members: []Person{
            hiranotomoki,
        },
    }

少しだけ良くなりました。この改良でProductUnitに人以外が混在することはなくなりました。

また、人の名前とユニットの名前が混在することもなくなりました。

改良2: Unitの特性を型で表現する

例題を見直すと、プロダクトユニットにはそれぞれ特性があります。プロダクトユニットならばエンジニア、デザインユニットならばデザイナーである必要があります。

現在のままでは、tkitsunaiという人がデザインチームに入れられるかもしれません(それはそれで楽しそうですが)。これも型で制限していきます。

type (
    UnitName string
    PersonName string

    Person struct {
        Name PersonName
    }
    Engineer Person // <- Person型をwrapしたEngineer型として定義する
    Designer Person // <- Person型をwrapしたDesigner型として定義する

    ProductUnit struct {
        Name UnitName
        Members []Engineer
    }

    DesignUnit struct {
        Name UnitName
        Members []Designer
    }
)

利用する実装コードは以下です。

   tkitsunai := Engineer{
        Name: PersonName("tkitsunai"),
    }
    hiranotomoki := Designer{
        Name: PersonName("hiranotomoki"),
    }

    speedaProductUnitName := UnitName("SPEEDAプロダクトユニット")
    speedaDesignUnitName := UnitName("SPEEDAデザインユニット")

    speedaProductUnit := ProductUnit{
        Name: speedaProductUnitName,
        Members: []Engineer{
            tkitsunai,
        },
    }

    speedaDesignUnit := DesignUnit{
        Name: speedaDesignUnitName,
        Members: []Designer{
            hiranotomoki,
        },
    }

この改良によって、各ユニットで所属できる型が制限されたので、Engineerであるtkitsunaiはデザインユニットに入ることはできなくなりました(残念)。

型に厳格さを求める完全コンストラクタを活用する

現時点では、PersonNameやUnitNameにはどのような名前も許されていますが、オブジェクト生成時にルールを適用させることで、型を利用する場合にエラーとできるようにします。いわゆる完全コンストラクタを目指します。Go言語にはコンストラクタはありませんが、慣習としてPrefixにStruct名にNewをつけた関数を用意し、structとerrorを返すようにします。

func NewUnitName(unitName string) (UnitName, error) {
    if unitName == "" {
        return "", errors.New("ユニット名が必要です")
    }

    if strings.HasSuffix(unitName, "ユニット") {
        return "", errors.New("ユニット名は必要ありません")
    }
 
    return UnitName(unitName), nil
}

利用するコード

   speedaProductUnitName, err := NewUnitName("") // ユニット名はひとまず入れなくていーや
    if err != nil {
        panic(err)
    }

実行結果

panic: ユニット名が必要です

エラーを握りつぶされてしまったら元も子もないですが、これでUnitNameのルールを生成時に確認でき、空文字なユニット名は作ることはできなくなりました。

このように、UnitNameを持つProductUnitおよびDesignUnitは、UnitNameの完全コンストラクタを通じて、型とそのルールが表現されていきます。

ファーストクラスコレクションと型定義によってメンバーに対する操作を制限する

ProductUnitのフィールドであるMembersは[]Engineer型で表現されています。この状態では、ProductUnitがこのコレクションを直接操作しなければならず、コレクションを壊しかねません。型を抜き出して、ファーストクラスコレクションとしてstructを再定義し、操作を制限させることでコレクションの壊れにくさを作ります。

type Engineers struct {
    list []Engineer
}

func (p Engineers) Add(newEngineer Engineer) Engineers {
    engineers := make([]Engineer, len(p.list)+1)
    for i, engineer := range p.list {
        engineers[i] = engineer
    }
    engineers[len(p.list)] = newEngineer
    return Engineers{
        list: engineers,
    }
}

func (p *ProductUnit) JoinNewMember(engineer Engineer) {
    p.Members = p.Members.Add(engineer) // 増やす操作しかできない
}

上記の実装によって、増やすメソッドは存在するが、減らすメソッドを定義しないことで、コレクションに対する操作を制限できました。一度入ったら出られないプロダクトユニットへようこそ!

なお、上記の例ではストイックにstructを定義していますが、厳格さをそこまで求めないのならばEngineers []Engineerとしても良いかもしれません。

列挙型で表現力を高める

プログラム言語を持ったエンジニアを定義するような値とそれを持った型を作るならば、型表現とコレクションを応用して表現力を高めることができそうです。

type ProgrammingLanguage int
type ProgrammingLanguages []ProgrammingLanguage

const (
    Golang ProgrammingLanguage = iota
    Java
    JavaScript
    Clojure
    Elixir
    Rust
)

type Engineer struct {
    Person
    Skills ProgrammingLanguages
}

func (e Engineer) LetsGoProgramming() {
    for _, lang := range e.Skills {
        if lang == Golang {
            fmt.Println("OK, I will typing `func main() { //blablabla }`")
        }
    }
}

これもただの列挙型ではなく、型に別名をつけることで厳格化させます。

構造体埋め込みで表現をする

構造体埋め込みを利用して型定義に幅を持たせます。

package engineer

import "fmt"

type (
    FirstName string
    LastName  string
    person    struct {
        FirstName FirstName
        LastName  LastName
    }
    GoEngineer struct {
        person
    }
)

func (p person) Greet() {
    fmt.Println(fmt.Sprintf("hello I am %s", p.fullName()))
}

func (p person) fullName() string {
    return fmt.Sprintf("%s %s", p.FirstName, p.LastName)
}

func NewGoEngineer(firstName FirstName, lastName LastName) GoEngineer {
    return GoEngineer{
        person: person{
            FirstName: firstName,
            LastName:  lastName,
        },
    }
}

利用する実装コードは以下です。

package main

import (
    "engineer" // <= 擬似コード:engineerパッケージを読み込んだことにします
)

func main() {
    tkitsunai := engineer.NewGoEngineer(engineer.FirstName("takayuki"), engineer.LastName("kitsunai"))
    tkitsunai.Greet()
}

GoEngineer型のフィールドperson型定義によって型の恩恵を受けられます。同時にperson型はパッケージ外に公開していないため、完全コンストラクタを経由してGoEngineer型を利用すれば、personの詳細実装が外に漏れずに済みます。

構造体の埋め込みは普段活用していませんが、型をまとめるためだけでなく、制限範囲を限定することもできるため、応用がしやすいと思います。

まとめ

今回はGo言語における型表現について、ほんの一部だけ紹介しました。

  • プリミティブな型を作らない
  • 最初から汎用的な型を作らない
  • 完全コンストラクタで型が示す表現力を高める
  • ファーストクラスコレクションで操作を制限する
  • 列挙であるiotaも別名で型定義して利用する
  • 構造体埋め込みを上手く利用する

関連しているオブジェクト指向エクササイズも面白いので、気になる方はぜひ調べてみてください。

型を厳格にする意義

型を定義するという行為は、ルールや制限を明確かつ厳格化することであり、表現力を豊かにする行為そのものです。

オブジェクトには何かしらのルールが存在し、型そのものや、型自体で知識やルールを表現/体現する効果があります。

最近はドメイン駆動設計が随分と盛り上がりを見せており、ドメインレイヤの実装では、ドメインを型と型のメソッドで知識を表現/体現します。まさにオブジェクト指向プログラミングです。

型の恩恵を受けられるチャンスを増やしていきましょう。

最後に、この言葉をおくります。

ゆるゆるな型を使うだけでは、せっかくの型がかたなし

ありがとうございました!


SPEEDAプロダクトユニットではエンジニアを募集しています

まずは気軽に話を聞いてみたい!という方はtwitter @tkitsunai 宛にリプライをしてみてください。型が大好きな方もこれから取り組みたい方も、様々な強みを持った弊チームで働きませんか?

応募はこちらから。

apply.workable.com

Kotlinを3ヶ月書いて感じたJavaとの違い

はじめに

  • SPEEDA PDT歴3ヶ月の相川です
  • 今回はJavaを2年くらい書いていた私が、3ヶ月間で感じたKotlinの特徴を列挙していこうと思います

Kotlinの特徴

  • 型推論
  • データクラス
  • nullable
  • 検査例外の話
  • returnを明示的に書かなくても良い
  • 拡張関数
  • リスト操作の評価について
  • kotlin corutine

型推論について

  • Kotlinでは変数を定義する際に、varもしくはvalを使います
  • その際に、型推論が採用されているおかげで、Javaのように型を宣言しなくても良くなります
  • ただ、Java10から型推論は使えるようになってるので、あまりKotlinだけに言える話ではなくなっているかと思います
// Javaで書く場合
String name = "太郎";

// Kotlinで書く場合
var name = "太郎";

データクラスについて

  • Kotlinのデータクラスは非常に綺麗です
  • Javaですと、constructorやgetterを定義する必要があり、どうしてもデータクラスの記述量が増えがちです。(もちろんlombokを使えば、アノテーションで少しは減らすことはできると思います)
  • Kotlinでは、下記のように定義するだけでインスタンスを生成できますし、Getterを定義しなくてもプロパティ名からアクセスすることができます。
//  Kotlinのデータクラスはこれだけ
data class Sample(val name: String, val num: Int)

nullable

  • Kotlinを書くようになってから圧倒的にNull Pointer Exceptionにで会う回数が減りました。(嬉しいような...寂しいような)
  • というのも、Kotlinでは何も気にせず変数にnullを代入しようとすると怒られるからです。
  • なので、nullをどうしても使いたい場合は変数名の後ろに?をつける必要があるのですが、このおかげでnullが入りうる箇所を限定的にできるということがNull Pointer Exceptionを防ぐことに繋がっているのです(余計な防御的プログラミングを減らせるというのも、メリットだと思っています)
// コンパイルエラー
val hoge: String = null

// これは大丈夫
val hoge?: String = null

検査例外の話

  • Javaでは検査例外(RuntimeExceptionとそのサブクラスを除く例外たちのこと)を投げるメソッドを呼び出す際に、try-catchするなどハンドリングしてあげる必要がありました。
  • この検査例外という仕組みは現代では嫌われる傾向があり、Java以降はほとんどの言語で採用されてないようです
  • kotlinもその流れを汲んで検査例外というものを導入していないので、Javaではtry-catchを書いていた箇所も、kotlinでは書かなくてもコンパイルエラーにはなりません。

returnを明示的に書かなくても良い

  • Kotlinでは一つの式からなる関数はreturnを省略することができます
  • Javaでは返り値のある関数は全てreturnしていたかと思いますが、=で書けるようになります
  • また、返り値の型も省略できるので、簡単な関数を書く際などには=で書き、返り値の型も省略してしまった方が個人的には読みやすいと感じました。
data class Sample(val name: String, val num: Int)

fun hoge()  = Sample("hoge", 1);

fun piyo() : String = hoge().name

拡張関数と拡張プロパティ

  • Kotlinには継承をせずとも、既存のクラスを拡張できる機能が実装されています(拡張関数と拡張プロパティ)
  • これらを使うことで、既存のクラス(継承禁止クラスを含む)に、新たに関数やプロパティを追加することができます。

拡張関数

  • まず、拡張関数を見てみます。
  • やるべきことは、拡張するクラス or インターフェースの名前の後に、追加する関数名を定義します。
  • あとは、呼び出し側で関数を呼び出してあげるだけでokです
fun main() {
 // 拡張関数の呼び出し
    println("hoge".secondChar().toUpperCase())
 
}

// 拡張関数を定義する
fun String.secondChar() = this.get(1)

拡張プロパティ

  • 続いて拡張プロパティを使用してみます。
  • やるべきことは、拡張するクラス or インターフェースの名前の後に、追加するプロパティ名を定義します。
  • 拡張プロパティの場合、デフォルトのgetterが用意されないので、常に定義してあげる必要があるので、定義します。
  • あとは、呼び出し側からプロパティ名でアクセスできるようになります。
  • また必要に応じて、setterの定義もできます(その場合は、変数宣言をvarでする必要があります)
fun main() {
 // 拡張プロパティの呼び出し
    println("fuga".thirdChar)
}

// 拡張プロパティを定義する
val String.thirdChar : Char 
    get() = get(2)
  • これらの機能が追加されてる背景としては、Kotlinのメインテーマの1つとして、既存のコードとスムーズな統合というものがあります。
  • KotlinをJavaプロジェクトに統合するときに、編集対象外の既存のコードを修正せずにKotlinの長所を利用できるようにしたかったというのがこの拡張関数・拡張プロパティが追加された動機のようです。 参考

リスト操作の評価について

  • 説明に先立ってサンプルコードから書いてしまいます
  • 今までJavaを書いていた人間からすると、リスト操作をするとなると下記のような実装になると思います
fun main() {
 val list = listOf(1, 2, 3, 4, 5)
 list.filter{ ... }.map{ ... }
}
  • この場合、KotlinのIterableに対する操作とみなされ、先行評価型となります。
  • なので、下記のような実験的なコードを実行してみると・・・
fun eagerEvaluationMethods() {
    val list = listOf(1, 2, 3, 4, 5)
    var result = list.filter {
        println("iterable.filter : $it")
        it % 2 == 0
    }.map {
        println("iterable.map : $it")
        it * 2
    }.first()
    println(result)
  • コンソールを確認すると下記のような評価結果になります
iterable.filter : 1
iterable.filter : 2
iterable.filter : 3
iterable.filter : 4
iterable.filter : 5
iterable.map : 2
iterable.map : 4
4
  • 前から順番に評価されているのがわかります
  • 先行評価の場合、まず全件に対してfilterし、次に残ったものに対してmapして、最後にfirstしてるという具合です
  • これを遅延評価に変更したい場合はiterableをsequenceにします
fun lazyEvaluationMethods() {
    val list = listOf(1, 2, 3, 4, 5)
    var result = list.asSequence().filter {
        println("sequence.filter : $it")
        it % 2 == 0
    }.map {
        println("sequence.map : $it")
        it * 2
    }.first()
    println(result)
}
  • 変更後の結果はこんな感じです
sequence.filter : 1
sequence.filter : 2
sequence.map : 2
4
  • 遅延評価になっているのがわかります
  • 1件ずつ、順番にfilter→map→firstしたら終了といった具合に評価されています
  • Javaを書いていた人にとっては、今まで遅延評価されてきたものが急に先行評価に変わるので、予期せぬ結果を招くという話になるかもしれないです
  • ですので、遅延評価なのか先行評価なのかきちんと理解してCollection操作をしてあげる必要がありそうですね 参考

coroutineの話

  • Javaでは並列実行するように実装をしようとなった場合、CompletableFutureとかParallelStreamとか他にもいくつか標準APIの選択肢があると思います
  • 一方で、Kotlinには 1.3系から正式に使えるようになったcoroutineというものがあります。その話を最後にしようと思ったのですが、2回前に原田さんがブログでまとめてくださってるのでそちらを見てもらえればと・・・ KotlinのCoroutineを用いた,外部API呼び出しの並列数を指定できるライブラリを作成した話

  • あと、詳しい説明は 公式サイトを見て頂ければと思います

まとめ

  • 今回は簡単にKotlinの特徴をまとめてしまいましたが、他にもKotlinとJavaの違いはまだまだあります。
  • この記事を読んで、少しでもKotlinに興味を持ってもらえたら嬉しいです。
  • また補足ですが、Kotlinは同一プロジェクト内でJavaファイルと共存できるので(コンパイルかけると全部classファイルになるので)、今Javaしか書いてない人は少しずつ書いてみるのも手段ですし、IntelliJ IDEAというIDEではJavaのソースコードをKotlinに変換する機能もついているので、変換してみたらどうなるのか見るだけでもイメージが湧くのかなと思います。

Gaugeのsetupとteardownステップを用いて効率的に読みやすいテストを書く

こんにちは!SPEEDA開発チームの工藤です。

大分時間が開いてしまいましたが、Gaugeシリーズの第四回目です。

今回はe2eテスト書く際には必須であろうSet Up/Tear Down Stepsを、Gaugeではどのように実現できるのかをSPEEDA開発チームでの実例も交えてお伝えできればと思います。

過去3回分の記事はこちらから↓

  1. Gauge Test Automation Toolとアジャイル開発
  2. GaugeのConceptを用いてテストシナリオをより仕様書のように記述する
  3. GaugeのParameterを使いこなす

GaugeにおけるSet Up/Tear Down Stepsの実現方法

Gaugeには、Set Up StepsやTear Down Stepsを実現できる手段がいくつか用意されています。

用途に合わせて下記のいずれかを選択して使います。

  • Contexts
  • Tear Down Steps
  • Execution Hooks

上記3つについて順番にお伝えしていきます。

Contexts

GaugeではSet Up StepsをContextsと呼んでいます。 context stepsを使用することで、SpecファイルのScenarioの実行に必要な条件を指定できます。

Specファイル内先頭のScenarioの前にStepを記述すると、そのStepが全てのScenarioの最初に実行されます。(GaugeにおけるSpecやScenarioなどのワードに馴染みのない方は第一回目の記事をご覧ください)

下記例ではScenario 1の前に「ユーザーAでログイン」、「プロジェクトページに遷移する」というStepを記述していて、このStepがScenario 1、2の最初に実行されます。

# プロジェクトの削除

context steps
* ユーザーAでログイン
* プロジェクトページに遷移する

Scenario 1
## 1つのプロジェクトを削除
* プロジェクト"project_a"を削除
* プロジェクト"project_a"が削除されていることを確認

Scenario 2
## 複数のプロジェクトを削除
* プロジェクトリスト上の全てのプロジェクトを削除
* プロジェクトリストが空であることを確認

各Scenarioで必要だがあまり仕様的には重要ではないセットアップ処理などをContextsにまとめることで、Specificationの冗長さをなくすことができます。

SPEEDA開発ではSpecファイルをページ単位で切ることが多いのでテスト対象のページに遷移するステップや、ログイン処理をここに書くことが多いです。

Tear Down Steps

Tear Down StepsはSpecファイル内最後のScenarioの後に記載します。 Scenarioの実行を終えるために必要なStepがあればTear Down Stepsとして定義します。

3つ以上のアンダースコアを記述することでTear Down Stepsを指定することができます。

___
* Tear down step 1
* Tear down step 2
* Tear down step 3

下記の例では、アンダースコアの後に記述されている「ユーザーAでログアウト」と「ユーザーAを削除」がTear Down Stepsになります。このSpecificationが実行されると、下記の順で実行されます。

  1. Contextsの実行
  2. 1つのプロジェクトを削除のScenarioの実行
  3. Tear Down Stepsの実行
  4. Contextsの実行
  5. 複数のプロジェクトを削除のScenarioの実行
  6. Tear Down Stepsの実行
# プロジェクトの削除

* ユーザーAを作成
* ユーザーAでログイン

Scenario 1
## 1つのプロジェクトを削除
* プロジェクト"project_a"を削除
* プロジェクト"project_a"が削除されていることを確認

Scenario 2
## 複数のプロジェクトを削除
* プロジェクトリスト上の全てのプロジェクトを削除
* プロジェクトリストが空であることを確認
___
ここからTear Down Steps
* ユーザーAでログアウト
* ユーザーAを削除

SPEEDA開発では実はあまりTear Down Stepsは使っていません、Tear Downとしては後述するExecution Hooksを使う方が多いです。

Execution hooks

Execution hooksを使うとSuite,Spec,Scenario,Stepの単位で任意のテストコードを実行することができます。

ContextsやTear DownはSpecファイル毎且つScenarioにしか定義できませんが、Execution HooksはSpecファイルを跨いで且つ様々な単位で定義できます。

import com.thoughtworks.gauge.*

class ExecutionHooksExample {
    @BeforeSuite
    fun beforeSuite() {
        // 全てのテスト実施前の最初に一度だけ実行される処理
    }

    @AfterSuite
    fun afterSuite() {
        // 全てのテスト実施後の最後に一度だけ実行される処理
    }

    @BeforeSpec
    fun beforeSpec() {
        // 各Specファイルのテスト実施の先頭に一度だけ実行される処理
    }

    @AfterSpec
    fun afterSpec() {
        // 各Specファイルのテスト実施の最後に一度だけ実行される処理
    }

    @BeforeScenario
    fun beforeScenario() {
        // 各Scenario実施前に実行される処理
    }

    @AfterScenario
    fun afterScenario() {
        // 各Scenario実施後に実行される処理
    }

    @BeforeStep
    fun beforeStep() {
        // 各Step実施前に実行される処理
    }

    @AfterStep
    fun afterStep() {
        // 各Scenario実施後に実施される処理
    }
}

SPEEDAでは下記のような処理はBefore Suiteで実行しています

  • 一度だけ設定ファイルを読み込む
  • Read-Onlyデータの投入

また下記のような処理はAfter Scenarioで実行しています

  • ログアウト
  • WebdriverのClose処理

その他にもDBやモックのセットアップ処理もExecution Hooksを使用して任意のタイミングで実行しています。

Execution hooksを特定のTagが指定されている場合のみ実行されるようにすることも可能です。その場合は下記のように指定します。

// tag1 または tag2がついているScenarioでのみ前処理として下記を実行
@BeforeScenario(tags = {"tag1, tag2"})
fun setupDataBase() {
    // Code for before scenario
}

まとめ

GaugeはExecutable Specificationを謳っていてSpecファイルやScenarioは実行可能な"仕様書"である必要があります。

SPEEDA開発では今回ご紹介した機能を使ってSpecificationファイルの記述を出来るだけ簡潔にして、より仕様書として読みやすくするよう心がけています。

KotlinのCoroutineを用いた,外部API呼び出しの並列数を指定できるライブラリを作成した話

KotlinのCoroutineを用いた,外部API呼び出しの並列数を指定できるライブラリを作成した話

ユーザベースインターンの原田です.大学院で研究しながら京都でユーザベースのインターンをさせて頂いており,今回初めてブログを書かせて頂きます!

題名にある通り,今回KotlinのCoroutineを使用した並列数を指定して関数を実行できるライブラリ(ParallelExecutor)を作成しましたので,そのことについて投稿させて頂きます.

背景

外部のAPIを呼びだす処理を並列で呼びだしたいが,相手側の都合(サーバーへの負荷等)により並列数を制限したい状況が発生しました.しかしCoroutineは大量に起動出来てしまい,通常では並列数に制限をかけることが出来ません.そこでこれを実現する為に,ParallelExecutorを作成することにしました.

本記事の内容

本記事の内容は以下の通りです

  • そもそもCoroutineとは何か

  • Coroutine間で値を転送できるChannelについて

  • ParallelExecutorの説明

    Coroutine

    Coroutineは一言で言うと,軽量なスレッドです.そして以下のような特徴を持っています.

  • 中断が可能な計算インスタンスである
  • 特定のスレッドに束縛されない

ここではまずCoroutineの作成方法を示し,その後でこれらの特徴について説明します.

Coroutineの作成方法

下図はCoroutine作成のイメージです. f:id:harada-777:20191015180809p:plain:w400:left
CoroutineはCoroutine builderで作成することができます.しかし,その際にはCoroutineScope内で作成する必要があります.CoroutineScopeとはCoroutineが実行される仮想的な場所のようなものです.CoroutineはCoroutineScope内でのみ実行可能です. 実際のコードを作成してみます.Coroutineを使用する為に以下の依存を追加して下さい.

dependencies {
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0-RC"
}

以下はサンプルコードです.

fun main() {
    runBlocking {
        val job = launch {
            delay(1000L) // 1秒待つ
            println("World!") // print after delay
        }
        println("Hello,")
        delay(2000L) // プログラムを終了させない為にmain thread で2秒待つ
    }
}

runBlockingは現在のスレッドをブロックしてCoroutineScopeを作り出します.そしてlaunchはCoroutine builderの1つであり,Coroutineを生成することができます.launchはデフォルトでは親のCoroutineScopeで実行するCoroutineを作成します.ここで親のrubBlockingのCoroutineScopeであるmain threadで実行するCoroutineを作成しています.引数でどのCoroutineScopeで実行するかを指定することもできます.

次にCoroutineの特徴について説明します.

中断が可能な計算インスタンス [1]

coroutineが中断可能な計算インスタンスであることについて説明します.ここで計算インスタンスとは処理を記述したコードブロックのことを指しています.よってスレッドは大量に起動できませんが,Coroutineは以下のように10000個など大量に起動しても問題ありません. またCoroutineが中断可能とは,Cortouineの処理を途中で止めて,スレッドを解放することができることを意味します.その中断はsuspend関数と呼ばれる関数で行われます.以下のコードを見てください.

suspend fun apiCall() {
    println("ApiCall")
    delay(1000)
    println("Return")
}
fun main(args: Array<String>) {
    runBlocking {
        println("main1")
        val job = launch {
            apiCall() // coroutineを中断し、スレッドを解放する
        }
        delay(500) //delay1
        println("main2")
    }
}

このdelayはsuspend関数です.呼ばれるとCoroutineを中断しスレッドを解放しする関数です.またsuspend関数はsuspend修飾子を使って自分で宣言すること可能です.ここではcallApiがそれに当たります.このコードは以下の図のように実行されます. f:id:harada-777:20191015180549p:plain
ポイントはsuspend関数を呼びsuspend関数であるcallApiのdelayが呼ばれた後にスレッドを解放している点です.これがCoroutineの特徴でスレッドをブロックすることなく処理を実行できます.jobは処理の集合を表すインスタンスです. このコードの実行結果は以下のようになります.

main1
ApiCall
main2
Return

特定のスレッドに束縛されない

Coroutineは特定のスレッドに束縛されません.つまりCroutineとスレッドは1対1対応ではありません.Croutineはsuspend関数によって中断しスレッドを解放,そしてそのとき空いているスレッドを確保し再開されながら実行を行います.

f:id:harada-777:20191015182617p:plain:w400:left
こうすることでより1つのスレッドを有効に活用することが可能です.

Channelとは

Channelとはキューの一種です.Channelを用いることがCoroutine間で値を転送することが出来ます. f:id:harada-777:20191015181603p:plain:w400:left
channelのsendを呼ぶことで,値をchannelに書き込みchannelの片方でrecieveを呼ぶことでその値を順に呼び出すことができます.実際のコードは以下の通りです.

fun main() {
    runBlocking {
        val channel = Channel<Int>()
        launch {
            for (x in 1..5){
                channel.send(x * x) //値をchannelに書き込む
            }
        }
        repeat(5) { println(channel.receive()) } //値を取り出す
        println("Done!")
    }
}

このコードは以下のように書くこともできます.closeは特別な関数でchannelの終了を表すtokenを送ることができます.読み取り側でこのtokenが読み取られると繰り返しが終了し,全ての要素が読み取られたことを保証できます.

fun main() {
    runBlocking {
        val channel = Channel<Int>()
        launch {
            for (x in 1..5) {
                channel.send(x * x)
            }
            channel.close()
        }
        for (item in channel) {
            println(item)
        }
        println("Done!")
    }
}

ParallelExecutorについて

ここで今回作成したParallelExecutorについて説明します.使用は以下の通りです.

  • 並列数を指定してsuspend関数を並列に実行することができる

  • ParallelExecutorのインスタンスを共有することで,共有した部分で並列数を制御することができる

  • ParallelExecutorには入力として引数にシーケンスと実行したいsuspend関数を渡すことができる

  • 全てのシーケンスの要素は,ParallelExecutorに渡した関数に渡され実行される

  • 結果はChannelにResult型で書き込まれ,ParallelExecutorはそのChannelを返す

  • 途中で例外が発生すると自動的にchannelは閉じられ,channel最後の要素がその例外を持っている

実際のコードはこちらです. https://github.com/uzabase/ParallelExecutor/blob/master/src/main/kotlin/ParallelExecutor.kt

ParallelExecutorではCoroutineの並列数を指定する為にChannelをセマフォとして用いています.[2]セマフォとは共有資源に対するアクセス可能な数を示すものです. f:id:harada-777:20191015181641p:plain:w400:left
ParallelExecutorではセマフォに値を送れたCoroutineのみが処理を可能にしています.Channleのsendはsuspend関数なのでCroutineの処置を中断ができます.従ってCoroutineの並列数を制限することができます. 大きな流れを説明します.① でまずCoroutineがinputSeqの大きさ分起動します.その次に②でsemaphoreに値を送ろうとします.③semaphoreに値を送れたCoroutineは処理の開始を行い,④実際の処理が走ります. ParallelExecutorのが行なっていることのイメージが以下の図です. f:id:harada-777:20191015181807p:plain ④の中身を説明します. まずGlobalScopeでCroutineをinputSeq(入力として与えたシーケンス)の数起動をさせます.GlobalScopeはデフォルトでは用意されているBackground Thread Poolのスレッドを使用してCoroutineScopeを作成します.コードでは以下の部分です.

job = GlobalScope.launch(handler) {
            inputSeq.forEach { input ->
                launch { 
                            ・
                            ・

そして次に自分で用意したSenderThreadPoolをCoroutineScopeとして指定して,callFunction(input)を呼ぶCoroutineを作成します.コードでは以下の部分です. SenderThreadFactoryの定義

class SenderThreadFactory : ThreadFactory {
    private var count = 0
    override fun newThread(r: Runnable): Thread {
        return Thread(r, "sender-thread-" + ++count)
    }
}

SenderThreadFactoryによって作成されたPoolを用いてDispatcherを生成(これを渡すことでCoroutineのCoroutineScopeをこのPoolに指定できる)

private val dispatcher = Executors.newFixedThreadPool(capacity, SenderThreadFactory()).asCoroutineDispatcher()

Coroutineを起動する(withContextは値を返すCoroutineBuilderの一つ)

withContext(dispatcher) {
                        runCatching {
                            callFunction(input)
                        }
                    }

そしてその結果をResultに格納し,resultChに送り,semaphoreの値を1つ取り出します.そうすることで待機しているCoroutineが動き出します.コードでは以下の部分です.

 }.let { result ->
    resultCh.send(result)
    semaphore.receive()
    result.onFailure {
    throw it
}

またcallFunctionで例外が発生した場合は,例外ハンドラに処理が行き,jobのキャンセルが行われ残りの処理が素通りされるようになっています.そしてresultChとsemaphoreを閉じます.以下が例外ハンドラのコードです.

val handler = CoroutineExceptionHandler { _, exception ->
    exception.printStackTrace()
    job?.cancel()
    resultCh.close(exception)
    channel.close()
   }

jobのキャンセルは以下のように実装されています.jobのキャンセルが呼ばれるとisActiveがfalseになります.よって素通りしたい処理をif分で囲っています.

launch {
    if (isActive) {
        semaphore.send(Unit)
        withContext(dispatcher) {
                ・
                ・

まとめ

今回作成したライブラリとCoroutine周りの説明をさせて頂きました. 本ライブラリの実際の使用方法はこちらをご参照下さい. https://github.com/uzabase/ParallelExecutor

参考文献 [1] https://qiita.com/k-kagurazaka@github/items/8595ca60a5c8d31bbe37 [2] https://qiita.com/k-kagurazaka@github/items/0c30cc04dcef306ed3c7

ReactとReactHooksを使って、Flux的なアーキテクチャを実現する

こんにちは。SPEEDA開発チームの冨田です。

昨今のフロントエンドでは、Fluxというアーキテクチャが利用されることが多くなってきています。SPEEDAでもVueを使っている画面がありますが、そこではVuexというVue向けのFluxライブラリで状態管理をしています。

Fluxではデータの流れを一方向にすることで見通しのよい設計が行えるようになります。

今回は、素のReactを使ってデータの流れを単一方向にする設計を紹介します。

今回作ってみるもの

Todoアプリを作ってみましょう。 以下のようなことができる画面を作ります。なお、今回はデータの永続化は考えないものとします(つまり、ウェブページを更新すると、全て消えてしまいます)。

  • Todoを追加することができる
  • 全てのTodoのリストを見ることができる
  • Todoの完了状態を変更することができる

セットアップ

簡単のため、CodeSandboxなどで、React + TypeScriptなプロジェクトを作っておくとよいでしょう。以下は、今回作成したプロジェクトになります。

codesandbox.io

1. GettersとActionsをつくる

Todoアプリを実現するためのカスタムフックを作りましょう。

Gettersはコンポーネントからアクセスできるようにする(露出する)データの集まりです。Actionsはそれを呼び出すことで、なんらかの副作用を起こし、Gettersを更新する関数の集まりです(正確には、内部のStateを変更することで、Gettersが更新される)。これらはFluxで定義されているものと同様の概念だと考えていただいてよいでしょう。

今回はGetters, Actionsとして以下のものを定義しています。

  • todos: Todoのリストを保持する配列
  • add: 文字列を受け取り、Todoのリストに新規のTodoを追加する
  • update: idと真偽値を受け取り、リストの中の該当するTodoの完了状態を変更する
import { useState } from "react";

interface Todo {
  description: string;
  done: boolean;
}

interface State {
  todos: Todo[];
}

interface Getters {
  todos: Array<Todo & { id: number }>;
}

interface Actions {
  add(description: string): void;
  update(id: number, done: boolean): void;
}

export type Store = Getters & Actions;

export function useTodo(): Getters & Actions {
  const [state, setState] = useState<State>({ todos: [] });
  const { todos } = state;

  function add(description: string) {
    setState({
      todos: [...todos, { description, done: false }]
    });
  }

  function update(id: number, done: boolean) {
    setState({
      todos: todos.map((it, i) => (i !== id ? it : { ...it, done }))
    });
  }

  return {
    todos: todos.map((it, id) => ({ ...it, id })),
    add,
    update
  };
}

2. GettersとActionsをどこのコンポーネントからでもアクセスできるようにする

useContextを使います。 useContextは、createContextにより、あらかじめ生成しておいたコンテキストにどこからでもアクセスできるようにするReactHooksです。

例:Appコンポーネント内で、Context.Providervalue propsに渡したオブジェクトがChildElementコンポーネント内のuseContextで得られます。

const Context = createContext({});

const ChildElement = () => {
  const { foo } = useContext(Context);
  return (
    <div>{foo}</div>
  );
}

const App = () => {
  return (
    <Context.Provider value={{ foo: "bar" }}>
      <ChildElement />
    </Context.Provider>
  );
};

上記の仕組みを利用して、GettersとActionsに対してどこからでもアクセスできるようにし、さらにGettersに変更が行われると、再レンダーされるようにします。

createContextでコンテキストを作成します。ここでは引数に{} as anyとやってしまっていますが、Providerでvalueを提供しないときに初期値として得られるものなので、これでよいでしょう。

StoreProviderコンポーネントは、Context.Providerをラップしたものです。コンテキストにuseTodoで生成したGettersとActionsを保存し、子コンポーネントをレンダリングします。 ユーザーに、Contextやカスタムフックを意識させないための工夫です。

useContextもReactのuseContextをラップしたものです。こちらも、Contextを意識させないために作っています。

ここで定義したStoreProviderとuseContextを利用することで、画面のコンポーネントではカスタムフックについて意識することなく、useContextを呼ぶだけでなんか知らんけどGettersとActionsが使える、という状態になります。

import React, { createContext, useContext as useContextOriginal, FC } from "react";
import { useTodo, Store } from "./Store";

interface AppStore {
  todo: Store;
}

const Context = createContext<AppStore>({} as any);
const { Provider } = Context;

export const StoreProvider: FC = ({ children }) => {
  const todo = useTodo();
  return <Provider value={{ todo }}>{children}</Provider>;
};

export function useContext() {
  return useContextOriginal(Context);
}

3. Todoを表示するコンポーネントを作る

ユーザーが入力しているdescriptionについてはこのコンポーネントで管理するようにしています。これも上で作ったGetters, Actionsで管理してもよいのですが、useStateとの共存も示したかったので、今回はここに書きました。

実装自体は、普通のulとinput, buttonを使ったコンポーネントですが、useContextを利用することで、todos, add, updateにアクセスできるようになりました。

追加ボタンが押され、addが呼び出されることで、todosが更新され、再レンダリングが行われます。

チェックボックスが変更されることで、updateが呼び出され、todosが更新され、再レンダリングが行われます。

Getters(todos)は、画面をレンダリングするために必要なもの。Actions(add, update)は呼び出すことで、状態を変更し、再レンダリングを促すもの。データの流れが単一方向になっているのがわかるでしょうか。

import React, { FC, useState } from "react";
import { useContext } from "./Flux";

export const TodoComponent: FC = () => {
  const [currentDescription, setCurrentDescription] = useState("");
  const { todo: { todos, add, update } } = useContext();

  function onAddClick() {
    setCurrentDescription("");
    add(currentDescription);
  }

  return (
    <>
      <ul>
        {todos.map(({ id, description, done }) => (
          <li key={id}>
            {description}
            <input
              type="checkbox"
              checked={done}
              onChange={e => update(id, e.target.checked)}
            />
          </li>
        ))}
      </ul>
      <input
        value={currentDescription}
        onChange={e => setCurrentDescription(e.target.value)}
      />
      <button onClick={onAddClick}>追加</button>
    </>
  );
};

4. アプリコンポーネントを作る

最後に、アプリコンポーネント(一番外側のコンポーネント)を作ります。

2で作ったStoreProviderがここで登場します。 3で作ったTodoComponentをStoreProviderで包むだけです。 これにより、内部で状態が変化したときに、再レンダリングが走ります。

import React from "react";
import { render } from "react-dom";
import { TodoComponent } from "./TodoComponent";
import { StoreProvider } from "./Flux";

const App = () => {
  return (
    <StoreProvider>
      <TodoComponent />
    </StoreProvider>
  );
};

render(<App />, document.getElementById("root"));

5. テストを書く

順番が前後して申し訳ないのですが、最後におまけとして、Getters, Actionsのテストの書き方をご紹介します。

ReactのカスタムフックはReactコンポーネント内でしか呼び出すことができません。 すなわち、テスト用のReactコンポーネント内でカスタムフックを呼び出し、呼び出した結果をテストすることになるでしょう。

react-domが提供するactという関数のコールバックでレンダリングやカスタムフックが生成したGetters, Actionsを取得することができます。

今回はbeforeEachで、テスト用のコンポーネントをマウントしさらにuseTodoが返すGetters, Actionsを変数に保持しています。Specification内では得られたGetterやActionsのテストのみに注力しています。

注意点としてはact内でActionsを呼び出せるのは1回で、何度も呼び出したい場合はactを何度も書く必要があることです。actを抜けると、状態がGetterに反映されるイメージです。

テストケースは3つです。 ひとつめはtodosの初期状態が空配列であること。 ふたつめはaddすると、todosに要素がひとつ追加されること。 みっつめは、addした要素に対しupdateをかけると、その要素のdoneの状態が、指定したとおりに変更されること。

import React from "react";
import { render } from "react-dom";
import { act } from "react-dom/test-utils";
import { useTodo, Store } from "../Store";

describe("#useTodo", () => {
  let container;
  describe("useTodo called.", () => {
    let state: Store;
    beforeEach(() => {
      container = document.createElement("div");
      const App = () => ((state = useTodo()), null);
      act(() => {
        render(<App />, container);
      });
    });
    afterEach(() => (container = null));

    it("should have action and state.", () => {
      expect(state.todos).toEqual([]);
    });

    it("should add todos when 'add' called.", () => {
      act(() => state.add("foo"));
      expect(state.todos).toEqual([{ description: "foo", done: false, id: 0 }]);
    });

    it("should update done status when 'update' called.", () => {
      act(() => state.add("foo"));
      act(() => state.add("bar"));
      act(() => state.update(0, true));
      expect(state.todos).toEqual([
        { description: "foo", done: true, id: 0 },
        { description: "bar", done: false, id: 1 }
      ]);
    });
  });
});

以上のように、素のReactだけでFlux的なアーキテクチャを実現することができました。 よろしければ、ぜひお試しください。

pytest-mock使ってハマったこと

こんにちは。

SPEEDA開発チームの掛川です。

現在、私が参画しているプロジェクトではPythonを使ってサービスの開発を行なっています。 私自身、Pythonを書くのは今回が初めてなのですが、 テストを書く際にハマったことについて記事にしていきたいと思います。

環境といろいろ

・環境
    OS     Mac OS X  10.14.5
    python 3.7.3
・ライブラリ
    pytest 5.1.2
    pytest-mock 1.11.1


私が今まで参画していたプロジェクトでは、モックライブラリはMockKを使用していた(使用言語はKotlinでした)のですが 「MockitoやMockKと同じように表現したい時にpytestではどうやって書いたらいいの?」 と疑問に思いハマったことの中から今回は以下の2つの内容にフォーカスしていきます。

  • classメソッドをモックしたい時
  • 引数に応じて返す値を変えたい時


classメソッドをモックしたい時

今回は請求書を発行するシステムを想定しました。

f:id:kkyki:20191011013258p:plain
請求書のイメージ

まずは、請求書の発行を行うためのusecaseとusecaseのテストを書いてみました。
このusecaseで行いたいことは4つです。

  1. 渡されたuser_idをもとに顧客情報(customer)を取得してくる
  2. 渡されたuser_idをもとに購入情報(Details)を取得してくる
  3. 購入情報(Details)から購入の合計金額を算出する
  4. 請求書(Invoice)の発行に必要な情報をControllerに返す
class InvoiceUseCase:

    @classmethod
    def publish(cls, user_id):
        # 1.顧客情報(customer)の取得
        customer = CustomerGateway.get(user_id)
        # 2.購入情報(Details)を取得
        details = SalesHistoriesGateway.get(user_id)
        # 4. 請求書情報(Invoice)を返却
        return Invoice(
            billing_date=datetime.date(2019, 1, 1),
            customer=customer,
            details=details,
            total=details.total())  # 3. 合計金額を算出
class TestInvoiceSheetUseCase:

    def test_publish(self, mocker):
        # Customerをモックする
        customer = mocker.Mock(Customer)
        customer_gateway_mock = mocker.patch.object(
            CustomerGateway,
            'get',
            return_value=customer)

        # Detailをモックする
        detail1 = mocker.Mock(Detail)
        detail2 = mocker.Mock(Detail)
        details = Details(details=[detail1, detail2])
        sales_histories_gateway_mock = mocker.patch.object(
            SalesHistoriesGateway,
            'get',
            return_value=details)

        expected = Invoice(
            billing_date=datetime.date(2019, 1, 1),
            customer=customer,
            details=details,
            total=details.total())

        assert InvoiceUseCase.publish('user_id') == expected

        # CustomerGatewayのgetメソッドが指定した引数'user_id'で1回呼ばれたことの検証
        customer_gateway_mock.assert_called_once_with('user_id')
        # SalesHistoriesGatewayのgetメソッドが指定した引数'user_id'で1回呼ばれたことの検証
        sales_histories_gateway_mock.assert_called_once_with('user_id')

MockitoやMockKでclassメソッドをモックしたい時には以下のように
when(モックしたいクラスオブジェクト.モックしたいメソッド).thenReturn(返したい値);
every { モックしたいクラスオブジェクト.モックしたいメソッド } return 返した値
と書くことができると思いますが、 pytest-mockでは
mocker.patch.object(モックしたいクラスオブジェクト, 'モックしたいメソッド', return_value=返したい値)
または
mocker.patch('モックしたいクラスオブジェクト.モックしたいメソッド', return_value=返したい値)
と書くことができます。

引数に応じて返す値を変えたい時

上記で書いていたものは全てreturn_value を使っており、 モックしたいメソッドは毎回固定の値を返すように設定していました。
ですが、メソッドに渡された引数に応じて返す値を変えたい時はないでしょうか?
そういう場合は、side_effect を使います。
side_effect を使うと、モックしたいメソッドの代わりに side_effect で定義したメソッドが呼び出され(引数はモックしたいメソッドと合わせる)、そのメソッドが返す値がモックの返す値として使われます。


次に、顧客情報を扱うgatewayとgatewayのテストを書いてみました。
このgatewayで行いたいことは以下です。

  • 複数のuser_idを受け取り、それに対するそれぞれの顧客情報の集合体(Customers)を返す
class CustomerGateway:

    @classmethod
    def get_customers(cls, user_ids):
        return Customers(list(map(
            lambda user_id: CustomerDriver.find_by(user_id), user_ids)))
    def test_get_customers(self, mocker):
        def find_by_id(user_id):
            if user_id == 'user_id1':
                return Customer(
                    id='user_id1',
                    name='Alice',
                    zip_code=77777,
                    address='Anaheim, CA',
                    number=123456789)
            elif user_id == 'user_id2':
                return Customer(
                    id='user_id2',
                    name='Bill',
                    zip_code=88888,
                    address='New York, NY',
                    number=111111111)
            else:
                self.fail('invalid user_id!!')

        mocker.patch.object(
            CustomerDriver,
            'find_by',
            side_effect=find_by_id)

        assert CustomerGateway.get_customers(['user_id1', 'user_id2']) == Customers([
            Customer(
                id='user_id1',
                name='Alice',
                zip_code=77777,
                address='Anaheim, CA',
                number=123456789),
            Customer(
                id='user_id2',
                name='Bill',
                zip_code=88888,
                address='New York, NY',
                number=111111111)
        ])


mockerについて

mockerはPythonの標準ライブラリであるmockライブラリの薄いラッパーで、unittestで提供しているモックパッケージと同じ引数をサポートしており、 テストメソッドに引数として渡すことで使うことができます。
また、mockerの便利なところはpatchしたクラスメソッドを初期化する必要がないところです。 mockerでモックしたインスタンスは実行対象のテストメソッドの実行後に自動的にリセットされます。

mocker.patch.object()mocker.patch() はどちらを使ってもモックすることは可能ですが、両者の違いは
mocker.patch.object() は外部から注入されたインスタンスに対してモックしますが、 mocker.patch() では渡された文字列から内部で対象のクラスとメソッドの参照を取得しモックします。


上で書いたgatewayに、指定されたuser_idを受け取り対応する顧客情報(Customer)を返すメソッドを追加して
mocker.patch.object()mocker.patch() のそれぞれを使ってテストを書いてみました。

class CustomerGateway:

    @classmethod
    def get(cls, user_id):
        return CustomerDriver.find_by(user_id)
<mocker.patch.object()を使った場合>
class TestCustomerGateway:

    def test_get_customer(self, mocker):
        customer = Customer(
            id='id001',
            name='Alice',
            zip_code=77777,
            address='SEATTLE USA',
            number=123456789)
        # mocker.patch.object(
        #    モックしたいクラスオブジェクト,
        #    'モックしたいメソッド',
        #    return_value=返したい値)
        m = mocker.patch.object(
            CustomerDriver,
            'find_by',
            return_value=customer)

        assert CustomerGateway.get('user_id') == Customer(
            id='id001',
            name='Alice',
            zip_code=77777,
            address='SEATTLE USA',
            number=123456789)

        m.assert_called_once_with('user_id')
<mocker.patch()を使った場合>
class TestCustomerGateway:

    def test_get_customer(self, mocker):
        customer = Customer(
            id='id001',
            name='Alice',
            zip_code=77777,
            address='SEATTLE USA',
            number=123456789)
        # mocker.patch(
        #    'モックしたいクラスオブジェクト.モックしたいメソッド',
        #    return_value=返したい値)
        m = mocker.patch(
            'app.main.driver.customer_driver.CustomerDriver.find_by',
            return_value=customer)

        assert CustomerGateway.get('user_id') == Customer(
            id='id001',
            name='Alice',
            zip_code=77777,
            address='SEATTLE USA',
            number=123456789)

        m.assert_called_once_with('user_id')

終わりに

初めてPythonのテストを書いた私がハマったことをつらつらと書いてみましたが、
この記事が私と同じように初めてPythonのテストを書く方のお役に立てれば幸いです。

参考資料

ペアプロと育休の取得しやすさの関係について

こんにちは。SPEEDA開発チームの鈴木です。

昨年一児(娘)の父になりまして、凄い勢いで変化していく様子に喜んだり困ったりしながら過ごしております。
色々できることが増えると嬉しいのですが、それは同時にいたずらの幅が広がることも意味するんですよね。例えばものを引っ張ることを覚えたのは嬉しいのですが、私の髪の毛をひっぱってむしるのはやめていただきたい。そんな感じです。

f:id:kenji-suzuki:20191014235944p:plain:w250
髪をむしる娘の図。言葉は通じない。

今回はそんなうちの子が産まれたときに取得した育休の話をしたいと思います。
育休の話とはいっても育休をハックする話とか育児アプリとかの話ではなく、「育休を取得しやすい(と私は思う)SPEEDA開発チームの環境」についての話をします。
本編がそこそこ長い(スミマセン)ので前置きはここら辺で切り上げることとします。

男性の育休取得率

さて突然ですが、2018年度の日本における男性の育休取得率がどのくらいかご存知でしょうか?

6.16%です。

これは過去最高の取得率だそうです。
ちなみに同年の女性の育休取得率が82.2%だそうなので、過去最高とはいえども残念ながらまだまだ低い数値といえるのではないでしょうか。

このような状況の育休ですが、SPEEDA開発チームでは昨年私を含めた2人が育休を取得しています。
母数が少ないので一概に「育休取得率が高い」とは言えないですが、間違いなく育休を取得しやすい環境であると思いますし、だからこそ私は育休を取得したので、今回は私たちの環境の紹介を兼ねて次のような話をしようと思います。

1. 男性が育休を取得しなかった理由  
2.「1」の理由の原因を考える
3.「2」の原因を解決するにはどうしたらいいか  
4. SPEEDA開発チームにおいてはどうしているか  

男性が育休を取らなかった理由

では、まず一般的にどういう理由で育休が取られていないのか、2018年度のデータを見てみましょう。

f:id:kenji-suzuki:20191003170319p:plain
育休を取得しなかった理由2018

これを見ると、男性が一体どういう理由で育休を取得しなかったかがわかります。
(出展: 三菱UFJリサーチ&コンサルティング「平成29年度仕事と育児の両立に関する実態把握のための調査研究事業」)

Top3はこのようになっています。

1位: 業務が繁忙で職場の人手が不足していた(38.5%)
2位: 職場が育児休業を取得しづらい雰囲気だった(33.7%)
3位: 自分にしかできない仕事や担当している仕事があった(22.1%)

今回はこのうち2位の「職場が育児休業を取得しづらい雰囲気だった」と3位の「自分にしかできない仕事や担当している仕事があった」にフォーカスして話を展開したいと思います(人手不足の話は採用や業務効率化に絡んだ話だと思いますが、そこについては別軸の問題だと思いますので今回は言及しません)。

育休を取らなかった理由の原因を考えてみる

次に、上記2つの理由について私なりに原因を考えてみたいと思います。

職場が育休を取得しづらい雰囲気になっている原因

なぜ育休を取得しづらい雰囲気になってしまっているのでしょうか。
原因を私なりに考えてみましたが、このようなことが考えられるのではないかと思います。

1.普通の休暇すら取りづらい
普通に1日2日の休暇を取ることが難しい環境の場合、ある程度まとまった期間になるであろう育休は余計取りづらく感じることかと思います。
どういう場合に休暇が取りづらくなるのか、2つ思い当たります。
1つ目は、自分が担当している仕事に期限があり、休むことで間に合わなくなったり、後々辛くなったりと自分が困るケースです。
2つ目は、自分が休むと自分の仕事を周囲の誰かが余分に担当することになり、迷惑がかかるケースです。
1つ目のケースで休んだ分を自分でカバーできずに間に合わなくなった場合は、2つ目のように他の誰かがフォローにまわり結局周囲の負荷が増えることが考えられます。
そういうことを考えていくとやはり休みづらくなるのではないでしょうか。
また、こういった懸念は想像に過ぎず実際は自分が休んだところで大した迷惑はかからないのかもしれませんが「安心して休める仕組み」が整っていなければ心理的に休みが取りづらいということはあるでしょう。

2.周りの人が普通の休暇すら取らない
自分だけではなく、周りの人も「1」と同じように自分が困ったり、周囲への迷惑を気にして休みを取得しないような環境では休みづらくなるかと思います。
お互いをお互いで縛り合っている感じです。同調バイアスによる負の連鎖と言えるかもしれません。
休暇と同様に、子供が産まれたのに誰も育休を取得しないような環境では遠慮してしまう(遠慮しなければならない気持ちになる)のではないでしょうか。

3.男性の育休に理解がない
周りの人たちが男性の育休に対して理解がない場合、育休は取得しづらい雰囲気になるでしょう。

どうすれば育休を取得しやすい環境になるか

私が挙げた原因はどれも環境が原因となっていますが、どうしたら育休を取得しやすい雰囲気を作れるでしょうか。
「1」は色々な解決方法があるでしょうが、上述のとおり休んだ場合に自分を含めて誰も困らないような「安心して休める仕組み」があれば解決しそうです。
「2」の解決には「1」の仕組みの存在に加えて実際その仕組みが働いている必要があります。
「3」は「1」の仕組みで部分的に解決しそうです。なぜなら「3」のような人たちが育休に反対する理由は”育休を取得することによって自分や他の人たちに迷惑がかかる”ということにあったりするからです。
「誰かが迷惑するから育休には反対」とは言えても「誰にも迷惑はかからないが育休には反対」とは言えないのではないでしょうか。

このように考えてみると 「安心して休める仕組み」が存在して運用されていることが、育休を取得しづらい雰囲気を解消する方法の一つになりそうです。

安心して休める仕組みとは

休みを取得しづらい理由として「休むと自分が困る」というものがありました。
なぜ自分が休むと困る状況になっているのでしょうか。それは「仕事が個人に対してアサインされている」からではないでしょうか。
そもそも自分がやらないといけない仕事なので、誰かに代わってもらいづらいというわけです。
このような環境では他の人も同様に「自分の仕事」をもっているでしょうから。
では「仕事がチーム対してアサインされている」ならば問題はないのでしょうか。
いいえ。 チームで仕事をしていても、仕事が属人化しないようにしないと今回の文脈では結局困ることになります。
仕事が属人化していると、計画的に休む場合は引き継ぎが発生するでしょうし、突発的に休むなら引き継げない分を自力で調査したりする手間が発生するからです。
ですので「仕事がチームに対してアサイン」されており、属人化も防がれていなければなりません。

SPEEDAの開発チームではどうしているか

SPEEDAの開発チームでは基本的に「仕事はチームに対してアサイン」されます。
そして「ペアプロ」が属人化を防ぐのに役立っています(やっと本題!)。
(属人化を防ぐためだけにペアプロをしているわけではないことを補足しておきます。ペアプロには属人化の軽減以外にも沢山のメリットがあるんです。ただ今回の話では「属人化」の観点にフォーカスします。)

ペアプロについて具体的に

SPEEDAの開発チームは、すべてのチームがほぼ100%ペアプロで作業しています(ペアプロそのものについての詳しい説明は割愛させていただきます) 。
そしてプログラミング以外(例えば採用活動)でもペア作業をします。
更にペアを組むメンバーを一日のうち何度も入れ替えており、様々なメンバーと様々な開発ストーリーに取り組むことになります。
領域もUI/UX含めたフロント周りから、バックエンド、インフラ、CI/CDなどすべてを皆で担当します。
その結果として、チームの中で「フロント部分はAさんしか知らない」「バックエンド部分はBさんしか知らない」という属人化が起きなくなっています。

チームの人数が多く、かつ小規模なストーリーの場合、自分が担当する前にストーリーが終わってたということはありますが、自分しか知らないという状況にはなりません。
またチームの人数が奇数の場合は一人作業※が発生しますが、その場合は一人でも十分なストーリーを選ぶような工夫をしています。 ※一人で作業するというのは普通かもしれませんが私たちの場合ペアが基本なため一人は特別な感じです。
そのため、誰かが休んで困るということが基本的にありません。
なので用事があれば休みますし、周りがそうなので上述のように「周りが休まないから休みづらい」ということがありません。

ペア作業のイメージ

ここで補足としてペア作業についてイメージしやすいように絵で表してみることにします。
あるチームにメンバーとして、Aさん、Bさん、Cさん、Dさんがいるとします。
f:id:kenji-suzuki:20191006232514p:plain

ペアは、AさんとBさん、CさんとDさんという組み合わせとします。
環境はこのような感じです。

f:id:kenji-suzuki:20191014011451p:plain
ペアプロ環境

一つのデスクトップPCにモニター2つとキーボード2つが接続されています。
マウスが見当たらないのは画像の手抜きではなく、Thinkpadのトラックポイント付きキーボードを使っているためです。
各々に対してキーボードが存在するため、ドライバーとナビゲーターの役割がスムーズに交代できます(キーボードが一つしかなかったら、心理的にキーボードを渡してもらったり渡したりというのがやりづらくなるかと思います)。
モニターはミラーリングされていたり左右で分割されていたり、ペアによってやりやすいように変えています。

CさんDさんペアも同じ構成で作業します。
f:id:kenji-suzuki:20191014011751p:plain

作業の流れ

時間は13時、AさんとBさんペアはストーリーXに着手します。CさんとDさんペアはストーリーYに着手します。
f:id:kenji-suzuki:20191006232845p:plain

時間が14時になりました。ここでペアを入れ替えます。1コマの時間(ペア交代までの時間)は、チームで話し合って決めます。この例では1時間で交代するものとします。今度はDさんAさんペア、BさんCさんがペアになりました。横にスライドする形ですね。
f:id:kenji-suzuki:20191006234446p:plain
入れ替えの際は、作業開始時にいまどういう状況なのかの共有が行われることが多いです。
(省いていますが、本当は適宜休憩を入れます(休憩超大事!))
また、OSやディレクトリ構成などは意図的に統一しているため、別のマシンに使ったとき困りません。

時間が15時になりました。またペアを入れ替えます。CさんDさんペアと、AさんBさんペアです。
f:id:kenji-suzuki:20191006235005p:plain
これで全員がストーリーX、ストーリーY両方に少しずつ関わったことになり、特定のメンバーしかわからないということがなくなりました。
複数のストーリーに少しずつ関わるということは、一つのストーリーに100%関われないことも同時に意味し、自分の知らないコードが存在し得るようになります。
こういった問題に対しては、他のメンバーに伝えた方がよさそうなことについて(例えば設計の部分)は都度々々共有したり相談したりしながら進めることで対応しています。
メンバーが増えていくとこういった共有も大変になってきたり、上記のペアの入れ替え方法だとペアを組めない人たち(例えばAさんとCさんはペアになれない)が出てきたりと、ペア作業については話題が尽きないのですが、今回の趣旨はペア作業によって属人化が軽減されているということであるため割愛させていただきます。

チームの入れ替え

チーム内でペアの入れ替えを行っていることは上述しましたが、チームをまたいだメンバーの入れ替えも行われています。
例えばXチームとYチームという2つのチームがあったとします。 このチームに対し、それぞれが担当している仕事はそのままに、メンバーを一部入れ替えるのです。 f:id:kenji-suzuki:20191007004951p:plain

↑を↓のように入れ替える。

f:id:kenji-suzuki:20191007005034p:plain
入れ替えの頻度は決まっておらず、リソース調整の結果であったり、タイミング的なものであったりします。
また、基本的には本人の意思が最大限に尊重され本人が希望するチームに移動することになります。強い理由があって同じチームに長く残るということもあります。
このような入れ替えが行われることにより、SPEEDAの開発チームにおける属人性は極めて少なくなっているといってもよいかと思います。
※ペアプロと同様に属人化を防ぐためだけに入れ替えをしているわけではなく、知見の共有であったりチームに新たな風を起こしたりと他にも色々メリットがあるから入れ替えが行われています。

最後に

私がSPEEDA開発チームにおいて、育休を取得しづらいとは思わなかった理由は、ユーザベースという会社自体も周りの人たちも男性の育休に理解があったということもありますが、ペアプロとチームの入れ替えによって「属人化」が軽減されており「自分が休むと誰かが困る」という心理的負担がなかったことも大きいと思います。
今回は主に「属人化」という点にフォーカスしてペアプロの話をしましたが、ペアプロがもたらす良い作用は他にも沢山あるので、(いきなりは難しいかもしれませんが)興味がわいた方は是非ペアプロを取り入れてみてはいかがでしょうか。
いきなりガッツリではなく、小規模に始めたり、短時間やってみたりするだけでも「良さ」がわかるかもしれません。