一から勉強させてください

最下級エンジニアが日々の学びをアウトプットしていくだけのブログです

iPhoneアプリ開発初心者がSwift + CoreDataでシンプルなTODOアプリをつくってみた

(2015/11/7 追記)Swift2.0 に対応しつつ、リネームやリファクタリングなど修正を加えたものをGitHubにあげております。この記事内で説明している通りに実装しても動作しないと思うので、もしよろしければ GitHub のコードも参考にしてみてください。


僕は普段、Rails とか JavaScript とか触っているようなへっぽこ Web エンジニアでして、iOSAndroid アプリの開発経験はありません。

HogeClass *hoge = [[HogeClass alloc] init]; // (゚Д゚≡゚Д゚)マジ?

て感じでした。

でも最近は Objective なんたらの 100 億倍書きやすそうな Swift という言語が出ていたりするので、楽しそうだしちょっと手を出してみるかと。

そこでまず iOS 開発勉強用のサンプルとして Swift でシンプルな TODO アプリをつくってみたので、今回はその時の実装メモを書きたいと思います。

iOS 開発とか1ナノもわからない状態からスタートしたので、同じような境遇の人のお役に立てたら幸いです。

事前準備

Swift どころか、Objective なんたらも C もまともにやったこと無い人間だったので、さすがにいきなり開発はツラいと。

そこでまずは鉄板のドットインストールさんで事前に Swift やら iOS の開発がどんな感じかをざっくり学びました。(プレミアム会員なりましたよ)

僕は「ストーリーボードって何ですの?」レベルだったのでかなり参考になりました。

その後、Swift の言語仕様を学びたい欲が高まってきたので、以下の本を読み始めました。

個人的に Swift はいろんな言語のいいとこ取りしてるような感じで面白い言語だなぁという印象でした。

あとハマるのはツラいので最低限のデバッグ方法をなんとなく調査。

こちらこちらを見る感じだとブレークポイント仕込んで、po ◯◯ってやれば値をいじくれそうな気配がします。 JS の DEBUG を Chrome でやるような感じでなんとなくできそうです。po!!

そんな感じで本を読みながら、ブレークポイントを仕込みながら、楽しく TODO アプリ開発スタートです。

作成したアプリ

今回作成したものはこんな感じです。

f:id:d_animal141:20150228000945g:plain

コードはこちらに公開しております。

TODO を新規に登録できて、それを編集できて、スワイプしたら DELETE ボタン出てきて削除できて、みたいな。

いい感じのチュートリアルを見つけたのでそちらをモロパクリした次第です(以下、チュートリアルはこの記事のことを指すものとします)。英語です。

CoreData tutorial in Swift 5 using NSFetchedResultsController – Ravi Shankar

基本的に上記チュートリアルを参考にしながら、改造していくスタイルで開発しました(チュートリアルに載っているコード、たまに動かなかったので気をつけてくださいw)

あとチュートリアルと僕の作成したもので命名がちょいちょい違うところあると思いますが、特に深い理由はないのでお気になさらず。

ちなみに CoreData なるものを使っていて割とそこがツラかったりしましたが、まあ何とかなると思いますw

プロジェクト作成

Create a new File -> New -> Project でテンプレートを選択します。

今回は Master-Detail Application ではなく、Single View Application で一から作っていきます。

あと Use Core Data のチェックを忘れずに。

UI 開発

チュートリアルに図が豊富に載っているし、ドットインストールでストーリーボードの使い方も勉強したので、UI まわりはほとんど詰まることなく進めることができました。

強いて言うなら最初、プロジェクト作成後に自動で作られる ViewController を消して、新たに TableViewController を追加するところ。 僕は viewController のルートを表すストーリーボードの → マークが消えて「どこいったぁーーーー」と荒れ狂ったのですが、そういう人は新しく追加した tableViewController を選択した状態で、

attributes inspector -> is initial view controller

にチェックを入れると幸せになれると思います。

とりあえず TODO 一覧画面、詳細画面を作成して、+ボタンで遷移できるところまで作ったとします。また、一覧、詳細の viewController はそれぞれTodosViewController, TodoDetailViewControllerとします。

CoreData の準備

続いて CoreData の準備にとりかかります。

といっても1ナノも使い方がわからないので、まずはざっとCore Data プログラミングガイドこちらの記事に目を通します。

とりあえず

  • CoreData は OR マッピングフレームワークであり、永続ストアには主に SQLite を使用する
  • 今回特に使いそうなのは NSManagedObject、NSManagedObjectContext、NSEntityDescription、NSFetchRequest、NSFetchedResultsController あたり

