りんごとバナナとエンジニア

エンジニア修行の記録

クリックすると一瞬だけ光るボタンを作る

Vue.jsの勉強としてFizzbuzzゲームを作っている。数字を出して答えを4択で選ばせるのだが、選んだときに正解なら緑、不正解なら赤に一瞬だけ光らせたい。
Vue.jsのtransition/animationの項目を読んだが、これは要素を表示/非表示にするときの話で、色を変えるときの話ではなかった。
CSS@keyframesを使おうともしたが、これは状態をAからBに変えるというものであり、今回のように色を変えてまた戻すという処理とは違う。

どう実装すればいいか悩んでいろいろ調べた結果、単純にCSSとJSで制御すればいいと気づいた。

実装

See the Pen Button Blink with CSS Transition by Udomomo (@Udomomo) on CodePen.

↑読み込まれないようなら下のリンクから見られる
Button Blink with CSS Transition

作り中のコードから引っ張ってきたのでVue.jsによる実装になっているが、この部分だけ見ればVue.jsならではの機能は全く使っていない。
まず、CSSでボタンのdiv要素全体のbackground-colorにtransitionを設定しておく。これで背景色が変わるときはアニメーションが発生するようになる。
そのうえで、JSでクリックイベントを検知し、背景色を変える処理をする。そして、一定時間が経ったらsetTimeoutを呼び出して元の色に戻す。
transitionの所要時間を十分短くし、かつそれより少しだけ長い時間を指定してsetTimeoutを発火させれば、一瞬だけ光って元に戻るように見える。

学んだこと

Vanilla JSは大事。
JSフレームワークの機能だけに囚われないようにしないと。

webpackは何をするものなのか

今までJSフレームワークを扱うときは特に深く考えずにwebpackを見よう見まねで使っていたが、業務で開発プロセスの改善について考えることになり、webpackの役割についてちゃんと理解しなければいけないことになった。
webpackの細かい機能は都度覚えていくしかないが、「何をするものなのか」はいろいろ調べて理解が深まったのでまとめておく。

webpackとはなにか

静的なasset(CSS, JS, 画像, フォントなど)をビルドしてバンドルするためのもの。バンドルとは平たく言うとひとつにまとめること。
静的なassetというのは、リクエストによって内容が変わったりしないファイルのこと。HTMLは、JSからDOMを操作されて内容が書き換えられたうえで返されることもあるが、CSSやJSの内容が書き換えられることはない。

webpack公式のトップページにあるこの図が全てを表している。

f:id:Udomomo:20180617124212p:plain

大規模な開発をするときは、require()などを使って、モジュールごとにファイルを分離して書くとわかりやすい。
しかし、ブラウザ上で動くJSはモジュール分割に対応しておらず、このままでは動かない。
そこで、webpack等のビルドツールを使うことで、モジュール同士の依存関係をふまえたうえでファイルをまとめてくれる。ファイルサイズが小さくなるようにminifyもしてくれる。
HTMLWebpackPluginを使うと、バンドルしたJSを読み込むHTMLも生成される。テンプレートがあればそれをもとにすることも可能。
あとはビルドで吐き出されたファイルとHTMLをサーバにアップロードすれば、開発中に想定していたとおりの動きをする。

また、webpackではJS以外の静的assetも管理できる。そのため、JSだけでなくCSSや画像などを読み込むときもrequire()を使って簡単に書くことができる。
例えば画像なら<img src={ require('../../assets/logo.png') } />などのように書ける。

静的assetを管理できると何がよいか?

まず、静的assetの過不足がなくなる。ビルドに使われるのは、コード内でrequire()されているもののみであるため、使わないassetが排除される。
逆に、JSは変えたがCSSが古いままだった・画像がリンク切れだったというようなことも起こらない。もしそのようなものがあれば、ビルドが通らないので本番公開前に気づくことができる。

また、静的assetをどう読み込むかもコントロールできるようになる。例えばbase64エンコードをかけて読み込んだり、サイズが大きければビルドに含めずwebpack外のURLから読み込む形にするなどの設定もでき、静的assetを柔軟に扱いやすくなる。

