꿈틀꿈틀 개발일기

20231209 / 계산기 코드분석 / 다형성

by jeongminy

# 어제 튜터님에게 배운 개념을 복습했다.

 

더보기

* 클래스 - 객체
* 설계도(키보드를 만들기 위한) 와 키보드 
* 클래스 : 설계도
* 객체 : 키보드
* 메소드 : 키보드의 기능

* 상속
* 쉽게 이야기 했을 때 두 가지 클래스가 등장을 합니다.
* 하나를 부모 클래스로 지정하고 하나를 자식 클래스로 지정하는게.
* 자식 클래스는 부모 클래스의 내용을 물려받는다.
* 자식 클래스가 더 크다!
* 활용1 : 부모 클래스에 있는 내용을 재사용하기 위해서 사용한다.
* 활용2 : 타입 계층을 만들어 낸다. ⇒ (추상화)

# 비유
아웃백에 가면 스테이크를 주문한다.
`스테이크 주문하기(셰프)`   // 함수를 호출하는 쪽. (Client 입장)
나는 이 셰프가 정확히 누군지 궁금하지 않다. 
  -> Client 는 해당 역할을 수행하는 객체가 누군지 관심을 가지지 않는다.
나는 셰프 "역할" 을 할 수 있는 사람이면 다 상관없어!

`val 셰프구현객체: 셰프 = 지금 근무중인 셰프 조회하기()`
`스테이크 주문하기(셰프)`

셰프(추상클래스)
 -> 셰프를 상속(구현)받는 클래스들이 있고 해당 클래스로 만들어진 객체들은 셰프 역할을 할 수 있는 사람이다.
셰프라는 곳에는 뭘 적어야하나?
* 셰프가 해야할 책임을 적어두는거에요!
셰프라는 *역할*을 구현하는 실제 객체는 여기 적혀있는 책임을 반드시 할 수 있다.
셰프를 상속받는 자식 중 하나일거에요!!!
셰프구현객체는 셰프가 가지는 책임을 반드시 할 수 있는 사람일 것이다!! -> 문법적 보장해주는 것
사실 이걸 위해서 상속을 받습니다.

`operate()` 라는 책임을 할 수 있는 애들을 만들기 위해서.. -> 책임을 할 수 있는 애들이라고 문법적으로 보장해주고 싶은거죠
`val op1: AbstractOperation = AddOperation()`
`val op2: AbstractOperation = SubstratOperation()`
`val op3: AbstractOperation = DivideOperation()`
`val op4: AbstractOperation = SampleOperation()` // 여기서만 에러가난다. 왜냐하면 SampleOperation 은 AbstractOperation 을 대체할 수 없다.
op1, op2, op3 얘네들의 공통점은 AbstractOperation 을 상속받았다. op4 상속을 안받았다.
`op1.operate()`
`op2.operate()`
`op3.operate()`
`op4.operate()` // 가지고 있으면 에러가 안나겠지만, 안가지고 있으면 에러가 난다.


# 업캐스팅에 대한 이야기
* AbstractOperation (부모) -  AddOperation (자식)
* 관계에서 제일 중요한건 자식은 부모에 명시된 행동을 모두 할 수 있다는걸 문법적으로 보장해준다!
* `val cal: Calculator = Calculator()`  // 양쪽이 똑같은 모양이다
-> 기본 자료형 : Int, Double …
-> 타입(참조 자료형) : 클래스로 정의된 타입
* `val cal2: AbstractOperation = AddOperation()`  // 양쪽 모양이 다르다. 자식모양을 부모에 담겠다.
-> 자, 그러면 돌아가서 "부모가 클까요? 자식이 클까요?", "자식은 부모가 할수있는일을 모두 할수있나요?"
-> 업캐스팅 -> 자연스러운일 -> 대체가능
* `val cal4: AbstractOperation = SampleOperation()`  // 에러납니다.
* `val cal3: AddOperation = AbstractOperation()`  // 양쪽 모양이 다르다. 부모모양를 자식에 담겠다.
-> 없다고 생각하는게 자연스러운 것! (다운캐스팅은 부자연스러운게 맞다!)
-> 가능한 상황이 있다!!!
-> 한번 업캐스팅 됐던 애를 원상복구 시킬 때!
`val cal2: AbstractOperation = AddOperation()`  // 업캐스팅(자연스러움)
`val cal3: AddOperation = cal2 as AddOperation`  // 다운캐스팅(부자연스러운 일이지만 필요한 상황이 있다)

`AbstractOperation` 이 할 수 있는 일
1. operate()
`AddOperation` 이 할 수 있는 일
1. oeprate() : 해야만 하는 일 부모를 대신하기 위해서
2. printResult()
`SubstractOperation` 이 할 수 있는 일
1. operate() : 해야만 하는 일 부모를 대신하기 위해서

