🐕

Realmを扱うRepositoryとUseCseをジェネリクスに見直す

2024/02/04に公開

概要

SwiftでRealmでデータベースを扱う際に、これって、「ジェネリクス」にしたら、汎用的で、使い回し、今後楽に実装できるのではないかと考え、本記事にまとめたものである。
主に個人的なメモになっており、「ジェネリクス」にしたらどうなるか、に焦点を置いている。
そのため、Realmの導入や扱い方などについては省略している。
RepositoryパターンやDI、クリーンアーキテクチャを意識して記述したつもりである。

環境

以下に環境を示す

開発環境

  • Xcode:15.2
  • Swift:5.9.2
  • maxOS:14.1.2
  • Macbook Air 8G 256GB

ライブラリ環境

  • Realm:13.26

想定する読者層

  • 自身でSwiftを用いたiOSアプリ開発について学んでいる方
  • ジェネリクスとはなんぞやと思っている方
  • Realmのデータベースの扱いで、オブジェクト(データモデル)毎に似たようなコードを書くことに疑問や手間を感じ始めた方(私)

ジェネリクスとは

「型」をパラメータとして受け取ることで汎用的なプログラムを記述するための機能のこと[1]

基本的な定義方法と比較

以下のように関数名の後に<>で型引数を定義する。

func 通常の関数(引数名:型) {
    処理の内容
}

func ジェネリック関数<**型引数**>(引数名: **型引数**) {
    処理の内容
}

今回扱うデータモデル

以下のような運動に関するデータモデルをクラスとして定義した。
とても単純に、運動した日付とその回数を記録するためだけの構造である。

ExerciseData.swift
import Foundation
import RealmSwift

class ExerciseData : Object, Identifiable {
    /// 一意の値
    @Persisted (primaryKey: true) var _id: ObjectId
    /// 記録日
    @Persisted var recordDate: Date
    /// 記録回数
    @Persisted var recordCount: Int
}

このデータを扱うにあたって、はじめに実装した内容

ジェネリクスを意識する前は、以下のように実装していた。
メソッドは省略し、全権取得とデータの追加のみ記載。

Repository

データの永続化を目的とし、データベースの読み取りや更新処理(追加や削除)を担う。

ExerciseDataRepository.swift
import Foundation
import RealmSwift

protocol ExerciseDataRepositoryProtocol {
    func getAll() throws -> Results<ExerciseData>
    func add(_ object: ExerciseData) throws
}

final class ExerciseDataRepository: ExerciseDataRepositoryProtocol {
    func getAll() throws -> Results<ExerciseData> {
        do {
            let realm = try Realm()
            let objects = realm.objects(ExerciseData.self)
            return objects
            }
        } catch {
            throw error
        }
        
    }
    
    func add(_ object: ExerciseData) throws {
        do {
            let realm = try Realm()
            try realm.write {
                realm.add(object.self)
            }
        } catch {
            throw error
        }
    }
}

UseCase

ビジネスロジックを担当し、Repositoryから取得やRepositoryに値を返す処理を行う。

ExerciseDataUse.swift
import Foundation
import RealmSwift

protocol ExerciseDataUseCaseProtocol {
    func getAll() throws -> Results<ExerciseData>
    func add(_ object: ExerciseData) throws
}

final class ExerciseDataUseCase: ExerciseDataUseCaseProtocol {
    private let repository: ExerciseDataRepository
    
    init(repository: ExerciseDataRepository) {
        self.repository = repository
    }
    
    func getAll() throws -> Results<ExerciseData> {
        try repository.getAll()
    }
    
    func add(_ object: ExerciseData) throws {
        try repository.add(object)
    }
}

ジェネリクスを用いた場合

まずはジェネリクスでRealmのデータベースを扱うための汎用的なRepositoryとUseCaseを定義する。

汎用的なReposiotry

RealmRepository.swift
import Foundation
import RealmSwift

/// RealmのDatabaseを汎用的に扱うRepositpryのProtocol
protocol RealmRepositoryProtocol {
    /// DataModelという、ジェネリクス型を定義
    /// RealmのObject型であるという制約を与える
    associatedtype DataModel: Object
    
    func getAll() throws -> Results<DataModel>
    func add(_ object: DataModel) throws
}

/// Repository:Realmのデータベースへの追加・更新・削除などの操作を行う
class RealmRepository<DataModel: Object>: RealmRepositoryProtocol {
    func getAll() throws -> Results<DataModel> {
        do {
            let realm = try Realm()
            let objects = realm.objects(DataModel.self)
            return objects
        } catch {
            throw error
        }
    }
    
    func add(_ object: DataModel) throws {
        do {
            let realm = try Realm()
            try realm.write {
                realm.add(object.self)
            }
        } catch {
            throw error
        }
    }
}

汎用的なUseCase

RealmUseCase.swift
import Foundation
import RealmSwift
/// ジェネリクスに対応したUseCaseのProtocol
protocol RealmUseCaseProtocol {
    associatedtype DataModel: Object
    
