Skip to content

Latest commit

 

History

History
303 lines (188 loc) · 11.3 KB

다형성.md

File metadata and controls

303 lines (188 loc) · 11.3 KB

다형성

다형성이란 동일한 조작방법으로 동작시키지만 동작방법은 다른 것을 의미한다.

메서드의 다형성 vs 클래스의 다형성

  1. 한 타입의 참조변수로 여러 타입의 객체를 참조할 수 있도록 함

객체지향개념에서 다형성이란 **'여러 가지 형태를 가질 수 있는 능력'**을 의미하며 자바에서는 한 타입의 참조변수로 여러 타입의 객체를 참조할 수 있도록 함(= 하나의 클래스가 다양한 동작방법을 가지고 있는 것)으로써 다형성을 프로그램적으로 구현하였다.

  1. 하위클래스 특화된 메서드를 실행 OR 오버로딩

“상위 클래스의 좀 더 포괄적인 개념을 이용해서 하위 클래스들에 동일한 방식으로 접근하되, 그 하위 클래스의 특화된 메서드를 실행하는 기법”

JAVA에서 다형성의 구현

  1. Java에서 다형성은 상속과 인터페이스를 통해 이루어진다.
  2. Java에서 다형성은 오버라이딩과 오버로딩을 통해 이루어진다.

같은 행위를 하지만 용도와 목적에 부합하여 다양한 기능수행과 처리, 결과를 낳을 수 있는 것이다.

참고로 overloading이 다형성인지 아닌지에 대해서는 이견이 존재하는 것으로 보인다. By 생활코딩

호출할 수 있는 메소드 역시 타입에 따라 달라진다는 것이다. 상속의 오버라이딩을 설명하면서 오버라이딩을 하게 되면 컴파일러는 실제 객체의 메소드를 바라보는 것이 아니라. 변수 선언 타입의 메소드를 본다.

다형성을 통해 확장성을 획득한다. 즉 확장의 원리인 OCP를 획득한다.

LSP를 바탕으로 OCP는 확장하는 부분에 다형성을 제공해 변화에 열려있는 프로그램을 만들 수 있도록 합니다.

다형성 구현 방법

클래스의 상속이나 인터페이스를 구현하는 자식 클래스에서 메서드를 재정의(오버라이딩) 하고 자식 클래스를 부모 타입으로 업캐스팅한다. 그리고 부모 타입의 객체에서 자식 멤버를 참조하여 다형성을 구현한다. 이전 포스팅에서 알아보았던 업 캐스팅의 예제도 다형성의 방법이다.

-> 부모 클래스로 업캐스팅 할 경우, 부모에서 정의되지 않는 메서드 등은 사용 할 수 없다. 즉, 자식을 업캐스팅 함으로써 부모 클래스처럼 동작하게 함. 동시에 클래스 B에서 오버라이딩한 맴버의 동작방식은 그대로 유지한다.

package org.opentutorials.javatutorials.polymorphism;
abstract class Calculator{
    int left, right;
    public void setOprands(int left, int right){
        this.left = left;
        this.right = right;
    } 
    int _sum() {
        return this.left + this.right;
    }
    public abstract void sum();  
    public abstract void avg();
    public void run(){
        sum();
        avg();
    }
}
class CalculatorDecoPlus extends Calculator {
    public void sum(){
        System.out.println("+ sum :"+_sum());
    }
    public void avg(){
        System.out.println("+ avg :"+(this.left+this.right)/2);
    }
} 
class CalculatorDecoMinus extends Calculator {
    public void sum(){
        System.out.println("- sum :"+_sum());
    }
    public void avg(){
        System.out.println("- avg :"+(this.left+this.right)/2);
    }
} 
public class CalculatorDemo {
    public static void main(String[] args) { 
        Calculator c1 = new CalculatorDecoPlus();
        c1.setOprands(10, 20);
        c1.run();
         
        Calculator c2 = new CalculatorDecoMinus();
        c2.setOprands(10, 20);
        c2.run();
    }
   
}

