redux/examples/real-world を見て学ぶ

GitHub - reactjs/redux: Predictable state container for JavaScript apps

Githubのアカウント名を入力すると、 starしているリポジトリと作者の一覧が表示される。

一覧表示のリポジトリ名やアカウント名をクリックすると、それぞれ対応したページに切り替わる

ページ遷移

GitHub - acdlite/redux-router: Redux bindings for React Router – keep your router state inside your Redux store

redux-router を活用してアカウント、リポジトリをURLで表現することで、ページの遷移を実現している。

以下のコードで、アカウント名を入力してgoボタンを押すと、"/アカウント名" に遷移する処理を実現。

// container/App.jsから対象部分を抜粋

import { browserHistory } from 'react-router'
import Explore from '../components/Explore'
...

class App extends Component {
  ...
  handleChange(nextValue) {
    browserHistory.push(`/${nextValue}`)
  },
  ...
  render() {
    const { children, inputValue } = this.props
    return (
      <div>
        <Explore value={inputValue}
                 onChange={this.handleChange} />
        <hr />
        {this.renderErrorMessage()}
        {children}
      </div>
    )
  }
  ...
}
// container/Explore.js内

export default class Explore extends Component {
  ...
  handleGoClick() {
    this.props.onChange(this.getInputValue())
  },
  ...
}

ページ遷移後のデータ取得

処理の流れは下記のようなイメージ。

  • URLからアカウント名 or リポジトリ名を取得して、github apiをcallするアクションを実行
  • api callを処理するmiddlewareでアクションを type:REQUEST, type:SUCCESS or FAILURE に分割したアクションを生成
  • SUCCESSを処理するreducerからstateを生成

URLからアカウント名 or リポジトリ名を取得して、github apiをcallするアクションを実行

redux-routerの機能で、URLに応じて、読み込むコンポーネントを切り分けて、さらにURLからコンポーネントへ渡すPropsを定義している。

// ./routes.js
export default (
  <Route path="/" component={App}>
    <Route path="/:login/:name"
           component={RepoPage} />
    <Route path="/:login"
           component={UserPage} />
  </Route>
)

この記述により、例えばUserPageの場合、mapStateToProps内で、 アカウント名を取得することが可能

// containers/UserPage.js

...

function mapStateToProps(state, ownProps) {
  ... 
  const login = ownProps.params.login.toLowerCase() // アカウント名を取得
  ...
}

UserPage#componentWillMount でアカウント名、Github APIからデータを取得するアクションをdispatchする

// containers/UserPage.js

...
import { loadUser, loadStarred } from '../actions'
...

function loadData(props) {
  const { login } = props
  props.loadUser(login, [ 'name' ])
  props.loadStarred(login)
}

...

class UserPage extends Component {
  ...
  componentWillMount() {
    loadData(this.props)
  }
  ...
}

...

export default connect(mapStateToProps, {
  loadUser,
  loadStarred
})(UserPage)

CALL API アクションのdispatch

上記のUserPageコンポーネントでimportされる loadUser の定義.

// actions/index.js

...

function fetchUser(login) {
  return {
    [CALL_API]: {
      types: [ USER_REQUEST, USER_SUCCESS, USER_FAILURE ],
      endpoint: `users/${login}`,
      schema: Schemas.USER
    }
  }
}

// Fetches a single user from Github API unless it is cached.
// Relies on Redux Thunk middleware.
export function loadUser(login, requiredFields = []) {
  return (dispatch, getState) => {
    const user = getState().entities.users[login]
    if (user && requiredFields.every(key => user.hasOwnProperty(key))) {
      return null
    }

    return dispatch(fetchUser(login))
  }
}

...

fetchUserで定義されているフォーマットは、

GitHub - agraboso/redux-api-middleware: Redux middleware for calling an API.

で定義されているAPI CALLを行うアクションのフォーマット。

アクションを処理するmiddleware

storeにmiddlewareを設定することで、特定のフォーマットのアクションへの処理を追加することができる。

例えば

GitHub - agraboso/redux-api-middleware: Redux middleware for calling an API.

を導入すると、上記のAPI CALL フォーマットのアクションを適切に処理してくれるようになる。

今回のexampleでは.middleware/api.js に自前で実装されている。

やっていることは

  • actionが対象のフォーマットかどうかを確認. 対象でなければ何もしないで終了
  • REQUEST typeのアクションを実行
  • api call後にSUCCESS or FAILURE typeのアクションをレスポンスをくっつけて実行

