読者です 読者をやめる 読者になる 読者になる

理系学生日記

おまえはいつまで学生気分なのか

忍者TOOLS

楽天Edyのオートチャージ額変更にはオンライン/オフラインの両方で設定変更が必要になる

今年から楽天 Edy を使うようになりました。 ぼくにとっては初の電子マネーだったわけですが、財布から小銭を取り出すことなしに買い物ができ、かつ、お釣りとして小銭が返ってこないというのは大変良いですね。毎日、会社最寄りの FamilyMart で Edy で買い物をするようになりました。

で、この Edy にはオートチャージの機能がありまして、残金が設定した金額以下になると、一定額を自動チャージしてくれるというものになります。

使い始めるときに設定したもので、チキンなぼくは 1,000 円を切ると 1,000 円をオートチャージするようにしていました。 しかしですね、このような設定ですと、1,000 円を切った状態で 1,000 円以上の買い物ができないわけですね。これは不便だ。 というわけですので、オートチャージの額の設定を変更しようと思ったら、マジで違和感ある方法になっていました。

オートチャージの設定変更は、

  1. Web でオートチャージ設定を変更する。これにより、カードに設定反映待ちの状態になる
  2. FamiryMart の Fami ポート、あるいは、楽天 Edy リーダー等でカードに対する変更をおこなう

という 2 ステップが必要になります。

Web 側、というか、Edy のシステム側だけで、オートチャージ設定情報を保持しているのだと思っていたんですが、カード側にも何らかの情報が記憶されているんですかね。

PoEAA: Two Step View

最終的に HTML を出力する場合であっても、一度論理的な View に変換した後で、それを実際の View に変換するという 2 ステップを踏むパターンを、文字通り Two Step View と呼びます。 JVM の中間コードだったり、LLVM の IR みたいなもの。主なメリットは、個々のページで共通的な箇所を論理的な View で抽象化でき、全体のページのレイアウト等を変える等しやすい点。

Patterns of Enterprise Application Architecture (Addison-Wesley Signature Series (Fowler))

Patterns of Enterprise Application Architecture (Addison-Wesley Signature Series (Fowler))

最初に読んだときはあまりイメージが分かりませんでしたが、なるほど、

  1. Action でドメイン情報を含んだ XML を出力し
  2. その XML を XSLT で論理 Screen (論理的な View) の XML に変換し、
  3. その XML をさらに XSLT で HTML に変換する

