2020 年の振り返り 〜 2021 年の抱負

明けましておめでとうございます。

久しぶりにブログ開いたら、昨年は 1 記事しか書かなかったようで、アウトプット不足に驚愕しています。

2020 年の振り返り

まずは、なんといっても新型コロナウイルスの影響が大きかったと思います。

仕事的には大きなリプレース案件があったので結構忙しかったので、息抜きに勉強会に参加したいところだったのですが、軒並みオンライン開催になったので、2020 年は一度も東京に行きませんでした (たぶん)。 出張も減ったので、ほぼ家と会社を往復する 1 年でした。

マネージャーのような立ち振る舞いが多くなり、大変だった年でもありました。マネジメントに苦手意識を持っていたので、その辺りの本を多く読みました。

  • SCRUM BOOT CAMP THE BOOK     この本は、改訂版が出版されたので買い直しました。ちなみに、改訂前の本はどこかの誰かに貸してるはず。(見当たらない)
  • SCRUMMASTER THE BOOK     Scrum をもっと学びたいと思って読みました。チームをどう成長させていくか、チームと Scrum Master のレベルなど、ためになる話が多かったです。
  • エンジニアリング組織論への招待     こちらはまだ読んでいる途中ですが、Chapter 1 だけ読んでも学びがあったので、じっくり読みたいと思っています。

現在のチームが、2 年目、1 年目のメンバーでまだまだ若いので、自己組織化したチームを目指して、一緒に成長していきたいと思います。

技術的には、新たに学ぶというよりは、学んできたことの答え合わせをしてきた 1 年だった気がします。

ここ数年はドメイン駆動設計を学んできているため、できれば仕事に導入したかったのですが、既に開発ツールを導入していたプロジェクトだったため、ドメインの知識をコードに落とすということができませんでした。

そのお陰で、不具合や仕様変更のたびに設計書をひっくり返して読むプロジェクトができあがっているので、全て開発ツールではなく、業務的な要件が多いところからコードを書くように、プロジェクトを変えていきたいと思っています。

開発ツール頼りな面もあり、開発メンバーが入社からほぼコーディング経験がないというちょっとアレな状態はどうにかしなければいけないと考えています。

2021 年の抱負

あまり良くはないかもしれませんが、アプリを作る際の技術スタックを固めたいなと考えています。

  • フロントエンド: Next.js (勉強中)、GraphQL (勉強中)
  • バックエンド: Spring Framework、Node.js
  • インフラ: AWS

といった感じで、フロントエンドがほぼ仕事で使ったことがなく弱いため、フロントエンド周辺を強化していきたいと思っています。

あとは、ほぼ仮想サーバーとして使っている AWS をもう少し活用できるようにしていきたいと思っています。 特に AWS の認定資格も目指してなかったのですが、ちょっと勉強してみたいなと思っています。 それと、仕事でも BI や AI など、データを活用していく機会が多くなりそうなので、勉強中の E 資格や、統計も身につけたいと思います。

その他として、コロナの影響で在宅勤務やテレワーク可能な会社も増えてきているので、色々な環境を見てみたいなーとは思っています。 あとは、増え続ける体重をセーブしなければ・・。

・・・ということで、2021 年はより技術的なところを伸ばしていきたいと思っています。本年もどうぞよろしくお願いいたします。

読書メモ「ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本」

ドメイン駆動設計入門」を読み終えたので、感想を疑問に思ったことを残しておく。

www.amazon.co.jp

本書のコードは C# で書いてあるが、あえて Java に変更して記載する。

長文注意。

全体を通して

最終的に分かったのは、これまでやっていた DDD のほとんどが「軽量 DDD」であった、ということ。 加えて「画面に必要だから・・・」とか「検索条件的に・・・」という理由で SQL に条件やら集計処理をガンガン書いていくスタイル。 これをリポジトリに実装していたもんだから、「DDD のメリットとは?」という状態だった。