感想

stateの持ち方はかなりシンプルにして、middlewareやcomponentのcomponentWillMount, componentWillReceivePropsを活用することで、複雑な実装をクリアにしている感じが参考になった。

redux/examples/shopping_cart を見て学ぶ

redux/examples/shopping-cart at master · reactjs/redux · GitHub

仕様

  • 商品リストとカートがあり、商品をカートに入れることができる

  • 商品は在庫があり、在庫がなくなると、sold out と表示される

  • カートでは合計金額を表示する

state間の連携について

stateとして、商品情報のproductsとカート情報の cartがある。

カートでは商品IDと個数を保持している。

カートで合計金額と取得したい時に、state:cart内には金額の情報がないので、productsから商品の情報を取得する必要がある。

こういった処理をどこに、どうやって書いていくかというのがreduxでよく解っていないポイント。

今回の例では, reducers/index.js に getTotal関数が実装されていて、exportされ、コンポーネントのmapStateToPropsから呼び出されている。

// reducers/index.js内の合計金額を取得する実装

function getQuantity(state, id) {
  return fromCart.getQuantity(state.cart, id)
}

function getProduct(state, id) {
  return fromProducts.getProduct(state.products, id)
}

export function getTotal(state) {
  return getAddedIds(state).reduce((total, id) =>
    total + getProduct(state, id).price * getQuantity(state, id),
    0
  ).toFixed(2)
}

reducerを単純にアクションからstateを生成する役割に加えて、stateからデータを取得する役割を負っている。

複数のstateをまたがったデータを取得する際は、index.jsに定義し、各子stateから必要な情報を取得し、調整して返すようにされている。

この程度の規模のプログラムだと問題なさそうだけど、もう少し大きくなってくるとindex.jsがカオスになりそうなので、もう少し工夫する必要がありそう。

stateの持ち方の工夫

state:productsが単純な配列ではなく、下記のように定義されている。

{
  byId: {}, // id => product のオブジェクト。 state.byId[product_id] で商品データにアクセスできる
  visibilityIds: [] // 商品IDのリスト
}

これは一長一短ありそう。

感想

基本理念以外の部分は結構自由に実装されている印象。

FWに従って思考停止ではなく、作るプログラムに合わせて色々考えて工夫する必要がありそう。

Command pattern - Design Pattern in Ruby

「何かを実行するもの」をオブジェクトとして扱う方法。

これによる恩恵として、実行の記録の保管が容易になり、undo, redoなどの実装もしやすくなる。

よく使用されるケースとしては、GUIのパーツで、パーツのデザインや配置などの処理と、何かしらのアクションに応じた処理を切り離して管理することができる。

compositeパターンの考え方を用いてCompositeCommandを実装すると、複数のコマンドを単一のコマンドと同じように扱うことができる。

簡易実装

ボードにピースを配置するという簡単なプログラムで、ピースの追加、移動等の操作をCommandとして実装した。

class Board
  attr_accessor :pieces
  def initialize (size)
    @pieces = Array.new size
    @commands = []
    @command_index = 0
  end

  def execute_command(command)
    puts command.describe
    command.execute
    display_state
    @commands[@command_index] = command
    @command_index += 1
  end

  def undo
    puts 'Undo:'
    command = @commands[@command_index - 1]
    puts "\t" + command.describe
    command.unexecute
    display_state
    @command_index -= 1
  end

  def display_state
    state = '|'
    @pieces.each do |ele|
      if ele
        state << ele.name
      else
        state << '  '
      end
      state << '|'
    end
    puts state
  end
end

class Piece
  attr_accessor :name
  def initialize(name)
    @name = name
  end
end

class CompositeCommand
  def initialize
    @commands = []
  end

  def add_command (command)
    @commands << command
  end

  def execute
    @commands.each {|c| c.execute}
  end

  def unexecute
    @commands.reverse.each {|c| c.unexecute}
  end

  def describe
    @commands.map{|c| c.describe}.join "\n"
  end
end

class AddPieceCommand
  def initialize (piece, index, board)
    @piece = piece
    @index = index
    @board = board
    @previousPiece
  end

  def execute
    @previousPiece = @board.pieces[@index]
    @board.pieces[@index] = @piece
  end

  def unexecute
    @board.pieces[@index] = @previousPiece
  end

  def describe
    "Add Piece: #{@piece.name} to #{@index}"
  end
