ブロックについてのまとめ

Rubyを触り始めて早一年が経ちました。
やっと、ブロックつきメソッドを定義し、実装することができました。
そこで、Rubyのブロックに付いて簡単にまとめたいと思います。


参考

たのしいRuby 第3版

たのしいRuby 第3版

ブロックとは

  • 「do 〜 end」や「{}」で囲まれた部分のこと
hoge.fuga do ブロック end
hoge.fuga{ブロック}

たとえば、

sum = 0
ary = [1, 2, 3, 4]
ary.each{|i| sum += i}
p sum
# => 10
ブロックつきメソッド
  • ブロックを引数としてとるメソッド
  • 先ほど例で言うとeachがこれにあたる
  • ブロックつきメソッドにブロックを引数として渡して実行することをブロックつき呼び出しという
ブロックつき呼び出し
  • ブロックつき呼び出しの一般系
# *args => 引数の並び
# block_variables => ブロック変数の並び

obj.method(*args) do |block_variables|
  # ブロック
end

obj.method(*args){|block_variables|
  # ブロック
}
  • メソッドの動作時にブロックが実行される

イテレータ

ブロックつき呼び出しの中で、繰り返しを行うためのイテレータについての説明

配列の場合
sum = 0
ary = [1, 2, 3, 4]
ary.each{|i| sum += i}
  • 配列「ary」の一要素がブロック変数「i」に代入され、ブロック「sum += i」が評価させる
ハッシュの場合
sum = 0
hash = {:a => 1, :b => 2, :c => 3, :d => 4}
hash.each{|k, v| sum += v}
  • ハッシュ「hash」のキーとバリューがブロック変数「k」「v」に代入され、ブロック「sum += v」が評価される

ブロックつきメソッドを定義

自分で、ブロックつきメソッドを定義してみる

def hoge
  yield
end

def fuga
  hoge{p "ブロック呼び出し"}
end

fuga
#=> "ブロック呼び出し"
  • yieldに呼び出し元で指定したブロックが渡される
  • つまり、hogeがブロックつきメソッド


ブロック引数がある場合のメソッド

def hoge
  yield "ブロック呼び出し"
end

def fuga
  hoge{|str| p str}
end

fuga
#=> "ブロック呼び出し"
  • yieldの引数が、ブロック変数へ代入され、ブロックが実行される
使用例

あるメソッドを定義したけど、そのメソッド内の処理を呼び出し元に依存させたい時にブロックを使いました。
使いどころは、まだまだ分かっていませんが、使えるとかなり便利だと感じました。


次はこれで勉強します。

Rubyによるデザインパターン

Rubyによるデザインパターン


それでは、Proc.newとかlamdaとかprocに関してはまた今度。


最後までお読みいただきありがとうございました。

ページネーションでハマったと思ったら、findの問題だった

開発をしていて、ページネーションでハマったと思って、原因を探してみたら、findの仕様だったということがあったのでメモ

環境

  • rails1.2.3

データ構造

  • リレーション
class User < ActiveRecord::Base
  has_many :diaries
end

class Diary < ActiveRecord::Base;
  belongs_to :user
end

事象

やりたかったこと
  • 一度でも日記を書いたことのあるユーザーを日記をincludeして取得する
作戦
  • INNER JOINを利用して、diariesを持っていないユーザーを取得しないでおく
@pages, @users = paginate(
  :user,
  :joins => "INNER JOIN diaries dummy ON dummy.user_id = users.id",
  :include => :diaries,
  :per_page => 5
)

# @pages.item_count #=> 15
# @users.size #=> 3

意図したとおりにレコードが取得できるはず!と思っていたのですが、なぜか、@users.sizeの値がper_pageに満たない。

paginateメソッドを追ってみる

rails1.2.3なので、ActionControllerにPaginationが標準実装されている
結局、paginateメソッドは以下のことをやっていだだけだった

    # File actionpack/lib/action_controller/pagination.rb, line 182
182:     def find_collection_for_pagination(model, options, paginator)
183:       model.find(:all, :conditions => option[:conditions],
184:                  :order => options[:order_by] || options[:order],
185:                  :joins => options[:join] || options[:joins], :include => options[:include],
186:                  :select => options[:select], :limit => options[:per_page],
187:                  :offset => paginator.current.offset)
188:     end
 

findを呼び出している以外に特別なことは何もしていない。
つまり、今回の事象は、findの仕様によるものだったのだ。

findメソッドを追ってみる

以下、重要なメソッドだけを記載する

     # File activerecord/lib/active_record/base.rb, line 411
 411:       def find(*args)
 412:         options = extract_options_from_args!(args)
 413:         validate_find_options(options)
 414:         set_readonly_option!(options)
 415:
 416:         case args.first
 417:           when :first then find_initial(options)
 418:           when :all   then find_every(options)
 419:           else             find_from_ids(args, options)
 420:         end
 421:       end

今回、paginateメソッドでは、find(:all)が実行されているので、find_everyメソッドをみる

     # File activerecord/lib/active_record/base.rb, line 994
 994:         def find_every(options)
 995:           records = scoped?(:find, :include) || options[:include] ?
 996:             find_with_associations(options) :
 997:             find_by_sql(construct_finder_sql(options))
 998: 
 999:           records.each { |record| record.readonly! } if options[:readonly]
1000: 
1001:           records
1002:         end

