Uzabase Tech Blog

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

Android Studioに入門しよう ~UI・レイアウト編~

こんにちは。SPEEDA開発チームの佐藤です。
今回はAndroid Studioに入門しようということで、
初心者向けに簡単な使い方から、今回は主にUI部品・レイアウトについて紹介したいと思います!

Android Studioとは?


JetBrains社のIntellJ IDEAをベースとしたAndroidアプリ開発のための統合開発環境で、
Windows、Mac、Linuxなど複数の環境向けに用意されています。
まずはAndroid Studioをインストールして環境を設定しておきましょう。

developer.android.com

初期プロジェクト作成


インストールしたAndroid Studioを起動してください。このような画面がでますので、
Start a new Android Studio Projectを押してください。

f:id:teddy0x0:20200319125229p:plain


そのあとテンプレートを選ぶ画面が出てきますので、 Empty Activityを選択してください。
(※テンプレートにはナビゲーションが下部についたものや、 フローティングアクションボタンが右下に表示されたものなど、様々なパターンが用意されています! 説明は省きますが、自分が作りたいアプリに近いテンプレートを選ぶことができるようになっています)

f:id:teddy0x0:20200319135348p:plain

あとは写真のようにプロジェクトに名前を付けて、
適当なフォルダを指定すれば初期プロジェクトは完成です!
ちなみに開発言語を選ぶこともできて、ひとまずJavaになっていますがKotlinも選べますので、
好きな方で開発できます!

f:id:teddy0x0:20200319135520p:plain

動作確認の方法


次にレイアウトとUI部品について紹介したいと思います。
プロジェクトを作成するとこのような画面に変わって、activity_main.xmlMainActivity.javaの二つが既にOpenされています。

f:id:teddy0x0:20200319141701p:plain

画面のレイアウトの記述はこのactivity_main.xmlに記載します。
それでは試しにボタンを表示させてみましょう。
activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal">

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="BUTTON"
        android:id="@+id/id1"/>
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="BUTTON"
        android:id="@+id/id2"/>
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="BUTTON"
        android:id="@+id/id3"/>

</LinearLayout>

記載できたら実行してみたいと思います。
Android Studioにはエミュレータと呼ばれる実行環境が用意されていて、
実機を使用しなくてもレイアウトや挙動の確認をすることができます。
エミュレータを使用するためには、実行するためのVirtual Device環境を設定する必要がありますので、
そちらを設定してからエミュレータを起動させましょう。
画像のとおりAVD Managerをクリックし
Create Virtual DeviceSelect Hardware 画面に進み、デバイスの設定をしてください。

AVD Manager
f:id:teddy0x0:20200319145605p:plain

Create Virtual Device
f:id:teddy0x0:20200319150551p:plain

Select Hardware
f:id:teddy0x0:20200319151021p:plain

System Image
f:id:teddy0x0:20200319151241p:plain

Verify Configuration
f:id:teddy0x0:20200319151431p:plain

ちなみにメモリ設定の部分はわたしの環境だとこのようになっております

f:id:teddy0x0:20200319152021p:plain

設定が終わるとこのようにエミュレータにデバイスが追加されますので
これで実行環境の設定はおしまいです。

f:id:teddy0x0:20200319152446p:plain

あとは今設定したデバイスを選んで実行ボタンを押せば、
さきほど記載したレイアウトの確認ができます。

f:id:teddy0x0:20200319152845p:plain

f:id:teddy0x0:20200319155939p:plain

ボタンが3つ表示されましたね!
エミュレータでの動作確認ができたのでレイアウトの説明に入りたいと思います。

レイアウト


UI部品をどのように配置するかはレイアウトによって決まります。
レイアウトには下記の通りいくつか種類があります。

1. LinearLayout
2. RelativeLayout
3. FrameLayout
4. CoordinatorLayout
5. ConstraintLayout

今回は基本となる1と2のレイアウトについて紹介しようと思います。
(3、4、5は動的なレイアウトを作成したいときに使います!気になる人は調べてみてください)

LinearLayout

LinearLayoutとは縦もしくは横一列に要素を並べて表示させる一番シンプルなレイアウトです。
先ほど記述したactivity_main.xmlでは、このLinerLayoutを使用しました。
それではLinerLayoutの属性について、もう少し詳しく見ていきましょう。

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal">
</LinearLayout>
  • xmlns:android=
    XML 名前空間を "http://schemas.android.com/apk/res/android" として定義している必須の記述

  • android:layout_width=
    縦の大きさを指定する属性。設定値はmatch_parentもしくはwrap_contentの二つ。
    match_parentは親の要素と同じ大きさにしたいときに、
    wrap_contentは自身のサイズと同じ大きさにしたいときに指定します。
    一番外側のレイアウトでは親の要素=画面サイズになります。

  • android:orientation=
    並べ方を縦横どちらにするかを指定する属性。設定値はhorizontalverticalの二つ。

RelativeLayout

RelativeLayoutとは要素の位置関係を相対的に決めるレイアウトのことです。
別のUI部品に対して相対的に位置を決めるか、もしくは親に対してどのように配置するかを指定できます。
では先ほどのactivity_main.xmlを次のように書き換えてみてください。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="5dp"
    android:paddingLeft="5dp"
    android:paddingRight="5dp"
    android:paddingTop="5dp"
    tools:context=".MainActivity">

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="中央寄せ"/>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:text="中央上"/>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true"
        android:text="中央下"/>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_centerVertical="true"
        android:text="中央左"/>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_centerVertical="true"
        android:text="中央右"/>

</RelativeLayout>

書き換えてエミュレータを実行すると写真のようになると思います。

f:id:teddy0x0:20200319165700p:plain

RelativeLayoutの属性についてはpaddingが指定されているだけなので割愛します。
Buttonの中で指定されている属性値については下記のとおりです。

属性名配置方法
layout_centerVerticaltrueにすると親に対して垂直方向中央寄せ
layout_centerHorizontaltrueにすると親に対して水平方向中央寄せ
layout_alignParentToptrueにすると親に対して上寄せ
layout_alignParentBottomtrueにすると親に対して下寄せ
layout_alignParentLefttrueにすると親に対して左寄せ
layout_alignParentRighttrueにすると親に対して右寄せ
layout_centerInParenttrueにすると親に対して水平垂直方向共に中央寄せ


ここまでが親に対して配置する場合の属性値です。
それでは他の部品に対する配置を行うパターンも見てみましょう。
activity_main.xmlを次のように書き換えてから、エミュレータを起動しなおしてみてください。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="5dp"
    android:paddingLeft="5dp"
    android:paddingRight="5dp"
    android:paddingTop="5dp"
    tools:context=".MainActivity">

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:text="ボタン1"
        android:id="@+id/id1"/>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:text="ボタン2"
        android:layout_toLeftOf="@+id/id1"
        android:id="@+id/id2"/>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:text="ボタン3"
        android:layout_toRightOf="@+id/id1"
        android:id="@+id/id3"/>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:text="ボタン4"
        android:layout_below="@+id/id1"
        android:id="@+id/id4"/>
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:text="ボタン5"
        android:layout_below="@id/id4"
        android:id="@+id/id5"/>