end

board = Board.new 8

initial_command = CompositeCommand.new
initial_command.add_command AddPieceCommand.new Piece.new('A'), 2, board
initial_command.add_command AddPieceCommand.new Piece.new('B'), 4, board

board.execute_command initial_command
# Add Piece: A to 2
# Add Piece: B to 4
# |  |  |A|  |B|  |  |  |

board.undo
# Undo:
# Add Piece: A to 2
# Add Piece: B to 4
# |  |  |  |  |  |  |  |  |

ActionController#respond_to - code reading

requestのMIME TYPEに応じたレスポンスを定義する機能

定義は

rails/mime_responds.rb at 48f140cf7459c963a54637c897448b959dbbfd26 · rails/rails · GitHub

ActionController:: MimeResponds#respond_to

def respond_to(*mimes)
      raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given?

      collector = Collector.new(mimes, request.variant)
      yield collector if block_given?

      if format = collector.negotiate_format(request)
        _process_format(format)
        _set_rendered_content_type format
        response = collector.response
        response.call if response
      else
        raise ActionController::UnknownFormat
      end
    end

Collectorを生成して、blockが与えられている場合は、collector を yield させる。

なので、よくある下記のようなコードのformatの中身はCollectorのインスタンス

respond_to do |format|
      if @kihu.save
        format.html { redirect_to @kihu, notice: 'Kihu was successfully created.' }
        format.json { render :show, status: :created, location: @kihu }
      else
        format.html { render :new }
        format.json { render json: @kihu.errors, status: :unprocessable_entity }
      end
    end

与えられたブロックの処理を通した後に、negotiate_formatで返信すべきMIMEタイプを取得し、

collector.responseでレスポンスを生成するProcを取得し、実行している。

ActionController:: MimeResponds::Collector

いわゆるformatの実態で、respond_toで渡されるブロックで定義されるMIMEタイプごとの処理を保持する。

format.html を呼び出した場合、includeされているAbstractController::Collectorで定義されているmethod_missingを通して、下記の処理が呼び出され、mimeタイプの名前のメソッドが作成され、実行される。

# AbstractController::Collector. generate_method_for_mimeを実行し、呼び出している。

def method_missing(symbol, &block)
      unless mime_constant = Mime[symbol]
        raise NoMethodError, "To respond to a custom format, register it as a MIME type first: " \
          "http://guides.rubyonrails.org/action_controller_overview.html#restful-downloads. " \
          "If you meant to respond to a variant like :tablet or :phone, not a custom format, " \
          "be sure to nest your variant response within a format response: " \
          "format.html { |html| html.tablet { ... } }"
      end

      if Mime::SET.include?(mime_constant)
        AbstractController::Collector.generate_method_for_mime(mime_constant)
        send(symbol, &block)
      else
        super
      end
    end

rails/collector.rb at 565094a8b5cdfa158fef6ae75252fd98a4ba8fe4 · rails/rails · GitHub

