LSP를 위반해도 괜찮습니까?


10

이 질문 에 후속 조치를 취하고 있지만 코드에서 원칙으로 초점을 전환하고 있습니다.

의 나의 이해에서 Liskov 대체 원칙 방법은 내 기본 클래스에있는 어떤 (LSP), 그들은 내 서브 클래스에서 구현되어야하며,에 따라 페이지는 기본 클래스의 메소드를 오버라이드 (override)하는 경우 그것은 아무것도하지 않는다 또는를 던졌습니다 예외, 당신은 원칙을 위반합니다.

이제 내 문제는 다음과 같이 요약 될 수있다 : 나는 추상이 Weapon class, 2 개 개의 클래스를, Sword하고 Reloadable. 이라는 Reloadable특정을 포함하는 경우 다운 캐스트하여 액세스해야 하며 이상적으로는 피하고 싶습니다.methodReload()method

그런 다음을 사용하는 것을 생각했습니다 Strategy Pattern. 이런 식으로 각 무기는 자신이 수행 할 수있는 행동 만 알 수있었습니다. 예를 들어, Reloadable무기는 분명히 재 장전 Sword할 수는 있지만을 (를) 인식하지 못하고 심지어조차도 알지 못합니다 Reload class/method. 스택 오버플로 게시물에서 언급했듯이 다운 캐스트 할 필요가 없으며 List<Weapon>컬렉션을 유지할 수 있습니다 .

또 다른 포럼 , 첫 번째 대답은 할 수 있도록 제안 Sword을 인식하는 Reload단지 아무것도하지 않습니다. 위와 연결된 Stack Overflow 페이지에서도 이와 동일한 답변이 제공되었습니다.

이유를 완전히 이해하지 못합니다. 왜 원칙을 위반하고 Sword가 인식 Reload하고 비워 두는가? 내 스택 오버플로 게시물에서 말했듯이 SP는 내 문제를 거의 해결했습니다.

실행 가능한 솔루션이 아닌 이유는 무엇입니까?

public final Weapon{

    private final String name;
    private final int damage;
    private final List<AttackStrategy> validactions;
    private final List<Actions> standardActions;

    private Weapon(String name, int damage, List<AttackStrategy> standardActions, List<Actions> attacks)
    {
        this.name = name;
        this.damage = damage;
        standardActions = new ArrayList<Actions>(standardActions);
        validAttacks = new ArrayList<AttackStrategy>(validActions);
    }

    public void standardAction(String action){} // -- Can call reload or aim here.  

    public int attack(String action){} // - Call any actions that are attacks. 

    public static Weapon Sword(String name, damage, List<AttackStrategy> standardActions, List<Actions> attacks){
        return new Weapon(name, damage,standardActions, attacks) ;
    }

}

공격 인터페이스 및 구현 :

public interface AttackStrategy{
    void attack(Enemy enemy);
}

public class Shoot implements AttackStrategy {
    public void attack(Enemy enemy){
        //code to shoot
    }
}

public class Strike implements AttackStrategy {
    public void attack(Enemy enemy){
        //code to strike
    }
}

2
할 수 있습니다 class Weapon { bool supportsReload(); void reload(); }. 클라이언트는 다시로드하기 전에 지원되는지 테스트합니다. reloadiff를 던지기 위해 계약 상 정의됩니다 !supportsReload(). 그것은 LSP를 준수합니다. iff 구동 클래스는 방금 설명한 프로토콜을 준수합니다.
usr

3
당신은 떠나 여부 reload()빈 여부를 standardActions다시로드 작업을 포함하지 않는 것은 단지 다른 메커니즘입니다. 근본적인 차이는 없습니다. 둘 다 할 수 있습니다. => 귀하의 솔루션 실행 가능합니다 (귀하의 질문이었습니다) .; 무기에 빈 기본 구현이 포함되어 있으면 검에 재 장전을 알 필요가 없습니다.
usr

