はじめに
Swiftではクラスを使用する必要性がない限り構造体を使用することが推奨されています。
そこで複数の構造体に共通すプロパティをDRYに実装する方法を検討します。
Swiftのバージョンは5.5.2を使用しています。
DRYにする前のコード
例としてStudentとTeacherという2つの構造体を考えてみます。いずれもPersonプロトコルに準拠しています。
以下のコードでは、Personプロトコルで宣言されているプロパティをStudent, Teacherのそれぞれの構造体で定義しています。
// 性別
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)"
}
}
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)
学生 学年:3年生, 氏名:山田太郎, 年齢:15 性別:男性, 血液型: O
教員 役職:教諭, 氏名:坂本金八, 年齢:40 性別:男性, 血液型: A
共通するプロパティを構造体に切り出しDRYに
Student, Teacherの両方の構造体に共通する氏名、年齢等のプロパティ(Personプロトコルで宣言されているプロパティ)をまとめた構造体をPersonCommonとして作成します。さらにこの構造体を保持するPersonBaseというプロトコルを作成します。このプロトコルのエクステンションで氏名、年齢等のプロパティをPersonCommonに委譲しています。PersonCommon, PersonBaeseの宣言はいずれもprivateになっており、他のファイルからは参照できません。
Student, Teacherからは氏名、年齢等のプロパティを削除し、それに代わってPersonCommonをプロパティとして保持するようにし、PersonBaseプロトコルに準拠させます。これによって変更前と変わらずPersonプロトコルで宣言したプロパティにアクセスすることができます。PersonCommonを保持するプロパティはfileprivateとして宣言しているため、外部のファイルからは実装の詳細は隠蔽されます。
変更の前後で呼び出し側のコードであるmain.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つしかなく、また共通するプロパティの数も少ないため、変更後の実装の方がデメリットの方が多いと感じています。
しかし、同じプロパティが多数の構造体に共通していて、かつ、共通するプロパティの数が多い場合は変更後の方法の採用を検討しても良いかなと思いました。
おわりに
今回説明した方法は思いつきなので、他に良い方法がありましたらコメント欄で教えていただけると助かります。