OOP에 이어 SOLID도 포스팅 해보았습니다.
SOLID는 객체 지향 프로그래밍(OOP) 및 설계의 다섯 가지 기본 원칙
읽기 쉽고 확장하기 쉽게
될 때까지 소스 코드를 리팩토링하기 위한 지침이다.SRP
, OCP
, LSP
, ISP
, DIP
5가지의 원칙을 말한다.SRP는
단일 책임 원칙
으로,하나의 클래스
는하나의 책임
만 가져야 한다는 원칙
클래스는 하나의 기능을 담당하고 수행
하는 데에 집중되어 있어야 함을 말한다.책임(기능)을 여러 클래스에 분산
하여 유지보수
를 쉽게 할 수 있다.코드를 수정
해야 하는 경우, 해당 책임(기능)을 가진 클래스만 수정
하면 된다.가독성
이 향상된다.책임의 소재를 알 수 있도록 작명
해야 한다.결합도
와 응집도
를 고려한다.특정 데이터를 생성을 보고하고 그 보고서를 저장하는 경우
→ 예시이기 때문에 기본적인 틀만 작성하였습니다.
// Report는 두 가지의 책임을 가지고 있다.
// 1: 데이터 생성 보고, 2: 보고서 저장
class Report {
var data: String
init(data: String) {
self.data = data
}
func generateReport() {
// 데이터 생성 보고
print("Generating report with data: \(data)")
}
func saveToFile() {
// 데이터가 생성되었음을 알리는 보고서 저장
print("Saving report to file")
}
}
// Report는 데이터 생성 알림이라는 하나의 책임을 가지고 있고,
// ReportSaver는 데이터 저장이라는 하나의 책임을 가지고 있다.
class Report {
var data: String
init(data: String) {
self.data = data
}
func generateReport() {
// 데이터 생성 보고
print("Generating report with data: \(data)")
}
}
class ReportSaver {
func saveToFile(report: Report) {
// 데이터가 생성되었음을 알리는 보고서 저장
print("Saving report to file")
}
}
OCP는
개방 폐쇄 원칙
으로, 소프트웨어 개체(클래스, 함수 등)는확장에 대해 열려있어야 하고 수정에 대해 닫혀있어야 한다
는 원칙
확장을 통해 손쉽게 구현
하면서, 확장에 따른 클래스 수정은 최소화
하도록 설계해야 한다는 원칙이다.추상화
를 의미한다고 보면 되기 때문에, 다형성과 확장을 가능하게 하는 객체 지향의 장점을 극대화
하는 원칙이다.유연한 확장
이 가능하여 유지보수
가 쉽다.새로운 유형의 객체를 생성하는 경우
// 새로운 멤버가 추가되는 경우, 기존에 있던 speakName() 내부의 switch문 또한 수정해야 한다.
enum Member {
case Jun
case Jung
case Hyeok
case Won
}
class Person {
let name: Member
init(name: Member) {
self.name = name
}
}
func speakName(person: Person) {
switch person.name {
case .Jun:
print("Hi, I'm Jun.")
case .Jung:
print("Hi, I'm Jung.")
case .Hyeok:
print("Hi, I'm Hyeok.")
case .Won:
print("Hi, I'm Won.")
}
}
// 새로운 멤버가 추가되는 경우, 기존에 있던 Member 프로토콜을 채택한 struct만 새로 만들면 된다.
protocol Member {
var name: String { get }
}
struct Jun: Member {
let name: String = "Jun"
}
struct Jung: Member {
let name: String = "Jung"
}
struct Hyeok: Member {
let name: String = "Hyeok"
}
struct Won: Member {
let name: String = "Won"
}
class Person {
let name: Member
init(name: Member) {
self.name = name
}
}
func speakName(person: Person) {
print("Hi, I'm \(person.name).")
}
LSP는
리스코프 치환 원칙
으로, 부모 유형의 객체가 자식 유형의 객체로 치환(교체)되어도 프로그램이 문제 없이 실행. 즉,자식 유형의 객체
가부모 유형의 객체의 기능
을의도대로 실행
해야 한다는 원칙
유지보수
하기 쉽다.다형성
이 분해되어 OOP와 맞지 않는 방향으로 개발을 하게될 수 있다.직사각형과 정사각형의 넓이를 구하는 경우
// 일반적으로 우리는 정사각형은 직사각형에 속한다고 알고 있지만,
// 코드로 작성하여 정사각형이 직사각형을 상속 받는 경우,
// 정사각형의 너비와 높이를 각각 3, 5로 set을 하고 출력을 하면
// 25라는 결과가 도출된다. 부모 클래스에서 의도된 결과는 직사각형 기준 15이기 때문에
// 이는 LSP를 위반한다.
class Rectangle {
var width: Double
var height: Double
init(width: Double, height: Double) {
self.width = width
self.height = height
}
func setWidth(_ width: Double) {
self.width = width
}
func setHeight(_ height: Double) {
self.height = height
}
func calculateArea() -> Double {
return width * height
}
}
class Square: Rectangle {
override func setWidth(_ width: Double) {
super.setWidth(width)
super.setHeight(width)
}
override func setHeight(_ height: Double) {
super.setWidth(height)
super.setHeight(height)
}
}
// 위 코드와는 다르게 넓이를 계산하는 기능을 가진 Shape라는 protocol을 따로 선언하였다.
// 직사각형의 넓이나 정사각형의 넓이나 결과는 부모인 Shape의 calculateArea()가 나타내는
// 넓이 계산이라는 예상 동작이 그대로 유지되기 때문에 LSP를 준수한다.
protocol Shape {
func calculateArea() -> Double
}
class Rectangle: Shape {
var width: Double
var height: Double
init(width: Double, height: Double) {
self.width = width
self.height = height
}
func calculateArea() -> Double {
return width * height
}
}
class Square: Shape {
var sideLength: Double
init(sideLength: Double) {
self.sideLength = sideLength
}
func calculateArea() -> Double {
return sideLength * sideLength
}
}
ISP는
인터페이스 분리 원칙
으로, 클라이언트가 자신이 이용하지 않는 메서드에 의존하지 않아야 한다는 원칙. 즉,인터페이스를 사용에 맞게 끔 각각 분리
를 해야한다는 설계 원칙
인터페이스의 단일 책임을 강조
한다.목적과 용도에 적합한 인터페이스
만을 제공해야한다.유지보수
를 쉽게 할 수 있다.계속해서 분리하는 행위
를 하지 않는다.원격 근무와 일반 근무를 하는 회사원에 대한 클래스를 구현 경우
// OfficeWorker는 Worker 프로토콜을 상속 받고, 추상 메서드인
// 일(work), 휴식(takeBreak), 회의 참석(attendMeeting)
// 세 가지 기능을 모두 수행할 수 있다.
// RemoteWorker는 Worker 프로토콜을 상속 받지만,
// 회의 참석을 할 수는 없다. Worker 프로토콜이 하는 일이 많아
// 원격 회사원이 불필요한 기능까지 구현하고 있는 것이다.
protocol Worker {
func work()
func takeBreak()
func attendMeeting()
}
class OfficeWorker: Worker {
func work() {
print("회사에서 일하기.")
}
func takeBreak() {
print("회사에서 휴식하기.")
}
func attendMeeting() {
print("회사 내부 회의실에서 열리는 회의에 참석하기.")
}
}
class RemoteWorker: Worker {
func work() {
print("원격으로 집에서 일하기.")
}
func takeBreak() {
print("원격으로 집에서 편하게 휴식하기.")
}
func attendMeeting() {
print("회사에 가지 않으니 회의실을 가지 못해.")
}
}
// OfficeWorker는 일(Workable), 휴식(Breakable), 회의 참석(Meetingattenable)에 대한
// protocol을 각각 상속 받아 세 가지 기능을 모두 수행한다.
// RemoteWorker는 일, 휴식에 대한 protocol만을 상속 받고
// 회의 참석은 어짜피 수행하지 못하기에 받지 않는다.
// 그렇기에 위 코드와 다르게 불필요한 코드를 구현하지 않아도 된다.
protocol Workable {
func work()
}
protocol Breakable {
func takeBreak()
}
protocol MeetingAttendable {
func attendMeeting()
}
class OfficeWorker: Workable, Breakable, MeetingAttendable {
func work() {
print("회사에서 일하기.")
}
func takeBreak() {
print("회사에서 휴식하기.")
}
func attendMeeting() {
print("회사 내부 회의실에서 열리는 회의에 참석하기.")
}
}
class RemoteWorker: Workable, Breakable {
func work() {
print("원격으로 집에서 일하기.")
}
func takeBreak() {
print("원격으로 집에서 편하게 휴식하기.")
}
// attendMetting()은 MeetingAttendable protocol을 상속받지 않아 구현할 필요 없음
}
DIP는
의존관계 역전 원칙
으로,상위 계층이 하위 계층에 의존하는 관계를 역전
시켜상위 계층
이하위 계층의 구현으로부터 독립
되게 할 수 있는 원칙. 죽, 객체에서 클래스를 참조해야 한다면,직접 클래스를 참조하는 것이 아닌
대상의 상위 요소(추상 클래스 또는 인터페이스)를 참조
하라는 원칙
추상화에 의존
해야 한다.유지보수
가 쉬워진다.과도한 추상화
를 하지 않는다.정의를 알맞게
해야한다.사용자를 생성하고 데이터베이스에 저장하는 경우
// UserManager는 MySQLDB라는 특정 클래스를 참조하고, 메서드를 사용한다.
// 이 경우, 두 클래스 사이의 결합력이 높아지며 의존 관계가 형성된다.
// 또한, MySQLDB가 아닌 OracleDB를 사용하고 싶은 경우엔
// database 변수의 타입을 직접 수정까지 해주어야 한다.
class MySQLDB {
func connect() -> String {
return "MySQL 데이터베이스에 연결되었습니다."
}
func disconnect() -> String {
return "MySQL 데이터베이스와 연결이 끊겼습니다."
}
}
class OracleDB {
func connect() -> String {
return "Oracle 데이터베이스에 연결되었습니다."
}
func disconnect() -> String {
return "Oracle 데이터베이스와 연결이 끊겼습니다."
}
}
class UserManager {
private let database = MySQLDB()
func createUser(name: String) {
print("유저 생성중: \(name)")
let connection = database.connect()
print("유저 생성 완료: \(name)")
let disconnection = database.disconnect()
print(disconnection)
}
}
// Database라는 protocol을 정의하고,
// 내부에 connect()와 disconnect()라는 메서드를 정의했다.
// UserManager에서는 이제 특정 클래스가 아닌 Database라는 protocol만 참조하고
// MySQLDB와 OracleDB는 모두 Database를 상속받는다.
// 따라서, UserManager에서는 원하는 데이터 베이스를 수정 없이 참조할 수 있게 되었고
// 의존도가 낮아졌기 때문에 DIP를 준수한다.
protocol Database {
func connect() -> String
func disconnect() -> String
}
class MySQLDB: Database {
func connect() -> String {
return "MySQL 데이터베이스에 연결되었습니다."
}
func disconnect() -> String {
return "MySQL 데이터베이스와 연결이 끊겼습니다."
}
}
class OracleDB: Database {
func connect() -> String {
return "Oracle 데이터베이스에 연결되었습니다."
}
func disconnect() -> String {
return "Oracle 데이터베이스와 연결이 끊겼습니다."
}
}
class UserManager {
private let database: Database
init(database: Database) {
self.database = database
}
func createUser(name: String) {
print("유저 생성중: \(name)")
let connection = database.connect()
print("유저 생성 완료: \(name)")
let disconnection = database.disconnect()
print(disconnection)
}
}
https://ko.wikipedia.org/wiki/SOLID(객체지향_설계)
https://inpa.tistory.com/entry/OOP-💠-객체-지향-설계의-5가지-원칙-SOLID
https://inpa.tistory.com/entry/OOP-💠-아주-쉽게-이해하는-DIP-의존-역전-원칙?category=967430