という例や、

  1. 論理 Screen を Template View を使って ` として構成して、
  2. それをさらに HTML に変換する

という例が提示されていて、なるほど〜〜〜と思いました。抽象化というのはそういうものですが、現実を抽象化できない抽象化はもはや抽象化ではないのであって、うまく抽象化できないと死ぬパターンですねこれ。 また、2 ステップによる View の構築は、エンジニアがデザインに巻き込まれることを意味しており、このあたりがデメリットであるというのも本文の中で語られています。

The Go Programming Language

The Go Programming Language を読みました。

The Go Programming Language (Addison-Wesley Professional Computing Series)

The Go Programming Language (Addison-Wesley Professional Computing Series)

Golang が流行ってきていたので、じゃぁ買って理解するかーと思って購入したそのすぐ後で日本語訳が出版されたので、なかなか辛いものがある。

プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)

プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)

内容

本としては、おおよその言語仕様を詳説しているという内容でした。

  • データ型
  • 関数
  • メソッド
  • Goroutine
  • Channel
  • 並行プログラミング
  • リフレクション

このため、標準パッケージそのものに焦点を当てた記述は特になかったように思われるのですが、それでも様々な箇所のサンプルコードで標準パッケージが使われており、それに対する言及も行われていて、全体として基本的なパッケージの使い方も分かるようになっています。 その意味では、K&R のプログラミング言語 C の位置づけだと思うと分かりやすいかもしれません。

プログラミング言語C 第2版 ANSI規格準拠

プログラミング言語C 第2版 ANSI規格準拠

プログラミング言語としての Go

C 言語のアップグレード版とも称される Go ですが、確かに C よりはずっと書きやすいだろうなぁという印象でした。 構造体やポインタがある点は共通ですが、ポインタの値を直接演算することはできません。また、言語として Map を持っているというのが扱いやすさとして大きく違うと思います。 何より、標準ライブラリの数が全く違う (その意味では、C や C++ が少ないというのはありそうですが) ので、何か困ったときであっても大概の処理は標準ライブラリに任せれば良いなぁという安心感があります。

それでも、言語としての最も大きなメリットは、クロスコンパイルだと思います。 Windows 環境で開発して実運用は Linux、とか、MacOS で開発して動かすのは Windows とか、そういうことが多い状況なので、1 ソースで、様々な環境に適応できるのは嬉しいなぁと。 これからも学んでいきたいと思います。

go 1.8 でのサポート環境は以下のとおり。

$ gox -osarch-list
Supported OS/Arch combinations for go1.8 are shown below. The "default"
boolean means that if you don't specify an OS/Arch, it will be
included by default. If it isn't a default OS/Arch, you must explicitly
specify that OS/Arch combo for Gox to use it.

darwin/386      (default: true)
darwin/amd64    (default: true)
linux/386       (default: true)
linux/amd64     (default: true)
linux/arm       (default: true)
freebsd/386     (default: true)
freebsd/amd64   (default: true)
openbsd/386     (default: true)
openbsd/amd64   (default: true)
windows/386     (default: true)
windows/amd64   (default: true)
freebsd/arm     (default: true)
netbsd/386      (default: true)
netbsd/amd64    (default: true)
netbsd/arm      (default: true)
plan9/386       (default: false)
dragonfly/386   (default: false)
dragonfly/amd64 (default: false)
nacl/amd64      (default: false)
nacl/amd64p32   (default: false)
nacl/arm        (default: false)
solaris/amd64   (default: false)
android/arm     (default: false)
plan9/amd64     (default: false)
darwin/arm      (default: false)
darwin/arm64    (default: false)
linux/arm64     (default: false)
linux/ppc64     (default: false)
linux/ppc64le   (default: false)

Golangでジオロケーションを行ってみる

大量の住所を Google Maps 上にマッピングする必要が生じ、住所を緯度と経度に変換することになったので、いわゆるジオコーディングをプログラムすることにしました。 ジオコーディングに関しては、Google Maps API が無料で使用できて、かつ、扱いやすいので、これを使うことにします。

Google Maps API では、緯度や経度が 10 進数で返却されるのですが、今回は諸事情で 60 進数 (度分秒) で表現する必要があったので、そのあたりの処理も入れています。

仕様

  1. 入力は住所やランドマークを記載した CSV ファイル
  2. 出力は、住所やランドマーク、緯度、経度を記載した CSV ファイル

具体的な例としては、以下のような入力に対して、

東京ドーム
東京駅

以下のファイルが出力されます。

東京ドーム,35°42'20.3"N,139°45'6.8"E
東京駅,35°40'52.7"N,139°45'58.5"E

緯度、経度をこういう形式で記述するのは初めてでしたが、Google Maps はこの形式でも場所を特定できるんですね。知らなんだ。

技術要素

Golang 入門者なので、今回は

  • CSV からの読み取り、書き込み
  • JSON のデコード
  • Channel
  • errgroup

あたりにチャレンジしました。

JSON から Go Struct の生成

Go は静的言語であるが故に、基本的には JSON と Go の構造体 (Struct) をマッピングする必要があります(interface を使用して回避することはできますが、逆に実装がダルいことになります…) 今回は、Google Maps API のレスポンスのうち、必要なデータは限られていたので、以下のような struct を作っています。

type Geo struct {
    Results []struct {
        FormattedAddress string `json:"formatted_address"`
        Geometry         struct {
            Location struct {
                Lat float64 `json:"lat"`
                Lng float64 `json:"lng"`
            } `json:"location"`
        } `json:"geometry"`
    } `json:"results"`
    Status string `json:"status"`
}

この Struct とのマッピングを記述するのはわりかしダルいので、ぼくは JSON-to-Go というサービスを使いました。

左側のテキストボックスに JSON をぶちこむと、右側に Go の struct が出力されます。 こういうところを手で書くほど潤沢な時間が人類に残されているわけではないので、今後も積極的に使っていきたい。

実装コード

Go、まだ慣れない。

package main

import (
    "context"
    "encoding/csv"
    "encoding/json"
    "flag"
    "fmt"
    "io"
    "math"
    "net/http"
    "net/url"
    "os"

    "golang.org/x/sync/errgroup"
)

func main() {
    var infile = flag.String("i", "", "住所が入ったCSVファイル")
    var outfile = flag.String("o", "", "出力するCSVファイル")

    flag.Parse()

    if *infile == "" {
        fmt.Fprintf(os.Stderr, "specify csv file with -i\n")
        os.Exit(1)
    }
    if *outfile == "" {
        fmt.Fprintf(os.Stderr, "specify output file with -o\n")
        os.Exit(1)
    }

    if err := newApp(*infile, *outfile).run(); err != nil {
        fmt.Fprintf(os.Stderr, "%v", err)
    }
}

type Geo struct {
    Results []struct {
        FormattedAddress string `json:"formatted_address"`
        Geometry         struct {
            Location struct {
                Lat float64 `json:"lat"`
                Lng float64 `json:"lng"`
            } `json:"location"`
        } `json:"geometry"`
    } `json:"results"`
    Status string `json:"status"`
}

type App struct {
    AddressFile   string
    GeoDecodeFile string
    Client        *http.Client
}

// NewApp creates a new application with input and output file pointer.
func newApp(infile, outfile string) *App {
    return &App{
        AddressFile:   infile,
        GeoDecodeFile: outfile,
        Client:        &http.Client{},
    }
}

func (app *App) run() error {

    infp, err := os.Open(app.AddressFile)
    if err != nil {
        return fmt.Errorf("open %s: ", app.AddressFile)
    }
    defer infp.Close()

    outfp, err := os.Create(app.GeoDecodeFile)
    if err != nil {
        return fmt.Errorf("open %s: ", app.GeoDecodeFile)
    }
    defer outfp.Close()

    eg, ctx := errgroup.WithContext(context.Background())
    q := make(chan string, 1000)

    eg.Go(func() error {
        return app.enqueue(ctx, infp, q)
    })

    eg.Go(func() error {
        return app.putGeocode(ctx, outfp, q)
    })

    if err := eg.Wait(); err != nil {
        return err
    }

    return nil
}

func (app *App) enqueue(ctx context.Context, fp *os.File, q chan<- string) error {
    reader := csv.NewReader(fp)
    reader.LazyQuotes = true

    for {
        records, err := reader.Read()
        if err == io.EOF {
            break
        }
        if err != nil {
            return fmt.Errorf("error while reading %s", fp.Name())
        }

        select {
        case <-ctx.Done():
            return ctx.Err()
        case q <- records[0]:
        }
    }
    close(q)
    return nil
}

func (app *App) putGeocode(ctx context.Context, fp *os.File, q <-chan string) error {
    for address := range q {
        lat, lng, err := app.geocode(ctx, address)
        if err != nil {
            return fmt.Errorf("decode %s", err)
        }
        fmt.Fprintf(fp, "%s,%sN,%sE\n", address, convert(lat), convert(lng))
    }
    fp.Sync()
    return nil

}

// 10 進数による座標を 60 進数(度分秒)に変換する
// ref. http://www.benricho.org/map_latlng_10-60conv/
func convert(n float64) string {
    degree := math.Trunc(n)
    leftover := (n - degree) * 60

    minute := (int)(math.Trunc(leftover))
    leftover -= (float64)(minute)

    second := leftover * 60
    return fmt.Sprintf("%d°%d'%3.1f\"", int(degree), minute, second)
}

func (app *App) geocode(ctx context.Context, address string) (lat, lng float64, err error) {
    values := url.Values{}
    values.Add("address", address)

    req, err := http.NewRequest("GET", "https://maps.googleapis.com/maps/api/geocode/json", nil)
    if err != nil {
        return -1, -1, err
    }
    req = req.WithContext(ctx)

    req.URL.RawQuery = values.Encode()
    resp, err := app.Client.Do(req)
    if err != nil {
        return -1, -1, err
    }
    defer resp.Body.Close()

    var geo Geo
    decoder := json.NewDecoder(resp.Body)
    err = decoder.Decode(&geo)
    if err != nil {
        return -1, -1, err
    }

    fmt.Printf("[%d] %s\n%s\n\n", resp.StatusCode, address, geo)
    l := geo.Results[0].Geometry.Location
    return l.Lat, l.Lng, nil
}

任意のプログラムをWindowsサービスとして構成する

Windows で Java のデーモンをサービスとして起動させときたかったんですが、なんか良いものないかなと思ったら winsw というのを見つけました。

作者は Jenkins の川口耕介さんであり、もともと winsw も Jenkins を念頭に作成されたらしいです。 実際に GitBucket を動かしてみたところ、問題なくサービス化できています。

手順

Github 上にインストールガイドがあるので、それに従えば良いと思います。 というのも乱暴かもしれませんので、以下に概要を記載しておきます。ここでは、prg というプログラムをサービス化するという前提で記載します。

  1. winsw.exe をダウンロードして、prg.exe にリネームする
  2. 設定ファイルとして、prg.xml を作成する (後述)
  3. prg.exeprg.xml を同じフォルダに配置する
  4. コマンドラインで、prg.exe install を実行する。これで Windows に prg がサービス登録される。

あとは、Windows Service Manager を使って、Windows の起動に合わせて起動をかける等、通常のサービスと同様に設定が可能になります。

設定ファイル

以下が Jenkins 用のサンプル設定。

<service>
  <id>jenkins</id>
  <name>Jenkins</name>
  <description>This service runs Jenkins continuous integration system.</description>
  <env name="JENKINS_HOME" value="%BASE%"/>
  <executable>java</executable>
  <arguments>-Xrs -Xmx256m -jar "%BASE%\jenkins.war" --httpPort=8080</arguments>
  <logmode>rotate</logmode>
</service>

書いてあることはだいたい読めば分かりますが、上で記述されている中で分かりづらいものがあるとすると、BASE くらいかと思います。

  • BASE: winsw.exe (をリネームしたもの) が配置されているディレクトリ。環境変数として扱われるので、%BASE% で値が参照できる。

結構細かな設定が可能でして、

  • 作業ディレクトリ
  • サービスから出力されるログの扱い (ローテーション等)
  • サービス起動時のタイムアウト
  • 起動時に設定ファイル等をダウンロードできる (auto update)

とかが可能です。

Windows で何かデーモン的なものを立ち上げるときには、採用を検討しても良いのではないでしょうか。

PoEAA: Transform View

今日の Transform View は、View として、ドメイン層のデータをインプットにして、HTML を出力するトランスレータを採用するパターンです。

Patterns of Enterprise Application Architecture (Addison-Wesley Signature Series (Fowler))

Patterns of Enterprise Application Architecture (Addison-Wesley Signature Series (Fowler))

この種のパターンで実際に多く使用されているのは XSLT です。ぼく自身も、XSLT でフロントエンドの HTML を描画する B2C のシステムを担当したことがあります。XML を返す API と XSLT を組み合わせて HTML を作成するやり方です。 昨日の内容と合わせると、Template View と Transform View、2 つの View が登場したことになりますが、これまでの IT では

  1. Template View に対しては多数の対応エディタが存在している
  2. Transform View に対しては、それほど対応エディタは多くなく、また、保守が難しくなる

というのが一般的です。 しかし、それでも Transform View が使用されているのには訳があります。

  1. XSLT は「変換」に焦点が置かれるため、ロジックを不用意に導入することが少なくなる
  2. Web サーバがなくとも View のテストが可能

PoEAA: Template View

Template View パターンは、静的 HTML と同じ形で HTML を記述した上で、動的に変更するようなところにのみマーカーを埋め込むパターンです。 静的なところは WYSIWYG のエディタで記述でき、動的な部分は実際にレンダリングする際に、マーカーがプログラムの実装と連携する形でコンテンツを描画します。

Patterns of Enterprise Application Architecture (Addison-Wesley Signature Series (Fowler))

Patterns of Enterprise Application Architecture (Addison-Wesley Signature Series (Fowler))

この「マーカー」というのにもいくつか種類がありますが、通常はこのマーカーを 1 から作るということはありません。これを如何に効率的に使うか、また、新しいマーカーを(プラットフォームが提供する形式に沿って)作るか、ということが求められます。 一般的な例で言うと、ASP/JSP/PHP などがあります。 これらの技術にも、条件分岐やループを記述する専用のマーカーが存在していますが、これらを導入する際は、ページの側にロジックを持たせないように注意しなければなりません。ページの側にロジックを持たせると、同種のロジックが個々のページに散らばる等、望ましくない結果を招きます。

Template View の強みは、(プログラマではなく)デザイナーがページ構造を見ながら記述が可能なことです。Template View パターンが広まっている今、これが実現できているかについては多説ありますが…。 逆に、その弱みは、

  1. 容易にロジックが書けてしまうため、その後の保守が極めて難解になり得ること
  2. Web サーバと密結合することが多いため、テストがしにくい。(この点では、Transform View の方がテスト容易性がある)

となります。

PoEAA: Front Controller

今日は Front Controller。 Front Controller は セキュリティや国際化など、リクエストに対する共通処理を一元的に行い、その後、リクエストをコマンドオブジェクトに引き渡すというパターンです。

Patterns of Enterprise Application Architecture (Addison-Wesley Signature Series (Fowler))

Patterns of Enterprise Application Architecture (Addison-Wesley Signature Series (Fowler))

Front Controller は以下の 2 つで構成されます。

  1. Web Handler: URL と Request からどのコマンドオブジェクトに処理を移譲するかを決める
  2. Command: リクエストに対するアクションを行う

Web Handler については、Interceptor として使うのが有用だとされています。そういえば、某フレームワークもそんな形でした。

Command については、Web Handler からリクエスト毎に生成されます。このため、Command をスレッドセーフに作る必要はありません。

没入したのは恐怖でした、BIOHAZARD 7

遅ればせながら、BIO HAZARD 7 をクリアしました。グロテスク ver. の方です。

BIO HAZARD 7 については「原点回帰」と言われていたのですが、その評判は確かでした。 初代の BIOHAZARD を想起される恐怖がそこかしこに満ちていて、ゆっくり開くドアの向こうに潜むであろう得体の知れないものを恐れながら、ただただ進まざるを得ないという何とも言えない感覚に支配されていて、ぼくはこのゲーム、今でも一人でプレイしたくありません。

初代の BIOHAZARD に回帰しながらも大きく違うのは、1 つは没入感だと思います。 初代 BIOHAZARD は固定カメラ視点で展開されていきますが、BIOHAZARD 7 ではいわゆる First Person View で物語が進んでいきます。

主人公であるイーサンが体験する視界はゲームをプレイする自分が体験する視界であり、イーサンが体験する未知は自分が体験する未知になります。 そして眼前に広がる世界は、PS1 を遥かに凌駕する精細さで目の前に展開されて、この不気味な廃屋に自分だけが取り残されたかのような、そんな感覚から逃げ出すことができませんでした。

また、イーサンは戦闘に関しては素人です。少ない武器、乏しい体術、攻撃を除けることもままならない中、いつ何が出てくるか分からない状態での探索というのは過大なストレスを作ります。 きしむ足音、うなる風音の一つにも反応してしまうほどに空気を張り詰めさせ、人間が本能として持っているのであろう恐怖センサーを過敏にし、もう勘弁してくれという状況で襲いくる敵との対峙。退屈を全く感じさせないゲームでした。

誰だよ絶対 PS VR でプレイしようとか言ってたのはよー。これ VR でやったらトイレ行けなくなるだろうがよー。

やって損はしないゲームだと思います。しかし、グロテスク ver. と無印の価格差は一体なんだったのか。

tabindexで消耗していた話

tabindex には苦しまされることが多いです。

ぼくは以前に tabindex でタブオーダーが明示的に指定されているシステムのテストを行うことになったんですが、タブオーダーが設計上で明示的に指定されている以上タブオーダーがテスト観点に挙がり、打鍵でタブオーダーを一つ一つ確認するというテストを行いましたが、 タブオーダーのようなユーザインタラクションに関するテストについては「どのようにそのテスト実施者が行ったテスト実施結果の正しさを確認するのか」という問題があり、たいへんつらいものでした。

今日のエントリでは、そもそも論として、tabindex の機能、そして、その使い方のベストプラクティスは何なのかを考えてみます。

tabindex とは

tabindex というのは、HTML 仕様で定義されている属性で、

  1. tabindex 属性を付与された要素がフォーカスを持てることを示す
  2. (TAB キー等で)フォーカスを移していく操作によって、フォーカスを移動できるかを示す
  3. フォーカスの移動順(タブオーダー順)を示す

といったことを可能にします。最後に示した フォーカスの移動順 というのは、実は tabindex 属性のオプション機能でしかありません。

tabindex が付与されていないタグに対しては、ブラウザはデフォルトの動作を適用します。 具体的には、<a><audio><button><img><input> といったタグ (これらを Interactive Content と呼びます) に対しては、デフォルトでフォーカスが当たります。 逆に言えば、<span><div> といった Interactive Content ではないタグに対してフォーカスを当てるためには、明示的な tabindex 属性の付与が必要です。つまり、tabindex 属性はタグを Interactive Content に変える、といっても良いかもしれません。

tabindex の使い方

tabindex については、大きく分けて 3 つの使い方があります。

  1. tabindex="0" を指定する
    • 指定されたタグは、ブラウザ上でフォーカスを受けることが可能になります。"0" が指定された場合、そのフォーカス順 (タブオーダー) はブラウザが決めます。ブラウザは通常、DOM 上での出現順にタブオーダーを割り当てます。
  2. tabindex="-1" 等、負の数を指定する
    • 指定されたタグは、フォーカスを当てることは可能になりますが、タブオーダーの中には含まれません(タブを押すだけでは、フォーカスを与えられません)。フォーカスを当てるためには、JavaScript 等で明示する必要があります。
  3. tabindex="1" 等、正の数を指定する
    • 指定されたタグは、ブラウザ上でフォーカスを受けることが可能になります。また、タブオーダー順で考慮されます。

最後の「タブオーダー順で考慮される」っていうのはちょっと複雑なんですが、大まかに言うと、

  1. tabindex が指定されている要素の方が、指定されていない要素よりも前
  2. tabindex の値が正の数の場合、tabindex が 0、または負の数の要素よりも前
  3. あとは大体、tabindex の値順

っていう感じになります。

ベストプラクティス

長々と書いてしまいましたが、tabindex については、

  • 使うなら負の数指定、あるいは 0 指定
  • 正の数は指定するな

が推奨になります。 これは、W3C の HTML 仕様ARIA のベストプラクティス にも記載があります。

W3C HTML 仕様

Using a positive value for tabindex to specify the element’s position in the sequential focus navigation order interacts with the order of all focusable elements. It is error-prone, and therefore not recommended. Authors should generally leave elements to appear in their default order.

ARIA-Practices

When using roving tabindex to manage focus in a composite UI component, the element that is to be included in the tab sequence has tabindex of “0” and all other focusable elements contained in the composite have tabindex of “-1”.

正の値を指定しないのは、タブオーダーの指定がミスしやすいからです。

通常のタブの遷移は、いわゆる Z 型、左から右、上から下に遷移するというのが自然なタブオーダーとされていて、普通に HTML/CSS を書くと、このあたりはブラウザが標準で行ってくれます。 しかし、tabindex 属性で正の値を指定する場合、これらのタブオーダー遷移が崩れやすくなります。要素の追加、削除に伴なって、tabindex の値を正しく保つのは並大抵のことではありません。

この保守コストを支払ってでも、タブオーダーを操作したいケースというのはそうそうあるものではないと思うので、tabindex を使って明示的にタブオーダーを操作する必要があるのかは、慎重に考えた方が良いところだと思います。