업캐스팅 했으면,
`val cal2: AbstractOperation = AddOperation()` 
cal2 는 부모에요? 자식이에요? 
알아맞출수 있나요? 모르죠! 가 정답입니다.
우리가 알수 있는건 하나죠. 누가오든 적어도 AbstractOperation 일은 할 수 있는 애가 올거다! (문법적 보장이 됐으니까!)
`cal2.operate()` // 할 수 있죠?
`cal2.printResult()` // 할 수 있을 수도 있지만 문법적 보장이 안되어있기 때문에 에러가난다.

`val cal3: AddOperation = cal2` 이렇게 자식모양으로 다시 넘겨야 `printResult()` 을 할 수 있게된다.

val add = AddOperation()
add.printResult()
calculator = operate(add, 10, 20)
add.printResult()

# 다형성
```
fun operate(operation: AbstractOperation, num1: Double, num2: Double) {
operation.operate(num1, num2)
  // 해도돼? -> Y -> Why? 여기 올수있는 애들은 AbstractOperation 을 상속받았고, 그 말은 부모가 할 수 있는 일을 모두 대체할 수 있는 객체만 넘어올 수 있기 때문이다!
  // 아까 그 문법적인 보장이 되기 때문에 여기서 막 operate 를 할 수 있게되는 거다!!
}
```

`val op4: AbstractOperation = SampleOperation()` 
`fun operate(SampleOperation(), 10, 20)` // 에러가 납니다.

* 다형성 : `operation: AbstractOperation` 얘를 활용해서 `operation.operate(num1, num2)` 함수를 호출했죠.
* 그럼 위 결과는 어떻게 될까요?
* 어떤 일이 일어날지 모릅니다. -> 연산
* 이게 다형성!!


# 그래서 이런거 왜하는지?
* 변경에 유연한 코드 작성하기!
* 예를 들어서 `MultiplyOperation` 을 만들고 싶어!
1. 클래스 만들고
2. 그걸 사용하는쪽(Client 객체) 에 와서 `MultiplyOperation()` 에 대한 코드를 작성해줘야합니다.
* 이러한 구조는 변경이 전파된다.
* 만약에 추상화 타입을 만들었다면?
1. `MultplyOperation` 클래스 만들고, `AbstractOperation` 상속받도록 하면돼. -> 부모가 가지는 책임까지 다 하도록해
* 그럼으로써 변경에 유연해졌다.


4개의 객체가 공통적으로 할 수 있는 일이 있잖아요?
이거를 문법적으로 보장해주기 위해서 부모를 만든거죠! -> "추상화 시켰다!"
부모는 자격증으로써 나를 상속받은 애들은 operate 를 할 수 있어!!

지금 이거 설명한거는 객체지향에 대한 이해가 좀 필요해서.
SOLID 설계원칙 참고.
1. OCP
2. LSP

 

참고: https://velog.io/@slolee/%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5%EC%97%90%EC%84%9C-%ED%83%80%EC%9E%85%EA%B3%84%EC%B8%B5%EC%9D%B4-%EC%A4%91%EC%9A%94%ED%95%9C-%EC%9D%B4%EC%9C%A0

 


 

# 다형성

다형성(多形性)이란 한자 이름 그대로 어떤 객체의 속성이나 기능이 상황에 따라 여러 가지 형태를 가질 수 있는 성질을 의미합니다.

비유적으로 표현하자면, 어떤 중년의 남성이 있다고 했을 때 그 남자의 역할이 아내에게는 남편, 자식에게는 아버지, 부모님에게는 자식, 회사에서는 회사원, 동아리에서 총무 등 상황과 환경에 따라서 달라지는 것과 비슷하다고 할 수 있습니다.

 

객체 지향에서의 다형성도 이와 비슷합니다. 즉, 어떤 객체의 속성이나 기능이 그 맥락에 따라 다른 역할을 수행할수 있는 객체 지향의 특성을 의미합니다.

객체 지향 프로그래밍에서 다형성이란 한 타입의 참조변수를 통해 여러 타입의 객체를 참조할 수 있도록 만든 것을 의미합니다. 좀 더 구체적으로, 상위 클래스 타입의 참조변수로 하위 클래스의 객체를 참조할 수 있도록 하는 것입니다.

출처 : https://www.codestates.com/blog/content/%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%ED%8A%B9%EC%A7%95

 

# Kotlin에서의 다형성

1. 상속과 인터페이스:
- 클래스는 다른 클래스를 상속할 수 있음.
- 상속은 부모 클래스의 특성과 동작을 자식 클래스가 물려받게 해줌.
-  인터페이스를 사용하여 클래스가 특정 메서드를 구현하도록 강제할 수 있음.
-  클래스는 여러 인터페이스를 구현할 수 있음.

