개요
디자인 패턴을 본격적으로 공부하기 전에 객체지향에서 어떠한 프로그래밍을 지향해야 하는지에 대해서 부터 배우기로 했다. 애초에 디자인패턴이라는게 대체적으로 함수형 프로그래밍 언어에서는 해당되지 않는다.
...
객체 지향 설계 5대 원칙들의 앞자들만 줄여서 SOLID라고 부른다. 하나하나 천천히 살펴보자.
1. Single Responsibility Principle(단일 책임의 원칙)
제일 간단하다고 생각되는 원칙이다.
말 그대로 하나의 클래스는 하나의 책임만 져야한다.
// 플레이어 조작을 처리하는 클래스
public class PlayerController {
public void Move(PlayerInfo player, int x, int y) {
// 플레이어 이동 처리
}
public void Attack(PlayerInfo player, PlayerInfo target) {
// 공격 처리
}
}
// 플레이어 클래스
public class Player {
private PlayerController controller;
public void Move(int x, int y) {
controller.Move(info, x, y);
}
public int Health {
get { return info.Health; }
set { info.Health = value; }
}
}
간단한 예시이다.
뭐 MonsterFactory에서는 몬스터의 생성만, MonsterManage이라면 몬스터의 관리만.. 하는 것은 당연하다.
(단, Manager 키워드를 남용하는 것은 좋지않다. Manage라는 범위가 모호하기 때문)
위의 예시에서 좀 주의깊에 봐야할 것은 playerClass 안에 PlayerConroller controller 키워드로 선언한 것이다.
클래스를 분리해서 굉장히 깔끔해졌고, 단일 책임의 원칙을 위배하지 않았다. 심지어 다른 클래스에서 재사용도 가능할 것이다. 또한 필드로 선언했기 때문에 클래스 안에 있는 것 처럼 사용이 가능하다.
2.Open Closed Principle(개방-폐쇄 원칙)
개방폐쇄 원칙도 좀 간단한 편이다. 확장에는 열려있고, 변경에는 닫혀있어야 한다는 것이다.
잉 ? 확장에 열려있고 변경에는 닫혀있는게 뭐시여 빠르게 예시로 보자
// 캐릭터 클래스
public abstract class Character {
public abstract void Move();
}
// 전사 클래스
public class Warrior : Character {
public override void Move() {
// 전사의 이동 로직 구현
}
}
// 마법사 클래스
public class Wizard : Character {
public override void Move() {
// 마법사의 이동 로직 구현
}
}
만약 개방 폐쇄원칙을 적용하지 않는다면 Character에 공통된 기능인 Move에 모든 코드를 때려박아도 된다.
그러나 전사와 마법사의 이동 로직이 다를 것을 예상하고 미리 추상 메서드로 정의해놓은 뒤 따로 따로 구현하면 된다.
3.Liskov Substitution Principle(리스코프 치환 원칙) LSV로 줄여서 부른다.
치환이라는 말을 제외하면, 전혀 패턴의 예상이 가지 않는 법칙이다.
서브 타입은 언제나 그것의 기반 타입으로 대체될 수 있어야 한다. 즉, 상속받은 클래스는 기반 클래스의 기능을 모두 사용할 수 있어야 한다는 뜻이다.
말로만 들어서 잘 이해가 안가는데 쉽게 비유하자면 다형성(polymorhism) 을 생각하면 쉽다. 다형성은 하나의 객체가 여러가지의 상태를 가질 수 있다는 것인데.. 그냥 코드로 보자
// 캐릭터 클래스
public abstract class Character {
public abstract void Move();
}
// 전사 클래스
public class Warrior : Character {
public override void Move() {
// 전사의 이동 로직 구현
}
}
// 마법사 클래스
public class Wizard : Character {
public override void Move() {
// 마법사의 이동 로직 구현
}
}
// 적 클래스
public class Enemy {
public void Attack(Character character) {
// 적의 공격 로직 구현
character.Move();
}
}
위의 코드는 LSV를 잘 지킨 코드이다. Move 메서드를 상속받고, override로 잘 구현했기 때문이다.
잉 ? 당연한거 아닌가 ? 라고 생각할 수 있지만 애초에 당연하니까 법칙인 것..
잘못된 경우는 다음과 같겠다. 첫번째 코드는 실행조차 되지 않는다.
// 잘못된 코드. 매개변수가 다름!
public class Wizard : Character {
public override void Move(float acceleration) {
// 마법사의 이동 로직 구현
}
public override string ToString()
{
//ToSting 메서드는 일반적으로 객체의 상태를 문자열로 나타내는데
//이 함수는 일관적이지 않게 메서드를 오버라이딩했다.
return "!A!AQ!";
}
}
다시 간단하게 정리하자면 부모 클래스의 메서드를 일관적으로 원칙에 맞게 구연하는게 LSV이다.
4.Interface Segregation Principle(인터페이스 분리 원칙)
여러개의 구체적인 인터페이스를 사용하는 것이 일반적인 인터페이스 하나를 사용하는 것보다 유리하다.
하나의 일반적인 인터페이스는 많은 동작을 정의하고, 이를 구현하는 클래스는 불필요한 동작까지 구현할 가능성이 높다. 이에 비해, 여러개의 구체적인 인터페이스는 필요한 동작만 정의하는 것이다. 딱히 예제 코드가 필요 없을 것 같다.
5.Dependency Inversion Principle(의존 역전 원칙)
DIP는 두 가지 원칙으로 구성된다.
1. 고차원 모듈과 저차원 모듈 사이의 의존 관계를 역전시키는 것
2. 추상화와 구체적인 것 사이의 의존 관계를 역전시키는 것
캐릭터 클래스와 무기 클래스를 예시로 들어보자. 캐릭터 클래스가 상위 클래스이고, 무기 클래스는 하위 클래스가 될 것이다. 이때 칼 클래스를 직접 참조하면, 만약 나중에 칼 클래스에서 활 클래스로 바꿔야 하는 상황이 생긴다면, 모든 코드를 수정해야 하는 번거로움이 생긴다.
이렇게 되면 캐릭터 클래스가 칼과 활 클래스에 얽매이게 되어, 상위 클래스인 캐릭터 클래스가 하위 클래스인 칼과 활 클래스에 의존하게 된다는 것이다. 이러한 상황은 효율적인 코드를 작성하는 데 방해가 된다. 이 문제를 해결하기 위해서는 인터페이스를 사용하면 된다는 것이다 !
따라서, 캐릭터 클래스가 칼과 활 클래스에 직접 의존하지 않고, 인터페이스나 추상화를 사용하여 의존성을 역전시키면 나중에 무기 클래스를 수정해도 캐릭터 클래스를 수정할 필요가 없어지며, 코드를 더 유연하고 확장 가능하게 만들 수 있다!
// 인터페이스 정의
public interface IWeapon {
void Attack();
}
public interface IItem {
void Use();
}
// 추상 클래스 정의
public abstract class Character {
protected IWeapon weapon;
public abstract void Move();
public void Attack() {
weapon.Attack();
}
}
// 구현 클래스 정의
public class Sword : IWeapon {
public void Attack() {
Console.WriteLine("Swinging sword!");
}
}
public class Bow : IWeapon {
public void Attack() {
Console.WriteLine("Firing gun!");
}
}
'게임공부 > 디자인패턴' 카테고리의 다른 글
[디자인패턴] Flyweight Pattern 경량패턴 (0) | 2023.05.18 |
---|---|
[디자인 패턴] 객체 지향 디자인 패턴 1 (1) | 2023.05.15 |
[디자인 패턴] 디자인 패턴 개요 (0) | 2023.05.12 |
Design Patterns - 싱글톤(Singleton) (0) | 2023.04.28 |