Golangで関数をグローバル変数に代入してテスト時にスタブする

こんにちは、小野マトペです。タイトル全部です。

昨日、現在時刻によって条件分岐するロジックを含むGoプログラムを書いていて、どうテストするか困ったのですが、うまい(と思う)やり方を思いついたのでここに書いておきます。

書いていたのはまあだいたいこういうソースコードです。

// main.go
package main

import (
        "fmt"
        "time"
)

func main() {
        fmt.Println(Greet("マトペ"))
}

func Greet(n string) string {
        t := time.Now()
        if 6 <= t.Hour() && t.Hour() <= 18 {
                return fmt.Sprintf("こんにちは%sさん。今は%d時ですよ!", n, t.Hour())
        } else {
                return fmt.Sprintf("こんばんは%sさん。今は%d時ですよ!", n, t.Hour())
        }
}
go run main.go
こんにちはマトペさん。今は12時ですよ!

で、このGreet関数をテストしたいんですが、このままでは夜の挨拶を出すのに18時まで待つ必要があります。それでは困るのでtime.Nowの部分をなんとかちょろまかしていつでも全ての条件をテスト出来るようしないといけません。いわゆるスタブ化ですね。

すぐに思い浮かぶのは、Greet関数を下のようにDIっぽく依存性である現在時刻を引数から受け取るように変更する方法だと思います。

// main.go
func Greet(n string, t time.Time) string { // 引数に現在時刻を追加
        if 6 <= t.Hour() && t.Hour() <= 18 {
                return fmt.Sprintf("こんにちは%sさん。今は%d時ですよ!", n, t.Hour())
        } else {
                return fmt.Sprintf("こんばんは%sさん。今は%d時ですよ!", n, t.Hour())
        }
}

これなら任意の時間の場合のGreet関数のユニットテストが書けますが、現在時刻のテストのために本来不要な引数が増えるのは大げさで好ましくありません(好みの問題です)。それに、結局コールスタックのどこかでtime.Now()を呼ぶ必要があり、その呼び出しポイントを含むテストはスタブ化できません。

ここはやはりtime.Now関数をテスト実行時だけ動的に上書きしたいところですが、Golangはあまりスタブ化に優しい言語ではなく、例えばtimeパッケージをテスト時に動的に他のパッケージに差し替えるとかはできません。これが自作パッケージで提供している構造体かなにかであれば、interfaceでもなんでも使ってスタバブルにすればいいと思いますが…。

