Uzabase Tech Blog

SPEEDA, NewsPicks, FORCASなどを開発するユーザベースの技術チームブログです。

Sealed Secretsを利用したKubernetes Secretリソースのセキュアな管理

はじめに

はじめまして、UZABASE SPEEDA SREの鈴木(@sshota0809)です。

今回は、Sealed Secretsを利用したKubernetesのSecretリソースをセキュアに管理する方法を紹介します。

目次

TL;DR

KubernetesのSecretリソースはBase64の形式にエンコードされるため、エンコードされた内容が確認できれば簡単にデコードできてしまいます。 そのため、SecretリソースをGitHubなどで公開してしまうと機密情報を盗まれてしまうリスクがあります。

一方で、昨今はGitOpsが流行っていたりとKubernetesのリソース(マニュフェストファイル)をGitHub等など管理する需要は高く、当然Secretリソースもその例外ではありません。

  • Bitnamiが開発するOSSであるSealed Secretsを利用することで、Secretリソースをセキュアに管理することが可能となる

Sealed Secretsとは

概要

Bitnamiが開発している、KubernetesのSecretリソースをセキュアに管理することを目的としたOSSです。

github.com

Sealed Secrets以外にもSecretリソースのセキュアな管理を実現するツールは存在しますが、Sealed Secretはアーキテクチャやできる事がシンプルで学習コストが極めて低いという部分が魅力だと思います。

アーキテクチャ

f:id:sshota0809:20200310103551p:plain
Sealed Secretsアーキテクチャ

まず、Sealed Secretsのアーキテクチャを構成しているメインとなるリソースは下記となります。

  • sealed-secretsコントローラー(を管理するDeployment)
    • Sealed Secretsのコアとなるコントローラー。kind: SealedSecretのCRDをCreate/Update/Deleteを監視する
  • 公開鍵/秘密鍵のペアを格納したSecretリソース
    • Secretリソースを暗号化/復号化するために利用するキー。

上記のリソースを利用し、下記のような流れでSecretリソースを管理します。

  1. Sealed Secretsによって生成された公開鍵を利用しkubesealコマンドによってSecretリソースをパース
    • kubesealコマンドによってパースすることでkind: SealedSecretリソースという機密情報が公開鍵で暗号化されたマニュフェストファイルが生成される
  2. kubectlコマンドでSealedSecretリソースをKubernetesクラスタにデプロイ
  3. sealed-secretsコントローラーがSealedSecretリソースのデプロイを検知
  4. sealed-secretsコントローラーがSealed Secretsによって生成された秘密鍵を利用しSealed SecretsリソースをSecretリソースにパースしデプロイ
    • 1の手順で利用した公開鍵のペアとなっている秘密鍵を利用しパースすることで暗号化された内容が復号化されたマニュフェストファイルがデプロイされる

このように、暗号化されたSealedSecretリソースをKubernetesクラスタにデプロイすることで、sealed-secretsコントローラがそれを検知し、自動的に暗号化された内容が復号化されたSecretリソースをクラスタ内部にデプロイしてくれます。

また、SealedSecretリソースは暗号化されているため、GitHub等に公開したとしても対となる秘密鍵を知っている人にしか復号化することができません。 これによりセキュアにSecretリソースをGitHub等で管理することが可能になります。

インストール〜リソースデプロイ

アーキテクチャを説明したところで、実際にインストールからSecretリソースのデプロイまでを行いたいと思います。

インストール

今回はhelmを利用してインストールを行います。 helmのチャートは下記リポジトリにstableなものが公開されています。

github.com

$ git clone https://github.com/helm/charts.git
$ helm template charts/stable/sealed-secrets --name sealed-secrets --namespace sealed-secrets > sealed-secrets.yaml
$ kubectl apply -f sealed-secrets.yaml

helmによって必要なリソースがすべてデプロイされます。 それではデプロイされたリソースの一覧を見てみます。

$ kubectl get all -n sealed-secrets
NAME                                 READY   STATUS    RESTARTS   AGE
pod/sealed-secrets-fff45fbcf-29mrg   1/1     Running   0          4d10h