scopeはかかっていないが、includeは指定しているので、find_with_associationsメソッドをみる

     # File activerecord/lib/active_record/associations.rb, line 1020
1020:         def find_with_associations(options = {})
1021:           catch :invalid_query do
1022:             join_dependency = JoinDependency.new(self, merge_includes(scope(:find, :include), options[:include]), options[:joins])
1023:             rows = select_all_rows(options, join_dependency)
1024:             return join_dependency.instantiate(rows)
1025:           end
1026:           []
1027:         end

JoinDependencyのインスタンスであるjoin_dependencyでJOIN句に入れるべきものを制御している
そして、select_all_rowsメソッドの中で、construct_finder_sql_with_included_associationsが呼び出されている


このメソッドで、DBへ投げるSQLを生成している
以前紹介したjoins オプションとinclude オプションの決定的な違い - mic_footprintsのincludeの処理もこの当たりで対応している

     # File activerecord/lib/active_record/associations.rb, line 1172
1172:         def construct_finder_sql_with_included_associations(options, join_dependency)
1173:           scope = scope(:find)
1174:           sql = "SELECT #{column_aliases(join_dependency)} FROM #{(scope && scope[:from]) || options[:from] || table_name} "
1175:           sql << join_dependency.join_associations.collect{|join| join.association_join }.join
1176:  
1177:           add_joins!(sql, options, scope)
1178:           add_conditions!(sql, options[:conditions], scope)
1179:           add_limited_ids_condition!(sql, options, join_dependency) if !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit])
1180: 
1181:           sql << "GROUP BY #{options[:group]} " if options[:group]
1182:  
1183:           add_order!(sql, options[:order], scope)
1184:           add_limit!(sql, options, scope) if using_limitable_reflections?(join_dependency.reflections)
1185:           add_lock!(sql, options, scope)
1186:  
1187:           return sanitize_sql(sql)
1188:         end

paginateメソッドが生成したlimitオプションがあるので、add_limited_ids_condition!が実行され、一度、該当するidが取得される
その取得されたidを基に、アソシエーションするべくクエリが生成される
結論から言うと、この取得するidに問題があった


今回のハマりポイントに到着!!

     # File activerecord/lib/active_record/associations.rb, line 1172
1190:         def add_limited_ids_condition!(sql, options, join_dependency)
1191:           unless (id_list = select_limited_ids_list(options, join_dependency)).empty?
1192:             sql << "#{condition_word(sql)} #{table_name}.#{primary_key} IN (#{id_list}) "
1193:           else
1194:             throw :invalid_query
1195:           end
1196:         end

結局、問題は上記メソッド内で実行されるselect_limited_ids_listメソッドにあった!

  • 正確に言うと、もっと奥のメソッドだけど

add_limited_ids_condition!自体は、select_limited_ids_listメソッドの返り値(idの配列)を元にSQL文を生成しているに過ぎない


select_limited_ids_listメソッド内で以下が実行される

     # File activerecord/lib/active_record/associations.rb, line 1205
1205:         def construct_finder_sql_for_association_limiting(options, join_dependency)
1206:           scope       = scope(:find)
1207:           is_distinct = include_eager_conditions?(options) || include_eager_order?(options)
1208:           sql = "SELECT "
1209:           if is_distinct
1210:             sql << connection.distinct("#{table_name}.#{primary_key}", options[:order])
1211:           else
1212:             sql << primary_key
1213:           end
1214:           sql << " FROM #{table_name} "
1215:             
1216:           if is_distinct
1217:             sql << join_dependency.join_associations.collect(&:association_join).join
1218:             add_joins!(sql, options, scope)
1219:           end
1220:             
1221:           add_conditions!(sql, options[:conditions], scope)
1222:           if options[:order]
1223:             if is_distinct
1224:               connection.add_order_by_for_association_limiting!(sql, options)
1225:             else
1226:               sql << "ORDER BY #{options[:order]}"
1227:             end
1228:           end
1229:           add_limit!(sql, options, scope)
1230:           return sanitize_sql(sql)
1231:         end

注目は、1216行目のif文
これがあることで、今回の問題が生じた
1217, 1218で、JOIN句にjoinやincludeで指定したものを入れてくれている
が、is_distinct #=> falseなので、if内は回避される

include_eager_conditions?(options)
# WHEREに指定されるtableがJOIN句への挿入を必要としているか?

include_eager_order?(options)
# ORDER BYに指定されるtableがJOIN句への挿入を必要としているか?

今回の場合、どちらもfalseである


というわけで、JOIN句には何も挿入されないまま、idが取得される


そのあとで、idを元に、INNER JIONが指定されるので、結果としてlimitよりも少ない数が取得されてしまうことがある


今回の場合だと、以下の方法で実現が可能

@pages, @users = paginate(
  :user,
  :include => :diaries,
  :conditions => "diaries.id IS NOT NULL",
  :per_page => 5
)


この問題って、2.系以降だと変更されていそうな気もします。まだ追っていませんが。
というか、そもそも問題なのか。。。ご意見あれば是非!



最後までお読みいただきありがとうございます。

クラスとインスタンスのまとめ

Rubyをはじめたころの自分の記事が意外にも分かりやすかったので、改変して再掲載しますw
クラスメソッドとインスタンスメソッド - mic_footprints

☆クラスメソッド

