1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Swiftで複数の構造体に共通するプロパティをDRYに実装する

Posted at

はじめに

Swiftではクラスを使用する必要性がない限り構造体を使用することが推奨されています。
そこで複数の構造体に共通すプロパティをDRYに実装する方法を検討します。

Swiftのバージョンは5.5.2を使用しています。

DRYにする前のコード

例としてStudentとTeacherという2つの構造体を考えてみます。いずれもPersonプロトコルに準拠しています。
以下のコードでは、Personプロトコルで宣言されているプロパティをStudent, Teacherのそれぞれの構造体で定義しています。

Person.swift
// 性別
enum Gender: CustomStringConvertible {
    case male
    case female
    case other
    case notApplicable
    
    var description: String {
        switch self {
        case .male: return "男性"
        case .female: return "女性"
        case .other: return "その他"
        case .notApplicable: return "回答しない"
        }
    }
}

// 血液型
enum BloodType: CustomStringConvertible {
    case a
    case b
    case o
    case ab
        
    var description: String {
        switch self {
        case .a: return "A"
        case .b: return "B"
        case .o: return "O"
        case .ab: return "AB"
        }
    }
}

// 人プロトコル
protocol Person {
    var name: String { get }
    var age: Int { get }
    var gender: Gender { get }
    var bloodType: BloodType { get }
}

// 生徒
struct Student: Person, CustomStringConvertible {
    let name: String
    let age: Int
    let gender: Gender
    let bloodType: BloodType
    let grade: Int
    
    var description: String {
       "学生 学年:\(grade)年生, 氏名:\(name), 年齢:\(age) 性別:\(gender), 血液型: \(bloodType)"
    }
}

// 教員
struct Teacher: Person, CustomStringConvertible {
    let name: String
    let age: Int
    let gender: Gender
    let bloodType: BloodType
    let jobTitle: String

    var description: String {
        "教員 役職:\(jobTitle), 氏名:\(name), 年齢:\(age) 性別:\(gender), 血液型: \(bloodType)"
    }
}
main.swift
let student: Person = Student(name: "山田太郎", age: 15, gender: .male, bloodType: .o, grade: 3)
let teacher: Person = Teacher(name: "坂本金八", age: 40, gender: .male, bloodType: .a, jobTitle: "教諭")

print(student)
print(teacher)
出力結果.txt
学生 学年:3年生, 氏名:山田太郎, 年齢:15 性別:男性, 血液型: O
教員 役職:教諭, 氏名:坂本金八, 年齢:40 性別:男性, 血液型: A

共通するプロパティを構造体に切り出しDRYに

Student, Teacherの両方の構造体に共通する氏名、年齢等のプロパティ(Personプロトコルで宣言されているプロパティ)をまとめた構造体をPersonCommonとして作成します。さらにこの構造体を保持するPersonBaseというプロトコルを作成します。このプロトコルのエクステンションで氏名、年齢等のプロパティをPersonCommonに委譲しています。PersonCommon, PersonBaeseの宣言はいずれもprivateになっており、他のファイルからは参照できません。

Student, Teacherからは氏名、年齢等のプロパティを削除し、それに代わってPersonCommonをプロパティとして保持するようにし、PersonBaseプロトコルに準拠させます。これによって変更前と変わらずPersonプロトコルで宣言したプロパティにアクセスすることができます。PersonCommonを保持するプロパティはfileprivateとして宣言しているため、外部のファイルからは実装の詳細は隠蔽されます。

変更の前後で呼び出し側のコードであるmain.swiftと出力結果は変わりません。

Person.swift
// 性別
enum Gender: CustomStringConvertible {
    case male
    case female
    case other
    case notApplicable
    
    var description: String {
        switch self {
        case .male: return "男性"
        case .female: return "女性"
        case .other: return "その他"
        case .notApplicable: return "回答しない"
        }
    }
}

// 血液型
enum BloodType: CustomStringConvertible {
    case a
    case b
    case o
    case ab
    
    var description: String {
        switch self {
        case .a: return "A"
        case .b: return "B"
        case .o: return "O"
        case .ab: return "AB"
        }
    }
}

// 人プロトコル
protocol Person {
    var name: String { get }
    var age: Int { get }
    var gender: Gender { get }
    var bloodType: BloodType { get }
}

// 人のプロパティを構造体として定義
// privateなのでファイルの外からは参照不可能
private struct PersonCommon {
    let name: String
    let age: Int
    let gender: Gender
    let bloodType: BloodType
}

// PersonCommonを保持するプロトコル
// privateなのでファイルの外からは参照不可能
private protocol PersonBase {
    var common: PersonCommon { get }
}

// Personプロトコルのプロパティを委譲を用いて定義する
extension PersonBase {
    var name: String { common.name }
    var age: Int { common.age }
    var gender: Gender { common.gender }
    var bloodType: BloodType { common.bloodType }
}

// 生徒(新たにPersonBaseに準拠させる)
struct Student: Person, PersonBase, CustomStringConvertible {
    fileprivate let common: PersonCommon
    let grade: Int
    
    var description: String {
       "学生 学年:\(grade)年生, 氏名:\(name), 年齢:\(age) 性別:\(gender), 血液型: \(bloodType)"
    }
    
    init(name: String, age: Int, gender: Gender, bloodType: BloodType, grade: Int) {
        self.common = PersonCommon(name: name, age: age, gender: gender, bloodType: bloodType)
        self.grade = grade
    }
}

// 教員(新たにPersonBaseに準拠させる)
struct Teacher: Person, PersonBase, CustomStringConvertible {
    fileprivate let common: PersonCommon
    let jobTitle: String
    
    var description: String {
        "教員 役職:\(jobTitle), 氏名:\(name), 年齢:\(age) 性別:\(gender), 血液型: \(bloodType)"
    }
    
    init(name: String, age: Int, gender: Gender, bloodType: BloodType, jobTitle: String) {
        self.common = PersonCommon(name: name, age: age, gender: gender, bloodType: bloodType)
        self.jobTitle = jobTitle
    }
}

変更後のコードの問題点

変更後のコードには、Student, Teacherで共通していたプロパティが削除されDRYになったかわりに以下のデメリットがあります。

  • コードが複雑になり、可読性も下がっている。
  • DRYにするために用いたプロトコルや構造体を他のファイルから隠蔽するためにprivateとして宣言しているため、Personプロトコルに準拠する構造体を全て同じファイル内に定義する必要がある。

上で示したコード例ではプロパティが共通している構造体の数は2つしかなく、また共通するプロパティの数も少ないため、変更後の実装の方がデメリットの方が多いと感じています。
しかし、同じプロパティが多数の構造体に共通していて、かつ、共通するプロパティの数が多い場合は変更後の方法の採用を検討しても良いかなと思いました。

おわりに

今回説明した方法は思いつきなので、他に良い方法がありましたらコメント欄で教えていただけると助かります。

1
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?