Uzabase Tech Blog

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

Vue.jsでComposition APIを使ってクリーンアーキテクチャ

こんにちは!
Saas Product Teamの板倉です。

今回は少し前にバージョン3がリリースされたVue.jsとComposition APIを使ってクリーンアーキテクチャをどう組むのかを書いてみたいと思います。
クリーンアーキテクチャについてはこちらを参照ください

今回のエントリーで使用したバージョンは以下の通りです。

Vue: 3.0.0
Typescript: 3.9.7

作成したコードはこちら

準備

まずはプロジェクトを作っていきましょう!
vue-cliを使って作っていきます。
vue-cliが入っていない方は yarn global add @vue/cli を実行してインストールしましょう。

vue create <PROJECT_NAME>

Manually select features を選択して Typescript を追加で選択し、Vueのバージョンを3とします。
全体としては、以下のように選択しました。

Vue CLI v4.5.9
? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, TS, Linter
? Choose a version of Vue.js that you want to start the project with 3.x (Preview)
? Use class-style component syntax? No
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)?
 Yes
? Pick a linter / formatter config: Prettier
? Pick additional lint features: Lint on save
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files

始める前に

そもそもComposition APIって何?

コンポーネントロジックの柔軟な構成を可能にする、関数ベースのAPIです。
こちらはVue2でも使用可能です。
Vue2ではオプショナルなプラグインとして提供されていましたが、Vue3では標準で使用可能となりました。
※ 2020年11月時点ではVue3はまだIE11に対応していません。レガシーブラウザに対応する必要がある方はVue2.xを使いましょう。

Composition API RFC | Vue Composition API

作っていきましょう

モジュールを作る

クリーンアーキテクチャの登場人物である各レイヤーのモジュールを作っていきます。
依存関係については先に挙げた作成コードをご覧いただければと思いますので、説明については割愛します。

  • Entity
  • UseCase
  • Gateway
  • Controller
  • Presenter
  • Driver

上記の他に、画面に表示する内容を管理するためのViewStateを作ります。

モジュールの準備

Composition APIの inject, provideを使って実装します。
inject, provideは setup関数から実行する必要がありますので、モジュールの登録は Application のセットアップで行います。

export default defineComponent({
  name: "App",
  setup() {
    provide(Keys.TaskViewStateKey, reactive(new TaskViewState()));
    provide(Keys.TaskStorageKey, new TaskStorage());
    provide(
      Keys.TaskGatewayKey,
      new TaskGateway(inject(Keys.TaskStorageKey)!!)
    );
    provide(
      Keys.TaskPresenterKey,
      new TaskPresenter(inject(Keys.TaskViewStateKey)!!)
    );
    provide(
      Keys.TaskUseCaseKey,
      new TaskUseCase(
        inject(Keys.TaskGatewayKey)!!,
        inject(Keys.TaskPresenterKey)!!
      )
    );
    provide(
      Keys.TaskControllerKey,
      new TaskController(inject(Keys.TaskUseCaseKey)!!)
    );
  }
});

ここで大事なのは画面で参照するViewStateは reactive で包むことです。
そうすることでViewStateの値を更新するだけで画面が更新されるようになります。

Componentから利用する

VueComponentでモジュールを取得するのも同じようにsetup関数で行います。

<template>
  <h1>タスク一覧</h1>
  <task-list :tasks="state.values"></task-list>
</template>
export default defineComponent({
  name: "TodoList",
  components: {
    TaskList
  },
  setup: () => {
    const controller = inject(Keys.TaskUseCaseKey)!!;
    const state = inject(Keys.TaskViewStateKey)!!;
    onBeforeMount(() => {
      controller.find();
    });
    return {
      state
    }
  }
});

今回作ったアーキテクチャの処理の流れ

以下の図は処理の流れを簡単に示したものです。

f:id:diskit:20201120151333j:plain

VueComponentでは値を取得して更新するのではなく、処理の呼び出しを行うだけです。
上の図の青い部分以外はフレームワークに依存しないので、テストも書きやすいです!

まとめ

Vuexを使うと少し頑張らないとクリーンアーキテクチャは組みづらかったのですが、Composition APIを使うことでより簡単に組むことができると思いました。
作るアプリケーションのサイズによってはtoo muchだなと思われるかもしれません。状況に応じて崩すのもありだと思います。
ただ、しっかりとアーキテクチャを作っておくことでアプリケーションの成長に伴って手を出しづらくなるのを予め防ぎやすくなると思います。

さいごに

一緒にプロダクトを開発する仲間を募集しています!
Saas Product Teamってどんな感じなの?という疑問に対しては、以下の記事が参考になるかと思います!

journal.uzabase.com

少しでも興味のある方は一度お話ししませんか?
以下のリンクからエントリーしていただけると嬉しいです

SPEEDA - ソフトウェアエンジニア(サーバー/フロント) - Uzabase, Inc.

WebComponentsを使ってみよう(その1)

こんにちは、SaaS Product Teamのとみたです。

今回は、WebComponentsについて調べてみたことを書きます。その1として、 カスタムエレメント に注目して書いています。ほかの項目については、また別の機会に書きます。

また、今回のサンプルコードと、デモです。

WebComponentsとは