クラスとは
  • クラスとは、オブジェクトの種類を表したもの
  • つまり、複数のオブジェクト(インスタンス)の枠組みと捕らえてよいでしょう
自然言語で言い表してみた

もっと分かりやすくするために、クラスが学級で、インスタンスが生徒とします
生徒(インスタンス)は漏れなく学級(クラス)に属しています

  • 学級(クラス)には、学級ごとのルールがある
    • 生徒(インスタンス)は、学級(クラス)のルールに従って行動(メソッド)を起こす
    • 生徒(インスタンス)は、このルールによって、行動(メソッド)が限定される
    • つまり、インスタンスは、クラスによって使えるメソッドが違うということ

クラスメソッドとは
  • クラスに直接働きかけるメソッドのこと
Student.find(1)
    • findメソッドはStudentクラスに直接働きかけている
    • つまり、クラスメソッド


学級と生徒で例えてみる

  • 状況
    • C組という学級(クラス)がある
    • C組には太郎君、花子さんがいる
    • つまり、太郎君、花子さんは、C組クラスのインスタンス(C組オブジェクト)
  • C組の太郎君を探すメソッドを呼び出す
C.sagasu("太郎君")
    • ここでのsagasuメソッドは、クラスメソッド
    • sagasuメソッドはC組に直接働きかけている


たとえば、C組の太郎君を廊下に立たせるためのメソッドというのはクラスメソッドではありません
これは、次で説明するインスタンスメソッドです

インスタンスメソッド

インスタンスとは
  • データを表現する基本的な単位
  • オブジェクトともいう
  • インスタンスには、様々な種類があり、それが先ほど説明したクラスということになる
インスタンスメソッドとは
["1","2","3"].index(1)


先ほど同様、学級と生徒で例えてみる

  • 太郎君を廊下に立たせるメソッド
# 変数taroに「C組クラスの太郎君」を代入
taro = C.sagasu("太郎君")

# もちろん、taroはC組クラスのインスタンスということになる

taro.rouka_ni_tataseru
    • ここでのrouka_ni_tataseruメソッドは、インスタンスメソッド
    • rouka_ni_tataseruメソッドはC組クラスのインスタンスであるtaroに直接働きかけている

☆メソッドとレシーバ

  • メソッド(method)が働きかける先をレシーバ(receiver)という
メソッド
  • オブジェクトに関連する操作を行う
  • Rubyでは、全ての操作がメソッドとして実装されている


メソッドの呼び出し

オブジェクト.メソッド名(第一引数, 第二引数, 第三引数, ・・・, 第N引数)
  • メソッドを実行するということは、「オブジェクトにメッセージを送る」ということ
  • 引数がある場合は、引数と共にメッセージが送られるというイメージ
レシーバ
  • メソッドから操作されるもの
  • つまり、「オブジェクトはメッセージを受け取る(receive)」ということ


クラスメソッドとインスタンスメソッドに於いて


個人的な感想ですが、レシーバを意識することで、オブジェクト指向の感覚がつかめた気がします。
プログラミングをはじめたばかりの方々に少しでも参考になれば幸いです。


たのしいRuby 第3版

たのしいRuby 第3版


最後まで、お読み頂きありがとうございました!

SQL改善によるパフォーマンス向上

事例

実際にアプリケーション開発を行っていて、検索機能を実装していたときに超絶重い検索ロジックに出会いました。
その対処をSQL文の改善で行なったので紹介します。

データ構造

mysql> DESCRIBE users;
+-------+-------------+------+-----+---------+----------------+
| Field | Type        | Null | Key | Default | Extra          |
+-------+-------------+------+-----+---------+----------------+
| id    | int(11)     |      | PRI | NULL    | auto_increment |
| name  | varchar(40) | YES  |     |         |                |
+-------+-------------+------+-----+---------+----------------+
2 rows in set (0.00 sec)

mysql> DESCRIBE diaries;
+------------+----------+------+-----+---------+----------------+
| Field      | Type     | Null | Key | Default | Extra          |
+------------+----------+------+-----+---------+----------------+
| id         | int(11)  |      | PRI | NULL    | auto_increment |
| user_id    | int(11)  |      | MUL | 0       |                |
| body       | text     | YES  |     |         |                |
| created_at | datetime | YES  |     |         |                |
+------------+----------+------+-----+---------+----------------+
3 rows in set (0.00 sec)

重いクエリ

  • ユーザーにとって一番最近書いた日記で、内容に「最新」が含まれる日記を書いたユーザーを取得する
SELECT users.*
FROM users
LEFT OUTER JOIN diaries ON diaries.user_id = users.id
WHERE
  users.id IN (
    SELECT diaries.user_id
    FROM (
      SELECT MAX(diaries.created_at) AS created_at, diaries.user_id
      FROM diaries
      GROUP BY diaries.user_id
    ) recent_diaries, diaries
    WHERE
      diaries.user_id = recent_diaries.user_id AND
      diaries.created_at = recent_diaries.created_at AND
      diaries.body LIKE '%最新%'
  );
