decorator, presenter, exhibit という3つの実装パターンについて

@a_suenami さんのこのツイートの Decorator, Presenter, Exhibit が気になったので調べてみた。


結果、以下にまとまっていたので、記事中のコードを引用しながら色々と考えてみる。
http://mikepackdev.com/blog_posts/31-exhibit-vs-presenter





Decorator とは?

Decorator とは、おそらく GoFの DecoratorPattern のことだと思う。
元記事では最初の方に Car クラスで説明されている。

以下は元記事のコードだが、見れば大体分かると思う。

# ruby の SimpleDelegator によって Car のメソッドを delegate している。
class Decorator < SimpleDelegator
end

class Car
  def price
    1_000_000
  end
end

class CarWithHeatedSeats < Decorator
  #Car.price を CarWithHeatedSeats.price でオーバーライドしている
  def price
    super + 5_000
  end
end

car = Car.new
car.price #=> 1000000

#Car を CarWithHeatedSeats でラップしている。
#ラップに関しては ruby の SimpleDelegator がいい感じにやってくれている。
car = CarWithHeatedSeats.new(car)
car.price #=> 1005000

CarWithHeatedSeats で Car をラップすることで、
本来の Car.price の挙動に任意の挙動を加えることができる。
上記の例だと +5000 されているのが任意の挙動にあたる。

さらに、目的によっては以下のように Car に存在しないメソッドを実装して、
Car を拡張したオブジェクトを作ることもできる。

class CarWithHeatedSeats < Decorator
  #Car.price を CarWithHeatedSeats.price でオーバーライドしている
  def price
    super + 5_000
  end

  # Car を拡張するメソッド
  def hello
  	#省略
  end

end


目的別に CarWithHeatedSeats のような Decorator を作って Car をラップすれば、
Car を目的別に拡張することができる。
継承を利用しないオブジェクトの拡張手段といったところだろうか。

Presenter とは?

Presenter は "表示に関するロジック" を責務に持つ実装パターンであり、
結構一般的なパターンだと思う。

元記事では以下のように description という表示に関するロジックを持ったメソッドを実装している。

class CarPresenter < Decorator
  def description
    if price > 500_000
      "Expensive!"
    else
      "Cheap!"
    end
  end
end

car = CarPresenter.new(Car.new)
car.price #=> 1000000
car.description => "Expensive!"



"表示に関するロジック" とはなんだろう?
具体的には以下が該当するかなと思っている。

  • User クラスの status の値に応じて "プレミアム会員", "一般会員" のように文言を出し分ける。
  • int の金額をカンマ区切りの文字列にする。
  • User クラスの name に "様" を付ける。

こういった Viwe 周りのロジックはサーバサイドのビジネスロジック自体にはあまり関係ないかもしれないが、
ユーザーに見せる部分なので色々と気を使って加工することが多い。

これらをモデルである Car クラスに全て実装してもいいが、
Car クラスはどんどん肥大化するだろう。
この肥大化を避けたい場合、 Presenter を導入すると良い。

Exhibit とは?

Exhibit は "描画" を責務に持つ実装パターンである。

以下は記事中の Exhibit のコード。
何をやっているかは見れば大体分かると思う。

class CarWithTwoDoorsExhibit < Decorator
  def initialize(car, context)
    @context = context
    super(car) # Set up delegation
  end

  def additional_info
    "Some cars with 2 doors have a back seat, some don't. Brilliant."
  end

  def render
    @context.render(self)
  end
end

class TextRenderer
  def render(car)
    "A shiny car! #{car.additional_info}"
  end
end

class HtmlRenderer
  def render(car)
    "A <strong>shiny</strong> car! <em>#{car.additional_info}</em>"
  end
end

car = CarWithTwoDoorsExhibit.new(Car.new, TextRenderer.new)
car.render #=> "A shiny car! Some cars with 2 doors have a back seat, some don't. Brilliant."
car.price #=> 1000000

car2 = CarWithTwoDoorsExhibit.new(Car.new, HtmlRenderer.new)
car2.render #=> "A <strong>shiny</strong> car! <em>Some cars with 2 doors have a back seat, some don't. Brilliant.</em>"

CarWithTwoDoorsExhibit は特定の文言を テキスト or HTML で表示することができる。
表示する文言も additional_info() にて定義されている。
文言表示には Car が持っている情報が必要なので、コンストラクタでは Car を引数にとっている。

これを見たときに、
フルスタックな WAF についている HTML Helper, View Helper 的な印象を受けた。

自分は CakePHP を使った経験があるが、
以下のように HTML に PHP のタグを書いて form を表示していた。

<?php echo $this->Form->create($article); ?>


PHPの例は HTML の form を生成するためのヘルパーであるが、
"create() の引数に $article というオブジェクトを渡すことで、特定の HTML を描画する"
という性質は Exhibit に近いものがあるのではないか? と思っている。

Presenter と Exhibit の違いは?

  • Presenter = "表示に関するロジック" を責務に持つ
  • Exhibit = "描画" を責務に持つ

と説明したが、言葉で説明すると違いが抽象的になってしまい、なかなか難しい。

Presenter のセクションで以下は Presenter が持つべきロジックであると紹介しているが、

  • User クラスの name に "様" を付ける。