パターンを使えばいいってものじゃないが、リポジトリは集約の永続化と再構築をするもので、 細かい検索は本書にも書いてある「リードモデル」を使って実装するなど、実装の場所を決めてあげることが重要だと思った。

残念ながら今のプロジェクトは開発ツールを使っているため、ドメイン駆動設計を持ち込むのが難しい状況なのだが、 今後のプロジェクトで生かしていける知識が得られたと感じている。

おかげで半分飛ばしなが読んだエヴァンス本、IDDD 本を読もうという気になったw

著者の @nrslib さん、関係者の方々、いい本をありがとうございます!

以下、疑問に思ったことなどをまとめてみた。

P39 2.5.2 「不正な値を存在させない」

値オブジェクトを作成する動機として挙げられているもののひとつで、プログラム内に不正な値が存在しないようにするため、というもの。

本書では、例として「ユーザー名は 3 文字以上」というルールを上げている。

// NG:存在してはいけない値
String userName = "me";

この場合、値オブジェクト UserName を作成し、コンストラクターにて値をチェック (本書では「ガード節」と記載されていた。) し、不正なインスタンスが生成されないようにしていた。

public class UserName {

    private final String value;

    public UserName(String value) {
        // null はエラー
        if (value == null) throw new NullPointerException("value");

        // ユーザー名が 3 文字未満はエラー
        if (value.length < 3) throw new IllegalArgumentException("ユーザー名は 3 文字以上です。(value)");

        this.value = value;
    }

}

疑問に思ったのは、UI 側でも同じエラーチェックをしたい場合、どのようにルールを 1 箇所で管理するかどうか。

ドメイン駆動設計に関して似たようなパターンとしては「仕様」があるが、それを使って UI とドメインのエラーチェックを共通化できるのではないかと考えた、ただし、それはあくまで UI とドメインが同じプログラミング言語を使っているとき限るのかなと思った。

いまのプロジェクトの場合は、最終的にはデータベースに永続化するため、テーブル定義書に桁数やコード体系などをまとめているため、そちらを参照して別々に実装してしまうケースが多い。

また、主題とは関係ないが、C#ArgumentException は、どの引数への例外かを表すコンストラクターの引数が定義されているため、メッセージの構築が楽だなと思った。Java の例外クラスは使いにくい気がする。

P78 コラム「ドメインサービスの基準」

その処理がドメインサービスかどうかを見極める際に筆者が重要視していることは、ドメインに基づくものかそうでないかという点です。 「ユーザーの重複」という考えがドメインに基づくものであれば、それを実現するサービスはドメインサービスです。 (中略) もちろん、可能な限り入出力はドメインサービスで取り扱わないようにするという方針には賛成です。

この方針は私も賛成。もしドメインサービスにてリポジトリの参照が必要な場合、リポジトリインターフェースのみをドメイン層に定義するようにしている。

・・・と書きつつ、リポジトリインターフェースが散在するのを避けるため、ドメイン層にすべて配置することが多い。

P87 コラム「リポジトリドメインオブジェクトを際立たせる」

前項とも被る話題ではあるが、リポジトリインターフェースをドメイン層に配置することが多い。本書ではリポジトリドメインの概念ではないとあり、私の考えと異なるが、これは私の中でドメインとアプリケーションの境界が曖昧であるため、と考えている。

顧客とドメインの話しをする際、次のような会話がある。

  • 受注伝票をもとに在庫を確認し、在庫があれば出荷の手続きに入る。
  • 受注伝票は会計のためファイルに閉じておく。

さて、受注伝票をファイルに閉じておくのはドメインの知識かどうか、というところが曖昧だった。この本を読んでからは、ドメインの知識を「集約」として洗い出したあと、それを保管しておくという業務手順については「リポジトリ」として、ドメイン層ではない場所に配置したほうがいいのかと思うようになった。

P127 コラム「煩わしさを減らすため」

