예제는 한국에서 코딩하는 사람이면 다 통하는 스타 예제로 진행합니다.
아래 글을 종합하면 다음과 같습니다.
구현하는 부분중 변하는 부분과 변하지 않는 부분이 있다면
변하지 않는 부분은 상속을, 변하는 부분은 인터페이스로
구현하여 관리하며
결국 객체중에서도 자주 변화하는 부분을 떼어
관리하는 기법이라고 생각된다.
결국 객체중에서도 자주 변화하는 부분을 떼어
관리하는 기법이라고 생각된다.
1. 개념
구현하려고 하는 기능은 아래와 같아요
유닛 : 마린, 탱크, 종이비행기
각 유닛은 이동, 공격 두가지의 행동을 지니는데.
같은 행동이지만 아래와 같은 차이점을 지닌다.
마린은 걷고(이동), 총쏘고(공격)
탱크는 바퀴를 굴리고(이동), 대포를 쏜다(공격)
종이비행기는 날고(이동), 프로젝트빔을 쏜다(공격)
1) 이정도 간단한 내용이라면 클래스 하나에 작성이 가능하고
2) 좀더 객체지향적이라면 유닛 클래스를 만든뒤 이동, 공격을 구현후
각 3개의 클래스에서 받아서 오버라이드 해서 써도 괜찮을 것 같은데
구현 후에 다음의 추가사항을 가정합니다.
(물론 가정이고 여기서는 구현하지 않습니다.)
마린은 총을 쏘는데 총 종류가 많아졌어요 그리고 각 마린들별로
사용하는 총이 틀려요
탱크는 대포 구경이 여러가지고, 로봇 다리로 이동하는 경우가 생겼어요
종이비행기는 날지 않기도 하고(작은 바퀴로 이동),
프로젝트 빔 뿐만 아니라 레이저빔도 쏴요
그리고 대기, 멍청하게 서 있기 명령을 추가해야 합니다.
이런 추가사항들이 끼어들 경우 이전에 구현한 소스를
뒤져보고 어느정도 수정해야하고
디버깅도 다시해야 하는 사항이 발생합니다.
뒤져보고 어느정도 수정해야하고
디버깅도 다시해야 하는 사항이 발생합니다.
새로운 요구사항이 2,3,4,5,6,7,8,9 이상 계속 발생한다면?
그리고 그런 요구사항의 발생은 현실이기도 합니다.
2. 소스(예제)
1) 프로젝트 구성입니다.
2) 설명입니다.
케릭터 관련 패키지
케릭터는
CommUnit 이라는 대장이 있고(부모클래스) 이를 상속 관계의
여러 클래스( 마린, 탱크, 비행기) 가 있으며
introduce() 라는 메소드를 구현하려고 합니다.
근데 특이한점이 있네요 클래스 멤버변수로
MoveBehavior, AttackBehavior 가 추가되어 있는게 보입니다.
이부분을 보면 아래와 같습니다.
가보니 해당 클래스는 인터페이스 네요 그리고 해당 인터페이스를
인플리먼트 하여 구현한 3개의 클래스에 같은 메소드가 위치하는데
각각 다른 행동을 하도록 구현되어 있습니다.
물론 여기서는 콘솔창으로 한줄 메시지만 전달하지만
실제로 이동이라는 행동에 대한 구체적인 내용들이 저 클래스에서
이루어질 겁니다.
이를 정리해보면 전체적인 모습은 다음과 같아집니다.
3) 소스 및 설명입니다.
(1)우선 AttackBehavior 이라는 인터페이스 와 그 인터페이스를
구현한 3개의 하위 클래스 입니다.
interface AttackBehavior
1
2
3
4
5
|
package UnitAttack;
public interface AttackBehavior {
void attack();
}
|
class AttackWithGun
1
2
3
4
5
6
7
8
|
package UnitAttack;
public class AttackWithGun implements AttackBehavior{
public void attack() {
System.out.println("공격은 총으로 땅야땅야 하고 쏩니다.");
}
}
|
class AttackWithCannon
1
2
3
4
5
6
7
8
|
package UnitAttack;
public class AttackWithCannon implements AttackBehavior{
public void attack() {
System.out.println("공격은 대포로 쏩니다.");
}
}
|
1
2
3
4
5
6
7
8
|
package UnitAttack;
public class AttackWithLaser implements AttackBehavior{
public void attack() {
System.out.println("공격은 레이저 빔으로 따끔따끔하게 쏩니다.");
}
}
|
attack() 라는 같은 메서드에 다른 내용이 담긴걸 볼 수 있습니다.
(2) 다른 행동인 MoveBehaivor 인터페이스와 이를 구현한 클래스입니다.
1
2
3
4
5
|
package UnitMove;
public interface MoveBehavior {
void move();
}
|
1
2
3
4
5
6
7
|
package UnitMove;
public class MoveWithDrive implements MoveBehavior{
public void move() {
System.out.println("움직입니다만 바퀴를 굴려서 이동합니다.");
}
}
|
1
2
3
4
5
6
7
8
|
package UnitMove;
public class MoveWithFly implements MoveBehavior{
public void move() {
System.out.println("비행물체는 날라서 이동합니다.");
}
}
|
1
2
3
4
5
6
7
8
|
package UnitMove;
public class MoveWithWork implements MoveBehavior{
public void move() {
System.out.println("무브 명령으로 움직입니다만 걸어서 이동합니다.");
}
}
|
(3)이제 캐릭터의 공통 유닛 클래스로 넘어갑니다. + 상속받은 클래스들
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
package Caractor;
import UnitAttack.AttackBehavior;
public class CommUnit {
AttackBehavior attackBehavior;
public void performAttack(){
attackBehavior.attack();
}
public void introduce() {
// TODO Auto-generated method stub
}
public void setAttackBehavior(AttackBehavior ab){
attackBehavior = ab;
}
}
|
멤버변수로 AttackBehavior 를 가지고 있고
Attack() 실행 할 수 있도록 performAttack() 함수를 지정했어요
introduce() 는 밑에서 구현하도록 놔두었구요
set~~ 함수는 밑에서 구현할때 함수를 이용해서 다른 클래스로
변경하라고(편하게 쓰라고) 해놓은 겁니다.
이제 공통 유닛 클래스를 상속받은 클래스들입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
package Caractor;
import UnitAttack.AttackWithCannon;
import UnitMove.MoveWithDrive;
public class Tank extends CommUnit{
public Tank(){
moveBehavior = new MoveWithDrive();
attackBehavior = new AttackWithCannon();
}
public void introduce(){
System.out.println("여기는 탱크병장이 커버합니다.");
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
package Caractor;
import UnitAttack.AttackWithLaser;
import UnitMove.MoveWithWork;
public class AirPlane extends CommUnit{
public AirPlane(){
moveBehavior = new MoveWithWork();
attackBehavior = new AttackWithLaser();
}
public void introduce(){
System.out.println("상공은 종이비행기가 커버합니다.");
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
package Caractor;
import UnitAttack.AttackWithGun;
public class Marin extends CommUnit{
public Marin(){
attackBehavior = new AttackWithGun();
}
public void introduce(){
System.out.println("일단 여기는 마린자리예요");
}
}
|
마린 클래스는 unit 클래스를 상속받는데 생성자에 뭘 집어넣어놨네요
객체를 생성할때 부모에게서 상속받은 인터페이스 객체에
총으로 공격하겠다고 넣었네요, 만약 메인에서
마린 객체를 생성한다면 해당 클래스의 move() 가 셋팅됩니다.
introduce() 는 따로 구현되어 있는데 이도 move()처럼 구현하는것도
가능합니다.
(4) 이제 잘 동작하는지 확인하기 위해서 부가적인 클래스를 작성해서 확인합니다.
1
2
3
4
5
6
7
8
9
10
11
12
|
package Simulation;
import Caractor.CommUnit;
import Caractor.Marin;
public class SimulationCaractor {
public static void main(String[] args) {
CommUnit kimUnit = new Marin();
kimUnit.performMove();
System.out.println();
}
}
|
1
2
|
공격은 총으로 땅야땅야 하고 쏩니다.
|
클레스가 많은거에 비해서 출력 결과물은 쓸데 없어 보이지만
마린이 전혀 관계 없을것처럼 보이는AttackWithGun 클래스의
attack() 를 실행하고 있는걸 볼 수 있습니다.
이제 Unit 을 상속받은 다른 클래스를 작성합니다.
탱크랑 비행기를 추가하고 공격 인터페이스 밑에 탱크 , 비행기 attack() 를 구현한 클래스를 추가한뒤
Unit 밑에 탱크랑 비행기 생성자에 적절한 인터페이스 를 임플리먼트한 클래스로 객체를 만든다면
각 객체별로 다른 모습을 보여줍니다.
그리고 여기에 이제 공격 방식에 대한 구체적인 작성이 들어가는 경우
Unit 패키지를 건들지 않고 AttackBehavior 밑의 클래스만 수정하면
다른 동작을 보이겠네요
여기에 무브를 추가한다면 어떨까요
무브를 추가하기 위해서
인터페이스를 먼저 만들고 이를 상속받은 다른 무브들을 구현한뒤
메인함수에는 멤버변수와 실행문 메서드를 추가
그리고 추가하고싶은 클래스 생성자를 추가한뒤
실행하고자 하는 시점에 performMove() 함수만 콜하면 됩니다.
무브에 대한 수정이 발생할 경우 유닛이 가지고 있는 인터페이스를 임플리먼트해 구현한 클래스의 매서드만 수정하면 되겠네요
(위 목적을 위해서 스트래티지 패턴을 사용합니다.)
파일수는 좀 많아지지만 작성해야 할 소스량이 많아진다면
클래스 또는 함수 하나당 복잡도를 낮출 수 있는 좋은 방법입니다.
3. 참고
4. 관련 패턴
1) Flyweight 패턴
ConcreteStrategy 역할을 Flyweight 패턴을 사용해서
여러곳에서 공유가 가능합니다.
2) Abstract Factory 패턴
전체가 아닌 부분을 교체하는 방법이 Abstract Factory 입니다.
3) State 패턴
위임하는곳을 교체하는점, 클래스간 관계도 비슷함 하지만
패턴의 목적은 상이합니다.
댓글 없음:
댓글 쓰기