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
# 다형성
다형성(多形性)이란 한자 이름 그대로 어떤 객체의 속성이나 기능이 상황에 따라 여러 가지 형태를 가질 수 있는 성질을 의미합니다.
비유적으로 표현하자면, 어떤 중년의 남성이 있다고 했을 때 그 남자의 역할이 아내에게는 남편, 자식에게는 아버지, 부모님에게는 자식, 회사에서는 회사원, 동아리에서 총무 등 상황과 환경에 따라서 달라지는 것과 비슷하다고 할 수 있습니다.
객체 지향에서의 다형성도 이와 비슷합니다. 즉, 어떤 객체의 속성이나 기능이 그 맥락에 따라 다른 역할을 수행할수 있는 객체 지향의 특성을 의미합니다.
객체 지향 프로그래밍에서 다형성이란 한 타입의 참조변수를 통해 여러 타입의 객체를 참조할 수 있도록 만든 것을 의미합니다. 좀 더 구체적으로, 상위 클래스 타입의 참조변수로 하위 클래스의 객체를 참조할 수 있도록 하는 것입니다.
# 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 메소드를 오버라이딩하면서 자식클래스의 기능을 추가하고 있음. // 여기서는 나머지기능을 추가함
}
'📒 TIL - Today I Learned' 카테고리의 다른 글
20231212 / 업 캐스팅과 다운 캐스팅 (0) | 2023.12.12 |
---|---|
20231211 / 키오스크 만들기 (0) | 2023.12.11 |
20231207 / 계산기 만들기 (0) | 2023.12.07 |
20231206 / 계산기 만들기 (0) | 2023.12.06 |
20231205 / 두 수의 나눗셈 / Kotlin 강의 필기 (1) | 2023.12.05 |
블로그의 정보
꿈틀꿈틀 개발일기
jeongminy