-각각의 모듈 , 클래스 , 함수는 하나의 책임만 가져야한다는 의미이다.
위에서 말한 책임이란 해당 모듈 , 클래스 , 함수를 수정하고자 하는 이유를 의미한다.
-즉, 어떤 모듈 , 클래스 , 함수가 존재한다는 것은 존재 목적이라는 것이 있을 것이고 , 우리가 모듈 , 클래스 , 함수를 생성할 때 단 하나의 존재 목적만을 가지고 만들어야 한다는 것이다.
-존재목적이 변경될 때 앱에 미치는 영향이 지나치게 많으면 유지보수가 너무 힘들기에 해당 모듈 , 클래스 , 함수의 변경 사항이 다른 객체들에게 미치는 영향을 최소화 시키는 방향으로 모듈 , 클래스 , 함수의 존재 목적을 지정해줘야 한다.
-해당 모듈 , 함수 , 클래스를 수정해야 하는 이유 즉, 책임영역이 확실해지기 때문에 하나의 책임 영역의 변경이 다른 책임 영역의 변경을 발생시키지 않는다. 따라서 유지보수가 편리하고 , 재사용성이 높다.
-정리 : SRP란 내가 앱에서 가지고 있는 역할이 단, 하나여야 한다는 것
-각각의 모듈 , 클래스 , 함수는 확장에는 열려있고 , 수정에는 닫혀 있어야한다. 즉 , 기존의 코드는 수정하지 않으면서 역할을 가진 모듈 , 클래스 , 함수를 추가 할 수 있어야 한다는 뜻.
-보다 더 자세히 말하면 , 역할을 가진 임의의 알파를 추가 할 때에 기존의 역할을 구성하는 코드들에는 변화가 없고 (코드 수정에 닫혀있음), 알파를 구성하는 코드들만 추가적으로 작성하면 된다. (코드 확장에는 열려있음)
-그렇다면 어떻게 해야지 코드 수정 없이 임의의 알파를 구성하는 코드를 자유롭게 작성 할 수 있을까? 정답은 추상화이다. 즉, 추상클래스 또는 인터페이스의 구현을 통해 임의의 알파가 애플리케이션에서 가지는 역할을 코드 상에서 구현하면 코드 확장에 열려있게 된다. 추상화를 구체화 시킴으로서 코드를 확장하는 것이므로 기존의 코드 수정에는 닫혀있게 된다.
-정리 : OCP란 , 추상화를 구체화함으로서(추상화를 구현) 임의의 알파를 구성하는 코드를 추가하기 때문에 코드 확장에 자유로울 수 있고, 기존의 코드에 대한 수정에는 닫혀있을 수 있다. 따라서
-Type S가 Type T의 서브타입 일 때 , T의 객체를 S의 객체로 대체해도 프로그램의 기능에 어떠한 영향도 끼칠 수 없다는 것이다.
-즉, 임의의 베타가 임의의 알파를 상속 받았다면 , 알파의 객체를 사용하다가 베타의 객체로 변경하여 사용하여도 코드가 여전히 잘 작동해야 한다는 것이다.
-상속시 하위 개념은 상위 개념의 수행 영역과 일치해야 한다는 의미로서 수행 내용이 다르더라도 수행 영역은 상위 하위 간에 반드시 일치해야 한다. 만약 일치하지 않는다면 상속을 받으면 안된다.
-LSP 원칙을 지키기 위한 규칙은 메소드 오버로딩 시 메서드 규약을 반드시 유지해야 된다는 것이다. 그러나 필연적으로 메서드 규약을 벗어난 코드를 작성해야 할 때도 있다. 이럴 때는 메서드를 interface 블락안으로 넣어서 interface를 implement 시키는 방식으로 사용하면 된다. (상속 대신 합성을 사용하라는 의미야.)
-정리 : LSP란 상위 개념의 객체 자리를 하위 개념의 객체로 치환하여도 코드 수행에 이상이 없도록 하위 개념이 상위 개념의 수행 영역과 일치하게 상속을 잘 받아야 한다는 것이다. (수행 동작은 달라도 된다. 단, 반드시 수행 영역은 서로 일치해야 한다.)
-클래스는 자신이 사용하지 않는 인터페이스는 구현하면 안 된다는 것이다. 즉, '하나의 일반적인 범용 인터페이스'보다는 '여러개의 구체적인 인터페이스가 더 낫다'라는 것이다. 그러므로 인터페이스를 사용 목적에 맞게 최소 단위로 분리해야 한다는 것이다.
-그러나 이 인터페이스가 변경되거나 인터페이스를 한 번더 분리하게 된다며 해당 인터페이스를 구현하는 구현부의 코드를 전부 변경해야 되기 때문에 이 인터페이스는 절대로 변하면 안되는 정책이다.
-정리 : ISP는 인터페이스를 최소단위로 분리함으로서 , 임의의 알파가 인터페이스를 구현할 때 불필요한 구현을 막을 수 있다. (단 구현하는 내용은 각기 다르므로 다양한 기능을 갖는 것은 가능하다 => SRP와의 차이점 : SRP는 하나의 기능만을 갖는다. , cf : SRP 만족이 반드시 ISP를 만족시키는 것은 아니야.)
(2)
class FileDataRepository : DataRepository {
override fun save(data: String) {
println("Data '$data' is saved to a file.")
// 파일 시스템에 데이터 저장하는 로직
}
}
(1)
interface DataRepository {
fun save(data: String)
}
(3)
class DataProcessor(private val repository: DataRepository) {
fun process(data: String) {
println("Processing data: $data")
repository.save(data)
}
}
fun main() {
val fileRepository = FileDataRepository()
// LSP 원칙이 있기 때문에 , DataRepository를 구현한 FileDataRepository가
// DataProcessor()의 Parameter 자리에 들어갈 수 있는 것이야.
val dataProcessor = DataProcessor(fileRepository)
dataProcessor.process("Some data")
}