def self.generate_method_for_mime(mime)
      sym = mime.is_a?(Symbol) ? mime : mime.to_sym
      class_eval <<-RUBY, __FILE__, __LINE__ + 1
        def #{sym}(*args, &block)
          custom(Mime[:#{sym}], *args, &block)
        end
      RUBY
    end

customを実行するメソッドを定義している。

custom内では、@responses[mime_type]にレスポンス処理をするProcを保管。

blockが与えられていない場合、VariantCollectorを生成して保管しているが、この辺りはよくわからないので後回し。

def custom(mime_type, &block)
        mime_type = Mime::Type.lookup(mime_type.to_s) unless mime_type.is_a?(Mime::Type)
        @responses[mime_type] ||= if block_given?
          block
        else
          VariantCollector.new(@variant)
        end
      end

上記で保持されたProcが、#responseで返され、#respond_to内で呼び出される。

Iterator pattern - Design Pattern in Ruby

複数の要素を持つオブジェクトが、それぞれの要素に順繰りにアクセスする方法を提供するパターン。

  • External iterator

    has_next, next_item のような順繰りに必要なメソッドを提供することで、外部からの要素へのアクセスを可能にする。

    loop処理などを自分で書く必要があるが、その分柔軟な記述が可能

  • Internal iterator

    要素を引数にもったコードブロックを渡して、内部で各要素付きで実行してもらう。

    簡潔に処理したいコードを書くことができる。

実装

eachを実装して、Enumerableをincludeするミニマムのコードを書いてみた。


class Player
  attr_reader :time
  def initialize (name, time)
    @name = name
    @time = time
  end

  def <=> (other)
    @time <=> other.time
  end
end

class PlayerGroup
  include Enumerable
  def initialize
    @players = []
  end

  def add_player (player)
    @players << player
  end

  def each (&block)
    @players.each {|p| yield p}
  end
end

pg = PlayerGroup.new
pg.add_player Player.new 'kiryuu', 10.0
pg.add_player Player.new 'bolt', 9.58
pg.add_player Player.new 'gatorin', 9.81

p pg.sort

Composite pattern - Design Pattern in Ruby Chapter 6

共通のオペレーションを行う要素と、その要素の集合体を同じように扱うことで、複雑な構造をシンプルに扱う。

用語

  • Component 共通のインターフェース

  • Leaf ミニマムの要素

  • Composite 要素の集合体

注意点

compositeとleaf,異なる属性のものを全く同じように扱うのは無理。

目的に沿って、うまく設計する必要がある。

実践

ViewCompnentをサイズを共通オペレーションとして持つような想定で実装.

class ViewComponent
  attr_accessor :name, :position
  def initialize(name, position)
    @name = name
    @position = position
  end
  def get_size
    {
      width: 0,
      height: 0
    }
  end
end

class Element < ViewComponent
  def get_size
    {
      width: 50,
      height: 100
    }
  end
end

class ViewContainer < ViewComponent
  def initialize(name, position)
    super(name, position)
    @children = []
  end

  def add_child(child)
    @children << child
  end

  def get_size
    width = 0
    height = 0
    @children.each do |child|
      w = child.position[:x] + child.get_size[:width]
      h = child.position[:y] + child.get_size[:height]
      width = w if w > width
      height = h if h > height
    end
    {
      width: width,
      height: height
    }
  end
end

box = ViewContainer.new 'Box', {x: 10, y: 10}
box.add_child Element.new 'ele1', {x:20, y: 20}
p box.get_size
# {:width=>70, :height=>120}

box.add_child Element.new 'ele2', {x:100, y: 200}
p box.get_size
#{:width=>150, :height=>300}

Observer Pattern - Design Pattern i Ruby Chapter 5

注意点

  • 更新頻度を考える。 考えなしにすべての変更時にobserverオブジェクトに通知すると、かなりの数の冗長な通知が飛び交ってしまう可能性がある。 どういったタミングで通知を行うかの戦略は考えておくべき。

  • 変更の一貫性 上記の高新頻度にも似ているが、細かい情報の変更を全て通知していると、整合性が取れなくなるケースが考えられる。 どういった変更のまとまりで通知するかを考えておく必要がある。

バリエーション

ruby標準のobserver

module Observable (Ruby 1.8.7)

updateメソッドを実装したobserverオブジェクトを定義することで簡単に実装できる。


コードベースのobserver

observerオブジェクトにupdateメソッドを実装したクラスのオブジェクトではなく、Procオブジェクトを使うことで、より簡単にObserverオブジェクトを登録できる。


updateメソッドの引数

一番簡単な実装はシンプルにselfを渡す実装だけど、この場合はobserverがobservableオブジェクトの内容から変更箇所を特定するような処理が必要になったりする。

こういったことが問題になるようなケースでは、変更箇所ごとに細かいupdate_***系のメソッドを用意するといい。

ただこの場合だと標準のobserverモジュールが使えないため、自分でmoduleを定義する必要がある。

実践

NewsFeedの通知を受け取るような想定で実装

require 'observer'

class NewsFeed
  include Observable
  def initialize
    @news = []
    @next_news_id = 1
  end

  def add_news(title)
    @news << {
      id: @next_news_id,
      title: title
    }
    @next_news_id += 1
    changed
  end

  def publish
    notify_observers self
  end

  def getNewsAfter(id)
    @news.select {|news| news[:id] > id}
  end
end

class NewsListener
  def initialize
    @last_received_news_id = 0
  end

  def update(news_feed)
    unread = news_feed.getNewsAfter @last_received_news_id
    @last_received_news_id = unread.last[:id]
    p unread
  end
end

news_feed = NewsFeed.new
news_feed.add_observer NewsListener.new

news_feed.add_news 'news1'
news_feed.publish

news_feed.add_news 'news2'
news_feed.add_news 'news3'
news_feed.publish