Javaエンジニア、Ruby on Railsでアプリを作る

整理券アプリを作成中です

期間が重複しているかを判定する条件式の導出方法

開始時間(start_time)と終了時間(end_time)を持つテーブル(例: schedule)で、あるレコードXと時間が重複したデータを抽出するSQLは、

SELECT *
FROM schedule
WHERE
    start_time < レコードXのend_time
    AND
    end_time > レコードXのstart_time

です。 しかしstart_timeとend_timeの関係性を考えるのに毎回頭が混乱してしまうので、今回はこのSQLの導出方法を書いておきます。

ちなみに、このSQLは、 一方のend_timeが他方のstart_timeと同じときには重複してい ないと判定する場合です。 例えば、[start_time, end_time] = [14: 00, 15:00][start_time, end_time] = [15: 00, 16:00]の2つのレコードは重複として判定しません。 [start_time, end_time] = [14: 00, 15:00][start_time, end_time] = [15: 00, 16:00]を重複と判定する場合は、不等号にイコール記号を加えてください。

重なりのパターン

レコードXとほかのレコードとの重なりのパターンは以下のレコードABCDEFの6パターンです。

f:id:yucatio:20180816175358p:plain

このうち、レコードXと重複するのは、レコードB,C,D,E、重複しないのがレコードA,Fです。

レコードB,C,D,Eに当てはまり、レコードA,Fに当てはまらない条件を導き出せればよいのですが、自分にはすぐには導き出せませんでした。

重ならない条件

そこで、一旦重ならないパターンに焦点を当てます。

重ならないパターンはレコードA,Fの2つで、レコードAは、

end_time <= レコードXのstart_time

となっているもの、

もう片方(レコードF)は、

レコードXのend_time <= start_time

です。(不等式にイコールを、含むかどうかは仕様によります。今回は、一方のend_timeが他方のstart_timeと同じときには重複してい ないと判定するものとします)

このどちらかの条件を満たしたすべてのレコードがレコードXと重複しません。式で書くと以下になります。

(end_time <= レコードXのsrart_time) OR (レコードXのend_time <= start_time)

重なる条件

重ならない条件式ができあがったので、これの否定をとれば、重なる条件式になります。

NOT((end_time <= レコードXのstart_time) OR (レコードXのend_time <= start_time))

ド・モルガンの法則により、

NOT(end_time <= レコードXのsrart_time) AND NOT(レコードXのend_time <= start_time)
↓
(end_time > レコードXのsrart_time) AND (レコードXのend_time > start_time)

となり、不等式が導出できました。

こちらもどうぞ

yucatio.hatenablog.com


Railsでvalidation中にDBアクセスする場合は、will_save_change_to_#{attr_name}?を使用する

Webアプリでvalidationを書いていると、DBに問い合わせをしなければいけない時があるのですが、その時に、値が変更されているかチェックしてからDB問い合わせした方がよいよというお話です。値が変更されているかは、Railsではwill_save_change_to_#{attr_name}?というメソッドが用意されています。#{attr_name}の部分はModelの属性(カラム名)で置き換えます。

値が変更されているか調べてからvaldiateする

前回に引き続き、会議室予約アプリを例にして、具体的な使い方をみてみます。

会議室(meeting_room)は複数の会議予約(meeting)をもちます。会議予約は、開始時刻(start_time)と終了時刻(end_time)をもちます。

f:id:yucatio:20180809214216p:plain

★前回の記事

yucatio.hatenablog.com

yucatio.hatenablog.com

仕様

  • 会議予約(前回実装済み)
    • subjectは32文字以下で必須項目
    • start_timeは必須項目
    • end_timeは必須項目
    • end_timeはstart_timeより後であること
    • 新規登録時、start_timeは現在時刻より後であること
    • start_timeが変更された時、start_timeは現在時刻より後であること
    • 保存済みのstart_timeが現在より前の場合は、start_timeを変更できない
    • 保存済みのstart_timeが現在より前の場合は、end_timeを変更できない

これに、

  • 会議予約
    • 同一の会議室に属するmeetingの期間は、重複してはならない。つまり、ある時刻に1つの会議室で開催されている会議は最大で1つである。

