TableViewメソッドの引数として与えられるセクションの情報を、値オブジェクト(ValueObject)を通して利用する

15563 ワード

Swift中心にiOSアプリの開発を学んでいます。学習期間は1年半程度の初心者の記事ですので、誤りがあればご指摘いただけるとありがたいです。

この記事は、TableViewメソッドの引数から渡されているInt型のセクションの値を、自身で実装したメソッドに渡そうとする際に正しく渡せない可能性がある対策として、値オブジェクト(ValueObject)を利用する方法について記載しています。

引数として受け取るプリミティブな型の問題

TableViewを利用する場合にはいくつかのProtocolに準拠するために、メソッドを実装しなくてはなりません。その際に、たびたび登場するのがIndexType型です。IndexType型はあくまで型であって、利用する際には型のプロパティであるrowやsectionを取り出して扱います。またメソッドによっては直接sectionをInt型として渡すものもあります。

こういう形です、誰もが一度は見ているんじゃないかと思います。

 func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
	//ここではsectionという引数でInt型が渡される
    }
    
// 各セルの内容を返すメソッド
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
       //ここではindexPathという引数でIndexPath型が渡される
    }

これらのメソッドはUIKitのライブラリの中で自動的に引数に対して値を渡してくれるため使い分けは問題になりません。けれど、自身のメソッドで.row.sectionでInt型を受け取る場合には注意が必要になることがあります。

たとえば、TableViewメソッド内でセクションの情報を扱う場合を考えると、最終的に扱いたいのはセクションを指定するためのInt型の値のはずです。その際に、自身で作ったメソッドには引数として何を渡すべきでしょうか、IndexPath型でしょうか。

TableViewのメソッドは上記のようにIndexPath型だけでなく直接sectionという引数名でInt型が渡されることもあるため、IndexPathを受け取るというだけにしてしまうのは難しそうです。であれば、必要になるのがInt型であるから、Int型を指定すればよいでしょうか。

Int型にしても注意が必要になるのがTableViewのメソッドからIndexPath型が与えられた時の判断です。

引数として受け取るindexPathはIndexPath型であり、Int型ではありません。そのプロパティとしてInt型の.row.sectionがあります。もし値を受け取るのであれば、それらのプロパティから値を取り出して使う必要があります。

Int型というのはプリミティブ、基本になる型であるから、かなり広い範囲で値として与えられてしまう可能性が残っています。それは本来必要な値以外が与えられてしまう危うさ、保守性の低さにつながります。コードを理解している間は、「ここに.sectionの値を入れるんだ」と考えていても、場合によっては.rowを用いて必要なセクションを取り出す場合も出てきたりすることも考えられ、次第に把握できなくなってくるかもしれません。indexPath.sectionを与えるべきところで、indexPath.rowを与えても、Int型であるから通ってしまうことも問題です。わかりやすい命名は一つの対策にはなりますが、保守性の低さは命名の難しさや、不自然さにつながってしまうように感じます。

値オブジェクト(ValueObject) での解決

次第にセクションのために利用する値としてなにを引数にとればよいか命名から判断できずに立ち止まってしまうかもしれません。そのような危うさの解消、言い換えれば保守性を向上するために利用できる考え方として値オブジェクト(VO:ValueObject) があります。

どんなところからも受け取りやすいプリミティブな型ではなく、その型を与えるための構造体(もしくはクラス) を作り引数にすることで、目的としていないところから値を受け取ってしまうことをふせぐことができます。

たとえば上記のコードに対して、Sectionという構造体を作り引数に指定することで、目的とする領域を絞りやすくなり、イニシャライズの際の命名もいくらか自然になります。

//ValueObject
struct Section {
    //値は不変
    let number: Int
    
    //IndexPath型を通す場合
    init(at indexPath: IndexPath) {
        self.number = indexPath.section
    }
    
    //sectionを直接渡す場合
    init(_ section: Int) {
        self.number = section
    }
}

