본문 바로가기

유니티/Pattern

[ Pattern ]SOLID 원칙

[ Gof와 SOLID 원칙의 소개 ]

1 . GOF

[ GOF ]

- 디자인 패턴을 만들어낸 그룹

- 디자인 패턴은 SOLID 원칙을 지키기 위해 사용된다 .


2 . SOLID 원칙

[ SOLID 원칙 ]

- SOLID 원칙은 소프트웨어 설계를 더 이해하기 쉽고, 유연하며 유지보수 가능하게 만드는 설계 원칙의 집합 .

- 객체 지향 프로그래밍에서 유용하다 . 


[ SOLID 원칙의 종류 ]

  • Single Responsibility ( 단일 책임 원칙 )
  • Open - Closed ( 개방 폐쇄 원칙 )
  • Liskov substitution ( 리스코프치환 원칙 )
  • Interface segregation ( 인터페이스 분리 원칙 )
  • Dependency Inversion ( 의존 역전 원칙)

[ SOLID 원칙 ]

1 . Single Responsibility ( 단일 책임의 원칙 )

[ 설명 ]

- 모든 클래스는 하나의 책임만 가진다 .

- 클래스는 그 책임을 완전히 캡슐화 해야 한다 .

- 클래스가 제공하는 모든 기능은 이 책임에 부합해야 한다 .

- 클래스 변경의 이유도 한가지여야 한다 .


[ 장점 ]

- 가독성 : 짧은 클래스가 읽기 쉽다 .

- 확장성 : 작은 클래스로부터 상속이 쉽다 .

- 재사용성 : 부분에서 재사용할 수 있도록 작고 모듈식으로 설계


[ 예시 ]

// 잘못된 예: Player 클래스가 플레이어 입력과 이동을 모두 처리함.
public class Player : MonoBehaviour
{
    void Update()
    {
        HandleInput();
        Move();
    }

    void HandleInput()
    {
        // 입력 처리 로직
    }

    void Move()
    {
        // 이동 로직
    }
}

// 올바른 예: 책임을 두 개의 다른 클래스로 분리함.
public class Player : MonoBehaviour
{
    private PlayerInput playerInput;
    private PlayerMovement playerMovement;

    void Awake()
    {
        playerInput = new PlayerInput();
        playerMovement = new PlayerMovement();
    }

    void Update()
    {
        playerInput.HandleInput();
        playerMovement.Move();
    }
}

public class PlayerInput
{
    public void HandleInput()
    {
        // 입력 처리 로직
    }
}

public class PlayerMovement
{
    public void Move()
    {
        // 이동 로직
    }
}

 


 

2 . Open - Close ( 개방폐쇄 원칙 )

[ 설명 ]

- 확장 ( 기능의 추가 ) 에 대해서는 개방적(open)이고, 수정에 대해서는 폐쇄적(closed)이어야 한다

- 추상화가 좋은 예시이다 .

 

  • 확장에 대해 개방적이다 .
    • 모듈의 동작을 확장 할 수 있음을 의미한다 .
    • 요구 사항이 변경될때 , 새로운 동작을 추가해 모듈을 확장한다 .
    • 즉 , 모듈이 하는 일을 변경 할 수 있다 .
  • 수정에 대해 폐쇄적이다 .
    • 새로운 변경사항이 발생했을 때 객체를 직접 수정하지 않고 적용해야 한다 .
    • 모듈의 라이브러리가 그 예시이다 .

[ 장점 ]

- 다형성과 확장을 가능케 하는 객체지향의 장점을 극대화 할 수 있다 .


[ 예시 ]

// 잘못된 예: 새로운 동작을 추가하기 위해 Enemy 클래스를 수정함.
public class Enemy : MonoBehaviour
{
    public void TakeDamage(int damage)
    {
        // 피해 로직
    }

    public void Move()
    {
        // 이동 로직
    }

    public void SpecialAttack()
    {
        // 특수 공격 로직
    }
}

// 올바른 예: Enemy 클래스를 수정하지 않고 상속을 사용하여 확장함.
public class Enemy : MonoBehaviour
{
    public virtual void TakeDamage(int damage)
    {
        // 피해 로직
    }

    public virtual void Move()
    {
        // 이동 로직
    }
}

public class SpecialEnemy : Enemy
{
    public override void Move()
    {
        base.Move();
        // 특수 적의 추가 이동 로직
    }

    public void SpecialAttack()
    {
        // 특수 공격 로직
    }
}

3 . LisKov Substitution ( 리스코프 치환 원칙)

[ 설명 ]

- 파생 클래스는 기본 클래스를 대체 할 수 있어야 한다 .

- 즉 , 클래스의 방향성이 유지되어야 한다는 것 .

- 하위 클래스를 강력하고 유연하게 만드는 원칙


[ 장점 ]

