不格好エンジニア

wordpress.comから引っ越しました。

【本場でも通じるふりかえりテクニック】アンチパターンとその対策

本場ってどこやねん。。。

"改善MTG(ふりかえり)"あるある

プロジェクトの節目に行うふりかえりMTG、改善MTG
これって、盛り上がらなかったり、自然消滅してしまう事も多いですね。その原因となるアンチパターンや対策について考えてみました。
上からメセニストとしてではなく、自戒を込めて書いています。

ふりかえる必要性を感じているメンバーがいない

本当に、ふりかえる必要性を感じているメンバーがいないのであれば、そのチームは完全無欠で理想的な状態にあるのかもしれません。ただ、そんなチームあるのかな。。。

ふりかえった事がなくてキョドる

いきなり"ふりかえりMTGやりましょう"と言われたら、ほとんどの人がどうしていいかわからない。
ただ、ちゃんと探せば、やり方まとめてくれてる人がいるんですね。勉強しようっと。
下のリンクは、めっちゃわかりやすいです。

ふりかえり会のススメ(進捗会議編)
http://objectclub.jp/download/files/pf/RetrospectiveMeetingScenario.pdf

一度のみ振り返り

こういうものは、一度で効果が出る方がむしろ稀なので、繰り返さないと駄目ですね。そもそも一度だけ振り返っても効果が計測できないですし。
自戒を込めて書きました。

効果がない(改善案が生かされない、実行に移されない)

プロジェクトの最後にふりかえりを行ってしまうと、こうなりがちですね。
この手のMTGは、プロジェクトの途中、まさに問題に直面している過程で定期的に行う方が良さそうです。結果をどう生かすのか、まずメンバー間で共有してからMTGを始めなければいけないのだと、お仕事を通じて学びました。

どうやって進めればいいのかわからない

何となく改善MTGを導入すると、一人一人が反省点を口にして、何となく終わってしまう。未来につながるお話にならない。そこでKPT。「型」を導入する事で、進め方をまず共有する。

批判の応酬、責任のなすり付け合いになってしまった

これ、僕もやってしまった事があります。。。そもそも、うまくいっていないチームだと、こういう場を設けた時に、一気に不満が噴出する事があります。これは解決が難しそうですが。。。
まずMTGの始めに、"keep(これからも続けたい、良い取り組み)"を挙げて、自分たちの成長を認め合うこと。

あとは、"**のせい"という論調になりかけた時に、「自分ならどうする?」「これからはどうする?」と自責で、未来志向で考えられるように声がけを行う事でしょうか。不思議なもので、第三者がこういった声がけを行うだけで、ほとんどの発言者は無意識のうちに自責で考え、未来に向けた思考を行えるようになるものです。僕自身も、何度もこの言葉に救われた気がします。

言葉の持つ力を甘く見てはいけないですね。

また、不満を抱えているメンバーには、MTGが始まる前に個別にお話をして、ガス抜きをする事も重要かもしれません。

Keep飛ばし

いきなり"Problem(問題)"から言及して、みんなどよーんとする。お通夜MTGの幕開け。
対策は、まずMTGの始めに、"keep(これからも続けたい、良い取り組み)"を挙げて、自分たちの成長を認め合うこと。まずMTGの始めに、"keep(これからも続けたい、良い取り組み)"を挙げて、自分たちの成長を認め合うこと。大事な事なので、2度書きました。

メンバー大杉

聞いている時間の長いMTGは、集中力が薄れがち。メンバーをむしろ3,4人に絞った方が良いのかも。あとは、発言者の時間を強制的に区切るとか、ポストイットを使うとか。

ほとんどのメンバーが効果を感じていない or 楽しくない

では、なぜ、メンバーが効果を感じていないのか。効果が目に見えないから、もしくはフィードバックを受け取るまでのサイクルが長過ぎるからではないでしょうか。
効果が計測できて、フィードバックまでのサイクルを短くする事ができれば、やっているうちに楽しくなってくるはず。では、どうすれば可視化できるのか、フィードバックを短くできるのか。

考えられる解決策としては、このあたりでしょうか。

  1. 可視化できるものを目標にする。
  2. フィードバックサイクルが長過ぎるものは、短く分割して、短期間にフィードバックを得られるようにする。

なげっぱなし、言いっぱなしのTry

誰が何時、何時までに、何をどうする。などより具体的で明確な内容に落とし込む。場合によっては、実行されたかどうかの確認方法まで盛り込む。
。。。。というより、こういう風に具体的に落とし込めないものは諦めて、本当に出来そうなものだけ、3つだけTryする、というのが良いかも。
短期間で、そんなに5個も10個もTryしながら、改善活動できない気がする。

【参考】

Project Facilitation From Hiranabe


アート・オブ・アジャイル デベロップメント ―組織を成功に導くエクストリームプログラミング (THEORY/IN/PRACTICE)

仕事を楽しくする【AWSを使わない】はてぶの記事をサムネイル付きでJSON配信する3ステップ

はじめに

類似するネタは、オンライン上に色々な方が書いて下さっているのですが、"サムネイルはAWS S3に保存しよう!"的なネタが多く、VPS一台で処理を完結したい自分にとっては、そのものズバリの記事がありませんでした。需要があるかどうかわかりませんが、書いてみたいと思います。