開発用サーバも立てられる

フロントエンド開発でネックになるのは、ビルドしないとブラウザ上で動かないこと。
動作確認をするのに、毎回npm run buildしてサーバにアップロードしなければいけないのでは面倒すぎる。

そこでwebpackでは、ローカルに簡単なサーバを立てる機能も提供されている。よく使われているのはwebpack-dev-server。
ローカルでの開発のための、小さなexpress製アプリである。localhost:8080/assets/bundle.jsのように指定すれば、すぐにそのファイルを擬似的にデプロイして確認できる。 watchの設定をすれば、ローカルサーバが立っている状態でファイルを更新すると、自動でコンパイルし直して画面に反映してくれる。
また、webpack-dev-serverサーバサイドとAPIで通信させたいときは、proxyの設定でリクエストを送るURLとサーバサイドのURLを対応させることができる。

webpackは今後も必要か?

ECMA2015では標準仕様としてモジュールが使えるようになったため、今後はブラウザ上でもモジュール分割を使えるようになっていき、webpackなどを使わなくてもよくなる場面が増えるかもしれない。
とはいえ、TypeScriptを使って開発する場合など、webpackがあった方が開発な楽なケースはまだまだあるだろう。
この辺りの話は以下のページがわかりやすい。

ics.media

ドライバからDBを操作するときはPreparedStatementを使う

今仕事で使っているコードではJavaからCassandraとMySQLを動かしているが、その両方で使っているのがPreparedStatement。
これはどういうものかというと、あらかじめクエリのひな形を用意しておいて、実際に投げるときに値をバインドするというもの。

例えばJavaのCassandra Driverなら、

PreparedStatement psSelectTestTable = session.prepare("SELECT * FROM test_table WHERE id = ?;");

のようにあらかじめクエリを宣言しておく。 そして投げるときは、

BoundStatement bdSelectTestTable = psSelectTestTable.bind("1");
ResultSetFuture resultSetFuture = session.executeAsync(bdSelectTestTable);

のようにする。PreparedStatementの"?"のプレースホルダ部分に1が入った状態で、クエリが投げられることになる。

このようなPreparedStatementに相当する機能は、Cassandra Driverだけの話ではなく、Pythonのmysqlclientなど、他のDBドライバにもある。
名前は異なれど、「クエリのひな形を用意して後から値をバインドする」という方式を取っているものが多いだろう。

PreparedStatementを使うと何が良いのかは、大きく2つある。
1つは、クエリをあらかじめ用意しておくことで、毎回コンパイルする手間を減らし、パフォーマンスを向上させること。
しかしこれは、単にクエリを1回投げるだけであれば意味がない。むしろ、アプリケーションのサーバサイドなど、同じクエリのひな形を何度も繰り返し使うときに有用となる。
この意味で、上にあげたようなCassandra DriverはPreparedStatementを積極的に使うべき例といえる。上の例ではクエリを1回しか投げていないが、Cassandraは非同期で多数のリクエストを処理するのが得意なDBなので、大規模データを高速に処理したい状況で使われることが多いためである。

もう1つは、SQLインジェクションの基本的な対策ができること。クエリの構文解析自体はプレースホルダの状態で行われ、それが通った後に値をはめ込んで実行する。このため、値の部分に悪意あるクエリが入力されたとしても、そもそもクエリとはみなされないため、実行されてしまうことがない。

CassandraのDELETEの振る舞いについて

Cassandraを障害時のバックアップとして使うにあたり、DELETE時の処理を考える過程で学んだこと。

DELETEしてもすぐには消えない

CassandraのDELETEコマンドは、RDBMSとは異なりすぐに消えるものではない。
ある行をDELETEしても、その行のデータはディスクに残り続ける。その代わり、tombstoneというフラグが立ち、ユーザが削除の指示をしたことがわかるようになっている。つまり、DELETEコマンドもまたUPDATEコマンドの一種にすぎない。

