-
Notifications
You must be signed in to change notification settings - Fork 0
[week06] SOLID
Daehoon Lee edited this page Jun 3, 2023
·
8 revisions
SOLID 원칙을 지킴으로써 유지보수가 쉽고, 유연하고, 확장이 쉬운 소프트웨어를 만들 수 있습니다.
약어 | 개념 | |
---|---|---|
S | SRP | 단일 책임 원칙(Single Responsibility Principle) : 한 클래스는 하나의 책임만 가져야 한다. |
O | OCP | 개방-폐쇄 원칙(Open/Closed Principle) : 소프트웨어 요소는 확장에는 열려있으나 변경에는 닫혀 있어야한다. |
L | LSP | 리스코프 치환 원칙(Liskov Substitution Principle) : 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야한다. |
I | ISP | 인터페이스 분리 원칙(Interface Segregation Principle) : 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다 |
D | DIP | 의존관계 역전 원칙(Dependency Inversion Principle) : 프로그래머는 추상화에 의존해야지, 구체화에 의존하면 안된다. 의존성 주입은 이 원칙을 따르는 방법 중 하나다. |
- 모든 클래스는 하나의 책임만 가지며, 클래스는 그 책임을 완전히 캡슐화해야 한다
- 클래스의 수정이유는 단 하나여야한다
- 하나의 클래스는 하나의 책임을 가져야한다
- 하나의 책임이 여러개의 클래스에 나뉘어 있어서도 안된다
- 소프트웨어 객체는 확장에 대해 열려 있어야하고, 수정에 대해서는 닫혀 있어야한다
- 확장에는 열려 있으나, 변경에는 닫혀 있어야한다(기능 케이스를 추가할 때도 기존 코드를 변경하지 않고 확장해야한다)
- 객체가 변경될 때는 해당 객체만 바꿔도 동작이 잘되면 OCP를 잘 지킨것이다
- 모듈이 주변환경에 지나치게 의존해서는 안된다
- 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다
- 서브타입은 상속받은 기본타입으로 대체 가능하다
- 자식 클래스는 부모 클래스 동작(의미)를 바꾸지 않는다
- 상속을 했을 때 서브클래스는 자신의 슈퍼클래스 대신 사용되도 같은 동작을 해야한다
- 클라이언트가 자신이 이용하지 않는 메서드에 의존하지 않아야 한다는 원칙이다. ISP는 큰 덩어리의 인터페이스들을 구체적으고 작은 단위들로 분리시킴으로써 클라이언트들이 꼭 필요한 메서드들만 이용할 수 있게 한다. 이와 같은 작은 단위들을 인터페이스라고도 부른다. ISP를 통해 시스템의 내부 의존성을 약화시켜 리팩토링, 수정, 재배포를 쉽게 할 수 있다.
- 클래스 내에서 사용하지 않는 인터페이스는 구현하지 맣아야한다
- 클라이언트 객체는 사용하지 않는 메서드에 의존하면 안된다
- 인터페이스가 거대해지는 경우 SRP를 어기는 경우가 생길 수 있고, 해당 인터페이스를 채택해서 사용하는 경우 쓰지 않는 메소드가 있어도 넣어야 하는 경우가 발생할 수 있으니 최대한 인터페이스를 분리하는 것을 권장한다
- 상위 계층이 하위 계층에 의존하는 전통적인 의존관계를 반전(역전)시킴으로써 상위 계층이 하위 계층의 구현으로부터 독립되게 할 수 있다. 첫째, 상위 모듈은 하위 모듈에 의존해서는 안된다. 상위 모듈과 하위 모듈 모두 추상화에 의존해야한다. 둘째, 추상화는 세부 사항에 의존해서는 안된다. 세부사항이 추상화에 의존해야한다. DIP는 '상위와 하위 객체 모두가 동일한 추상화에 의존해야 한다'는 객체 지향적 설계의 대원칙을 따른다.
- 상위레벨 모듈은 하위레벨 모듈에 의존하면 안된다
- 두 모듈은 추상화된 인터페이스(프로토콜)에 의존해야한다
- 추상화 된 것은 구체적인 것에 의존하면 안되고, 구체적인 것이 추상화된 것에 의존해야한다
- 하위레벨 모듈이 상위레벨 모듈을 참조하는 것은 되지만 상위레벨 모듈이 하위레벨 모듈을 참조하는 것을 안하는게 좋다(제너릭이나 Associatetype 사용)
- DIP를 만족하면 의존성 주입을 사용하여 변화를 쉽게 수용할 수 있다
- 객체지향 프로그래밍에서는 다형성이란 하나의 코드가 여러 가지 형태로 실행되는 것을 의미한다
- 오버로딩과 오버라이딩을 통해 구현됨(재정의)
- 오버로딩
- 오버로딩은 같은 이름의 함수를 매개변수에 따라 다양하게 구현하는 것을 의미한다
- 같은 이름의 함수라도 입력 받는 매개변수의 갯수에 따라 다르게 구현할 수 있다
- 오버라이딩
- 오버라이딩이란 부모 클래스에게서 상속 받은 메서드를 그대로 사용하지 않고, 자식 클래스에 맞게 변경하여 사용하는 것이다.
-
protocol은 타입으로 Swift의 class나 struct의 행동을 정의하는 역할을 한다(Java의 interface와 같은 역할을 한다)
protocol Shop() { func sell() } class CoffeeShop: Shop { func sell() { print("Sell Coffee") } }
위 프로토콜을 보면
sell()
메서드는 정의만 되어있을 뿐 구현은 하지 않는다.sell()
메서드를 구현하기 위해서는 class나 struct에서 상속 받아 구현을 한다. -
객체의 유연한 설계를 위해서는 정의와 구현이 분리되어야 한다. 즉, 객체가 OCP 원칙을 지키기 위해서는 정의와 구현을 분리해야 한다. 객체의 행동을 정의하는 타입을 만드는 것은 이 시작이다. 만약 func이 구체 타입인 class나 struct를 인자로 받는다면 해당 인자에는 해당 class나 func만이 들어갈 수 있다. 하지만, protocol을 인자로 받게 되면 protocol을 구현하는 어떤 인자이든지 들어갈 수 있게 된다.
func doSellAction(shop: Shop) { shop.sell() } let coffeeShop = CoffeeShop() doSellAction(shop: coffeeShop) let cakeShop = CakeShop() doSellAction(shop: cakeShop) let bakeryShop = BakeryShop() doSellAction(shop: bakeryShop)
- override 시 부모와 자식간의 함수의 형태가 같아도 override 키워드를 붙이지 않으면 에러가 난다!
- override를 하면 LSP원칙을 지키는 것인가? 깨지게 하는 것인가? -> 가치의 기준을 어디에 두느냐에 따라 의견이 다를 수 있다. 예를 들어 함수의 동작을 기준으로 본다면 오버라이드를 하게 된다면 깨진다고 생각이 된다. 함수의 역할(목적)의 범위로 생각한다면 오버라이드를 했을 시 함수의 목적을 위배하지 않았다면 오버라이드를 해도 LSP원칙을 지킨다고 볼 수 있다.
📝 간단하게 솔리드에서 리스코프 치환을 작성해봐라?
- 부모의 기능을 오버라이딩 하여 부모와는 다른 동작을 수행한다
- 부모의 기능을 받아 쓰지 못한다면 의미가 없다 (새로 정의 및 재정의 하게된다면)
class Vehicle { var currentSpeed = 0.0 var description: String { return "traveling at \(currentSpeed) miles per hour" } func makeNoise() { print("부모다!!") } } class Train: Vehicle { override func makeNoise() { super.makeNoise() print("자식이다!!") } } var vehicle = Vehicle() var train = Train() train.makeNoise()
- 여러 타입에 대해 작동하는 코드를 작성하고 해당 타입에 대한 요구 사항을 지정한다
- 같은 동작을 하는 다른 타입을 하나로 묶어서 사용할 수 있게 하는 타입으로 추상화된 방식으로 코드를 작성할 수 있다
- Array, Dictionary 또한 실제 Generic 타입을 사용한다
public struct Array<Element>: _DestructorSafeContainer {}
- Generic(
<Element>
)은 Any와 달리 처음들어오는 타입으로 결정 - placeHolder의 역할
- 클래스, 구조체, 열거형, 그리고 프로토콜을 포함
- 사용자 정의 타입은, 프로그래머가 데이터를 용도에 맞게 묶어 표현하고자 할 때 유용하게 쓰인다. 구조체와 클래스는 프로퍼티와 메소드를 사용하여 구조화된 데이터 및 기능을 가질 수 있다. Swift에서 이 둘은 문법상 생김새는 비슷하나, 구조체는 값 타입, 클래스는 참조 타입이라는 것이 가장 큰 차이라고 할 수 있다.
- 구조체(Struct)
struct SoccerPlayer { // Properties var name: String // 이름 var age: Int // 나이 var club: String // 소속팀 let birthdate:String static let isHuman: Bool = true // 타입 프로퍼티 // Methods func dribble(){ print("dribble") } func pass(receiver:SoccerPlayer){ print("\(self.name) passed to \(receiver.name)") } func tackle(){ print("tackle") } // static 키워드를 붙이면 타입 프로퍼티 혹은 타입 메서드가 된다. 아래는 타입 메서드이다. static func introduceSoccer(){ print("Soccer is ...") } }
- 클래스(Class)
class Person { var name: String = "someName" var age: Int = 20 var weight: Double = 0.0 let birthDate : String = "1997-03-17" static let isHuman: Bool = true func sayHello(){ print("\(self.name) says Hello") } // 상속 후 재정의 불가능 static func typeMethod(){ print("Type Method - static : 재정의 불가") } // 상속 후 재정의 가능 class func classMethod(){ print("Type Method - class : 재정의 가능") } }
객체 지향 프로그래밍 특징
먼저 장점부터… 소개하자면
- 코드의 재사용성이 높아진다
- 코드의 가독성이 좋다
- 코드의 유지보수가 좋다
- 큰 규모의 소프트웨어를 개발할 때 효과적이다.
- 전부… 잘 구현했을때의 일이라고 생각한다… 그리고 더하여 5가지의 큰 특징이 있다
- 추상화, 캡슐화, 은닉화, 상속성, 다형성
- 추상화
- 대상의 불필요한 부분을 무시하며 복잡성을 줄이고 목적에 집중할 수 있도록 단순화 시키는것이다
- 추상화는 객체들의 공통적인 부분을 뽑아내서 따로 구현해해놓는 것이다
- 사물들의 공통적인 부분만 사용하여 만들고 차이점은 우선 빼둔다
- 캡슐화
- 객체 내부의 상태와 행위를 하나로 묶고, 외부에서 직접 접근하지 못하게 제한하는 것
- 정보 은닉화와 보호를 위해 사용된다
- 은닉화
- 객체의 상세한 내용을 외부에 숨기고, 인터페이스를 통해 필요한 부분만 노출시킨다
- 객체의 내부 구현을 변경해도 외부 코드에 영향을 주지 않게 한다.
- 상속성
- 부모 클래스의 특성과 기능을 자식 클래스가 물려받는것(부모는 자식에서 정의한 특성 및 기능을 사용할 수 없다)
- 자식 클래스는 상속받은 특성과 기능을 수정하거나 확장해서 사용할 수 있다(부모객체와 자식객체 2가지 사용)
- 다형성
- 객체지향 프로그래밍에서는 다형성이란 하나의 코드가 여러 가지 형태로 실행되는 것을 의미한다
- 오버로딩과 오버라이딩을 통해 구현됨(재정의)