27
이 문제를 해결하기위한 다양한 기술과 관련된 다양한 문제를 탐구하는 일련의 기사를 작성했습니다. 결론 : 언어 유형 시스템에서 게임 규칙을 포착하려고 시도하지 마십시오 . 유형 시스템 수준이 아니라 게임 논리 수준에서 규칙을 나타내고 적용하는 객체 에서 게임 규칙을 캡처합니다 . 사용하는 모든 유형 시스템이 게임 로직을 나타낼 정도로 정교하다고 믿을 이유는 없습니다. ericlippert.com/2015/04/27/wizards-and-warriors-part-one
Eric Lippert

2
@EricLippert-링크 주셔서 감사합니다. 나는이 블로그를 여러 번 보았지만 몇 가지 요점을 이해하지 못했지만 귀하의 잘못이 아닙니다. OOP를 스스로 배우고 있으며 SOLID 교장을 만났습니다. 처음으로 블로그를 방문했을 때 전혀 이해가되지 않았지만 조금 더 배우고 블로그를 다시 읽고 천천히 말한 내용의 일부를 이해하기 시작했습니다. 어느 날 나는 그 시리즈의 모든 것을 완전히 이해할 것입니다. 희망 : D

6
@SR "아무것도하지 않거나 예외가 발생하면 위반 한 것입니다."-해당 기사의 메시지를 잘못 읽은 것 같습니다. 문제는 직접 setAltitude가 아무 것도하지 않았다는 것이 아니고, "조류가 정해진 고도에 그려 질 것"이라는 사후 조건을 충족시키지 못했다는 것입니다. "재 장전"의 사후 조건을 "충분한 탄약을 사용할 수있는 경우 무기는 다시 공격 할 수 있습니다"로 정의한 경우, 탄약을 사용하지 않는 무기에 대해 완벽하게 유효한 구현은 없습니다.
Sebastian Redl

답변:


16

LSP는 서브 타이핑 및 다형성에 관심이 있습니다. 모든 코드가 실제로 이러한 기능을 사용하는 것은 아니며,이 경우 LSP는 관련이 없습니다. 하위 유형의 경우가 아닌 상속 언어 구문의 일반적인 두 가지 사용 사례는 다음과 같습니다.

  • 상속은 기본 클래스의 구현을 상속하는 데 사용되지만 인터페이스는 상속하지 않습니다. 거의 모든 경우에 구성이 선호되어야합니다. Java와 같은 언어는 구현과 인터페이스의 상속을 분리 할 수 ​​없지만 C ++에는 private상속이 있습니다.

  • 합계 유형 / 연합을 모델링하는 데 사용되는 상속입니다 (예 : a BaseCaseA또는) CaseB. 기본 유형은 관련 인터페이스를 선언하지 않습니다. 인스턴스를 사용하려면 올바른 콘크리트 유형으로 캐스트해야합니다. 캐스팅은 안전하게 수행 할 수 있으며 문제는 아닙니다. 불행히도 많은 OOP 언어는 기본 클래스 하위 유형을 원하는 하위 유형으로 제한 할 수 없습니다. 외부 코드가을 만들 수있는 경우 a 만 또는 잘못 될 수 CaseC있다고 가정하는 코드 입니다. 스칼라는 개념으로 안전하게 할 수 있습니다 . Java에서 이것은 개인 생성자를 가진 추상 클래스이고 중첩 된 정적 클래스가 기본에서 상속 될 때 모델링 될 수 있습니다 .BaseCaseACaseBcase classBase

실제 객체의 개념적 계층 구조와 같은 일부 개념은 객체 지향 모델에 매우 잘못 매핑됩니다. “총은 무기이고, 칼은 무기이므로, 나는 Weapon기본 계급이 Gun있고 Sword상속받습니다”라는 오해의 소지가 있습니다. 실제 단어 – 관계는 모델에서 그러한 관계를 암시하지 않습니다. 하나의 관련 문제는 객체가 여러 개념적 계층에 속하거나 런타임 동안 계층 구조 소속을 변경할 수 있다는 점입니다. 상속은 일반적으로 클래스 별이 아니고 디자인 타임이 아닌 디자인 타임에 정의되므로 대부분의 언어는 모델링 할 수 없습니다.

OOP 모델을 설계 할 때 계층 구조 나 한 클래스가 다른 클래스를 어떻게 "확장"하는지 생각해서는 안됩니다. 기본 클래스는 여러 클래스 의 공통 부분을 제외 할 있는 장소가 아닙니다 . 대신, 객체의 사용 방법, 즉 이러한 객체의 사용자에게 어떤 종류의 동작이 필요한지 생각해보십시오.