やりたかった事
  1. ユーザーにとっての最新の日記群(recent_diaries)を作る
  2. そのrecent_diariesのuser_idとcreated_atが一致するdiariesのbodyに「最新」が含まれているdiariesのuser_idを取得する
    • recent_diariesはuser_idでGROUP BYしているので、意図したdiariesの情報が取得できない
      • この場合は、bodyの情報
    • 意味があるデータとして取得できるのは、user_idと最新のcreated_atのセットのみになる
    • ユーザーにとっての最新の日記を取得するためには、recent_diaries.user_idとrecent_diaries.created_atがdiaries.user_idとdiaries.created_atとが一致しているかを見ることによってのみ実現する
    • さらに、そのユーザーにとっての最新の日記のbodyに「最新」が含まれているかを見ることによって、意図した日記群が取得することが出来る
  3. 取得したuser_id群にusers.idが含まれているusersを取得する
    • 意図した日記群のuser_idをSELECTし、そのuser_id群にusers.idが含まれているかを見ることによって意図したユーザーを取得する

原因

  • 相関サブクエリを使用している
相関サブクエリとは( DEPENDENT SUBQUERY )
  • サブクエリにおいて外部クエリのカラムを参照しているサブクエリのこと
  • つまり、外部クエリのカラムの値と相関関係を持っているサブクエリのこと
    • 例えば、
SELECT users.id
FROM users
WHERE users.id = (
  SELECT diaries.user_id
  FROM diaries
  WHERE
    diaries.user_id = users.id AND
    diaries.body LIKE "%相関%"
  GROUP BY diaries.user_id
);

※相関サブクエリを使わなくても意図したレコードは取得できますが、あくまでも例としてあげています。

  • 外部クエリusers.id = ()に応じて、()内のDEPENDENT SUBQUERYが実行される
  • 基本的に、相関サブクエリは重いとされているが、インデックスを貼ることで、速度は大幅に改善される
    • この場合、diaries.user_idにindexを貼ることで速度改善が見込める
相関サブクエリはどこ?
  • 一見、問題のクエリに相関性は内容に見える
  • EXPLAINで出力
+----+--------------------+------------+-------+--------------------------------------------------------------+--------------------------+---------+---------------+-------+-------------+
| id | select_type        | table      | type  | possible_keys                                                | key                      | key_len | ref           | rows  | Extra       |
+----+--------------------+------------+-------+--------------------------------------------------------------+--------------------------+---------+---------------+-------+-------------+
|  1 | PRIMARY            | users      | ALL   | NULL                                                         | NULL                     |    NULL | NULL          | 23912 | Using where |
|  1 | PRIMARY            | diaries    | ref   | index_diaries_on_user_id,user_id                             | user_id                  |       4 | test.users.id |     1 |             |
|  2 | DEPENDENT SUBQUERY | <derived3> | ALL   | NULL                                                         | NULL                     |    NULL | NULL          |  3455 |             |
|  2 | DEPENDENT SUBQUERY | diaries    | ref   | index_diaries_on_created_at,index_diaries_on_user_id,user_id | index_diaries_on_user_id |       4 | func          |     1 | Using where |
|  3 | DERIVED            | diaries    | index | NULL                                                         | index_diaries_on_user_id |       4 | NULL          | 45561 | Using where |
+----+--------------------+------------+-------+--------------------------------------------------------------+--------------------------+---------+---------------+-------+-------------+
5 rows in set (2.95 sec)
  • 3行目と4行目、「DEPENDENT SUBQUERY」との記載がある
    • つまり、相関サブクエリが存在することなっている
  • ユーザーの気持ちとしては、users.id IN (DEPENDENT SUBQUERY)を以下の通りに処理してほしいはず
    • ()内をusers.idとの相関性を持たせるのではなく、()内のSUBQUERYを実行した結果を元にusers.id IN ()を評価する
  • しかし、実際はusers.idと相関を持ち、()内が評価されてしまう

現時点でのMySQL(バージョン5.1)では、サブクエリはまず外部クエリの条件から評価される。そして、外部クエリの条件に合致する行が見つかると、その行がサブクエリの条件に合致するかどうかが評価されるわけである。即ち、サブクエリにおいてフェッチしなければいけない行数が平均N行、外部クエリでフェッチされる行数がM行のとき、サブクエリにおいてM×N行の評価が行われることになる。これは膨大な計算量である。

参照:漢(オトコ)のコンピュータ道: なぜMySQLのサブクエリは遅いのか。

  • つまり、usersの数だけ()内のSUBQUERYが走ることになる
    • この場合、()内の処理単体で0.3秒かかっていた
    • 0.3sec * 23912 = 119.56min
    • つまり、単純計算で、最低でも2時間はかかるクエリとなってしまっていたのだ!

改善

先に、ユーザーにとっての最新の日記群を作ってしまう
  • usersの数だけ、最新の日記群を作るクエリが走ることが問題であれば、先にその日記群を作ってしまえば、毎回最新の日記群を作らなくて済むはず
SELECT users.*
FROM users, (
  SELECT MAX(diaries.created_at) AS created_at, diaries.user_id
  FROM diaries
  WHERE
    diaries.event_name NOT LIKE '%選考' AND
    diaries.user_impression IS NOT NULL
  GROUP BY diaries.user_id
) recent_diaries
LEFT OUTER JOIN diaries ON diaries.user_id = users.id
WHERE
  users.id IN (
    SELECT diaries.user_id
    FROM diaries
    WHERE
      diaries.created_at = latest_records.created_at AND
      diaries.user_id = latest_records.user_id AND
      diaries.body LIKE '%最新%'
  );
  • こうすることで、最新の日記群を作るクエリをusersの数だけ走らせない様にすることは実現できた
  • しかし、次の問題が生じた
  • EXPALINで見てみよう