というvalidationを追加します。

コード

仕様を満たすコード
class Meeting < ApplicationRecord
  # 略

  validate :meeting_time_should_not_overlap

  private
  # 略

  def meeting_time_should_not_overlap
    return unless start_time && end_time

    if Meeting
           .where(meeting_room_id: meeting_room.id)
           .where('start_time < ?', end_time)
           .where('end_time > ?', start_time)
           .where.not(id: id).exists?
      errors.add(:base, '他の会議と重複しています。')
    end
  end
end

Meeting.where(meeting_room_id: meeting_room.id).where('start_time < ?', end_time).where('end_time > ?', start_time)で登録しようとする会議予約と同じ会議室の中で、開始終了時間と重複するレコードを検索します。直感的にわかりづらい条件ですが、これで検索できます。

yucatio.hatenablog.com

続くwhere.not(id: id)で、更新対象のレコードを除外しています。除外しないと対象レコードも検索に引っかかってきてしまうためです。最後にexsists?をつけて、存在するかどうかだけを確認しています。

生成されるSQLは以下のようになります。

SELECT  1 AS one 
FROM "meetings" 
WHERE "meetings"."meeting_room_id" = ?
    AND (start_time < '2018-08-10 12:00:00')
    AND (end_time > '2018-08-10 11:00:00')
    AND "meetings"."id" != ? 
LIMIT ?
[["meeting_room_id", 1], ["id", 3], ["LIMIT", 1]]

このコードで問題なく動くのですが、ひとつ気になるのは、このSQLは、validationのたびに実行されるということです。例えば、subjectしか更新しないようなリクエストでも毎回重複チェックのためのSQLが実行されます。

一般的にSQLの実行はコストが高いため、必要なとき以外は実行しないようにコードを書き換えます。

値が変更された場合のみDBアクセスをするコード

validationが必要なのは、登録時と開始時間または終了時間が変更されたときです。

class Meeting < ApplicationRecord
  # 略

  validate :meeting_time_should_not_overlap, if: :new_or_start_or_end_time_changed

  private
  # 略

  def new_or_start_or_end_time_changed
    new_record? || will_save_change_to_start_time? || will_save_change_to_end_time?
  end

  def meeting_time_should_not_overlap
    return unless start_time && end_time

    if Meeting
           .where(meeting_room_id: meeting_room.id)
           .where('start_time < ?', end_time)
           .where('end_time > ?', start_time)
           .where.not(id: id).exists?
      errors.add(:base, '他の会議と重複しています。')
    end
  end
end

new_record?(登録時)、will_save_change_to_start_time?(start_timeが変更されているとき)、そしてwill_save_change_to_end_time?(end_timeが変更されているとき)にのみSQLを実行するように変更できました。

実行結果

現在の予約状況

f:id:yucatio:20180811230448p:plain

他の会議室101の会議時間を重複する会議は登録できない↓

f:id:yucatio:20180811231217p:plain

他の会議室101の会議と重複しない会議は登録できる↓

f:id:yucatio:20180811231627p:plain

他の会議室101の会議時間を重複する日時に変更できない↓

f:id:yucatio:20180811232313p:plain

他の会議室101の会議と重複しない会議は登録できる↓

f:id:yucatio:20180811233203p:plain

日時が変更されない(subjectのみ変更)場合は、重複チェックSQLが実行されない↓

f:id:yucatio:20180811233935p:plain

環境

参考

Rails4とRails5ではDBの値をとるメソッドや変更されたかを取得するメソッドの名前が変更されています。rail5の方がわかりやすいメソッド名ですね。#{attr_name}_in_databasewill_save_change_to_#{attr_name}? 以外のメソッドもこちらにまとまっています。

qiita.com

[商品価格に関しましては、リンクが作成された時点と現時点で情報が変更されている場合がございます。]

Ruby on Rails 5アプリケーションプログラミング [ 山田祥寛 ]
価格:3888円(税込、送料無料) (2018/8/9時点)


Railsで変更前の(DBに保存されている)値を利用してvaldiateする