カスタムエレメント、ShadowDOM、HTMLTemplateといった技術的要素をまとめた言葉です。 カスタムエレメントとは、divinputのようなHTMLElementと同様にmy-web-componentsのように独自のコンポーネントを定義し、使うことができる仕組みです。 コンポーネントの利用者は、自分のアプリケーションに<my-web-components>という感じでコンポーネントを差し込むだけで、inputなどのエレメントと同じように機能させることができるようになります。

ShadowDOMとは、外部からアクセスできないようにしたDOMツリーを構築することができるようになる要素のことです。ShadowDOMをルートエレメントとして、ぶら下げたDOMツリーは外側のCSSに影響されず、querySelectorなどで列挙することが難しくなります。これにより、スタイルやスクリプト化をより自由に行えるようになります。

HTMLTemplateとは、template, slotという特殊なタグのことを指します。DOMの構造を再利用しやすくする機能です。

マイクロフロントエンドのやりかたの一つとして挙げられることが多いようです。

iframeとの違い

マイクロフロントエンドとして、iframeでアプリケーションを埋め込む場合がありますね(embedやobjectと呼ばれるさらに古いものもあるようですが、よく知りません)。たとえば、このはてなブログもポップアップするエディター画面はiframeで埋め込まれています。WebComponentsもiframeもアプリケーションに何かを埋め込むという点では同じことができます。

大きな違いは何かというと、動作するホストでしょう。

iframeはアプリケーションとは別のURLのhtmlを埋め込むことができ、iframe内では親のホストとは異なるホストで動作します。iframe用のアプリケーションを提供する側はCORSを気にする必要がありません。例えば、Google Mapをiframeで埋め込んでいますが、どこに埋め込まれているかを気にせずに利用することができています。

一方で、WebComponentsは親のホスト上で動作することになるため、コンポーネントを提供する側はCORSを考慮したWebAPIを用意する必要があります。

他にも、カスタムエレメントは他のHTMLElementと同様に周りのエレメントの幅や高さに影響されますが、iframeは幅や高さが影響されず、iframe内でスクロールします。これは利点でもありますが、スクロールさせたくない場合には欠点にもなるでしょう。

俺のカスタムエレメントを作ってみる

1. 時計

まずは、時計を作成しましょう。

これで最低限、カスタムエレメントをどのように定義し、描画するかを学びます。

class SimpleClock extends HTMLElement {
  constructor() {
    super();
    // 1. HTMLから分離されたノード(ShadowDOM)を作成する。
    //    これで、親のドキュメントのCSSなどに影響されない環境を作成できます。
    this.shadow = this.attachShadow({mode: 'open'}); 
    // 2. 今の時間を表示する h1 エレメントを ShadowDOM 下に作成する。
    const now = new Date();
    const clockElement = document.createElement("h1");
    clockElement.textContent = `${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}`;
    this.shadow.append(clockElement);
  }
}

(function main() {
  // 3. simple-clock という名前でSimpleClockクラスを登録する
  customElements.define("simple-clock", SimpleClock);
})();

このJSをhtmlから呼び出して、<simple-clock>というカスタムエレメントを配置すれば、ページをロードした時間が表示されるようになるでしょう。

<!-- simple-clock.jsという名前でアクセスできるものとする -->
<script src="simple-clock.js"></script>
<simple-clock></simple-clock>
※注意点
  • カスタムエレメントの名前はdivinputなどの通常の要素と区別するため、ハイフンで区切られた名前である必要があります。

2. チャート

続いて、グラフを表示しましょう。チャートは外からデータを受け取り、そのデータをグラフに描画します。チャートライブラリとしてC3.jsを利用します。

ここでは、外からデータを受け取る方法と、独自のスタイルシートを適用する方法を学びます。

import * as c3 from "c3";

class SimpleChart extends HTMLElement {
  constructor() {
    super();
    this.shadow = this.attachShadow({ mode: "open" });
    // 1. ShadowRootのinnerHTMLに link タグを入れることで、ShadowRoot内限定で外部のCSSをロードできます。
    //    style タグも利用できます。
    this.shadowRoot.innerHTML = `
      <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/c3/0.7.20/c3.css">
      <div></div>
    `;
    // 2. ここは重要ではありませんが、c3.jsはShadowRootに直接バインドできなかったので div エレメントを用意しています。
    this.div = this.shadowRoot.querySelector("div");
    // 3. attribute "columns" に文字列でデータが渡されることを想定しています。
    //    '[["data1", 3, 2, 1], ["data2", 1, 2, 3], ...]' のような c3.js の columns プロパティ用のデータです。
    const columns = JSON.parse(this.getAttribute("columns"));
    this.renderedChart = c3.generate({
      bindto: this.div,
      data: {
        columns,
      },
    });  
  }
}

これは以下のように利用できます。

<simple-chart columns='[["data1", 1, 2, 3], ["data2", 10, 9, 8]]'></simple-chart>
※注意点
  • 外からなんらかのデータを渡すには、jsxのpropsなどと同様にAttributesを利用します。なお、通常のHTMLタグと同様文字列のみが利用できます。AttributeでなくWebComponentsのエレメントのプロパティにデータをセットすることもあるかもしれませんが、あまり良い方法ではないでしょう。
  • CSSを読み込む方法はこちらを参考にするとよりよいでしょう。

