技術のメモ帳

気が向いたときに書いてます

FactoryMethodパターンのメモ

雑な話。

  • 抽象クラス: 作り方は同じだけど、「何」を作るか(対象)は分からない(対象は実装しない)
  • 具象クラス: 作り方は抽象クラスを継承し、「何」を作るかだけ指定している(対象だけを実装する)

この抽象クラスが具象に処理を任せていることを「FactoryMethod」と言う。

だがしかし、Rubyならば継承ではなく、具象クラス(実装)をモジュールに分割して、extendしてもよさそう。

発言はフェアに、アイデアの採用はアンフェアに

この記事を読んで、ちょっと共感したフレーズがあったのでメモ。

d.hatena.ne.jp

熱量とコミットを伴う発言は重く、とりあえず言ってみただけの傍観者発言は軽く扱う。すべての発言を平等に扱うという民主的なアプローチではプロジェクトは動かない。

寄せ集めの場合、コミット感がまちまちだろうからなのだろうが、これは企業のプロジェクトでも同じことが言えると思う。各メンバーの熱量・コミット感は異なるため、『とりあえず言ってみただけ』は往々にして発生する(もちろん、良かれと思って...であろうけど)。プロジェクトの運営は、「平等・民主的=良いこと」と考えがちだが、それを目的とするべきではないと日頃より思う。

メンバーの発言を寄せ集めただけ、良いプロダクトが生まれたら、誰も苦労しないだろう。そこには優先順位もあるし、そういう意見はあったとしても、やらないほうがよいことだってある。若干、話は逸れるが、サービスやプロダクトを作るとき、たとえば、あるスマホアプリないしモバイルゲームの場合、「こんな機能があったら便利」と、あれもこれも足したくなる『足し算症候群』的な状態に陥ることはよくあるが、メンバー各々が発する足し算をすべて行ったところで、「何だかよくわからないもの」にしかならない。

意見を言うことは平等であるべきだけれど、その採用は、発言者が誰かに依らず、本当に妥当かは検討を重ねられるべきだし、不平等で良いと思うのだ。「なんだよ、あいつのアイデアばかりが採用されているな」というのも、それはそれで問題がない状況とみなしたい(人間関係がギクシャクするのは避けたいところだけれど)。

Docker practice (1/2)

Environment

Mac OSX 10.11.6 (EI Capitan)
VirtualBox 5.1.4
docker 1.11.1
docker-machine 0.7.0
docker-compose 1.7.0

Installation

$ brew cask install virtualbox
$ brew install docker-machine
$ brew cask install dockertoolbox

Boot docker-machine

What is Docker Machine?

Docker Machine is a tool that lets you install Docker Engine on virtual hosts, and manage the hosts with docker-machine commands. You can use Machine to create Docker hosts on your local Mac or Windows box, on your company network, in your data center, or on cloud providers like AWS or Digital Ocean.

Setup

Initialize

$ docker-machine create --driver virtualbox --virtualbox-hostonly-cidr "192.168.99.99/24" default
$ eval "$(docker-machine env default)"

Restart

$ docker-machine start default
$ eval "$(docker-machine env default)"

Stop, Remove

$ docker stop $(docker ps -aq)
$ docker rm $(docker ps -aq)
$ docker rmi $(docker images | awk '/^<none>/ { print $3 }')
$ docker-machine rm default

Examples

All examples have to setup docker-machine.

Ruby

$ docker pull ruby:2.3.1
$ docker run ruby:latest ruby -v
$ docker run -it ruby:latest /bin/bash # -i: Keep STDIN open even if not attached, -t: Allocate a pseudo-TTY

latest is a version of image.

MySQL

$ docker pull mysql:5.6
$ docker run -p 3307:3306 -v ~/docker/mysql/conf.d:/etc/mysql/conf.d -e MYSQL_ROOT_PASSWORD=pass -d mysql:5.6
$ mysql -h$(docker-machine ip default) -P3307 -uroot -p # enter "pass"

Redis

