- SRP 준수 => 클래스의 변경 이유는 한가지가 되도록 설계
- 인스턴스 변수를 최소화
- 클래스는 작게 만들 것
ex) 책임이 많은 클래스 ❌
class SuperDashboard: SomeModule {
func getLastFocusedComponent() -> Component
func setLastFocused()
func getMajorVersionNumber() -> Int
func getMinorVersionNumber() -> Int
func getBuildNumber() -> Int
}
변경할 이유가 단 한나뿐이어야 한다는 원칙
SRP는 책임
이라는 개념을 정의하며 적절한 클래스 크기를 제시
위 SuperDashboard 클래스의 변경할 이유는 2가지 이므로 SRP를 위반 (소프트웨어 버전 추적, 포커스된 컴포넌트 추적)
ex) 수정한 클래스 ⭕️
소프트웨어 버전 추적
관련 코드를 제외시켜서, SRP를 준수하도록 설계 (동시에 클래스의 크기도 작아지는 장점이 존재)class Dashboard: SomeModule {
func getLastFocusedComponent() -> Component
func setLastFocused()
}
class Version {
func getMajorVersionNumber() -> Int
func getMinorVersionNumber() -> Int
func getBuildNumber() -> Int
}
SRP만 준수해도 클래스를 관리하기 용이하고 추상화 시키는 작업에도 도움됨
응집도(Cohesion)
응집도의 개념: 클래스에 속한 메소드와 변수가 서로 의존하며 논리적인 단위로 묶인다는 의미
응집도의 기준: 인스턴스 변수가 메소드에서 많이 사용
클래스는 인스턴스 변수 수가 작아야함
(메소드는 인스턴스 변수를 하나 이상 사용해야 하므로 메소드가 변수를 더 많이 사용할수록 "메소드+클래스" 응집도가 높음)
❌ 응집도가 높은 코드
ex) elements란 인스턴스가 3개의 메소드에서 모두 사용되고 있으므로 응집도가 가장 높은 코드
class Stack {
var elements = [Int]()
func size() -> Int {
return elements.count
}
func push(_ element: Int) {
elements.append(element)
}
func pop() -> Int? {
guard !elements.isEmpty else { return nil }
let element = elements.removeLast()!
return element
}
}
응집도를 유지하면 작은 클래스가 여러개가 탄생
ex) 응집도를 유지하여 새로운 클래스로 분리되는 과정
변수 4가지를 사용하는 큰 함수가 존재
->큰 함수 일부를 함수 하나로 빼내고 싶은 상황
->빼내려는 코드가 큰 함수에 정의된 변수 4가지를 사용
->변수 4개를 새 함수의 인수로 넘기지 말고, 클래스의 인스턴스로 지정
->클래스의 인스턴스가 여러개 생겨났으므로 독자적인 클래스로 분리
SRP를 이용하여 변경하기 쉬운 클래스 만들기
핵심: SRP를 준수하며, 함수의 인수들을 인스턴스로 변경하여 클래스들로 최대한 작게 쪼갤 것
ex) SRP 위반 클래스 ❌
class Sql {
var table: String
var columns: [Column]
init(table: String, columns: [Column]) {
self.table = table
self.columns = columns
}
func create() -> String { ... }
func insert(table: [Any]) { ... }
func selectAll() { ... }
func findByKey(keyColumn: String, keyValue: String) { ... }
func select(column: Column, pattern: String) { ... }
func select(criteria: Criteria) { ... }
func preparedInsert() { ... }
private func columnList(columns: [Column]) { ... }
private func valuesList(fields: [Any], columns: [Column]) { ... }
private func selectWithCriteria(criteria: Criteria) { ... }
private func placeholderList(columns: [Column]) { ... }
}
옳은 방법 ⭕️
// 닫힌 클래스 집합
class Sql {
let table: String
let columns: [Column]
init(table: String, columns: [Column]) {
self.table = table
self.columns = columns
}
func generate() -> String {
fatalError("This method must be overridden")
}
}
class CreateSql: Sql {
override func generate() -> String {
// Implement generate for CreateSql
}
}
class SelectSql: Sql {
override func generate() -> String {
// Implement generate for SelectSql
}
}
class InsertSql: Sql {
let fields: [Any]
init(table: String, columns: [Column], fields: [Any]) {
self.fields = fields
super.init(table: table, columns: columns)
}
override func generate() -> String {
// Implement generate for InsertSql
}
private func valueList(fields: [Any], columns: [Column]) -> String {
// Implement valueList
}
}
class SelectWithCriteriaSql: Sql {
let criteria: Criteria
init(table: String, columns: [Column], criteria: Criteria) {
self.criteria = criteria
super.init(table: table, columns: columns)
}
override func generate() -> String {
// Implement generate for SelectWithCriteriaSql
}
}
class SelectWithMatchSql: Sql {
let pattern: String
init(table: String, columns: [Column], pattern: String) {
self.pattern = pattern
super.init(table: table, columns: columns)
}
override func generate() -> String {
// Implement generate for SelectWithMatchSql
}
}
class FindByKeySql: Sql {
let keyColumn: String
let keyValue: String
init(table: String, columns: [Column], keyColumn: String, keyValue: String) {
self.keyColumn = keyColumn
self.keyValue = keyValue
super.init(table: table, columns: columns)
}
override func generate() -> String {
// Implement generate for FindByKeySql
}
}
class PreparedInsertSql: Sql {
override func generate() -> String {
// Implement generate for PreparedInsertSql
}
private func placeholderList(columns: [Column]) -> String {
// Implement placeholderList
}
}
class Where {
let criteria: String
init(criteria: String) {
self.criteria = criteria
}
func generate() -> String {
// Implement generate for Where
}
}
class ColumnList {
let columns: [Column]
init(columns: [Column]) {
self.columns = columns
}
func generate() -> String {
// Implement generate for ColumnList
}
}
struct Column {
// Implement Column struct members
}
struct Criteria {
// Implement Criteria struct members
}
이점
- 메서드 하나를 추가할 때 기존 클래스를 변경할 필요가 없다.
- 각 클래스는 극도로 단순하므로 보자마자 순식간에 이해되는 형태
- 테스트 관점에서 모든 논리를 구석구석 증명하기에 용이하기 쉬운 형태
- 확장에 개방적이고 수정에 폐쇄적이어야 한다는 원칙 OCP(Open Close Principle) 준수 (새 기능 추가 시 기존 코드 변경 x)