ということがわかりました。

そして今回使いそうなクラスについて

  • NSManagedObject

モデルクラス。CoreData で永続化するオブジェクトは必ずこのクラス、またはこのクラスのサブクラスのオブジェクトでなければならない。

  • NSManagedObjectContext

データ操作を行うためのコンテキスト(作業領域)。NSManagedObjectに対する操作はすべてこのコンテキストを介して行われる。CRUD 操作、Undo Redo、Validation などを担当。

  • NSEntityDescription

エンティティ記述という、エンティティの定義を管理するクラス。エンティティとは CoreData で永続化の対象となるデータのこと。エンティティとNSManagedObjectオブジェクトの関係は DB でいうところのテーブルとレコードの関係みたいな感じですかね。

  • NSFetchRequest

データの検索条件を管理するクラス。ここで指定した条件が SQL に変換されてデータ検索に使われる。

  • NSFetchedResultsController

NSManagedObjectオブジェクトを監視するコントローラークラス。NSManagedObjectオブジェクトが挿入、変更、削除された時にNSFetchedResultsControllerDelegateオブジェクトに通知する。

ふむ。

プロジェクト作成時に Use Core Data をチェックした時点で CoreData を使うための設定はある程度やってくれているみたいですね。

では上記を踏まえ、チュートリアルに沿って進めていきます。

まずは xcdatamodeld ファイルを更新します。チュートリアルではエンティティを Tasks としていますが、僕は Todos として、Attributes は String 型の content を持つものとしました。

Todos.swift はとりあえずこんな感じになります。

import Foundation
import CoreData

class Todos: NSManagedObject {

    @NSManaged var content: String

}

ちなみにここで@NSManagedというものがついていますが、もともと Objective なんたらでは NSManagedObject のサブクラスのプロパティは@dynamicという属性をつけていたらしく、「ここはゲッターセッターの自動生成は無しにして、NSManagedObjectの実装に任せましょう」みたいな意味だったらしいです。(参考)

これが Swift ではNSManagedObjectのサブクラスにのみ利用可能な@NSManagedという属性が使われるようになったようです。

では、いよいよ TODO を登録するところを実装していきます。

TODO の登録

TODO を登録できるようにするためにTodoDetailViewControllerを修正していきます。

まずNSManagedObjectContextオブジェクトを用意します。

let managedObjectContext = (UIApplication.sharedApplication().delegate as AppDelegate).managedObjectContext

iOS では 1 アプリにつき必ず 1 つの UIApplication が存在しており、そのシングルトンオブジェクトはUIApplication.sharedApplicationで取得できます。(参考)

そいつからAppDelegateインスタンスmanagedObjectContextを取得します。

そしてテキストフィールドに入力したテキストを保存できるようにメソッドを追加していきます。

func createTodo() {
    let entity = NSEntityDescription.entityForName("Todos", inManagedObjectContext: managedObjectContext!)
    let todo = Todos(entity: entity!, insertIntoManagedObjectContext: managedObjectContext!)

    todo.content = self.textField.text
    managedObjectContext!.save(nil)
}

func dismissViewController() {
    self.navigationController?.popViewControllerAnimated(true)
}

@IBAction func save(sender: AnyObject) {
    self.createTodo()
    self.dismissViewController()
}

おそらくお作法なのだろうと、真似して保存するところまで書いてみます。(エラーハンドリング等はとりあえず無視)

まずエンティティを取得して、それを使ってNSManagedObjectオブジェクトを取得。そいつにテキストフィールドの値を入れてNSManagedObjectContextオブジェクトにて保存すると。

@IBAction func save(sender: AnyObject)で Save ボタンを押したときの処理が書けるので、TODO の保存と詳細画面を閉じる処理を追加します。

登録した TODO の取得

次にTodosViewControllerの修正です。

こちらの viewController でも CoreData をインポートし、NSFetchedResultsControllerDelegateプロトコルに適合させていきます。またデータ操作のためのNSManagedObjectContextオブジェクトも必要です。

import CoreData

class TodosViewController: UITableViewController, NSFetchedResultsControllerDelegate {