単純に記述数が増えることを嫌う開発者は一定数います。 そうしたとき取れる手段は、彼らに降りかかる煩わしさの肩代わりをするものを用意することです。 具体的にはドメインオブジェクトを指定すると、その DTO となるクラスコードを生成するツールを作るとよいでしょう。

ドメイン駆動設計や、レイヤードアーキテクチャをやると、レイヤー間のデータ転送に DTO を用意することが多い。 上手く行っているプロジェクトは、Excel ファイルなどからソースコードを生成するツールを用意していることが多いので、確かにそのとおりだと思った。

P157 6.6.1.「サービスは状態をもたない」

Register メソッドは sendMail の値によって処理が分岐します。 (中略) 状態がもたらす複雑さは多くの開発者を混乱させるものです。 状態をもたせる以外の方法を考えてください。

ユーザー登録処理の中で登録時にメール送信をするという処理を例に、メールを送信する / しないをサービスクラスのインスタンス変数 sendMail で切り替えるという悪い例を挙げている。

メール送信も、業務によっていくつかのパターンがあると思う。

  1. テスト環境ではメールを送信しないが、本番環境ではメールを送信したい。
  2. ユーザーの設定によってメールを送信する / しないを切り替えたい。

今回はユーザー登録であるため、上記 1 が該当すると想定した場合の実装を考えてみる。(登録前にユーザーごとのメール送信する / しないの設定はないだろうという想定。)

Spring Framework の場合、設定クラス (Configuration) を DI してそちらからメールを送信するか / しないかを切り替えるための設定を読み出す方法が考えられる。 ただしこの場合、インスタンス変数がグローバル変数に移ったようにしか見えないため、完全な正解ではない気もする。

環境が用意できるのであれば、登録処理イベントを発行し、それを受信した「登録メール送信コマンド」がメール送信をするという「イベントソーシング」にしてもいいと思われる。 メール送信に時間がかかったり、失敗したときにユーザー登録処理自体を失敗としたくない場合などは、この方法が有効だと思う。

P199 8.4.1. 「ユーザ登録処理のユニットテスト

テスト用のリポジトリがデータ保管先としているフィールドを外部から操作できるようにすることは、きめ細かい検索を可能にし、テスト用モジュールのり弁性を向上させます。 フィールドを無闇に公開することは避けるべきですが、通常利用されるのは IUserRepository であるため Store プロパティを操作はできません。

これは目からウロコ。 確かに、C# の場合は LINQ で、Java の場合は Stream でコレクションを自由に操作できるため、テスト用リポジトリのフィールド等を公開しておけばテストの煩わしさがなくなる。

フィールドは private であるべき、という固定概念が崩れて、柔軟に考えられるようになった気がする。 あくまで外部からの操作はすべてインターフェース経由、ということを考えると、テスト用リポジトリだけではなく、本番用リポジトリテストのために フィールドを公開するのは有効だと思った。

P220 9.4 「複雑な生成処理をカプセル化しよう」

ポリモーフィズムの恩恵に与るためにファクトリを利用する以外に、単純に生成方法が複雑なインスタンスを構築する処理をまとめるためにファクトリを利用するのもよい習慣です。

あまり意見は無いんだけど、筆者の Web+DB での特集にて、インスタンス構築 と、インスタンス再構築 をメソッドで分けて実装されていたのを思い出した。

この使い分けはいいなーと思った記憶があるが、本書では特に構築と再構築を分けて記載しているわけではなかった。 プロジェクトや規模ごとに異なるとは思うが、上記の分け方は分かりやすいなと思っている。

P230 10.3.2 「ユニークキー制約との付き合い方」

ドメインのルールを守るための具体的な方法としてユニークキー制約に頼ることは得策と言えません。 ではユニークキー制約はまったく使えないものなのか、というとそれも間違いです。 (中略) ユニークキー制約はルールを守る主体ではなく、セーフティネットとして活用されるべき機能です。