おおまかに実装方針を立てると、このようになります。

  • はてぶのAPIから記事を収集する(XMLをフェッチしてパース)
  • はてぶの記事URLからサムネイルを生成してDBに保存(dragonflyもしくはcarrierwaveを用います。どちらでも可能です)
  • 記事とサムネイルをJSON形式で配信する

今回は主要部分を抜粋してご紹介します。ソースコードはこちらに公開しております。

tjnet/geeknews-rails · GitHub

(1)はてぶのAPIから記事を収集する(XMLをフェッチしてパース)

まずはとにかく記事データが必要ということで、railsを用いて記事を収集します。データ収集に利用したのは以下のコードです。

#coding: utf-8
require 'pp'
require 'net/http'
require 'uri'
require 'rexml/document'

module Tasks
  class ArticlePicker
    # initialize article data
    def self.execute
      ActiveRecord::Base.connection.execute("TRUNCATE TABLE articles")
  
      feed_list = Feed.all
      articles = []
  
      feed_list.each do |feed|
        next if feed.url.blank?
        articles = fetch_and_parse(feed.url)
        save_articles(articles, feed.category_id)
      end
    end
  
    # save articles
    def self.save_articles(articles, category_id)
      return if articles.empty?
      pp '--'
      pp Time.now
  
      articles.each do |article|
      #pp sprintf('save %s of %d', article['title'], category_id)
      pp sprintf('save %s of %d', article['link'], category_id)

        Article.create!(
          :category_id => category_id,
          :title => article['title'], 
          :link => article['link'], 
          :description => article['description'], 
        )

      end
    end
  
    # return parsed articles
    def self.fetch_and_parse(url)
      res = fetch(url)
      articles = xml_to_array(res)
      pp articles
      articles
    end
  
  
    # fetch response from url
    def self.fetch(url)
      pp URI.escape(url)
  
      uri = URI.parse URI.escape(url)
      res = Net::HTTP.get uri
  
      res
    end
  
    # return parsed XML
    def self.xml_to_array(xml)
      items_rx = []
      doc = REXML::Document.new xml
      doc.elements.each('//item') do |e|
        i = Hash.new
        i["title"] = e.elements['title'].text
        i["link"] = e.elements['link'].text
        i["description"] = e.elements['description'].text
        items_rx << i
      end
  
      return items_rx
  
    end
  
  end
end

リトライ処理などは、別のスクリプトに切り出しています。

    begin
      pp "updating article ..."
      Tasks::ArticlePicker.execute
    rescue Exception => e
      pp "[ERROR] update_article #{e.message}"
      retry_count += 1
      retry if retry_count <= 5
      pp "retried 5 times so exit"
      exit
    end
    pp "updated article"

(2)はてぶの記事URLからサムネイルを生成してDBに保存(dragonflyもしくはcarrierwaveを用います。どちらでも可能です)

URLからサムネイルを作成するためにwkhtmltoimageを実行しています。
DBへの保存はcarrierwaveを用いています。

   # Generate and assign an image or set a validation error
    begin
      tempfile = temp_thumbnail_path
      cmd = "wkhtmltoimage --quality 40 --width 50 --height 50 \"#{self.link}\" \"#{tempfile}\""
         p "*** grabbing thumbnail: #{cmd}"
      system(cmd) # sometimes returns false even if image was saved
      
      self.photo = File.new(tempfile) # will throw if not saved
      self.save
    rescue => e
         p "*** thumbnail error: #{e}"
      self.errors.add(:base, "Cannot generate thumbnail. Is your URL valid?")
    ensure
    end

DBへのデータ保存が終われば、wkhtmltoimageで生成した一時ファイルは不要となりますので、modelのコールバックで後始末します。

 #Cleanup temp files when we are done
  after_save :cleanup_temp_thumbnail
   
  # Cleanup the temporary thumbnail image
  def cleanup_temp_thumbnail
    File.delete(temp_thumbnail_path) rescue 0
  end

(3)記事とサムネイルをJSON形式で配信する

ここまででDBに画像データを保存できました。後は、これをcontrollerからJSONとして配信します。

class Api::V1::ArticleController < ApplicationController
 # return articles
 def index
   render json: Article.list(request)
 end
end
 # 
 #= return articles as array of hash
 #
 def self.list(request)
   res = Array.new
   Article.all.each do |article|
     res.push({
       :category_id => article.category_id,
       :title => article.title,
       :link => article.link,
       :description => article.description,
       :image_url =>(article.photo.blank?) ? '' :  "#{request.protocol}#{request.host_with_port}#{article.photo.url}",
     })
   end
   res
 end

記事URLを生成するために、controllerからrequestを受け取っています。
もう少し簡潔に書けそうですが。。。

はまりどころ

うまく動かないときは、このあたりをチェックしてみて下さい。

  • サムネイルを一時的に保存するディレクトリのパーミッションなど
  • public/以下のディレクトリ構造
  • 本番環境だけで動かないときは、デプロイ設定やrackサーバの再起動が正常になされているか

TODO

次は、このあたりに取り組みたいですね。

  • cronの設定
  • サムネイルのクオリティを向上させる
  • 静的ファイルの配信を高速化