    let managedObjectContext = (UIApplication.sharedApplication().delegate as AppDelegate).managedObjectContext
    var fetchedResultController: NSFetchedResultsController = NSFetchedResultsController()

これだけだとfetchedResultControllerは無力なので、getFetchedControllerメソッドを定義して、viewDidLoadの中でゴニョゴニョやります。(viewDidLoadがいつ呼ばれるか等、iOS のイベントに関してはこちらを参考にさせて頂きました)

override func viewDidLoad() {
    super.viewDidLoad()

    fetchedResultController = self.getFetchedResultController()
    fetchedResultController.delegate = self
    fetchedResultController.performFetch(nil)
}

func getFetchedResultController() -> NSFetchedResultsController {
    fetchedResultController = NSFetchedResultsController(fetchRequest: self.todoFetchRequest(), managedObjectContext: managedObjectContext!, sectionNameKeyPath: nil, cacheName: nil)
    return fetchedResultController
}

func todoFetchRequest() -> NSFetchRequest {
    let fetchRequest = NSFetchRequest(entityName: "Todos")
    let sortDescriptor = NSSortDescriptor(key: "content", ascending: true)
    fetchRequest.sortDescriptors = [sortDescriptor]
    return fetchRequest
}

まずtodoFetchRequestにて検索条件を作成します。そしてそいつをセットしてNSFetchedResultsControllerオブジェクトを作成。performFetchで検索条件をもとにデータ取得といった感じでしょうか。 あーキツくなってきたぞw

登録した TODO の一覧表示

次、一覧表示しますよ。まず、ストーリーボードの TodosViewController の Cell を選択した状態で、identifier を Cell としてください。

そんでからチュートリアルのコードをパクります。どん。

//こいつは必要なさそうな?
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
    let numberOfSections = fetchedResultController.sections?.count
    return numberOfSections!
}

override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    let numberOfRowsInsection = fetchedResultController.sections?[section].numberOfObjects
    return numberOfRowsInsection!
}

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    var cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell
    let todo = fetchedResultController.objectAtIndexPath(indexPath) as Todos
    cell.textLabel?.text = todo.content
    return cell
}

func controllerDidChangeContent(controller: NSFetchedResultsController!) {
    tableView.reloadData()
}

こちらの記事を参考に tableView まわりのメソッドを整理していきます。

  • numberOfSectionsInTableView

テーブル全体のセクションの数を返すように実装する。

  • tableView(tableView: UITableView, numberOfRowsInSection section: Int)

セクション無しのテーブルを作成する場合はこのメソッドでセルの数を返すように実装する。テーブルにセクションを付ける場合はnumberOfRowsInSectionで指定されたセクションの項目数を返すように実装する。

  • tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) cellForRowAtIndexPathで指定したインデックスパスのセル(UITableViewCell)を作成し、そのインスタンスを返すように実装する。

ただ今回、別にセクション分けされた tableView を作っているわけではないのでnumberOFSectionsInTableViewは要らない気がしました。実際消しても何も問題起きないので僕は削除しました。

あと、tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath)の以下の部分。

let todo = fetchedResultController.objectAtIndexPath(indexPath) as Todos

ここで Swift には NameSpace があるので、このダウンキャストはモジュール名.クラス名じゃないと使えないと。

てなわけで xcdatamodeld ファイルを再度開き、Data Model Inspector から上記に従ってクラス名を変更します。僕はiOSTodosというプロジェクト名だったのでiOSTodos.Todosです。

ここまでで新規登録と登録した TODO の一覧表示はできるかと。あーーキツイ!!

登録した TODO の削除

スワイプしたら DELETE ボタンが出てきて、クリックしたら削除できるあれですね。

override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
    let managedObject: NSManagedObject = fetchedResultController.objectAtIndexPath(indexPath) as NSManagedObject
    managedObjectContext?.deleteObject(managedObject)
    managedObjectContext?.save(nil)
}

こんな感じになりました。コンテキストを介して、対象のNSManagedObjectオブジェクトを削除して保存。新規登録と同じ感じですね。

登録した TODO の編集

TODO を編集できるようにします。一覧画面からセルを選ぶと詳細画面に遷移して、編集、保存を行えるようにします。

まずストーリーボードの TodosViewController の Cell から TodoDetailViewController に向かってセグエを作成します。あとでセグエを特定するために identifier が必要になるので、identifier は edit とします。

次にTodosViewControllerの修正をします。

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    if segue.identifier == "edit" {
        let cell = sender as UITableViewCell
        let indexPath = tableView.indexPathForCell(cell)
        let todoDetailController: TodoDetailViewController = segue.destinationViewController as TodoDetailViewController
        let todo: Todos = fetchedResultController.objectAtIndexPath(indexPath!) as Todos

        todoDetailController.todo = todo
    }
}

