돌연변이 방법에 대한 별도의 인터페이스


11

나는 코드를 리팩토링하는 일을하고 있었고, 토끼 구멍에서 첫 단계를 밟은 것 같다. Java로 예제를 작성하고 있지만 무시할 수 있다고 생각합니다.

인터페이스를 Foo다음과 같이 정의했습니다.

public interface Foo {

    int getX();

    int getY();

    int getZ();
}

그리고 다음과 같은 구현

public final class DefaultFoo implements Foo {

    public DefaultFoo(int x, int y, int z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public int getZ() {
        return z;
    }

    private final int x;
    private final int y;
    private final int z;
}

또한 MutableFoo일치하는 뮤 테이터를 제공하는 인터페이스 가 있습니다

/**
 * This class extends Foo, because a 'write-only' instance should not
 * be possible and a bit counter-intuitive.
 */
public interface MutableFoo extends Foo {

    void setX(int newX);

    void setY(int newY);

    void setZ(int newZ);
}

존재하는 몇 가지 구현 MutableFoo이 있습니다 (아직 구현하지 않았습니다). 그들 중 하나는

public final class DefaultMutableFoo implements MutableFoo {

    /**
     * A DefaultMutableFoo is not conceptually constructed 
     * without all values being set.
     */
    public DefaultMutableFoo(int x, int y, int z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    public int getX() {
        return x;
    }

    public void setX(int newX) {
        this.x = newX;
    }

    public int getY() {
        return y;
    }

    public void setY(int newY) {
        this.y = newY;
    }

    public int getZ() {
        return z;
    }

    public void setZ(int newZ) {
        this.z = newZ;
    }

    private int x;
    private int y;
    private int z;
}

내가 이것을 나누는 이유는 각각이 사용될 가능성이 있기 때문입니다. 즉, 이러한 클래스를 사용하는 누군가가 변경 불가능한 인스턴스를 원하기 때문에 변경 불가능한 인스턴스를 원할 것입니다.

내가 가지고있는 주요 사용 사례 StatSet는 게임의 특정 전투 세부 정보 (히트 포인트, 공격, 방어)를 나타내는 인터페이스 입니다. 그러나 "유효"통계 또는 실제 통계는 절대 변경 될 수없는 기본 통계 및 훈련 된 통계의 결과이며 증가 할 수 있습니다. 이 두 가지는

/**
 * The EffectiveStats can never be modified independently of either the baseStats
 * or trained stats. As such, this StatSet must never provide mutators.
 */
public StatSet calculateEffectiveStats() {
    int effectiveHitpoints =
        baseStats.getHitpoints() + (trainedStats.getHitpoints() / 4);
    int effectiveAttack = 
        baseStats.getAttack() + (trainedStats.getAttack() / 4);
    int effectiveDefense = 
        baseStats.getDefense() + (trainedStats.getDefense() / 4);

    return StatSetFactory.createImmutableStatSet(effectiveHitpoints, effectiveAttack, effectiveDefense);
}

훈련 된 통계는 다음과 같은 전투마다 증가합니다

public void grantExperience() {
    int hitpointsReward = 0;
    int attackReward = 0;
    int defenseReward = 0;

    final StatSet enemyStats = enemy.getEffectiveStats();
    final StatSet currentStats = player.getEffectiveStats();
    if (enemyStats.getHitpoints() >= currentStats.getHitpoints()) {
        hitpointsReward++;
    }
    if (enemyStats.getAttack() >= currentStats.getAttack()) {
        attackReward++;
    }
    if (enemyStats.getDefense() >= currentStats.getDefense()) {
        defenseReward++;
    }

    final MutableStatSet trainedStats = player.getTrainedStats();
    trainedStats.increaseHitpoints(hitpointsReward);
    trainedStats.increaseAttack(attackReward);
    trainedStats.increaseDefense(defenseReward);
}

그러나 그들은 전투 직후 증가하지 않습니다. 특정 아이템을 사용하고 특정 전술을 사용하며 전장을 영리하게 사용하면 모두 다른 경험을 할 수 있습니다.

내 질문에 대해 :