3. Attributesが更新されたら再描画する

class SimpleFormatter extends HTMLElement {
  constructor() {
    super();
    this.shadow = this.attachShadow({ mode: "open" });
    // 1. 描画には render(再描画でも使う) を使うようにします
    this.render();
  }

  static get observedAttributes() {
    // 2. 変更を監視するAttributeの名前を配列で複数指定できます。
    //    ここに書かれたAttributeが変更された場合、attributeChangedCallbackが呼び出されます。
    return ["name"];
  }

  attributeChangedCallback() {
    // 3. Attributeが更新されたら、attributeChangedCallbackが呼ばれます。
    //    isConnectedがtrueであれば、すでに描画済みです。
    //    attributeChangedCallbackは初回の描画よりも先に呼ばれることがあり、
    //    その際にrenderするのを避けるためにチェックしています。
    if (this.isConnected) {
      this.render();
    }
  }

  render() {
    const name = this.dataset.name;
    this.shadow.textContent = name;
  }
} 

Reactなどからこのように呼び出すことができます。

const App = () => {
  const name = ...
  return <simple-name name={name} />;
}

まとめ

WebComponentsの基本的な作り方を書きました。

  • ShadowRootを使えばスタイルが外部に影響されないものを作ることができる(外からスタイルを適用する方法もある)
    • 今回はShadowRootしか使っていませんが、代わりに普通のdivエレメントなどを使うことも可能です。
    • また、ShadowRootはWebComponents専用の機能ではなく、普通に利用できます。
  • カスタムエレメントのタグ名には - ハイフンが必須
  • CSSは link タグや style で適用できる

サンプルコードの話

  • components/clock: 時計のカスタムエレメントです。
    • Reactで書かれていて、250ms間隔で時間を更新するようになっています。
  • components/chart: チャートのカスタムエレメントです。
    • 素のjsとc3jsで書かれています。
    • チャートの上にinput要素を表にして並べています。ここに数値を入れたり、+で要素を増やしたりすると、チャートが更新されるようになっています。attributeChangedCallbackで更新されます。
    • c3.js用のCSSをCDNから取得しています。adoptedStyleSheetsの機能を使いたかったのですが、なぜか利用できませんでした。まだ対応してないのでしょうか。
  • app: 上記2つのカスタムエレメントを利用しているアプリケーションです。
    • Reactで書かれています。

ペアプログラミングではいつコードレビューするのか?

こんにちは、SaaS Productチームの比嘉です。

私たちSaaS Product チームは常日頃からペアプログラミングを行っています。

チームペアプロの細かい流れは過去に鈴木さんが紹介しています。

tech.uzabase.com

そんな中、あるときエンジニアの友人から質問されました。

「ペアプログラミングではいつコードレビューするの?」

話を聞いてみると、その会社ではペアプロを導入し始めたのですが、従来から行なっているコードレビューと役割が重複していることに気づいたそうです。

そこで今回はペアプログラミングとコードレビュー*1について書いてみようと思います。

離れてペアプロする猫と熊の画像です。コロナ時代はリモートペアプロ中です。
時世を鑑みてリモートペアプロ中

なぜコードレビューを行うのか?

なぜコードレビューを行うのでしょうか?

Wikipediaによると、コードレビューには次の効果が期待できるとされています。

  1. レビューで発見された同様・類似バグについてレビュー参加者内で共通認識を図ることができる。
  2. バグの隠蔽を減少させることが期待できる。
  3. レビューを行うことへの意識により、人に見せるコードを書くようになるため可読性が向上する。
  4. コーディング規約等に対する各自の認識のずれを修正することができる。

またきのこ14では次のように述べられています。

コードレビューの目的は、ただコードの誤りを修正するだけではありません。重要なのは、チーム全員に同じ知識を共有させること、またコーディングにおいて全員が守るべきガイドラインを確立することです。

つまり、コードレビューにはコードに問題について指摘したりチーム内で知識を共有したりする目的があるということです。

ペアプロはいつコードレビューしている?

ペアプロでは常にコードレビューを行っています。ペアプロはドライバー(キーボードを使う人)とナビゲーター(キーボードを使わない人)をお互いに交換して作業を行います。このナビゲーターがコードレビューをしているのです!

ナビゲーターがいると、コードを書いているそばから問題点をつかんで対処することができます。また、ドライバーは書いているコードをナビゲーターに見せることになるため、可読性を考慮してコードを書くことになります。

チームでペアプロを行えば、定期的にペアを交換することになります。ペアを交換することで同じコードを複数人が触ることになります。複数人で触ることできのこ14「チーム全員に同じ知識を共有させること」が可能になります。

ペアプロでコードレビューできてる?

ちょっと待って!ペアプロで行われているコードレビューは信頼しても良いのでしょうか? チームメンバーを集めてじっくりコードレビューした方がいいじゃないでしょうか?

調べてみると、ペアプロをしつつもコードレビューをする現場*2もあるようです。コードレビューの時間を作ることには、ペアを交代した時に抜け落ちた知識(やるはずだったリファクタリングや仕様の抜け漏れ)を集めたり、できるだけコードに関わっていない開発者のレビューを取り入れる意図があるようです。

