言語化する朧げな

Baby steps to Giant strides!

TCPのウィンドウ制御とコネクション識別

ウィンドウ制御

TCPは送信と応答確認によってデータ送信の信頼性を高めているが、パケット毎にそれをやっていると効率が悪い。

この問題を解決するために、ウィンドウ制御という概念を取り入れている。

具体的には、応答確認をする単位を1パケットではないく、ある一定サイズ(ウィンドウサイズ)ごとに行うようにすることで、通信を効率化している。

サイズの目安

  • 1パケット 1.5KB (イーサネットの最大パケットサイズ)

  • ウィンドウサイズの最大 64KB このサイズをスケールさせる規約もあり、近代のOSは対応しているらしい(気が向いたら調べる)

ASCII.jp:帯域を効率的に利用するTCPの仕組みとは? (1/2)|TCP/IPまるわかり

コネクション識別

TCPヘッダには接続IDのフィールドがないので、どうやっているのか気になったので調べてみた。

2つのendpointのIP:portで識別している。

The TCP/IP Guide - TCP Ports, Connections and Connection Identification

initialPropsとstate - React.js

Props in getInitialState Is an Anti-Pattern | React

getInitialState内でpropsを用いてstateを生成するやり方は、どのデータが本筋のデータかがわかりづらくなってしまう状況になってしまうことがあるので、注意して使ったほうがいい。

コメントフォームを例に考えてみる。

var CommentBox = React.createClass({
  getInitialState: function(){
    return {
      comment: this.props.comment
    }
  },
  handleChange: function(e) {
    this.setState({comment: e.target.value})
  },
  render: function() {
    return <div className='comment-box'>
      <textarea 
        onChange={this.handleChange}
        defaultValue={this.state.comment} />
    </div>
  }
})

上記のようなコードの場合、props.commentとstate.commentがあり、コードの複雑性が増してくると、どちらがどのケースで本筋のデータかがわかりづらくなってしまう可能性がある。

なので、下記のようにpropsは明示的に初期化用のデータということを表す名前にしたほうが良い。

var CommentBox = React.createClass({
  getInitialState: function(){
    return {
      comment: this.props.initialComment
    }
  },
  handleChange: function(e) {
    this.setState({comment: e.target.value})
  },
  render: function() {
    return <div className='comment-box'>
      <textarea 
        onChange={this.handleChange}
        defaultValue={this.state.comment} />
      <button onClick={this.handleUpdateClick}
    </div>
  }
})

Model周り - helpy code reading

共通部分を切り出す

CakePHPでいうBehaviorやComponent, Helper

Rails 共通部分を切り出す (View, Controller, Model) メモ - Qiita

関連したモデルのフォームでの扱い

Rails ネストした関連先のテーブルもまとめて保存する (accepts_nested_attributes_for、fields_for) - Qiita

helpyではtopicが複数のpostを持つ事ができる構造で、topic.rb内で 'accepts_nested_attributes_for :posts' と記述し、

viewでは下記のように定義している。

<%= f.simple_fields_for :posts do |p| %>
      <%= p.input :body, input_html: {:rows => 8, :cols => 60, placeholder: I18n.t(:how_can_we_help), label: 'Message', class: 'disable-empty form-control topic-placeholder'} %>
    <% end %>

model attachments

こういったプラグインはどのMVCフレームワークでも頻繁にお世話になりそう。

GitHub - assembler/attachinary: Attachments handler for Rails that uses Cloudinary for storage.

postgresに特化にした検索用のGem

GitHub - Casecommons/pg_search: pg_search builds ActiveRecord named scopes that take advantage of PostgreSQL’s full text search

# モデル内で下記ように定義すると
include PgSearch
pg_search_scope :admin_search,
                  against: [:id, :name, :user_name, :current_status, :post_cache]

# 下記のように呼び出せる
 @topics = Topic.admin_search(params[:q]).page params[:page]

polymorphicな関連

helpyではtopicsとpostsに投票機能が付いていて、どちらもvoteモデルの関連で実現されている。

# vote.rb

class Vote < ActiveRecord::Base

  belongs_to :voteable, :polymorphic => true

  ...

# topic.rb
class Topic < ActiveRecord::Base

  ...
  
  has_many :votes, :as => :voteable

ログイン後の初期設定 - helpy code reading

github.com

初期ログイン後に表示される初期設定画面周りのコードを見ていく。

流れ

ログイン後のリダイレクト

admin's root admin/dashboard#index to admin/topics#index