Webアプリでvalidationを書いていると、DBに保存されている値が必要になる場合があります。Railsでは、DBに保存されている値は、#{attr_name}_in_database、保存されている値から変更があったかは、will_save_change_to_#{attr_name}?というメソッドが用意されています。#{attr_name}の部分はModelの属性(カラム名)で置き換えます。

変更前の値を利用してvaldiateする

前回に引き続き、会議室予約アプリを例にして、具体的な使い方をみてみます。

会議室(meeting_room)は複数の会議予約(meeting)をもちます。会議予約は、開始時刻(start_time)と終了時刻(end_time)をもちます。

f:id:yucatio:20180809214216p:plain

★前回の記事

yucatio.hatenablog.com

仕様

  • 会議予約(前回実装済み)
    • subjectは32文字以下で必須項目
    • start_timeは必須項目
    • end_timeは必須項目
    • end_timeはstart_timeより後であること
    • 新規登録時、start_timeは現在時刻より後であること
    • start_timeが変更された時、start_timeは現在時刻より後であること

これに、

  • 会議予約
    • 保存済みのstart_timeが現在より前の場合は、start_timeを変更できない
    • 保存済みのstart_timeが現在より前の場合は、end_timeを変更できない

この2つのvalidationを追加します。

コード

class Meeting < ApplicationRecord
  # 略

  validate :start_time_cannot_be_changed
  validate :end_time_cannot_be_changed

  private
  # 略

  def after_meeting_start?
    start_time.present? && !new_record? && start_time_in_database <= Time.zone.now
  end

  def start_time_cannot_be_changed
    if after_meeting_start?
      if will_save_change_to_start_time?
        errors.add(:start_time, 'は変更できません。会議開始時刻を過ぎています')
      end
    end
  end

  def end_time_cannot_be_changed
    if after_meeting_start?
      if will_save_change_to_end_time?
        errors.add(:end_time, 'は変更できません。会議開始時刻を過ぎています')
      end
    end
  end
end

after_meeting_start?メソッド内の start_time_in_databaseでは、DBに保存されている(つまり更新前の)start_timeの値を取得できます。 start_time.present? && !new_record? && start_time_in_database <= Time.zone.now は、start_timeがnilでなく、新規レコードでなく(≒更新で)、DBに保存されているstart_timeが、現在時刻より前である、という意味です。

start_time_cannot_be_changedメソッド内でwill_save_change_to_start_time?を使用して、値が変更された時にエラーを出すようになっています。end_time_cannot_be_changedメソッドも同様です。

実行結果

更新時

※実行時は2018年8月10日

開始時間が過去で登録されていて、日時が変更無しのとき(subjectのみ変更)は更新できる↓

f:id:yucatio:20180811154046p:plain

開始時間が過去の場合、他の日時へは変更できない↓

f:id:yucatio:20180811222525p:plain

開始時間が未来の場合、別の未来の日時には変更できる↓

f:id:yucatio:20180811161128p:plain

環境

続く

こちらもどうぞ

yucatio.hatenablog.com

参考

Rails4とRails5ではDBの値をとるメソッドや変更されたかを取得するメソッドの名前が変更されています。rail5の方がわかりやすいメソッド名ですね。#{attr_name}_in_databasewill_save_change_to_#{attr_name}? 以外のメソッドもこちらにまとまっています。

qiita.com

[商品価格に関しましては、リンクが作成された時点と現時点で情報が変更されている場合がございます。]

Ruby on Rails 5アプリケーションプログラミング [ 山田祥寛 ]
価格:3888円(税込、送料無料) (2018/8/9時点)


Railsで値が変更されているか調べてvaldiateする

Webアプリでvalidationを書いていると、値が変更になった時にだけvalidationをかけたい場合があります。Railsではwill_save_change_to_#{attr_name}?というメソッドが用意されています。#{attr_name}の部分はModelの属性(カラム名)で置き換えます。

値が変更されているか調べてからvaldiateする

会議室予約アプリを例にして、具体的な使い方をみてみます。

会議室(meeting_room)は複数の会議予約(meeting)をもちます。会議予約は、開始時刻(start_time)と終了時刻(end_time)をもちます。

f:id:yucatio:20180809214216p:plain