一緒にコードを書くことによってレビューの視点に偏りを持ってしまうことは確かにありそうです。また、知識の断片化についてはどう対処していけばいいのでしょうか?

SaaS Product チームの場合

冒頭の繰り返しですが、Productチームではチームでペアプログラミングを採用しています。目的はシンプルに生産性をあげるためです。また、特別に時間をとってコードレビューはしていません。すべてはペアプロの中で行われています。コードレビューをしない代わりに仕組みや文化が補っています。

その1つがテスト駆動開発です。私たちはドキュメントを作成していません(一部、手順書などはあります)。仕様はE2Eテストに書いています。ペアが交代したときに今のコードに何が足りないのか、これから何をすべきかはテストが通っているかどうかで判断することができます。

また「人に聞く」ことを推奨しています。仕様の抜け漏れの確認や、コードを書く上で決断に悩んでいる場合はより詳しい他の人に聞いています。たとえば、テストが仕様を満たせているのかをテストエンジニアに相談したり、クラスの責務分けをベテランエンジニアに相談したりしています。Productチームでは頻繁にチームメンバーが入れ替わります。もしかしたら他のチームに情報を持っている人がいるかもしれません。その時は個々のチームを越えて話にいくことも日常茶飯事です*3

まとめ

今回は「ペアプログラミングではいつコードレビューするの?」について答えてみました。 ペアプログラミングでは常にコードレビューをしています。そしてそれにはメリット・デメリットがありました。

SaaS Productチームでは生産性をあげるためにペアプロをしています。メリットを享受しつつも、デメリットを補う文化があることを紹介しました。

この記事がペアプロで悩んでいる方の一助になれれば幸いです。

*1:この記事にあるコードレビューは、プルリクエストやマージリクエストなどであげられたコードをチェックして指摘したり承認したりする行為を指します。

*2:http://appresso.hatenablog.com/entry/2017/03/03/114855

*3:https://tech.uzabase.com/entry/2019/08/19/154623

EnvoyをFront Proxyとして利用する

こんにちは、ユーザベースのProductチームでSREをやっています阿南です。弊社ではKubebrnetes + Istioを利用してサービスメッシュの構築、マイクロサービスの運用を行っています。Istioでは sidecar proxyとしてEnvoyが利用されていますが、このEnvoyをFront Proxyとしても利用できないかと思い、よく使われる設定について調べてみました。下記目次です。

少し長いので、気になる部分だけでも読んで頂ければ幸いです。 今回作成した全体の設定はこちらにありますので、適宜参照してください。

envoy proxy for frontend · GitHub

また、実際にEnvoyを起動して確認してみたい方は、下記に環境構築について記載しています。

GitHub - tanan/migrate-from-apache-to-envoy

EnvoyをFront Proxyとして利用するメリット

(Front Proxyに限らずですが...) Envoyを利用することで下記のようなメリットがあります。

  • 柔軟なLoadBalancing機能
    • Blue/GreenやWeightを利用して簡単にBalancingの設定を変更することができます。また、rate limitやcircuit breakerの機能も有しているため、適切に設定を行うことでエラーが全体に波及するのを防ぐことができます。
  • control-planeやファイルベースのdynamic configurationが利用できる
    • Apache等では明示的に各プロセスに対してreload/restart等の動作を実行する必要がありますが、Envoyではcontrol-planeを利用して設定をDynamicに反映することができます。Apacheでもupstream先を有効/無効にする等は動的に設定できますが、プロセスがrestartすると状態が消えてしまいます。
  • Observabilityを高めるための他ツールとの連携のしやすさ
    • prometheus / grafana / jaegerとの連携が簡単に実現できるエコシステムが整っています。

Envoyのバージョン

envoyバージョンはv1.16.0-devで、v3 APIを利用しました。

Front ProxyのためのEnvoy Configuration

現在の弊社環境はざっくり下記のような構成になっています。WebサーバとしてApacheを利用し、元々開発されてきたtomcatのモノリスアプリケーションと新しく開発しているマイクロサービス群にPathベースでルーティングをしています。(Apacheとtomcatはon-premiseに構築されています。)

f:id:tanan55:20200927132457p:plain
architecture

今回は、このApacheをEnvoy Proxyに代替するために、基本的な設定を行いましたので、以降でそれぞれの設定を見ていきたいと思います。

80(HTTP),443(HTTPS)でアクセスを受け付ける

listenerを定義することで特定のポートでアクセスを受け付ける事ができます。

static_resources:
  listeners:
  - name: listener_0
    address:
      socket_address: { address: 0.0.0.0, port_value: 80 }

TLSの設定をする場合は、filter_chainsにtls_contextを設定します。

tls_context:
  common_tls_context:
    tls_certificates:
      - certificate_chain:
          filename: "/etc/envoy/certs/example-com.crt"
        private_key:
          filename: "/etc/envoy/certs/example-com.key"

ここではfilenameを指定していますが、証明書の内容を直接記載することも可能です。

Pathベースのルーティングを設定する

Routingの設定はlistenerのfilter部分に記載します。