+----+--------------------+------------+-------+--------------------------------------------------------------+--------------------------+---------+------------------------+-------+-------------+
| id | select_type        | table      | type  | possible_keys                                                | key                      | key_len | ref                    | rows  | Extra       |
+----+--------------------+------------+-------+--------------------------------------------------------------+--------------------------+---------+------------------------+-------+-------------+
|  1 | PRIMARY            | <derived2> | ALL   | NULL                                                         | NULL                     |    NULL | NULL                   |  3455 |             |
|  1 | PRIMARY            | users      | ALL   | NULL                                                         | NULL                     |    NULL | NULL                   | 23912 | Using where |
|  1 | PRIMARY            | diaries    | ref   | index_diaries_on_user_id,user_id                             | user_id                  |       4 | test.users.id          |     1 | Using index |
|  3 | DEPENDENT SUBQUERY | diaries    | ref   | index_diaries_on_created_at,index_diaries_on_user_id,user_id | index_diaries_on_user_id |       4 | recent_diaries.user_id |     1 | Using where |
|  2 | DERIVED            | diaries    | index | NULL                                                         | index_diaries_on_user_id |       4 | NULL                   | 45561 | Using where |
+----+--------------------+------------+-------+--------------------------------------------------------------+--------------------------+---------+------------------------+-------+-------------+
5 rows in set (0.26 sec)
  • 一行目のテーブルは、id=2のDERIVED(5行目)に由来するテーブルであることを示している
    • つまり、FROM句のサブクエリの結果のテーブル「recent_diaries」のことである
    • このようなDERIVEDに由来するようなテーブルを「テンポラリーテーブル」と呼ぶ
  • テンポラリーテーブルrecent_diariesのkeyはNULLである
    • つまり、indexが使えないのである!
  • テンポラリーテーブルはフルテーブルスキャンが走る!!
  • ・・・テンポラリーテーブルのレコード数が多いと、爆弾クエリになってしまうorz
別の切り口を考えてみる
  • 作戦
    • そもそも、ユーザーにとっての最新の日記は一つである
    • 相関サブクエリを利用して、ユーザーごとの最新の日記という条件を加える
    • さらに、日記のbodyに「最新」が含まれているという条件を加える
    • 上記の条件に当てはまった日記を持つ学生を取得する
SELECT users.*
FROM users
LEFT OUTER JOIN diaries ON diaries.user_id = users.id
WHERE
  diaries.id = (
    SELECT
      diaries.id
    FROM
      diaries
    WHERE
      diaries.user_id = users.id
    ORDER BY diaries.created_at DESC
    LIMIT 1
  ) AND
 diaries.body LIKE '%最新%';
  • うん!シンプルかつ高速なクエリができました!

まとめ

SQLを組むときは以下のことに注意したい

  • 意図しない相関サブクエリが生成されていないか?
  • 正確な場所にインデックスが貼られているか?
  • テンポラリーテーブルが巨大になっていないか?


ぐんぐん実力がつく! 逆算式SQL教科書

ぐんぐん実力がつく! 逆算式SQL教科書


以上です。
最後までお読みいただきありがとうございました。

開発のチーム体制についてまとめてみた

Wed+DB PRESS Vol.60でプロジェクト運用ノウハウについて学んだのでまとめました。

アウトプットを最大化するチームづくり

  • チームラボ(株)
チーム開発の重要性
  • 求められるアウトプットに対して最適なチームを運営する必要がある
    • チームメンバー個人個人がサービスの目的や、最適な開発フローを常に考えてることが重要
    • 一人一人のアウトプットの結晶が、プロジェクト全体のアウトプットにつながる
    • 個人個人のアウトプットを担保するのがチーム
    • チームとして品質を向上させることが必要
レビューの重要性
  • プロジェクトの方向性を疑うことを忘れないのが大切
    • プロジェクト内のみにとどまらない
    • プロジェクト間においても横断的に、かつ定期的にレビューを行う
  • レビューを活性化するために
    • プロジェクトに直接コミットしていない、異なるスペシャリストもレビューに含める
    • レビューはフラットな関係性で行う
チーム分けのパターン
  • スキルの高いメンバーだけで構成されている場合
    • 機能でチームを分ける
    • 各担当者が1つの機能の開発を担当
    • 設計から動作確認まで自分で全て行う
    • 開発の楽しさを感じられるチーム編成
  • メンバーのスキルにバラつきがある場合
    • システムの処理レイヤでチームを分ける
      • プレゼンテーションレイヤ
      • ビジネスレイヤ
      • DBレイヤ
      • etc.
    • スキルに応じてメンバを配置し、品質を保ちつつアウトプットを最大化する
    • チームを横断して一機能を開発
      • チーム単位で動作確認を行うことができない
      • 結合テスト時に問題が多発
      • 不具合の検知も後半になりがち
    • チームリーダー感での進捗の共有が重要
    • 各チーム間でのインターフェースを予め設計
    • 内部処理は各チームで開発
    • チーム内の設計は処理の共通化を意識する必要がある