ルーティングはこちらです。

# routes.rb
Rails.application.routes.draw do
  resources :meeting_rooms do
    resources :meetings
  end
end

仕様

  • 会議予約
    • subjectは32文字以下で必須項目
    • start_timeは必須項目
    • end_timeは必須項目
    • end_timeはstart_timeより後であること
    • 新規登録時、start_timeは現在時刻より後であること
    • start_timeが変更された時、start_timeは現在時刻より後であること

コード

本題ではない方のvalidation
  • 会議予約
    • subjectは32文字以下で必須項目
    • start_timeは必須項目
    • end_timeは必須項目
    • end_timeはstart_timeより後であること

このへんは今回の本題ではないのでコードだけ載せておきます

class Meeting < ApplicationRecord
  belongs_to :meeting_room

  validates :subject, length: {maximum: 32}, presence: true
  validates :start_time, presence: true
  validates :end_time, presence: true

  validate :start_time_should_be_before_end_time

  private
  def start_time_should_be_before_end_time
    return unless start_time && end_time

    if start_time >= end_time
      errors.add(:start_time, 'は終了時間よりも前に設定してください')
    end
  end
end

値の変更時にvalidationをかける

  • 会議予約
    • 新規登録時、start_timeは現在時刻より後であること
    • start_timeが変更された時、start_timeは現在時刻より後であること

こちらを実装したコードは以下です。

class Meeting < ApplicationRecord
  # 略

  validate :start_time_should_be_after_current, if: :new_or_start_time_changed

  private
  # 略

  def new_or_start_time_changed
    start_time.present? && (new_record? || will_save_change_to_start_time?)
  end

  def start_time_should_be_after_current
    if start_time <= Time.zone.now
      errors.add(:start_time, 'は現在時刻よりも後に設定してください')
    end
  end
end

new_or_start_time_changedメソッドの中で、 will_save_change_to_start_time?を使うことによって、start_timeが変更されたかどうかを判定しています。

start_time.present? && (new_record? || will_save_change_to_start_time?)は、start_timeがnilでないかつ、新規またはstart_timeの値が変更されている、という意味です。

if: :new_or_start_time_changedを外して、

class Meeting < ApplicationRecord
  # 略

  validate :start_time_should_be_after_current

  private
  # 略

  def start_time_should_be_after_current
    if start_time <= Time.zone.now
      errors.add(:start_time, 'は現在時刻よりも後に設定してください')
    end
  end
end

と書いてしまうと、start_timeが過去になってしまうと、全ての項目(subjectなど)が更新できなくなってしまうので注意してください。

実行結果

新規登録時

※実行時は2018年8月10日

開始時間に過去の日時は登録できない↓

f:id:yucatio:20180811152352p:plain

開始時間に未来の日時は登録できる↓

f:id:yucatio:20180811152403p:plain

更新時

※実行時は2018年8月10日

開始時間が過去で登録されていて、日時が変更無しのとき(subjectのみ変更)は更新できる↓

f:id:yucatio:20180811154046p:plain

開始時間が過去の場合、未来の日時へ変更できる↓

f:id:yucatio:20180811154934p:plain

開始時間が過去の場合、別の過去の日時には変更できない↓

f:id:yucatio:20180811155634p:plain

開始時間が未来の場合、 過去の日時には変更できない↓

f:id:yucatio:20180811160509p:plain

開始時間が未来の場合、別の未来の日時には変更できる↓

f:id:yucatio:20180811161128p:plain

環境

続く

こちらもどうぞ

yucatio.hatenablog.com

yucatio.hatenablog.com

参考

Rails4とRails5ではDBの値をとるメソッドや変更されたかを取得するメソッドの名前が変更されています。rail5の方がわかりやすいメソッド名ですね。#{attr_name}_in_databasewill_save_change_to_#{attr_name}? 以外のメソッドもこちらにまとまっています。

qiita.com

[商品価格に関しましては、リンクが作成された時点と現時点で情報が変更されている場合がございます。]

Ruby on Rails 5アプリケーションプログラミング [ 山田祥寛 ]
価格:3888円(税込、送料無料) (2018/8/9時点)