  1. 접근 자와 뮤 테이터의 인터페이스를 별도의 인터페이스로 분할하는 이름이 있습니까?
  2. 그들은 똑같이 가능성이 사용되는 경우이 방법으로 분할 그들에게 '올바른'방법인가, 아니면 내가 대신 사용해야 다른, 더 허용 패턴이있다 (예. Foo foo = FooFactory.createImmutableFoo();반환 할 수있는 DefaultFoo또는 DefaultMutableFoo그러나 때문에 숨겨져 createImmutableFoo반환 Foo)?
  3. 인터페이스 계층 구조를 복잡하게하기 위해이 패턴을 사용하는 즉시 예측 가능한 단점이 있습니까?

내가 이런 식으로 디자인을 시작한 이유는 인터페이스의 모든 구현자가 가능한 가장 간단한 인터페이스를 준수하고 더 이상 아무것도 제공하지 않아야한다는 사고 방식이기 때문입니다. 인터페이스에 세터를 추가하면 이제 해당 부분과 상관없이 유효 통계 를 수정할 수 있습니다.

EffectiveStatSet기능을 확장하지 않기 때문에에 대한 새로운 클래스를 만드는 것은 의미가 없습니다. 우리는 구현을 바꿀 수 있고 EffectiveStatSet, 서로 다른 두 개의 합성물을 만들 수 StatSets있지만, 이것이 올바른 해결책이 아니라고 생각합니다.

public class EffectiveStatSet implements StatSet {

    public EffectiveStatSet(StatSet baseStats, StatSet trainedStats) {
        // ...
    }

    public int getHitpoints() {
        return baseStats.getHitpoints() + (trainedStats.getHitpoints() / 4);
    }
}


1
문제는 내 '불변'인 인터페이스에 액세스하더라도 객체가 변경 가능하다고 생각합니다. Player.TrainStats ()로 가변 객체를 만드는 것이 좋습니다
Ewan

@gnat 내가 그것에 대해 더 많이 배울 수있는 곳에 대한 언급이 있습니까? 편집 된 질문에 따라 어떻게 적용 할 수 있는지 확실하지 않습니다.
Zymus

@ gnat : 귀하의 링크 (및 2도 및 3도 링크)는 매우 유용하지만, 눈에 띄는 지혜로운 표현은 거의 도움이되지 않습니다. 오해와 멸시 만 불러옵니다.
rwong

답변:


6

나에게 그것은 당신이 문제를 찾는 해결책을 가지고있는 것 같습니다.

접근 자와 뮤 테이터의 인터페이스를 별도의 인터페이스로 분할하는 이름이 있습니까?

이것은 약간 도발적 일 수 있지만 실제로는 "과도한 디자인"또는 "과도한 복잡한 것"이라고 부릅니다. 동일한 클래스의 변경 가능하고 변경 불가능한 변형을 제공함으로써 동일한 문제에 대해 기능적으로 동등한 두 가지 솔루션을 제공하며, 이는 성능 동작, API 및 부작용에 대한 보안과 같은 비 기능적 측면에서만 다릅니다. 어느 쪽을 선호할지 결정하기를 두려워하거나 C #에서 C ++의 "const"기능을 구현하려고하기 때문일 것입니다. 나는 모든 경우의 99 %에서 사용자가 가변 또는 불변의 변형을 선택하면 큰 차이를 만들지 않을 것이라고 생각합니다. 그는 하나 또는 다른 문제를 해결할 수 있습니다. 따라서 "사용할 클래스의 가능성"

단 수천 명의 프로그래머가 사용할 새로운 프로그래밍 언어 또는 다목적 프레임 워크를 설계 할 때는 예외입니다. 그러면 범용 데이터 유형의 변경 불가능한 변경 가능한 변형을 제공 할 때 실제로 확장 성이 향상 될 수 있습니다. 그러나 이것은 수천 개의 서로 다른 사용 시나리오가있는 상황입니다. 아마도 당신이 직면 한 문제는 아닐 것입니다.

그들이 똑같이 사용될 가능성이 있거나 다른, 더 인정 된 패턴이있는 경우, 이러한 방식으로 '올바른'접근법을 나누고 있습니까?

"허용되는 패턴"을 KISS라고합니다. 단순하고 멍청하게 유지하십시오. 라이브러리의 특정 클래스 / 인터페이스에 대해 변경 가능성에 대해 결정하십시오. 예를 들어, "StatSet"에 12 가지 이상의 속성이 있고 대부분 개별적으로 변경되는 경우 변경 가능한 변형을 선호하고 수정하지 않아야 할 기본 통계는 수정하지 않습니다. Foo속성 X, Y, Z (3 차원 벡터)를 가진 클래스와 같은 경우에는 불변 변형을 선호합니다.

인터페이스 계층 구조를 복잡하게하기 위해이 패턴을 사용하는 즉시 예측 가능한 단점이 있습니까?

지나치게 복잡한 설계는 소프트웨어를 테스트하기 어렵고 유지 관리하기 어렵고 진화하기 어렵게 만듭니다.


1
"
KISS-

2

접근 자와 뮤 테이터의 인터페이스를 별도의 인터페이스로 분할하는 이름이 있습니까?

이 분리가 유용하고 내가 보지 못한 이점 을 제공하는 경우 이것에 대한 이름이있을 수 있습니다 . 분리가 혜택을 제공하지 않으면 다른 두 가지 질문은 의미가 없습니다.

두 개의 분리 된 인터페이스가 우리에게 도움이되거나이 문제가 학문적 문제 (YAGNI) 인 비즈니스 유스 케이스를 알려주시겠습니까?

나는 고객이 더 이상 기사를 변경할 수없는 주문이 될 수있는 가변 카트 (더 많은 기사를 넣을 수 있음)로 상점을 생각할 수 있습니다. 주문 상태는 여전히 변경 가능합니다.

내가 본 구현은 인터페이스를 분리하지 않습니다.

