Slide 1

Slide 1 text

DCI with Ruby わかった気になる DCI

Slide 2

Slide 2 text

こんにちは! ロージーです!

Slide 3

Slide 3 text

Dec 21 Dec 22 Dec 23 今日はこの4日間で僕が学んだ DCI の話をします (クリスマスなんて知ったこっちゃねぇ) Dec 24

Slide 4

Slide 4 text

1. What is DCI

Slide 5

Slide 5 text

Data Context Interaction データ 文脈 挙動

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

全然わかんない! 教えて!マスター=センセイ!

Slide 8

Slide 8 text

Ruby on Rails: The Bad Parts https://vimeo.com/50568777

Slide 9

Slide 9 text

DCI and the application builds our mental models https://vimeo.com/51957143

Slide 10

Slide 10 text

The Right Way to Code DCI in Ruby http://mikepackdev.com/blog_posts/24-the-right-way-to-code-dci-in-ruby

Slide 11

Slide 11 text

and Github Repositories Alias DCI https://github.com/rebo/alias_dci DCI-Sample https://github.com/vsavkin/DCI-Sample DCI https://github.com/rubyworks/dci

Slide 12

Slide 12 text

Data ex: User Behavior ex: Customer Story ex: Buy the Item Data Context Interaction

Slide 13

Slide 13 text

2. Where is Logics

Slide 14

Slide 14 text

『ユーザーが商品を買う』 というロジックはどこにあるべきか論

Slide 15

Slide 15 text

例をご用意しました 『ユーザーがアイテムを買う』 User has_many :purchases Purchase belongs_to :user belongs_to :item Item has_many :purchases

Slide 16

Slide 16 text

Case 1: Model class User def buy_item(item) self.purchases.create(item: item) end end https://gist.github.com/285ec33e6cfceb0ba0cf#file-case_1_model-rb

Slide 17

Slide 17 text

Case 2: Controller class PurchasesController def create item = Item.find(params[:item_id]) current_user.purchases.create(item: item) end end https://gist.github.com/285ec33e6cfceb0ba0cf#file-case_2_controller-rb

Slide 18

Slide 18 text

Case 3: DCI class BuyItemContext def initialize(user, item) @user = user @item = item @user.extend(Customer) end def process @user.buy(item) end end module Customer def buy(item) self.purchases.create(item: item) end end https://gist.github.com/285ec33e6cfceb0ba0cf#file-case_3_dci-rb

Slide 19

Slide 19 text

このサイズだと判断が難しいと思うけれど とりあえずコントローラにあるのはヤバそうな気配がする

Slide 20

Slide 20 text

User#buy_item は、正しいのか

Slide 21

Slide 21 text

Wikipedia 曰く Model View Controllerページ Model ͦͷΞϓϦέʔγϣϯ͕ѻ͏ྖҬͷσʔλͱखଓ͖ʢϏδωεϩδοΫ - γϣοϐϯάͷ߹ܭֹ΍ૹྉΛܭࢉ͢ΔͳͲʣΛදݱ͢ΔཁૉͰ͋Δɻ· ͨɺσʔλͷมߋΛviewʹ௨஌͢Δͷ΋modelͷ੹೚Ͱ͋Δʢmodelͷมߋ Λ௨஌͢ΔͷʹObserver ύλʔϯ͕༻͍ΒΕΔ͜ͱ΋͋Δʣɻ ଟ͘ͷΞϓϦέʔγϣϯͰ͸σʔλͷ֨ೲʹӬଓతͳهԱͷ࢓૊Έʢσʔ λϕʔεͳͲʣ͕࢖ΘΕ͍ͯΔɻMVCͷ֓೦Ͱ͸ɺσʔλͷʢUIҎ֎ͷʣ ೖग़ྗ͸औΓѻΘͳ͍ͷͰɺσʔλΞΫηε΋ຊདྷMVCͷ֓೦ͷൣᙝΛ௒ ͑Δ΋ͷͰ͸͋Δ͕ɺ׶͍͑ͯ͑͹modelͷதʹӅṭ͞ΕΔͱߟ͑ΒΕΔɻ

Slide 22

Slide 22 text

正しいみたい!

Slide 23

Slide 23 text

No content

Slide 24

Slide 24 text

お疲れ様でした! まだ続きます

Slide 25

Slide 25 text

3. be Fat

Slide 26

Slide 26 text