prepareForSegueは画面遷移が始まった時に呼ばれるメソッドですね。

クリックされた Cell をもとにindexPathを取得して TODO を特定します。そして、segue.destinationViewControllerで遷移先の viewController が取得できるのでそいつに TODO を渡しています。 TodoDetailViewControllerのほうでtodoを定義しないといけません。

TodoDetailViewControllerの修正です。

var todo: Todos?

override func viewDidLoad() {
    super.viewDidLoad()

    if todo != nil {
        self.textField.text = todo?.content
    }
}

一覧画面から TODO が渡ってきた場合、テキストフィールドにそいつのcontentをセットしています。

そして編集メソッド。

func editTodo() {
    todo?.content = self.textField.text
    managedObjectContext!.save(nil)
}

入力したテキストフィールドの値で上書き保存します。

Save ボタンをおした時の挙動が新規登録、編集の2パターンになったので修正します。

@IBAction func save(sender: AnyObject) {
    if todo != nil {
        self.editTodo()
    } else {
        self.createTodo()
    }
    self.dismissViewController()
}

これで新規登録、一覧表示、削除、更新、ひと通りの処理ができるようになりました。キツかった。。

テキストフィールドで Return した時にキーボードが閉じるようにする

現状ではテキストを入力し終わってドヤ顔で Return してもキーボードが閉じず、恥ずかしい思いをしてしまいます。

TodoDetailViewControllerを修正していきます。

class TodoDetailViewController: UIViewController, UITextFieldDelegate {
    略

    func textFieldShouldReturn(textField: UITextField!) -> Bool {
        self.view.endEditing(true)
        return false
    }

UITextFieldDelegateプロトコルに適合して、Return した時に呼ばれるtextFieldShouldReturnを実装しておきます。これでキーボードが閉じるはず。

まとめ

以上、チュートリアルを参考に TODO アプリを作ってみました。作る過程で様々な本や記事に触れ、少しだけですが iPhone アプリ開発のことを知ることができました。やはり今までやったことない開発は楽しいし、Swift という言語自体にもかなり惹かれているので今後も勉強を続けていきたいなと思いました。ただどうしても現状 Objective なんたらの知識が求められる場面が多いのでそこはツラいですねw 諦めてそちらも勉強していきます。。

ちなみに他にもテキスト空の状態で登録できないようにしたり、ちょいちょい改良はしてみたのですが、長くなるので後はこちらで見ていただければと思います。

また本記事や公開しているコードにおいて、初心者ゆえの間違いも多々あるかもしれません。僕は非常に傷つきやすいので、もし発見された方は優しくコメントして頂けると幸いです。

参考

Chromeのコンソール上でXPathのテストをする

最近、仕事で Web スクレイピングの機能を実装する機会があって XPath を使う必要に駆られました。そこで ChromeXPath が正しく機能しているかテストする便利な技があったことを知ったのでメモです。

$x を使用する

Chrome のコンソールから、$xを使えば OK。

たとえば/html/bodyでちゃんと body 要素が取得できるか確認したい場合は、

$x('/html/body')

としてやれば body 要素がちゃんと返ってきます。こいつは便利。知らなかった。。

まとめ

スクレイピングする時ぐらいしか使わないかもしれませんが、もし複雑な XPath を書く必要に迫られて困っている人は試してみてください!

参考

procとlambdaの挙動の違い

Ruby の Proc オブジェクトはKernel#proc(またはProc#new)で作るパターンとKernel#lambdaで作るパターンがありますが、それらは地味に結構挙動が違っていたりします。

そこで今回はこのKernel#procKernel#lambda でつくった時の Proc オブジェクトの挙動の違いについて見ていこうと思います。

return の時の挙動が違う

def proc_hoge
  proc { return 1; puts 'hoge' }.call
  'hogehoge'
end

def lambda_hoge
  -> { return 1; puts 'hoge' }.call
  'hogehoge'
end

proc_hoge #=>1
lambda_hoge #=> 'hogehoge'

こんな感じで proc のreturnProc#callを読んだコンテキストから抜けます。

よって、もしトップレベルで

proc { return 1; puts 'top level!' }.call #=> LocalJumpError

とやるとLocalJumpErrorとなります。

一方、lambda の方はKernel#lambdaで作成したオブジェクトの制御を抜けるだけです。

トップレベルで呼び出しても問題ありません。

-> { return 1; puts 'hoge' }.call #=> 1

break の時の挙動が違う

def proc_hoge
  proc { break 1; puts 'hoge' }.call
  'hogehoge'
end

def lambda_hoge
  -> { break 1; puts 'hoge' }.call
  'hogehoge'
end

proc_hoge #=>LocalJumpError
lambda_hoge #=> 'hogehoge'

次にbreakの挙動ですが、proc の場合はbreakで常にLocalJumpErrorが発生します。トップレベルだろうがメソッドの中だろうがエラーです。

一方、lambda の場合はreturnと同様、制御を抜けるだけです。

これはかなり挙動が違うので注意する必要がありそうですね。

対策としてはnextを使うとどちらの場合でも制御を抜けるだけになるので、それを使うといいと思われます。

def proc_hoge
  proc { next 1; puts 'hoge' }.call
  'hogehoge'
end

def lambda_hoge
  -> { next 1; puts 'hoge' }.call
  'hogehoge'
end

proc_hoge #=> 'hogehoge'
lambda_hoge #=> 'hogehoge'

引数に対する厳密さが違う

proc { |x, y| [x, y] }.call(1, 2, 3) #=> [1, 2]
proc { |x, y| [x, y] }.call(1) #=> [1, nil]
proc { |x, y| [x, y] }.call([1, 2]) #=> [1, 2]

# lambda { |x, y| x }.call(1, 2, 3)と同じ
-> (x, y) { x }.call(1, 2, 3) #=> ArgumentError
-> (x, y) { x }.call(1) #=> ArgumentError
-> (x, y) { x }.call([1, 2]) #=> ArgumentError

proc に引数を渡すときは