IndexPath型については、直接渡してしまって内部で.sectionを呼ぶ方が間違いがありません。もしこれをInt型を受け取るようにしてしまうと、結局.rowを渡すのか.sectionを渡すのか、その判断を求められてしまうからです。そのあたりはイニシャライザ内であらかじめ処理しておければ問題になりません。

ダメなコード

ちなみにはじめはそこまでは考えが至らず、値オブジェクトで作った構造体も次のように書いてしまっていました。

struct Section {
    let number: Int
    
    //Int型を渡す場合
    init(at section: Int) {
        self.number = section
    }
}

//これをIndexPath型あるいはsectionで渡されるInt型で利用しようとすれば
let section = Section(at: indexPath.section)
let section2 = Section(at: indexPath.row) 
let section3 = Section(at: section)

indexPath.sectionを含めて、セクションを指定するために セクション(at: セクション) みたいな名前になってしまっています。また、section2のように.rowを受け取れてしまう余地があります。そこでIndexPath型を直接渡してイニシャライザ内で処理をしてあげることで.rowを受け取る余地をなくし,外部引数名にしているatはIndexPath型を受け取る場合のみにすることで命名も少しだけ自然になりました。

Section(_ section: Int)は、受け取る際にSection(section)ですが...

値オブジェクトをこのように利用することで、セクションをコードから読んで指定することができます。自前のメソッドでうまく書けていなくてわかりづらいかもしれませんが。

//①検索されたかどうかで並べ替えて返すメソッド
   func searchedResultFoods(in section: Section) -> [Food] {
        let resultedFoods: [Food]
        if searchController.isActive {
            resultedFoods = arrangeCell(of: searchedFoodList, in: section)
        } else {
            resultedFoods = arrangeCell(of: foodList, in: section)
        }
        return resultedFoods
    }
    
 //②カテゴリー分けされたセクション毎並べ替えて返すメソッド
        func arrangeCell(of foods: [Food], in section: Section) -> [Food] {
        // section.numberでInt型を受け取る
        let sectionCategory = CategoryType.allCases[section.number]
        //略
    }

実際に利用する際には、indexPath.rowのように、section.numberとして利用します。このように利用することで、先ほどのTableViewのメソッド内で次のようにIndexPath型もInt型も受け取れて、それをSectionというわかりやすい名前の構造体で返してあげることができます。

   // データの数(=セルの数)を返すメソッド
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    	//...略...
	//Sectionにsectionを入れる形になっているのだけがちょっとつらい
        let searchedResultFoods = searchedResultFoods(in: Section(section))
	}
    // 各セルの内容を返すメソッド
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {  
        //...略...
        let searchedResultFoods = searchedResultFoods(in: Section(at: indexPath))
    }

この値オブジェクトについてはDDDの入門本で読んで頭には入れていたのですが、なるほどと思いながら具体的に利用できたことがありませんでした。またアプリ道場サロンでValueObjectを利用するという話を聞いても、それが値オブジェクトのことだと結びついておらず、あまり理解できていなかったように思います。

この例からわかることは、システムに最適な値が必ずしもプリミティブな値であるとは限らないということです。システムには必要とされる処理にしたがって、そのシステムならではの値の表現があるはずです。

成瀬 允宣. ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本
(Japanese Edition) (p.48). Kindle 版.

値オブジェクトの性質

詳しくは下記で参考に挙げている書籍を読んでいただくとして、今回取り上げた値オブジェクトはその性質として次の要素が挙げられます。

・不変である(値)
・交換が可能である(代入)
・等価性によって比較される(例えばEquatble)

があります。特に不変に関しては、今回読み返してとても勉強になりました。

まとめ

自前のメソッドでセクションの情報を利用したい場合、値オブジェクト(ValueObject)を利用し引数として与えることで、必要とする値を限定しやすくなり、命名も無理がなくなります。結果的に、コードから意図を読みやすくすることができます。

参考

今回値オブジェクトを利用すること考えたのは、この方の記事でValueObject=値オブジェクトということを知って、問題として抱えていたセクションの扱いにこの考えが当てはめられると気付いたのがきっかけです。とても勉強になりました。