Rubyで全角空白も対象としたstrip(trim)

rubyではtrimじゃなくてstripというそうです。 stripは先頭と末尾の半角空白やタブ、改行などを除去するメソッドです。

全角空白は除去されないので、独自で定義する必要があります。

うまくいく例

class String
  def strip_all_space!
    gsub!(/(^[[:space:]]+)|([[:space:]]+$)/, '')
  end

  def strip_all_space
    self_clone = clone
    self_clone.strip_all_space!
    self_clone
  end
end
puts '   前後空白 全角半角混合   '.strip_with_2byte_space
# => '前後空白 全角半角混合'

str = '   前後空白 全角半角混合   '
str.strip_with_2byte_space!
puts str
# => '前後空白 全角半角混合'

仕様

strip_all_spaceメソッドは、文字列の先頭と末尾の空白文字を除去した新しい文字列を返します。 空白文字は、正規表現の[:space:]の文字と同じで、全角空白などユニコードの空白文字も含まれています。

strip_all_space!メソッドは、レシーバ自身の文字列から先頭と末尾の空白文字を除去します。変更があった場合はレシーバ自身を、変更がない場合はnilを返します。

正規表現

gsub!(/(^[[:space:]]+)|([[:space:]]+$)/, '')

こちらのメソッド呼び出しですが、まず gsub!は、正規表現でマッチした部分を繰り返し置換するメソッドです。

マッチさせる正規表現(^[[:space:]]+)|([[:space:]]+$)の前半の(^[[:space:]]+)は、「文字列の先頭に続き、[:space:]クラスの1文字以上の繰り返し」を表します。 後半の([[:space:]]+$)は、「[:space:]クラスの1文字以上の繰り返し、その直後に終端(行末)」を表します。その2つを|で結合することによって、そのどちらにもマッチするようになります。

マッチした部分を空文字に置き換えることにより、行頭と行末の空白文字を除去しています。

strip_all_space(非破壊的メソッド)からstrip_all_space!(破壊的メソッド)を呼び出す

def strip_all_space
  self_clone = clone
  self_clone.strip_all_space!
  self_clone
end

cloneで自身のコピーを作成し、コピーした方に破壊的変更をすることで、自身の情報には変更を加えず、またコードの重複なく目的のメソッドを作成できました。

Stringクラスの破壊的メソッドの戻り値

strip!sub!gsub!tr!などのStringクラスに定義されている破壊的メソッドの戻り値は、 変更があった場合はレシーバ自身を、変更がない場合はnilを返します。

変更がないときにnilを返すのは少し奇妙に思いましたが、これは、変更があった時(もしくはなかった時)に処理をするためだと思います。

str = ' test '
if str.strip!
  # 変更があったとき
  puts '文字列が破壊的に変更されました'
else
  # 変更がなかったとき
  puts '文字列は変更されませんでした'
end

独自にStringクラスを拡張する場合もこの規則に従い、破壊的変更をする場合は、変更があった場合はレシーバ自身を、変更がない場合はnilを返すようにするべきと考えます。

誤った例

全角空白も対象としたstripはweb上にいくつか情報が存在し、参考にさせてもらったのですが、いくつか求める仕様と異なっていることに気づかず使おうとしてしまったものもありました。同じ間違いをする人が出ないように、例とどの場合にうまくいかないかを示します。

誤った例 (その1)

def strip_all_space!
  sub!(/^[[:space:]]*([^[:space:]]+)[[:space:]]*$/, '\1')
end

行頭に続き、[:space:]クラスの0回以上の繰り返し、[:space:]クラスでない文字の1回以上の繰り返し、[:space:]クラスの0回以上の繰り返し、行末、にマッチさせ、中間の部分を抜き出す表現です。

うまくいかない場合ですが、

まず、' 'のように空白のみで構成される文字列にマッチしません。 また、' 文 字 列 'のように空白でない文字と文字の間に空白が入る文字列にもマッチせず、前後の空白が除去されません。

誤った例 (その2)

def strip_all_space!
   gsub!(/^[[:space:]]*(.*?)[[:space:]]*$/, '\1')
end

