[SOLID principles of Android] SOLID 원칙 정리와 안드로이드에서 사용 예

2022. 7. 4. 16:19Android/깨알 개념 정리

SOLID 원칙

컴퓨터 프로그래밍에서 SOLID란 로버트 마틴이 2000년대 초반에 명명한 객체 지향 프로그래밍 및 설계의 다섯 가지 기본 원칙을 마이클 페더스가 두문자어 기억술로 소개한 것이다. 프로그래머가 시간이 지나도 유지 보수와 확장이 쉬운 시스템을 만들고자 할 때 이 원칙들을 함께 적용할 수 있다. SOLID 원칙들은 소프트웨어 작업에서 프로그래머가 소스 코드가 읽기 쉽고 확장하기 쉽게 될 때까지 소프트웨어 소스 코드를 리팩토링 하여 코드 냄새를 제거하기 위해 적용할 수 있는 지침이다.

 

SRP(Single Responsibility Principle): 단일 책임 원칙

class는 오직 하나의 이유만 가져야 한다 즉, 하나의 변경 작업을 가져야 한다는 것을 의미한다

 

EX) 

RecyclerView 의 onBindViewHolder 는 생성된 viewHolder에 데이터를 바인딩 해주는 역할을 한다 

하지만 아래처럼 name 체크 후 값을 변경해주는 기능이 들어갈 경우 SRP에 위배된다. 

 

BAD

override fun onBindViewHolder(holder: LocationViewHolder, position: Int) {
    val data = getItem(position).copy(
        name = if(getItem(position).name.isBlank()) getItem(position).id.toString() else "${getItem(position).name} goo"
    )
    holder.bind(data)
}

 

두 가지 기능이 들어가므로 이름 체크하는 기능을 제거 분리하는 작업을 해준다. 

 

GOOD

override fun onBindViewHolder(holder: LocationViewHolder, position: Int) {
    val data = getItem(position).copy(
        name = getItem(position).name.checkBlack()
    )
    holder.bind(data)
}

 

OCP(Open Closed Principle): 개방 폐쇄 원칙

모듈은 확장에 열려있고, 수정에는 닫혀있어야 한다.

 

EX)

시간 int를 str로 변경하는 class 가 있다 

아래 클래스에서 시간의 경우가 추가될 경우 when 절에 추가적인 조건이 필요하게 된다

이는 기존 코드를 수정하게 되므로 OCP에 위배된다.

 

BAD

class TimeActivity: AppCompatActivity(){
    private var time = 0

    fun getStrTime(): String{
        return when(time){
            1 -> "one"
            2 -> "two"
            else -> "no str"
        }
    }
    
    fun setTime(time: Int){
        this.time = time
    }
}

 

interface를 생성하여 기존 코드 수정 없이 새로운 클래스를 생성해 기능을 추가하도록 수정한다. 

 

GOOD

class TimeActivity: AppCompatActivity(){
    private lateinit var time: TimeToStr

    fun getStrTime(): String{
        return this.time.toStr()
    }

    fun setTime(time: TimeToStr){
        this.time = time
    }
}

interface TimeToStr {
    fun toStr(): String
}

class One: TimeToStr {
    override fun toStr(): String {
        return "one"
    }
}

class Two: TimeToStr {
    override fun toStr(): String {
        return "two"
    }
}

 

LSP(Liskov Substitution Principle): 리스코프 치환 원칙

자식 클래스는 부모 클래스의 유형 정의를 변경 없이 하위 유형의 인스턴스로 대체할 수 있어야 합니다.
자식 클래스는 부모 클래스의 역할을 모두 수행해야 한다.

 

EX) 장난감을 나타내는 부모 클래스 Toy 가 있고 장난감 종류에 해당하는 Car와 Plane 자식 클래스가 있을 때, Car 클래스는 fly 기능을 하지 못하므로 해당 메소드를 오버라이드 하는 것은 불필요하다 즉, fly 기능을 충실히 실행하고 있지 않다 

 

BAD

abstract class Toy{
    abstract fun on()
    abstract fun off()
    abstract fun fly()
}

class Car: Toy(){
    override fun fly() {
        TODO("Not yet implemented")
    }
    override fun off() {
        TODO("Not yet implemented")
    }
    override fun on() {
        TODO("Not yet implemented")
    }
}