def index
    #@topics = Topic.mine(current_user.id).pending.page params[:page]

    if current_user.is_admin? || current_user.is_agent?
      redirect_to admin_topics_path
    elsif current_user.is_editor?
      redirect_to admin_categories_path
    else
      redirect_to root_url
    end
  end

ダイアログの表示

初期設定 = onboardingというキーワードで表現されていて、済みかの判定は下記のメソッドで定義されている。

# Admin::BaseController
def show_onboarding?
    User.first.email == 'admin@test.com' && current_user.email == 'admin@test.com' && current_user.is_admin?
  end

上記のメソッドをhelper_methodに登録して、html側で呼び出している。

# views/admin/topics/index.html.erb
<% if show_onboarding? %>
<div id='modal' class="modal" tabindex="-1" role="dialog" data-backdrop="static">
  <div class="modal-dialog modal-lg">
    <div class="modal-content">
      <iframe src="/admin/onboarding/index" width="100%" height="900" frameborder="no" scrolling="no"></iframe>
    </div>
  </div>
</div>
<script>
// Fire the onboarding modal if needed
$('#modal').modal('toggle');
</script>
<% end %>

onboarding modal

上記のmodal内のiframeに設定されているところを見ていく。

登録のステップとして、メッセージ表示、サービス設定、アカウント設定がある。

htmlは全てonboarding/index.html.erbに記載されていて、class onboard-panel を遷移させていく形で実装してある。

panel遷移のJS処理は assets/javascripts/admin.js に描かれている

var Helpy = Helpy || {};

...

Helpy.showPanel = function(panel) {
  var currentPanel = panel-1;
  $('.onboard-panel').addClass('hidden');
  $('#panel-' + panel).removeClass('hidden');
  $('li.step-' + currentPanel).html("<span class='glyphicon glyphicon-ok'></span>").addClass('filled-circle');
  $('li.step-' + panel).addClass('active-step');
  return true;
};

...

サービス設定のフォームはAjax化されている。

# onboarding/index.html.erb
...

<div id="panel-2" class="onboard-panel text-center hidden">
  <div class="onboard-message">
    <h2>Great! Lets Customize your Helpdesk:</h2>
  </div>
  <div class="text-left">
  <%= bootstrap_form_tag url: admin_onboard_settings_path, method: 'patch', remote: true do |f| %>

...

コントローラー側は下記のようになっている。

# admin/onboarding_controoer.rb

def update_settings

    設定保存処理

    respond_to do |format|
      format.html { redirect_to(admin_settings_path) }
      format.js {
          render js: "Helpy.showPanel(3);$('#edit_user_1').enableClientSideValidations();"
      }
    end
  end

アカウント設定のフォームはAjaxではなくて、onboarding#update_user で設定保存後にonboarding#complete リダイレクトして動作完了。

参考になる箇所

Decorator Pattern - Design Pattern in Ruby

あるオブジェクトの機能に追加の機能を付け足したい、

さらにいろいろな組み合わせで追加機能を実装したいという時に使えるパターン。

Interface

元の機能を持ったオブジェクト(ConcreteComponent)と追加機能を実装するオブジェクト(Decorator)という登場人物がいて、同じインターフェース(Component)を持つことで、クライアントからは実際にはどのオブジェクトを扱っているかを気にせずに使用できるようになる。

Traditional implementation

DecoratorオブジェクトにConcreteComponentオブジェクトの参照を渡し、追加機能以外の部分をConcreteComponentに委譲する。

Easing the Delegation

forwardableモジュールをextendすることで、delegateの処理を簡潔に書くことができる。

In Ruby

Wrapping Methods

w = SimpleWriter.new('out')

class << w
  alias old_write_line write_line
  def write_line(line)
    old_write_line("#{Time.new}: #{line}")
  end
end

元のメソッドの参照をaliasキーワードで old**に移して、old**をdecorateするメソッドを新たに定義する。

ちょっとした処理を簡単に追加できるが、複数のdecorate処理を買いたい時に、aliasに設定する名前が難しくなってくる。


Decorating with Modules

instance.extend(module) の方法でインタンスにmoduleを設定することで実現する。

簡単なテキストデコレーションを書いてみた。

class TextPrinter
  def output(str)
    print str
  end
end

module TextEmphasizeDecorator
  def output(str)
    print "** "
    print super(str)
    print " **"
  end
end

module TextBracketsDecorator
  def output(str)
    print '('
    super str
    print ')'
  end
end

to = TextPrinter.new
to.extend(TextEmphasizeDecorator)
to.extend(TextBracketsDecorator)

to.output('hoge')

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