行頭に続き、[:space:]クラスの0回以上の繰り返し、任意の文字列の0回以上の繰り返し、[:space:]クラスの0回以上の繰り返し、行末、にマッチさせ、中間の部分を抜き出す表現です。(.*?)の部分で最短マッチを行なっています。

' 文 字 列 'のように空白でない文字と文字の間に空白が入る文字列も問題なく'文 字 列'のように前後の空白が除去されます。

ただ、'文字列'のように変更がない文字列にも正規表現がマッチしてしまい、「変更がない場合はnilを返す」という仕様に合いませんでした。

誤った例 (その3)

class String
  def strip_all_space!
    gsub!(/(^[[:space:]]+)|([[:space:]]+$)/, '')
  end

  def strip_all_space
    clone.strip_all_space!
  end
end

非破壊的メソッドからcloneメソッドを使用して破壊的メソッドを呼ぶ方法をwebで発見したのですが、cloneに対して破壊的メソッドを呼び、戻り値をそのまま返しています。

strip_all_space!メソッドは変更がないときにnilを返すので、戻り値をそのまま返すとバグになります。cloneを一旦別の変数に保存し、破壊的メソッドを呼び出した後に、cloneしたものを返す必要があります。


Railsでの標準ライブラリの拡張方法

Stringを拡張する際のrailsでの記述方法はこちらが参考になりました。 ただし、この方法では、起動時にしかStringクラスが読み込まれず、書き換えた場合に再起動が必要という問題があります。

qiita.com

テストケース

簡単ですが、rspecでテストを書きました。正規表現のテストケース書くの難しい。

describe 'Strip all space behavior' do
  describe '#strip_all_space!' do
    subject { str.strip_all_space! }

    context 'When all space string' do
      let(:str) { '  ' }
      it { is_expected.to eq ''}
      it 'should remove spaces' do
        subject
        expect(str).to eq ''
      end
    end

    context 'When all space string including 2-byte space' do
      let(:str) { '   ' }
      it { is_expected.to eq ''}
      it 'should be empty string' do
        subject
        expect(str).to eq ''
      end
    end

    context 'When space string including 2-byte space at start of string' do
      let(:str) { '   文字列' }
      it { is_expected.to eq '文字列'}
      it 'should remove spaces' do
        subject
        expect(str).to eq '文字列'
      end
    end

    context 'When space string including 2-byte space at end of string' do
      let(:str) { '文字列    ' }
      it { is_expected.to eq '文字列'}
      it 'should remove spaces' do
        subject
        expect(str).to eq '文字列'
      end
    end

    context 'When space string including 2-byte space around string' do
      let(:str) { '    文字列    ' }
      it { is_expected.to eq '文字列'}
      it 'should remove spaces on start/end of string' do
        subject
        expect(str).to eq '文字列'
      end
    end

    context 'When space around string including white space' do
      let(:str) { '    文 字 列    ' }
      it { is_expected.to eq '文 字 列'}
      it 'should remove start/end spaces. Spaces between string are preserved' do
        subject
        expect(str).to eq '文 字 列'
      end
    end

    context 'When empty string' do
      let(:str) { '' }
      it { is_expected.to eq nil}
      it 'should not be changed' do
        subject
        expect(str).to eq ''
      end
    end

    context 'When string does NOT have any white space' do
      let(:str) { '文字列' }
      it { is_expected.to eq nil}
      it 'should not be changed' do
        subject
        expect(str).to eq '文字列'
      end
    end
  end

  describe '#strip_all_space' do
    subject { str.strip_all_space }

    context 'When all space string' do
      let(:str) { '  ' }
      it { is_expected.to eq ''}
      it 'should be original string' do
        subject
        expect(str).to eq '  '
      end
    end

    context 'When all space string including 2-byte space' do
      let(:str) { '   ' }
      it { is_expected.to eq ''}
      it 'should be original string' do
        subject
        expect(str).to eq '   '
      end
    end

    context 'When space string including 2-byte space at start of string' do
      let(:str) { '   文字列' }
      it { is_expected.to eq '文字列'}
      it 'should be original string' do
        subject
        expect(str).to eq '   文字列'
      end
    end

    context 'When space string including 2-byte space at end of string' do
      let(:str) { '文字列    ' }
      it { is_expected.to eq '文字列'}
      it 'should be original string' do
        subject
        expect(str).to eq '文字列    '
      end
    end

    context 'When space string including 2-byte space around string' do
      let(:str) { '    文字列    ' }
      it { is_expected.to eq '文字列'}
      it 'should be original string' do
        subject
        expect(str).to eq '    文字列    '
      end
    end

    context 'When space around string including white space' do
      let(:str) { '    文 字 列    ' }
      it { is_expected.to eq '文 字 列'}
      it 'should be original string' do
        subject
        expect(str).to eq '    文 字 列    '
      end
    end

    context 'When empty string' do
      let(:str) { '' }
      it { is_expected.to eq ''}
      it 'should be original string' do
        subject
        expect(str).to eq ''
      end
    end

    context 'When string does NOT have any white space' do
      let(:str) { '文字列' }
      it { is_expected.to eq '文字列'}
      it 'should be original string' do
        subject
        expect(str).to eq '文字列'
      end
    end
  end