차이점은 아래와 같다. 아래는 예전 코드다.

img

아래는 변경된 코드의 내용이다.

img

차이점은 Calculator를 상속 받은 클래스들을 인스턴스화 할 때 Calculator를 데이터 타입으로 하고 있다. 이렇게 되면 인스턴스 c1과 c2를 사용하는 입장에서 두개의 클래스 모두 Calculator인 것처럼 사용할 수 있다.

또한 execute()라는 메서드 제작시에, 다형성을 통해서 각각의 자식 CalculatorDecoPlus, CalculatorDecoMinus을 파라미터로 받는 메서드를 제작하지 않고, Calculator객체를 파라미터로 받는 메서드만 제작해도된다.

execute(Calculator calculator) 메서드 내부에서는, Calculator 여러 자식 객체 들이 담긴 calculator라는 참조변수임에도, Calculator의 기능을 보장하므로 내부 로직을 Calculator메서드로 구현할 수 있게 된다.

예제

https://m.blog.naver.com/PostView.nhn?blogId=heartflow89&logNo=220979244668&proxyReferer=https%3A%2F%2Fwww.google.com%2F

즉, 오버워치라는 하나의 인터페이스에서 그것을 구현하는 각 캐릭터(객체) 들에 대해 동일한 버튼(메소드 오버라이딩)을 클릭하여 서로 다른 스킬을 사용하게 된다(다형성).

다형성이 왜 필요한가?

  1. 코드 재사용성, 유지보수성
  2. 확장성(메서드를 재정의) 유연성(하나의 인터페이스를 일관성 있게 사용자 중심적으로 제공 = 편리성)
  3. 객체지향 설계원칙에 부합함
  4. 의존성 약화
  5. 한 가지 타입으로 관리하여 묶어서 사용하기 위함(ex. 한 가지 타입으로 배열로 묶어 사용할 수 있음)

다형성의 특징

  1. 동적 바인딩 : 런타임시 호출 클래스 및 메소드 결정
  2. 재사용성 , 추상화, 상속성 개념 포함
  3. 메세지

리스코프 치환 원칙LSP

부모 클래스와 자식 클래스 사이의 행위가 일관성 있어야 한다

즉, '컴퓨터 프로그램에서 자료형 S가 자료형 T의 하위형이라면 필요한 프로그램의 속성의 변경 없이 자료형 T의 객체를 자료형 S의 객체로 치환 할 수 있어야 한다' 는 원칙.

package com.dubbing.test;
 
/**
 * 
 * @author leesungwoo
 * Dubbing 이네 동물농장 
 * 
 */
public class AnimalHouse {
    public static void main(String[] args) {
        //Cat nabi = new Cat();
        Animal nabi = new Cat(); 
        // Cat의 상위타입인 Animal로 바꿔도 기능에 문제가 발생하지 않는다.
        // 또한 다른 동물 클래스들이 추가되어도 LSP원칙을 따르기 때문에 문제가 발생하지 않음을 의미함. (= 확장에 열려있다.)
        nabi.makeSound();
        nabi.makeSound();
        nabi.makeSound();
        
        nabi.eatFood();
    }
}

위반 예제

https://2dubbing.tistory.com/25

public class Duck {
    protected boolean starve = true;
    public void sing() {
        System.out.println("quack quack");
    }
    public void eat() {
        System.out.println("munch munch");
        this.starve = false;
    }
    public boolean isStarve() {
        return this.starve;
    }
}
 
public class ToyDuck extends Duck {
    @Override
    public void eat() {
        System.out.println("toy duck cannot eat!");
    }
}

eat()을 재정의하여 '장난감 오리는 음식을 먹을 수 없어요.' 가 출력되도록 기능을 변경했습니다.

하지만, 상위클래스의 eat()에서는 starve 값을 변화해주고 있는데,

하위클래스에서 재정의한 eat() 에는 starve 값을 바꿔주는 기능을 넣지 않았습니다.

