티스토리 뷰

오늘 아침, 코틀린 커뮤니티 포럼에 계산값에 대한 질문이 올라와 있었다. Double 연산이 제대로 되지 않는다는 것이었다.

Original discussions: discuss.kotlinlang.org/t/how-to-do-an-operation-on-a-variable-and-assign-it-that-new-value-for-easing-calculations/19621

[

How to do an operation on a variable and assign it that new value for easing calculations?

Do calculations with double seems a mess. But within my calculation i have numbers like 2.245, so my vars can’t be int Horrible How do youe guys manage that?

discuss.kotlinlang.org

](https://discuss.kotlinlang.org/t/how-to-do-an-operation-on-a-variable-and-assign-it-that-new-value-for-easing-calculations/19621)

아래 코드가 질문자가 최초로 올린 코드이다.

/*e is a number from 0 to 100
t comes as 0
n and r comes as 100*/
if ((e / r) < 1 / 2.75) {
        println("IS A")
        return (n * 7.5625 * e * e + t).toDouble()
    } else if (e < 2 / 2.75) {
        println("IS B")
        return (n * (7.5625 * (e- 1.5 / 2.75) * e + 0.75) + t).toDouble()
    } else if (e < 2.5 / 2.75) {
        println("IS C")
        return (n * (7.5625 * (e - 2.25 / 2.75) * e + 0.9375) + t).toDouble()
    } else {
        println("IS D")
        return (n * (7.5625 * (e - 2.625 / 2.75) * e + 0.984375) + t).toDouble()
    }

그리고 멤버들 간의 많은 토론 끝에 질문자는 동작하는 자신의 최종버전을 올렸다.

fun bounceEaseOut(dt:Float, b:Float=0f, c:Float=100f, d:Float=100f):Float {
   var t=dt
    t /= d
    if (t < (1/2.75f)) {
        return c*(7.5625f*t*t) + b
    } else if (t < (2/2.75f)) {
        var dt=t-(1.5f/2.75f)
        return c*(7.5625f*(dt)*dt + .75f) + b
    } else if (t < (2.5/2.75)) {
        var dt=t-(2.25f/2.75f)
        return c*(7.5625f*(dt)*dt + .9375f) + b
    } else {
        var dt=t-(2.625f/2.75f)
        return c*(7.5625f*(dt)*dt + .984375f) + b
    }
}

그리고 바로 다음에, 한 개발자가 좀 더 코틀린스러운 코드를 올려주었다.

fun bounceEaseOut(t: Float, b: Float = 0f, c: Float = 100f, d: Float = 100f): Float {
    // It's idiomatic to shadow a parameter to turn it into a var if that is needed by your code
    var t = t / d
    // b gets added to each value in the end, and so it can be moved to the start of the when expression. The when now is only responsible for calculating the multiplier for c, which reduces duplication and the possibility that later on someone might make a mistake and change the return value of a branch
    return b + c * when {
        t < 1 / 2.75f -> 7.5625f * t * t
        t < 2 / 2.75f -> {
            t -= 1.5f / 2.75f
            7.5625f * t * t + .75f
        }
        t < 2.5f / 2.75f -> {
            t -= 2.25f / 2.75f
            7.5625f * t * t + .9375f
        }
        else -> {
            t -= 2.625f / 2.75f
            7.5625f * t * t + .984375f
        }
    }
}

사실 위의 코드는 Intellj나 Android Studio를 사용하면 IDE가 제공해주는 sugesstion 기능을 사용하면 거의 같은 결과를 얻을 수 있다. 하지만 개인적인 생각으로 코드를 좀 더 손보면 좀 더 읽기 쉽고 정리된 코드로 바꿀 수 있을 것으로 보이며, 리팩토링을 해보고자 한다.

 

먼저 테스트 코드부터 만들자. 테스트코드는 매번 코드 변경시마다 실행될 것이며, 리팩토링이 기존 동작을 제대로 수행하는지 검증해 주기 때문에 아주 중요하다. 사실 질문자 올렸던 코드는 정확하게 어떤 동작을 하는지 이해하지 못했다. 따라서 테스트 값과 결과는 기존 코드를 실행해서 나온 결과를 가지고 만들었다.

@RunWith(Parameterized::class)
class BounceEraseOutTest(val testData: Pair<Int, Float>) {
    companion object {
        @JvmStatic
        @Parameterized.Parameters
        fun data(): List<Pair<Int, Float>> = listOf(
                0 to 0.0f,
                1 to 0.075624995f,
                2 to 0.30249998f,
                3 to 0.68062496f,
                4 to 1.2099999f,
                5 to 1.890625f,
                6 to 2.7224998f,
                7 to 3.7056253f,
                8 to 4.8399997f,
                9 to 6.1256256f,
                10 to 7.5625f,
                20 to 30.25f,
                30 to 68.06251f,
                40 to 91.0f,
                50 to 76.5625f,
                60 to 77.25f,
                70 to 93.0625f,
                80 to 94.0f,
                90 to 98.8125f,
                100 to 100.0f,
        )
    }

    @Test
    fun testLowest() {
        val (dt, expected) = testData
        assertEquals(expected, bounceEaseOut(dt.toFloat()), "erase value for $dt invalid")
    }
}

먼저 공통적으로 사용되는 숫자들, 2.757.5625를 상수로 축출하자. 참고로 상수의 이름은 임의대로 주었다.

const val DIVISOR = 2.75f  
const val RATIO = 7.5625f

fun bounceEaseOut(t: Float, b: Float = 0f, c: Float = 100f, d: Float = 100f): Float {    
  var t = t / d  
  return b + c * when {  
      t < 1 / DIVISOR -> RATIO * t * t  
      t < 2 / DIVISOR -> {  
           t -= 1.5f / DIVISOR  
           RATIO * t * t + .75f  
      }  
      t < 2.5f / DIVISOR -> {  
           t -= 2.25f / DIVISOR  
           RATIO * t * t + .9375f  
      }  
      else -> {  
           t -= 2.625f / DIVISOR  
           RATIO * t * t + .984375f 
      }  
   }  
}

테스트를 돌려보자. 이상없이 잘 동작한다. 다음으로는 매개변수 t를 다른 이름으로 변경하자. 왜냐하면 IDE가 Name shadowed: t라는 경고 메세지를 보여주기 때문이다. 이런 불필요한 경고 메세지는 가능하면 제거해 주는 것이 좋다.
매개 변수 이름은 원래 질문자가 사용했던 dt를 사용하기로 하자. (이게 무슨 의미인지 아직 모르겠다)

fun bounceEaseOut(dt: Float, b: Float = 0f, c: Float = 100f, d: Float = 100f): Float {   
  var t = dt / d
  //No changes other lines
}  

테스트도 잘 동작한다.

그럼, 여기서 코드를 잠시 잘 살펴보자. 어떤 데이터 구조가 필요할까? when으로 고정된 경우의 수를 가지고 있고, 각각의 경우에 따라 다른 Float값을 기반으로 계산을 하는 로직이다. 이런 고정된 경우의 숫자를 가지는 primitive 타입의 경우 enum 이나 sealed class를 사용하여 처리할 수는 있는 경우가 많다. 코드가 별다른 복잡한 부분이 없으므로, enum을 사용해 보자. 이경우 enum은 마치 소형 데이터베이스나 lookup table 같은 역할을 한다.

enum class BounceType {
   LOW, MEDIUM, HIGH, DEFAULT
}

각 멤버의 이름은 편의상 위처럼 주었고 멤버의 숫자는when의 브랜치의 갯수와 동일하다. 이제 각각의 when 브랜치에 필요한 항목들을 enum의 생성자 멤버로 설정해 준다.

enum class BounceType(val remainder: Float) {  
    LOW(1f / DIVISOR)
}

fun bounceEaseOut(dt: Float, b: Float = 0f, c: Float = 100f, d: Float = 100f): Float {  
    var t = dt / d  
    return b + c * when {  
        t < BounceType.LOW.remainder -> RATIO * t * t
         //No changes for other branches
    }

우선 케이스 브랜치 하나만 변경하고 테스트 해보자. 잘 동작하므로 다른 브랜치들도 변경해 보자.

enum class BounceType(val remainder: Float) {  
    LOW(1f / DIVISOR),  
    MEDIUM(2f / DIVISOR),  
    HIGH(2.5f / DIVISOR),  
    DEFAULT(0f / DIVISOR)  
}

fun bounceEaseOut(dt: Float, b: Float = 0f, c: Float = 100f, d: Float = 100f): Float {  
    var t = dt / d  
    return b + c * when {  
        t < BounceType.LOW.remainder -> RATIO * t * t  
        t < BounceType.MEDIUM.remainder -> {  
            t -= 1.5f / DIVISOR  
            RATIO * t * t + .75f  
        }  
        t < BounceType.HIGH.remainder -> {  
            t -= 2.25f / DIVISOR  
            RATIO * t * t + .9375f  
        }  
        else -> {  
            t -= 2.625f / DIVISOR  
            RATIO * t * t + .984375f  
        }  
    }  
}

다시 테스트를 실행한다.

이제 브랜치 간에 공통적인 코드가 더 있는지 살펴보자.

RATIO * t * t + .75f

위 부분은 모든 브랜치에 공통적으로 들어가 있다. 따라서 이부분을 BounceType 으로 옮겨 보자.

enum class BounceType(val remainder: Float, val adder: Float) {  
  LOW(1f / DIVISOR, 0f),  
  MEDIUM(2f / DIVISOR, .75f),  
  HIGH(2.5f / DIVISOR, .9375f),  
  DEFAULT(0f / DIVISOR, .984375f)  
}

fun bounceEaseOut(dt: Float, b: Float = 0f, c: Float = 100f, d: Float = 100f): Float {  
    var t = dt / d  
    return b + c * when {  
        t < BounceType.LOW.remainder -> RATIO * t * t + BounceType.LOW.adder  
        t < BounceType.MEDIUM.remainder -> {  
            t -= 1.5f / DIVISOR  
            RATIO * t * t + BounceType.MEDIUM.adder  
        }  
        t < BounceType.HIGH.remainder -> {  
            t -= 2.25f / DIVISOR  
            RATIO * t * t + BounceType.HIGH.adder  
         }  
        else -> {  
            t -= 2.625f / DIVISOR  
            RATIO * t * t + BounceType.DEFAULT.adder  
        }  
    }  
}

테스트도 성공적이다.

t -= 1.5f / DIVISOR

이 부분은 옮길 수 없을까? 첫번째 when 브랜치에는 이 계산식이 없지만 사실은 아래와 마찬가지 식이된다.

t < BounceType.LOW.remainder ->  {  
    t -= 0  
    RATIO * t * t + BounceType.LOW.adder  
}

내 추측이 맞는지 테스트를 돌려보자.

이제 모든 브랜치가 아주 유사한 모습이 되었다.

fun bounceEaseOut(dt: Float, b: Float = 0f, c: Float = 100f, d: Float = 100f): Float {  
    var t = dt / d  
    return b + c * when {  
        t < BounceType.LOW.remainder ->  {  
            t -= 0  
  RATIO * t * t + BounceType.LOW.adder  
  }  
        t < BounceType.MEDIUM.remainder -> {  
            t -= 1.5f / DIVISOR  
 RATIO * t * t + BounceType.MEDIUM.adder  
  }  
        t < BounceType.HIGH.remainder -> {  
            t -= 2.25f / DIVISOR  
 RATIO * t * t + BounceType.HIGH.adder  
  }  
        else -> {  
            t -= 2.625f / DIVISOR  
 RATIO * t * t + BounceType.DEFAULT.adder  
  }  
    }  
}

각 브랜치의 첫번째 계산식 또한 BounceType으로 옮겨보자.

enum class BounceType(val remainder: Float, val adder: Float, val subtract: Float) {  
    LOW(1f / DIVISOR, 0f, 0f),  
  MEDIUM(2f / DIVISOR, .75f, 1.5f / DIVISOR),  
  HIGH(2.5f / DIVISOR, .9375f, 2.25f / DIVISOR),  
  DEFAULT(0f / DIVISOR, .984375f, 2.625f / DIVISOR)  
}


fun bounceEaseOut(dt: Float, b: Float = 0f, c: Float = 100f, d: Float = 100f): Float {  
    var t = dt / d  
    return b + c * when {  
        t < BounceType.LOW.remainder ->  {  
            t -= BounceType.LOW.subtract  
  RATIO * t * t + BounceType.LOW.adder  
  }  
        t < BounceType.MEDIUM.remainder -> {  
            t -= BounceType.MEDIUM.subtract  
  RATIO * t * t + BounceType.MEDIUM.adder  
  }  
        t < BounceType.HIGH.remainder -> {  
            t -= BounceType.HIGH.subtract  
  RATIO * t * t + BounceType.HIGH.adder  
  }  
        else -> {  
            t -= BounceType.DEFAULT.subtract  
  RATIO * t * t + BounceType.DEFAULT.adder  
  }  
    }  
}

테스트가 잘 동작한다. 이제 한숨 고르면서 어떤 부분이 더 남았는지 살펴보자.
모든 브랜치가 동일한 패턴을 가지고 있다. 이 말은 각 브랜치의 코드를 해당 BounceType 멤버로 옮길 수 있다는 말이다.
먼저 모든 코드를 BounceType로 옮기기 전에 각 브랜치의 조건을 체크하는 함수를 추가해 보자.

enum class BounceType(val satisfied: (t: Float) -> Boolean, val adder: Float, val subtract: Float) {  
  LOW({ t -> t < 1f / DIVISOR }, 0f, 0f),  
  MEDIUM({ t -> t < 2f / DIVISOR }, .75f, 1.5f / DIVISOR),  
  HIGH({ t -> t < 2.5f / DIVISOR }, .9375f, 2.25f / DIVISOR),  
  DEFAULT({ true }, .984375f, 2.625f / DIVISOR)  
}

fun bounceEaseOut(dt: Float, b: Float = 0f, c: Float = 100f, d: Float = 100f): Float {  
    var t = dt / d  
    return b + c * when {  
        BounceType.LOW.satisfied(t) -> {  
            t -= BounceType.LOW.subtract  
  RATIO * t * t + BounceType.LOW.adder  
  }  
        BounceType.MEDIUM.satisfied(t) -> {  
            t -= BounceType.MEDIUM.subtract  
  RATIO * t * t + BounceType.MEDIUM.adder  
  }  
        BounceType.HIGH.satisfied(t) -> {  
            t -= BounceType.HIGH.subtract  
  RATIO * t * t + BounceType.HIGH.adder  
  }  
        else -> {  
            t -= BounceType.DEFAULT.subtract  
  RATIO * t * t + BounceType.DEFAULT.adder  
  }  
    }  
}

이제 계산을 하는 코드를 BounceType에 Lamda함수로 추가한다.

enum class BounceType(val satisfied: (t: Float) -> Boolean, val calculate: (Float) -> Float) {  
    LOW({ t -> t < 1f / DIVISOR }, { t ->  
        RATIO * t * t  
    }),  
    MEDIUM({ t -> t < 2f / DIVISOR }, { t ->  
        val tt = t - 1.5f / DIVISOR  
        RATIO * tt * tt + .75f  
    }),  
    HIGH({ t -> t < 2.5f / DIVISOR }, { t ->  
        val tt = t - 2.25f / DIVISOR  
        RATIO * tt * tt + .9375f  
    }),  
    DEFAULT({ true }, { t ->  
        val tt = t - 2.625f / DIVISOR  
        RATIO * tt * tt + .984375f  
    })  
}

fun bounceEaseOut(dt: Float, b: Float = 0f, c: Float = 100f, d: Float = 100f): Float {  
    var t = dt / d  
    return b + c * when {  
        BounceType.LOW.satisfied(t) -> BounceType.LOW.calculate(t)  
        BounceType.MEDIUM.satisfied(t) -> BounceType.MEDIUM.calculate(t)  
        BounceType.HIGH.satisfied(t) -> BounceType.HIGH.calculate(t)  
        else -> BounceType.DEFAULT.calculate(t)  
    }  
}

테스트를 수행하는 것을 잊지 말자.

여기까지만 해도 처음 코드에 비해 상당히 깔끔해 졌으며, 읽기 편해 졌고, 계산의 책임을 각 BounceType의 멤버에게 위임하였다. 마지막으로 한가지 더 when 표현식은 collection함수를 통해 좀 더 간단하게 만들 수 있다.

val t = dt / d
val bounceType = BounceType.values().find { it.satisfied(t) } ?: BounceType.DEFAULT  
return b + c * bounceType.calculate(t)

다음은 최종 변경된 코드이다.

const val DIVISOR = 2.75f  
const val RATIO = 7.5625f  

enum class BounceType(val satisfied: (t: Float) -> Boolean, val calculate: (Float) -> Float) {  
    LOW({ t -> t < 1f / DIVISOR }, { t ->  
        RATIO * t * t  
    }),  
    MEDIUM({ t -> t < 2f / DIVISOR }, { t ->  
        val tt = t - 1.5f / DIVISOR  
        RATIO * tt * tt + .75f  
    }),  
    HIGH({ t -> t < 2.5f / DIVISOR }, { t ->  
        val tt = t - 2.25f / DIVISOR  
        RATIO * tt * tt + .9375f  
    }),  
    DEFAULT({ true }, { t ->  
       val tt = t - 2.625f / DIVISOR  
       RATIO * tt * tt + .984375f  
    })  
}  

fun bounceEaseOut(dt: Float, b: Float = 0f, c: Float = 100f, d: Float = 100f): Float {  
    val t = dt / d  

    val bounceType = BounceType.values().find { it.satisfied(t) } ?: BounceType.DEFAULT  
  return b + c * bounceType.calculate(t)  
}  

@RunWith(Parameterized::class)  
class BounceEraseOutTest(val testData: Pair<Int, Float>) {  
    companion object {  
        @JvmStatic  
 @Parameterized.Parameters  fun data(): List<Pair<Int, Float>> = listOf(  
                0 to 0.0f,  
  1 to 0.075624995f,  
  2 to 0.30249998f,  
  3 to 0.68062496f,  
  4 to 1.2099999f,  
  5 to 1.890625f,  
  6 to 2.7224998f,  
  7 to 3.7056253f,  
  8 to 4.8399997f,  
  9 to 6.1256256f,  
  10 to 7.5625f,  
  20 to 30.25f,  
  30 to 68.06251f,  
  40 to 91.0f,  
  50 to 76.5625f,  
  60 to 77.25f,  
  70 to 93.0625f,  
  80 to 94.0f,  
  90 to 98.8125f,  
  100 to 100.0f,  
  )  
    }  

    @Test  
  fun testLowest() {  
        val (dt, expected) = testData  
  assertEquals(expected, bounceEaseOut(dt.toFloat()), "erase value for $dt invalid")  
    }  
}

'Object Oriented Programming > Refactoring' 카테고리의 다른 글

IfElse 대신 상속 사용하기  (0) 2020.09.17
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함