「"様" を付けるのはロジックというよりは、"描画" の責務ではないのか?」
と反論されると、
「そうかもしれない」と感じてしまう。

しかし、このような "値を加工するロジック" を Exhibit の責務にしてしまうと、
Presenter が持つべきロジックがかなり限定されてしまい、
Exhibit が肥大化してしまう可能性が高くなる。
個人的には Presenter に実装するだろう。

ということで、個人的な感覚になってしまって申し訳ないが、
Presenter は View に表示する変数を加工するための責務を持ち、
Exhibit はその変数を利用する View 周りのパーツ(HTMLのタグや文章)を描画する責務を持つイメージがある。

これは "View を描画する" という仕組みを考えたときに、
責務としてどう分割するか? を考えた結果である。
ココらへんの責務に関しては "責務を分ける必要性" というセクションでも触れるので、
そちらも読んで欲しい。

Decorator, Presenter, Exhibit の関係性

関係性としては以下になる。

  • Decorator = オブジェクトを拡張する実装パターン
  • Presenter = "表示に関するロジック" を責務に持つ Decorator
  • Exhibit = "描画" を責務に持つ Decorator

Presenter も Exhibit も Decorator という実装パターンを利用しているという点でいうと同じであるが、責務は異なる。

Presenter + Exhibit

Presenter, Exhibit はそれぞれ異なる責務を持つので、
当然ながら組み合わせることができる。

以下は記事中のコードだが、見れば大体分かるはず。
Presenter が内部に Exhibit を保持しており、
render() が利用できるようになっている。

class CarPresenter < Decorator
  def initialize(car)
    exhibit = rand(2) == 1 ? CarWithTwoDoorsExhibit : CarWithFourDoorsExhibit
    super(exhibit.new(car, TextRenderer.new))
  end

  def description
    if price > 500_000
      "Expensive!"
    else
      "Cheap!"
    end
  end
end

car = CarPresenter.new(Car.new)
car.description #=> "Expensive!"
car.render #=> "A shiny car! ..."

この CarPresenter は以下の機能を持つ。

  1. description() にて特定の文言を出し分ける。
  2. render() にて特定のテキストを出し分ける。

ユースケースとしては、CarPresenter を View に渡して、View 内で利用する想定なのだろうか?
以下は PHP のコードだが、こんなイメージ?

<html>
	<?php $car_presenter.description() ?>
	<?php $car_presenter.render() ?>
</html>



記事中には載っていないが、Exhibit が Presenter を持てば、
Presenter で加工した値を Exhibit が利用できるようになる。

以下は CarExhibit が CarPresenter を持ち、
render() にて CarPresenter.price() を利用するパターン。
個人的にこちらの方がしっくりくる。

#ruby書けないのでシンタックス間違っているかもしれません・・・。
class CarExhibit
  def initialize(car_presenter)
  	@car_presenter = car_presenter
  end

  def render
  	"this is " + @car_presenter.price()
  end
end



責務を分ける必要性

Presenter, Exhibit はそれぞれ別の責務を持ち、組み合わせることができると説明したが、
そもそも Presenter, Exhibit で責務を分ける必要はあるのだろうか?

直前のセクションで以下のように Exhibit に Presenter を持たせていたが、

class CarExhibit
  def initialize(car_presenter)
  	@car_presenter = car_presenter
  end

  def render
  	"this is " + @car_presenter.price()
  end
end



以下のようにPresenter.price() のロジックを直接 Exhibit に持たせてもいいのではないか?
こうすれば、わざわざ Presenter を実装する必要はなくなる。
Exhibit は "描画" を責務に持つので、描画に必要なメソッド(今回は render() になる)のみ公開している。
price() のようなロジックは private なメソッドにした方が良いだろう。

class CarExhibit
  #コンストラクタには Car クラスを渡す
  def initialize(car)
  	@car = car
  end

  def render
  	"this is " + price()
  end

  private
    def price
    	#CarPresenter.price() 相当のロジックを実装する
      @car.price() + 5000
    end
end



結論から言うと、 "描画する" という目的を達成するのであれば問題ないと思う。
わざわざ CarPresenter を用意する必要はなさそう。

では、Exhibit にロジックを実装すれば Presenter は不要なのか?
というと、そうでもない。
Presenter を用意することによって View で利用する値を各所で使い回しやすくなる。

例えば、
User クラスの status の値に応じて "プレミアム会員", "一般会員" のように文言を出し分ける
という仕様があった場合、
"プレミアム会員", "一般会員" のような文言は複数の画面で表示する必要があるかもしれない。

その場合、 Exhibit で実装してしまうと、文言を出し分けるロジックが複数の Exhibit でコピペされてしまう可能性がある。

具体的なコードで説明すると以下の price() のようなロジックが、

class CarExhibit
  #コンストラクタには Car クラスを渡す
  def initialize(car)
  	@car = car
  end

  def render
  	"this is " + price()
  end

  private
    def price
  	  #CarPresenter.price() 相当のロジックを実装する
      @car + 5000
    end
end


以下のように各 Exhibit にコピペされる可能性がある。

class XxxExhibit
  #省略

  def render
  	"Xxx is " + price()
  end
  
  private
    def price
      @car + 5000
    end
end

class YyyExhibit
  #省略
  
  def render
  	"<b>Yyy is " + price() + "</b>"
  end

  private
    def price
      @car + 5000
    end