これはその通り。 ユニークキー制約の他、外部キー制約やチェック制約などいくつかあるが、どの程度ドメイン駆動設計と併用されているのだろうと気になった。

P277 12.1.3 「内部データを隠蔽するために」

最初にもっとも単純な一般的なアプローチとして挙げられるのがルールによる防衛です。 (中略) もうひとつのアプローチは通知オブジェクトを使う方法です。

この通知オブジェクトというのは知らなかったが、エヴァンス本などにも書いてあるのだろうか。

ざっくり書くと、集約に通知メソッドを用意し、通知オブジェクトのインターフェースを経由して内部データを通知してもらう ものを指す。 通知オブジェクトのインターフェースはドメイン側で用意するため、通知する側にて内容を制限することもできるし、 通知される側も、通知オブジェクトのインターフェースを必要なものだけ実装すればいい。 もしドメイン側で項目が追加された場合、インターフェース実装クラスで追加実装が必要となり、コンパイルエラーとなる。

なかなかいい仕組みなのではないか? もう少し詳しく知りたいが、通知オブジェクトで検索してもあまり出てこないな・・・。

P320 14.1 「アーキテクチャの役割」

開発者は「一事が万事」といった言葉に目を向けず、いつかリファクタリングをすべきタイミングが訪れる、という夢を信じがちです。 (中略) もちろんそんなときは訪れません。

心当たりがありすぎて耳が痛い・・・。

P354 コラム「ユビキタス言語と日本語の問題」

オブジェクト名、メソッド名などを日本語で記述する試みは弊社でもいくつか例がある。 読みやすいという意見が多いが、エディターの補完機能の恩恵を受けることができないのは辛い。

例えば「店舗」を「te」と入力した時点で候補を出してくれるような、日本語とローマ字を突き合わせて補完候補を絞ってくれるプラグインなどがあればすごくいいんだけど。 結局「tempo」で実装するのが入力のしやすさと読みやすさのバランスがいい。仕事や会社のメンバーを考えると、無理に英語を使う必要はないかな・・・。

macOS Mojave (10.14.3) に Python3 の環境を構築する

お久しぶりです。

キカガクさんが公開している「MacPythonを使って『機械学習』を学ぶための環境構築」の手順で Python3 の環境を構築したところ、homebrew でインストールする際にエラーが発生したのでメモとして残しておきます。

play.kikagaku.co.jp

  • 1. Python3 のインストールにて失敗
  • 2. Python3 を再インストールする
  • 3. おまけ
  • まとめ
  • 参考サイト
続きを読む

Selenium + Internet Explorer 11 でファイルダウンロード

Selenium WebDriver で Internet Explorer 11 を操作し、ファイルダウンロードをしようとした時にハマったのでメモ。

問題

IE 11 でファイルをダウンロードした場合、状況によって 3 種類の画面が表示されます。

  1. ダウンロードダイアログ
  2. 通知バー
  3. ダウンロードの表示 ダイアログ

これらの画面がどういうもので、どういう状況で表示されるかについては、IE サポートチームのブログ記事をご覧ください。

ファイル ダウンロード時の通知バー、ダイアログ、ダウンロードの表示と追跡について – Japan IE Support Team Blog

Selenium + IEDriver でファイルをダウンロードした場合、上記画面が表示されると WebDriver を動かしているスレッドが停止してしまいます。

StackOverflow で調べると、その画面を操作するには java.awt.Robot を使いボタン操作で対応するという回答がありますが、そもそも処理が停止されているので Robot の処理が動きません。

対応

3 種類の画面のうち、いくつか試したところほぼ 1 と 2 が表示されたため、それらに対する対応方法です。

Robot の操作を別スレッドで処理し、ダウンロード画面を操作してメインスレッドを復帰させます。

以下、Kotlin でのサンプルコードです。

import org.openqa.selenium.ie.InternetExplorerDriver
import java.awt.Robot
import java.awt.event.KeyEvent
import java.nio.file.Files
import java.nio.file.Paths
import java.time.LocalDateTime
import kotlin.concurrent.thread