class Plane: Toy(){
    override fun fly() {
        TODO("Not yet implemented")
    }
    override fun on() {
        TODO("Not yet implemented")
    }
    override fun off() {
        TODO("Not yet implemented")
    }
}

 

Toy를 인터페이스로 변경하고 FlyingToy 추상 클래스를 생성하여 Plane 자식 클래스에서만 FlyingToy 클래스를 상속받도록 하고, Car 자식 클래스에서는 Toy 인터페이스를 구현하도록 수정한다.

 

GOOD

interface Toy{
    fun on()
    fun off()
}
abstract class FlyingToy: Toy{
    abstract fun fly()
}

class Car: Toy{
    override fun off() {
        TODO("Not yet implemented")
    }

    override fun on() {
        TODO("Not yet implemented")
    }
}

class Plane: FlyingToy(){
    override fun fly() {
        TODO("Not yet implemented")
    }

    override fun on() {
        TODO("Not yet implemented")
    }

    override fun off() {
        TODO("Not yet implemented")
    }
}

 

 

ISP(Interface Segregation Principle): 인터페이스 분리 원칙

사용자가 필요하지 않은 것들에 의존하게 되지 않도록, 인터페이스를 작게 유지하라.

 

EX) 장난감을 나타내는 interface Toy 가 있고 이를 구현하는 구현체 Car, Plane이 있다 이때, Car 클래스는 fly() 기능이 필요가 없다. 

 

BAD

interface Toy{
    fun on()
    fun off()
    fun fly()
}

class Car: Toy{
	//no use
    override fun fly() {
        TODO("Not yet implemented")
    }
    override fun off() {
        TODO("Not yet implemented")
    }
    override fun on() {
        TODO("Not yet implemented")
    }
}

class Plane: Toy{
    override fun fly() {
        TODO("Not yet implemented")
    }
    override fun on() {
        TODO("Not yet implemented")
    }
    override fun off() {
        TODO("Not yet implemented")
    }
}

 

FlyingToy 인터페이스를 생성하여 fly() 기능을 분리하고 Plane 클래스에서 추가적으로 FlyingToy 인터페이스를 구현하도록 수정해준다.

 

GOOD

interface Toy{
    fun on()
    fun off()
}

interface FlyingToy{
    fun fly()
}

class Car: Toy{
    override fun off() {
        TODO("Not yet implemented")
    }

    override fun on() {
        TODO("Not yet implemented")
    }
}

class Plane: FlyingToy, Toy{
    override fun fly() {
        TODO("Not yet implemented")
    }

    override fun on() {
        TODO("Not yet implemented")
    }

    override fun off() {
        TODO("Not yet implemented")
    }
}

 

 

DIP(Dependency Inversion Principle): 의존 역전 원칙

1. 고수준 모듈은 저수준 모듈에 의존하지 않아야 한다. 둘 다 추상화에 의존해야 한다.
2. 추상은 세부사항에 의존해서는 안 된다. 세부사항은 추상에 의존해야 한다.
3. 확장성에 용이하고 객체간의 의존성을 낮춰줘야 한다.

 

EX)  장난감 회사 A의 색상과 B의 색상을 나타내는 class 가 각각 있고 ToyClass를 통해 해당 색상 값을 리턴하도록 구현되어있다.

하지면 여기서 장난감 색상 class 가 늘어날 경우 고수준 모듈의 변경이 발생해야 한다.

 

BAD

class AToy {
    fun color(): String{
        return "RED"
    }
}

class BToy {
    fun color(): String{
        return "BLUE"
    }
}


class ToyClass {
    lateinit var aToy: AToy
    lateinit var bToy: BToy

    fun color(toy: String): String{
        when(toy){
            "A" -> aToy.color()
            "B" -> bToy.color()
        }
    }
}

 

Color를 나타내는 인터페이스(고수준)를 생성하고 이를 구현하는 장난감 클래스(저수준)들을 생성한다 이를 ToyClass(고수준)에서 생성하여 사용하게 되면 해당 코드 변경 없이 추가가 가능해진다. 

 

GOOD

interface Color {
    fun color(): String
}

class BToy: Color{
    override fun color(): String{
        return "BLUE"
    }
}

class AToy: Color{
    override fun color(): String{
        return "RED"
    }
}


class ToyClass {
    lateinit var toyColor: Color

    fun toyColor(color: Color){
        this.toyColor = color
    }
    
    fun color(toy: String) = 
        toyColor.color()
}