end


Exhibit は "描画" の責務を持つので、
render() によって出力される文字列を利用するのが目的となる。
そして、render() には HTML のタグのような "特定のページでしか利用しない装飾" を扱う可能性があるので、
Exhibit 同士を組み合わせることが難しいケースがある。
そういった場合、安易にロジックをコピペすることで解決してしまうかもしれない。

では、 private になっている price() を public にしたらどうだろう?
public にすれば、Exhibit 同士を組み合わせることでコピペを防ぐことができる。

以下のように XxxExhibit にて CarExhibit.price() を参照するイメージ。

class XxxExhibit
  #コンストラクタに CarExhibit を渡す
  def initialize(car_exhibit)
  	@car_exhibit = car_exhibit
  end

  def render
  	"Xxx is " + @car_exhibit.price()
  end
  
end



しかし、CarExhibit には render() のように描画の責務を持つメソッドが実装されているので
CarExhibit を XxxExhibit に渡してしまうと、
"price() で算出される値のみ利用したい" という目的に対して、
CarExhibit の責務が大きすぎる気がする。

例えば、以下のように CarExhibit.render() を利用することも可能になってしまう。

class XxxExhibit
  #コンストラクタに CarExhibit を渡す
  def initialize(car_exhibit)
  	@car_exhibit = car_exhibit
  end

  def render
  	"Xxx is " + @car_exhibit.price()
  end

  #render() が利用できてしまう
  def xxx
  	"Xxx is " + @car_exhibit.render()
  end
  
end

一見便利そうに見えるが、
実際に Exhibit が持つメソッドは render(), price() だけとは限らない。
Car が持つそれぞれの値に対して price() のようなロジックを持つことも考えられる。

Exhibit オブジェクトが大きくなればなるほど想定外の依存が発生する可能性は高くなる。
「特定の Exhibit の特定のメソッドを修正したら想定外の Exhibit に影響してしまった」
なんてことがありえるかもしれない。

Exhibit のロジックを Presenter に切り出せば、各オブジェクトの責務は小さくなる。
基本的にクラスや関数は責務が限定されればされるほど再利用性が上がり、
修正による影響を予想しやすくなるので、
Exhibit にロジックを実装するのが常に正しいとは限らない。

このように Exhibit, Presenter で責務を分けるかどうか? というのはケースバイケースである。
自分たちのケースに合った最適な実装を選択しよう。

Presenter, Exhibit を導入する必要性

ここまで Presenter, Exhibit について説明してきたが、
そもそもこれらのパターンは必要なのだろうか? というのも考えなければいけない。

開発方針に依存するが、
サーバサイドで HTML をレンダリングするようなWebサービスでない限り、
View 周りの値の装飾や加工はクライアント側がやってくれるかもしれない。
その場合、サーバが View 周りのロジックを持つ機会というのは減ってくるだろう。

Presenter, Exhibit をわざわざ導入するまでもないケースだって当然ありえるはずなので、
そもそも必要なんだっけ? というのも考える必要がある。

ちょっとした加工であれば、
Controller でチャチャッとやってしまうというのもありだろう。
それが問題になったときに Presenter, Exhibit を導入すればいい。

ちなみに、自分は以下のように View 用の値を全て詰め込むオブジェクトを用意して、

package response

type UserListPage Struct {
	ID int
	Name string
}



モデルからそのオブジェクトを生成するような関数を実装するだけで済ますことが多い。
最初はこの程度で十分やっていけたりするし、
この程度であれば Controller にべた書きしてもいいと思う。

func NewUserListPage(user *User) *UserListPage {
	return &UserListPage {
		ID: user.ID,
		Name: user.Name + " 様", //ここで値の加工をしてしまう
	}
}



値の加工ロジックが複数箇所で必要になる場合は、
そのロジックを関数に切り出してまとめてしまう。

func NewUserListPage(user *User) *UserListPage {
	return &UserListPage {
		ID: user.ID,
		Name: displayUserName(user.Name), //切り出した関数を利用する
	}
}

//関数に切り出す
func displayUserName(name string) string {
	return name + " 様"
}



しかし、切り出し対象の関数が多いと、
切り出した関数に Prefix, Saffix が多くなってきて管理が辛くなってくることがある。
そうなると、 Presenter 相当のオブジェクトで管理したくなってくる。
1つのモデルに対して1つの Presenter を用意する感じ。

func NewUserListPage(user *User) *UserListPage {
	p := NewUserPresenter(user)
	return &UserListPage {
		ID: p.ID,
		Name: p.Name, //UserPresenter を利用する
	}
}

type UserPresenter struct {
	ID int
	Name string
}

func NewUserPresenter(user *User) *UserPresenter {
	//省略	
}



上記は Decorator を利用しているわけではないのだが、
NewUserListPage() という関数が Exhibit のような責務(= 描画の責務)を持っていて、
UserPresenter が Presenter のような責務(= 表示に関するロジック)を持っている感じになっている。

このように具体的な実装方法は異なっていても、
結局 "描画" と "表示に関するロジック" という責務に分ける感じになるので、
Presenter, Exhibit 相当のものを必要に応じて好みの実装方法で導入していけばいいと思っている。

まとめ

特にないです。