물론 코드를 실행해보면 컴파일 에러가 발생하거나 별다른 에러가 발생하진 않습니다.

하지만, 하위클래스는 상위클래스의 명세를 따르지 않았기에 추후 원하는 데이터를

얻지 못할 가능성이 생기게 됩니다.

2. 직사각형 예제

https://nesoy.github.io/articles/2018-03/LSP

정사각형 클래스는 항상 너비와 높이가 같다고 간주할 수 있다.

정사각형 객체가 직사각형을 다루는 문맥에서 사용되는 경우, 정사각형의 크기는 독립적으로 변경할 수 없기 때문에 (혹은 그래서는 안되기 때문에) 예기치 못한 행동을 하게 된다.

정사각형 클래스의 할당 메서드를 수정하여 정사각형의 불변 조건(즉, 너비와 높이가 같음)을 유지하면, 이 메서드는 크기를 독립적으로 변경할 수 있다고 설명한 직사각형의 할당자의 사후 조건을 무력화(위반)한다.

해결 방법

  • 상속의 관계를 제거하는 방법.
  • 기능을 제대로 하지 못하는 area()를 자식 클래스로 이동시키는 방법.

만약 DiscountedBag 클래스를 위와 같이 만들면 실행 결과가 달라진다.

따라서, 일관성이 깨져 LSP를 만족하지 않고 피터 코드의 상속 규칙에서 "서브 클래스가 슈퍼 클래스의 책임을 무시하거나 재정의하지 않고 확장만 수행한다"라는 규칙에도 어긋난다.

이는 슈퍼 클래스의 메서드를 오버라이딩 하지 않는 것과 같다. 즉, 피터 코등의 상속 규칙을 지키는 것은 LSP를 만족시키는 하나의 방법이다.

개방 폐쇄 원칙

개방-폐쇄 원칙이 잘 적용되면, 기능을 추가하거나 변경해야 할 때 이미 제대로 동작하고 있던 원래 코드를 변경하지 않아도, 기존의 코드에 새로운 코드를 추가함으로써 기능의 추가나 변경이 가능하다.

모듈은 추상화를 조작할 수 있다. 이런 모듈은 고정된 추상화에 의존하기 때문에 수정에 대해 닫혀 있을 수 있고, 반대로 추상화의 새 파생 클래스를 만드는 것을 통해 확장도 가능하다. 따라서 추상화는 개방-폐쇄 원칙의 핵심 요소이다.

예시

public class Computer {
	private final SKeyboard sKeyboard = new SKeyboard();
	public void boot() {
		System.out.println("부팅 중~~");
		sKeyboard.connect();
	}
}

이때 SKeyboard를 사용하지 못하게 된다면, 컴퓨터를 사용할 수 없게된다.

따라서 확장성 있는 소프트웨어를 목표로 코드를 수정한다.

public class Computer {
	private Keyboard keyboard;
	public void setKeyboard(Keyboard keyboard) {
		this.keyboard = keyboard;
	}
	public void boot() {
		System.out.println("부팅 중~~");
		keyboard.connect();
	}
}

위처럼 Keyboard 인터페이스를 정의하고, set메서드를 통해 특정 제조사의 키보드 객체를 주입받는다.

즉 키보드 객체를 추상화 시킴으로써 KeyBoard 인터페이스를 구현한 어떠한 키보드 클래스도 주입 받을 수 있게 되었다.

변경 후 장점

  1. 키보드를 변경할 시 Computer클래스를 수정할 필요가 없다. 외부에서 키보드 객체를 주입해 주기만 하면 된다. 즉, 이를 통해 OCP원칙 중 하나인 "변경에는 닫혀있다."는 원칙이 성립된다.
  2. 이제 위의 컴퓨터 소프트웨어에서, 다른 키보드를 사용하기 위해서 Keyboard 인터페이스를 구현하기만 하면된다. 즉 확장성을 얻게 되었다. "즉, 확장에는 열려있다."는 조건을 만족한다.