fun main(args: Array<String>) {

    // IEドライバー設定
    System.setProperty("webdriver.ie.driver", "C:\\Selenium\\InternetExplorer\\IEDriverServer.exe")

    val driver = InternetExplorerDriver()

    driver.get("http://example.com/files")
    driver.get("http://example.com/files/bigdata.csv")

    // 本ブログ記事のポイント
    // 別スレッドで、Alt + S ボタンを送信し、ファイルダウンロードする。
    thread {
        Thread.sleep(2000) // ダウンロード画面が表示されるのを待つ。
        val robot = Robot()
        robot.autoDelay = 250
        robot.keyPress(KeyEvent.VK_ALT)

        Thread.sleep(1000)
        robot.keyPress(KeyEvent.VK_S)
        robot.keyRelease(KeyEvent.VK_ALT)
        robot.keyRelease(KeyEvent.VK_S)
    }

    // driver#close() を呼び出すとブラウザが終了しファイルダウンロードもされないので、ファイルがダウンロードされるまで待機。
    // これを WebDriver 側で検知する方法を見つけられておらず、ファイル名で確認するしかなさそう。
    val downloadedFilePath = "C:\\Users\\rabitarochan\\Downloads\\bigdata.csv"
    val path = Paths.get(downloadedFilePath)
    while (!Files.exists(path)) {
        println("${LocalDateTime.now()} - ファイルがありません。")
        Thread.sleep(1000)
    }

    driver.close()

上記コードでは、ファイルダウンロード中にブラウザーが終了しないよう、ダウンロードファイルの作成をチェックしています。 IEDriver では、おそらくファイルダウンロード先を実行時に制御することはできないため、ユーザープロファイル以下の Downloads フォルダーにファイルが作成されるかどうかチェックしています。

まとめ

IE 縛りの Web ページ辛い。

参考サイト

Array を指定した個数で分割する ChunkPipe を作った

作ったってほどの話ではないです。

Bootstrap を使っていると、Array に入っている要素からグリッドを作成していことがあります。あるんです。

<!-- こんなデータがあったとして

let items = [
  { name: "はてなブログ", url: "http://hatenablog.com/" },
  { name: "はてなブックマーク", url: "http://b.hatena.ne.jp/" },
  { name: "人力検索はてな", url: "http://q.hatena.ne.jp/" },
  { name: "はてなアンテナ", url: "http://a.hatena.ne.jp/" },
  { name: "はてなキーワード", url: "http://k.hatena.ne.jp/" }
];

-->
<div class="container">
  <div class="row">
    <!-- container > row の中に col-md-4 のグリッドが 5 つ入ってしまう -->
    <div class="col-md-4" *ngFor="let item of items"> 
      <p><a [href]="item.url">{{ item.name }}</a></p>
    </div>
  </div>
</div>

Bootstrap 的には以下のように定義するべきだと思います。 (テンプレート展開後)

<div class="container">
  <div class="row">
    <div class="col-md-4"> 
      <p><a href="http://hatenablog.com/">はてなブログ</a></p>
    </div>
    <div class="col-md-4"> 
      <p><a href="http://b.hatena.ne.jp/">はてなブックマーク</a></p>
    </div>
    <div class="col-md-4"> 
      <p><a href="http://q.hatena.ne.jp/">人力検索はてな</a></p>
    </div>
  </div>
  <div class="row">
    <div class="col-md-4"> 
      <p><a href="http://a.hatena.ne.jp/">はてなアンテナ</a></p>
    </div>
    <div class="col-md-4"> 
      <p><a href="http://k.hatena.ne.jp/">はてなキーワード</a></p>
    </div>
  </div>
</div>

div.rowdiv.col-md-4 の 2 つのループが登場することになります。 まず元の Array を 3 個に分割して、その要素をループして・・・となりますが、元の Array を分割する処理をどこに書くか ? という問題が起きました。

今回は、Array を指定した個数で分割する ChunkPipe を定義して使ってみようと思います。

定義

ChunkPipe の定義は以下のとおりです。

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'chunk'
})
export class ChunkPipe implements PipeTransform {