</RelativeLayout>



f:id:teddy0x0:20200319174013p:plain

他の部品に対する配置方法と属性の種類は次のようなものがあります

属性名配置方法
layout_below指定したUI部品に対し下側に配置
layout_above指定したUI部品に対し上側に配置
layout_toEndOf指定したUI部品に対し右側に配置
layout_toStartOf指定したUI部品に対し左側に配置
layout_toRightOf指定したUI部品に対し右側に配置
layout_toLeftOf指定したUI部品に対し左側に配置
layout_alignRight指定したUI部品の右端の位置とViewの右端を揃えて配置
layout_alignLeft指定したUI部品の左端の位置とViewの左端を揃えて配置
layout_alignBottom指定したUI部品の下端の位置とViewの下端を揃えて配置
layout_alignTop指定したUI部品の上端の位置とViewの上端を揃えて配置


使用するときは、android:layout_below="@id/基準にしたい部品のid"という指定の仕方で配置位置を決めます。
部品のID名はandroid:id="@+id/部品のid名"の記述で決定します。

UI部品


続いてAndroidの代表的なUI部品をいくつか説明したいと思います。
各UI部品にはイベントがあった際に特定のメソッドが呼び出されるイベントリスナーという仕組みがありますが、 本記事ではイベントリスナーの説明は省きます。
UIに対するイベント(メソッド)の設定については別の機会で詳しく説明したいと思います。

1. Button
f:id:teddy0x0:20200330172032p:plain
ボタンです。
クリックされるとonClickメソッドが呼ばれます。

@Override
public void onClick(View v) {
        println("クリックされました");
    }

2. TextView
f:id:teddy0x0:20200330174018p:plain
文字を表示するために使用するUI。

3. EditText
f:id:teddy0x0:20200330175946p:plain
ユーザーからの入力を受け付けるUI。
ボタンとセットで使用し、ボタンのonClickメソッドの中で入力値を受け取る、
というような使い方をします。

@Override
 public void onClick(View v) {
        textView.setText(editText.getText().toString());
    }


4. TimePickerDialog
f:id:teddy0x0:20200330181038p:plain
ユーザーに時刻の入力をさせたいときに使用するUI。
以下のように初期値を設定してダイアログを表示させる。

private void createTimePickerDialog() {
        TimePickerDialog timePickerDialog = new TimePickerDialog(this,
                new TimePickerDialog.OnTimeSetListener() {
                    @Override
                    public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
                        println(String.valueOf(hourOfDay) + ":" + String.valueOf(minute));
                    }
                },13, 0, true);
        timePickerDialog.show();
    }


おわりに

今回はAndroid Studioの代表的なレイアウト、UI部品について紹介してみました。
UI部品の詳細な使い方や簡単なアプリ作成までは本記事で解説できなかったので、 また別の機会で改めてこれらについての説明とアプリ作成のハンズオン記事を書ければと思います。
Android Studioについて少しでも興味が湧いた方は、ぜひ実際に触ってみてください!

Smalltalkで『オブジェクト指向設計実践ガイド』の「第3章 依存関係を管理する」をハンズオンしたら快適で楽しかった

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

前回の続きです。趣旨も同じ。

『オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方』のサンプルコードを
Ruby から Smalltalk に翻訳しながら読み進めることで、ただの写経をアクティブな学びにし、 いろいろな道草、発見をしながら楽しもう、というものです。

前回も触れましたが、やはり自分のコードとクラスライブラリの境界が無く、よいお手本がすぐに手に入るのがよいです。
わざわざドキュメントを紐解いたり、ググる必要がほぼないのですね。
Smalltalk 環境で完結します。
シンプルです。

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

今回も、わりと忠実な写経が可能でした

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

Ruby と Smalltalk がすごく似ていることは重々わかりました。
しかし、そこはアクティブ写経なので、気付きや考えたことがありました。
それを書いていきます。

ダック・タイピング良好

「 3.2 疎結合なコードを書く」では、本来的に避けられないオブジェクト間の共同作業の中で、依存関係をいかに管理するか、蜜結合なオブジェクトの巨大団子を作らないための設計テクニックがいくつか紹介されています。

まずは、「依存オブジェクトの注入」です。
コンストラクタで、 Wheel オブジェクトを注入しています。

Class {
    #name : #Gear,
    #superclass : #Object,
    #instVars : [
        'chainring',
        'cog',
        'wheel'
    ],
    #category : #'Example-OOD-gear'
}