  • Java에는 ReadOnlyList 인터페이스가 없습니다.

ReadOnly 버전은 "WritableInterface"를 사용하며 쓰기 메소드를 사용하는 경우 예외를 발생시킵니다


기본 사용 사례를 설명하기 위해 OP를 수정했습니다.
Zymus

2
"자바에는 ReadOnlyList 인터페이스가 없습니다."-솔직히 말해야합니다. List <T>를 매개 변수로 받아들이는 코드 조각이 있으면 수정할 수없는 목록으로 작동하는지 쉽게 알 수 없습니다. 목록을 반환하는 코드는 안전하게 수정할 수 있는지 여부를 알 수있는 방법이 없습니다. 유일한 옵션은 잠재적으로 불완전하거나 부정확 한 문서에 의존하거나 경우에 대비하여 방어적인 사본을 만드는 것입니다. 읽기 전용 컬렉션 유형을 사용하면이 작업이 훨씬 간단 해집니다.
Jules

@ Jules : 실제로 Java 방식은 위의 모든 것 같습니다. 문서를 완벽하고 정확하게 작성하고 경우를 대비하여 방어 사본을 만드는 것입니다. 대기업 프로젝트로 확장 할 수 있습니다.
rwong 5

1

변경 가능한 콜렉션 인터페이스와 읽기 전용 계약 콜렉션 인터페이스의 분리는 인터페이스 분리 원리의 예입니다. 나는 각 원칙의 적용에 특별한 이름이 없다고 생각합니다.

"읽기 전용 계약"및 "컬렉션"이라는 단어가 있습니다.

읽기 전용 계약은 클래스 A가 클래스 B에 읽기 전용 액세스 권한을 부여하지만 기본 컬렉션 개체가 실제로 변경 불가능하다는 것을 의미하지는 않습니다. 불변은 에이전트에 상관없이 미래에는 절대 변하지 않을 것임을 의미합니다. 읽기 전용 계약은 수신자가 변경할 수 없다고 말합니다. 다른 사람 (특히 class A)은이를 변경할 수 있습니다.

객체를 불변으로 만들려면 진정으로 불변이어야합니다. 에이전트가 요청한 데이터에 관계없이 데이터를 변경하려는 시도를 거부해야합니다.

이 패턴은 목록, 시퀀스, 파일 (스트림) 등의 데이터 모음을 나타내는 개체에서 가장 많이 나타납니다.


"불변"이라는 단어는 희미 해지지 만 새로운 개념은 아닙니다. 그리고 불변성을 사용하는 방법에는 여러 가지가 있으며 다른 것 (예 : 경쟁사)을 사용하여 더 나은 디자인을 얻는 방법도 많이 있습니다.


여기 내 접근 방식이 있습니다 (불변성을 기반으로하지 않음).