NAME                     TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
service/sealed-secrets   ClusterIP   10.60.174.242   <none>        8080/TCP   77d

NAME                             READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/sealed-secrets   1/1     1            1           77d

NAME                                        DESIRED   CURRENT   READY   AGE
replicaset.apps/sealed-secrets-fff45fbcf    1         1         1       33d

$ kubectl get secret -n sealed-secrets
NAME                         TYPE                                  DATA   AGE
default-token-94w7c          kubernetes.io/service-account-token   3      77d
sealed-secrets-keysmg5q      kubernetes.io/tls                     2      77d
sealed-secrets-token-59zdg   kubernetes.io/service-account-token   3      77d

重要なのはSecretリソースの一覧に表示されているsealed-secrets-keysmg5qというリソースです。 こちらが公開鍵と秘密鍵のペアを定義しているSecretリソースとなります。

リソースの中身は、確かに下記のようにtls.crttls.keyが定義されているのが確認できます。

apiVersion: v1
data:
  tls.crt: .........
  tls.key: .........
kind: Secret
...

インストール手順の一貫として、SecretリソースをSealedSecretリソースにパースする際に利用するkubesealコマンドをインストールします。 筆者の環境はmacOSのため、それじ準じた手順としております。

$ brew install kubeseal

Secretリソースのパース

それでは、インストールが完了し準備が整ったのでSecretリソースをパースしてSealedSecretリソースを生成します。

繰り返しにはなりますが、kubesealコマンドによってSecretリソースをパースする際、Sealed Secretsによって生成された公開鍵が必要となります。 そのため、何らかの方法でそれを抜き出す必要があります。

やり方は自由なのですが、kubesealコマンドでは公開鍵のexport機能があるため、それを利用します。

$ kubeseal --fetch-cert \
  --controller-namespace=sealed-secrets \
  --controller-name=sealed-secrets \
  > pub-cert.pem

こうすることで、kubesealコマンドを実行しているローカルの環境にpub-cert.pemという形で公開鍵をexportすることができます。 それでは、この公開鍵を使ってSecretリソースをパースしたいと思います。

今回利用するテスト用のSecretリソースは下記となります。

test-secret.yaml

apiVersion: v1
data:
  test.txt: cGFzc3dvcmQK
kind: Secret
metadata:
  name: test-secret
  namespace: sealed-secrets
type: Opaque

下記コマンドでパースを行います。

$ kubeseal --format=yaml --cert=pub-cert.pem < test-secret.yaml > test-sealedsecret.yaml

すると下記のようなtest-sealedsecret.yamlが生成されました。

test-sealedsecret.yaml

apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  creationTimestamp: null
  name: test-secret
  namespace: sealed-secrets
spec:
  encryptedData:
    test.txt: AgCKSFmH7nARRrUmQSMsJs5YuRiaFe43eG7ynScc3yKAsJaOUSWyCAyGHauyU0jGlySqhXV2LNnhEtzR1gVIsi2ScWb1+fLOqsw7ronOkmVO8XwddII4ytE9Cb8UA5plZgy6ujRaea++Zr85UuXwLavsQRSMp/rne4bJ7bvng1a4znjSRN5JXPoAIpf4zUWKoLUrhOt6HHhmS8VZ8tM7yf+uS3t3dqDLYfuGStH6ECKszQaCFlNUdokTBv0th1V2rzbzgeRTT0eA3inThsFCa+cnVGqUeM3EdvTLxIltRCQsLBQJmkmGug8xATkPFDWAvn9SLvow4jHcQolzVrRSXZCVs3oyceriK6f2k/Umohs2g78u9lCGRWzrBp+kWsdfQh+cv7+mZvr46evYbAQUJ7pbRk6axXwPJmD3GCg9oIk6RtB/ug8AIUjdkI81RELfMcliQYuySSb2GF8+hkCAgMXLBRZkS1oxCHinbn0AOeRQbm87QwcXFpICBWlyliLtjZ7oa9QB53zRf3pbxsNcxYSTzA6fZEH8GFopADjuZ8OvS2qT83I9ULPhONb2Y7wJkDrpuKQIBqzRsMgv7zMQD16TilzFcXtEVWkDz+Zjwsoio/+lJ83QLJLkGZw5Y8p4rW9D0lfqOo2W0l1q6BpiH4TmZawLmP5TOxQ+xwcWfHtDf6nzJLwvfzsZRhqrmjXD3j+pexf3QeqI3aA=
  template:
    metadata:
      creationTimestamp: null
      name: test-secret
      namespace: sealed-secrets
    type: Opaque