- 코드 재사용성, 유지 관리성 및 유연성을 향상하여 더 나은 소프트웨어 설계를 촉진한다 .

- 기능을 인터페이스로 나누면 수월할것이다 .


[ 예시 ]

// 잘못된 예: 파생 클래스가 기본 클래스의 예상 동작을 변경함.
public class Weapon
{
    public virtual void Fire()
    {
        // 발사 로직
    }
}

public class LaserGun : Weapon
{
    public override void Fire()
    {
        // 레이저 전용 발사 로직
    }
}

public class Player : MonoBehaviour
{
    private Weapon weapon;

    void Start()
    {
        weapon = new LaserGun();
    }

    void Update()
    {
        weapon.Fire();
    }
}

// 올바른 예: 파생 클래스가 기본 클래스의 계약을 준수함.
public abstract class Weapon
{
    public abstract void Fire();
}

public class LaserGun : Weapon
{
    public override void Fire()
    {
        // 레이저 전용 발사 로직
    }
}

public class Player : MonoBehaviour
{
    private Weapon weapon;

    void Start()
    {
        weapon = new LaserGun();
    }

    void Update()
    {
        weapon.Fire();
    }
}

 


4 . Interface Segregation ( 인터페이스 분 원칙)

[ 설명 ]

- 인터페이스를 잘게 분리하여 , 클라이언트의 목적과 용도에 적합한 인터페이스 만을 제공하는 것 .

- 즉, 큰 덩어리의 인터페이스를 구체적이고 작은 단위로 분리한다 .

- 클라이언트는 자신이 이용하지 않는 메서드에 의존하지 않아야 한다 .


[ 장점 ]

- 시스템 내부의 의존성을 약화하고 , 유연성을 강화한다 .


[ 예시 ]

// 잘못된 예: 큰 인터페이스가 클래스에 필요하지 않은 메서드를 구현하도록 강요함.
public interface ICharacter
{
    void Attack();
    void Defend();
    void Heal();
}

public class Warrior : MonoBehaviour, ICharacter
{
    public void Attack()
    {
        // 공격 로직
    }

    public void Defend()
    {
        // 방어 로직
    }

    public void Heal()
    {
        // 힐 로직 (Warrior에게는 필요하지 않음)
    }
}

// 올바른 예: 특정 기능을 위한 작은 인터페이스로 분리함.
public interface IAttack
{
    void Attack();
}

public interface IDefend
{
    void Defend();
}

public interface IHeal
{
    void Heal();
}

public class Warrior : MonoBehaviour, IAttack, IDefend
{
    public void Attack()
    {
        // 공격 로직
    }

    public void Defend()
    {
        // 방어 로직
    }
}

public class Medic : MonoBehaviour, IHeal
{
    public void Heal()
    {
        // 힐 로직
    }
}

5 . Dependency Inversion ( 의존 역전 원칙)

[ 설명 ]

- 상위 모듈은 하위 모듈의 것을 직접 가져오는게 아니라 추상화에 의존해야 한다 .

- 즉 , 객체에서 어떠한 class를 참조해서 사용해야 한다면 , 직접 참조하는게 아닌 대상의 상위 요소 (추상 클래스 / 인터페이스)로 참조해야 한다 .

- 추상화는 세부사항에 의존해서는 안되고 , 세부사항이 추상화에 의존해야 한다 .

- 클래스가 다른 클래스와 관계가 있으면 안된다 .


[ 장점 ]

-  디커플링으로 서로간의 의존성을 줄인다 .


[ 예시 ]

// 잘못된 예: 고수준 모듈이 저수준 모듈에 직접 의존함.
public class GameManager : MonoBehaviour
{
    private Database database;

    void Start()
    {
        database = new Database();
        database.Save();
    }
}

public class Database
{
    public void Save()
    {
        // 저장 로직
    }
}

// 올바른 예: 고수준 모듈과 저수준 모듈이 모두 추상화에 의존함.
public interface IDatabase
{
    void Save();
}

public class Database : IDatabase
{
    public void Save()
    {
        // 저장 로직
    }
}

public class GameManager : MonoBehaviour
{
    private IDatabase database;

    void Start()
    {
        database = new Database();
        database.Save();
    }
}

 

출처

https://inpa.tistory.com/entry/OOP-%F0%9F%92%A0-%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5-%EC%84%A4%EA%B3%84%EC%9D%98-5%EA%B0%80%EC%A7%80-%EC%9B%90%EC%B9%99-SOLID?category=967430

 

💠 객체 지향 설계의 5가지 원칙 - S.O.L.I.D

객체 지향 설계의 5원칙 S.O.L.I.D 모든 코드에서 LSP를 지키기에는 어려움. 리스코프 치환 원칙에 따르면 자식 클래스의 인스턴스가 부모 클래스의 인스턴스를 대신하더라도 의도에 맞게 작동되어

inpa.tistory.com