tombstoneが立った行は、スキーマでのgc_grace_secondsで指定した秒が経過して初めてディスクから消去される。
これは、大量データの処理速度を最優先させるというCassandraの設計思想に基づいたものだ。

DELETEが起こるテーブルを設計するときのポイント

この性質は、テーブルの設計をする時にも考えなければいけない。
例えば、ある行をDELETEした後、残った行をSELECTでスキャンする場合。
このとき、gc_grace_seconds以内であれば、DELETEした行はまだディスクには残っているため、当然スキャンの対象となる。「DELETEして行の数が減ったぞ」と思っていると、予想以上にSELECTに時間がかかってしまうことがある。

さらに、Cassandraはスキャンをする際に、tombstoneをコーディネーターに伝えるためメモリ上に保管しておく。このため、短時間に多くの行をDELETEしてtombstoneがたまると、パフォーマンスに影響を及ぼし、最悪の場合はサーバ上のヒープが吹っ飛ぶ可能性もある。
具体的な検証をまじえた解説は、Cassandra公式の以下のブログで行われている。

www.datastax.com

また、同じ値の主キーを何度もDELETE/INSERTする時も注意が必要になる。
これは特定の行を頻繁にUPDATEしていることと同じになる。ミリ秒単位でtimestampが同じになるほど頻繁にDELETE/INSERTをした結果、予想外の結果が返ってきたという例もあるようだ。

stackoverflow.com

もちろんgc_grace_secondsの設定があるのでtombstoneが永遠に残ることはないが、上記のようなことが短時間で頻繁に起こるのは良くない。
最も良いのは、削除したものも含めて常に主キーが一意となり、そして大量のスキャンをなるべく避けられるような設計のテーブルを作ることだろう。削除した行が再びSELECTやUPDATEの対象となる可能性を減らすことができれば、パフォーマンスへの影響も出にくくなる。

そのバグが本当にバグなのかを見極める

今の会社では、カスタマーサポートなど非エンジニアもイシューを上げられるようにしているが、彼らがバグとしてあげてきたイシューには、実は対応が不要なものも多く混じっている。
何から対応するか決めるときには、「バグ」と「仕様の問題」を混同しないように気をつけた方がいい。

  • バグ: 仕様として想定されているのとは異なる動作をするということ。
  • 仕様の問題: 動作自体は仕様の通り。ただしそれが特定のユーザにとって望ましくない可能性がある。

この2つは、その後の意思決定が大きく異なる。バグならもちろんすぐ潰すべきだ。
しかし仕様の問題なら、直そうとすると仕様変更になる。その変更が本当に全てのユーザにとって望ましい変更であるか、その問題はどれほど頻繁に発生しどれほど影響が大きいかなどを考えたうえで、対応するか見送るかを決めなければならない。

また、ごく特別な状況でしか起こらないという場合もありうる。
例えば、特定のブラウザでのみ発生する、特定の時刻でたまたま処理が重くなっていたためにエラーが起きる、などのようなことが実際にあった。このような場合、やはり即時対応はできない。
バグが上がってきた場合、まずはそれが本当にバグとして扱ってよいものかを立ち止まって考える必要があるだろう。

Pythonで深さ優先探索を実装する

前の記事で幅優先探索を実装したので、ついでにこちらも。
問題はAtCoder Typical Contest 001A。一応解説スライドはあるが、スタックを使った実装方法が書いてないので、自分でやってみることにした。

実装

from collections import deque

def dfs(maze, visited, sh, sw):
    stack = deque([[sh, sw]])
    visited[sh][sw] = 1
    while stack:
        h, w = stack.pop()
        if maze[h][w] == "g":
            return "Yes"
        for j, k in ([1, 0], [-1, 0], [0, 1], [0, -1]):
            new_h, new_w = h+j, w+k
            if new_h < 0 or new_h >= H or \
               new_w < 0 or new_w >= W:
                continue
            elif maze[new_h][new_w] != "#" and \
                visited[new_h][new_w] == 0:
                visited[new_h][new_w] = 1
                stack.append([new_h, new_w])
    
    return "No"