status: {}

Kubernetesクラスタへのデプロイ

SealedSecretリソースが生成できたら、最後にこれをKubernetesクラスタにデプロイします。

$ kubectl apply -f test-sealedsecret.yaml

すると下記のようにSealedSecretリソースとコントローラーがパースしたSecretリソースそれぞれがデプロイされたことが確認できます。

$ kubectl get sealedsecret -n sealed-secrets
NAME          AGE
test-secret   49s

$ kubectl get secret -n sealed-secrets
NAME                  TYPE                                  DATA   AGE
test-secret      Opaque                                      1      55d

後はこのSecretリソースを通常通り各種リソースで利用するだけです。

おわりに

Sealed Secretsは学習コストも低く、できる事もSecretリソースを暗号化/復号化という部分にフォーカスされているのでとてもシンプルです。 GitOpsを実践しようと思っているけどSecretリソースの管理をどうしよう、と悩んでる方ぜひ利用してみてください。

弊社SPEEDAチームではGitOpsはまだ実践できていませんが、アプリケーションに利用する機密情報を一部Sealed Secretsを利用し管理し始めたりしています。

方法より原理 〜正規化ルールとリレーショナルモデルについて〜 【理屈編】

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

アプリケーションデータの永続化を担うデータストアには様々な選択肢があります。
その1つとして、リレーショナルデータベース(以下、RDB)がありますが、
RDBを選択した場合、データの容れものとしてリレーショナルモデルを選択した、という表明になります。
ひいては、このモデルを正しく使用することが生産性の観点から必要となります。
(明白な設計によるコミュニケーションや制約によるデータ不整合の回避など)
その方法の1つとして正規化ルールがあります。

正規化ルール遵守は有効か

あの星野源さんも知っているはずという、正規化ルールですが、基本情報技術者の試験範囲でもあり、エンジニアであれば少なくとも聞いたことはあり、多かれ少なかれ意識しているものだと思います。
このルールをみんなで正しく運用できればよいのですが、それにはいくつかの阻害要因があると考えています。

「あたりまえ」と軽視されがち

正規化のWikiページの解説は、厳密な定義として書かれているため読みやすくはないと思いますが、
一度理解に達すると「あたりまえのことしか言っていないな」と思うでしょう。
そう考えて安心してしまうと、このルールを積極的に遵守しようという姿勢は失われます。
また、「常識」でしかないのにルールとして厳然と存在しているため疎ましく感じることすらあるかもしれません。

「設計時のみに発生するタスク」という誤解

(ルールを軽視せず)RDBのテーブル設計を行う際に、設計の妥当性を確認するためのチェックリストとして正規化ルールを適用するのはよいのですが(付番されているためチェックリストになりやすいですね)、それで終わりではありません
設計し終わったテーブルに操作を加えるとまた新しいテーブルが生まれます。
ここで言う操作とはリレーショナル演算のことで(SQLでは例えば単体のSELECT文もそのひとつで「射影」という操作です)、
すべてのリレーショナル演算は入力と出力に同じモノを想定します(閉包性*1を持つ、と言います)。
つまり、テーブルをSELECTした結果もまたテーブルだということです。
その出力結果であるテーブルを新たな操作対象(入力)として見た際には当然、正規化ルールが適用済みであることが期待されます。
急いでさっきのチェックリストのレ点を全部消しましょう!
だいぶ疎ましく感じてきました…。

以上のことから、RDBを正しく扱うために、正規化ルールの適用はもちろん必要ですが、ただ「正規化ルールを守りましょう!」というスローガンを掲げるだけでは、効果的とは言えないと考えました。
まさに、言うは易し…ですね。

f:id:yhamaro:20200302123615p:plain
すてちまおう