記事中では "View を描画する" という HTML を描画するようなケースを前提の内容でしたが、
JSON でも値の加工は必要なので、考え方自体は変わらないかなという感じです。

"すえなみチャンス" というのを聞いて "早川チャンス" を思い出しました。
全然別物なのですが、懐かしい気持ちになりました。

ruby のコードは雰囲気で書いたので、おそらく間違っていると思います。
なんとなーくなイメージが伝われば十分かなーと思い、横着してしまいました。

マサカリあれば、 twitter or ブログのコメント欄に連絡いただければと思います。

Go における FunctionalOptionPattern と MethodChaining について考える

きっかけ

この記事を書くきっかけは以下のブログで、
MethodChaining の代わりに FunctionalOptionPattern を利用したという記事。
https://www.calhoun.io/using-functional-options-instead-of-method-chaining-in-go/

この記事は @ono_matope さんのツイートで知った。




このツイートにリツイートした結果、
@hnakamur2 さんも加わり、3人でちょっとした議論をした。
議論の内容については以下を追ってもらえればと思う。
https://twitter.com/ono_matope/status/967788305830985728
*本体同士で面識もない方々とこういった議論ができるあたり、ツイッターって便利ですね。

せっかくなので、
上記ツイッター上でのやりとりを整理して、
自分なりの結論を書いてみようと思う。


FunctionalOptionPattern

これについては特に何も言及することはない。

FunctionalOptionPattern については以下を読むといい。
https://qiita.com/weloan/items/56f1c7792088b5ede136

この記事によると原典は以下なので、2014年からあったパターンみたい。
https://commandcenter.blogspot.jp/2014/01/self-referential-functions-and-design.html


MethodChaining

ここからが本題。

以下の記事は golang で MethodChaining の代わりに FunctionalOptionPattern を利用したという内容になっている。
https://www.calhoun.io/using-functional-options-instead-of-method-chaining-in-go/

ここからは自分の考えも交えて上記の記事を説明していくので、
記事中に載っていない内容を書いたり、
載っている内容を書かなかったりするかもしれません。
元記事を読んでから読み進めた方がいいと思います。


MethodChaining の問題点

なぜ MethodChaining の代わりに FunctionalOptionPattern を利用したかと言うと、
golang の戻り値でエラーを返すという言語仕様とMethodChainingの相性が悪いから。

java などの例外が存在する言語であれば、以下のようにメソッドをチェインすることができる。

User user = new User.Builder()
  .name("Michael Scott")
  .email("michael@dundermifflin.com")
  .role("manager")
  .nickname("Best Boss")
  .build();



しかし、golang の場合は戻り値でエラーを返すのでメソッドをチェインできない。
以下のように毎回エラーチェックをする必要がある。

var db *gorm.DB
var err error

db, err = db.Where("id = ?", 123)
if err != nil { ... }

db, err = db.Where("email = ?", "jon@calhoun.io")
if err != nil { ... }



このように golang のエラーハンドリングの言語仕様と MethodChaining の相性が悪いという点が問題となっている。


Error フィールドによる解決方法

golang の言語仕様と MethodChaining の相性が悪いという点はあるものの、
struct に Error フィールドを持たせることで、
それっぽく実装することができる。

記事中で例として扱われていたのは GORM 。

GORM は gorm.DB という struct に Error フィールドを持たせ、
MethodChainingによるエラーを格納しているらしい。
https://github.com/jinzhu/gorm/blob/48a20a6e9f3f4d26095df82c3337efec6db0a6fc/main.go#L15

MethodChainingが完了したタイミングで、
DB.Error が nil かどうかをチェックすることで、
エラーを補足できるようになっている。
http://gorm.io/docs/error_handling.html#Error-Handling

DB.Error のおかげで各メソッドで error を返す必要がなく、
メソッドをチェインすることができる。

if err := db.Where("name = ?", "jinzhu").First(&user).Error; err != nil {
  // error handling...
}



Error フィールドによる解決方法の問題

しかし、Error フィールドによる解決方法にも問題がある。

1. 各メソッドでエラーが発生しないような印象を受ける

DB.Error がエラーを格納するので、
チェイン対象の各メソッドは error を返さない。

そのため、そのメソッドに対してエラーにならない操作のような印象を受ける。
しかし、実際はエラーが発生する可能性がある。

おおげさに言うと、
エラーが発生するという危機感が薄れてしまう。


2. エラーチェックを忘れそう その1

golang のエラーハンドリングは戻り値でエラーを補足するので、
MethodChaining後に DB.Error のエラーチェックをするのを忘れそう。


3. エラーチェックを忘れそう その2

gorm は Open() する際は error を返す。

func Open(dialect string, args ...interface{}) (db *DB, err error) {
	//省略
}

https://github.com/jinzhu/gorm/blob/48a20a6e9f3f4d26095df82c3337efec6db0a6fc/main.go#L44

しかし、DB.Error のエラーチェックも必要なので、
「DB.Error には、gorm.Open() 時のエラーも格納されるはず」という思い込みがあると、
gorm.Open() 時に発生するエラーをハンドリングせず、
結果的に不具合につながる。

逆に DB.Error のチェックが不要な場合でもチェックするようなコードを書いてしまうこともある。

ちなみに、
gorm.Open() で発生するエラーが DB.Error に格納されるかどうかは調べていません。
あくまで「DB.Error には gorm.Open() のエラーが格納されない」と仮定した話です。