チーム分けの際に同時に考えること
  • あくまでもチームはアウトプットへのツール
  • アウトプットにつながらなければ必要ないし、無駄
  • 仲の良いチームやお互いが成長できるチームを作りたくなるが、それは目的ではない
  • プロジェクト、チームの目的、成功の定義をしっかりと認識したうえで、チーム構成やメンバを配置する
  • 管理工数やコミュニケーション工数が増大しないようにチーム編成する
チームリーダー
  • チームリーダーはメンバの、最高のパフォーマンスと最高のアウトプットを担保する必要がある
  • ただし、その目的はプロジェクト全体のアウトプットに向けられているべきで、決してチーム内のアウトプットを担保するものではない
  • チームリーダーは、チーム内で技術的にもっとも優れている必要はない
  • チーム内で最もアウトプットに対して求めるクオリティの意識が高い人である
  • 全体のアウトプットの進捗や品質を意識しながら、自分のチームへとブレークダウン
  • 他のチームへ正確な情報を伝達することも求められる
  • チーム内の進捗の把握と共有は、リーダーが管理するためのものではなく、プロジェクト全体のアウトプットを最高のものにするために、正しい情報をリーダーたちと共有するために行うべき
  • プロジェクトのアウトプットを考えて、自分のチームが苦戦を強いられているときは即座にアラートを出す

アジャイルな見積りと計画づくり

  • (株)永和システムマネジメント
受託開発の難しさ
  • プロダクトは「百聞は一見にしかず」
  • プロジェクトは「一期一会」
見積もり
  • 近いものほど細かく、遠そうなものほど粗く見積もる
  • それぞれを相対的な大きさで見積もる
  • 見積もりはなるべく開発チームメンバー全員で行う
完了の定義
  • 完了の定義をお客様と開発者で合わせておくことが重要
  • 完了の定義をフィーチャ(プロダクトで実現したいことをユーザー視点で記述したもの)ごと、イテレーションごと、プロジェクトごとに設定することも重要

アジャイルUXDによる"試行錯誤"のプロセス化

  • ゼロベース(株)
UXD
  • ユーザーエクスペリエンスデザイン
設計と失敗
  • 作る前に考えすぎることなく、まず現物を作ってから考えること
  • なるべく早く、少しずつ、実際に動くものを作ること
  • 実験して「失敗」しても「学習」と捉える
    • 取り返しの付く小失敗は学習のために不可欠
  • その学習を短期間に何度も繰り返す
  • 「分からない」とこは「分からない」と認めることが重要
    • あらゆる願望は現実の前に無力
    • 現実に立脚することでしか、失敗を避けられない
    • 取り返しのつかない大失敗は、それが失敗だと気がつかずにどんどん進んでしまったときに発生する

不都合を生じる可能性があるものは、いずれ必ず不都合を生じる

マーフィーの法則

アジャイルUXDのプロセス
  1. 戦略
    • 何をしようとしているのか、利用者にどんな体験を提供しようとしているのかを共有
  2. スコープ
    • スコープ(要件リスト)を検討し、決定
    • 各メンバーのパフォーマンスを最大化するようなタスクの割り振りを行う
  3. 構造、骨格
    • 要件を実際に設計する際には、まず構造レベルでUIフローを考る
    • その後骨格レベルの「HTMLモック」を組む
  4. 表層
    • HTMLモックをもとに静的HTMLのテンプレートを作成
  5. テストとリリース
信頼とコミュニケーション
  • 新規事業の不確実なことが多いので、「やってみなければわからない」ことが多い
  • 細かい修正をやり直している暇があったら、そのままリリース
  • 利用者の反応を見ながら、別の要件を開発したほうがよい
  • このことにより、チーム全体のパフォーマンスを上げる
タスクベースの計画
  • 「何をすれば新規事業が成功するか」ということが事前に分かっていることはない
  • つまり、事前に計画しすぎないことが大切
    • これにより、急な戦略変更/要件変更にも柔軟に対応できる
    • 戦略とスコープを押さえた上で、直近のタスクの優先順位だけをしっかり決める
    • チームメンバーは最も重要な作業から一つづつ処理していく
  • 要点は、チーム全体のパフォーマンスを最大化すること

ソーシャルゲームの創り方・育て方

考えた人が作る
  • スピードを第一に考えると、「どんなものを作るか」について頭の中にイメージがある人がそのままコーディングして実現するのが理想的
  • エンジニア自身がデータ分析に関わり、ユーザーのニーズやユーザビリティを理解し、仕様を決める過程に多く関わるようにする
シンプルかつ少数精鋭でユーザーインパクトの大きいものを
  • システムはできるだけシンプルに
    • 少人数でも開発・運用できるものに
  • ユーザーへのインパクトはできるだけ大きなものに
    • できるだけ多くのユーザーに喜んでもらえるものに
前例がないものを作る
  • 前例がないものを作るときはやってみないとわからない
    • 手っ取り早く動くものを作って、自らそれを使ってみる
    • 動かしてみたら、違う側面も見えてくる
    • そこでプログラミングしなおす
  • 動かして、修正するといういわば「粘土をこねる」ような作業を繰り返す
  • 誰も見たことのないサービスやゲームが生まれる
  • つまり、サービスの企画者が実装も行えると、良いものを考えながら作ることができる
  • サービスの実現性が技術と表裏一体になっていて、出来れば、一人の人間が両方の側面を持っているのが理想的