正規化ルールを忘れ、モデルに導かれて、正規化ルールに至る

正規化をルールとして運用することは難しいことがわかりました。
それならばまず、原点に立ち返る意味で、一度正規化ルールを忘れ、正規化ルールが依って立つところの背景、原理を理解しましょう。
そして、その原理のみによって自然に導かれて設計を行った結果と、正規化ルールを適用したそれとが少なくとも同等であるなら、「ルール」を「運用」する必要が無くなると考えました。
あるべき姿さえ正しく捉えていれば、ルールに縛られずとも大筋で間違うことはなく、且ついつでもどこでも(基底テーブルでも、派生テーブルでも)正しい設計ができるのではないでしょうか。
その原理とはリレーショナルモデルそのものです。

f:id:yhamaro:20200302123639p:plain
こころのめでみるのだ

リレーショナルモデルとは

リレーショナルモデルのリレーション(SQLのテーブルに相当)のデータ構造はひとことで、
「属性のドメイン値の、取りうる組み合わせのすべてを規定しているもの*2」です。
なので、リレーションを設計する行為は上記に当てはめると、「…のすべてを規定すること」と言えます。

ひとことで言ってもわかりにくいと思うので順を追って説明していきます。

まず、「属性のドメイン値」について。
「属性」(SQLの列に相当)には名前と型があります。
その名前と型に規定された、取りうる値の集合がドメインです。
例えば、よく交差点で見かける車両用の信号機の色は赤、青、黄ですが、これをひとつの属性としてみてみると、
「交通信号」という属性名に、「色」という型付けがなされて { '赤', '青', '黃' } というドメインを形成します。

次に、「取りうる組み合わせのすべて」について。
仮に「ハリウッド俳優たちの代表作」というリレーションがあり、
その属性が {'俳優名', '映画名' } の2つ、
ドメイン値がそれぞれ {'ポール・ニューマン', 'デニス・ホッパー' } の2つ、
および、 {'暴力脱獄', 'タワーリング・インフェルノ', 'ブルーベルベット' } 3つだとしたときに、
以下の通り、組み合わせで6通りのタプルが設計上想定されるものとして決定されます。

俳優名映画名
ポール・ニューマン暴力脱獄
ポール・ニューマンタワーリング・インフェルノ
ポール・ニューマンブルーベルベット
デニス・ホッパー暴力脱獄
デニス・ホッパータワーリング・インフェルノ
デニス・ホッパーブルーベルベット

これで設計は終わりです(ちなみに属性名の下線は一意キーを表しています。上記だと { '俳優名', '映画名' } の複合キーです)。
あとはこの属性の設計が表している、下記の述語に対して、真の命題(SQLの行に相当)を入れていくことになります。

ハリウッド俳優、 (俳優名) の最高傑作は『 (映画名) 』である

デニス・ホッパーについては『イージーライダー』をまだ観ていないので、この中から最高傑作を選ぶことは出来ません。
また、『ブルーベルベット』にポール・ニューマンは出演していないのでこれも真の命題になりません。
『タワーリングインフェルノ』と『暴力脱獄』ですが、私は圧倒的に後者の方が好きなので、以下のタプルのみ真の命題としてこのリレーションに入れることにします。

( 'ポール・ニューマン', '暴力脱獄' )

これを入れると、述語に当てはめて、以下の命題が真であることを世界に表明したことになります。

ハリウッド俳優、 ポール・ニューマン の最高傑作は『 暴力脱獄 』である

他の5つのすべて組み合わせについては、"未表明"などというあいまいな扱いではなく、偽であることを表明したことになります。(閉世界仮設に基づいています)

ハリウッド俳優、 ポール・ニューマン の最高傑作は『 タワーリング・インフェルノ 』ではない
(残り4つも同じ)

また、命題は事実について述べているので重複しません
大事なことは2回言ったほうがいいかもしれませんが、事実自体は変わりません。
あと、複数の命題間で順序はありません
大事なことは先に言ったほうがいいかもしれませんが、どちらでも事実自体は変わりません。

