void Player::Update() {
if (health <= 0) {
// 죽었을 때 다양한 처리들을 다 제거하고
// isAlive = false;
// animation.Play("Death");
// sound.Play("DeathSFX");
// gameManager.NotifyPlayerDied(this);
// 하나의 함수로 묶기
HandleDeath();
}
}
수식으로만 표현하지말고, 변수로 수식의 의도 알려주기
나중에 본인도 다시봤을 때, 이해하기 쉬움
디버깅할때도 중간값 보기 쉬움
// 수식을 보고 이해해야 함
float damage = baseDamage * (1 + criticalChance * 2.0f);
// 변수로 수식의 의도를 정확히 알림
float critMultiplier = 1 + criticalChance * 2.0f;
float damage = baseDamage * critMultiplier;
class Player {
FString Name;
FString Address;
FString Email; };
class ContactInfo {
FString Address;
FString Email; };
class Player {
FString Name;
ContactInfo Contact; };
추출을 하는 경우는 보통 복잡하거나 의도를 명확히 알리고 싶어서 함
이미 의미를 충분히 알 수 있는 경우 추출하지 않는 것이 옳다
bool Player::IsDead() {
return health <= 0;
}
void Player::Update() {
if (IsDead())
HandleDeath();
}
void Player::Update() {
if (health <= 0)
HandleDeath();
}
class Enemy
{
public:
float X, Y; // Position이라는 Class로 굳이 추출하지 않음
};
| 세분화가 적합한 상황 | 통합이 적합한 상황 |
|---|---|
| 코드가 복잡하고 이해하기 어려울 때 | 추상화가 오히려 복잡성을 증가시킬 때 |
| 재사용 가능성이 있을 때 | 간단한 로직이 불필요하게 분리되어 있을 때 |
| 변경 가능성이 높은 부분 | 사용되는 곳이 한 곳뿐인 단순한 코드 |
| 무엇과 어떻게를 분리할 필요가 있을 때 | 함수/변수 이름이 실제 로직에 가치를 더하지 않을 때 |
냉장고에서 음식 꺼내 먹는 것까지 한 번에 하더라도, 중간에 양념을 치고 싶을 때 수정하기 쉽지 않음
따라서 한 번에 모든 단계를 하지말고, 단계별로 세분화해서 구현
void LoadAndDisplayItem() {
// 한 번에 하는 경우
Item item = LoadItemFromDisk("item.json");
RenderItemOnScreen(item);
}
// 2단계로 나누어 처리
Item item = LoadItem();
DisplayItem(item)
하나의 반복문에선 하나의 역할만 하도록
반복문 2번 더 돌린다고 성능에 영향 별로 없음. 오히려, 의도를 파악하는 것이 더 중요한 문제
// 반복문 하나에 서로 관련 없는 작업 한 번에 하지 말기
// for (const Item& item : Inventory) {
// item.CalculateWeight();
// item.ApplyDurabilityDecay();
// }
// 차라리 반복문을 한 번 더 하는게 낫다
for (const Item& item : Inventory) {
item.CalculateWeight();
}
for (const Item& item : Inventory) {
item.ApplyDurabilityDecay();
}
// 타이머 관련 함수이므로
// void StartTimer();
// void StopTimer();
// float GetElapsedTime();
// 하나의 클래스로 묶는다
class Timer {
public:
void Start();
void Stop();
float GetElapsed() const;
private:
float StartTime;
float EndTime;
};
| 세분화가 적합한 상황 | 통합이 적합한 상황 |
|---|---|
| 책임이 명확히 분리될 때 | 강한 응집력이 필요할 때 |
| 다른 용도로 재사용 가능할 때 | 관련 데이터와 동작이 함께 있는 것이 자연스러울 때 |
| 독립적으로 테스트하고 싶을 때 | 공유 상태에 대한 관리가 필요할 때 |
| 복잡한 단계를 나누어 명확히 하고 싶을 때 | 여러 작은 함수들이 항상 함께 사용될 때 |
player->SetHealth(100); // 체력은 중요한 data
class InventorySystem {
public:
void AddItem(Item* NewItem) {
if (!NewItem || IsFull()) return;
Items.Add(NewItem);
}
const TArray<Item*>& GetItems() const { return Items; }
};
// 너무 긺
// FString name = gameManager->GetPlayerManager()->GetMainPlayer()->GetName();
FString name = gameManager->GetMainPlayerName();
FString GameManager::GetMainPlayerName() const
{
return PlayerManager->GetMainPlayer()->GetName();
}
| 간접 접근이 적합한 상황 | 직접 접근이 적합한 상황 |
|---|---|
| 데이터 검증이나 부가 처리가 필요할 때 | 과도한 래퍼가 복잡성만 증가시킬 때 |
| 변경 추적이 필요할 때 | 성능이 중요한 핫스팟일 때 |
| 향후 구현 변경 가능성이 있을 때 | 단순한 데이터 구조에서 |
| 중복된 접근 로직이 여러 곳에 있을 때 | 위임 체인이 너무 길어질 때 |
조건문에 여러 조건들이 있으면 의도를 이해하기 힘듦
여러 조건문을 하나의 함수로 묶어 의도를 명확히 함
// 문 여는 조건인데 너무 많아 이해 힘듦
// if (player.HasKey() && player.IsAlive() && !player.IsStunned())
// OpenDoor();
// 하나의 함수로 묶기
if (player.CanOpenDoor())
OpenDoor();
// 둘 다 비슷한 동작
// if (player.IsDead())
// return false;
// if (player.IsDisconnected())
// return false;
// 하나로 묶어주기
if (!player.CanParticipate())
return false;
// 타입마다 조건문 넣는 것도 힘듦
float Enemy::GetDamage() {
if (Type == "Orc") return Strength * 1.2f;
if (Type == "Goblin") return Strength * 0.8f;
return Strength;
}
// 다형성으로 쉽게 해결
class Enemy { virtual float GetDamage() const = 0; };
class Orc : public Enemy {
float GetDamage() const override { return Strength * 1.2f; }
};
현재 캐릭터 있는지 없는지, 이름 있는지 없는지 등 항상 nullptr을 확인하는 것은 비효율적
심지어 이름 없을 때 "None" 이름 반환하게 했는데 "Guest"로 바꾸라고 하면 일일이 다 수정해야함
if (player)
ShowPlayerName(player->Name);
else
ShowPlayerName("Guest");
class NullPlayer : public Player {
public:
FString GetName() const override { return "Guest"; }
};
Player* GetPlayerOrNull(int id)
{
Player* player = FindPlayer(id);
if (!player)
{
static NullPlayer nullPlayer;
return &nullPlayer;
}
return player;
}
// 이제 어디서든 null 걱정 없이 사용 가능
Player* player = GetPlayerOrNull(playerId);
ShowPlayerName(player->GetName()); // 항상 안전!
| 조건문이 적합한 상황 | 다형성이 적합한 상황 |
|---|---|
| 단순한 분기 로직일 때 | 타입별 동작 차이가 뚜렷할 때 |
| 일회성이거나 지역적인 결정일 때 | 타입 추가가 자주 발생할 때 |
| 성능이 매우 중요한 곳일 때 | 타입별 코드가 반복적으로 나타날 때 |
| 타입 배열이 고정적일 때 | 동작이 확장될 가능성이 높을 때 |
class Enemy {
public:
virtual void Die() { /* 공통 죽음 처리 */ }
int Health; // 공통 필드
};
// 이후 Orc, Goblin 자식들이 상속 받음
// 다른 객체여도 공통 부분이 있음
class Player {
FString Name;
FVector Position;
};
class NPC {
FString Name;
FVector Position;
};
// 하나의 슈퍼클래스로 묶기
class ActorBase
{
FString Name;
FVector Position;
};
class Player : public ActorBase {};
class NPC : public ActorBase {};
class Enemy {
virtual FString GetSound() const = 0;
};
class Orc : public Enemy {
FString GetSound() const override { return "Roar"; }
};
class SoundBehavior
{
public:
virtual FString GetSound() const = 0;
};
class Enemy
{
TUniquePtr<SoundBehavior> Sound;
FString MakeSound() const { return Sound->GetSound(); }
};
따라서, 컴포넌트로 소리내는 기능을 위임시키고 런타임 도중 변경 가능하도록 함
"has-a" 관계이면, 위임시키기
상속은 가족관계, 위임은 계약관계
| 상속이 적합한 상황 | 위임이 적합한 상황 |
|---|---|
| 명확한 "is-a" 관계가 있을 때 | "has-a" 관계나 행동 공유일 때 |
| 공통 기능이 많고 타입 계층이 필요할 때 | 런타임에 동작을 변경해야 할 때 |
| 다형성을 활용한 확장이 자연스러울 때 | 다중 상속과 같은 효과가 필요할 때 |
| 코드 재사용이 수직적일 때 | 기존 클래스 변경 없이 기능 확장이 필요할 때 |