open class Shape

class Circle : Shape()

interface Drawable {
    fun draw()
}

class Square : Shape(), Drawable {
    override fun draw() {
        // 구현
    }
}

 

2. 오버라이딩과 다양성:
서브클래스는 슈퍼클래스의 메서드를 오버라이딩하여 동일한 메서드 시그니처를 가진 메서드를 제공할 수 있습니다.다형성은 같은 메서드 호출에 대해 실행 시간에 서로 다른 구현을 사용할 수 있는 능력을 말합니다.

open class Animal {
    open fun makeSound() {
        println("Some generic sound")
    }
}

class Dog : Animal() {
    override fun makeSound() {
        println("Woof!")
    }
}

class Cat : Animal() {
    override fun makeSound() {
        println("Meow!")
    }
}

fun main() {
    val myDog: Animal = Dog()
    val myCat: Animal = Cat()

    myDog.makeSound() // 출력: Woof!
    myCat.makeSound() // 출력: Meow!
}

 

3. 인터페이스 다형성:
인터페이스를 통해 객체가 특정 메서드를 구현하는지 확인할 수 있으며, 인터페이스를 구현한 객체들을 동일한 인터페이스 타입으로 다룰 수 있습니다.

interface SoundMaker {
    fun makeSound()
}

class Car : SoundMaker {
    override fun makeSound() {
        println("Vroom!")
    }
}

class Guitar : SoundMaker {
    override fun makeSound() {
        println("Strumming guitar strings")
    }
}

fun main() {
    val myCar: SoundMaker = Car()
    val myGuitar: SoundMaker = Guitar()

    myCar.makeSound()   // 출력: Vroom!
    myGuitar.makeSound()// 출력: Strumming guitar strings
}

 

 

 

# 만들었던 계산기의 코드를 분석해봤다.

fun main() {
    val calcadd = Calculator(AddOperation()) // 더하기 연산을 하는 Calculator인스턴스 생성
    val calcminus = Calculator(MinusOperation()) // 빼기 연산을 하는 Calculator인스턴스 생성
    val calcmultiply = Calculator(MultiplyOperation()) // 곱하기 연산을 하는 Calculator인스턴스 생성
    val calcdivide = Calculator(DivideOperation()) // 나누기 연산을 하는 Calculator인스턴스 생성
    val calcrest = Calculator(RestOperation()) // 나머지 연산을 하는 Calculator인스턴스 생성

    // Calculator() 는 다른 종류의 연산을 수행할 수 있도록
    // 다양한 AbstractOperation의 구현체 AddOperation(), MinusOperation(), MultiplyOperation(), DivideOperation(), RestOperation() 를 받아들인다.
    // 이것이 가능한 이유는 Calculator 클래스의 생성자 매개변수로 AbstractOperation을 받기 때문이다.


    println("계산기를 실행합니다.")

    println("첫번째 숫자를 입력해 주세요")
    var num1 = readLine()!!.toDouble() // 사용자로부터 입력받은 문자열을 실수(Double)로 변환하는 코드
    // readLine() 함수는 사용자로부터 콘솔에서 한 줄을 읽어오는 함수이다. 이 함수의 반환 값은 사용자가 입력한 문자열이다.
    // !!(단언연산자)를 사용한 이유 : 이 연산자는 변수 또는 표현식이 null이 아님을 개발자가 명시적으로 단언할 때 사용한다.
    // readLine() 함수는 콘솔에서 사용자 입력을 받아오는 함수이며, 사용자가 아무런 입력을 하지 않거나 입력이 null인 경우가 있는데,
    // 이때, 사용자의 입력이 null이 아님을 확실히 알고 있을 때 !! 연산자를 사용하여 Kotlin 컴파일러에게 "이 변수는 null이 아님을 확실히 알고 있으니까 안전하게 사용해도 된다"고 알려주는 것이다.

    println("두번째 숫자를 입력해 주세요")
    var num2 = readLine()!!.toDouble()

    println("연산자를 입력해 주세요.")
    var operator = readLine()!!.toString()

    var result = 0.0
    // 초기 값을 설정하기 위한 것이다.
    // 프로그램이 시작될 때 result 변수를 초기화하여 나중에 계산된 결과를 저장하는 변수이다.
    // 0.0으로 초기화 한 이유: result가 실수형(Double)으로 선언되었기 때문
    // 변수를 초기화 하는 이유: 변수를 사용하기 전에 적절한 값으로 설정하여 안전성을 확보하고, 프로그램의 예측 가능성을 높이기 위함이다.


// 입력된 연산자에 따라 적절한 Calculator 인스턴스의 operate 메서드 호출
    when (operator) {
        "+" -> result = calcadd.operate(num1, num2) // 결과 = calcadd의 operate메소드(num1과 num2를 넣은) 이다.
        "-" -> result = calcminus.operate(num1, num2) // 결과 = calcminus 의 operate메소드(num1과 num2를 넣은) 이다.
        "*" -> result = calcmultiply.operate(num1, num2) // 결과 = calcmultiply 의 operate메소드(num1과 num2를 넣은) 이다.
        "/" -> result = calcdivide.operate(num1, num2) // 결과 = calcdivide 의 operate메소드(num1과 num2를 넣은) 이다.
        "%" -> result = calcrest.operate(num1, num2) // 결과 = calcrest 의 operate메소드(num1과 num2를 넣은) 이다.
    }

    println("${num1}과 ${num2}의 ${operator} 결과는 ${result} 입니다.") //계산기의 계산 결과를 콘솔에 출력한다.
}