リレーションとその設計、そこに値やタプルをマッピングすることが何を意味しているかを述べました。
また、リレーションは先述したリレーショナル演算子が処理できるものである必要があります。
リレーションとリレーショナル演算子がリレーショナルモデルを構成します。
リレーショナルモデルがどんなものであるか、おおよその雰囲気を掴んでいただけたかと思います。

この続き

実践編では、上記の理解だけに基づき、つまり正規化ルールに依らず設計行為を行ったらどうなるかを実例をもって確かめてみたいと思います。
尚、私が書いていることは『データベース実践講義』に書いてあることの理解に基づいています。正確な定義や語彙についてはこの書籍を当たるとよいと思います。

*1:テキストに対してUnixコマンドが、S式に対してLisp関数が、オブジェクトに対してSmalltalk関数が持っていると考えています。

*2:ひとつの数式で現すと、 L ⊆ X1 × … × Xk となります

GraalVMに入門する

はじめに

はじめまして、2019年11月に入社しましたヒロオカ(@yuya_hirooka)です!!

先日、同僚との雑談の中でQuarkusのNative Image化が話題に挙がりました。その際に「そういえば、GraalVMとかNative Imageってよく聞くけどあまり知らないなぁ...」と思い色々と調べてみたのでまとめます。 この記事ではGraalVMの中核技術の概要を把握し、簡単にその機能を使ってみることをゴールとします。

前提知識

記事を読む上で以下のようなことを前提知識として進めます。
(つまるところ、私自身のレベル感です)

  • GraalVMという名前は聞いたことがある
  • Javaもしくはその他のJVM言語を使ってプログラムを書いたことがある
  • JVMやコンパイラに関するふんわりとした理解
    • JITコンパイル、AOTコンパイルと聞いてなんとなくどういったものかイメージがつく
    • JVMのC1、C2と聞いてなんとなくどういったものかイメージがつく

GraalVMとは

GraalVMはユニバーサルなVMで、JVM言語はもちろんのことJavaScript、Pythonなど動的言語、LLVM-basedなネイティブコード等の様々な言語を実行できます。
GraalVMは大きく以下2つの特徴を持っています。

  • Polyglot
  • Native

Polyglot

Weblioの訳では「数か国語に通じている人」と説明されています。訳の通りGraalVMは多言語対応されており、JVM言語以外の言語の実行環境としても動作します。
また、JavaからJavaScriptのAPIを呼び出すなど、多言語を混ぜたコードの記述が可能で、各言語のエコシステムを活かしたコーディングが可能になります。

Native

GraalVMではJIT(Just In Time)コンパイルとは別に、AOT(Ahead Of Tim)コンパイルと呼ばれるコンパイルを行なうことができ、JavaのコードをNative Imageと呼ばれるスタンドアローンで実行可能な形にコンパイルすることができます。Native Imageに関して詳しくは後述します。

GraalVMの中核技術

GraalVMはJVMの機能を包括しており、JVMのできることは基本的にすべて行えます。また、HotSpotVMをベースに作られています。
JVMのとは違ったGraalVMにおける中核の技術として以下の3つが挙げられます。

  • Graal
  • Truffle
  • GraalVM Native Image

Graal

GraalはGraalVMのにおけるJITコンパイルラです。JVMCI(JVM Compiler Interfae)を利用してC2コンパイラを置き換えます。JVMCIはJEP 243で定義されるJavaで、JITコンパイラを記述する際のインタフェースです。
これまでのJVMにおけるC2コンパイラやCompiler InterfaceはC++で実装されていますが、GraalVMでのGraalやJVMCIはJavaでの実装がされています。

f:id:yuya_hirooka:20200122144503p:plain
GraalVM

GraalとGraalVMは別物で、GraalVMというとJVMの機能を包括したVMを指し、GraalはJITコンパイラの事を指します。また、GraalはJava10以降のOpenJDKからの利用も可能です。(JEP 317)

Truffle

Truffleは言語実装フレームワークで、GraalVM上でJVM以外の言語が動作する際に使われる仕組みです。
このフレームワークを利用することにより、GraalVMはPolyglotなVMとして、さまざまな言語を動作させることができると同時に、独自の言語を実装することも可能です。

f:id:yuya_hirooka:20200122144537p:plain
Truffle