$ docker pull redis:3.2
$ docker run --name some-redis -d redis:3.2
$ docker run -it --link some-redis:redis --rm redis:3.2 redis-cli -h redis -p 6379

Rails

  1. Prepare

    $ docker pull ruby:2.3.1
    $ { \
        echo 'source "https://rubygems.org"'; \
        echo 'gem "rails"'; \
    } > Gemfile
    $ touch Gemfile.lock
    
  2. Create Dockerfile

    FROM ruby:2.3.1
    
    ENV APP_ROOT /usr/src/sample_app
    
    WORKDIR $APP_ROOT
    
    RUN apt-get update && \
        apt-get install -y nodejs \
                           mysql-client \
                           postgresql-client \
                           sqlite3 \
                           --no-install-recommends && \
        rm -rf /var/lib/apt/lists/*
    
    COPY Gemfile $APP_ROOT
    COPY Gemfile.lock $APP_ROOT
    
    RUN \
      echo 'gem: --no-document' >> ~/.gemrc && \
      cp ~/.gemrc /etc/gemrc && \
      chmod uog+r /etc/gemrc && \
      bundle config --global build.nokogiri --use-system-libraries && \
      bundle config --global jobs 4 && \
      bundle install && \
      rm -rf ~/.gem
    

    And execute docker build -t sample_app ..

  3. Create Rails app

    $ docker run --rm -it -v "$PWD":/usr/src/sample_app sample_app rails new . -BT
    
  4. Edit Dockerfile

    Append these lines:

    COPY . $APP_ROOT
    
    EXPOSE 3000
    CMD ["rails", "s", "-b", "0.0.0.0"]
    

    And execute docker build -t sample_app ..

  5. Setup DB

    $ docker run --rm sample_app rails db:create db:migrate
    
  6. Run

    $ docker run -d -p 3000:3000 sample_app
    $ open "http://$(docker-machine ip default):3000"
    

Next Step...

Using docker-compose.

References

ihcomega.hatenadiary.com

qiita.com

docs.docker.com

ActiveRecord::Base.transactionの使い方を誤解していた

  • (誤解)テーブル名が異なる場合、 ActiveRecord::Base から実行しないとNG!?
  • (正解)同じクラスからコールされるので、Baseのサブクラスならば Model.transaction でも model_instance.transaction でもOK
# == Different Active Record classes in a single transaction
#
# Though the #transaction class method is called on some Active Record class,
# the objects within the transaction block need not all be instances of
# that class. This is because transactions are per-database connection, not
# per-model.
#
# In this example a +balance+ record is transactionally saved even
# though #transaction is called on the +Account+ class:
#
#   Account.transaction do
#     balance.save!
#     account.save!
#   end
#
# The #transaction method is also available as a model instance method.
# For example, you can also do this:
#
#   balance.transaction do
#     balance.save!
#     account.save!
#   end

ちゃんとコード読もうぜ、という話だった。

参考

github.com

deviseのカスタマイズをしていて思ったこと

deviseのなんだかなー感

「遷移変えたい場合は、継承したコントローラ作って、特定メソッドをオーバーライドする」という流儀に対して、それをやりたい箇所が増えてくると、ぐぬぬ感がはんぱない。あと、次のようなルーティングですかね。

Rails.application.routes.draw do
  devise_for :accounts,
             path: "",
             path_names: {
               sign_up:      "",
               sign_in:      "login",
               sign_out:     "logout",
               registration: "signup",
               confirmation: "verification",
               password:     "reset_password",
             },
             controllers: {
               registrations:      "accounts/registrations",
               confirmations:      "accounts/confirmations",
               omniauth_callbacks: "accounts/omniauth_callbacks",
             }
  devise_scope :account do
    get :sent_verification, to: "accounts/sent_confirmations#index", as: :sent_confirmation
  end

/login とか /signup とかやりたい場合は、上記のようなカスタマイズする感じになるので、あとで見た時にわからなくなるかもなーと。

ただ、deviseの作者に文句を言うつもりもなく、むしろ使わせてもらって感謝という態度しかない。

devise以外の選択肢

あたりでしょうか。前者は使ったことあるけれど、実装は難しくないですが、リセットパスワードなど自前で実装する必要は当然でてきます。

まあ、ケースバイケースって話ですね。

BigQueryのQueryBuilderをgemで公開してみた

昨日、ようやくgemとして公開できました。

github.com

6月中旬あたりから始めて、移動中などの時間があるときに コツコツ書いてきたので、「一ヶ月もかかってしまったかー」とも思うのだけど 公開できたので、よかったなーと素直に喜びたいなと思います。

なにができるの?

BigQueryのSELECT文をAR風に書けますよー! ってgemです。 あくまで「風」であって、再現度はそこまで高くないんですけどね!

コード見てもらった方が早いかと思います。

BB.select("word", "corpus", "COUNT(word)").
   from("publicdata:samples.shakespeare").
   where(word_cont: "th").
   group(:word, :corpus).
   to_sql

# => "SELECT word, corpus, COUNT(word) FROM publicdata:samples.shakespeare WHERE (word CONTAINS 'th') GROUP BY word, corpus"

なにがうれしいの?

「え、こんなんわざわざメソッドチェーンで書かないで、素のSQL書いたらいいじゃん」と思いがちですが、

フォームから渡されたparamsに沿って動的に実行クエリを変えて、リクエストを送る という要件があったとき、 毎度毎度、WHERE句を作るのが、とってもダルいのです。case文とか毎回書いてられんのです。

たとえば、次のようなフォームオブジェクトがあったとします。

class Form
  include Virtus.model

  attribute :name, String
  attribute :created_at_gteq, DateTime
  attribute :created_at_lt, DateTime
end

そして処理は次のようなオーソドックスな流れです。

form_for→format_params(StrongParameter)→Form.new(params)→valid→search★

★で、BigQueryとやりとりするとき、Formのattributesをそのまんま使って、クエリを構築したいなと、 ずっと思っていたのですが、いい感じのgemが世の中になかったんですね。

そんな経緯もあって、作ってみた次第です。

使い方

単純なビルダーなので、部分的にも使えるようにしました。 上述のような、WHERE句だけで使う...なんてこともできるように。

READMEのまんまですが、ざっとおさらいです。

SELECT 句

これはフツウに、文字列ないしシンボル渡すと、 よしなにSELECT句を作るってメソッドです。

BB.select(:id, :name, :state).to_sql
# => "SELECT id, name, state"

BB.select("id", "name", "COUNT(*)").to_sql
# => "SELECT id, name, COUNT(*)"

FROM 句

TABLE_DATE_RANGE 関数に対応したり、Array渡してUNION作ったりと、 なんだかんだとBigQueryの仕様に沿っています。

BB.from("publicdata:samples.shakespeare").to_sql
# => "SELECT * FROM publicdata:samples.shakespeare"

BB.from("[applogs.events_20120501]", "[applogs.events_20120502]", "[applogs.events_20120503]").to_sql
# => "SELECT * FROM [applogs.events_20120501], [applogs.events_20120502], [applogs.events_20120503]"

BB.from("applogs.events_", on: Date.new(2012, 5, 1)).to_sql
# => "SELECT * FROM applogs.events_20120501"

BB.from("mydata.people", from: Date.new(2014, 3, 25), to: Date.new(2014, 3, 27)).to_sql
# => "SELECT * FROM TABLE_DATE_RANGE(mydata.people, TIMESTAMP('2014-03-25'), TIMESTAMP('2014-03-27'))"

BB.from(BB.from("publicdata:samples.shakespeare"), as: shakespeare).to_sql
# => "SELECT * FROM (SELECT * FROM publicdata:samples.shakespeare) AS shakespeare"

JOIN 句

複数JOINにも対応していて、 JOIN EACH など、BigQuery独自のJOINにも対応しています。

BB.from(:customers, as: :t1).inner_join(:orders, as: :t2).on("t1.customer_id = t2.customer_id").to_sql
# => "SELECT * FROM customers AS t1 INNER JOIN orders AS t2 ON t1.customer_id = t2.customer_id"

BB.from(:customers, as: :t1).join_each(BB.select(:id, :name).from(:orders), as: :t2).on("t1.customer_id = t2.customer_id").to_sql
# => "SELECT * FROM customers AS t1 JOIN EACH (SELECT id, name FROM orders) AS t2 ON t1.customer_id = t2.customer_id"

WHERE 句

ここはAR風に、プレースホルダー、または、ハッシュ渡して条件作れます。

BB.where(id: 1..10, name: "donald", flag: false).to_sql
# => "WHERE (id BETWEEN 1 AND 10 AND name = 'donald' AND flag IS false)"

BB.where("id = ? OR name CONTAINS ?", 123, "john").to_sql
# => "WHERE (id = 123 OR name CONTAINS 'john')"

じゃっかんARと変えたのは、 ornot を直感的に書きやすくしたところでしょうか。

BB.where(id: 123).or.where(id: 456).to_sql
# => "WHERE (id = 123) OR (id = 456)"

BB.not.where(id: 123).or.not.where(id: 456).to_sql
# => "WHERE (id <> 123) OR (id <> 456)"

また、ハッシュの場合はSuffixで条件が変えられるようにもしています。

BB.where(name_cont: "Jack", id_gteq: 123).to_sql
# => "WHERE (name CONTAINS "Jack" AND id > 123)"

OMIT RECORD IF句

BigQuery独自のClauseです。

BB.omit_record_if("COUNT(payload.pages.page_name) <= ?", 80).to_sql
# => "OMIT RECORD IF (COUNT(payload.pages.page_name) <= 80)"

GROUP BY 句

GROUP EACH に対応しているくらいで、他は特に言うことないっす。

BB.group(:age, :gender).to_sql
# => "GROUP BY age, gender"

BB.group("ROLLUP(year, is_male)").to_sql
# => "GROUP BY ROLLUP(year, is_male)"

BB.group_each(:age, :gender).to_sql
# => "GROUP EACH BY age, gender"

HAVING 句

ほぼWHERE句の流用なので割愛。

ORDER BY 句

これはほぼARと同じです。

LIMIT 句

OFFSET オプションを付けたくらいかな。

BB.limit(1000).to_sql
# => "LIMIT 1000"

BB.limit(1000).offset(500).to_sql
# => "LIMIT 1000 OFFSET 500"

ネームスペースについて

当初は、「BigQuery::QueryBuilder」とバカ正直なネーミングで 作っていたのですが、名前が長すぎて、使う気になれないんですよね。

なので、2文字くらいで使えるように短くしました。

なにより「b_b」ってnamingは、カオっぽくていいんですよ!

作ってみて良かったこと

  1. 設計力の足りなさに気づけた。

    コードリーディングの分量が足りず、引き出し少ない感をヒシヒシ感じました。 結局は、デザインパターンを再学習したり、コード読みまくることでしか解決できなそうです。

    「この書き方(or設計)、オシャンティやんか」という喜びも発見できるので、 他人のコード読むのは、非常に楽しい瞬間でもあります。

  2. テクが向上した。

    まだまだだなーと思うところもあるのですが、Ruby力のベースはあがった気がします。 gem作ってみると、業務でRails書いていると気付かないことに、いろいろ気付かされます。

  3. 取り組める時間の少なさに気づいた。

    フリーのエンジニアなので、鎌倉←→新宿を毎日往復しているのですが、そこでの1〜1.5時間程度、運良く座れた場合に限り、コードを書いていました。

    「くっ...時間さえあれば...」と何度か心が折れそうになりました。リモート or 鎌倉で業務委託したいと本気で願いました(それくらい時間が作りにくかった)。

  4. スクリプト書くの楽しいよねー!という悦び。

最後に

ということで、興味がある方は gem install b_b してみて使ってみてください。

また、こんなんじゃなくて、BigQueryとActiveRecordの「本気な関係」をお望みの方は、以下で甘いひとときをお過ごしください(Good luck)

github.com

Dashの独自CheatSheetをYAMLから生成するgemを作った

cheatset-yamlという、Dashの独自CheatSheetをYAMLから生成するgemを作りました。

github.com

動機に至るまで

  • 今更ながらSpotlightを使うのをやめて、Alfredを導入した。
  • Alfred導入に併せて、Dashを使うようになった。
  • 独自CheatSheetを作りたくなった。
  • DSLrubyファイル作る必要があることを知る。
  • YAMLで書けたらいいのになー。

DashでCheatSheetを作るには?

詳しくは下記が詳しいですが、Kapeli/cheatset というgemのDSLでツラツラと書いていく必要があります。

qiita.com

github.com

DSLは次の通り(READMEより)

cheatsheet do
  title 'Sample'               # Will be displayed by Dash in the docset list
  docset_file_name 'Sample'    # Used for the filename of the docset
  keyword 'sample'             # Used as the initial search keyword (listed in Preferences > Docsets)
  # resources 'resources_dir'  # An optional resources folder which can contain images or anything else

  introduction 'My *awesome* cheat sheet'  # Optional, can contain Markdown or HTML

  # A cheat sheet must consist of categories
  category do
    id 'Windows'  # Must be unique and is used as title of the category

    entry do
      command 'CMD+N'         # Optional
      command 'CMD+SHIFT+N'   # Multiple commands are supported
      name 'Create window'    # A short name, can contain Markdown or HTML
      notes 'Some notes'      # Optional longer explanation, can contain Markdown or HTML
    end
    entry do
      command 'CMD+W'
      name 'Close window'
    end
  end

  category do
    id 'Code'
    entry do
      name 'Code sample'
      notes <<-'END'
        ```ruby
        sample = "You can include code snippets as well"
        ```
        Or anything else **Markdown** or HTML.
      END
    end
  end

  notes 'Some notes at the end of the cheat sheet'
end

YAMLで書くには?

YAMLだと次のような感じになります。

cheatsheet:
  title: Sample             # Will be displayed by Dash in the docset list
  docset_file_name: Sample  # Used for the filename of the docset
  keyword: sample           # Used as the initial search keyword (listed in Preferences > Docsets)
  resources: resources_dir  # An optional resources folder which can contain images or anything else

  introduction: 'My *awesome* cheat sheet'  # Optional, can contain Markdown or HTML

  # A cheat sheet must consist of categories
  categories:
    - id: Windows # Must be unique and is used as title of the category
      entries:
        - name: Create Window # A short name, can contain Markdown or HTML
          notes: Some notes   # Optional longer explanation, can contain Markdown or HTML
          command:
          - CMD+N       # Optional
          - CMD+SHIFT+N # Multiple commands are supported
        - name: Close Window
          command: CMD+W
    - id: Code
      entries:
        - name: Code sample
          notes: >
            You can include code `snippets` as well
            Or anything else **Markdown** or HTML.

  notes: 'Some notes at the end of the cheat sheet'

それぞれの定義内容を知らないといけない時点で、そこまで変わらん気もしますが、YAMLの定義を見るだけで大体察しがつく内容かと思います。

使い方

YAMLを用意して、コマンド叩くだけ。

$ gem install cheatset-yaml
$ cheatset-yaml generate ${filename}.yml

コードの話

特に難しいことはしていませんが、一応説明。

  1. cheatsetのgem(以下本家)をラップする方向で。
  2. YAMLをパースした後、ERBでRubyファイルを作成(Tempfile)。
  3. 本家のコンテクスト評価クラスに、そのファイルを渡す。
  4. generateメソッドで、CheatSheetを生成。
  5. CLIは本家同様、thorを使った。
  6. (最初はテスト書こうと思ったが)書くまでの内容ではないので割愛した。

ということで、Dashで独自CheatSheetを作ってみたい方は、ぜひ使ってみてください!

github.com

なお、docsetは生成した後、 open コマンドで展開するか、finderからダブルクリックで展開してくださいねー:)