サービスは「創る」だけではなく「育てる」もの
  • 開発のゴールはリリースではない
  • リリースしたあと、どれだけサービスとして成長させられるかが勝負
    • ユーザーの行動をDBやアクセスログなどからつかみ、改善点を見つけて改善していく
  • 状況を把握し、次の打つ手を考え、開発/投入し、効果を分析して次にまた活かすというサイクルを、できるだけ少人数で素早く回していく
  • 考える人とコードをいじる人を分割しないことが重要
インフラチームと開発チーム
  • インフラチーム
    • サーバの運用/保守、システム性能監視を担当し、障害発生時の一時切り分けなどを担当
    • それに加えて、データベースのテーブル設計のレビューやSQLチューニングでも貢献
  • 開発チーム
    • 新規開発時や中規模以上の機能追加や改修時には、インフラチームにトラフィックの見積もりなどを提示してレビューを受ける
    • データベースの負荷が上がっている場合には、SQLレベルまで踏み込んで問題点を解析し、チューニングを提案
  • このような体制を敷くことによって
    • インフラチームが常時性能監視を実施し、適切な対応方針を定めることができるため、急激なユーザーの増加や新規機能追加時のシステム負荷増加に即時対応出来る
    • そのためには、インフラチームの高いシステム運用力が必要
大きめのチームによる開発サイクル
  1. 案件出し
    • 開発サイクルは案件出しから始まる
    • Redmineにチケットを登録
      • 指示する人と指示される人の境界を無くしたほうが各自の創造性が発揮されるので、誰でもアイデアを出すことができる
      • 登録する案件は、イベントの他にも追加機能、システムの保守系タスク、バグ対応などあらゆるものが対象
      • この時点ではスケジュール未定だけどとりあえず登録する
      • 障害発生時などは、対応後、事後登録するという対応をとる
  2. 案件計画
    • 2週間に一度、登録された案件を確認し、優先順位をつけて、スケジュール概要と開発担当を決める
  3. 日々の作業
    • 業務終了時に現時点での作業状況をチケットに記載する
    • 朝会で前日にアップデートされた案件リストを見ながら各メンバーが作業報告を行う
    • 進捗が計画より遅れている場合は、他のメンバーに手伝ってもらうように依頼する
    • 逆に前倒しで進んでいる場合は「このチケットも吸収できそう」というような提案を行う
    • 前日の実装を通して見えてきた仕様に関する議論を行うこともある
    • 朝会は、「今」に焦点を合わせたMTG
  4. 週次MTG
    • 長期的なスパンの話題をする場
    • 分析の結果やアイデア出しなど
  5. 案件レビュー
    • イベントがひとつ終わるごとに関係したメンバーで振り返りを行う
    • 売上やユーザー行動の分析を行い、次の施策に結びつける
    • 障害が発生した場合は、再発防止の検討やプロセス自体の改善について議論する
情報共有・記録
  • チーム間での情報共有がおろそかになりがちなので、情報共有の場の提供を意識する
    • エンジニア全員が集まる定例会議を実施
    • 各ゲーム開発の進捗状況を共有するとともに、発生した障害の共有、技術/運用ノウハウの共有を行う
  • Wikiには、技術情報、運用情報など記録
    • やるべきことのチェックリストなどを記録化し、作業の抜け漏れを防ぐ
まとめ
  • 体制を変化
    • 関わる人数が増えることで体制を変化させてきた
      • ある程度のルール化や会議体を設定
      • 自由度を持たせながらも効率よくビジネスの要求を満たせるような開発が進むような体制
    • 人数に応じたルールやプロセスを選ぶことも重要だが、変化に対応することがもっと大切
      • 状況の変化に応じて、どういうやり方にすればもっとうまくいくかということ考えて適宜取り入れていく
  • 重要なこと
    • 同じことを続けない
    • 常に最善な方法を検討する
    • 誰でもそういった提案を行って改善につなげるということ


最後までお読みいただきありがとうございました。

ブックをCSVファイルに変換するマクロを書いてみた

友人から以下のような依頼が来たのでVBAを書いてみました

フォルダ内のExcelのBookファイルをすべてCSV形式で保存したい
なにか早い方法はないか?

  • 参照ライブラリ
  • コード