end
環境

「オブジェクト指向とは何か」の答えが『なぜ、あなたの仕事は終わらないのか(中島聡[著])』に書いてあったので紹介します

オブジェクト指向とは

『なぜ、あなたの仕事は終わらないのか(中島聡[著])』には以下のように書いてあります。

なんらかの対象(オブジェクト)を先に選択したうえで動作を指定することをオブジェクト指向といいます。

考えてみる

Wikipediaオブジェクト指向を調べてみると、

オブジェクト指向 - Wikipedia

オブジェクト指向(オブジェクトしこう)とは、オブジェクト同士の相互作用として、システムの振る舞いをとらえる考え方である。

とあります。これに異論はありません。

ただ、Wikipediaの文章は、オブジェクト指向がわかる人であれば理解できる文ですが、初心者にとっては抽象的すぎて何を言ってるのか捉えにくいかもしれません。

冒頭で引用した説明もこれだけでは何を言っているのかよくわからないかも知れませんが、 書籍中では、 Windows95での右クリックやダブルクリック、ドラック&ドロップが現在のようになった、という話の中で出てきており、理解がしやすいようになっていました。 以下に例を示します。

オブジェクト指向GUI

WindowsののGUIでは、 ファイルの上で右クリックすると、選択できる動作が表示されます。

f:id:yucatio:20180801122931p:plain

また、ダブルクリックで、文書ファイルならMicrosoft Wordが起動し、音楽ファイルなら音楽プレーヤーが起動します。

注目すべきは、動作(ファイルを開く、ファイルを削除する)を始めに指定するのではなく、ファイルを先に指定しているという点です。

オブジェクト指向以前は動作が先

オブジェクト指向以前では、ユーザがコマンドという文字列を最初に入力してそのあとにファイルやディレクトリを指定していました。

例えばエクセルファイルを開くのにはexcel.exeコマンドを使用します。

excel.exe 集計.xls

ファイルの削除はdelコマンドを使用します。(delはdeleteの略)

del 集計.xls

扱う言語(自然言語)による違い

書籍には続いて以下のように書いてあります。

オブジェクト指向のわかりやすい例として、私たちがいつも使っている日本語が挙げられます。

あなたがテーブルの上の塩を取ってほしいとき、あなたは「すいません、塩を……」まで言葉にしたところで一呼吸置くと思います。「塩」という対象を指定した時点で、あなたが相手にしてほしいことは決まり切っているからです。

(中略)

これはWindowsにおけるダブルクリックと非常によく似ていると思いませんか?このようなグラフィカルでオブジェクト指向な機能を思いつくことができたのは、もしかしたら私が当時マイクロソフトで唯一の日本語話者だったからかもしれません。

これは面白い考察でした。確かに英語の命令文だとはじめに動詞が来ます。なのでコンピュータに対する命令を入力するときに、始めにコマンド(動詞であることが多い)、そのあとに対象ファイルを指定するのが自然な考えでしょう。

日本語がオブジェクト指向な言語かどうかよくわかりません。目的語が最初というより、動詞が最後にくるだけな気がしますがどうなのだろう。

