객체가 할 수 있는 행위들을 전략 클래스로 생성하고
이 행위들을 캡슐화하는 인터페이스를 정의하여
동적으로 객체의 행위를 바꾸는 패턴
비슷한(연관된) 속성과 메서드들을 하나의 객체에 묶는 특성을 말한다.
여기서는 객체의 행위들에서 비슷한 속성과 메서드들을 하나로 묶어서 하나의 인터페이스로 만드는 것에 캡슐화를 사용한다.
객체가 할 수 있는 행위들을 하나의 인터페이스로 만들고,
이를 구현한 여러개의 전략 클래스로 만들게 되면
객체를 직접 수정하지 않고 행위를 유연하게 변경할 수 있다.
아래 그림처럼 Strategy 인터페이스를 상속하는 여러개의 StrategyClass들을 만들게 되면
실제로 하나의 객체 Context에서 이를 유연하게 변경하면서 행위를 바꿀 수 있다는 뜻이다.
- 그림 출처: 위키피디아
예시를 들어서 설명하자면,
동물(객체)은 저마다 각자 울음소리가 있다.
강아지는 "멍멍"(전략 클래스)
고양이는 "야옹"(전략 클래스)
그리고 이 울음소리(인터페이스)를 갖는 동물(객체)를 만드는 것이다.
그렇게 되면 "음머~"(전략클래스)를 추가하고 싶을 때
울음소리(인터페이스)를 구현한 "음머~"(전략클래스)를 새로 만들고
이를 setter함수인 setCry(CowCry)를 이용해서
객체의 속성필드를 CowCry(해당 전략클래스)로 변경하여 확장이 가능하고
강아지의 소리를 "왈왈"로 변경하고 싶을 때
강아지 울음소리를 갖는 전략클래스만 수정하고 Animal 객체 자체의 수정을 하지 않고도 행위 내용을 변경할 수 있다.
이를 코드를 사용해서 나타내면
public Interface CryBehavior {
cry();
}
울음소리행위에 대한 인터페이스를 위와 같이 지정해주고
해당 인터페이스에는 cry()라는 우는 행동을 하는 함수가 있다.
public class DogCry implements CryBehavior {
cry() {
System.out.println("멍멍");
}
}
public class CatCry implements CryBehavior {
cry() {
System.out.println("야옹");
}
}
public class CowCry implements CryBehavior {
cry() {
System.out.println("음머~");
}
}
이 울음소리 행동 인터페이스를 구현한
강아지 울음소리, 고양이 울음소리, 소 울음소리를 각각 전략 클래스들을 생성한다.
public class Animal {
CryBehavior cryBehavior;
setCry(CryBehavior someAnimalsCryBehavior) {
this.cryBehavior = someAnimalsCryBehavior;
}
animalCry() {
cryBehavior.cry();
}
}
최종적으로 Animal 객체에서 CryBehavior 인터페이스 타입의 멤버 변수를 갖고
이를 변경할 수 있는 setter함수를 설정한 뒤
animalCry()라는 울음소리를 내는 함수를 만들 때
현재 설정된 CryBehavior을 구현한 전략 클래스의 울음소리로 울음소리를 낼 수 있게끔 할 수 있다.
이렇게 되면 Animal 객체를 생성하고 난 뒤
Animal animal = new Animal();
animal.setCry(new DogCry());
animal.animalCry(); // "멍멍"
animal.setCry(new CatCry());
animal.animalCry(); // "야옹"
animal.setCry(new CowCry());
animal.animalCry(); // "음머~"
이런식으로 객체 내용을 직접적으로 바꾸지 않으면서도 행위들을 바꿀 수 있게 된다.
그렇다면, 전략 패턴은 SOLID의 어떠한 원칙들을 잘 지키고 있을지 추측해보자!
SRP: 각 전략클래스들이 하나의 책임만을 가지고 나뉘고 있다
OCP: 울음소리에 대해 객체 자체 변경에는 닫혀있고 전략클래스를 통한 확장에 열려있다.
LSP: CryBehavior 타입의 cryBehavior에 DogCry, CatCry, CowCry같은 자식 객체들이 문제없이 모두 들어갈 수 있다.
ISP: 이러한 비슷한 행위들에 따라서 인터페이스로 캡슐화 하고 구체적인 전략클래스를 설정하려면 우선 비슷한 기능(우는 기능)을 분리해서 나누었다.
DIP: 상위객체(CryBehavior)에 의존하여 우는 기능을 만들었다.
위 전략 패턴의 설명에서 행위들을 캡슐화 하는 Interface를 정의한다고 하였지만, Interface가 자바에서 의미하는 Interface만을 의미하는 것이 아니고, 캡슐화가 가능하게 만들면 되는 것이다. 자바에는 이를 가능하게 하는 Interface와 Abstract Class가 있다.
Abstract Class | Interface |
---|---|
상수, 변수 필드 포함 가능 인스턴스 메소드 포함 가능 다중 상속 불가능 |
상수 필드만 포함 가능 추상메소드, 디폴트메소드, 정적메소드만 포함 가능 다중 상속 지원 |
동일한 메서드가 모두 필요한 상황에서 인터페이스를 사용하게 된다면 그 인터페이스를 구현한 모든 클래스에서 동일한 메서드를 똑같이 Overriding해서 사용해야 하기 때문에 이때는 추상클래스를 사용하는 것이 맞다.
한 클래스 내에 두 개 이상의 같은 이름을 가진 메소드를 작성하는 것으로,
두 메소드의 이름이 같으면서, 매개변수의 갯수가 다르거나, 매개변수의 타입이 달라야 한다.
오버로딩은 같은 기능과 이름을 가진 새로운 메소드를 만드는 것으로 같은 이름을 가진 기존 메소드에 추가로 과적(Overload)하여 새로운 메소드를 만드는 것이다.
인자(argument)의 자료형과 매개변수(parameter)의 자료형이 일치하는 메소드가 우선적으로 실행되고, 없을 시에 자동 형변환을 통해서 작은 범위의 자료형(int)이 큰 범위의 자료형(long)으로 변환되어 오버로딩된 함수가 호출되고 이를 Promotion 되었다고 한다.
슈퍼클래스의 메소드를 상속받은 서브 클래스에서 메소드의 내용을 재정의(변경)하는 것으로,
메소드의 이름, 매개변수의 타입 및 개수, 리턴 타입이 모두 같아야 한다.
메소드 오버라이딩을 하게 되면 동적 바인딩을 통해서 오버라이딩 된 서브 클래스의 메소드가 실행되게 된다.
이때, 오버라이딩된 메소드의 접근자(public, private,...)는 슈퍼 클래스의 메소드의 접근지정자보다 그 범위가 좁을 수 없다.
슈퍼클래스 -> public
서브클래스 -> protected(x)
private, final 접근지정자로 정의된 메소드는 오버라이딩 될 수 없다.