  • 仮引数の数より多く引数が渡された時、余分な引数は無視する
  • 引数の数が足りない時はnilを渡す
  • 配列が 1 つだけ渡されると展開される

のような特徴があります。

一方、lambda の場合は上記はすべてArgumentErrorになります。なかなか手厳しいですね。

まとめ

以上がKernel#procKernel#lambda でつくった時の Proc オブジェクトの主な違いです。全体的に lambda でつくった Proc オブジェクトのほうがよりメソッドぽい雰囲気がしますね。

参考は安定のこちら。いつもありがとうございます。

Rubyのcase文はwhen節に渡すオブジェクトによって振る舞いが異なる

今回は Ruby の case 文についてです。

Ruby の case 文はどのように値を比較するか

Ruby の case 文では case 節に比較対象となる値を、when 節に case 節の値と比較する値を記述します。 んで、when 節の値をレシーバとして、===メソッドによって比較が行われ、最初に真となった when 節に続く処理が実行されます。

hoge = 'hoge'

case hoge
when 'foo'
  puts 'foooo!'
when 'bar'
  puts 'baaar!'
when 'hoge'
  puts 'hogeee!'
else
  puts "わかりまへーん"
end

#=> hogeee!

このように===で比較するわけで、数値や文字列を比較する時は単純に==的な比較を行います。

しかし、実はいくつかのクラスでは===の振る舞いが異なります。具体的にはRange, Regexp, Proc, ModuleClassです。

Range クラスのオブジェクトと比較する場合

score = 43

case score
when (1..40)
  puts '追試ですよー'
when (41..80)
  puts '微妙ですよー'
when (81..1000)
  puts 'いい感じですよー'
end

#=> 微妙ですよー

Range===では引数が自身の範囲内に含まれていたら真を返します。今回はscore = 43なので追試は間逃れています。

Regexp クラスのオブジェクトと比較する場合

lang = 'ruby'

case lang
when /php/
  puts 'phpですよー'
when /ruby/
  puts 'rubyですよー'
else
  puts 'そんなの知りませんよー'
end

#=> rubyですよー

Regexp===は引数の文字列がマッチした場合に真を返します。今回は簡単な例ですが、ちゃんと正規表現を書けばかなり柔軟な比較ができそうです。

Proc クラスのオブジェクトと比較する場合

obj = 'hoge'

case obj
when proc { |x| x.is_a? String }
  puts 'Stringでしたよー'
when proc { |x| x.is_a? Numeric }
  puts 'Numericでしたよー'
else
  puts 'よくわからないですよー'
end

#=> Stringでしたよー

Proc===では右辺の値を引数にしてProcの処理を実行します。

よって上記のように条件判断のためのProcオブジェクトを仕込んでおくと、case に渡された値を引数として処理を実行するので、柔軟な条件分岐が可能になります。

Class や Module クラスのオブジェクトと比較する場合

obj = 'hoge'

case obj
when String
  puts 'Stringでしたよー'
when Numeric
  puts 'Numericでしたよー'
else
  puts 'よくわからないですよー'
end

#=> Stringでしたよー

ClassModule===では右辺のオブジェクトが左辺のクラス(もしくはそのサブクラス)のインスタンスだった場合、真を返します。 なのでString === Stringfalseですが、String === 'hoge'trueになります。

これは地味に間違えそうなやつですね。

まとめ

クラスによって比較の仕方が違うというのは、知らないとハマりポイントになりそうなので注意が必要です。また、例えばクラスの異なるオブジェクト同士を比較する場合は、どちらをレシーバにするかによって結果が変わる可能性もあるのでそこも注意が必要そうです。

今回も参考にしたのは安定のパーフェクト Ruby でございました。

Rubyのメソッド探索についてまとめてみた

今回はパーフェクト Ruby で「Ruby のメソッド探索」について学んだのでそのメモです。

オブジェクトに対してメソッド呼び出しが行われる際、どのようにメソッドが呼び出されているのか、書いてみたいと思います。

自身のメソッドを呼び出した時

class BaseClass
  def hello
    :hello
  end
end

base_object = BaseClass.new
base_object.hello #=> :hello

f:id:d_animal141:20141223140446p:plain

これは最も単純なケース。 自身のクラスのhelloメソッドを探しにいって、あれば実行します。

親クラスのメソッドを呼び出した時

class InheritClass < BaseClass
end

inherit_object = InheritClass.new
inherit_object.hello #=> :hello

f:id:d_animal141:20141223141122p:plain

この場合、まず Rubyインタプリタinherit_objectのクラスInheritClassを参照しますが、ここにhelloメソッドはありません。なので、次にその親クラスであるBaseClassを探しにいってそこで見つかったhelloメソッドを呼び出します。

特異メソッドを呼び出した時

def base_object.hello
  :singleton_method_hello
end

base_object.hello #=> :singleton_method_hello

f:id:d_animal141:20141223142442p:plain

base_objectの特異メソッドにhelloを定義した場合。これはbase_objectの特異クラスのメソッドなので上図のような関係になります。 特異クラスはオブジェクトのクラスよりも先にメソッド探索されるようになっています。よって今回の場合、インタプリタはまずbase_objectの特異クラスを参照し、そこにあるhelloメソッドを呼び出します。

ちなみにbase_objectの特異クラスにはbase_object.singleton_classでアクセス可能(特異クラスはそれを定義したり、確認しようとしたタイミングで作成される)です。

クラスに include したモジュールのメソッドを呼び出した時

module HelloModule
  def hello_from_module
    :hello_from_module
  end
end

class InheritClass
  include HelloModule
end

inherit_object = InheritClass.new
inherit_object.hello_from_module #=> :hello_from_module

f:id:d_animal141:20141223144035p:plain

モジュールをクラスにincludeした場合、そのモジュールの機能を取り込んだクラスが自身と親クラスの間に挿入されます。そして自分、モジュールの機能を取り込んだクラス、親クラスの順に探索します。

今回の場合はモジュールの機能を取り込んだクラス内でhello_from_moduleを発見し、それを呼び出しています。

ちなみに複数のモジュールをincludeした場合は後からincludeしたモジュールが前に挿し込まれる感じになります。

クラスに extend したモジュールのメソッドを呼び出した時

module HelloModule
  def hello_from_module
    :hello_from_module
  end
end

class InheritClass
  extend HelloModule
end

inherit_object = InheritClass.new
inherit_object.hello_from_module #=> NoMethodError: undefined method ...
InheritClass.hello_from_module #=> :hello_from_module

f:id:d_animal141:20141223151354p:plain

モジュールをクラスにextendした場合、レシーバ(今回はInheritClass)の特異クラスに対してextendしたモジュールがincludeされます。 よって上図のような関係になり、モジュールで定義しているメソッドがInheritClassのクラスメソッドとして使えるようになります。inherit_objectのメソッド探索は直線部なので、破線部にあるhello_from_moduleは見つかりません。

存在しないメソッドを呼び出した時

method_missingメソッドが定義されているクラスを探し続けます。 それが見つからない場合は最終的にBaseObject#method_missingを呼び出し、NoMethodErrorが発生します。

まとめ

特異メソッド、特異クラス、mixin など何気なく使っているものの動作を図で整理できてよかったです。 なにか間違えていたらご指摘いただけると幸いです。

今回も参考にしたのはこちら。