여기서 사용자는 attack()무기를 사용해야 할 수도 reload()있습니다. 타입 계층 구조를 만들려면,이 두 가지 방법 모두 기본 타입이어야하지만, 재 장전 할 수없는 무기는 그 방법을 무시하고 호출 될 때 아무 것도 수행하지 않을 수 있습니다. 따라서 기본 클래스는 공통 부분을 포함하지 않지만 모든 서브 클래스의 결합 된 인터페이스를 포함합니다. 서브 클래스는 인터페이스가 다르지 않고이 인터페이스를 구현할 때만 다릅니다.

계층을 만들 필요는 없습니다. 두 가지 유형 Gun이며 Sword완전히 관련이 없을 수 있습니다. 반면, Gunfire()reload()a는 Sword만 할 수있다 strike(). 이러한 오브젝트를 다형성으로 관리해야하는 경우 어댑터 패턴을 사용하여 관련 측면을 캡처 할 수 있습니다. Java 8에서는 기능 인터페이스 및 람다 / 메서드 참조를 사용하여 편리하게 사용할 수 있습니다. 예를 들면 당신은있을 수 있습니다 Attack사용자가 제공하는 전략 myGun::fire또는 () -> mySword.strike().

마지막으로 하위 클래스를 전혀 피하는 것이 합리적이지만 단일 유형을 통해 모든 객체를 모델링하는 것이 좋습니다. 많은 게임 개체가 어떤 계층 구조에도 잘 맞지 않고 다양한 기능을 가질 수 있기 때문에 이는 특히 게임과 관련이 있습니다. 예를 들어, 롤 플레잉 게임은 퀘스트 아이템, 장비가 장착 된 상태에서 능력치를 +2로 강화하고,받는 피해를 무시할 확률이 20 %이며 근접 공격을 제공합니다. 또는 * 마법 *이기 때문에 재 장전 가능 검일 수도 있습니다. 이야기가 필요한 것을 누가 알겠습니까?

해당 혼란에 대한 클래스 계층 구조를 파악하는 대신 다양한 기능을위한 슬롯을 제공하는 클래스를 사용하는 것이 좋습니다. 이 슬롯은 런타임에 변경할 수 있습니다. 각 슬롯은 OnDamageReceived또는 과 같은 전략 / 콜백 Attack입니다. 무기, 우리는 할 수 있습니다 MeleeAttack, RangedAttackReload슬롯. 이 슬롯은 비어있을 수 있으며이 경우 개체가이 기능을 제공하지 않습니다. 그런 다음 슬롯을 조건부로 호출합니다 if (item.attack != null) item.attack.perform()..


어떤면에서 SP와 같은 종류. 왜 슬롯을 비워야합니까? 사전에 동작이 포함되어 있지 않으면 아무 것도하지 마십시오

@SR 슬롯이 비어 있는지 또는 존재하지 않는지는 실제로 중요하지 않으며 이러한 슬롯을 구현하는 데 사용되는 메커니즘에 따라 다릅니다. 나는 슬롯이 인스턴스 필드이고 항상 존재하는 상당히 정적 인 언어 (예 : Java의 일반 클래스 디자인)를 가정 하여이 답변을 썼습니다. 슬롯이 사전의 항목 (예 : Java의 HashMap 또는 일반 Python 객체 사용) 인보다 동적 인 모델을 선택하면 슬롯이 없어도됩니다. 보다 역동적 인 접근 방식은 많은 유형의 안전성을 제공하므로 일반적으로 바람직하지 않습니다.
amon

나는 실제 물체가 잘 모델링되지 않는다는 데 동의합니다. 게시물을 이해하면 전략 패턴을 사용할 수 있다고 말하는가?

2
@SR 예, 어떤 형태의 전략 패턴은 합리적인 접근법 일 것입니다. 관련 Type Object Pattern : gameprogrammingpatterns.com/type-object.html
amon

3