(参考:go - Is there an easy way to stub out time.Now() globally in golang during test? - Stack Overflow

で、どうしたかというと、タイトル通りですがmain.goでtime.Nowの関数ポインタをグローバル変数に代入しておき、テスト時にmain_test.goで任意の時刻を返す関数にさしかえるという方法を使いました。

// main.go
package main

import (
        "fmt"
        "time"
)

var timeNowFunc = time.Now // 関数をグローバル変数に代入

func main() {
        fmt.Println(Greet("マトペ"))
}

func Greet(n string) string {
        t := timeNowFunc() // グローバル変数でtime.Now関数を呼び出し
        if 6 <= t.Hour() && t.Hour() <= 18 {
                return fmt.Sprintf("こんにちは%sさん。今は%d時ですよ!", n, t.Hour())
        } else {
                return fmt.Sprintf("こんばんは%sさん。今は%d時ですよ!", n, t.Hour())
        }
}

Goは関数が一級市民なのでシグネチャさえ合致すれば変数に代入できます。この場合、timeNowFunc変数は型推論func() time.Time型として宣言され、time.Now関数で初期化されます。で、これを次のようにテストします。

// main_test.go
package main

import (
        "testing"
        "time"
)

const timeformat = "2006-01-02 15:04:06" // timeのフォーマット指定文字列

// timeNowFuncグローバル変数の関数を入れ替える関数
func setNow(t time.Time) {
        timeNowFunc = func() time.Time { return t }
}

func TestMain(t *testing.T) {

        evening, _ := time.Parse(timeformat, "2014-08-14 14:10:00")
        night, _ := time.Parse(timeformat, "2014-08-14 22:30:00")

        // 昼のテスト
        setNow(evening)  // スタブ!timeNowFuncに昼時刻を返す関数をセット
        if ret := Greet("まとぺ"); ret != "こんにちはまとぺさん。今は14時ですよ!" {
                t.Errorf("Greet Fails. Got [%s]\n", ret)
        }

        // 夜のテスト
        setNow(night)  // スタブ!timeNowFuncに昼時刻を返す関数をセット
        if ret := Greet("まとぺ"); ret != "こんばんはまとぺさん。今は22時ですよ!" {
                t.Errorf("Greet Fails. Got [%s]\n", ret)
        }
}
$ go test
PASS
ok      _/Users/matope/dev/go/mock     0.006s

テスト内で宣言しているsetNow(time.Time)関数は、グローバル変数のtimeNowFuncに、任意の時刻を返す関数を代入する関数です。これでテストごとにtimeNowFuncの返す時刻をコントロールできるので、任意の時刻のテストが書けます。しかも面倒なスタブ用クラス実装や不要な引数の引き回しなしで、テスト時にのみ作用するスタブです。

グローバル変数と聞いてつらくなる人もいるかもしれませんが、Goのグローバル変数は実際にはパッケージスコープで、頭文字を小文字にしておけばパッケージの外からアクセスできないので、比較的クリーンです。Goはテストがコードと同じパッケージ空間で実行できるので、テストコードに対してのみExportされたパラメータとして扱えます。

この、テスト時にスタブしたい関数の呼び出しをグローバル変数でプロキシしておいてtest側の初期化で差し替える方法は、同一パッケージ内であればモック化のための追加の記述量が少なく、データベースのテストなど、いろいろ応用が利きそうだなと思いました。複数パッケージにまたがるtime.Now()を一括で差し替えたいような場合は、スタブプロキシ用のパッケージを作ってそこのExported(大文字から始まる)グローバル変数にセットしてやればよさそうですね。

ちなみに豆ですが、Grouped Globalを使えばこんな感じでちょっとかっこよくグローバル変数を呼べます。もはや紛らわしいような気もしますが。

// main.go
package main

import (
        "fmt"
        "time"
)

var _time = struct {
        Now   func() time.Time
        Since func(time.Time) time.Duration
}{
        time.Now,
        time.Since,
}

func main() {
        fmt.Println(_time.Now()) // ← ちょっとかっこいい
}

追記:同じやり方を解説してる記事 があったのでこっち読んだ方がよさそう

Go Concurrency Patterns: Context[翻訳]

7月29日付けのgolang.orgブログエントリーで context というパッケージが紹介されました。

Go Concurrency Patterns: Context - The Go Blog

参考: Go言語のcontextパッケージについてのやりとり - ワザノバ | wazanova

今現在、業務でGo言語を使ったWebサービスを書いているのですが、contextはリクエストのキャンセルとタイムアウトとリクエストスコープの変数を扱う、Google社内で利用が標準化されているパッケージだという事なので、エントリーを翻訳しました。

以下和訳。

Introduction

Goサーバーでは、入ってくる各リクエストは専用のgoroutineで処理されます。リクエストハンドラは、データベースやRPCサーバーにアクセスするためにしばしば追加のゴルーチンを開始します。リクエスト上のゴルーチンは、一般的にエンドユーザーのアイデンティティ、認証トークン、リクエストのデッドラインなどのリクエスト特有の情報にアクセスする必要があります。リクエストがキャンセルされるかタイムアウトした場合、システムが資源を再利用できるようにするため、そのリクエスト上で動作していた全てのゴルーチンは即座に終了すべきです。

Googleで私たちは、リクエストスコープの値、キャンセレーションシグナル、そしてAPI境界をまたぐデッドラインを、リクエスト処理に関わる全てのゴルーチンに渡す事を簡単にするためのcontextパッケージを開発しました。パッケージは code.google.com/p/go.net/contextで公開されています。この記事では、パッケージの使い方と完全な動作例を紹介します。

Context

contextパッケージの核となるのはContext型です:

// A Context carries a deadline, cancelation signal, and request-scoped values
// across API boundaries. Its methods are safe for simultaneous use by multiple
// goroutines.
type Context interface {
    // Done returns a channel that is closed when this Context is canceled
    // or times out.
    Done() <-chan struct{}

    // Err indicates why this context was canceled, after the Done channel
    // is closed.
    Err() error

    // Deadline returns the time when this Context will be canceled, if any.
    Deadline() (deadline time.Time, ok bool)

    // Value returns the value associated with key or nil if none.
    Value(key interface{}) interface{}
}

(この記述は要約です。godocが信頼できます)

DoneメソッドはContextを実行中の関数へのキャンセレーションシグナルとして振る舞うチャンネルを返します:そのチャンネルがクローズされたら、関数は自分たちの仕事を放棄してreturnするべきです。ErrメソッドはそのContextがキャンセルされた理由を示すerrorを返します。Pipelines and CancelationでDoneチャンネルイディオムについてより詳しく議論しています。

Contextには、Doneチャンネルがreceive-onlyであることと同じ理由によりCancelメソッドがありません:キャンセレーションシグナルを受信している関数は、通常そのシグナルを送信する関数と同じではないからです。特に、親オペレーションがサブオペレーションのためにゴルーチンを開始したとき、それらのサブオペレーションは親をキャンセルできるべきではありません。そのかわりに、WithCancel関数(後述)は新しいContext値をキャンセルする方法を提供します。

Contextは複数ゴルーチンによる同時利用に対して安全です。コードは単一のContextをいくつのゴルーチンにでも渡せますし、それら全てにシグナルしてそのContextをキャンセルする事も出来ます。

Deadlineメソッドにより、関数は自分が仕事を開始するべきかどうか判断することができます;もし残り時間がほとんどなければ、全くの無駄になるかもしれません。コードはI/Oオペレーションのタイムアウトを設定するのに使う事も出来ます。

Valueにより、Contextでリクエストスコープなデータを運ぶ事が出来ます。そのデータは複数ゴルーチンによる同時利用に対して安全である必要があります。

派生コンテキスト

contextパッケージは既存のContextから新しいContextを派生するための関数を提供します。これらの値はツリーを形成します; あるContextがキャンセルされたとき、そこから派生している全てのContextもまたキャンセルされます。

Backgroundは全てのContextツリーのルートです;決してcancelされることはありません。

// Backgroundは空のContextを返します。これは決してキャンセルされず、デッドラインを持たず、値も持ちません。
// Backgroundは一般的にmain,init,そしてtests内で、やってくるリクエストのトップレベルコンテキストとして使われます。
func Background() Context

WithCancelとWithTimeoutは、親Contextがキャンセルされるよりも早くキャンセルできる派生Contextの値を返します。到着したリクエストに関連づけられたContextは、一般的にはリクエストハンドラがreturnした時にキャンセルされます。WithCancelは複数レプリカ使用時に冗長なリクエストをキャンセルするのにも便利です。WithTimeoutは、バックエンドサーバーへのリクエストにデッドラインを設定するのに便利です:

// WithCancel は、parent.Doneがクローズされるかcancelが呼び出されると
// Doneチャンネルがクローズされるようなparentのコピーを返します
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

// CancelFuncはContextをキャンセルします
type CancelFunc func()

// WithTimeoutはparent.Doneがクローズされるかcancelが呼び出されるかtimeoutが
// 経過するとDoneチャンネルがクローズされるようなparentのコピーを返します。
// 新しいContextのDeadlineはnow+timeoutと、もしあればparentのdeadlineのうちの近い方です。
// もしtimerがまだ動いていれば、cancel関数はそのリソースをリリースします。
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

WithValueはrequest-scopedな値をContextに関連づける方法を提供します

// WithValue はValueメソッドがkeyに対するvalを返すようなparentのコピーを返します。
func WithValue(parent Context, key interface{}, val interface{}) Context

contextパッケージの使い方を見る一番の方法は、動いているサンプルを見る事です。

Example: Google Web Search

この例は、/search?q=golang&timeout=1s のようなURLを、クエリ"golang"をGoogle Web Search APIにフォワードして結果を表示するという風にハンドルするHTTPサーバーです。timeoutパラメーターはサーバーにこの時間が経過した後にリクエストをキャンセルするように指示します。このコードは三つの部分に分かれています:

  • server はmain関数と/search のハンドラを提供します
  • userip はユーザーIPアドレスをリクエストから取得してContextに関連づける関数を提供します
  • google はクエリをGoogleに送信するSearch関数を提供します

The server program

serverプログラムは/search?q=golang のようなリクエストに対して最初のいくつかのgolangGoogle検索結果を返します。ハンドラはctxと呼ばれる初期Contextを作成し、ハンドラがreturnした時にキャンセルされるように準備します。リクエストがtimeout URLパラメータを含んでいたら、Contextはtimeout経過後に自動的にキャンセルされます:

func handleSearch(w http.ResponseWriter, req *http.Request) {
    // ctxはこのハンドラのContextです。cancleを呼ぶと、このハンドラ
    // が開始したリクエストへのキャンセレーションシグナルである
    // ctx.Doneチャンネルがクローズします
    var (
        ctx    context.Context
        cancel context.CancelFunc
    )
    timeout, err := time.ParseDuration(req.FormValue("timeout"))
    if err == nil {
        // リクエストはタイムアウトを持つので、timeoutがexpireしたら
        // 自動的にキャンセルされるcontextを作ります
        ctx, cancel = context.WithTimeout(context.Background(), timeout)
    } else {
        ctx, cancel = context.WithCancel(context.Background())
    }
    defer cancel() // handleSearchがリターンしたらctxをキャンセル

ハンドラはリクエストからクエリを抜き出し、useripパッケージを呼び出す事でクライアントのIPアドレスを取得します。クライアントのIPアドレスはバックエンドリクエストに必要なので、handleSearchはそれをctxにくっつけます:

    // Check the search query.
    query := req.FormValue("q")
    if query == "" {
        http.Error(w, "no query", http.StatusBadRequest)
        return
    }

    // Store the user IP in ctx for use by code in other packages.
    userIP, err := userip.FromRequest(req)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    ctx = userip.NewContext(ctx, userIP)

ハンドラはctxとqueryでgoogle.Searchを呼び出します:

    // Run the Google search and print the results.
    start := time.Now()
    results, err := google.Search(ctx, query)
    elapsed := time.Since(start)

検索が成功したら、ハンドラは結果を表示します:

    if err := resultsTemplate.Execute(w, struct {
        Results          google.Results
        Timeout, Elapsed time.Duration
    }{
        Results: results,
        Timeout: timeout,
        Elapsed: elapsed,
    }); err != nil {
        log.Print(err)
        return
    }

Package userip

useripパッケージはリクエストからユーザーIPアドレスを抜き出してContextに関連づける関数を提供します。Contextはkey-valueマッピングを提供しますが、そのkeyとvalueはどちらもinterface{}型です。Key型は等価をサポートし、valueは複数ゴルーチンからの同時利用に対して安全である必要があります。useripのようなパッケージはこのマッピングの詳細を隠蔽し、特定のContext値への強く型付けされたアクセスを提供します。

キー衝突を避けるために、useripはexportされない型のkeyを定義してcontext keyとしてこの型の値を使います。

// The key type is unexported to prevent collisions with context keys defined in
// other packages.
type key int

// userIPkey is the context key for the user IP address.  Its value of zero is
// arbitrary.  If this package defined other context keys, they would have
// different integer values.
const userIPKey key = 0

FromRequestはuserIP値をhttp.Requestから抜き出します

func FromRequest(req *http.Request) (net.IP, error) {
    s := strings.SplitN(req.RemoteAddr, ":", 2)
    userIP := net.ParseIP(s[0])
    if userIP == nil {
        return nil, fmt.Errorf("userip: %q is not IP:port", req.RemoteAddr)
    }

NewContextは与えられたuserIP値を運ぶ新しいContextを返します:

func NewContext(ctx context.Context, userIP net.IP) context.Context {
    return context.WithValue(ctx, userIPKey, userIP)
}

FromContextはuserIPをContextから抜き出します:

func FromContext(ctx context.Context) (net.IP, bool) {
    // ctx.Value returns nil if ctx has no value for the key;
    // the net.IP type assertion returns ok=false for nil.
    userIP, ok := ctx.Value(userIPKey).(net.IP)
    return userIP, ok
}

Package Google

google.Search関数はGoogle Web Search APIへのHTTPリクエストを作ってJSONエンコードされた結果をパースします。これはContextパラメータctxを受け入れ、もしリクエストを飛ばしている間にctx.Doneがクローズされたらすぐにリターンします。

Google Web Search APIリクエストはサーチクエリとユーザーIPをクエリパラメータに含みます:

func Search(ctx context.Context, query string) (Results, error) {
    // Prepare the Google Search API request.
    req, err := http.NewRequest("GET", "https://ajax.googleapis.com/ajax/services/search/web?v=1.0", nil)
    if err != nil {
        return nil, err
    }
    q := req.URL.Query()
    q.Set("q", query)

    // If ctx is carrying the user IP address, forward it to the server.
    // Google APIs use the user IP to distinguish server-initiated requests
    // from end-user requests.
    if userIP, ok := userip.FromContext(ctx); ok {
        q.Set("userip", userIP.String())
    }
    req.URL.RawQuery = q.Encode()

Searchはヘルパー関数httpDoを使ってHTTPリクエストを発行し、もしリクエストかレスポンスが処理中にctx.Doneがクローズされたらキャンセルします。SearchはHTTPレスポンスを処理するクロージャをhttpDoハンドルに渡します:

    var results Results
    err = httpDo(ctx, req, func(resp *http.Response, err error) error {
        if err != nil {
            return err
        }
        defer resp.Body.Close()

        // Parse the JSON search result.
        // https://developers.google.com/web-search/docs/#fonje
        var data struct {
            ResponseData struct {
                Results []struct {
                    TitleNoFormatting string
                    URL               string
                }
            }
        }
        if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
            return err
        }
        for _, res := range data.ResponseData.Results {
            results = append(results, Result{Title: res.TitleNoFormatting, URL: res.URL})
        }
        return nil
    })
    // httpDo waits for the closure we provided to return, so it's safe to
    // read results here.
    return results, err

httpDo関数は新しいゴルーチンの中でHTTPリクエストを実行してレスポンスを処理します。もしゴルーチンがexitする前にctx.Doneがクローズしたらキャンセルします。

func httpDo(ctx context.Context, req *http.Request, f func(*http.Response, error) error) error {
    // Run the HTTP request in a goroutine and pass the response to f.
    tr := &http.Transport{}
    client := &http.Client{Transport: tr}
    c := make(chan error, 1)
    go func() { c <- f(client.Do(req)) }()
    select {
    case <-ctx.Done():
        tr.CancelRequest(req)
        <-c // Wait for f to return.
        return ctx.Err()
    case err := <-c:
        return err
    }
}

コードをContextに適合する

多くのサーバーフレームワークがリクエストスコープな値を運ぶためのパッケージと型を提供しています。私たちは、既存のフレームワークとContextパラメータを期待するコードとの間をブリッジする新しいContextインターフェイスの実装を定義する事が出来ます。

例えば、Gorillaのgithub.com/gorilla/contextパッケージは、HTTPリクエストからkey-valueペアへのマップを提供する事でハンドラにデータをリクエストに関連づける事を許します。gorilla.goにおいて、私たちはGorillaパッケージ内で特定のHTTPリクエストに関連づけられた値を返すValueメソッドを持つContext実装を提供します。

他のパッケージは、Contextに似たキャンセレーションサポートをサポートしてきました。例えば、TombはDyingチャンネルをcloseすることでキャンセルをシグナルするKillメソッドを提供します。Tomはまたこれらのゴルーチンが終了するのを待つ、sync.WaitGroupに似たメソッドも提供します。tomb.goでは、私たちは親のContextがキャンセルされるか与えられたTombがkillされるかの場合にキャンセルされるContext実装を提供します。

結論

Googleでは、私たちはGoプログラマーにContextパラメーターをリクエストの到着から返却までの間に呼ばれる全ての関数の第一引数にContextパラメーターを渡す事を要求しています。これは、たくさんの異なるチームで開発されたGoのコードに良好な相互運用性をもたらします。タイムアウトとキャンセレーションのシンプルな制御を導入し、セキュリティクレデンシャルのようなクリティカルな値が適切にGoプログラム内を移動する事を確かなものにします。

Context上に構築したいサーバーフレームワークは、それらのパッケージとContextパラメータを期待するものの間をブリッジするContext実装を提供するべきです。そうすればそれらのクライアントライブラリは呼び出しコードからContextを受け入れることでしょう。リクエストスコープなデータとキャンセレーションのための共通インターフェイスを確立する事により、Contextはパッケージ開発者がスケーラブルなサービスを作るためのコードを共有するのを簡単にするのです。

By Sameer Ajmani

Patterns of Enterprise Application Architecture読書メモ(データソース編)

Martin FowlerのPatterns of Enterprise Application ArchitectureすなわちPoEAA、何年か前に原著をKindleで買って読もうとして序盤で挫折していた。最近またデータソースアーキテクチャについて翻訳版と原著を合わせて読んでいるので、整理のためメモを残しておく。

パターン

本書で紹介されるパターンはアプリケーションアーキテクチャから通貨の値オブジェクトまで、粒度もレイヤーも様々。今回はデータソースアーキテクチャパターンを理解するのに必要な箇所だけ調べた。

ドメインロジックパターン

  • トランザクションスクリプト
    • ほとんど構造化されてない書き下しなロジックのこと。ちょろいスクリプトならこれでもいいけど、ちょっと複雑になるとすぐに破綻する。
  • ドメインモデル
    • 業務上の概念をオブジェクト化して、それらを組み合わせて複雑なロジックを構成する。データマッパーサービスレイヤーパターンと組み合わせると良い。
  • テーブルモジュール
    • 上記二つの中間。RDBの1テーブルに対応するクラスでドメインロジックを構成するため、あまり複雑なアプリケーションには適合できないが、RDBなどとの相性は良い。
  • サービスレイヤ -

データソースアーキテクチャパターン

データベース操作のSQLとアプリケーションロジックの分離の仕方、ModelがControllerとデータベースの変換をどう扱うのかという話題。

  • テーブルデータゲートウェイ
  • 行データゲートウェイ
  • アクティブレコード
  • データマッパー

の4パターンが紹介されている。

テーブルデーゲートウェイ

P of EAA: Table Data Gateway

  • ひとつのデータベーステーブルがひとつのオブジェクトインスタンスになるゲートウェイ。テーブルへの操作(find,update,insert,delete)を備える。
  • Record Set(ジェネリックなデータ構造)やTable Module パターンとの併用に適している。
  • データベースの戻り値のRecord Setを返してもいいし、ドメインオブジェクトにマッピングして返してもいい。Record Setを返してデータ変換オブジェクトを用いれば、ユーザーが好きな方を選べて良いのではないか
  • ドメインモデルを使っている場合、テーブルデータゲートウェイドメインオブジェクトを返せるが、双方向の依存性が発生するのであんまり良くない
  • テーブルモジュール

// この例ではFindメソッドの戻り値にジェネリックなRecordSetを返却しているが、上の議論のように別にPersonオブジェクトを返しても問題はない。
class PersonGateway {
  Find(id): RecordSet
  FindWithLastNames(String): RecordSet
  Update(id, lastname,firstname, numberOfDependents)
  Insert(lastname, firstname, numberObDependents)
  Delete(id)
}
...
RecordSet r = personGateway.Find(1)
printf( r.Column("lastName") )
personGateway.Insert(2,"John","Doe",3)
personGateway.Update(2,"John","Doe",4)
personGateway.Delete(2)

行データゲートウェイ

P of EAA: Row Data Gateway

  • クエリ結果の行ひとつひとつがオブジェクトのインスタンスになる。ドメインオブジェクトがp.update()とか振る舞うのでオブジェクト指向感がある。
  • でもインメモリのオブジェクトにデータベースのアクセスコードが仕込まれる必要があるので、データベースとオブジェクトが密に結合してテストがやりにくかったりするデメリットがある。
  • トランザクションスクリプトに最適。
  • Findの動作をどこに定義するのかが問題になりがち
  • 行データゲートウェイに含まれるのはデータベースアクセスロジックだけ。ドメインロジックが含まれたらアクティブレコードパターンに進化する。

//static Findメソッドは別クラス(e.g. PersonFinder)に実装してもよい

class PersonGateway{
  lastname
  firstname
  numberOfDependents

  static Find(id): PersonGateway
  static FindWithLastName(String): PersonGateway[]

  insert()
  update()
  delete()
}
...
p1 = new PersonGateway()
p1.firstname = "John"
p1.lastname = "Doe"
p1.insert()

p = PersonGateway.Find(2)
p.numberOfDependents = 10
p.update()
p.delete()

アクティブレコード

P of EAA: Active Record

  • 行データゲートウェイのクラスにドメインロジックを追加したようなもの。
  • 行データゲートウェイテーブルデータゲートウェイドメインモデルとあまり相性が良くない。ドメインモデルであればアクティブレコードが向いてる(複雑なものでなければ)。
  • アクティブレコードクラスは以下のメソッドを持つ
    • SQLの結果行からアクティブレコードインスタンスを構築
    • 新しいインスタンスを構築
    • 静的なfindメソッドを使用してアクティブレコードオブジェクトを返却
    • データベースにアクティブレコードデータを挿入
    • フィールド取得、設定
    • ビジネスロジックの一部(バリデーションとか)
  • フィールドの型はテーブル内の各列と一致しなければならない。get/set時に他の効率のよい型への変換が認められる。
  • findメソッドは多くの場合静的に実装されるが、別に独立したクラスに分離してもよい。(テストもそのほうがしやすい)

class PersonGateway{
  lastname
  firstname
  numberOfDependents

  static find(id): PersonGateway
  static findWithLastName(String): PersonGateway[]

  insert()
  update()
  delete()

  getExemption()
  isFlaggedForAudit()
  getTaxableEarnings()  
}

データマッパー

P of EAA: Data Mapper

オブジェクトとデータベーステーブルは所詮別物(継承、コレクション、Joinの有無など)なので、1:1の対応でハンドリングするには限界がある。両者を変換するマッパがあれば複雑なドメインロジックと複雑なデータベーステーブルをハンドル出来るよ、的なパターン。

  • データマッパードメインオブジェクトとデータベーステーブルの間のデータの受け渡し(ロード/ストア)を責務とするソフトウェアレイヤ。ドメインモデルとデータベースを完全に分離し独立を保つ。ドメインオブジェクトはデータベースについて一切関知しなくてよくなる。
  • 全てのDB通信とマッピングデータマッパーが担うため、DBスキーマドメインオブジェクトそれぞれ独立して設計・開発が進められるメリット。
  • データマッパーがゲートウェイをラップすることはあり得る
  • 本書で紹介される「オブジェクトリレーショナルマッピングパターン」は、全てデータマッパーでオブジェクトとテーブルを接合させるためのストラテジ

class PersonMapper{
  insert(Person)
  update(Person)
  delete(Person)
}
class Person{
  lastName
  firstName
  numberOfDependents

  getExemption()
  isFlaggedForAudit()
  getTaxableEarnings()
}

データソースレイヤーアーキテクチャパターンの選び方

この本によると

  • ドメインモデルを使わない場合→ゲートウェイ系パターンでもなんとかなる
  • シンプルなドメインモデルを使う場合→アクティブレコードがよい
  • 複雑なドメインロジックを使う場合→データマッパーじゃないときつい

関連する基本パターン ゲートウェイマッパーの違い

ゲートウェイとマッパーはともに、ドメインオブジェクトと外部サービス(データベースなど)のインターフェイスとなる似たパターンだが、依存の方向性が逆方向になる。

ゲートウェイ

  • 外部サービスをラッピングして他のドメインオブジェクトが簡単に使えるようにする。
  • テストのためのサービススタブや、将来的に他のサービスに切り替えるためのポイントにもなる。間接化によって柔軟性を提供する。
  • PofEAA's Wiki - Gateway

マッパー

  • サブシステム間の通信を取り持つ分離レイヤー。サブシステム間の相互作用をハンドリングする。
  • PofEAA's Wiki - Mapper
    • 図のように、 マッパーが全ての関連サブシステムに依存している。サブシステムからマッパーへの依存はない。

その他のパターン

(今回は調べてない)

  • オブジェクトリレーショナル振る舞いパターン
  • オブジェクトリレーショナル構造パターン
  • オブジェクトリレーショナルメタデータマッピングパターン
  • Webプレゼンテーションパターン
  • 分散パターン
  • オフライン平行性パターン
  • セッションステートパターン
  • 基本パターン

エンタープライズ アプリケーションアーキテクチャパターン (Object Oriented Selection)

エンタープライズ アプリケーションアーキテクチャパターン (Object Oriented Selection)

Patterns of Enterprise Application Architecture

Patterns of Enterprise Application Architecture

Golangでの文字列・数値変換

覚えられなくて使うたびにググってしまうので、以後楽をするためにスニペットを記す。

パッケージ

strconvパッケージを使う。

文字列 → 数値変換(パース)

func Atoi(s string) (i int, err error)

文字列を10進数のint型にパースする。ParseInt(s, 10, 0)の省略形。

  var i int
  i, _ = strconv.Atoi("255")
  fmt.Println(i)  // => 255
func ParseBool(str string) (value bool, err error)

文字列をbool型にパースする。

  var b bool
  b, _ = strconv.ParseBool("true")
  fmt.Println(b) // => true

受け付ける値は1, t, T, TRUE, true, True, 0, f, F, FALSE, false, False

func ParseInt(s string, base int, bitSize int) (i int64, err error)

文字列を任意の基数(2進数〜36進数)・任意のビット長(8〜64bit)のIntにパースする。

  var i32, i64, ib16, ib0 int64
  i32, _ = strconv.ParseInt("255", 10, 32)
  i64, _ = strconv.ParseInt("255", 10, 64)
  ib16, _ = strconv.ParseInt("ff", 16, 16)
  ib0, _ = strconv.ParseInt("0xff", 0, 16)
  fmt.Println(i32, i64, ib16, ib0)   // => 255 255 255 255

baseは変換に用いる基数(2〜36)。0の場合、s文字列の書式から判断する(0x接頭詞がついていたら16進数など)。bitSizeは0,8,16,32,64(それぞれint, int8, int16, int32, and int64に該当)。どのbitSizeでも戻り値の型自体はint64だが、それぞれの型に値を変えずに変換できる。

func ParseUint(s string, base int, bitSize int) (n uint64, err error)

文字列を任意の基数(2進数〜36進数)・任意のビット長(8〜64bit)のUintにパースする。ParseIntと同様。

  var ui uint64
  ui, _ = strconv.ParseUint("255", 10, 32)
  fmt.Println(ui) // => 255
func ParseFloat(s string, bitSize int) (f float64, err error)

文字列を任意のビット長(32,64bit)のUintにパースする。

bitSizeは32か64(float32とfloat64に相当)。戻り値の型自体はfloat64だが、それぞれの型に値を変えずに変換できる

  var f32, f64 float64
  f32, _ = strconv.ParseFloat("3.14159265359", 32)
  f64, _ = strconv.ParseFloat("3.14159265359", 64)
  fmt.Println(f32, f64) // => 3.1415927410125732 3.14159265359
ParseXXX関数のエラーハンドリング

ParseXXX関数のerrorの具象型はNumErrorで、(NumError).Errで範囲エラーか書式エラーかを判別できる。

  _, e = strconv.ParseInt("Bad number", 10, 32)
  if e != nil {
    if enum, ok := e.(*strconv.NumError); ok {
      switch enum.Err {
      case strconv.ErrRange:
        log.Fatal("Bad Range Error")
      case strconv.ErrSyntax:
        log.Fatal("Syntax Error")
      }
    }
  }

数値 → 文字列(フォーマット)

func Itoa(i int) string

func FormatBool(b bool) string

func FormatInt(i int64, base int) string

func FormatUint(i uint64, base int) string

各数値型を文字列にフォーマットする。

  fmt.Println(strconv.Itoa(255)) // => 255
  fmt.Println(strconv.FormatBool(true)) // => true
  fmt.Println(strconv.FormatInt(255, 10)) // => 255
func FormatFloat(f float64, fmt byte, prec, bitSize int) string

float64型を文字列にフォーマットする。

  fmt.Println(strconv.FormatFloat(1234.56789, 'b', 4, 64)) // => 5429687001335527p-42
  fmt.Println(strconv.FormatFloat(1234.56789, 'e', 4, 64)) // => 1.2346e+03
  fmt.Println(strconv.FormatFloat(1234.56789, 'E', 4, 64)) // => 1.2346E+03
  fmt.Println(strconv.FormatFloat(1234.56789, 'f', 4, 64)) // => 1234.5679
  fmt.Println(strconv.FormatFloat(1234.56789, 'g', 4, 64)) // => 1235
  fmt.Println(strconv.FormatFloat(1234.56789, 'G', 4, 64)) // => 1235
  fmt.Println(strconv.FormatFloat(1234.56789, 'G', 3, 64)) // => 1.23E+03

fmtは出力書式で'b','e','E','f','g','G'のいずれか。

precは桁数。eEFのとき小数点以下桁数。gGのとき全体の桁数。(g,Gはprecがfの少数点以上の桁数を満たしていればfまたはFになるらしい)

おまけ

たしかに。strconv.Atoiとfmt.Sprintだけ覚えておけば日常生活では支障無いですね。

デモ

Go Playground

Go言語のコマンドライン引数の使い方(サブコマンド等)

go言語でのコマンドライン引数の使い方でやや混乱したのでまとめる。

  • まとめること
    • flagの仕様
    • flagでgit push -fのようにオプション引数より前に非オプションの引数を置く方法・サブコマンドの例
    • go-flagパッケージのサブコマンドを試す

flagの仕様

Golang標準のflagパッケージは以下のように使う。

package main

import (
  "flag"
  "fmt"
  "os"
)

func main() {
  // -hオプション用文言
  flag.Usage = func() {
    fmt.Fprintf(os.Stderr, `
Usage of %s:
   %s [OPTIONS] ARGS...
Options\n`, os.Args[0],os.Args[0])
    flag.PrintDefaults()
  }

  var (
    opt1 = flag.String("opt1", "default-value", "First string option")
    opt2 = flag.String("opt2", "default-value", "Second string option")
  )
  flag.Parse()
  fmt.Println("opt1:", *opt1)
  fmt.Println("opt2:", *opt2)
  fmt.Println("args:", flag.Args())
}
$ ./argtest -h
Usage of ./argtest:
   ./argtest [OPTIONS] ARGS...
Options
  -opt1="default-value": First string option
  -opt2="default-value": Second string option

$ ./argtest -opt1 aaa -opt2 bbb AAA BBB 
opt1: aaa
opt2: bbb
args: [AAA BBB]

flag.Parse()関数は、"-"で始まらない(非フラグな)コマンドライン引数に到達すると、未解釈の引数をflag.Args()に格納し、パースをストップしてしまう。

なので、例:./argtest -opt1 aaa -opt2 bbb FILE1 FILE2...のようにオプション群を非オプション引数より前に置くのなら簡単にパースできる

flagでコマンドラインオプションをコマンドライン引数の後ろに置きたい

では、例:./argtest FILE1 -opt1 aaa -opt2 bbbという風にオプションを引数の後ろには書けないのだろうか。たとえばgit push -fみたいなサブコマンドをやりたいときに必要になる。

そういうときは、FlagSet型を使う。

これは複数のFlag設定を切り替えられるようにする仕組みだけど、FlagSetのParse()メソッドはパースする範囲をスライスとして渡せるので、これを利用して先頭に非フラグの引数を置いたり、サブコマンドを実現したりできる。

  fs := flag.NewFlagSet(os.Args[0], flag.ExitOnError)
  var (
    opt1 = fs.String("opt1", "default-value", "First string option")
    opt2 = fs.String("opt2", "default-value", "Second string option")
  ) 

  arg1 := os.Args[1]
  fs.Parse(os.Args[2:])
  fmt.Println("arg1:", arg1)
  fmt.Println("opt1:", *opt1)
  fmt.Println("opt2:", *opt2)
  fmt.Println("args:", fs.Args())
$ ./argtest XXX -opt1 AAA -opt2 BBB YYY ZZZ
arg1: XXX
opt1: AAA
opt2: BBB
args: [YYY ZZZ]

サブコマンドの実装についてはVegetaが参考になった。各サブコマンド内で別々のフラグを設定している。See vegeta.

一応、こんなかんじになる。

subcommand.go

go-flagsを試してみる

調査の一環で非標準パッケージのgo-flagsを試してみたので感想

  • フラグ・非フラグの順序関係なくすべてパースできる。便利だけど、逆に位置を固定したいときにどうすればいいのか分からなかった。
  • サブコマンドをサポートしているようなので試してみた(下記)けど、コマンドがgo-flagsに依存してしまうしちょっと大げさだった
  • 使わなくていいかも。
package main

import (
  "fmt"
  "github.com/jessevdk/go-flags"
  "log"
  "os"
)

type Options struct {
  Option1 string     `short:"1" long:"opt1" description:"First string option"`
  Option2 string     `short:"2" long:"opt2" description:"Second string option"`
  Put     PutCommand `description:"Command to put something" command:"put" subcommands-optional:"true"`
  Get     GetCommand `description:"Command to put something" command:"get" subcommands-optional:"true"`
}

var opts Options
var parser = flags.NewParser(&opts, flags.Default)

type PutCommand struct {
  PutOption string `short:"p" long:"put-opt" description:"First string option"`
}
type GetCommand struct {
  GetOption string `short:"g" long:"get-opt" description:"First string option"`
}

func (x *PutCommand) Execute(args []string) error {
  fmt.Printf("Putting someting (opt=%v): %#v\n", x.PutOption, args)
  return nil 
}
func (x *GetCommand) Execute(args []string) error {
  fmt.Printf("Getting someting (opt=%v): %#v\n", x.GetOption, args)
  return nil 
}

func main() {
  if _, err := parser.Parse(); err != nil {
    os.Exit(1)
 }
}
$ ./command --opt1=AAA put XXX YYY ZZZ --put-opt=OPT
Putting someting (opt=OPT): []string{"XXX", "YYY", "ZZZ"}

$ ./command --opt1=AAA get XXX YYY ZZZ --get-opt=OPT 
Getting someting (opt=OPT): []string{"XXX", "YYY", "ZZZ"}

$ ./command --opt1=AAA
Please specify one command of: get or put

./command VVV --opt1=AAA get XXX YYY ZZZ --get-opt=OPT
Getting someting (opt=OPT): []string{"VVV", "XXX", "YYY", "ZZZ"} // <- argsにgetより前の引数が含まれている

参考

tus - Resumable File Uploads

Resumable File Uploads

tusというHTTP/1.1上で再開可能なファイルアップロードの規格を策定するプロジェクトを見つけた。2013年4月に始まった取り組みで、リファレンス実装が多い。提案されているプロトコルでのアップロードの流れをメモする(v0.2.1準拠)

初期化(File Creation)

空のPOSTリクエストでファイルを作成する。Entity-Length ヘッダはファイル全体の長さを表す。

リクエスト
POST /files HTTP/1.1
Host: tus.example.org
Content-Length: 0
Entity-Length: 100
レスポンス

新規に作成されたリソースのURIがLocationに乗って201が返ってくる。

HTTP/1.1 201 Created
Location: http://tus.example.org/files/24e533e02ec3bc40c387f1a0e460e216

アップロード

HEADリクエストで、どこまでアップロードされているかを取得する(Offsetヘッダ)

リクエスト
HEAD /files/24e533e02ec3bc40c387f1a0e460e216 HTTP/1.1
Host: tus.example.org
レスポンス
HTTP/1.1 200 Ok
Offset: 70
リクエスト

オフセット70バイトからPATCHメソッドでアップロードする

PATCH /files/24e533e02ec3bc40c387f1a0e460e216 HTTP/1.1
Host: tus.example.org
Content-Type: application/offset+octet-stream
Content-Length: 30
Offset: 70

[remaining 30 bytes]
レスポンス
HTTP/1.1 200 Ok

その他

チェックサム、並列チャンク、メタデータ、ストリームについてこれから定義予定らしい。

ファイル作成がPOSTで示されているけど、PUTでリソース指定したい時とか、リソースを更新したい時はどうするんだろうなと思った。暇があったらもうちょっと調べる。

(翻訳) Trelloは何が違うのか by Joel Spolsky

最近、rebuild.fm で紹介されていたタスク管理ツールのTrelloを試しに使ってみたんですが、個人的にかなりヒットでした。このTrello、Joel on Softwareで有名なJoel Spolsky氏のサービスだと知って驚いたんですが、氏のTrelloに関するブログエントリーが興味深かったので、著者の許諾を得て翻訳します。


原文:How Trello is different - Joel on Software

Trelloは何が違うのか(How Trello is different)

2012年1月6日 金曜日

つい数ヶ月前、私たちは非常にシンプルなWebベースのチーム・コラボレーション・システムのTrelloをローンチした。まだ始まったばかりの1.0状態だというのに、フィードバックは圧倒的に好感触で、採用の勢いは非常に力強い。

Fog Creekにとって、Trelloは新しい種類の開発プロジェクトだ。100%ホストされており、「インストール・ソフトウェア」バージョンのTrelloというものは決して存在しないだろう。このおかげで、私たちは開発プロセスを多くの面で現代化できた。私たちはTrelloに関わる いかなる部分にもVisual Basicコードが全く含まれていない ことをお知らせ致します。次はなんだ?空飛ぶ車か?

あなたが(もっぱらソフトウェア開発者向けに投入された過去製品と比較して)気づくであろう最も大きな違いは、Trelloが完全に水平的なプロダクトだということだ。

水平的とは、様々な職業の人々がそれを使えるということだ。ワープロやWebブラウザーは水平的だ。歯医者がドリルであなたを拷問にかけるために使うソフトウェアは垂直的だ。

垂直的ソフトウェアは成功させやすくお金を儲けやすいので、あなたの最初のスタートアップとしては良い選択だ。これには2つのキーとなる理由がある。

  • 顧客を見つけやすい。もしあなたが歯医者ソフトを作るのなら、あなたはどの集会に出かけて、どの雑誌に広告を打てばいいのか知っている。ただ歯医者を見つけさえすればいいのだ。
  • 利ざやが良い。あなたのユーザーは仕事のプロフェッショナルで、あなたが彼らの問題を解決できるのなら、彼らがあなたにお金を払うのは理にかなっている。

様々な職業の人にとって便利な水平的な製品を作るというのは、成功させるのは殆ど不可能だ。あなたは、開発コストを膨大なユーザー数で償却できる他の水平的プロダクトと競合しているので、あまり高額な料金を請求することはできない。ハイリスク・ハイリターンだ。ゼロから起業したスタートアップには向かないが、Fog Creekのように成熟し安定した起業の2つ目や3つ目の製品のアイデアとしては悪くない。

すまないが、1991年に戻って私がMicrosoft Excelチームにいた時の話をさせてくれ(ああ、確かにきみが当時まだ生まれていなかったのは認めるが、コンピューターは発明されていたんだ。分かったら僕の膝に飛び乗っててじっとしていてくれ)。

誰しも、Excelを財務モデリングアプリケーションだと考えていた。これは式やそのようなものを使って、計算モデルを作り出すために使われていた。例えば、"もし金利が来年0.00001%増加するなら、ラスベガスの住宅所有者の何パーセントが破産するか?"のような計算をするものだと思っておいてほしい。

1993年前後、私たちのうち二、三人が、人々がどのようにExcelを使っているかを知るために顧客のもとに出かけた。

私たちはそこで、高度に統制された巨大な公共事業の"今週の負傷者数"スプレッドシートを管理することだけが仕事だという男を発見した。

彼は週に一度、施設名と、その週に負傷者が0であったという意味の数字の0を含む、十の施設が列挙されたExcelスプレッドシートを開いた(負傷者が出たことは一度もなかった)。

彼は今日の日付をスプレッドシートの一番上に入力し、印刷し、三穴バインダーに閉じ、そしてそれが彼の、まさに仕事のすべてだった。少し悲しいことだった。彼は一日二度の昼休みを取っていた。私だってもしそれが私の仕事の全てだったらそうしただろう。

次の二週間、私たちは数十のExcelの顧客の元を訪れたが、Excelを実際に「計算」と呼べるもののために使っている人間はどこにもいなかった。彼らのほとんど全員は、表を作るのに便利だからExcelを使っていたのだ。

(無関係な注:私たちが見つけた数少ない「計算」をしていた顧客は銀行で、彼らは「デリバティブ」と呼ばれる爆破装置を考えだしていた。彼らは10年のうち9年は銀行家のボーナスを最大化し、10年に一度西洋文明に崩壊を引き起こしかけるためにExcelを使っていた。ブラック・スワンに関する何かだ。多分、浮動小数点の丸め誤差だろう)

 何の話をしてたんだっけ?そうそう、ほとんどの人々がExcelをリストを作るために使ってたということだ。突如として私たちは、Excelを時代遅れにせんとした、上等な未来的スプレッドシートであるLotus Improvがなぜ完全に失敗したのかを完全に理解した。あれは計算が大得意だったが、表の作成はひどく下手で、そしてみんなはExcelを計算ではなく表のために使っていたからだ。

ビンゴ!頭のなかで明かりが灯った。

素晴らしい水平的キラーアプリケーションとは、実際には着飾ったデータ構造に過ぎないのだ。

スプレッドシートは単にwhat-if分析をするための道具ではない。これらは特定のデータ構造を提供する。表だ。ほとんどのExcelユーザーは式を入力しない。彼らは、表が必要な時にExcelを使う。再計算などではなく、罫線こそがExcelの最重要機能だったのだ。

ワープロは単に本やレポートや手紙を書く道具ではない。これらは特定のデータ構造を提供する。自動的に折り返されてページに分割されるテキストの列だ。

パワーポイントは単に退屈な会議を作るためのツールではない。これは特定のデータ構造を提供する。フルスクリーン画像の配列だ。

Trelloを見て「お、カンバンボードじゃないか。アジャイルにソフトウェアを開発するためのものだろ」という人もいる。ああ、そのとおりだとも。でも結婚式の計画にも使えるし、休暇に出かける場所の候補を家族にシェアするのにも使えるし、求人の応募の経緯を記録するのにも使えるし、他の無数の用途にも使える。実際のところ、Trelloはあなたが人々のグループとともにリストのリストを管理したいと思う全ての場所で使えるのだ。

そのような種類のデータ構造を必要とするものは無数にあり、そしてTrello以前に素晴らしい「リストのリスト」アプリケーションは存在していなかった(アウトラインプロセッサはあったが、あれは私に言わせればUIデザインの袋小路だ。だからプログラマーには魅力的だが、一般人には役に立たない)。

いちどTrelloに足を踏み入れれば、あなたは全てのことにこれを使うだろう。私は常に約30個のTrelloボードを使い、それらをいままでみんなと使ってきた。年老いた両親から、ともに休暇を計画する人と、仕事上のすべてのチームと、私が関わっているほとんど全てのプロジェクトで。

だから、オーケー、これがTrelloの最初の大きな違いだ:垂直的ではなく、水平的であること。しかし他にもたくさんの違いがある。

継続的にデリバリーされている。 メジャーリリースとマイナーリリースがあるというよりも、私たちはほとんど単に、新しい機能を開発者のもとから顧客に押し出すだけだ。あなたが開発してテストして、しかし次のメジャーリリースを待っているがために届けられていない機能というのは、在庫になる。在庫は重荷だ。あなたが費やしたお金は、売上を上げることもなくどんどん無駄になってゆく。たしかに、100年前には私たちも「CD-ROM」と呼ばれるものを持っていて、そういう方法でソフトウェアを出荷していた。だから私たちは世界中の顧客にそれを押し付けるまで機能を一箇所に集めておく経済的な理由があった。だが、もはやそういう方法で仕事をする理由は存在しない。もちろんもう知っているだろう。ちょっと言ってみるだけだがー5分前にVisual Basicを使うのを止めたんだ。「すばらしい新世界」だ。

リリース前に徹底的なテストをしていない。 私たちはそれでもなんとかなると考えた。なぜならTrelloは無料で、顧客はより寛容だからだ。だが本当のことをいうと、本当の理由はバグは数ヶ月ではなく数時間で修正されるので、「人々が経験するバグ」の数は少ないからだ。

取り組みを公開している。 Trelloチームのルールは「デフォルトで公開」だ。私たちが現在取り組んでいることとこれから取り組むものを全て一覧できる公開のTrelloボードがある。私たちはこれを、顧客にお気に入りの機能に投票とコメントをしてもらうために使っている。ところでTrelloの開発中には、これは秘密だった。開発チームがリーン・スタートアップの原則を使えるように、私たちはカスタマーフィードバックをくれるたくさんのベータテスターを抱えていたが、バージョン1.0をつくり上げるために費やしたその秘密の9ヶ月は、我々に競争市場で大幅なリードをもたらした。だた今や私たちは製品を出荷したので、私たちの計画を喋らない理由はない。

”ベンとジェリーの”ではなく、”高速に成長する”製品である。 Strategy Letter Iを読んでくれ。Trelloのビジネスゴールは、究極的には10億人のユーザーを獲得することだ。これは、私たちの最優先事項が、採用を妨げるいかなる障害も取り除くことであると意味している。人々がTrelloを使わない理由に挙げそうな全ての事柄を発見し、排除する必要がある。例えば:

Trelloは無料である。 製品に料金を請求することでおきる摩擦は、大規模な成長のための最大の障害だ。長い目で見れば、少数のユーザーから大量のお金を取り出す方法より、多数のユーザーから少量のお金を取り出す方法を見つける方がずっと簡単だと私は思う。ひとたび10億人のユーザーを獲得すれば、その中の誰があなたの作った製品から最大の価値を得ているか見つけるのは簡単だ。その最大価値を受けている人は、あなたに喜んでお金を払うだろう。他の者はそれほどサポートコストがかからない。

APIとプラグインアーキテクチャが最優先事項である。 他の言い方で言うとこうだ:もしそれが基本的なAPIを公開して高価値なユーザー(あなたのプラットフォームから最大価値を受け取っている人たち)が私たちのために作ってくれるものなら、決して自分たちで作るな。Trelloチームでは、プラグインで提供できる機能は全てプラグインで提供しなくてはならない。

APIは現在とても原始的な形をしている。既にそれを使ってなにか面白いことができるが、現在鋭意開発中だ。)

最先端の技術を使っている。 そのせいで私たちはたまに自分の指を切ってしまうのだが。私たちの開発者は、MongoDB, WebSockets, CoffeeScript, そしてNodeに血を流している。だが少なくとも、彼らは楽しんでいる。それに今日の厳しいジョブマーケットでは、偉大な開発者は彼らが何に取り組むかについて、大きな支配権を持っている。あなたが彼らに、何百万人もの人々を感動させるエキサイティングな製品を与え、彼らが単純な仕組みがうまくいっていない理由を探り当てようとしている間にTCP-IPの奥深くまで潜らせることができるなら、彼らは楽しみ、仕事を愛するだろう。それに、私たちは次の10年に取り組むプロダクトを作っているのだ。今日の単なる「最新鋭」の技術は、五年後には古びて軋みが生じるだろう。私たちは「最新式」をほんの少しだけ上回りたい。これは計算済みのリスクなのだ。

過激すぎるものはひとつもない。要するに、Fog Creek Softwareは全てのY Combinatorのスタートアップが、spezが真夜中にLispが壊れた時にRedditをリブートするためにラップトップを抱いて寝ていた頃から使ってきたテクニックでインターネット製品を作る。もしまだTrelloを試していないなら試してみてくれ。うまくいったら、私にTwitterで教えてくれ

Joel on Software

Joel on Software

More Joel on Software

More Joel on Software