    func getAll() throws -> Results<DataModel>
    func add(_ object: DataModel) throws
}

class RealmUseCase<DataModel: Object>: RealmUseCaseProtocol {
    
    private let repository: RealmRepository<DataModel>
    
    init(repository: RealmRepository<DataModel>) {
        self.repository = repository
    }
    
    func getAll() throws -> Results<DataModel> {
        try repository.getAll()
    }
    
    func add(_ object: DataModel) throws {
        try repository.add(object)
    }
}

ExerciseDataを扱うRepository

RealmRepositoryProtocolを継承したProtocolを定義することで、そのデータモデル固有の処理がない場合、簡略化できる。
Repositoryには、RealmRepository<ExerciseData>のように、実際に扱うデータモデルの型を反映させて継承させる。

ExerciseDataRepository.swift
import Foundation
import RealmSwift

protocol ExerciseDataRepositoryProtocol: RealmRepositoryProtocol {
    // ExerciseData固有のメソッドなどを追加する
}

final class ExerciseDataRepository: RealmRepository<ExerciseData>, ExerciseDataRepositoryProtocol {
    // ExerciseData固有のメソッドなどを追加する
}

ExerciseDataを扱うUseCase

UseCaseについてもRepository同様である。
例えば以下のように、データベースの中で最も多い記録回数を取得したい場合は、メソッドを追加することで、実際に扱うことが可能になる。

ExerciseDataUse.swift
import Foundation
import RealmSwift

protocol ExerciseDataUseCaseProtocol: RealmUseCase<ExerciseData> {
    // ExerciseData固有のメソッドなどを追加する
    func getMaxRecordCount() -> Int?
}

final class ExerciseDataUseCase: RealmUseCase<ExerciseData>, ExerciseDataUseCaseProtocol {
    // ExerciseData固有のメソッドなどを追加する
    func getMaxRecordCount() throws -> Int? {
        do {
            let exerciseData = try getAll()   
            let maxRecordCount = exerciseData.max(ofProperty: "recordCount") as Int?
            return maxRecordCount
        } catch {
            throw error
        }
    }
}

これで何が良くなったのか?

これではコード量が増えただけで、メリットは感じられないだろう
しかし、このように汎用性を持たせることで、追加で食事の記録を扱いたい、と考えた時、本領を発揮する
以下のように、Repository[2]やUseCaseを、少ないコード量で実装することができる

食事記録のデータを別途Realmで扱いたい

簡単にするため、食事の時間と食べたもの1つを登録するモデルを考える。

食事記録のデータモデルを作成

mealData.swift
import Foundation
import RealmSwift

class MealData : Object, Identifiable {
    /// 一意の値
    @Persisted (primaryKey: true) var _id: ObjectId
    /// 食べた時間
    @Persisted var recordTime: Date
    /// 食べたもの
    @Persisted var meal: String
}

Repositoryを作成

基本的にReposiotyはデータの永続化に関するものであり、筆者は追加する処理はないと認識している。

MealDataRepository.swift
import Foundation
import RealmSwift

protocol MealDataRepositoryProtocol: RealmRepositoryProtocol {
    // MealData固有のメソッドなどを追加する
}

final class MealDataRepository: RealmRepository<MealData>, MealDataRepositoryProtocol {
    // MealData固有のメソッドなどを追加する
}

UseCaseを作成

特に特別な処理を行わず、データベース上での操作しか行わない場合である。

MealDataUse.swift
import Foundation
import RealmSwift

protocol MealDataUseCaseProtocol: RealmUseCase<MealData> {
    // MealData固有のメソッドがなければ特に記載しない
}

final class ExerciseDataUseCase: RealmUseCase<MealData>, MealDataUseCaseProtocol {
    // MealData固有のメソッドがなければ特に記載しない
}

結論

先述したように、同じようにデータベースを扱う際に、扱うデータの種類が増えれば増えるほど、同じ処理を記述することが減る、というメリットがある。
個人開発の中ではあまり扱うことは少ないかもしれない。しかし、複数データを扱いたいが、繰り返し書くことが面倒だな、と感じた場合は、汎用性を持たせることを意識すると良いのではないかと思われる。

最後に

本記事の内容は、以下のような学習中の人物が記載した記事であり、説明不十分な箇所や、認識が誤っている記述もあると思われる。
https://zenn.dev/akkii/articles/46e30c4e053449
また、コードの書き方や、記事の書き方などでも不十分なところが多々見受けられると思うが、温かい目で見ていただければと思う。
また、温かなご指摘やコメントをいただければ、幸いである。

参考文献

  • 書籍:Swift実践入門
  • Realm公式ドキュメント
脚注
  1. 書籍:Swift実践入門より ↩︎

  2. 今回の場合Repositoyはデータベースを扱う処理になるため、そのデータモデル固有の処理がないものと筆者は考えており、必要性は現状ないのではと考えている。コメントお待ちしてます ↩︎

Discussion