if __name__ == "__main__":
    H, W = map(int, input().split())
    maze = [input() for _ in range(H)]

    for i in range(H):
        if maze[i].find("s") != -1:
            sh = i
            sw = maze[i].find("s")
            break

    visited = [[0]*W for _ in range(H)]
    print(dfs(maze, visited, sh, sw))

基本的には、幅優先探索の場合と流れはほぼ同じ。
ただし、今回は後入先出のスタックを使っているため、上下左右にマスが分岐する際に、どれかひとつをこれ以上進めなくなるまで調べ尽くす。

今回の問題のように、単にゴールまでのルートがあるかを素早く調べるには、深さ優先探索の方がよいだろう。

Pythonで幅優先探索を実装する

アルゴリズムの勉強のために、幅優先探索を書いてみた。
使ったのはAtCoder Beginers Contest 007Cの問題。この頃はアルゴリズムがそのまま出題されてたようだ。

特殊事項として、この問題ではスタートからゴールまでは必ず行くことができる前提がある。さらに周り中が壁で囲まれているので、盤面からはみ出すのを考慮する必要がない。

実装

from collections import deque

def bfs(maze, visited, sy, sx, gy, gx):
    queue = deque([[sy, sx]])
    visited[sy][sx] = 0
    while queue:
        y, x = queue.popleft()
        if [y, x] == [gy, gx]:
            return visited[y][x]
        for j, k in ([1, 0], [-1, 0], [0, 1], [0, -1]):
            new_y, new_x = y+j, x+k
            if maze[new_y][new_x] == "." and \
                visited[new_y][new_x] == -1:
                visited[new_y][new_x] = visited[y][x] + 1
                queue.append([new_y, new_x])

if __name__ == "__main__":
    R, C = map(int, input().split())
    sy, sx = map(int, input().split())
    gy, gx = map(int, input().split())
    sy, sx, gy, gx = sy-1, sx-1, gy-1, gx-1

    maze = [input() for i in range(R)]
    visited = [[-1]*C for j in range(R)]
    print(bfs(maze, visited, sy, sx, gy, gx))

たった25行で書けた。

原理としては、今いるマスから上下左右に隣り合うマスの座標をキューに入れるとともに、visitedリストにも記録していくという繰り返し。
キューは先入先出なので、今いるマスから上下左右に隣り合うマスを全て探索した後、それらの上下左右に隣り合うマスを探索する、というように探索を行っていく。
全マス調べるため時間はかかるが、ゴールへの最短ルートを確実に求められる。

速度アップの工夫

実をいうと、上記のコードは2度目の提出だった。
その前に書いたバージョンでは、bfs関数を以下のようにしていた。

def bfs(maze, visited, sy, sx, gy, gx):
    queue = deque([[sy, sx]])
    count = 0
    while queue:
        //ここでforループを回している
        for _ in range(len(queue)):
            coord = queue.popleft()
            visited[coord[0]][coord[1]] = 1
            if coord == [gy, gx]:
                return count
            for j, k in ([1, 0], [-1, 0], [0, 1], [0, -1]):
                next_coord = [coord[0]+j, coord[1]+k]
                if maze[next_coord[0]][next_coord[1]] == "." and \
                   visited[next_coord[0]][next_coord[1]] != 1:
                    queue.append(next_coord)
        count += 1

visitedリストの値を0=未訪問と1=訪問のみとしていたため、bfs関数の中のwhile queue以下でforループを回して、ループを何回まわったかをカウントしスタートからの距離を求めていた。しかし、テストケースの最初の4分の1くらいで実行時間が2秒を超え、あえなくTLEに。
そこで、visitedリストの値にスタートからの距離を直接書かせることにした。まずスタートマスの値を0、未訪問のマスの値を-1としておく。そして新しいマスを探索するときは、visitedリストから前にいたマスの値を取り出し、それに1を足した値を新たに書けばよい。
こうすることで、forループをひとつ減らすことができ、実行時間も全体で25msと大幅に速くなった。リストをうまく使うことで、回数カウント目的のforループをなくすというテクニックは、他の問題でも応用できそうだ。