  1. 값 튜플이라고도하는 DTO (데이터 전송 객체)를 정의합니다.
    • 이 DTO에는 세 개의 필드 hitpointsattack,, 가 포함됩니다 defense.
    • 필드 만 : 공개, 누구나 쓸 수 있고 보호 할 수 없습니다.
    • 그러나 DTO는 버리기 객체 여야합니다. 클래스 A가 DTO를 클래스에 전달해야하는 경우 B이를 복사하여 대신 전달합니다. 따라서 BDTO에 영향을주지 않으면 서 원하는대로 (쓰기) DTO를 사용할 수 있습니다 A.
  2. grantExperience기능은 두 가지로 나뉩니다.
    • calculateNewStats
    • increaseStats
  3. calculateNewStats 두 개의 DTO에서 입력을받습니다. 하나는 플레이어 통계를 나타내고 다른 하나는 적 통계를 나타냅니다.
    • 입력의 경우 , 발신자 는 필요에 따라 기본, 훈련 또는 유효 통계 중에서 선택해야합니다.
    • 그 결과, 각각의 필드 (DTO 새로운 것 hitpoints, attack, defense)를 저장 양이 증가되는 것을 능력.
    • 새로운 "증가량"DTO는 해당 값의 상한 (최대 한도)에 대해서는 신경 쓰지 않습니다.
  4. increaseStats "증가 금액"DTO를 취하는 플레이어 (DTO가 아닌)의 방법으로, 플레이어가 소유 한 DTO에 해당 증분을 적용하고 플레이어의 훈련 가능한 DTO를 나타냅니다.
    • 이 통계에 적용 가능한 최대 값이있는 경우 여기에 적용됩니다.

calculateNewStats다른 플레이어 나 적 정보에 의존하지 않는 경우 (두 입력 DTO의 값을 넘어서)이 방법은 프로젝트의 어느 곳에 나 위치 할 수 있습니다.

calculateNewStats플레이어와 적 오브젝트에 전적으로 의존하는 것으로 밝혀진 경우 (즉, 향후 플레이어와 적 오브젝트는 새로운 속성을 가질 수 있으며 calculateNewStats가능한 한 많은 새로운 속성을 사용하도록 업데이트되어야 함) calculateNewStats이 두 가지를 수용해야합니다 DTO뿐만 아니라 그러나 계산 결과는 여전히 증분 DTO이거나 증분 / 업그레이드를 수행하는 데 사용되는 정보를 전달하는 모든 단순 데이터 전송 객체입니다.


1

여기에는 큰 문제가 있습니다. 데이터 구조가 변경 불가능하다고해서 수정 된 버전이 필요하지 않다는 의미는 아닙니다 . 실제 데이터 구조의 변경 불가능한 버전도 제공하지 않습니다 setX, setYsetZ방법은 - 그들은 단지 반환 새로운 당신이 그들을에서 호출 된 개체를 수정하는 구조를 대신.

// Mutates mutableFoo
mutableFoo.setX(...)
// Creates a new updated immutableFoo, existing immutableFoo is unchanged
newFoo = immutableFoo.setX(...)

그렇다면 시스템의 한 부분을 변경하면서 다른 부분을 제한하는 기능을 어떻게 제공합니까? 불변 구조체의 인스턴스에 대한 참조를 포함하는 가변 객체. 기본적으로 플레이어 클래스가 통계 객체를 변경하는 동시에 다른 모든 클래스에 통계에 대한 변경 불가능한 뷰를 제공하는 대신 통계 변경 불가능하며 플레이어 는 변경 가능합니다. 대신에:

// Stats are mutable, mutates self.stats
self.stats.setX(...)

당신은 가질 것이다 :

// Stats are immutable, mutates self, setX() returns new updated stats
self.stats = self.stats.setX(...)

차이점을 보시겠습니까? stats 오브젝트는 완전히 변경할 수 없지만 플레이어의 현재 통계는 변경할 수 없는 오브젝트에 대한 변경 가능한 참조 입니다. 두 개의 인터페이스를 전혀 만들 필요가 없습니다. 단순히 데이터 구조를 완전히 변경 불가능하게 만들고 상태 저장에 사용하는 위치에서 변경 가능한 참조를 관리하십시오.

즉, 시스템의 다른 객체는 stats 객체에 대한 참조에 의존 할 수 없습니다. stats 객체는 플레이어가 통계를 업데이트 한 후 플레이어가 가진 통계 객체와 같지 않습니다.

어쨌든 개념적 으로 변화 하는 통계 가 아니기 때문에 플레이어의 현재 통계 입니다. 통계 오브젝트가 아닙니다. 기준 통계에 플레이어 오브젝트가 갖는 개체. 따라서 해당 참조에 따라 시스템의 다른 부분은 player.currentStats()플레이어의 기본 통계 객체를 파악하고 어딘가에 저장하고 돌연변이를 통해 업데이트하는 것에 의존하는 대신 명시 적으로 참조 하거나 일부 를 참조해야합니다 .


1

와우 ... 이건 정말 나를 다시 데려 간다. 나는이 같은 생각을 몇 번 시도했다. 배우기에는 좋은 것이있을 것이라고 생각했기 때문에 포기하기에는 너무 고집이났다. 다른 사람들도 이것을 코드로 시도하는 것을 보았습니다. 대부분의 구현 나는 당신과 같은 읽기 전용 인터페이스라고 보았다 FooViewer또는를 ReadOnlyFoo하고는 쓰기 전용 인터페이스 FooEditor또는 WriteableFoo내가 본 것 같아요 자바 또는 FooMutator한 번. 이런 식으로 일을하는 공식적이거나 일반적인 어휘가 있는지 잘 모르겠습니다.

이것은 내가 그것을 시도한 곳에서 나에게 유용한 것을 결코하지 않았다. 나는 그것을 완전히 피할 것입니다. 다른 사람들이 제안하고 한 걸음 물러서서 더 큰 아이디어에서이 개념이 정말로 필요한지 고려할 것입니다. 나는 이것을 시도하는 동안 생성 한 코드를 실제로 유지하지 않았기 때문에 이것을 수행하는 올바른 방법이 있는지 확실하지 않습니다. 매번 나는 상당한 노력과 단순화 된 일들을 철회했다. 그리고 그것은 다른 사람들이 YAGNI와 KISS, DRY에 대해 말한 내용과 관련이 있습니다.

한 가지 가능한 단점 : 반복. 잠재적으로 많은 클래스에 대해 이러한 인터페이스를 작성해야하며 하나의 클래스에 대해서만 각 메소드의 이름을 지정하고 각 서명을 두 번 이상 설명해야합니다. 코딩에서 나를 태우는 모든 것들 중에서, 이런 방식으로 여러 파일을 변경 해야하는 것은 가장 큰 어려움을 겪습니다. 결국 한 곳에서 다른 곳으로 변경하는 것을 잊어 버립니다. Foo라는 클래스와 FooViewer 및 FooEditor라는 인터페이스가 있으면 Bar를 호출하는 것이 더 좋을 것이라고 결정하면 실제로 놀랍도록 똑똑한 IDE가없는 한 세 번 리팩토링-> 이름을 바꿔야합니다. 코드 생성기에서 상당한 양의 코드를 사용했을 때도 마찬가지였습니다.

개인적으로, 나는 단지 하나의 구현만을 가진 것들에 대한 인터페이스를 만드는 것을 싫어합니다. 그것은 내가 바로 이동할 수없는 코드 주위에 여행 할 때 의미 만을 직접 구현, 내가 인터페이스의 구현 또는 몇 여분의 키가 얻을 수있는 최소한을 누를 때 다음 인터페이스로 이동하고있다. 그런 것들이 추가됩니다.

그리고 당신이 언급 한 합병증이 있습니다. 내 코드로 다시이 특정 방식으로 갈지 의심 스럽습니다. 이것이 코드 (내가 작성한 ORM)에 대한 전반적인 계획에 가장 잘 맞는 곳이라도 이것을 뽑아서 코드로 만들기 쉬운 것으로 대체했습니다.

나는 당신이 언급 한 작곡에 대해 더 많은 생각을 할 것입니다. 왜 그렇게 나쁜 생각인지 궁금합니다. 나는 것으로 예상했을 것이다 EffectiveStats작성하고 불변 Stats과 같은 것을 StatModifiers추가로 몇 가지 세트 구성 것이다 StatModifier(일부 개선 지역, 피로 임시 효과, 항목, 위치) 통계를 수정할 수있는 어떤 표현들이지만이 그 EffectiveStats때문에 이해할 필요하지 않을 StatModifiers겠습니까 그 것들이 무엇인지, 얼마나 많은 효과와 어떤 통계에 어떤 종류의 영향을 미칠지 관리하십시오. 그만큼StatModifier다른 것들에 대한 인터페이스가 될 수 있고 "존에 있을까", "약이 닳 았을 때"등과 같은 것을 알 수 있지만, 다른 물건들에게 그런 것들을 알려주지 않아도됩니다. 현재 어떤 스탯과 그 수정법 만 말하면됩니다. 더 적절 하지만 StatModifier단순히 변경 되었기 때문에 Stats다른 불변을 기반으로 새로운 불변을 생성하는 방법을 노출시킬 수 Stats있습니다. 그런 다음 같은 currentStats = statModifier2.modify(statModifier1.modify(baseStats))것을 Stats할 수 있고 모든 것은 불변입니다. 나는 직접 코딩조차하지 않을 것입니다. 아마도 모든 수정자를 반복하고으로 시작하는 이전 수정 자의 결과에 각각 적용 할 것입니다 baseStats.

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