ところで、オブジェクト指向において、メソッド呼び出しを「メッセージ」と呼ぶのは、オブジェクト指向のプログラミングにおいて、オブジェクト.命令の部分を英語的に読みくだしたい場合に、オブジェクトに対して呼びかけている様子をイメージさせるためかと思いました。

# オブジェクト指向的にファイルを開くコード
fileA.open()

# 英語的に読みくだしてみる(適当)
# Hey fileA! Open!

まとめ

オブジェクト指向がよくわからない、と思ったときには、 windowsのファイルアイコンの操作のように、「先に操作の対象(ファイルやディレクトリ)を決めて、次に動作(開く、移動する、削除する、など)を決める」 というイメージを持つとよいかと思います。

実際、JavaIDEでは、対象を指定した段階で、呼び出す動作(メソッド)の候補が表示されます。

f:id:yucatio:20180801115942p:plain

(オブジェクト指向をよくわかっている人の場合にはこのイメージだけでは物足りないと思いますが。カプセル化とかポリモーフィズムとか)。

書籍の紹介

引用した書籍はプログラミングの本ではなく、仕事術の本ですが、著者がマイクロソフトWindows95の開発をしていた頃のエピソードも盛り込まれています。そのエピソードがエキサイティングなので読み物としてもとても楽しめました。


引用した文章はこちらで読めます

www.huffingtonpost.jp

あとがき

普段オブジェクト指向でプログラミングをしていて、オブジェクト指向の利点を利用したプログラムを書いていますが、いざ「オブジェクト指向とは何か言葉で説明しなさい」ともし言われたとしたら答えられない、ということに気づきました。

この記事を書くにあたり、オブジェクト指向とは何かというのをwebで検索しました。書籍に書かれていたような定義をしているサイトがなく(そもそもサイトごとに定義が違う)、"書籍の中で使用している「オブジェクト指向」と私が思っている「オブジェクト指向」は別物なのでは"という疑念も湧きましたが、

オブジェクト指向と20年戦ってわかったこと

ここの記事に書いてあった、

言語ごとの多少の表記法の違いはあれど、「最大公約数」を取り出そうとしたら、たぶん

オブジェクト.メソッド(引数...)

という部分しか残らないのでは、と思います。

という記述を見つけ、安心しました。

複数行の式とRubyMineのフォーマッタとの相性が悪いのでIssueをあげました

こちらの記事を書いてる最中に、複数行の式とRubyMineのフォーマッタの相性が悪いと気づきました。

yucatio.hatenablog.com

算術オペレータを最後に置いて改行する場合

算術オペレータを最後に置く方法だと、フォーマットをかけた際に、RubyMineは4個のスペースを入れるのに対し、RuboCopは、オペランド(=足されている数)を揃えるようにと警告が出ます。

フォーマットかける前↓(オペランドが揃っていて、RuboCopの警告は出ない)

f:id:yucatio:20180628123856p:plain

フォーマッタをかけた後↓(インデントが崩れ、RuboCopの警告が出る)

f:id:yucatio:20180628123413p:plain

バックスラッシュを用いて改行する場合

一方、バックスラッシュを用いる場合は、フォーマットをかけるたびに続く行がどんどん後ろへとずれていく現象が起こります。

フォーマットかける前↓

f:id:yucatio:20180628125116p:plain

フォーマット1回かけた後↓

f:id:yucatio:20180628125151p:plain

さらに4回フォーマットをかけたとき↓

f:id:yucatio:20180628125414p:plain

さらに5回フォーマットをかけたとき↓

f:id:yucatio:20180628125435p:plain

全てのバックスラッシュを用いた改行でこの現象が起こるわけではなく、ある特定の条件の場合のみのようですが、どのような条件で起きるかは定かではありません。

Issueをあげておきました

JetBrainsのバグトラックシステムにissueをあげました。

Reformat CodeするとRubocopの警告でる : https://youtrack.jetbrains.com/issue/RUBY-21925

Reformat Codeのたびに空白が足される : https://youtrack.jetbrains.com/issue/RUBY-21927

環境