전략을 세우는 attack것만으로는 충분하지 않기 때문입니다. 물론, 아이템이 할 수있는 행동을 추상화 할 수 있지만 무기의 범위를 알아야 할 때 어떤 일이 발생합니까? 아니면 탄약 용량? 아니면 어떤 종류의 탄약이 필요한가요? 당신은 그것을 얻기 위해 다운 캐스팅으로 돌아 왔습니다. 유연성 수준이 높으면 UI를 구현하기가 다소 어려워집니다. 모든 기능을 처리하려면 비슷한 전략 패턴이 필요하기 때문입니다.

나는 당신의 다른 질문에 대한 답변에 특별히 동의하지 않습니다. sword상속 을 받는 weapon것은 끔찍하고 순진한 OO로, 항상 코드에 흩어져있는 방법이나 유형 검사가 발생하지 않습니다.

그러나 문제의 근원에서 해결책이 잘못되었습니다 . 두 가지 솔루션을 모두 사용하여 재미있는 재미있는 게임을 만들 수 있습니다. 각 솔루션에는 선택한 솔루션과 마찬가지로 자체 장단점이 있습니다.


나는 이것이 완벽하다고 생각합니다. SP를 사용할 수는 있지만 트레이드 오프이므로이를 알고 있어야합니다. 내가 생각한 것에 대한 나의 편집을 참조하십시오.

1
Fwiw : 칼에는 탄약이 무한합니다. 영원히 읽을 필요없이 계속 사용할 수 있습니다. reload는 처음부터 무한히 사용하기 때문에 아무 것도하지 않습니다. 1 / 근접 범위 : 근접 무기입니다. 근접과 원거리 모두에 적합한 방식으로 모든 스탯 / 액션에 대해 생각하는 것은 불가능하지 않습니다. 그래도 나이가 들어감에 따라 인터페이스, 경쟁 및 Weapon칼과 총 인스턴스가 있는 단일 클래스 를 사용하는 이름이 무엇이든간에 상속을 사용하지 않습니다 .
CAD97

운명 2의 검은 어떤 이유로 탄약을 사용합니다!

@ CAD97-이 문제와 관련하여 본 유형의 사고입니다. 탄약이 무한한 검을 가져다가 다시 장전하지 마십시오. 이것은 단지 문제를 해결하거나 숨 깁니다. 수류탄을 도입하면 어떻게 되나요? 수류탄에는 탄약이나 쏘기가 없으며 그러한 방법을 알고 있으면 안됩니다.

1
CAD97을 사용하고 있습니다. 그리고 WeaponBuilder전략의 무기를 구성하여 검과 총을 만들 수있는 을 만들 것 입니다.
Chris Wohlert

3

물론 실행 가능한 솔루션입니다. 아주 나쁜 생각입니다.

기본 클래스에 다시로드하는이 단일 인스턴스가 있으면 문제가 아닙니다. 문제는 또한 "스윙", "슈트" "패리", "노크", "광택", "분해", "날카롭게"및 "클럽의 뾰족한 끝의 못을 교체"해야한다는 것입니다. 기본 클래스의 메소드.

LSP의 요점은 최상위 알고리즘이 작동하고 이해해야한다는 것입니다. 따라서 다음과 같은 코드가 있으면

if (isEquipped(weapon)) {
   reload();
}

이제 구현되지 않은 예외가 발생하고 프로그램이 중단되면 매우 나쁜 생각입니다.

코드가 다음과 같다면

if (canReload(weapon)) {
   reload();
}
else if (canSharpen(weapon)) {
  sharpen();
}
else if (canPollish(weapon)) {
  polish();
}

그러면 코드는 추상적 인 '무기'아이디어와 관련이없는 매우 특정한 속성으로 복잡해질 수 있습니다.

그러나 1 인칭 슈팅 게임을 구현하고 있으며 특정 무기에서 칼 하나를 제외하고 모든 무기를 쏘거나 재 장전 할 수 있다면 나이프 재 장전은 예외와 확률이므로 아무것도하지 않는 것이 좋습니다. 특정 속성으로 기본 클래스를 복잡하게 만드는 것이 낮습니다.

업데이트 : 추상 사례 / 용어에 대해 생각해보십시오. 예를 들어, 모든 무기에는 "준비"동작이있어 총을 재장 전하고 칼을 칼집으로 만들 수 있습니다.