route_config:
  name: backend_route
  virtual_hosts:
  - name: backend
    domains:
    - "www.example.com"
    - "example.com"
    routes:
    - match:
        prefix: "/service/2"
      route:
        prefix_rewrite: "/"
        cluster: service2
        hash_policy:
          - cookie:
              name: balanceid
              ttl: 0s
    - match:
        prefix: "/"
      route:
        cluster: service1

上記の設定は、

  • ドメインはwww.example.com,example.comのみ許可
  • pathが/service/2にマッチした場合、service2のclusterを参照する
  • pathが/にマッチした場合、service1のclusterを参照する

という設定になります。ちなみにpathのルールは上から順に判定されるようで、matchの順番を逆にするとすべてservice1にリクエストが送られるので注意が必要です。

clusterではupstreamのendpointやbalancing ruleを定義します。 lb_policyには、ROUND_ROBIN の他に、 LEAST_REQUEST や後述の RING_HASH などが指定できます。

clusters:
  - name: service1
    connect_timeout: 0.25s
    type: STRICT_DNS
    lb_policy: ROUND_ROBIN
    load_assignment:
      cluster_name: service1
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: service1-1
                port_value: 8080
        - endpoint:
            address:
              socket_address:
                address: service1-2
                port_value: 8080

また、 https_redirect をtrueにすることで、HTTPSへのリダイレクトが可能です

routes:
- match:
    prefix: "/"
  redirect:
    https_redirect: true

レスポンスに特定のヘッダーを付与する

response_headers_to_add に追加したいヘッダーを記載することができます。 注意点としては、v1.8でRouteActionレベルでは本パラメータがdeprecatedになりましたので、Routeレベルで設定する必要があります。 その他、リクエストヘッダーに特定の値を付与できる request_headers_to_addrequest_headers_to_remove もここで指定できます。

response_headers_to_add:
  - header:
      key: "x-frame-options"
      value: "sameorigin"
  - header:
      key: "x-xss-protection"
      value: "1; mode=block"

Healthcheckに失敗した場合、upstreamの対象から除外する

普段の運用ではupstreamに複数のエンドポイントを設定して、正しく応答が返せない場合はリクエストが割り振られないようにしたいケースがよくあると思います。Envoyではhealth_checks をclusterの設定に追加することでヘルスチェックを実施することができます。

health_checks:
  - timeout: 1s
    interval: 10s
    interval_jitter: 1s
    unhealthy_threshold: 6
    healthy_threshold: 2
    http_health_check:
      path: "/healthz"

jitterはinitial_jitterinterval_jitterが指定でき、ヘルスチェックがすべて同時にリクエストされないように、ばらつきを与えたい場合に設定します。 ヘルスチェックに失敗したupstreamはendpointのリストから除外され、healthy_thresholdの数だけ成功するとhealthyとみなされ再度リストに追加されます。 ちなみに初回起動時はhealthy_thresholdに関係なく1回の成功でhealthyとみなされます。

Cookieをもとに、upstreamを固定(sticky session)する

アプリケーション内にセッション情報を持っているようなシステムでは、純粋にROUND ROBINでリクエストが振られるとアプリが正常に動作しなくなってしまいます。そのため、Session Cookie(sessionが続く間だけ有効なcookie)を使って、ユーザーのリクエストを同じupstreamにルーティングするようにするsticky sessionが利用されます。 Envoyではclusterでlb_policy: RING_HASHを指定した上で、下記を設定することにより、sticky sessionが実現できます。

hash_policy:
  - cookie:
      name: balanceid   ## nameは何でもよい
      ttl: 0s

この設定でのポイントはttlです。ttlの有無により下記のように動作が異なります。

  • ttlが指定されない場合、cookieはつくられません。既にあればその値が利用されます。
  • ttlが指定されている場合、かつ、cookieがない場合はcookieを生成します。また、ttlが0sのときはsession cookieになります。

詳細はhashpolicy-cookieのDocumentを参照して下さい。

まとめ

EnvoyをFront Proxyとして利用するための設定を見ていきました。EnvoyのDocumentの読み方を覚えるまでに少し時間がかかりましたが、無事意図通り動作するよう設定できました。 実際に利用する場合は、control-planeを導入してdynamic configurationで設定反映をすることが多いかと思いますが、staticな設定を一度書いてみると理解が早まると思いますので、興味のある方はぜひやってみて下さい。

仲間募集!!

ユーザベースでは、「経済情報で、世界をかえる」 というミッションの実現に向けて、日々邁進しており、SREのメンバーを募集しています。 技術的にも様々なことにチャレンジしてますので少しでも興味を持ってくださった方はこちらまで!

Kubernetes で運用する JVM アプリケーションの OutOfMemoryError に備える

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

JVM アプリケーションの運用について回るのが、OutOfMemoryError (以下 OOM) への対処です。
しかし実際に発生した際に、適切なオペレーションを行うのは意外と難しいのではないでしょうか。

特に本番環境では、まず再起動して復旧を急ぐことも多いかと思います。しかし、ただそれを繰り返すばかりでは原因がいつまでも特定できません。

今回は Kubernetes で運用する JVM アプリケーションに対して、ダウンタイムを抑えつつ調査に役立つ情報を自動的に収集する仕組みを構築してみたいと思います。

環境構築

