SOLID 🤷🏻♂️
- 아래의 5가지 원칙의 앞 글자를 따서 만들어진 SOLID 원칙 ▾
- S ( Single Responsibility Principle ) : 단일 책임 원칙
- O ( Open / Close Principle ) : 개방/폐쇄 원칙
- L ( Liscov Substitution Principle ) : 리스코브 치환 원칙
- I ( Interface Segregation Principle ) : 인터페이스 분리 원칙
- D ( Denpendency Inversion Principle ) : 의존성 역전 원칙
Why used it ❓
- 쉽게 변경이 가능한 유연한 코드를 가지게 됩니다. 이것은 재사용과 유지 관리가 가능합니다.
- 그렇게 개발된 소프트웨어는 안정되고, 탄탄하며 확장성이 뛰어날 것입니다. ( 새로운 기능도 쉽게 추가할 수 있습니다. )
- 디자인 패턴의 사용과 함께 응집력이 높고(시스템의 요소가 밀접하게 연관되어 있는), 결합도가 낮은(요소들 간의 의존도가 낮은) 소프트웨어를 만들 수 있습니다.
Single Responsibility Principle 단일 책임 원칙 ▼
정의
- 이 원칙에 따르면, 클래스는 하나의 이유로만 변화해야 합니다.
- 클래스는 한 가지의 책임만 가지고 있어야 합니다.
class LoginUser {
func login() {
let data = authenticareuserViaAPI()
let user = decodeUser(data: data)
saveToDB(array: array)
}
private func authenticareUserViaAPI() -> Data {
// Call server to authenticate and return user's info
}
private func decodeUser(data: Data) -> User {
// Decode data (Codable protocol) into User object
}
private func saveUserInfoOnDatabase(user: User) {
// Save User info onto database
}
}
- 위 코드는 인증 & 디코딩 & 저장의 3가지 책임을 가지고 있습니다.
- 단일 책임 원칙을 충족하기 위해서는 이 책임을 각각의 작은 클래스로 분리해야 합니다.
class LoginUser {
let oAuthHandler: OAuthHandler
let decodeHandler: DecodeHandler
let databaseHandler: DataBaseHandler
init(oAuthHandler: OAuthHandler, decodeHandler: DecodeHandler, databaseHandler: DataBaseHandler) {
self.oAuthHandler = oAuthHandler
self.decodeHandler = decodeHandler
self.databaseHandler = databaseHandler
}
func login() {
let data = oAuthHandler.authenticareUserViaAPI()
let user = decodeHandler.decodeUser(data: data)
databaseHandler.saveUserInfoOnDatabase(user: user)
}
}
class OAuthHandler {
func authenticareUserViaAPI() -> Data {
// Call server to authenticate and return user's info
}
}
class DecodeHandler {
func decodeUser(data: Data) -> User {
// Decode data (Codable protocol) into User object
}
}
class DataBaseHandler {
func saveUserInfoOnDatabase(user: User) {
// Save User info onto database
}
}
같은 이유로 변하는 것을 모읍시다 ! 그리고 다른 이유로 변경되는 것을 분리합시다 !
Open/Close Principle 개방/폐쇄 원칙 ▼
정의
- 이 원칙에 따르면, 확장은 클래스의 행동의 변화 없이 가능해야 합니다.
- 확장에는 열려 있고, 변경에는 닫혀 있어야 합니다. → 이것은 추상화에 의해 가능해집니다.
예제 코드
class Scrapper {
func scrapVehicles() {
let cars = [
Car(brand: "Ford"),
Car(brand: "Peugeot"),
Car(brand: "Toyota"),
]
cars.forEach { car in
print(car.getScrappingAddress())
}
let trucks = [
Truck(brand: "Volvo"),
Truck(brand: "Nissan"),
]
trucks.forEach { truck in
print(truck.getScrappingAddress())
}
}
}
class Car {
let brand: String
init(brand: String) {
self.brand = brand
}
func getScrappingAddress() -> String {
return "Cars scrapping address"
}
}
class Truck {
let brand: String
init(brand: String) {
self.brand = brand
}
func getScrappingAddress() -> String {
return "Trucks scrapping address"
}
}
- 각각의 새로운 타입의 vehicle에 대해 getScrapingAddress()는 다시 구현되어야만 합니다.
- 이는 개방/폐쇄 원칙을 어기게 됩니다.
- 이것을 해결하기 위해 getScrapingAddress() 메소드를 포함한 Scrappable 프로토콜을 만듭니다.
protocol Scrappable {
func getScrapingAddress() -> String
}
class Scrapper {
func getScrapingAddress() {
let vehicles: [Scrappable] = [
Car(brand: "Ford"),
Car(brand: "Peugeot"),
Car(brand: "Toyota"),
Truck(brand: "Volvo"),
Truck(brand: "Nissan"),
]
vehicles.forEach { vehicle in
print(vehicle.getScrapingAddress())
}
}
}
class Car: Scrappable {
let brand: String
init(brand: String) {
self.brand = brand
}
func getScrapingAddress() -> String {
return "Cars scrapping address"
}
}
class Truck: Scrappable {
let brand: String
init(brand: String) {
self.brand = brand
}
func getScrapingAddress() -> String {
return "Trucks scrapping address"
}
}
커맨드 패턴 공부하면서 프로토콜을 왜 사용하는지 궁금했는데 개방/폐쇄 원칙을 위해서 사용한다는 것을 알게 되었습니다.
Liscov Substitution Principle 리스코브 치환 원칙 ▼
정의
- 프로그램에서 어떤 클래스도 그 기능에 영향을 주지 않고, 해당 클래스의 서브클래스로 대체 가능해야 합니다.
예제 코드
- 사용자와 연락하는(이메일 보내기와 같은) 책임을 가진 UserService 라는 클래스가 하나 있다고 가정합니다.
- 17세 이상의 사용자에게 이메일을 보내도록 비즈니스 로직을 변경해야 한다면, 새로운 비즈니스 로직을 가지는 서브 클래스를 만들 수 있습니다.
class UserService {
func contact(user: User) {
/// Retrieve user from database
}
}
class ValidUserService: UserService {
override func contact(user: User) {
guard user.age > 17 else { return }
super.contact(user: User)
}
}
- 이런 경우에 사용자의 나이가 17세 이상이라는 조건을 서브클래스가 추가했기 때문에 리스코브 치환 원칙은 충족되지 않습니다.
- 서브클래스를 추가하지 않고 UserService 에 (기본값을 포함한) 전제 조건을 추가하는 것으로 이러한 문제를 해결할 수 있습니다.
class UserService {
func contact(user: User, minAge: Int = 0) {
guard user.age > minAge else { return }
/// Retrieve user from database
}
}
진짜 조사하면서 효율적인 방법이라는 생각이 들었습니다. 앞으로 애용해보려고 합니다.
Interface Segregation Principle 인터페이스 분리 원칙 ▼
정의
- 일반적인 인터페이스를 가지는 것보다 구체적인 각각의 다른 인터페이스를 가지는 것이 낫다고 명시되어 있습니다.
- 사용하지 않는 메소드는 구현할 필요가 없다는 것을 명시합니다.
예제 코드
- 예를 들어, 동물이 움직이는 메소드를 포함한 AnimalProtocol을 구현해보겠습니다.
protocol AnimalProtocol {
func walk()
func swim()
func fly()
}
struct Animal: AnimalProtocol {
func walk() {}
func swim() {}
func fly() {}
}
struct Whale: AnimalProtocol {
func swim() {
/// Whale은 이 메소드만 구현하면 됩니다
/// 다른 메소드는 상관이 없습니다
}
func walk() {}
func fly() {}
}
- Whale이 프로토콜을 채택했음에도 메소드 두 개는 구현하지 않게 됩니다.
- 해결 방법은 3개의 인터페이스(프로토콜)를 만들어 각각 필요한 프로토콜만 채택하는 것입니다.
protocol WalkProtocol {
func walk()
}
protocol SwimProtocol {
func swim()
}
protocol FlyProtocol {
func fly()
}
struct Whale: SwimProtocol {
func swim() {}
}
struct Crocodile: WalkProtocol, SwimProtocol {
func walk()
func swim() {}
}
필요한 기능만 구현하니까 더 직관적이고 효율적으로 코딩할 수 있을 것 같습니다.
Dependency Inversion Principle 의존성 역전 원칙 ▼
정의
- 이 원칙을 따르면모듈 간의 의존성을 낮춰 클래스 간의 결합도를 낮추도록 합니다.
- 상위 클래스가 하위 클래스에 의존하지 않게 됩니다.
- 둘 다 추상 타입에 의존하게 됩니다.
- 추상 타입은 구체 타입에 의존하지 않게 됩니다.
- 구체 타입이 추상 타입에 의존하게 됩니다.
예제 코드
class User {
var name: String
init(name: String) {
self.name = name
}
}
class CoreData {
func save(user: User) {
/// Save user on database
}
}
class UserService {
func save(user: User) {
let database = CoreData()
database.save(user: user)
}
}
- 데이터를 저장할 때, Core Data 대신 Realm 데이터 베이스를 사용하고 싶다면 어떻게 해야 할까요?
- 위 예시처럼 클래스의 인스턴스는 강한 결합을 만들어내므로, 만약 다른 데이터베이스를 이용하고 싶다면 코드를 다시 실행해야 할 것입니다.
- 이것을 해결하기 위해, Core Data와 Realm이 공통적으로 채택할 프로토콜을 만들고, 데이터를 저장하는 타입을 해당 프로토콜로 두면 됩니다.
protocol Storable { }
extension Object: Storable { } /// Realm Database
extension NSManagedObject: Storable { } /// Core Data Database
protocol StorageManager {
/// Save Object into Realm database
/// - Parameter object: Realm object (as Storable)
func save(object: Storable)
}
- Storable 프로토콜을 채택하는 User와 UserService 클래스는 StorageManager 프로토콜을 채택하므로 데이터베이스를 변경하더라도 전체 구현 코드를 변경할 필요가 없습니다.
class User: Storable {
var name: String
init(name: String) {
self.name = name
}
}
class UserService: StorageManager {
func save(object: Storable) {
/// Saves user to database
}
}
프로젝트의 규모가 커질수록 이 원칙을 통해 수정에도 용이한 구조로 구현해야겠다고 다짐했습니다.
- SOLID 원칙을 찾아보면서 아직 배울게 많고, 적용해야 할 유용한 기술들이 많다는 것을 알게 되었습니다.
- 구현에 집중하는 것도 좋지만 그 구현이 얼마나 효율적이며 테스트에 용이하게 할 것인지를 고민하는 개발자가 되어야겠다고 다짐했습니다.
'iOS_Swift.zip' 카테고리의 다른 글
[iOS]CollectionView Layout (0) | 2022.01.11 |
---|---|
[iOS]HIG(Human Interface Guide) (0) | 2022.01.10 |
[iOS]옵저버 패턴(Observer Pattern) (0) | 2022.01.05 |
[iOS]싱글톤 패턴(Singleton Pattern) (0) | 2022.01.04 |
[iOS]FSCalendar 사용 후기 & 사용법 (5) | 2022.01.02 |