【VPSか、AWSか、Herokuか】結局、最後はさくらVPS+Unicorn+Rails+Capistranoに行き着いた【構築スクリプト付き】

お仕事以外にも色々手を出しています。
主に知り合いの方とサービス作ってみて頓挫したり、細々とiPhoneアプリを開発してみたり、、まったり作っていきたいと思います。

サーバを用意する際の選択肢について

自分のサービスやアプリを公開するとなった場合、どうしてもサーバが必要になります(僕の場合、自前のAPIと連携しないアプリって何となくモチベーションが湧かないのです。もちろん、既存のAPIを叩くだけでも、アプリを作成する事は可能ですが、はてなブックマークの記事を取得するだけのアプリでも、後からレコメンド機能を実装したり、記事を独自アルゴリズムで分類したり、、、やってみたいですよね。)

現実的な選択肢

みなさん、どんな観点でプラットフォームを選択されてるんでしょう。
ここでは、3つの観点から考えてみました。

  • 学習コスト
  • 技術的制約
  • ランニングコスト

学習コスト

まずAWSは、AWSそのものの作法を覚えなければならない為、(ベースになるLinuxの知識+AWSの知識)が必要となり、学習コストは最も高そうです。
さくらのVPSに関しては、色んな方が構築方法を公開されていて情報量も多く、ピュアなLinux単体の上に構築していくため、AWSと比較すれば、把握する範囲は比較的小さくなります。一定の水準でセキュリティを保つ設定が出来ていれば、なんとかなりそうですね。流れとしては、いったんは手動でLAMP環境を作ってはサーバを初期化して、、、といった事を繰り返してみるのが勉強になった気がしています。SSHの設定を間違えてログインできなくなり、泣く泣くサーバを初期化したのも、今となっては良い思い出。さすがに今は忙しくなって来たので、Ansibleで構築手順を半自動化しています。

AWSVPSでサービス開発を行う場合、手間がかかりそうな所といえば、デプロイ環境の構築でしょうか。手元のMacで開発=>本番のさくらVPSにデプロイ、といった構成にしたい場合は、自分でこれを設定しなければなりません。

セキュリティの設定、デプロイツールのセットアップをすっとばしたい、という場合はHerokuでしょうか。
デプロイに関しては、Herokuは圧倒的に簡単です。
手元のMacで開発したプロジェクト配下で"git push heroku master"とかやれば、デプロイ完了です。最も初速の出やすい開発プラットフォームではないでしょうか。

技術的制約

今回は、ニュースアプリの作成を予定しているため、サーバにサムネイルを一時保存したかったのですが、Heroku単体ではそれができない為、色々検証した結果、Herokuは選択肢から外しました。
次にAWS。スタートアップ界隈では実質業界標準になってきている(気がする)AWSには、正直何でも搭載されている感があります。スケールさせやすかったり、Herokuで出来ないファイル保存ができたり、、。ただ、趣味の開発でここまで必要なのか。。と言われると正直必要ない場面も多そうですね。色々有りすぎるために、把握しないといけない範囲が増えて手が止まるのは嫌ですね。
さくらのVPSは、AWSみたいにポチポチインスタンス増やしてスケールさせたりは出来ません。ただ、ピュアなLinuxであり、root権限も付与されているため、1台にNginx+(Unicorn+Rails)+MySQL全部盛り、みたいな構成にするならVPSで十分ですね。

ランニングコスト

ちょっとした検証用のWebアプリケーションであれば、Herokuで十分な事もあるかもしれません。
ただ、DBは無料で使用できるレコード件数に上限があったり、重めのバッチ処理を走らせると課金されてたり、意外と制約が多いな、、、という印象です。というより、少し課金体系がわかりにくいです。
また、AWSに関しては、「いつのまにか結構課金されとる。。」という状態に陥りがちなので、サービスがスケールされる道筋が見えていない現状では選択肢から外しました(インスタンス立ち上げっぱなしで忘れてた自分が悪いのですが)。
さくらのVPSは月額定額なので、わかりやすさでは一番ですね。

開発環境/本番環境を構築する上での前提

  • 手元のMacが壊れても、開発環境を再現できる
  • 本番環境(さくらVPS)の設定はすべてAnsibleを通す

というあたりを意識して作りました。個人開発なら、Ansibleで自動化するのが、おすすめですよ。

開発環境

Ansibleを用いてvagrant上に(CentOS+Unicorn+Rails)の環境を構築します。サーバサイドの開発はVagrant上で完結します。
こちらの記事なども、参考になりそうです^^

【5分で学べる】Vagrant上にRailsをAnsibleでかんたんクッキング(CentOS6, MySQL, Rails4, Unicorn) - 不格好エンジニア (引っ越しました)

サーバ設定などのアップデート

開発環境/本番環境を問わず、すべてAnsibleを通して行っています。実際にCentOS+MySQL+Unicorn+Railsを導入するための構築スクリプトは、こちらに公開しています。

tjnet/vagrant_sakuravps_rails · GitHub

本番へのデプロイ