FunctionalOptionPattern による解決方法

FunctionalOptionPattern を利用すると、以下のような書き方になる。
MethodChaining の問題点は解消されているはず。

var user User
err = db.First(&user,
  Where("email = ?", "jon@calhoun.io"),
  Where("id = ?", 2),
)
if err != nil {
  panic(err)
}
fmt.Println(user)



MethodChaining としての解決方法

記事の内容としては、FunctionalOptionPattern を使おうっていう内容なんだけど、
記事中には以下の記載がある。

Note: One way to fix this would be to update methods like First and Find in GORM to return both the gorm.DB and an error when called, but I’m not 100% sure how this would affect the rest of the library. Instead we are going to explore an alternative approach that I prefer anyway

First(), Find() というメソッドが gorm.DB と error を返すようにする修正方法もある とのこと。

これはおそらく、以下のイメージだと思う。

db, _ := db.Open()
db, err := db.Where().Where().Find()
if err != nil {
	panic()
}



実装は以下。
MethodChainingの最後に Find() を実行させるので、
Find() だけが error を返せばいい。

package db

func Open() (*DB, error) {
	return DB{}, nil
}

type DB struct {
	err error
}

func (d *DB) Where() *DB {
	if xxx {
		d.err = errors.New("error!")
		return d
	}
	return d
}

func (d *DB) Find() (*DB, error) {
	return d, d.Err
}



クエリの構築であれば、
Build() で error を返すようにして、
Find() はチェインできるようにしてもいいと思う。

func (d *DB) Find() *DB {
	return d
}

func (d *DB) Build() (*DB, error) {
	return d, d.err
}



まあ、どちらにしろ、チェインの最後で error を返すようにしてあげればいいはず。

これによって、MethodChainingを利用するケースでも、
Error フィールドの問題点は解決することができる。


Error フィールドの問題点は本当に問題なのか?

元記事にある Error フィールドの問題点は、そもそも問題なのだろうか?

以下では、struct に Error フィールドを持たせることで、
エラーチェックの繰り返しを避けるテクニックが紹介されている。
https://blog.golang.org/errors-are-values

上記の記事でも紹介されているように、
標準パッケージの scan は Error フィールドを持ってるし、
https://github.com/golang/go/blob/f7ac70a56604033e2b1abc921d3f0f6afc85a7b3/src/bufio/scan.go#L38

エラーをチェックするためのメソッドも提供している。
https://github.com/golang/go/blob/f7ac70a56604033e2b1abc921d3f0f6afc85a7b3/src/bufio/scan.go#L90

エラーを戻り値で返すよりも、分かりやすいかどうか? という観点だと、
劣るかもしれないが、
少なくとも Error フィールドを持たせる実装がアンチパターンというわけではないはず。
なので、そもそも問題ではないのかもしれない。