Sub ChangeBookToCsv()
  Application.DisplayAlerts = False
  
  Dim Path As String
  Dim OutputPath As String
  Dim FSO As New FileSystemObject
  Dim TargetFile As String
  Dim CsvFile As String
  
  Path = "" ' Bookが保存されているフォルダのパス
  OutputPath = "" ' CSVファイルを保存したいフォルダのパス
  
  TargetFile = Dir(Path & "\*.xls*")
  
  Do While TargetFile <> ""
    CsvFile = Split(TargetFile, ".")(0) & ".csv"
    Workbooks.Open (Path & "\" & TargetFile)
    Sheets(1).Select
    ActiveWorkbook.SaveAs Filename:=Path & "\" & CsvFile, FileFormat:=xlCSV
    ActiveWindow.Close
    TargetFile = Dir()
  Loop
End Sub


もっと早い効率的な方法があれば共有していただけると嬉しいです。

最後までお読みいただきありがとうございました。

EXCEL HACKSが教えてくれたこと

Excelとは

Excelは多くの業種に於いてビジネスを変革する基板となり、
世界中で意思決定のサポートをしている超強力な表計算ソフトです。

ちなみに、ExcelはMS社製ということもありWindowsのイメージが強いですが、初代ExcelMac用のソフトでした。

今回の参考資料

Excel Hacks 第2版― プロが教える究極のテクニック140選

Excel Hacks 第2版― プロが教える究極のテクニック140選

VBAとか言う前にやっぱりExcel

少しExcelが出来るとマクロとかに興味が出てくるものですが、
私は、Excel自体の機能についての理解をもっと深めることをおすすめしたいです。

というのも、Excel自体が超強力なソフトだからです。


それでは、そのExcelを最大限に利用する方法や、考え方などをまとめていきたいと思います。

表を作る前に

ある情報をまとめて表にしておきたいと思った際の注意点
  • 長期的な視点に立つ
  • データや数式の追加を常に念頭におく
    • 追加や変更は極めて頻繁に発生するのはご承知のとおりです
表とは
  • 表というのはユーザが正しい情報を得るためのものである
  • 情報をその場限りで見栄え良く表示出来ればいいというものではない
これらを踏まえた上で
  • 表の作成時間の8割を設計に割き、残りの2割を設計を元に具象化する作業にあてる
  • この方法は、短期的には非効率だが、長期的では十分にペイする非効率である


これ似た考え方がプログラマの世界にもあります。

スキルのレベルにかかわらず、プログラマーは全時間のおよそ10〜20%をコードを書くのにあてており、
たいていのプログラマーは完成品ができるまで一日あたりおよそ10〜12行のコードを書いています。
優秀なプログラマーは残りの90%のうち大部分を、考えること・調べること・最高の設計を見つけるための検証作業に費やします。

参照:Loading...


そう、知識労働に於いて体を動かすことよりも頭を動かすほうがはるかに重要なのです。

表のデータ配置

実は、Excelにはデータの配置にルールが存在します。
このルールを守らないと一部の機能が正しく動作しません。

表構成における悪い例
  • 必要以上に多くのブックにデータを分散させる
  • 必要以上に多くのワークシートにデータを分散させる
  • 必要以上に多くの表にデータを分散させる
  • 表の行列に空行を挿入する
  • 上や左のセルと同じ値のセルを空白のままにしておく


これらの基本的なルールを守らないとピポットテーブルなどの強力な機能の利用が制限される可能性があります。
特に、はじめの3点に関しては、「関連するデータは1つの連続した表の中に収める」ということで対応できますし、
これを怠ることで、Excelの機能が制限されるばかりでなくデータ不整合が起きて業務に致命的なダメージを与えるかも知れません。

ルール
  • 表の1行目にはそれぞれの列に対応する見出しを記述
  • 見出しのすぐ下からデータを配置する
  • それぞれの列の中で同じデータが繰り返されていても空欄を利用して繰り返しを表現しない
  • 可能なかぎりデータは並び替えておく


データの並び替えに関しては、検索や参照といった機能を利用するときに、
並び替えられていないデータでは、処理ができないことや、時間が大幅にかかってしまうということがあるからです。

また、これらを見てみるとExcelで列数<行数となる理由が分かってくると思います。

表内の書式

書式設定はトラブルの元

書籍設定を使って表を読みやすくし、理解しやすくすることは大切です。
ただ、この書式設定を使用することで処理の効率が落ちてしまっては意味がありません。
書式設定に時間を費やせば費やすほど効率性は低下してしまいます。
しかも、書式設定をすればするほどファイルサイズが増大し、挙動が重くなったりします。

セルの結合は問題の元

セルを結合する意味ってなんでしょうか?
一度考えてみてください。
これが、見た目を保つためであるのであれば、即刻利用をやめたほうがいいと思います。
※見出しは別です。
というのも、表のデータを処理する際にセルが結合していることで様々な困難に直面るからです。

でも、どのようにして見た目を保てばよいのでしょうか?

[書式(O)]→[セル(E)...]
[セルの書式設定]ダイアログボックスの[配置]タブ
[横位置]のリストから[選択範囲内で中央]を指定する

このような方法があるので、こちらに移行してみてください。
(Excel2003)

表内の数式

効率の悪い参照

A1〜H1000までのセル範囲からなる表があり、VLOOKUP関数等の検索関数を使用する場合に、
参照範囲をA1:H65536などに指定していませんか?またはA:H
(65536行はExcel2003の最大行数)

もちろん、表のレコード数(行数)が増加することは想定されます。
しかし、あまりにも効率が悪く、多用すると挙動が極めて遅くなることが想定されます。


では、どのようにすれば良いのでしょうか?
可変長の名前付きセル範囲を利用すればいいと思います。
新しいデータが追加される度に参照先のセル範囲を変更する必要がなくなり、また、効率的に参照が行われるので、便利です。

表計算ソフトの肝は計算

今まで挙げてきた問題などにより、挙動が遅くなったりしたときにすぐにやりがちなのが、手動計算への切り替えです。
しかし、この対処方法は極めて危険です。
なぜなら、Excelは表計算ソフトだからです。


手動計算指定をしていると知っているユーザは毎度F9を押せば、なんとかなるでしょう。
しかし、自動計算されると思っているユーザがその表に表示されている数字を正の数字と考えて対応してしまうと致命的なミスに繋がります。


このことを頭に入れて賢くExcelを使っていきましょう!
引き続き書いていきたいと思います。


最後までお読み頂きありがとうございました。