Railsプロジェクトのリポジトリにデプロイスクリプトを入れています。具体的にはCapistrano3(capistrano3-unicorn)を導入しています。
実際にはAnsibleでもデプロイは出来るのですが、Capistranoの方がRailsとの相性は良さそうです。staginやproductionといった複数環境へのデプロイに対応していたり、bundle、migration、unicornとの連携も考慮されており、使いやすいと思います。

作成中のサービスにCapistranoを導入した事例はこちら(config/deploy.rbのあたりは見よう見まねで、おかしい所が多々あるかもしれません)。


tjnet/geeknews-rails · GitHub


デプロイ設定をGitHubで公開するかどうかは、少し悩みました。ただ、ローカル(vagrant)のHostsファイルに本番環境のIPアドレスを記述するようにすれば、具体的なサーバの情報をGitHubに公開してしまう事もありません。アドバイスを頂けるチャンスもあるかもしれないですし、こんな情報でも誰かの役に立つかもしれないので、積極的にシェアしていきたいと考えています。

かかるコスト

かかるコストですが、シンプルに使ったサーバ台数x使用したVPSの月額料金です。

今後やりたいこと

  • CI環境の構築
  • TDD (後からでもテスト書こう)
  • 監視ツールの導入

今の構成がベストではないと思うので、まだまだ改善したいと思います。日々、勉強ですね!

【怠惰な】Swiftを学ぶにあたって最適なオープンソース【Objective-C経験者向け】

筆者のスペック

業務やプライベートプロジェクトでObjective-C を1年半ほど、経験しました。最近、サンプルコードやライブラリもSwiftを用いたものが増えて来て、少し焦っています。Swift勉強しなくちゃ。

困っていること

チュートリアルをチマチマ進めていくのが性に合わない

Railsチュートリアルは非常によく出来ていて、一連のストーリーが有ったので、進めていくのは苦痛ではありませんでした。ただ、Swiftに関しては、既にObjective-Cを経験したエンジニアであれば、いきなり小規模なアプリのソースコードをリファレンス引きながら読み進めて行く方が理解が早そうですね。

共有されているサンプルアプリが色々動かなくなっている

Swiftが発表され話題を集めた頃、ギークな方達が、たくさんのサンプルアプリをGitHubに晒して下さいました。ところが、これらのGitHubで共有されているコードがXcode6.1で動作しない事がよくあります。

学習に適したアプリ

とりあえず、いくつか試してみて、筆者の環境(Xcode6.1)で動作確認できたソースはこちら。


amitburst/HackerNews · GitHub

HackerNewsは、Objective-Cの教材としてもお世話になったプロダクトです。

Newsアプリは、内部動作も把握しやすいと思います。業務に応用できる部分も多いかと思いますので、こちらを読み進めていきます。

ちょっと読んでみた


AppDelegate.swiftに目を通してみます。

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    
    // MARK: Properties
    
    var window: UIWindow?

    // MARK: UIApplicationDelegate
    
    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: NSDictionary?) -> Bool {
        return true
    }

}

@UIApplicationMainの@は意味がわかりませんが、いったん後回しにしても良さそうです。

    var window: UIWindow?

この部分はプロパティ宣言ですね。後ろについている?は、nilを許容するかどうかを明示的に示しているそうです。このあたり、後で調べておきます。

    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: NSDictionary?) -> Bool {
        return true
    }

この部分は、下のObjective-Cソースコードと比較すれば、何となく理解できますね。問題なさそう。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    return YES;
}

AppDelegateは、何となくわかりました。
この調子で、読み進めていけそうです。

【あの有名サービスが】コードリーディングで学ぶAngularJS【丸裸に】

コードリーディング

AngularJS初心者です。
コードリーディングという形で、AngularJSで実装されたWebサービスソースコードを題材に使って、その動きや機能がどのように実装されているのかを、ソースコードのレベルで解析していこうと思います。

※時間のある時に、随時追記していきます。

題材「FMTube 」について

コードリーディングの題材とするのはこちら。
ゆーすけべーさんが、AngularJSの勉強中に作成したサービスだそうです。

FMTube!

アーティストの名前を入れると、楽曲リストを順番に再生してくれます。
素晴らしいです。勉強させて頂きます。

ソースコード

ソースコードですが、Webサービスとして公開しているので、当然、Webブラウザから確認することができます。
メインロジックは、こちらに記述されています。

http://yusukebe.github.io/FMTube/js/app.js

参考資料

正直、全然わからない。。。この辺りを参考にしながら進めていきます。

初心者向けAngularJS - その1 - albatrosary's blog

Service, Factory, Providerが何の事か分からない方はこちら。

AngularJs Service,Factory,Providerなどなど - senta.me/blog

エントリポイント

var app = angular.module('fmtube', ['ng']);

ここでは、このアプリケーションを初期化して、依存するモジュールを登録します。

view

次に、view部分をざっと眺めます。
#content, #listあたりに注目すれば処理内容を把握できそうです。