GraalVM Native Image

前述のようにGraalVMではJavaのコードをAOTコンパイルすることによりNative Image化を行なうことができます。
ここで、Native Imageは以下のようなものを含みます。

  • 依存関係にあるアプリケーションクラス群
  • 実行時に利用するJDKのクラス群
  • 静的にリンクされたJDKのネイティブコード
  • 「Substrate VM」と呼ばれるメモリーマネジメントやスレッドのスケジューリングなどの機能を持った実行時に必要なコンポーネント

Native Image化を行なうと以下のような特徴を得ることができます。

  • クラスローダなどのオーバヘッドがなくなり、起動が早くなる
  • JVM言語のメモリフットプリントを削減することができる

上記のような特徴からコンテナ化やサーバレスアーキテクチャでの利用の際にメリットがあると思われます。
ここで、注意すべきなのは、JITコンパイルではなくAOTコンパイルを行なうことにより、JITコンパイルの特徴(ホットスポットなコンパイル、OSの差異を吸収してくれる等)はなくなるということです。
また、リフレクションやダイナミックプロキシなどを行なう場合は、対象クラスを追加で設定するなど追加の作業が必要なようです。

余談ですが、リフレクションやダイナミックプロキシを多様しているSpring FramewarkでのNative Image化は複雑な設定を必要としたようですが、Springは5.3のアップデートで追加設定なしのNative Image化のサポートがされるようです。

Hello World

GraalVMの概要についてまとめたところで、実際に使ってみたいと思います。具体的にはHello Worldとして以下のことをやってみたいと思います。

  • GraalVMのインストール
  • JavaのHello Worldコードの実行する
  • JavaからJavaScript(Node.js)を呼び出す
  • Native Image化を行なう

実行環境

Hello Worldを実行する環境を以下にまとめます。

  • Ubuntu(18.04.3 LTS)
  • GraalVM(19.3.1)

GraalVMのインストール

まずはGraalVMをインストールします。

$ wget https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-19.3.1/graalvm-ce-java11-linux-amd64-19.3.1.tar.gz

$ tar -zxvf graalvm-ce-java11-linux-amd64-19.3.1.tar.gz

$ mkdir /usr/lib/graal-vm

$ mv graalvm-ce-java11-19.3.1/ /usr/lib/graal-vm/

$ export PATH=/usr/lib/graal-vm/graalvm-ce-java11-19.3.1/bin:$PATH 

以上で、インストールは完了です。 インストールするディレクトリはどこでも良いですが。今回は/usr/libとしました。
Javaのバージョンを確認してみます。

$ java -version
openjdk version "11.0.6" 2020-01-14
OpenJDK Runtime Environment GraalVM CE 19.3.1 (build 11.0.6+9-jvmci-19.3-b07)
OpenJDK 64-Bit Server VM GraalVM CE 19.3.1 (build 11.0.6+9-jvmci-19.3-b07, mixed mode, sharing)

GraalVMをインストールすると、通常のJavaコマンドとは別に以下のコマンドが利用可能になります。

  • js : JavaScriptのコンソール起動コマンド
  • node : GraalVMのJavaScriptエンジンを利用したNode.js用のコマンド
  • lli : GraalVMに組み込まれたLLVMバイトコードインタプリタ用のコマンド
  • gu (GraalVM Updater) : RubyやPythonのLangageパッケージ等をインストールするためのコマンド

JavaでHello world

まずは、よくあるJavaのHello Worldのコードを動かしてみたいと思います。
HelloWorld.javaを作成し以下のソースコードを記述します。

public class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Hello, World!");
  }
}

コンパイルと実行は通常のJavaと同様にjavacjavaコマンドで行います。

$ javac HelloWorld.java
$ java HelloWorld
Hello, World!

ここまではとくに面白みのない見慣れたHello Worldです。

JavaからJavaScriptを呼び出す

GraalVM上で、JVM言語からゲスト言語(JVM言語以外の言語)をThe GraalVM Polyglot APIを利用することで可能となります。
たとえば、JavaからJavaScriptを呼び出すコードを以下のように記述します。