{ #category : #private }
Gear >> setChainring: chainringInteger cog: cogInteger rim: rimInteger tire: tireFloat [
    chainring := chainringInteger.
    cog := cogInteger.
    rim := rimInteger.
    tire := tireFloat.
    ^ self
]

{ #category : #calculating }
Gear >> gearInches [
    ^ self ratio * wheel diameter
]
Class {
    #name : #Wheel,
    #superclass : #Object,
    #instVars : [
        'rim',
        'tire'
    ],
    #category : #'Example-OOD-gear'
}

{ #category : #calculating }
Wheel >> diameter [
    ^ rim + (tire * 2)
]

Ruby によるサンプルコードと同様、ダック・タイピングが使えるのでとても簡単でした。
「 diameter というメッセージに答えることが出来る、ある抽象的なもの」への依存を注入しています。

静的型付け言語の場合だと、「抽象的なもの」はインタフェースで表現されると思います。
陽に、インタフェースを定義するには名付けが必要で、そこに難しさがあると思います。
○○Able ? IWheel ? なかなかよい名前が浮かびません。
「 diameter というメッセージに答えうるもの」という概念が抽象的すぎるのでしょうか。
陰に、インタフェースがかたちづくられるダック・タイピングが、名付け問題を先送りに出来ているという意味で、ここでは有効だと思いました。
(インスタンス変数を wheel と名付けしていますが、クラス名、インタフェース名ほど目立つものではありません。)

その代わり、コンパイラが事前に実行時エラー検出してくれるとうメリットを捨てているわけですが、
以下のような心意気でテストを書いていればよいのでは、と考えています。

むしろソフトウェア開発は科学のようなものである。どれだけ最善を尽くしても正しくないことを証明できないことによって、その正しさを明らかにしているのである。
Clean Architecture 達人に学ぶソフトウェアの構造と設計 > 第4章 構造化プログラミング > テスト

上記、ダック・タイピングのWikiページの記述にもあるように、 OCaml などの言語は型推論により上記のいいとこ取りが出来るようなので、近いうちに使ってみたいなと思いました。

その他、依存を管理するテクニック

依存を隔離する」では、Wheel の初期化のためのメソッドを用意して依存をむしろ明示して局所化しています。
インスタンス変数の遅延初期化の書き方が以下のように異なりました。

# Ruby
def gear_inches
  ratio * wheel.diameter
end

def wheel
  @wheel ||= Wheel.new(rim, tire)
end
"Smalltalk"
gearInches [
    ^ self ratio * wheel diameter
]

wheel [
    wheel ifNil: [ wheel := Wheel rim: rim tire: tire ].
    ^ wheel
]

Ruby のほうがシンタックス・シュガー(||=)がある分、コード量は少ないですね。
Smalltalk にはシンタックス・シュガーが無いですが、この側面から見ると Ruby は Easy 、Smalltalk は Simple と言えると考えています。

次に、「引数の順番への依存を取り除く」です。
ここでは、その目的のためにハッシュを引数に渡すこと(デフォルト値にも対応)をしています。
同じことを Smalltalk で書くとこうなります。

setArgs: args [
    chainring := args at: #chainring ifAbsent: 40.
    cog := args at: #cog ifAbsent: 18.
    wheel := args at: #wheel.
    ^ self
]

または、デフォルト値を持つオブジェクトとのマージを使って書くと以下のようになります。

setArgs: args [
    defaults
        ifNil: [ defaults := Dictionary new.
            defaults at: #chainring put: 40.
            defaults at: #cog put: 18 ].
    defaults addAll: args.
    chainring := args at: #chainring.
    cog := args at: #cog.
    wheel := args at: #wheel.
    ^ self
]

Smalltalk ではハッシュ( Dictionary )同士のマージを addAll というメッセージで行います。
Ruby のサンプルコードでは merge というメソッド名になっていますが、
個人的には addAll という名前のほうが、キーが一致した場合に値が後勝ちすることがわかりやすい気がしていて好みです。

「自身より変更されないものに依存しなさい」というマントラ

「依存方向の管理」では、まず Gear と Wheel の依存関係を敢えて逆転させて、依存関係の方向は恣意的に選ぶものだと示します。

OOとは「ポリモーフィズムを使用することで、システムにあるすべてのソースコードの依存関係を絶対的に制御する能力」である。
Clean Architecture 達人に学ぶソフトウェアの構造と設計 > 第5章 オブジェクト指向プログラミング > まとめ

Ruby も Smalltalk も OO なので、依存関係を自由に制御できるわけですが、
選択基準として、「自身より変更されないものに依存しなさい」が示されます。
つまり、具象より抽象に依存すべきと言い換えられると思いますが、 さきほど行った Gear が Wheel を注入するようにしたのは、 依存方向は変わっていませんが、「 Wheel 」という具象より、「 diameter というメッセージに答えるもの」という抽象に依存するようにしており、結果的に依存の方向を強調したことになったと思っています。

まとめ

前回より、Ruby と Smalltalk の違いが際立たなかったので、すなおにハンズオンを進めていけました。(TDD遵守)
さらに加速して、第9章までやりきりたいと思います。

フロントエンドのコンポーネント設計で気をつけているn個のこと

はじめまして、昨年の12月に入社しました根岸です。

UZABASEに入社する前はフロントエンドエンジニアとして働いており、ここ1年間くらいはReactとTypeScriptの開発ばかりやっていました。 今回はフロントエンドのコンポーネントを設計するときに気をつけていることについてまとめます。

対象読者

  • ReactやVueなどフロントエンドフレームワークで開発している人
  • コンポーネントの設計に自信がない人
  • 拡張性の高いコンポーネントを作りたいと思っている人

Propsの名前に一貫性をもたせる

例えばページ内でスタイルをあわせるためにButtonコンポーネントを作ったとしましょう。

Buttonコンポーネントがクリックされたときに関数を呼び出したいとき、どうやって関数をpropsに渡しますか? 多くのひとがonClick propsに渡そうするでしょう。

もしclick propsに渡す必要があったら驚くでしょう。 なぜならば標準DOM要素<button>の場合はonClickに渡せばよいから。

標準DOM要素と同じようなことをしたい場合ときは、標準DOM要素のprops名に合わせましょう。

  • クリックしたときに関数を実行 -> onClick
  • フォームの送信時に関数を実行 -> onSubmit
  • link先のurl -> href (toが使われる場合も多い)
// 良い例
const Button = ({onClick, children}) => (
  <button onClick={onClick}>{children}</button>
)
// アンチパターン
const Button = ({click, children}) => (
  <button onClick={click}>{children}</button>
)

標準DOM要素と同様の役割を持つコンポーネントのpropsは標準DOM要素に合わせる

atom層のコンポーネントは標準DOM要素と同じ役割をもつケースが多く、標準DOM要素をラップするだけのことがよくあります。 その場合は標準DOM要素のpropsをすべて使えるようにしましょう。

どうやって実装するかというとすべてのpropsをラップした標準DOM要素に渡せばよいです。 スプレッド演算子を使うことでまとめてpropsを渡すことができます。

// 良い例
const Button = ({color, ...props}) => {
  // propsで受け取ったものをすべて<button>に渡す
    return <button style={{background: color}} {...props} />
}

const Example = () => {
  const handleClick = () => alert('click')
  return <Button type="button" disabled={false} name="example">ボタン</Button>
}
// アンチパターン
const Button = ({color, children}) => {
    return <button style={{background: color}}>{children}</button>
}

const Example = () => {
  const handleClick = () => alert('click')
  // typeやdisabledをButtonは<button>に渡していない
  // Buttonにtypeやdisabledを渡しても使えない
  return <Button type="button" disabled={false} name="example">ボタン</Button>
}

TypeScriptの場合はJSX.IntrinsicElementsから標準DOM要素のpropsの型を取得してReact.FCに渡せば良いです。

// 良い例
const Button: React.FC<JSX.IntrinsicElements['button']> = props => {
    return <button style={{background: 'red'}} {...props} />
}

styled-componentやemotionなどで使えるstyledを使うとより簡単です。

// 良い例

// Buttonはbuttonのpropsを継承する
const Button = styled.button`
  background: red;
`

レイアウトをコンポーネントに切り出す

f:id:t-NeGI:20200329164536p:plain
2カラムページ

上の図のようなページのコーディングを考えます。

図は左側にメニュー、右側にメインのコンテンツを表示する2カラムのレイアウトを持っています。 このようなレイアウトは他のページでも再利用されることが考えられます。 再利用される場合はレイアウトに関してもコンポーネントに切り出してコードの共通化をしましょう。

下の例ではレイアウトに関することはTwoColumnLayoutに切り出しています。 レイアウトをコンポーネント化する場合はpropsにReactElementを受け取るときれいに切り出すことができます。 レイアウトをTwoColumnLayoutに切り出したことによって、Pageコンポーネントは左側と右側のカラムにMenuとMainを表示することだけに関心を持てばよいコードになっています。

// 良い例
const Page = () => (
  <>
    <Header />
    <TwoColumnLayout>
      <Menu />
      <Main />
    </TwoColumnLayout>
  </>
)

const TwoColumnLayout = ({ children }) => (
  <div style={{display: 'flex'}}>
    <div style={{flex: '1 1 30%'}}>{children[0]}</div>
    <div style={{flex: '1 1 70%'}}>{children[1]}</div>
  </div>
)
// 良い例 (TypeScript版)
const TwoColumnLayout:React.FC<Props> = ({ children }) => (
  <div style={{display: 'flex'}}>
    <div style={{flex: '1 1 30%'}}>{children[0]}</div>
    <div style={{flex: '1 1 70%'}}>{children[1]}</div>
  </div>
)

interface Props {
  children: [React.ReactElement, React.ReactElement];
}

下のコードのPageコンポーネントはレイアウトの情報を持っています。 このように特定のコンポーネントがレイアウトの情報を持つと、共通のレイアウトを1つのコードで管理できなくなってしまいます。 また、コードの見通しも悪くなります。

// アンチパターン
const Page = () => (
  <>
    <Header />
    <div style={{display: 'flex'}}>
      <div style={{flex: '1 1 30%'}}>
        <Menu />
      </div>
      <div style={{flex: '1 1 70%'}}>
        <Main />
      </div>
    </div>
  </>
)

インタラクティブな部分もコンポーネント切り出す

f:id:t-NeGI:20200329164443g:plain
クリックすると開閉するメニュー

上の図のようなアイコンをクリックすると開くメニューのコーディングを考えます。

このメニューはアイコンをクリックすると開いたり閉じたりできます。 またメニューにはフェードイン・フェードアウトのアニメーションがついています。

「アイコンをクリックしたら要素がフェードイン・フェードアウトする」という動作は、メニュー以外でも使うことがあるでしょう。 その場合はレイアウトと同様にコンポーネントに切り出して再利用できるようにしましょう。

クリック時の動作をコンポーネントへ切り出さずにコーディングすると下のようになります。 ToggleMenuは表示する要素に加えて、開閉の状態やフェードイン・フェードアウトに関するスタイルなどクリック時の動作に関する役割も持っています。

// クリック時の動作をコンポーネントに切り出す前のコード
const ToggleMenu = ({ menuItems }) => {
  const [isOpen, setIsOpen] = useState(false);
  const handleClickButton = useCallback(() => setIsOpen(isOpen => !isOpen), [
    setIsOpen
  ]);

  const opacityStyle = isOpen ? { opacity: "1" } : { opacity: "0" };
  return (
    <>
      <FiMenu size="30" onClick={handleClickButton}/>
      <ul
        style={{
          ...opacityStyle,
          transition: "opacity 0.2s",
          border: "1px solid",
          width: "170px",
        }}
      >
        {menuItems.map(item => (
          <li>{item}</li>
        ))}
      </ul>
    </>
  );
};

下の例ではクリックしたときの動作に関することはToggleElementWrapperに切り出しています。 ToggleElementWrapperはクリックされる要素(clickableElement)と表示・非表示される要素(toggledElement)をpropsで受け取っています。 prosで受け取ったclickableElementtoggledElementにonClickとstyleをそれぞれのpropsに渡すことでクリック時に要素を表示・非表示する動作を実現しています。

また、ToggleElementWrapperを利用する側のToggleMenuは表示する要素だけに関心を持てば良いコードになっています。 開閉の状態やアニメーションに関してToggleMenuは一切考える必要がありません。

// 良い例
const ToggleMenu = ({ menuItems }) => {
  const clickableElement = <FiMenu size="30" />;
  const toggledElement = (
    <ul
      style={{
        border: "1px solid",
        width: "170px"
      }}
    >
      {menuItems.map(item => (
        <li>{item}</li>
      ))}
    </ul>
  );

  return (
    <ToggleElementWrapper
      clickableElement={clickableElement}
      toggledElement={toggledElement}
    />
  );
};

const ToggleElementWrapper = ({ clickableElement, toggledElement }) => {
  const [isOpen, setIsOpen] = useState(false);
  const handleClickButton = useCallback(() => setIsOpen(isOpen => !isOpen), [
    setIsOpen
  ]);

  const opacityStyle = isOpen ? { opacity: "1" } : { opacity: "0" };

  // clickableElementにonClick propsを追加している
  const clickableElementAddedOnClickProps = React.cloneElement(
    clickableElement,
    { onClick: handleClickButton }
  );

  // toggledElementにスタイルに関するpropsを追加している
  const toggledElementAddedOpacityStyle = React.cloneElement(toggledElement, {
    style: {
      ...toggledElement.props.style,
      ...opacityStyle,
      transition: "opacity 0.2s"
    }
  });
  return (
    <>
      {clickableElementAddedOnClickProps}
      {toggledElementAddedOpacityStyle}
    </>
  );
};

単一責任を意識してコンポーネントを作る

オブジェクト指向プログラミングでは単一責任の原則という設計の考え方があります。 Reactのコンポーネントを作るときも単一責任の原則を意識しましょう。

例としてレイアウトのコードを再掲してそれぞれのコンポーネントの役割について考えてみます。

const Page1 = () => (
  <>
    <Header />
    <TwoColumnLayout>
      <Menu />
      <Main />
    </TwoColumnLayout>
  </>
)

const TwoColumnLayout = ({ children }) => (
  <div style={{display: 'flex'}}>
    <div style={{flex: '1 1 30%'}}>{children[0]}</div>
    <div style={{flex: '1 1 70%'}}>{children[1]}</div>
  </div>
)

const Page2 = () => (
  <>
    <Header />
    <div style={{display: 'flex'}}>
      <div style={{flex: '1 1 30%'}}>
        <Menu />
      </div>
      <div style={{flex: '1 1 70%'}}>
        <Main />
      </div>
    </div>
  </>
)

Page1Header,Menu,MainTwoColumnlayoutというコンポーネントで表示するという役割だけを持っていて具体的なスタイルについて何も知りません。 またTwoColumnLayoutは具体的なスタイルの情報だけを持っています。 Page1TwoColumnLayoutはそれぞれ表示する要素をまとめるという役割と具体的なスタイルを定義する役割だけを持っており、単一責任の原則が守られています。

一方、Page2は表示するコンポーネントの情報とスタイルの情報の2つを持っています。 これは単一責任の原則違反であり、Page1TwoColumnLayoutのようにコンポーネントを分割すべきです。

propsを増やすことでコンポーネントのバリエーションを増やさない

f:id:t-NeGI:20200329164557p:plain

上の図のようなECサイトの商品紹介用のボックスについて考えます。 このボックスのコンポーネントを下のコードのように作ったとします。 Productは画像のsrc,商品名,価格をpropsから受け取って表示します。

const Product= ({image, name, price}) => {
  return (
    <Box>
      <Image src={image} />
      <Name>{name}</Name>
      <Price>価格:{price}円</Price>
    </Box>
  )
}

後日、セール時の価格を下の図のように表示したくなったとしましょう。

f:id:t-NeGI:20200329164608p:plain

このときどうやってコーディングすべきでしょうか。 安直に考えるとProductを拡張して、セールのときはセール価格をpropsで受け取り表記を変えれば実装できそうです。

const Product= ({image, name, price, salePrice}) => {
  return (
    <Box>
      <Image src={image} />
      <Name>{name}</Name>
      {/* salePriceがあれば打ち消し線を引く */}
      <Price style={!!salePrice ? {textDecoration: "line-through" } : {}}>
        価格:{price}円
      </Price>
      {/* salePriceがあるときだけ表示する*/}
      {!!salePrice && (
        <Price>セール価格:
          <Text color={"red"}>{salePrice}円</Text>
        </Price>
      )}
    </Box>
  )
}

しかし、salePriceをpropsに追加したおかげでProductのなかでセールのときとそうではないときの制御をしないといけなくなってしまいました。 また、さらに電子版や中古の商品があるときの価格を追加したい要件があったらどうなるでしょう。 Productにpropsを追加していくとどんどんカオスになっていってしまいます。

このようなコンポーネントのバリエーションを増やしたいときは、propsを増やすことで対応するのではなくて別のコンポーネントに分けることで対応しましょう。

const Product= ({image, name, price}) => {
  return (
    <Box>
      <Image src={image} />
      <Name>{name}</Name>
      <Price>価格:{price}円</Price>
    </Box>
  )
}

const SaleProduct= ({image, name, price, salePrice}) => {
  return (
    <Box>
      <Image src={image} />
      <Name>{name}</Name>
      <Price style={{textDecoration: "line-through" }}>価格:{price}円</Price>
      <Price>セール価格:
        <Text color={"red"}>{salePrice}円</Text>
      </Price>
    </Box>
  )
}

ProductSaleProductに分けることで、セールとそうでない場合の制御がなくなりシンプルなコードになりました。

ProductSaleProductを見るとコードの重複が気になる人もいるかも知れません。 しかし無理に1つのコンポーネントにまとめてコードを複雑にすることよりも、別のコンポーネントにわけてコードの重複を許容しつつシンプルなコードにしたほうが長期的に見て得だと私なら考えます。

まとめ

コンポーネントの設計に関しては資料もすくないため、初めはどうするのがよいか悩む人も多いと思います。 この記事を読んで、コンポーネント作るときに少しでも参考にしていただけると嬉しいです。

Mockitoを使ってDartでのTDDを加速させよう

初めて会社のブログに書きます。SPEEDA事業でCTOをしている林です。 TDDをこよなく愛する身として今日はDartでTDD、そしてテストの独立性を担保していく上で欠かせないMockライブラリーのMockitoについて書こうと思います。

Mockitoとは

Dart開発チームが作成している公式Mockライブラリーです。

名前の通りJavaにおいてメジャーなMockライブラリーの1つであるMockitoにインスパイアされたもので、DartでMockオブジェクトを使う場合においてもっともメジャーな選択肢となっています。

では使い方を見ていきましょう。Mockライブラリーを使ってテストを書いたことがある人であれば特に違和感なく使えると思います。

※以下単にMockitoと記述する場合はDartのMockitoを指します。

今回Mock化するクラス

以下のクラスに対してMockの振る舞いを定義していきたいと思います。

class Count {
  final int countValue;
  final int unit;
  Count(this.countValue, this.unit);

  int get nativeValue => countValue;

  Count increment() => Count(countValue + unit, unit);

  Count changeUnit(int newUnit) => Count(countValue, newUnit);

  Future<Count> sampling() => Future.value(Count(0, unit));

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is Count &&
          runtimeType == other.runtimeType &&
          countValue == other.countValue &&
          unit == other.unit;

  @override
  int get hashCode => countValue.hashCode ^ unit.hashCode;
}

事前準備

Mock用クラスを定義する

違和感無く使えると言ってすぐで申し訳ないのですが、この事前準備は他のMockライブラリーではあまり見ない形です。下記のようにモック対象のクラスをimplementsしてMockitoのMockクラスをextendsします。

import 'package:mockito/mockito.dart';

class CountMock extends Mock implements Count{}

正直な所、この一手間が毎回面倒だなと感じてしまう部分です。そんな面倒な事がなぜ必要かと言うと、Dartはリフレクションが非推奨になっているため裏でいろいろゴニョゴニョ黒魔術を使うのが難しいというのが大きな理由になります。*1 じゃあどうやってるのかというとextendsするMockクラスの方でNoSuchMethodをオーバーライドしていてその中でいろいろハンドリングをしています。通常、暗黙的インターフェースを実装したのみであればCountMockはコンパイルエラーになりますが、MockクラスでNoSuchMethodをオーバーライドしているためコンパイルエラーにはなりません。これを利用してMockitoはMockライブラリーとして必要な機能を提供しています。*2

ちょっと前置きが長くなってしまいましたが実際の振る舞い定義等を見ていきましょう。

振る舞いを定義する

下記のようにwhenでmock化したいメソッドを指定し、thenReturnで戻り値を定義します。

class CountMock extends Mock implements Count{}

void main() {
  test('incrementの振る舞いに関するサンプル', (){
    var mock = CountMock();
    when(mock.increment()).thenReturn(Count(0,1));
    expect(mock.increment(), Count(0, 1));
  });
}

振る舞いを定義する(Future、Streamの場合)

import 'package:mockito/mockito.dart';
import 'package:test/test.dart';

void main() {
  test('戻り値がFutureの場合に関するサンプル', () async {
    var mock = CountMock();
    when(mock.sampling()).thenAnswer((_) => Future.value(Count(1,1)));
    expect(await mock.sampling(), Count(1, 1));
  });
}

非同期のメソッドに対して振る舞いと戻り値を設定する場合は thenReturn ではなく thenAnswer を使用する必要があります。戻り値もFutureやStreamにしないといけない点などが面倒ですが thenReturn にしている場合はエラーになるので注意が必要です。

検証する

一般的なMockライブラリーと同様にMockitoでもメソッド呼び出しの検証が可能です。この検証を記述する事で「実は重要なメソッドを呼び出してなかった」というのを防げますし、TDDをしていく上でより良い設計の指針にもなります。

import 'package:mockito/mockito.dart';
import 'package:test/test.dart';

void main() {
  test('検証に関するサンプル', (){
    var mock = CountMock();
    when(mock.increment()).thenReturn(Count(0,1));

    expect(mock.increment(), Count(0, 1));

    verify(mock.increment());
  });
}

検証する(回数チェック)

対象のメソッドが何回呼び出されたかというチェックをしたい場合に使います。

import 'package:mockito/mockito.dart';
import 'package:test/test.dart';

void main() {
  test('検証に関するサンプル(回数)', (){
    var mock = CountMock();
    when(mock.increment()).thenReturn(Count(0,1));

    expect(mock.increment(), Count(0, 1));

    verify(mock.increment()).called(1);
    verify(mock.increment()).called(greaterThan(0));
    verifyNever(mock.nativeValue);
  });
}
  • calledにて指定した回数の検証
  • calledにはMatcherを指定可能
  • verifyNeverにて一度も呼び出されない事を検証

検証する(引数チェック)

検証時に引数を柔軟にチェックする事が出来ます。引数のチェックには柔軟性を持たせたい場合などに使います。

import 'package:mockito/mockito.dart';
import 'package:test/test.dart';

void main() {
  test('引数の検証に関するサンプル', (){
    var mock = CountMock();
    
    when(mock.format('回目です')).thenReturn('2回目です');

    expect(mock.format('回目です'), '2回目です');

    verify(mock.format(argThat(startsWith('回'))));
  });
}

argThat を使用します。引数にMatcherを受け取るので独自にMatcherを作成して検証する事も可能です。

検証する(呼び出し順序)

Mock化したオブジェクトの各メソッドがどういう順番で呼び出されたかというのを検証する事が出来ます。

import 'package:mockito/mockito.dart';
import 'package:test/test.dart';

void main() {

  test('呼び出し順序検証に関するサンプル',(){
    var mock = CountMock();
    when(mock.increment()).thenReturn(Count(0,1));
    when(mock.nativeValue).thenReturn(0);

    expect(mock.nativeValue, 0);
    expect(mock.increment(), Count(0, 1));

    verifyInOrder([
      mock.nativeValue,
      mock.increment()
    ]);
  });
}

verifyInOrder を使って呼び出されるべき順序通りに記述する事で呼び出される順番の検証が出来ます。

Fakeクラス

自分は使ったことが無いのですがFakeクラスというのが用意されています。これを使うとオーバーライドしてテスト用に独自に振る舞いを定義したメソッド以外を呼び出すとエラー(テストが失敗)になります。

import 'package:mockito/mockito.dart';
import 'package:test/test.dart';

class CountFake extends Fake implements Count{
  @override
  int get nativeValue => 10;
}

void main() {
  test('Fakeのサンプル', (){
    var mock = CountFake();
    mock.increment(); // OK
    mock.increment(); // UnimplementedErrorがThrowされる
  });
}

出来ない事

Mockitoでは出来ない事が結構あります。

  1. インスタンス化(new)のMock
  2. staticメソッドのMock
  3. 拡張関数のMock

リフレクションを使用していないというのが理由なのですが、JMockit等の強力なMockライブラリーに慣れている人等は不都合に感じるかと思います(実際自分はそうでした)

まとめ

Mockitoは上記のように出来ない事も結構あります。ただし逆に上記のような制限がある事でいろいろな依存関係を整理するきっかけになるとも思っています。インスタンスはRepositoryやPort等から必ず取得するように意識したり、staticなメソッド等は小さくラップするクラスを作って結合度を緩めたり。昔JMock*3を使い始めた時はそういうのを意識しないと綺麗なテストが書けなかったので、結果的に自分の設計力が上がったと思っています。なんでもやってくれるMockライブラリーは逆に設計に対する示唆に欠ける可能性があるとポジティブに考えてMockitを使うのが精神衛生上良いかなと思います!

以上になりますがいかがでしたでしょうか。それでは皆さんDartでも良いTDDライフをお送りください!

*1:dart:mirrorsがあるのですが、JSへのトランスパイルやFlutterでのビルド時のサイズが肥大化し、パフォーマンスも悪くなるためです

*2:Rubyのmethod_missing的なもの

*3:名前が似てますが上記のJMockitとは別。やれる事はJMockitほど多くなかった

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

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

理屈編では、まずリレーショナルデータベース(以下、RDB)の論理設計やその後において、
正規化ルールを運用する難しさについて述べました。

主な要因として、
例えば正規化を一度完了したテーブルに対し SELECT した結果もまたテーブル*1
つまりは正規形であることが求められるため、 SELECT するたびにいちいちその結果について、
「第 n 正規化についてクリアだな…次に第 n + 1 正規化についてはどうかな…」などと、
ルールに当てはめてチェックしなければならないことを挙げました。

リレーショナルモデルの世界で、常に正しい処理結果を得るため、
リレーショナル演算の閉包性を維持しなければならないためです。

そこで、ルールに頼らなくてもそれが依って立つリレーショナルモデルさえ正しく理解していれば常に、自然に正規形がつくれるのではないか、という仮説のもとモデルの説明をしました。

今回は、実際に設計を行いながら、それを確かめてみることにします。
正規化ルールを一旦忘れて取り組んでみます。

こんなデータをもらったら…

よくある飲食店のレシートです。

f:id:yhamaro:20200314083602p:plain
とある日

店主から依頼され、この取引を記録したいとしましょう。
この取引があったという事実を私が責任を持って永続化するのです。

とりあえず

レシートにある情報をもらさず、ひとつの取引事実としてRDBに格納しました。
(「合計」のような導出データは入れません。)

番号 日付 品目 種別 単価(円) 数量 店舗
1 2020/02/29 ラーメン, 餃子, ビール 麺飯類, 冷菜・点心, ドリンク 650, 450, 630 1, 2, 3 西東京_久我山店

ところで、店主が「月末の集計が楽しみだな。なにせウチは西東京でトップの売上らしいから…。」と言っているのを耳にしました。
ここ久我山店は「西東京」というエリアに属するらしいので、店舗名にその情報も加えておきました。

また、このチェーン店らしきお店で使っているレジは全店舗に設置されており、
レシートの番号は日毎に全店共通の連番として払い出されていることもわかりましたので、
日付と番号で一意キーを設定しておきます。(属性の下に二重線で表しています)

これでいいのだろうか…

今時点で、このレシート情報を記録、蓄積したものをどのように使うのかはよくわかっていません
誰かが(私かもしれない)がクエリでアドホックに分析を行うのか、これをデータストアとしたWebアプリケーションが後々できるのか…。

でも、そんなことを気にする必要はありません
リレーションに登録するのは事実であり、今時点で捉えたい事実を正しく登録できるよう設計すればよいのです。
分析要件やアプリケーションの仕様によって、リレーションに登録された事実が変わることは無いからです。
そういう意味では、RDBの論理設計はアプリケーション設計から独立していると言えるでしょう。

なので、今は自信と確信を持って事実をより正しくリレーションにマッピングすることに集中しましょう!

f:id:yhamaro:20200315081429p:plain
コックタイのほうを優先しました

最初に気になること

リレーションに登録するデータは命題の集合であることを前回述べました。
ここで、一部の属性を取り出して以下の述語を考えてみます。

(種別) として、 (品目) を注文しました。

これに登録したデータを当てはめて命題にしてみます。

麺飯類, 冷菜・点心, ドリンク として、 ラーメン, 餃子, ビール を注文しました。

この命題は事実を正しく表現できているでしょうか。
なにを言っているのか、わかりませんね。
いやわかるよ、という人も頭の中で以下の命題に分解しているはずです。

麺飯類 として、 ラーメン を注文しました。
冷菜・点心 として、 餃子 を注文しました。
ドリンク として として、 ビール を注文しました。

使用には、用法を守って正しく

前回、リレーションに格納された命題に順序が無いことを述べましたが、 リレーショナルモデルでは順序の概念を意図的に排除しています。
上記の設計のように属性の値に順序の概念を持ち込むとしたら、それを扱える演算を一緒に用意しなければなりません。

例えば、品目「ラーメン」の種別が知りたい場合、該当のレコードを取り出し、品目と種別の値をカンマで分割し、順序をまもってマッピングした後、「麺飯類」にたどり着けます。
そんな演算を実装できたとしても、少なくともリレーショナルモデルには持ち込みたくないですね。

なので、同じことをリレーショナル演算の一部である射影( SQL の SELECT ) と制限( SQL の WHERE ) で出来る設計に変えます。

番号 日付 品目 種別 単価(円) 数量 地域名 店舗名
1 2020/02/29 ラーメン 麺飯類 650 1 西東京 久我山店
1 2020/02/29 餃子 冷菜・点心 450 2 西東京 久我山店
1 2020/02/29 ビール ドリンク 630 3 西東京 久我山店
SELECT 種別 FROM 取引 WHERE 品目 = 'ラーメン';
-> '麺飯類'

できました。

わかったこと

上記の例は順序という概念で分割できるものでしたが、意味的に単一で無いものを属性の値として格納するのは(それを扱える演算をリレーショナルモデルに追加できないかぎり)避けるべきことがわかりました。
秒で反省して、「店舗」属性も、「地域名」と「店舗名」に分割しています。

ところで、ここで行った作業によって設計がリレーショナルモデルにとってふつうのカタチになったので、この作業を指して Normalization と呼び習わすことにします。

次に気になること

次の客が来て、ラーメンだけ食べて帰ったのでそれを記録します。

番号 日付 品目 種別 単価(円) 数量 地域名 店舗名
1 2020/02/29 ラーメン 麺飯類 650 1 西東京 久我山店
1 2020/02/29 餃子 冷菜・点心 450 2 西東京 久我山店
1 2020/02/29 ビール ドリンク 630 3 西東京 久我山店
2 2020/02/29 ラーメン 麺飯類 650 1 西東京 久我山店

ここでまた、一部の属性を取り出して以下の述語を考えてみます。

(品目) は、 (種別) である。

これに登録済みのデータを適用し、命題にしてみます。

ラーメン は 麺飯類 である。
餃子 は 冷菜・点心 である。
ビール は ドリンク である。
ラーメン は 麺飯類 である。

同じことを2度言っていますね
前回、事実は一度言えば十分であると述べました。
そうでなければならない理由は、この三千世界に遍在する多重管理による問題と同じです。
例えば、以下のような間違った命題が混入するのを避けるためです。

ラーメン は ドリンク である。

「種別」属性と同じく品目に従属する、「単価」属性においてはもっとシビアな問題が発生するかもしれません。
そこで、設計を見直して以下のようにしました。

番号 日付 品目 数量 地域名 店舗名
1 2020/02/29 ラーメン 1 西東京 久我山店
1 2020/02/29 餃子 2 西東京 久我山店
1 2020/02/29 ビール 3 西東京 久我山店
2 2020/02/29 ラーメン 1 西東京 久我山店


品目 種別 価格(円)
ラーメン 麺飯類 650
餃子 冷菜・点心 450
ビール ドリンク 630

同じことを一度しか言わなくなりました
同時に閉世界仮説により、『ラーメンはドリンク説』も完全に否定されました。

また、よく見ると以下も潜んでいるのでそれも一度しか言わなくなるようにします。

この日のレシート番号 1 番は 久我山店 に発行しました。
この日のレシート番号 1 番は 久我山店 に発行しました。
この日のレシート番号 1 番は 久我山店 に発行しました。
番号 日付 品目 数量
1 2020/02/29 ラーメン 1
1 2020/02/29 餃子 2
1 2020/02/29 ビール 3
2 2020/02/29 ラーメン 1


品目 種別 価格(円)
ラーメン 麺飯類 650
餃子 冷菜・点心 450
ビール ドリンク 630


番号 日付 地域名 店舗名
1 2020/02/29 西東京 久我山店
2 2020/02/29 西東京 久我山店

ところで、ここで行った作業によって設計がリレーショナルモデルにとってよりふつうのカタチになったので、この作業も Normalization と呼べるでしょう。
先程の作業結果を前提にしており、段階的なカタチを区別するために
最初のものを 1st Normal Form 、今出来たものを 2nd Normal Form と呼び習わすことにします。

もう気になっていたこと

もう気づいていましたが、まだ2回同じことを言っているところがあります。

久我山店 は 西東京 エリアの店舗です!
久我山店 は 西東京 エリアの店舗です!

以下のとおり設計を見直します。

番号 日付 品目 数量
1 2020/02/29 ラーメン 1
1 2020/02/29 餃子 2
1 2020/02/29 ビール 3
2 2020/02/29 ラーメン 1


品目 種別 価格(円)
ラーメン 麺飯類 650
餃子 冷菜・点心 450
ビール ドリンク 630


番号 日付 店舗名
1 2020/02/29 久我山店
2 2020/02/29 久我山店


店舗名 地域名
久我山店 西東京

ところで、ここで行った作業も Normalization と呼べるでしょう。
先程の作業と同じような基準で、同じようなことをした気がしますが、
先程の作業で一意キーにばかり気を取られていた自分を戒めるためにも、
今出来たカタチをきちんと区別して 3rd Normal Form と呼び習わすことにします。

おわりに

リレーショナルモデルのあるべきカタチに導かれて、無事に第3正規形( 3rd Normal Form )までたどり着けました。
論理設計は、だいたいここまでやれば大丈夫です。
設計はもちろん、リレーションを操作する場合も考えれば、
「正規化ルール」というカードをあえて持たないことが、最高の手なのかもしれません。

*1:SELECT が UNION のように一意性を保つ結果を返してくれると随分良くなると思います。 今は、 SELECT DISTINCT と意識的に書かなくてはいけません。 SELECT ⇔ UNION ALL、 SELECT DISTINCT ⇔ UNION というように表記の対象性を損なっているし、一意性は常に保つべきなので、 SELECT ALL ⇔ UNION ALL(一意性を保たない)、 SELECT ⇔ UNION(一意性を保つ) というようにすべきかと思います。

Ktor で小さな API を作る

こんにちは。SPEEDA 開発チームの緒方です。

システムをマイクロサービスで構成するメリットのひとつに、採用する技術にバリエーションを持たせることができるという点が挙げられると思います。

実際、SPEEDA でも様々な言語・フレームワークを利用してマイクロサービスを開発しています。 その中でも Kotlin はかなり多くのプロジェクトで採用されている言語です。

Kotlin で利用できるフレームワークと言えば Spring Boot など Spring 系のものが真っ先に思い付くと思うのですが、本当に小さな API を作りたい場合には少し大袈裟すぎる気もします。

今回はそういう場合に手軽に使える Ktor という小さなフレームワークを使った API について、簡単に紹介していきたいと思います。

ベースとなるプロジェクトの作成

まずはベースとなる Ktor プロジェクトを作成します。(Maven を使ったバージョン。)

公式の Maven - Quick Start - Ktor あたりが参考になると思います。

pom.xml の dependency に Ktor の依存関係を追加します。 サーバエンジンには Netty を利用します。

<dependency>
    <groupId>io.ktor</groupId>
    <artifactId>ktor-server-netty</artifactId>
    <version>${ktor.version}</version>
</dependency>

これで準備は完了です。最初のアプリケーションを作ります。

適当なパッケージに main 関数を作成します。

package sample

import io.ktor.application.call
import io.ktor.response.respondText
import io.ktor.routing.get
import io.ktor.routing.routing
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty

fun main() {
    embeddedServer(Netty, 8080) {
        routing {
            get("/") {
                call.respondText("Hello, world!")
            }
        }
    }.start(true)
}

実行してみます。

$ mvn exec:java -Dexec.mainClass=sample.MainKt

エラーなく実行できたら、curl コマンドを使って動作を確認します。

$ curl -v localhost:8080
*   Trying ::1:8080...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.68.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Length: 13
< Content-Type: text/plain; charset=UTF-8
<
* Connection #0 to host localhost left intact
Hello, world!

無事に実行できました!これでベースとなるプロジェクトは完成です。

簡単な説明

まず embeddedServer 関数を使って、このプロセスで起動するサーバを生成しています。第一引数の Netty はサーバエンジンとして Netty を利用することを明示しています。

ポイントはこの関数に渡しているブロックです。このブロック内でサーバの各種設定を行っています。 このブロックのレシーバは io.ktor.application.Application のインスタンスで、このインスタンスを操作することでサーバの設定をすることができます。

例えば、routing 関数は名前の通り API のルーティングを表す io.ktor.routing.Routing を登録するものです。 io.ktor.application.Application の拡張関数として定義されているので設定変更を行う関数としてこのブロック内で呼び出すことができます。

get は Routing を構成する Route を登録する関数で、パス "/" の HTTP GET メソッドに対応する Route を登録しています。

Route は Routing に対して複数設定することができます。試しに Route を増やしてみます。

package sample

import io.ktor.application.call
import io.ktor.response.respondText
import io.ktor.routing.get
import io.ktor.routing.routing
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty

fun main() {
    embeddedServer(Netty, 8080) {
        routing {
            get("/") {
                call.respondText("Hello, world!")
            }
            get("/ping") {
                call.respondText("Pong")
            }
        }
    }.start(true)
}
$ curl -v localhost:8080/ping
*   Trying ::1:8080...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /ping HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.68.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Length: 5
< Content-Type: text/plain; charset=UTF-8
<
* Connection #0 to host localhost left intact
Pong

Route を増やすことができました。

このように、Ktor では拡張関数とブロックを使った DSL (的な記法) で様々な設定を行っていきます。 慣れるまでは理解しにくいかもしれませんが、Kotlin の言語仕様をうまく使って直感的に定義できるようにした上手い記法だと思います。

レスポンスを JSON にする

さて、ここまではテキストでレスポンスを返していましたが、もちろん JSON 形式でレスポンスを返すこともできます。

Ktor には Feature と呼ばれる様々な機能が予め用意されています。

例えば認証を行ったりレスポンスヘッダを付与したりなど、一般的な用途であれば大抵のものに対しては自前で実装することなく利用が可能です。

レスポンスを JSON に変換する Content Negotiation と呼ばれる Feature もそのひとつです。

まず、pom.xml に依存を追加します。(Jackson を使ったバージョンを使うことにします。)

<dependency>
    <groupId>io.ktor</groupId>
    <artifactId>ktor-jackson</artifactId>
    <version>${ktor.version}</version>
</dependency>

次に、embeddedServer に対して Feature を install し、テスト用のルーティングを追加します。

package sample

import io.ktor.application.call
import io.ktor.application.install
import io.ktor.features.ContentNegotiation
import io.ktor.jackson.jackson
import io.ktor.response.respond
import io.ktor.response.respondText
import io.ktor.routing.get
import io.ktor.routing.routing
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty

fun main() {
    embeddedServer(Netty, 8080) {
        install(ContentNegotiation) {
            jackson {
                enable(SerializationFeature.INDENT_OUTPUT)
            }
        }
        routing {
            get("/") {
                call.respondText("Hello, world!")
            }
            get("/ping") {
                call.respondText("Pong")
            }
            get("/json") {
                call.respond(ResponseJson("JSON response"))
            }
        }
    }.start(true)
}

data class ResponseJson(val message: String)

リクエストしてみます。

$ curl -v localhost:8080/json
*   Trying ::1:8080...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /json HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.68.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Length: 27
< Content-Type: application/json; charset=UTF-8
<
* Connection #0 to host localhost left intact
{
  "message":"JSON response"
}

これだけです。めっちゃ簡単ですね。

jackson 関数に渡されるブロックのレシーバは Jackson の ObjectMapper となりますので、JSON 変換のオプションを指定したい場合はそのブロック内で指定します。

まとめと問題点

Ktor について簡単にまとめてみます。

  • サーバの設定は embeddedServer 生成時のブロック内で行うことがきる。(ルーティングなど。)
  • Feature と呼ばれる予め用意された機能を利用できる。使う場合は install 関数を呼び出して登録する。

ここまでのコード例で (本当にミニマムな) API サーバであれば作成することができると思います。 ですが同時に、改善の余地はかなり残っています。

例えば、

  • ルーティングが増えたら embeddedServer の初期化ブロックが肥大化するのでは?
  • リクエストハンドラは routing 内に記述する?
  • 設定ファイルの読み込みは?
  • DI コンテナは併用できない?

などです。

このあたりについては、改善案を紹介していければと思います。

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章までやりきりたいと思います。