<html ng-app="fmtube" ng-controller="controller" class="ng-scope"><head><style type="text/css">@charset "UTF-8";[ng\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide{display:none !important;}ng\:form{display:block;}.ng-animate-start{border-spacing:1px 1px;-ms-zoom:1.0001;}.ng-animate-active{border-spacing:0px 0px;-ms-zoom:1;}</style>
    <meta charset="utf-8">
    <title ng-bind="title" class="ng-binding">Eis essen by GReeen - FMTube!</title>
    <link href="css/bootstrap.min.css" rel="stylesheet">
    <link href="css/typeahead.js-bootstrap.css" rel="stylesheet">
    <link href="css/app.css" rel="stylesheet">
    <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
    <!--[if lt IE 9]>
      <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
      <script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
    <![endif]-->
    <script id="www-widgetapi-script" src="https://s.ytimg.com/yts/jsbin/www-widgetapi-vflAiKzF-/www-widgetapi.js" async=""></script><script src="http://www.youtube.com/iframe_api"></script><script id="twitter-wjs" src="http://platform.twitter.com/widgets.js"></script><script async="" src="//www.google-analytics.com/analytics.js"></script><script src="js/jquery.js"></script>
    <script src="js/typeahead.min.js"></script>
    <script src="js/bootstrap.min.js"></script>
    <script src="js/angular.min.js"></script>
    <script src="js/angular-resource.min.js"></script>
    <script src="js/app.js"></script>
    <script>
      (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
      (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
      m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
      })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
      ga('create', 'UA-492497-67', 'yusukebe.github.io');
      ga('send', 'pageview');
    </script>
  </head>
  <body style="" data-twttr-rendered="true">
    <div class="container">
      <div id="header" class="clearfix">
        <h1><a href="http://yusukebe.github.io/FMTube/">FMTube!</a></h1>
      </div>
      <!-- /header -->
      <div class="row">
        <div id="form" class="col-md-4">
          <form role="form" ng-submit="submit(true)" class="ng-valid ng-dirty">
            <div class="form-group">
              <span class="twitter-typeahead ng-valid ng-dirty" style="position: relative; display: inline-block; direction: ltr;"><input class="tt-hint" type="text" autocomplete="off" spellcheck="off" disabled="" style="position: absolute; top: 0px; left: 0px; border-color: transparent; box-shadow: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgb(255, 255, 255);"><input type="text" class="form-control typeahead tt-query" placeholder="Artist name" ng-model="artist" autocomplete="off" spellcheck="false" dir="auto" style="position: relative; vertical-align: top; background-color: transparent;"><span style="position: absolute; left: -9999px; visibility: hidden; white-space: nowrap; font-family: 'Alegreya Sans', sans-serif; font-size: 14.2857141494751px; font-style: normal; font-variant: normal; font-weight: 400; word-spacing: 0px; letter-spacing: 0px; text-indent: 0px; text-rendering: auto; text-transform: none;">GReeen</span><span class="tt-dropdown-menu" style="position: absolute; top: 100%; left: 0px; z-index: 100; display: none; right: auto;"><div class="tt-dataset-artist" style="display: block;"><span class="tt-suggestions" style="display: block;"><div class="tt-suggestion" style="white-space: nowrap; cursor: pointer;"><p style="white-space: normal;">Greeen Linez</p></div><div class="tt-suggestion" style="white-space: nowrap; cursor: pointer;"><p style="white-space: normal;">GReeen</p></div><div class="tt-suggestion" style="white-space: nowrap; cursor: pointer;"><p style="white-space: normal;">Greeen</p></div><div class="tt-suggestion" style="white-space: nowrap; cursor: pointer;"><p style="white-space: normal;">Dr.Greeen</p></div><div class="tt-suggestion" style="white-space: nowrap; cursor: pointer;"><p style="white-space: normal;">Al Greeen</p></div></span></div></span></span>
              <button type="submit" class="btn btn-default"><b>Play!</b></button>
            </div>
          </form>
        </div>
      </div>
      <div id="social-buttons">
        <span style="margin-right:4px;">
          <iframe class="hatena-bookmark-button-frame" title="このエントリーをはてなブックマークに追加" frameborder="0" scrolling="no" width="130" height="20" src="javascript:false" style="width: 111px; height: 20px;"></iframe><script type="text/javascript" src="http://b.st-hatena.com/js/bookmark_button.js" charset="utf-8" async="async"></script>
        </span>
        <iframe id="twitter-widget-0" scrolling="no" frameborder="0" allowtransparency="true" src="http://platform.twitter.com/widgets/tweet_button.d58098f8a7f0ff5a206e7f15442a6b30.en.html#_=1416154421769&amp;count=horizontal&amp;id=twitter-widget-0&amp;lang=en&amp;original_referer=http%3A%2F%2Fyusukebe.github.io%2FFMTube%2Findex.html&amp;size=m&amp;text=FMTube!&amp;url=http%3A%2F%2Fyusukebe.github.io%2FFMTube%2F" class="twitter-share-button twitter-tweet-button twitter-share-button twitter-count-horizontal" title="Twitter Tweet Button" data-twttr-rendered="true" style="width: 109px; height: 20px;"></iframe>
        <script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+'://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document, 'script', 'twitter-wjs');</script>
        <span style="margin-left:-20px;">
          <iframe src="//www.facebook.com/plugins/like.php?href=http%3A%2F%2Fyusukebe.github.io%2FFMTube%2F&amp;width=150&amp;layout=button_count&amp;action=like&amp;show_faces=true&amp;share=true&amp;height=21&amp;appId=640667512644377" scrolling="no" frameborder="0" style="border:none; overflow:hidden; height:21px; width:150px;" allowtransparency="true"></iframe>
        </span>
      </div>

      <hr>

      <div class="row">
        <div id="content">
          <iframe id="player" frameborder="0" allowfullscreen="1" title="YouTube video player" width="600" height="400" src="https://www.youtube.com/embed/5sveP1gSXGA?autoplay=1&amp;rel=0&amp;enablejsapi=1&amp;origin=http%3A%2F%2Fyusukebe.github.io"></iframe>
        </div>
        <div id="list">
          
          <!-- ngRepeat: track in tracks --><div class="list-item ng-scope" ng-repeat="track in tracks">
            <a class="clearfix list-active" ng-click="click($index)" ng-class="active_class($index)">
              <img ng-src="" class="pull-left">
              <b class="pull-left ng-binding">Eis essen</b>
            </a>
          </div><!-- end ngRepeat: track in tracks --><div class="list-item ng-scope" ng-repeat="track in tracks">
            <a class="clearfix" ng-click="click($index)" ng-class="active_class($index)">
              <img ng-src="http://userserve-ak.last.fm/serve/34s/97541019.jpg" class="pull-left" src="http://userserve-ak.last.fm/serve/34s/97541019.jpg">
              <b class="pull-left ng-binding">Gesundes Ego</b>
            </a>
          </div><!-- end ngRepeat: track in tracks --><div class="list-item ng-scope" ng-repeat="track in tracks">
            <a class="clearfix" ng-click="click($index)" ng-class="active_class($index)">
              <img ng-src="" class="pull-left">
              <b class="pull-left ng-binding">Eis essen - JuliensBlogContest</b>
            </a>
          </div><!-- end ngRepeat: track in tracks --><div class="list-item ng-scope" ng-repeat="track in tracks">
            <a class="clearfix" ng-click="click($index)" ng-class="active_class($index)">
              <img ng-src="http://userserve-ak.last.fm/serve/34s/92538915.jpg" class="pull-left" src="http://userserve-ak.last.fm/serve/34s/92538915.jpg">
              <b class="pull-left ng-binding">Ein gesundes Ego</b>
            </a>
          </div><!-- end ngRepeat: track in tracks --><div class="list-item ng-scope" ng-repeat="track in tracks">
            <a class="clearfix" ng-click="click($index)" ng-class="active_class($index)">
              <img ng-src="http://userserve-ak.last.fm/serve/34s/92538915.jpg" class="pull-left" src="http://userserve-ak.last.fm/serve/34s/92538915.jpg">
              <b class="pull-left ng-binding">Nur das Beste</b>
            </a>
          </div><!-- end ngRepeat: track in tracks --><div class="list-item ng-scope" ng-repeat="track in tracks">
            <a class="clearfix" ng-click="click($index)" ng-class="active_class($index)">
              <img ng-src="http://userserve-ak.last.fm/serve/34s/92538915.jpg" class="pull-left" src="http://userserve-ak.last.fm/serve/34s/92538915.jpg">
              <b class="pull-left ng-binding">Die Blüte meiner Selbst</b>
            </a>
          </div><!-- end ngRepeat: track in tracks --><div class="list-item ng-scope" ng-repeat="track in tracks">
            <a class="clearfix" ng-click="click($index)" ng-class="active_class($index)">
              <img ng-src="http://userserve-ak.last.fm/serve/34s/92538915.jpg" class="pull-left" src="http://userserve-ak.last.fm/serve/34s/92538915.jpg">
              <b class="pull-left ng-binding">Blauer Planet</b>
            </a>
          </div><!-- end ngRepeat: track in tracks --><div class="list-item ng-scope" ng-repeat="track in tracks">
            <a class="clearfix" ng-click="click($index)" ng-class="active_class($index)">
              <img ng-src="" class="pull-left">
              <b class="pull-left ng-binding">JottBeBe Qualifikation</b>
            </a>
          </div><!-- end ngRepeat: track in tracks --><div class="list-item ng-scope" ng-repeat="track in tracks">
            <a class="clearfix" ng-click="click($index)" ng-class="active_class($index)">
              <img ng-src="http://userserve-ak.last.fm/serve/34s/92538915.jpg" class="pull-left" src="http://userserve-ak.last.fm/serve/34s/92538915.jpg">
              <b class="pull-left ng-binding">Hovercraft</b>
            </a>
          </div><!-- end ngRepeat: track in tracks --><div class="list-item ng-scope" ng-repeat="track in tracks">
            <a class="clearfix" ng-click="click($index)" ng-class="active_class($index)">
              <img ng-src="" class="pull-left">
              <b class="pull-left ng-binding">Lächeln Exklusiv</b>
            </a>
          </div><!-- end ngRepeat: track in tracks --><div class="list-item ng-scope" ng-repeat="track in tracks">
            <a class="clearfix" ng-click="click($index)" ng-class="active_class($index)">
              <img ng-src="" class="pull-left">
              <b class="pull-left ng-binding">Hippie 2.0</b>
            </a>
          </div><!-- end ngRepeat: track in tracks --><div class="list-item ng-scope" ng-repeat="track in tracks">
            <a class="clearfix" ng-click="click($index)" ng-class="active_class($index)">
              <img ng-src="" class="pull-left">
              <b class="pull-left ng-binding">Scheiss doch einfach drauf (feat. Der Rote)</b>
            </a>
          </div><!-- end ngRepeat: track in tracks --><div class="list-item ng-scope" ng-repeat="track in tracks">
            <a class="clearfix" ng-click="click($index)" ng-class="active_class($index)">
              <img ng-src="" class="pull-left">
              <b class="pull-left ng-binding">Hab Wörter für dich</b>
            </a>
          </div><!-- end ngRepeat: track in tracks --><div class="list-item ng-scope" ng-repeat="track in tracks">
            <a class="clearfix" ng-click="click($index)" ng-class="active_class($index)">
              <img ng-src="" class="pull-left">
              <b class="pull-left ng-binding">JottBeBe Winin Battle</b>
            </a>
          </div><!-- end ngRepeat: track in tracks --><div class="list-item ng-scope" ng-repeat="track in tracks">
            <a class="clearfix" ng-click="click($index)" ng-class="active_class($index)">
              <img ng-src="" class="pull-left">
              <b class="pull-left ng-binding">Vaubetee Quali</b>
            </a>
          </div><!-- end ngRepeat: track in tracks --><div class="list-item ng-scope" ng-repeat="track in tracks">
            <a class="clearfix" ng-click="click($index)" ng-class="active_class($index)">
              <img ng-src="" class="pull-left">
              <b class="pull-left ng-binding">Pinselstrich für die Ohren</b>
            </a>
          </div><!-- end ngRepeat: track in tracks --><div class="list-item ng-scope" ng-repeat="track in tracks">
            <a class="clearfix" ng-click="click($index)" ng-class="active_class($index)">
              <img ng-src="" class="pull-left">
              <b class="pull-left ng-binding">Abseits der Norm</b>
            </a>
          </div><!-- end ngRepeat: track in tracks --><div class="list-item ng-scope" ng-repeat="track in tracks">
            <a class="clearfix" ng-click="click($index)" ng-class="active_class($index)">
              <img ng-src="" class="pull-left">
              <b class="pull-left ng-binding">Ich bin hier</b>
            </a>
          </div><!-- end ngRepeat: track in tracks --><div class="list-item ng-scope" ng-repeat="track in tracks">
            <a class="clearfix" ng-click="click($index)" ng-class="active_class($index)">
              <img ng-src="" class="pull-left">
              <b class="pull-left ng-binding">Frauen</b>
            </a>
          </div><!-- end ngRepeat: track in tracks --><div class="list-item ng-scope" ng-repeat="track in tracks">
            <a class="clearfix" ng-click="click($index)" ng-class="active_class($index)">
              <img ng-src="" class="pull-left">
              <b class="pull-left ng-binding">VauBeTee Qualifikation - Instrumental</b>
            </a>
          </div><!-- end ngRepeat: track in tracks -->
        </div>
      </div>
      
      <hr>
      <div id="footer">
        <address>
          © <a href="http://yusukebe.github.io/">yusukebe</a>,
          <b>FMTube</b> using Last.fm Web API, YouTube API, jQuery, AngularJS, Twitter Bootstrap and GitHub!
        </address>
      </div>
    </div>
</body></html>

run

またjs/app.jsに戻って、処理内容を確認します。

app.run(function(){
  var tag = document.createElement('script');
  tag.src = "http://www.youtube.com/iframe_api";
  var first_tag = document.getElementsByTagName('script')[0];
  first_tag.parentNode.insertBefore(tag, first_tag);
});

runって何ですか。。。?
StackOverflowさんによると、constantsやinstanceはrunブロックでしか注入できないらしい。

[参考]

breeze - AngularJS app.run() documentation? - Stack Overflow

Factory, Service, Providerについて

Service,Value,Factory,Provider ってなんぞ?そこについて前提知識がないと、以降のコードを理解するのは難しそうです。

先ず、サービスとは…
Angularサービスはシングルトンオブジェクト、またはWebアプリケーション共通の特定のタスクを実行する関数です。 Angularは、サーバにリクエストを送るブラウザのXMLHttpRequestオブジェクトにアクセスする$httpサービスのような、 いくつかの組み込みサービスを持ちます。 他のコアなAngular変数と識別子と同様に、組み込みのサービスは常に$から始まります。(前述した$httpのように) また、独自のカスタムサービスを作ることも可能です。
とのこと。 (http://js.studio-kingdom.com/angularjs/guide/understanding_services)
このサービスをDIに登録する方法が複数あり、大分とまどいました。

[参考]
AngularJs Service,Factory,Providerなどなど - senta.me/blog

DI

この際、ぶっちゃけると「DIとか何それおいしいの?」な人もたくさんいらっしゃる事でしょう。
僕もその1人です。こちらを読むと何となくわかった気になれます。


要するに DI って何なのという話 - 猫型の蓄音機は 1 分間に 45 回にゃあと鳴く


明日から、ドヤ顔で新人君に説明できますね。どや!

今日はここまで。また追記します。。。

Unicorn(Rackサーバ)+Nginx(リバースプロキシ)の構成が多用されている理由について、調べてみた話

使ってますか? Unicorn

Unicornは本番環境用に広く使われているRackサーバの一つです。位置づけとしてはMongrelやThinに近いものです。お仕事ではPhusion Passengerを使用していますが、今回はUnicornについて学んだ事をまとめてみました。

Unicornの特徴

  • 高速なクライアントからのレスポンスに対し、短時間の処理をすばやく返す
  • 運用コストが低い(Rainbows!やPumaと比較して枯れており、ノウハウが充実している)

ここでいう「高速なクライアント」とは、帯域をめいっぱい使えるクライアントの事です。
例えば、LAN上もしくは同一ホスト上のクライアントを指しています。
比較して、「低速なクライアント」とは、スマートフォンなど低速なネットワークを利用しているクライアントのことです。

Unicornアーキテクチャ

Unicornのmasterプロセスはリクエストを処理する為、設定に応じた数のworkerをforkします。
1つのworkerプロセスは、同時に複数リクエストを処理する事はできません。
そのため、低速なクライアントからのリクエストは、プロセスを長時間占有してしまうという問題が発生します。

Unicorn(Rackサーバ)+Nginx(リバースプロキシ)の構成が多用されている理由

先ほど書いた様に、低速なクライアントからのリクエストを単体で処理するのには不向きなアーキテクチャですが、Nginxをリバースプロキシとして用いる事でこの問題を解決できます。

具体的には、レスポンスデータが出来上がったら、クライアントへのデータ送信を(Unicornではなく)リバースプロキシから行います。
リバースプロキシにレスポンス送信を委譲する事によって、workerは別のリクエスト処理に移る事ができます。

これが、UnicornをRackサーバとして使用する際に、Nginxなどのリバースプロキシを併用する理由の一つだと思われます。このあたりについては、WEB+DB PRESS Vol.83|技術評論社が詳しいです。


【参考】
Everything You Need to Know About Unicorn
https://blog.engineyard.com/2010/everything-you-need-to-know-about-unicorn

サービス特性に合ったRackサーバを選ぼう
http://gihyo.jp/magazine/wdpress/archive/2014/vol83

「納品をなくせばうまくいく」を読みました

「納品」をなくせばうまくいく ソフトウェア業界の“常識

※旧ブログからの転載記事です。

購入のきっかけ
IT業界の課題、開発トレンドやエンジニアの働き方に対する問題意識から「納品のない受託開発」というビジネスモデルを提唱された倉貫さんの著書です。

受託開発専業のSIerから転身して、ここ2年ほど、Webサービス/モバイルアプリをコア事業とするチームで勤務しております。
倉貫氏は受託開発、私は自社サービス開発と、ややドメインは異なりますが、「開発、リリースして終わりではなく、リリース後も改善が必要となる」という意味では、参考にできる部分も多いのではないかと思い、購入しました。

メモ

覚えておきたい部分をいくつかメモしておきたいと思います。

・扱う技術を統一化する

例えば、WebアプリケーションのフレームワークRails、サーバはAWS上に構築する、と決定してしまいます。こうする事で、ノウハウの共有や社内での助け合いも容易になりそうです。

個々のエンジニアにとって考えてみると、「学習コストを下げる事により、より深いレベルに、より早く到達できる」為、メリットの大きい方法ではないでしょうか。なお、この視点は航空業界のLCCを参考にしたものだそうです。

・幅広いスキルを備えたエンジニアが要件定義から開発・運用まで兼務する

ソニックガーデンのエンジニアは顧客との対話、設計、プログラミング、サーバ運用まで全てをこなします。これにより伝言ゲームをなくし、無駄を最小限にします。オフショアやコストメリットの大きい新興国のエンジニアに対抗するヒントになる考え方だと思います。

今後、エンジニアとして生き残るには、ワンストップで要件定義から運用までこなせる、1人でやる事でコミュニケーションコストを最小化できる事がポイントになりそうです。

・自動化と合理化

機械に出来る事は機械にさせましょう、という基本を徹底されているようです。弊社でもまだまだ人力に頼る部分が多くあり、これからも業務改善を進めて行こうと気持ちを新たにしました。

ただ、「自動化による開発/運用工数の削減」というのはその効果が見えにくい場合もあり、どうしても「機能追加、新規プロダクトの開発」が優先されやすい傾向にあります。「業務改善のための工数」をどうやって確保するか、これは現在の僕にとっては非常に難しい問題です。このあたりは近道はなく、対話を繰り返す事、個々の取り組みによるビジネスインパクトを可視化していく事が重要だと考えています。

・「属人性の排除」より、「人を大事にする」

個人的に最も感銘を受けたのが、この部分です。1人のエンジニアが担当する領域が増えるほど、実は1人のエンジニアに対する依存度は高くなります。この点に関しては、細かいノウハウはあるにしても、マネジメントや技術的な対策よりも、長く一緒に働ける関係性を維持する方が健全だと述べられています。

エンジニアの退職リスクに対する対応って、小手先の方法論で語られる事が多くて、「そもそも人が簡単に辞めないようにしよう」という方向性で語られる事って少なかったように思います。こういう姿勢は大事ですね。。。

感想
このビジネスモデルを提唱するに至った背景が、ご自身の言葉で丁寧に描かれています。
倉貫さん自身がエンジニアでもあり、共感できる部分が多くありました。