実際に構築してみたサンプルを、こちらのリポジトリに用意しました。

https://github.com/old-horizon/k8s-oom-sample

動作確認は以下の環境で行っています。

  • Ubuntu 18.04
  • MicroK8s 1.18.6

clone して kubectl apply -f kubernetes/ を実行すると oom-sample ネームスペースに各種リソースが作られます。
(おそらく初回は途中で失敗するため、二度実行してみてください)

次章より、このサンプルを使って解説を進めていきます。

OOM 発生時に、自動的にヒープダンプを取得しコンテナを再起動する

ヒープダンプがあれば、Eclipse Memory Analyzer 等で解析してリークしていたオブジェクトを特定できます。

しかし OOM で処理が長時間滞留した場面では、さらに復旧を遅らせてまでダンプを取得するのに抵抗を感じ、再起動を優先させることもあるかと思います。

とはいえ通知を受けて手動で対応している限り、多少なりとも遅延は発生しているわけです。この際すべて自動化して、焦らなくてもユーザーへの影響が最小限に収まるようにしましょう。

java コマンドのオプション指定

Dockerfile (ktor-oom/Dockerfile) で、java コマンドに次のオプションを指定しました。

  • -XX:+ExitOnOutOfMemoryError
    OOM 発生時に JVM が終了する

  • -XX:+HeapDumpOnOutOfMemoryError
    OOM 発生時にヒープダンプを出力する

  • -XX:HeapDumpPath
    -XX:+HeapDumpOnOutOfMemoryError によるヒープダンプの出力先を指定する

JVM が終了すると、Kubernetes の自己修復機能によりコンテナが再起動されます。

補足

  • ヒープダンプは java_pid(プロセス ID).hprof のファイル名で出力されます。
    なお同名のファイルがすでに存在する場合は上書きされません。
    コンテナ環境の場合、プロセス ID が常に一定ゆえファイル重複が起きやすいためご注意ください。

  • -XX:OnOutOfMemoryError で OOM 発生時に任意のコマンドを実行できます。
    上記のオプションと併用可能です。

ヒープダンプ出力先のボリュームをマウント

ヒープダンプは Pod が存在する間のみ存続する emptyDir ボリュームに出力します。

Dockerfile (ktor-oom/Dockerfile) で、ヒープダンプ出力先に /dump を指定します。

-XX:HeapDumpPath=/dump

Deployment (kubernetes/ktor-oom.yml) で /dump にボリュームをマウントします。

        volumeMounts:
        - mountPath: /dump
          name: dump-volume
      volumes:
      - name: dump-volume
        emptyDir: {}

これにより、コンテナ再起動前に出力されたダンプが取得できるようになります。

readinessProbe によるヘルスチェック

コンテナの再起動直後は、リクエストを処理できない場合があります。
readinessProbe で正しく処理できる状態になったことを確認してから、Service のルーティング対象に復帰するようにします。

Deployment (kubernetes/ktor-oom.yml) より抜粋

        readinessProbe:
          httpGet:
            port: 8080
            path: /v1/ping

レプリカ数の確認

基本的なことですが、常に使用可能な Pod が複数存在するようにしましょう。
今回はサンプルなのでレプリカ数は 2 にしています。

Deployment (kubernetes/ktor-oom.yml) より抜粋

spec:
  replicas: 2

Prometheus + Grafana で JVM のメトリクスを可視化する

ライブラリ・フレームワークの設定ミスによるリークは、ネット上にも情報が多いためヒープダンプの解析結果から比較的特定しやすい印象があります。

一方で社内で実装したプロダクションコードでリークが起きている場合、先人の知恵に頼ることはできません。
そのため地道にコードを読む必要はありますが、ヒープ使用量の推移からリークが疑われる事象が発生するタイミングを特定できれば、調査範囲を絞ることができます。

そこで Prometheus + Grafana により JVM のメトリクスを可視化して、その記録をもとに疑うべきポイントに目星がつけられる状況を目指します。

JVM アプリケーションの Java agent に JMX Exporter を指定する

JMX とは

JVM アプリケーションでは、JMX (Java Management Extensions) という監視・管理フレームワークが利用できます。
以下に例を挙げますが、標準でかなり多くの指標を取得することができます。

  • 領域ごとのメモリー使用量
  • ロード済みクラス数
  • スレッド数

JMX から取得できた情報を Prometheus で収集するには、所定のテキストフォーマットに変換して HTTP で配信する必要があります。
今回アプリケーション本体には手を加えず、実行時にこの機能を追加するために Java agent を使います。

Java agent とは

Java agent は J2SE 5.0 で java.lang.instrument パッケージと同時に追加された仕組みです。
Javadoc によると、正式名称は「Javaプログラミング言語エージェント」といったところでしょうか。

-javaagent:(エージェント JAR ファイルへのパス) を指定して java コマンドを実行すると、main に先んじてエージェント JAR ファイル内の premain が呼び出されます。
このタイミングでバイトコード操作が可能になるため、AspectJ の動的ウィービングなどで利用されています。

いわゆる黒魔術を実現するための仕組みですが、応用することでアプリケーションの起動前に任意の処理を差し込むことができます。

Dockerfile の編集