  transform(value: any, args?: any): any {
    let array = value;
    let count = args;

    let len = Math.round(array.length / count);
    let chunked = [];
    for (let i = 0; i < len; i++) {
      chunked.push(array.slice(i * count, i * count + count))
    }
    return chunked;
  }

}

PipeTransform を実装した ChunkPipe クラスを定義します。 Pipe の定義方法は以下のドキュメントを参照してください。

Pipes - ts - GUIDE

このような処理は、PHP には array_chunk 関数で用意されています。 もちろん、NPM にもあります。

www.npmjs.com

使い方

row のループをする際に、ChunkPipe でまず Array を分割して、その後分割した Array で div.col-md-4 をループします。

<div class="container">
  <!-- row を ngFor でループする際に、chunk:3 を指定し、要素数が 3 個の Array を生成する。 -->
  <div class="row" *ngFor="let chunked of items | chunk:3">
    <!-- col-md-4 をループする -->
    <div class="col-md-4" *ngFor="let item of chunked"> 
      <p><a [href]="item.url">{{ item.name }}</a></p>
    </div>
  </div>
</div>

すると、あるべき姿の HTML を生成することができます。

注意点

今回定義した Pipe は、pure な Pipe を定義しています。 Array 自体や Array の中身を変更しても画面に表示されない場合、impure なPipe に変更すると上手くいくかもしれません。

pureimpure については、@mitsuruog さんのブログ記事で解説されています。

blog.mitsuruog.info

ただし、性能はちょっと悪くなる可能性もあります。

まとめ

Angular で Array を表示する際のちょっとした小ネタでした。 Component 側で分割した Array を作っている方などはぜひ使ってみてください。

DatePipe を IE11 等で動かすと変な表示になる件に対応する

Angular で日付をフォーマット表示する場合、DatePipe を利用します。 Pipe とは、AngularJS で言う Filter で、構文も変わっていません。

<p>Date: {{ now | date:'yyyy-MM-dd HH:mm:ss' }}</p>

さて、このシンプルなフィルターですが、ブラウザごとに異なる表示になってしまいます。 以下の URL を開いてみてください。

https://plnkr.co/edit/lVCkJcIopxTOk5L9iyCm?p=preview

どうでしたか ? IE11, Edge の方は上手く表示されていないのではないかと思います。

Expected: 2017-04-01 12:34:56

Actual: 2017-04-01 12:00:4/1/2017 12:34:56 PM:4/1/2017 12:34:56 PM

どーやったらこんな表示になるんだ・・・。

どうやら不具合っぽい

もちろん、Issue は立っています。

date pipe formatter doesn't format minutes and seconds properly in IE 11.0.9600.18350 and Edge · Issue #9524 · angular/angular · GitHub

また、ドキュメントには以下の通り書いてあります。

DatePipe - ts - API

WARNINGS:

  • this pipe is marked as pure hence it will not be re-evaluated when the input is mutated. Instead users should treat the date as an immutable object and change the reference when the pipe needs to re-run (this is to avoid reformatting the date on every change detection run which would be an expensive operation).
  • this pipe uses the Internationalization API. Therefore it is only reliable in Chrome and Opera browsers.

重要なのは最後の 1 文。 DatePipe は I18n API を利用しているので、ChromeOpera でのみ信頼性があります。

Workaround: moment を利用する

Issue にもコメントがありますが、Workaround として以下 2 つが提示されています。

  • date:'short'date:'shortDate' はちゃんと動くよ !
  • moment を使った Pipe を定義しよう !

任意のフォーマットで表示したいのに date:'short' は動くと言われても困ります。

ということで、 moment を使った Pipe を定義しましょう。

1. npm install

はじめに、moment への依存設定を追加します。