元記事の筆者も より良い方法は FunctionalOptionPattern だよ と言っているだけで、
MethodChainingを否定しているわけではないと思う。
(わからんけど・・・


どちらを使うべきなのか?

正直、好みの問題なのかなーと思っている。

FunctionalOptionPattern はオプションの実装に interface を利用しているので、
ライブラリを利用する側が独自の実装を適用させることができる。
なので、不特定多数が利用するライブラリであれば、 FunctionalOptionPattern の方が適している気もする。

しかし、MethodChaining もメソッドの挙動を interface によって差し替えるような実装にすることで、
同じようなことが可能なので、
実装の差し替えは決定打にはならないなーというのが正直なところ。

FunctionalOptionPattern の方が実装が若干面倒な気がするけど、
MethodChaining に interface を導入した場合、
どちらも大して変わらないと思う。

個人的には、

MethodChaining = オブジェクトを組み立てるための実装パターン
FunctionalOptionPattern = オプションを指定するための実装パターン

という認識がある。

どちらを使っても目的は達成できるが、
そもそも用途が異なるかなーと思っているので、
とりあえず、上記の認識通りに使い分けていこうかなーと思っている。

そして、MethodChaining を利用する場合、
MethodChaining としての解決方法 を適用するかどうかも気になってくるが、
これもどっちでもいいと思う。

ただ、個人的には、戻り値で戻ってくる方が分かりやすいかなーと思うので、
適用可能であれば適用するかなーというところ。

まとめ

FunctionalOptionPattern or MethodChaining を悩むんだったら、
もっと他に悩むべきコードがありそうなので、
そっちを優先した方がいいと思う。

書籍「Real World HTTP」の学習効率の高さについて

今更ながら、Real World HTTP を読んでみたので、
学習効率という視点で感想を書いてみようと思う。

www.oreilly.co.jp

対象となる読者が広い

この書籍のテーマは HTTP なので、対象となる読者はかなり多いはず。

さらに、書籍でも言及されているが、
HTTP のようなプロトコルは陳腐化しづらい知識なので、
一度身につけるとずっと役に立つ。

サーバサイドエンジニアに限らず、
アプリエンジニア、
WebFE エンジニア、
HTTP に触れるエンジニアであれば、目を通しておいた方がいい。

特に HTTP Header に指定する属性と値の知識は、実際のサービス開発で必須となることが多い。

サーバであっても、
クライアントであっても、
「ヘッダーに、この属性つけなくて良いんですか?」という一言が、
サービスのセキュリティやパフォーマンスを向上させるかもしれない。

対象読者が幅広く、陳腐化しづらい書籍なので、
この書籍を通して学んだ内容が無駄になることは少ない。
効率の良い書籍だと思う。

HTTPについて体系的に学べるので、
新卒などのWeb開発に関わって日が浅いエンジニアには是非読んでもらいたい。


スラスラ読める

そして意外にも(?)普段から HTTP に触れているエンジニアにもオススメできる。

なぜかというと、
この書籍の何割かの内容は、
すでに知っていたり、過去にどっかで調べていることがあるからだ。

「これあったなー」とか「こんなオプションもあったのか」という、
既存知識の部分的な補完や思い出しがメインになるはず。
なので、ページ数のわりにはスラスラ読める。

そして、たまに「これ全然知らなかった」というものが出てくる。

普段から HTTP に触れているエンジニアこそ、
時間を無駄にすることなく、
必要な情報だけ拾うことができると思う。


好きなところから読める

自分は普段「コードの書き方」みたいな書籍を読むことが多い。

こういった書籍は、
第1章でリファクタリング対象システムの仕様を説明し、
第2章から順にリファクタリングを進めるという流れになっていることがある。

第1章を読まなければ、第2章は理解できないし、
第2章のリファクタリング結果が理解できなければ、
第3章のリファクタリング内容も理解できない。
時間が空くと、第1章の仕様を忘れていることだってある。

一方、この書籍は、主に HTTP/1.0 ~ HTTP2 までを時系列にまとめているが、
前後の関係性は薄いので、
気になるところだけ読めばいい。

読む内容を選択でき、
容易に途中から読み進められるので、
こういった点もまた学習効率がいいなと感じる。


一通り目を通すことを推奨

この書籍は特定の章だけ読むことができる。

しかし、HTTP は仕様が多いので、
「実は知らなかったこと」を見逃さないためにも、
全く読まないのではなく、
一通り目を通すことを推奨する。

ページをパラパラめくって、知らない単語周辺を読むだけでいい。
ページをパラパラして、知識が増えるのであれば、パラパラした方がいい。

さらに、この書籍には、ちょっとした面白い内容が載っていたりする。

リファラーのスペルミスだったり
ダウンロードページの広告のことだったり

サラッと書いてある文章や注釈を1つ1つ読んで覚えておくと、
エンジニア飲みの時の話のネタになるかもしれない。


Go言語は関係ない

書籍の目次を見ると、Go言語による実装の章が目に入ると思う。
これを見て「Go言語知らないからな・・・」と敬遠するのはやめて欲しい。

この章は HTTP のリクエスト、レスポンスを実装しているだけなので、
正直、読み飛ばしても問題ない。
ちなみに、Go言語による実装の章を読まない場合、60ページ分削減されるので、
より効率よく HTTP を学ぶことができる。

仮に読もうとした場合でも、
実装対象がシンプルなので、サラッと見ても大体何をやっているのかが分かる。
読んでみて、無理だったら飛ばせばいい。

Go が読み書きできる人は HTTP2 の実装は目を通した方がいいかもしれない。
サーバプッシュ、ServerSendEvent が載っている。
これらを実装したことがあるエンジニアは少ないのではないだろうか。


キャッシュの仕様

自分はサーバサイドのアプリケーションエンジニアだが、
HTTP といえば、キャッシュの設定が複雑という印象が強い。

no-cache, no-store ?
Last-Modified, Expires, Etag?

どれがどれだかよく分からなくなる。

そして、事故るとパフォーマンスが劣化したり、
コンテンツが更新されなかったり、
大きなダメージを受ける

この書籍では、このキャッシュの仕様について、時系列で説明している。

時系列だから何? と思うかもしれないが、
時系列にすることによって、

「最初は xxx しかなかったけど、yyy を考慮して zzz が生まれました」

のような説明ができる。

そして、これが意外にもすんなり入ってくる。
この章だけでも読んでおくと良い。


HTTP2が載っている

HTTP2 について、体系的に扱っている書籍ってあまりないのでは? と思っている。
普段使いの HTTP/1.1 と一緒に HTTP2 についても学べるいい機会になるはず。


まとめ

学習効率が良い上に、読んで損しない書籍です。
良書だと思います。
併せて「Web API: The Good Parts」も読みましょう。

www.oreilly.co.jp



その他

レビューをした

自分と渋川さんは DeNA つながりということもあり、
今回、書籍のレビューに参加させていただいた。

ただ、予想以上にレビューは難しかった。

自分はHTTPに強いわけでもないので、
書籍の質を向上させるコメントが全然できない。
他のエンジニアのコメントを見て「ああ、なるほど・・・」と思ってばかりだった。

レビューを通じて、
自分のスキルの低さを痛感し、
謎の向上心が生まれたのを覚えている。

そして、今回書籍を読んでみて、
自分のレビュー内容が書籍に反映されたのは嬉しかった。


pospomeという単語が載っている

レビューをしたということもあり、
謝辞に pospome という単語が載っている。
オライリーに pospome という単語が載るとは思ってもいなかった・・・。


いまさら読んだ理由

レビューしたということもあり、書籍は献本していただいた。
ありがとうございます。

献本していただいたからには、レビュー書かないとなーと思い、
発売してから書こうと思ったが、
発売から数ヶ月間は「読んでみた系」のブログエントリーがちらほら流れてきた。

こういったタイミングで自分が感想を書いても、書かなくても、
書籍を買う人は買うだろう。

であれば、
「そーいえば、買ってなかった」という人に手に取ってもらい、
その人の技術力が少しでも上がればないいなと思ったので、
半年後くらいに書こうと思った。

golang の「埋め込み」を利用した実装アンチパターン

最近、time.Time を独自の struct で埋め込んだ実装をみかけたので書いておこうと思った。

結論から言うと、
標準パッケージやライブラリで定義されている型に対して「埋め込み」を使うと、
コードが正常に動かなくなる可能性があるので、
注意した方がいいというお話。

自分は、普段 GAE/Go で開発をしていて、
ストレージとして Datastore を利用しているが、
time.Time を独自の struct で埋め込んだ実装でハマった経験がある。

問題の説明

time.Time に限らず、
埋め込みによって、既存の struct を拡張するというアプローチはよくあるものだと思う。


以下の実装は既存の time.Time 型の機能に Hello() というメソッドを追加している。

package pospome_time

type Time struct {
	time.Time
}

func (t *Time) Hello() {
	fmt.Println("pospome time hello");
}

こうすることで、
time.Time のメソッドはそのまま利用できるし、
Hello() というアプリケーションコードに必要な振る舞いも定義できる。

ここで気をつけなければならないのは、
埋め込みは既存の struct の型を持たない という点。

上記の例で言うと、pospome_time.Time は time.Time 型ではない

なので、time.Time に依存する処理がある場合、上手く動かないことがある。

最初にサラッと説明した GAE/Go の Datastore に例で言うと、
以下のように time.Time の型を判定する処理がある。
*とりあえず、grep したパッと出てきたやつです。
https://github.com/GoogleCloudPlatform/google-cloud-go/blob/fdbfa8004f3a20b8b375215c71577a2d6ec48ab3/datastore/save.go#L72

pospome_time.Time は time.Time ではないので、
time.Time に対応した処理は実行されない。

これは pospome_time.Time の利用者からすると期待通りの動作ではない可能性がある。
なぜなら、pospome_time は time.Time を埋め込んでいるので、
あたかも time.Time を扱っているように思えてしまうから。

そして、これらのハマりどころは、
引数が interface{} になっている関数やメソッド が原因であることが多い。

以下のように関数の引数が time.Time だった場合、
pospome_time.Time は time.Time 型ではないので、
そもそも引数に指定できない。
このケースで事故ることはない。

// pospome_time.Time は指定できない。
// これは事故らない
func Xxx(t time.Time) {
}



一方、引数が interface{} である場合、
pospome_time.Time も time.Time も指定できてしまう。

// pospome_time.Time が指定できてしまう
// これは事故るかも?
func Xxx(src interface{}) {
	//ここの中で time.Time かどうかを判定している。
}

そして、こーゆー関数に限って関数内で型判定が存在する。
関数を実装する側は、具体的な型を知らなければならないので、当然といえば当然。

そして、以下のような type による独自型定義も time.Time としては扱われない。
埋め込みだけではなく、こちらも注意。

type PospomeTime time.Time



どう解決すればいいのか?

自分は、埋め込みが難しそうなときは、
以下のようにそれ用の関数を作っている。

func Hello(t time.Time) {
	//なんかする
}



以下のように、
埋め込みをした struct から、
元の struct へ変換するようなものも考えたが、
変換を忘れて、そのまま interface{} の引数に突っ込んでしまいそうなので避けている

package pospome_time

type Time struct {
	time.Time
}

func (t *Time) NewTime() time.Time {
	//レシーバの t を元に time.Time を作る。
}



interface{} の引数を持つ関数を利用する箇所が限られているのであれば、
上記の実装でも問題ないかもしれない。
ここは既存のアーキテクチャとチームの方針次第になる。


標準パッケージやライブラリに対する埋め込みはNGなのか

標準パッケージやライブラリに対する埋め込みはNGなのか?
というと、そんなことはない。

アプリケーションコードでしか利用しないような struct の拡張であれば問題はない。

time.Time は、
Datastore や RDB のようなストレージでよく利用される特性を持ち、
それらが提供している関数の引数が interface{} であることが多いので、
ハマったが、
それ以外のケースで利用する場合、
埋め込みでもいけると思う。

例えば、複雑な日時計算をするような場合、
time.Time を埋め込んだ struct を作成することで、
いい感じのコードになることは十分に考えられる。

ちなみに、型判定は reflect パッケージを利用する。
int, string のようなプリミティブ型であれば、
独自の型を定義しても reflect で判定可能なので、問題にならなかったりする。
https://github.com/GoogleCloudPlatform/google-cloud-go/blob/fdbfa8004f3a20b8b375215c71577a2d6ec48ab3/datastore/save.go#L76

//こーゆーのは問題ない
type MyInt int



しかし、struct に関しては、
reflect.Struct というザックリした型しか存在しない。

なので、
以下のように、それっぽい struct を想定したコードを書くのが限界。
https://github.com/GoogleCloudPlatform/google-cloud-go/blob/fdbfa8004f3a20b8b375215c71577a2d6ec48ab3/datastore/save.go#L71-L72

当然、Datastore の作者は pospome_time.Time を知るはずもないので、
ここに pospome_time.Time が追加されることはない。

今回の件は
interface{} の引数に渡されやすく、
複雑な計算が要求されやすい time.Time ならではのハマりどころなのかもしれない。


まとめ

特に無いです。

time.Time + 埋め込みの件については、
自分を含めてハマった人を複数人見たので書き残しておこうと思いました。
これから GAE/Go やる人増えてくると、そこそこハマるんじゃないかなーと思います・・・。

アサーション、reflect 周りの知識が弱いので、
間違いや補足があれば、教えてください。
修正させていただきます。

GAE の TaskQueue(PushQueue) で、delay パッケージと HTTP の受け口(handler)を定義するのは何が違うのか?

TQ を利用するとき、
いつもはタスクを受ける HTTP の受け口(handler)を定義して、
taskqueue パッケージで実装を完結させていただけど、
「今回は delay パッケージ使ってみようかなー」
と思ったので違いを調べてみた。

それぞれの使い方とかはネットに転がってるので、
ググってください。

挙動に大きな違いはない気がする

自分が軽く触った結果、挙動に大きな違いはなかった。

handler でも delay でも、
どっちでもできることは同じに思える。

細かい実装の違い

handler でも delay でも、
どっちでもできることは同じだが、
handler が HTTP を受けるのに対して、
delay は以下のような関数なので、
これらの違いによる 実装方法の違い は存在する。

var laterFunc = delay.Func("key", func(c context.Context, x string) {
    // ...
})



例えば、リトライの実装方法が違う。

handler は HTTP Status によってリトライするかどうかを制御する。
https://cloud.google.com/appengine/docs/standard/go/taskqueue/push/retrying-tasks

一方、delay は HTTP を受け取らないので、
関数の戻り値で error を返すかどうかによってリトライするかどうかを制御する。

If the function has any return arguments, and the last one is of type error, the function may return a non-nil error to signal that the function should be retried.

https://cloud.google.com/appengine/docs/standard/go/taskqueue/delay


HTTP Status を返すよりも、
error を返すほうが簡単かなーと思わなくもないが、
実際大して変わらないと思う。

パッと思いつくのは、
リトライ実装くらいだが、
handler or 関数 の性質の違いが実装方法に違いを与えることがある。

delay パッケージには Function.Call() がある

delay には Function.Call() というメソッドが生えている。
https://cloud.google.com/appengine/docs/standard/go/taskqueue/delay#Function

これは delay における以下のコードを、

t, _ := f.Task(...)
_, err := taskqueue.Add(c, t, "")



以下のように Call() のみで書くことができる。

err := f.Call(c, ...)



Call() では queue.yaml の name を指定することができないので、
queue name は default になる。

まあ、それだけなんだけど・・・。

delay パッケージはタスクに struct を突っ込める

delay では、
タスクを処理する関数側で user struct を以下のように受け取ることができる。

var laterFunc = delay.Func("key", func(c context.Context, user User) {
    // ...
})



なので、タスクを追加する際のコードでは、
以下のように user struct を突っ込むことができる。

laterFunc.Call(c, User{"pospome"})



一方、handler を定義する場合、
タスクに突っ込める値は url.Values になっているので、
string しか扱えない。

func NewPOSTTask(path string, params url.Values) *Task

https://cloud.google.com/appengine/docs/standard/go/taskqueue/reference#Task

ただ、
handler のタスクである taskqueue.Task の url.Values は、
以下の Task.Payload に []byte として突っ込まれるので、

type Task struct {
    // Path is the worker URL for the task.
    // If unset, it will default to /_ah/queue/<queue_name>.
    Path string

    // Payload is the data for the task.
    // This will be delivered as the HTTP request body.
    // It is only used when Method is POST, PUT or PULL.
    // url.Values' Encode method may be used to generate this for POST requests.
    Payload []byte

    //省略
}

delay のように struct を gob とかでシリアライズしてあげれば、
いけるのかもしれないが、
やはり delay の方が便利かなと思う。
*動作確認はしていません。

delay パッケージには罠がある

以下に載っている。
https://qiita.com/hogedigo/items/fae5b6fe7071becd4051

・delay関数の生成タイミング間違えてハマった(´・ω・`)
・delay関数使ったコードのファイル名変えてハマった(´・ω・`)

ファイル名の変更が NG というのは結構なハマりポイントな気がする・・・。
忘れそう・・・。

外部から叩けるかどうか

handler として定義すると、
以下のように handler をセキュアな設定にすると思うが、
https://cloud.google.com/appengine/docs/standard/go/taskqueue/push/creating-handlers#securing_task_handler_urls

この設定を取っ払ってしまえば、 handler を外部から叩くことができる。

オンプレの外部システムから handler を叩いてタスクを実行したい
というケースでは役に立つかもしれない。

ただ、delay を利用した場合でも、
それ用の handler を用意すればいいだけだし、
handler の処理は同期処理になると思うので、
結局 handler で定義することに明確なアドバンテージはない気がする。


どっちを使えばいいのか?

delay パッケージを触ってみて、
TQ(Push)は全部 delay でいーかなと思いました。

handler 定義した方がいいよ っていうケースがあれば、
ブログのコメント欄 or twitter で教えてください。