class Calculator(val operation: AbstractOperation) {
    // Calculator 클래스는 AbstractOperation을 받아들이는 생성자를 가지고 있음.
    // 변수operation은 추상클래스AbstractOperation가 '타입' 으로 선언되었다.
    // 이로서 변수operation은 추상클래스AbstractOperation를 통해서
    // 추상클래스 AbstractOperation에게서 상속받은 자식클래스들(AddOperation, MinusOperation, MultiplyOperation, DivideOperation, RestOperation)을 다룰수 있게 되었다.
    // 이것은 객체지향 프로그래밍의 다형성에 해당한다.
    fun operate(num1: Double, num2: Double): Double { // num1과 num2는 이 메소드를 호출할 때 전달되는 값이다.
        return operation.operate(num1, num2) // operation객체 의 operate메서드를 호출하는 것으로, 실제로 어떤 연산을 수행할지는 operation 객체에 의해 결정
    } //
    //
}

// AbstractOperation은 추상 클래스로, 다양한 연산을 나타내는 클래스들이 이를 구현합니다.
abstract class AbstractOperation {
    abstract fun operate(num1: Double, num2: Double): Double // 하위위래스에서 부모크래스의 메소드를 사용하려면 반드시 메소드 앞에도 abstract를 붙여주어야 한다.
}

// 다양한 연산을 나타내는 AbstractOperation의 하위 클래스들
class AddOperation : AbstractOperation() { // AddOperation클래스를 정의함 // 추상클래스AbstractOperation 를 상속받고있음.
    override fun operate(num1: Double, num2: Double): Double = (num1 + num2).toDouble() //상속받은 operate 메소드를 오버라이딩하면서 자식 클래스의 기능을 추가하고 있음. // 여기서는 덧셈기능을 추가함
}

class MinusOperation : AbstractOperation() { // MinusOperation클래스를 정의함 // 추상클래스AbstractOperation 를 상속받고있음.
    override fun operate(num1: Double, num2: Double): Double = (num1 - num2).toDouble() //상속받은 operate 메소드를 오버라이딩하면서 자식클래스의 기능을 추가하고 있음. // 여기서는 뺄셈기능을 추가함
}

class MultiplyOperation : AbstractOperation() { // MultiplyOperation클래스를 정의함 // 추상클래스AbstractOperation 를 상속받고있음.
    override fun operate(num1: Double, num2: Double): Double = (num1 * num2).toDouble() //상속받은 operate 메소드를 오버라이딩하면서 자식클래스의 기능을 추가하고 있음. // 여기서는 뺄셈기능을 추가함
}

class DivideOperation : AbstractOperation() { // DivideOperation클래스를 정의함 // 추상클래스AbstractOperation 를 상속받고있음.
    override fun operate(num1: Double, num2: Double): Double {   //상속받은 operate 메소드를 오버라이딩하면서 자식클래스의 기능을 추가하고 있음. // 여기서는 나눗셈기능을 추가함
        // 0으로 나누는 경우 예외 처리를 추가하여 프로그램의 안정성을 높인다.
        // num2가 0이라면 "Divide by Zero"를 발생시켜줘서 예외를 처리해준다
        require(num2 != 0.0) {
            ArithmeticException("Divide by Zero")
        }
        return (num1 / num2).toDouble()
    }
}

class RestOperation : AbstractOperation() { // RestOperation클래스를 정의함 // 추상클래스AbstractOperation 를 상속받고있음.
    override fun operate(num1: Double, num2: Double): Double = (num1 % num2).toDouble() //상속받은 operate 메소드를 오버라이딩하면서 자식클래스의 기능을 추가하고 있음. // 여기서는 나머지기능을 추가함
}

 

블로그의 정보

꿈틀꿈틀 개발일기

jeongminy

활동하기