import org.graalvm.polyglot.*;

public class HelloPolyglot {
  public static void main(String[] args) {
    System.out.println("Hello, Java!");

    // Polyglot API
    Context context = Context.create();

    // 単純なJavaScriptの実行
    context.eval("js","print('Hello, JavaScript!');");

    // JavaScriptの関数を定義
    Value function = context.eval("js","x => print('Hello, ' + x);");
    function.execute("Ployglot!");
  }
}

org.graalvm.polyglot.Contextクラスはゲスト言語に対する、実行環境を提供します。
実際のコードの実行はevalメソッドを利用します。第一引数に言語のIDを第二引数に実際のコードを記述します。
また、evalの実行結果をorg.graalvm.polyglot.Valueクラスで受け取ることにより、引数ありの関数としても定義することもできます。

コンパイルと実行は通常のJavaと同様にjavacjavaコマンドで行います。

$ javac HlloPolyglot.java
$ java HelloPloyglot
Hello, Java!
Hello, JavaScript!
Hello, Ployglot!

このようにして、GraalVMではPolyglotなコードの記述が可能となります。

Native Image化を行なう

GraalVMのもう1つの大きな機能であるNative Image化を試して見たいと思います。

native-imageコマンドのインストール

GraalVMのComunity Editionでは、Native Image化にnative-imageコマンドを利用できます。native-imageコマンドのインストールはguを利用できます。
また、native-imageコマンドを利用するためには以下の3つが事前にインストールされている必要があります。

  • glibc-devel
  • zlib-devel
  • gcc

以下のコマンドで、native-imageコマンドをインストールします。

$ gu install native-image

HelloWorldクラスをNative Image化

さきほどのJavaのHello Worldコードをネイティブイメージ化し、その実効速度の差を見てみます。

$ native-image HelloWorld

# JITコンパイルでの実行
$ time java HelloWorld
real    0m0.072s
user    0m0.075s
sys 0m0.025s

# Native Imageの実行
$ time ./helloworld
real    0m0.002s
user    0m0.002s
sys 0m0.000s

実行タイミングや環境にもよりますが、プログラムの実行速度を表すuserを比較すると30~100倍ほど早く実行されます。
前述していますが、Native Image化を行って早くなるのはあくまで起動速度みたいなので、デーモンのような常駐するプロセスでは恩恵が薄れていくと思われます。

HelloPolyglotクラスをNative Image化

今度は、JavaScriptを呼び出していたHelloPolyglotのクラスをNative Image化したいと思います。 この場合注意が必要で、さきほどと同じコマンドでNative Image化を行なうと実行時に以下のようなエラーが吐かれます。

$ native-image HelloPolyglot
$ ./hellopolyglot 
Hello Java!
Exception in thread "main" java.lang.IllegalStateException: No language and polyglot implementation was found on the classpath. Make sure a language is added to the classpath (e.g., native-image --language:js).
    at org.graalvm.polyglot.Engine$PolyglotInvalid.noPolyglotImplementationFound(Engine.java:831)
    at org.graalvm.polyglot.Engine$PolyglotInvalid.buildEngine(Engine.java:752)
    at org.graalvm.polyglot.Engine$Builder.build(Engine.java:506)
    at org.graalvm.polyglot.Context$Builder.build(Context.java:1444)
    at org.graalvm.polyglot.Context.create(Context.java:709)
    at HelloPolyglot.main(HelloPolyglot.java:6)

これは、通常のコンパイルではJavaScriptエンジンがImageの中に含まれないからです。 エラーメッセージにかかれている通り--language:jsフラグを指定して再度コンパイルしてみます。

$ native-image --language:js HelloPolyglot

$ ./hellopolyglot 
Hello Java!
Hello JavaScript!
Hello Ployglot

今度は実行できました。

終わりに(感想)

GraalVMについて調べて、少しだけその機能を試してみました。
前述しましたが、Truffleを使えば俺々言語みたいなのも実装できそうですし、理解が深まればまだまだ面白そうな点があるように感じました。
GraalVMで検索すればより質の高い記事や資料が日本語でたくさん見つかるので、より詳しくはそちらを参照してください。

参考資料

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