# Redmine - project management software # Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. require "digest/sha1" class User < Principal include Redmine::SafeAttributes # Different ways of displaying/sorting users USER_FORMATS = { :firstname_lastname => { :string => '#{firstname} #{lastname}', :order => %w(firstname lastname id), :setting_order => 1 }, :firstname_lastinitial => { :string => '#{firstname} #{lastname.to_s.chars.first}.', :order => %w(firstname lastname id), :setting_order => 2 }, :firstname => { :string => '#{firstname}', :order => %w(firstname id), :setting_order => 3 }, :lastname_firstname => { :string => '#{lastname} #{firstname}', :order => %w(lastname firstname id), :setting_order => 4 }, :lastname_coma_firstname => { :string => '#{lastname}, #{firstname}', :order => %w(lastname firstname id), Redmine app/models/user.rb

Slide 27

Slide 27 text

Token.delete_all ['user_id = ?', id] Watcher.delete_all ['user_id = ?', id] WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] end # Return password digest def self.hash_password(clear_password) Digest::SHA1.hexdigest(clear_password || "") end # Returns a 128bits random salt as a hex string (32 chars long) def self.generate_salt Redmine::Utils.random_hex(16) end end class AnonymousUser < User validate :validate_anonymous_uniqueness, :on => :create def validate_anonymous_uniqueness # There should be only one AnonymousUser in the database errors.add :base, 'An anonymous user already exists.' if AnonymousUser.exists? end def available_custom_fields [] end # Overrides a few properties def logged?; false end def admin; false end def name(*args); I18n.t(:label_user_anonymous) end def mail; nil end def time_zone; nil end def rss_key; nil end def pref UserPreference.new(:user => self) end # Anonymous user can not be destroyed def destroy false end end Redmine app/models/user.rb 706 lines

Slide 28

Slide 28 text

Token.delete_all ['user_id = ?', id] Watcher.delete_all ['user_id = ?', id] WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] end # Return password digest def self.hash_password(clear_password) Digest::SHA1.hexdigest(clear_password || "") end # Returns a 128bits random salt as a hex string (32 chars long) def self.generate_salt Redmine::Utils.random_hex(16) end end class AnonymousUser < User validate :validate_anonymous_uniqueness, :on => :create def validate_anonymous_uniqueness # There should be only one AnonymousUser in the database errors.add :base, 'An anonymous user already exists.' if AnonymousUser.exists? end def available_custom_fields [] end # Overrides a few properties def logged?; false end def admin; false end def name(*args); I18n.t(:label_user_anonymous) end def mail; nil end def time_zone; nil end def rss_key; nil end def pref UserPreference.new(:user => self) end # Anonymous user can not be destroyed def destroy false end end Redmine app/models/user.rb 706 lines

Slide 29

Slide 29 text

コードがすごく汚いとかではない (むしろいい方だと思う)

Slide 30

Slide 30 text

じゃあなんで太るのか

Slide 31

Slide 31 text

たいてい参照されるケースが多すぎる (User がやたら太るのはその理由)

Slide 32

Slide 32 text

90%のユーザーストーリーで必要のない 振る舞いを抱え続けている

Slide 33

Slide 33 text

常に必要のない振る舞いは 適宜与えてあげればいい

Slide 34

Slide 34 text

それって、DCIでは?

Slide 35

Slide 35 text

4. Rails with DCI

Slide 36

Slide 36 text

現状の Rails で DCI をするには 1. dci gem を頑張って使う(微妙) 2. DCI-Sample みたいに自分で頑張る(辛い) 3. user.extend(Customer) しまくる(妥当) 4. ESM に gem 化をお願いする(どうなん)

Slide 37

Slide 37 text

AOP じゃダメなの?

Slide 38

Slide 38 text

僕の理解が間違ってなければ AOP って #alias_method_chain なのでは

Slide 39

Slide 39 text

もう自作したほうが早いのでは?

Slide 40

Slide 40 text

Dicer https://github.com/rosylilly/dicer 作ってみた

Slide 41

Slide 41 text

WIP まだ実用に耐えない 僕にお酒を飲ませると実装が進みます!

Slide 42

Slide 42 text

5. the Future

Slide 43

Slide 43 text

Rails 4 ちょっと見たけど、 こういう問題への解決策の提示はなかった

Slide 44

Slide 44 text

このままだと、主要モデルが太り続けるのは 間違いない

Slide 45

Slide 45 text

こんだけ喋ったけど 僕もまだいい方法が思いつかない

Slide 46

Slide 46 text

モデルダイエット勉強会誰かやりませんか>< (僕 DCI の勉強進めて DCI の話します)

Slide 47

Slide 47 text

応質 答疑 Question https://speakerdeck.com/rosylilly/dci-with-ruby