npm install moment --save

なお、TypeScript の型定義は moment に含まれているため、別途 @types/moment を導入する必要はありません。(導入すると、追加しなくていいよと警告が表示されます。)

2. Pipe 実装

以下のように Pipe を実装します。

angular-cli をご利用の方は ng g pipe moment.pipe で雛形を生成できます。

import { Pipe, PipeTransform } from '@angular/core';
import * as moment from 'moment';  // (1)

@Pipe({
  name: 'moment' // (2)
})
export class MomentPipe implements PipeTransform {

  transform(value: any, args?: any): any {
    if (value === undefined || value === null) return '# value is undefined or null #';  // (3)
    if (args === undefined || args === null) return '# args is undefined or null #'  // (4)
    let s = moment(value).format(args);
    return s;
  }

}
No 解説
(1) moment を参照します
(2) name に指定した名前で Pipe を利用できます
(3) 日付 or 日付形式の文字列が定義されているかのチェックです
(4) 引数 (フォーマット文字列) が指定されているかのチェックです

3. Module 登録

Pipe を実装したら、忘れずに Module に登録します。

angular-cli を使って生成した場合は追加されているかも。

以下は、Pipe をまとめた PipesModule を定義した場合の例です。

// import 省略

@NgModule({
  imports: [
    CommonModule
  ],
  exports: [
    MomentPipe  // (1)
  ],
  declarations: [
    MomentPipe  // (2)
  ]
})
export class PipeModule { }
No 解説
(1) PipeModule を import したモジュールで MomentPipe を利用したい場合、exports に追加する必要があります。
(2) declarations に MomemtPipe を追加し、モジュール内で利用できるようにします。

まとめ

IE11 等で DatePipe でフォーマット指定がうまくいかないため、代替案として moment を利用した Pipe を作成しました。

DatePipe で困っている方は参考にしてみてください。

Angular で複数回発生するイベントを抑制する (debounce)

最近は Angular と戯れています。

Qiita の自動保存のように、入力イベントを検知し、最後の入力から指定時間経過した後に何らかの処理を実行する方法についてメモ。

Component クラス

Component クラスにて、入力イベントを扱う Subject を定義します。

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subject, Subscription } from 'rxjs';

@Component(/* 省略 */)
export class TestComponent implements OnInit, OnDestroy {

  // イベントをまとめる Subject
  autoSaveEvent: Subject<void> = new Subject<void>();

  // Subscribe 情報
  autoSaveSubscription: Subscription = null;

  ngOnInit() {
    // "debounceTime()" をコールし、イベントを 5000 ミリ秒抑制してから処理する。
    this.autoSaveSubscription =
      this.autoSaveEvent.debounceTime(5000).subscribe(
        () => { /* 何らかの処理 */ }
      );
  }

  ngOnDestroy() {
    // unsubscribe しないと、別の Component に遷移してもイベントが発行してしまうので注意。
    if (this.autoSaveSubscription != null) {
      this.autoSaveSubscription.unsubscribe();
    }
  }

}

Component クラスでのポイントは以下 2 点です。

  1. Subject<T>#debounceTime(milliSeconds: number) を呼び出し、イベントを指定時間抑制する。
  2. 不要になったら (上記例では、Component のインスタンスが廃棄されたタイミング) unsubscribe する。

特に 2 が重要です。Component が廃棄されても発火したイベントや Subscribe は自動で解除されません。

Template

Template では、Input 要素のイベント発生時に Subject<T>#next() をコールし、イベントを発生させます。

<input type="text" name="input1" [(ngModel)]="input1" (keyup)="autoSaveEvent.next()">
<textarea name="input2" [(ngModel)]="input2" (keyup)="autoSaveEvent.next()">

ここでのポイントは 2 点です。

  1. (keyup) で、keyup イベントにバインドする
  2. Component クラスに Subject を用意したため、複数のイベントを 1 つのイベントソースのようにみなして処理できる。