무기에 대한 조치를 보유하는 내부 무기 사전이 있고 사용자가 "재로드"를 전달하면 사전 (예 : weaponActions.containsKey (action))을 확인하고 연관된 오브젝트를 가져 와서 수행합니다. 그것. 여러 if 문이있는 무기 클래스 대신

위의 편집을 참조하십시오. SP를 사용할 때이 점을 염두에 두었습니다.

0

기본 클래스의 인스턴스를 대체하려는 의도로 서브 클래스를 작성하지 않고 편리한 기능의 저장소로 기본 클래스를 사용하여 서브 클래스를 작성하는 경우에는 괜찮습니다.

이것이 좋은 아이디어인지 아닌지에 대해서는 논란의 여지가 있지만, 서브 클래스를 기본 클래스로 대체하지 않으면 작동하지 않는다는 사실은 문제가되지 않습니다. 문제가있을 수 있지만이 경우 LSP는 문제가되지 않습니다.


0

LSP는 호출 코드가 클래스 작동 방식에 대해 걱정하지 않아도되기 때문에 좋습니다.

예. BattleMech에 장착 된 모든 무기에 대해 Weapon.Attack ()을 호출 할 수 있으며 일부는 예외가 발생하여 게임이 중단 될 수 있습니다.

이제는 새로운 기능으로 기본 유형을 확장하려고합니다. Gun 클래스는 탄약을 추적하고 소진되면 발사를 중지 할 수 있기 때문에 Attack ()은 문제가되지 않습니다. 그러나 Reload ()는 새로운 것이며 무기가 아닙니다.

쉬운 해결책은 다운 캐스트하는 것입니다. 성능에 대해 걱정할 필요가 없다고 생각합니다. 매 프레임 마다하지 않을 것입니다.

또는 아키텍처를 재평가하고 초록에서 모든 무기를 재 장전 할 수 있으며 일부 무기는 재 장전 할 필요가 없다고 생각할 수 있습니다.

그런 다음 더 이상 총 클래스를 확장하거나 LSP를 위반하지 않습니다.

그러나 Gun.SafteyOn (), Sword.WipeOffBlood () 등 더 특별한 경우를 생각해야하고 모두 무기에 넣으면 매우 복잡한 일반 기본 클래스가 유지되므로 장기적으로 문제가됩니다. 변경해야합니다.

편집 : 전략 패턴이 나쁜 이유 (tm)

그것은 아니지만 설정, 성능 및 전체 코드를 고려하십시오.

총을 다시 장전 할 수 있다는 설정이 필요합니다. 무기를 인스턴스화 할 때 해당 구성을 읽고 모든 방법을 동적으로 추가하고 중복 이름이 없는지 확인해야합니다.

메소드를 호출 할 때 해당 액션 목록을 반복하고 호출 할 문자열을 확인해야합니다.

코드를 컴파일하고 "attack"대신 Weapon.Do ( "atack")를 호출하면 컴파일 오류가 발생하지 않습니다.

여러 가지 무작위 방법을 조합하여 수백 가지 무기를 가지고 있지만 일부 OO와 강력한 타이핑의 이점을 많이 잃어 버린다고하면 일부 문제에 적합한 솔루션 일 수 있습니다. 다운 캐스팅을 통해 실제로 아무것도 저장하지 않습니다.


나는 SP (위 편집 참조), 총을 가지고있는 것 모두를 처리 할 수 있다고 생각 SafteyOn()하고 Sword있는 것입니다 wipeOffBlood(). 각 무기는 다른 방법을 알지 못합니다 (그렇지 않아야합니다)

SP는 훌륭하지만 형식 안전성이없는 다운 캐스팅과 같습니다. 나는 좀 다른 질문에 대답 한 생각, 내가 업데이트 할 수 있도록
이완

2
전략 패턴 자체가 목록이나 사전에서 전략의 동적 조회를 의미하지는 않습니다. 즉 weapon.do("attack"), 형식 안전 weapon.attack.perform()은 전략 패턴의 예일 수 있습니다. 리플렉션을 사용하는 것도 같은 유형 안전하지만 구성 파일에서 오브젝트를 구성 할 때만 이름으로 전략을 찾는 것이 필요합니다.
amon

Ewan
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.