Prometheus が提供する JMX Exporter を Java agent に指定してアプリケーションを起動します。
するとアプリケーションの main 実行前に、指定されたポートで JMX メトリクスを公開する HTTP サーバーが立ち上がります。

まずは JAR ファイルをダウンロードし、設定ファイルである config.yaml を作成します。
今回の用途ではデフォルト設定で問題ないため、中身は空のままです。

ADD https://repo1.maven.org/maven2/io/prometheus/jmx/jmx_prometheus_javaagent/0.13.0/jmx_prometheus_javaagent-0.13.0.jar .
RUN touch config.yaml

最後に java コマンドで Java agent を指定してアプリケーションを起動します。

-javaagent:./jmx_prometheus_javaagent-0.13.0.jar=9100:config.yaml

これで 9100 ポートで JMX メトリクスが公開されました。

詳細は Dockerfile (ktor-oom/Dockerfile) をご覧ください。

Prometheus の Service Discovery で動的に監視対象を指定する

Service Discovery について

Prometheus は、自身が監視対象からデータを収集する Pull 型アーキテクチャを採用しています。
したがって、基本的には監視対象への接続情報をあらかじめ知っておく必要があります。
しかし Kubernetes で運用するアプリケーションの場合、自動的に Pod のスケールや再配置が行われるため接続先を静的に定義しておくことはできません。

このように接続先が動的に変わる場合には、Service Discovery が便利です。
その名の通り、Prometheus 自身が監視対象を検出する機能です。Kubernetes の他にも Azure VM, AWS EC2, GCP GCE といった主要なクラウド仮想マシン等に標準で対応しています。

Kubernetes の設定

まずは、アプリケーションの Service (kubernetes/ktor-oom-svc.yml) に任意のアノテーションを付与します。
サンプルで使用したアノテーションの用途は次のとおりです。

  • prometheus.io/java_scrape
    メトリクスを取得する対象の場合は true
  • prometheus.io/java_port
    メトリクスを公開しているポート番号を指定
  annotations:
    prometheus.io/java_scrape: 'true'
    prometheus.io/java_port: '9100'

アプリケーション本体 の 8080 に加え、メトリクスの 9100 ポートも忘れずに公開します。

  ports:
  - name: app
    protocol: TCP
    port: 8080
  - name: metrics
    protocol: TCP
    port: 9100

同様に、Deployment (kubernetes/ktor-oom.yml) でも両方のポートを公開します。

        ports:
        - containerPort: 8080
        - containerPort: 9100

Prometheus の設定

先ほど付与したアノテーションを持つ Service 配下の Pod を監視対象として設定します。
今回は Kubernetes クラスタ内に Prometheus を立てましたが、認証情報を設定すればクラスタ外からも監視できます。

デフォルト設定の場合、Prometheus は /etc/prometheus/prometheus.yml を設定ファイルとして参照します。
サンプルでは ConfigMap の形式で Pod にマウントしているため、内容は (kubernetes/prometheus-config.yml) に記載されています。
以下、その一部を抜粋しながら解説していきます。

role が監視対象を検出する際のルールです。
endpoints を設定したことで Service のルーティング先であるバックエンドが対象になりました。

    kubernetes_sd_configs:
    - role: endpoints

続いて relabel_configs のブロックに入ります。

role: endpoints により、Prometheus は Kubernetes クラスタ上のすべての Service に紐づくバックエンドを検出します
しかし実際に監視したい対象は一部のため、条件を指定して絞り込んでいく必要があります。

Prometheus は検出した候補から取得できるメタデータを、ラベルとして扱うことができます。
このラベルを元にしてフィルタしたり、値を設定したりすることで必要なものだけが抽出されるようにします。

    - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_java_scrape]
      action: keep
      regex: true

アノテーション prometheus.io/java_scrape の値が、正規表現 true に一致した場合は候補に残ります。
一致しなければ、この段階で除外されます。

    - source_labels: [__address__, __meta_kubernetes_service_annotation_prometheus_io_java_port]
      action: replace
      target_label: __address__
      regex: ([^:]+)(?::\d+)?;(\d+)
      replacement: $1:$2

まずメトリクス取得先ホストが入った __address__ と、アノテーション prometheus.io/java_port の値を区切り文字 ; で連結します。
対象の Service は 2 つのポートを公開しているため、監視対象は Pod ごとに 8080 と 9100 の 2 つがヒットします。
したがって、ここまでの出力は (Pod の IP):(8080 または 9100);9100 になります。

次に正規表現 ([^:]+)(?::\d+)?;(\d+) でマッチさせ、$1$2 で置換した値を __address__ に設定します。
よって __address__(Pod の IP):9100 に上書きされ、正しく監視先 URL が組み立てられるようになりました。
このルールは __address__ に含まれるポートが 8080 でも 9100 でも同じ値を返すため、その結果 9100 だけが監視対象に残ります。

Grafana にダッシュボードを作成する

JMX Exporter の出力に対応する JVM dashboard を使用しました。

サンプルでは grafana-init ジョブにより、自動的にダッシュボードが追加されるようにしています。
簡単なシェルスクリプトで実現していますので、もし興味があれば kubernetes/grafana-init-config.yml をご覧ください。

動作確認

