はじめに
有名な設計の原則にSOLID原則というものがあります。SOLID原則とはいったいなんなのかを今回は解説したいと思います。
「S」ingle Responsibility Principle(単一責任原則)
「クラスを変更する理由はふたつ以上存在してはならない」と言う原則です。
以下のクラスはこの原則に違反しているでしょうか?
class User {
let name: String
let age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
func introduce() {
print("\(self.name)です、\(self.age)歳です。")
}
}
これは違反していないと言えますね。責任が一つしかないからです。では、違反している例を見てみましょう。
class User {
let name: String
let age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
func introduce() {
print("\(self.name)です、\(self.age)歳です。")
}
func nameAlert() {
print("\(name)アラート")
}
}
これは単一責任原則に違反しています。これでは、ユーザー名を保持する役割とユーザー名に応じてアラートを表示する役割を持つことになります。つまり、クラスを変更する理由がふたつ存在してしまいます。以下のように修正してみましょう。
class User {
let name: String
let age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
func introduce() {
print("\(self.name)です、\(self.age)歳です。")
}
}
class NameAlert {
static func present(name: String) {
print("\(name)アラート")
}
}
let user = User(name: "REON", age: 20)
NameAlert.present(name: user.name) // REONアラート
Userクラスは単一責任になっていますね。このようにすることで、以下のようにAnimalクラスにもこのアラートを使い回しすることができ、再利用性が高くなります。
class Animal {
let name: String
let owner: String
init(name: String, owner: String) {
self.name = name
self.owner = owner
}
}
let animal = Animal(name: "HARINEZUMI", owner: "REON")
NameAlert.present(name: animal.name) // HARINEZUMIアラート
Userクラスに書かれたままになっていれば、同じアラートの処理をAnimalクラスにも書かなければいけませんでした。
「O」pen/Closed Principle(開放閉鎖の原則)
「クラスは拡張に対して開いていて、修正に対して閉じていなければならない」という原則です。
この原則は、オブジェクト指向設計の核心だと言われています。
もう少し詳しくみていくと、以下のようなことをこの原則は表現しています。
・拡張に対して開いている(Open): 仕様が変更されても、モジュールに新たな振る舞いを追加することで変更に対処することができる。
・修正に対して閉じている(Closed): モジュールの振る舞いを拡張してもそのソースコードやバイナリコードは影響を受けない
これでも正直わかりづらいです。簡単に言うと、「変わらない部分」と「変わりやすい部分」を分離しましょうと言うことです。
以下のコードをみてください。
struct Language {
let name: String
}
let languages = [
Language(name: "Swift"),
Language(name: "Kotlin"),
]
func printLanguageDescription(languages: [Language]) {
for language in languages {
if language.name == "Swift" {
print("Swiftの由来はアマツバメだよ!")
} else if language.name == "Kotlin" {
print("Kotlinの由来は開発された場所の近くにあったコトリン島だよ!")
}
}
}
printLanguageDescription(languages: languages)
//Swiftの由来はアマツバメだよ!
//Kotlinの由来は開発された場所の近くにあったコトリン島だよ!
このような実装では、新しく言語が追加されたときにprintLanguageDescriptionメソッドを修正しないといけなくなります。
struct Language {
let name: String
}
let languages = [
Language(name: "Swift"),
Language(name: "Kotlin"),
Language(name: "Go"),
]
func printLanguageDescription(languages: [Language]) {
for language in languages {
if language.name == "Swift" {
print("Swiftの由来はアマツバメだよ!")
} else if language.name == "Kotlin" {
print("Kotlinの由来は開発された場所の近くにあったコトリン島だよ!")
} else if language.name == "Go" {
print("Goの由来は開発したのがGoogleだからだよ!")
}
}
}
printLanguageDescription(languages: languages)
//Swiftの由来はアマツバメだよ!
//Kotlinの由来は開発された場所の近くにあったコトリン島だよ!
//Goの由来は開発したのがGoogleだからだよ!
このように、言語を追加することでこのコードでは変更に硬く、扱いづらいのがわかります。
以下のように修正してみます。
protocol Language {
var name: String { get set }
func printLanguageDescription()
}
struct Swift: Language {
var name: String
func printLanguageDescription() {
print("Swiftの由来はアマツバメだよ!")
}
}
struct Kotlin: Language {
var name: String
func printLanguageDescription() {
print("Kotlinの由来は開発された場所の近くにあったコトリン島だよ!")
}
}
struct Go: Language {
var name: String
func printLanguageDescription() {
print("Goの由来は開発したのがGoogleだからだよ!")
}
}
let languages: [Language] = [
Swift(name: "Swift"),
Kotlin(name: "Kotlin"),
Go(name: "Go"),
]
for language in languages {
language.printLanguageDescription()
}
//Swiftの由来はアマツバメだよ!
//Kotlinの由来は開発された場所の近くにあったコトリン島だよ!
//Goの由来は開発したのがGoogleだからだよ!
コード量は多くなってしまいますが、このようにすることで、今後言語が増えたときもLanguageプロトコルを準拠させることで問題なさそうです。
「L」iskov Substitution Principle(リスコフの置換原則)
「継承よりプロトコルコンポジション」という金言もあるので、この原則については詳しく解説しませんが、簡単に言うと、
「サブクラスは、そのスーパークラスで代用可能でなければならない」という原則です。
つまり、スーパークラスにできてサブクラスにできないことがあってはいけません。
「I」nterface Segregation Principle(インターフェース分離の原則)
「クライアントに、クライアントが利用しないメソッドへの依存を強制してはならない」という原則です。
例えば、以下のようなコードはこの原則に違反しています。
protocol AnimalProtocol {
func fly()
func eat()
func sleep()
}
class Bird: AnimalProtocol {
func fly() {
print("飛ぶ")
}
func eat() {
print("食べる")
}
func sleep() {
print("寝る")
}
}
class Human: AnimalProtocol {
func fly() {
print("飛ぶ")
}
func eat() {
print("食べる")
}
func sleep() {
print("寝る")
}
}
Humanクラスがflyメソッドを実装することを強制されています。これは、fly,sleep,eatメソッドを宣言したAnimalProtocolをHumanクラスが準拠しているためです。これではAnimalProtocolの使える範囲が限られてしまいますし、不要なメソッドを実装しないといけなくなります。
以下のように修正してみましょう。
protocol SkyAnimalProtocol {
func fly()
}
protocol AnimalProtocol {
func eat()
func sleep()
}
class Bird: SkyAnimalProtocol, AnimalProtocol {
func fly() {
print("飛ぶ")
}
func eat() {
print("食べる")
}
func sleep() {
print("寝る")
}
}
class Human: AnimalProtocol {
func eat() {
print("食べる")
}
func sleep() {
print("寝る")
}
}
こうすることで、Humanクラスでflyメソッドを書かなくてすむようになりました。
「D」ependency Inversion Principle(依存関係逆転の原則)
「上位レベルのモジュールは下位レベルのモジュールに依存するべきではない、抽象に依存するべきである」、
「抽象は詳細に依存してはならない。詳細が抽象に依存すべきである」という原則です。
まず、一つ目の「上位レベルのモジュールは下位レベルのモジュールに依存するべきではない、抽象に依存するべきである」からみていきましょう。
あるモジュールは具体的な型ではなく、抽象(インターフェース=プロトコル)に依存していれば、その依存対象の具体型は差し替え可能です。
さらに、抽象化をすることにより、再利用性が高くなります。
上位レベルのモジュールは下位レベルのモジュールに依存しているパターンは以下のようなものです。
class BlackCore {
let color = "black"
func printColor() {
print(self.color)
}
}
class BallPen {
let core: BlackCore
init(core: BlackCore) {
self.core = core
}
func printColor() {
core.printColor()
}
}
let blackCore = BlackCore()
let pen = BallPen(core: blackCore)
pen.printColor() // black
これでは、上位モジュールであるBallPenクラスが下位モジュールであるBlackCoreクラスに依存してしまっています。黒以外の芯に変えたいときにBallPenクラスに修正が必要になります。
これを以下のように修正します。
protocol Core {
func printColor()
}
class BlackCore: Core {
let color = "black"
func printColor() {
print(self.color)
}
}
class RedCore: Core {
let color = "red"
func printColor() {
print(self.color)
}
}
class BallPen {
let core: Core
init(core: Core) {
self.core = core
}
func printColor() {
core.printColor()
}
}
let blackCore = BlackCore()
let redCore = RedCore()
let blackBallPen = BallPen(core: blackCore)
blackBallPen.printColor() // black
let redBallPen = BallPen(core: redCore)
redBallPen.printColor() // red
BallPenクラスは特定の芯に依存していないため、新しい色の芯クラスを作成してもBallPenクラスを変更する必要は無くなりました。
おわりに
SOLID原則を意識することで、より良い設計になりそうですね!