それでは、サンプルの動作確認を進めていきましょう。
なお MicroK8s 前提での手順になりますので、異なる環境をお使いの方は適宜読み替えてください。

まずは Kubernetes クラスタ内のアプリケーションにアクセスするための InternalIP を取得します。

$ kubectl get nodes -o jsonpath='{.items[*].status.addresses[?(@.type=="InternalIP")].address}'
192.168.0.11

各アプリケーションの Service は NodePort であり、ランダムに割り当てられたポートでクラスタ外に公開しています。
以下は prometheus-svc のポートを取得した例です。

$ kubectl -n oom-sample get svc prometheus-svc -o jsonpath='{.spec.ports}'
[map[nodePort:31122 port:9090 protocol:TCP targetPort:9090]]

上記より、Prometheus にアクセスするための URL は http://192.168.0.11:31122 であることが確認できました。
他のアプリケーションに接続する場合も、この要領で URL を取得してください。

JMX メトリクスの取得

Prometheus にアクセスして、画面上部メニューの Status > Targets を選択します。
以下のように、2 台の Pod それぞれが監視対象として認識されています。

f:id:old_horizon:20200816170455p:plain

次に Grafana を開きます。ユーザー名およびパスワードは admin です。
画面左メニューから Dashboards > Manage を選択すると、表示された一覧に JVM dashboard が存在しています。

f:id:old_horizon:20200816170522p:plain

実際に表示してみると、グラフ形式で値の推移が確認できることがわかります。

f:id:old_horizon:20200816170534p:plain

OOM を発生させてヒープダンプを取得してみる

サンプルアプリケーション (ktor-oom) は、/v1/oom にアクセスすると OOM が発生する実装になっています。
ここにリクエストして、構築した仕組みが動作することを確かめます。

最初に現在の Pod の状態を見てみます。
まだコンテナは一度も再起動していないため RESTARTS は 0 のはずです。

$ kubectl -n oom-sample get po -l app=ktor-oom -o wide
NAME                        READY   STATUS    RESTARTS   AGE   IP            NODE    NOMINATED NODE   READINESS GATES
ktor-oom-646544cc67-4x9bd   1/1     Running   0          42m   10.1.52.222   t470s   <none>           <none>
ktor-oom-646544cc67-ljgpt   1/1     Running   0          42m   10.1.52.223   t470s   <none>           <none>

どちらの Pod も /dump にファイルは存在しません。

$ kubectl -n oom-sample exec ktor-oom-646544cc67-4x9bd -- ls /dump
$ kubectl -n oom-sample exec ktor-oom-646544cc67-ljgpt -- ls /dump

それでは OOM を発生させてみましょう。

以下のコマンドで /v1/oom/v1/ping に連続してアクセスします。
/v1/ping のリクエストは正しく処理されたため、OOM が発生した Pod はルーティング対象から外されたことがわかります。

$ curl -v http://192.168.0.11:31072/v1/{oom,ping}
*   Trying 192.168.0.11...
* TCP_NODELAY set
* Connected to 192.168.0.11 (192.168.0.11) port 31072 (#0)
> GET /v1/oom HTTP/1.1
> Host: 192.168.0.11:31072
> User-Agent: curl/7.58.0
> Accept: */*
> 
* Empty reply from server
* Connection #0 to host 192.168.0.11 left intact
curl: (52) Empty reply from server
* Connection 0 seems to be dead!
* Closing connection 0
* Hostname 192.168.0.11 was found in DNS cache
*   Trying 192.168.0.11...
* TCP_NODELAY set
* Connected to 192.168.0.11 (192.168.0.11) port 31072 (#1)
> GET /v1/ping HTTP/1.1
> Host: 192.168.0.11:31072
> User-Agent: curl/7.58.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Content-Length: 4
< Content-Type: text/plain; charset=UTF-8
< 
* Connection #1 to host 192.168.0.11 left intact
pong

再度 Pod の状態を確認すると、片方の Pod の RESTARTS が 1 になっていました。
したがって、この Pod で OOM が発生したものと考えられます。

$ kubectl -n oom-sample get po -l app=ktor-oom -o wide
NAME                        READY   STATUS    RESTARTS   AGE   IP            NODE    NOMINATED NODE   READINESS GATES
ktor-oom-646544cc67-4x9bd   1/1     Running   0          73m   10.1.52.222   t470s   <none>           <none>
ktor-oom-646544cc67-ljgpt   1/1     Running   1          73m   10.1.52.223   t470s   <none>           <none>

実際にヒープダンプが出力されていることを確認できました。

$ kubectl -n oom-sample exec ktor-oom-646544cc67-ljgpt -- ls /dump
java_pid1.hprof

出力されたヒープダンプの回収には kubectl cp が利用できます。

$ kubectl -n oom-sample cp ktor-oom-646544cc67-ljgpt:/dump/java_pid1.hprof ./java_pid1.hprof

おわりに

OOM 発生時に十分な情報が取得できず、再現待ちとなってしまうケースを過去に見てきました。
しかし各種ダンプ・メトリクスがあるだけで調査は大いに捗るため、その取得の自動化は費用対効果が高いと思います。
まさに備えあれば憂いなしです。機会があればぜひお試しください。

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(一意性を保つ) というようにすべきかと思います。