어떤 속성이 값을 변경할 수 있고 어떤 속성이 일정하게 유지되도록 인터페이스를 어떻게 설계합니까?


12

.NET 속성과 관련된 디자인 문제가 있습니다.

interface IX
{
    Guid Id { get; }
    bool IsInvalidated { get; }
    void Invalidate();
}

문제:

이 인터페이스에는 두 가지 읽기 전용 속성 Id과가 IsInvalidated있습니다. 그러나 이들이 읽기 전용이라는 사실만으로도 그 값이 일정하게 유지 될 것이라는 보장은 없습니다.

다음과 같은 사실을 매우 명확하게하는 것이 나의 의도 였다고 가정 해 봅시다.

  • Id 상수 값을 나타내며 (따라서 안전하게 캐시 될 수 있음)
  • IsInvalidatedIX객체 의 수명 동안 값을 변경할 수 있으므로 캐시하지 않아야합니다.

interface IX계약을 충분히 명시 적으로 수정 하려면 어떻게 해야합니까?

해결책에 대한 세 가지 시도 :

  1. 인터페이스는 이미 잘 설계되었습니다. 호출 된 메소드가 Invalidate()있으면 프로그래머는 유사한 이름의 특성 값 IsInvalidated이 영향을받을 수 있음 을 유추 할 수 있습니다.

    이 인수는 메서드와 속성의 이름이 비슷한 경우에만 사용됩니다.

  2. 이벤트로이 인터페이스를 보강하십시오 IsInvalidatedChanged.

    bool IsInvalidated { get; }
    event EventHandler IsInvalidatedChanged;
    

    a의 존재 …Changed에 대한 이벤트 IsInvalidated이 속성 값을 변경할 수 있습니다 상태 등에 대한 유사한 이벤트의 부재는 Id그 속성 값을 변경하지 않을 것을 약속합니다.

    나는이 솔루션을 좋아하지만 전혀 사용되지 않을 수있는 많은 추가 자료입니다.

  3. 속성 IsInvalidated을 메소드로 바꾸십시오 IsInvalidated():

    bool IsInvalidated();

    이것은 너무 미묘한 변화 일 수 있습니다. 값이 매번 새로 계산됨을 암시해야합니다. 상수 인 경우에는 필요하지 않습니다. MSDN 항목 "속성과 방법 선택"에 대해 다음과 같이 설명합니다.

    다음과 같은 상황에서는 속성 대신 메서드를 사용하십시오. […] 연산은 매개 변수가 변경되지 않더라도 호출 될 때마다 다른 결과를 반환합니다.

어떤 답변을 기대합니까?

  • 나는 문제에 대한 완전히 다른 해결책과 그들이 나의 위의 시도를 어떻게 이겼는지에 대한 설명에 가장 관심이 있습니다.

  • 내 시도에 논리적으로 결함이 있거나 아직 언급되지 않은 중대한 단점이있어 하나의 솔루션 만 남아 있거나 내가없는 곳이 어디인지 듣고 싶습니다.

    결함이 경미하고이를 고려한 후 둘 이상의 솔루션이 남아있는 경우 의견을 말하십시오.

  • 최소한 어떤 솔루션을 선호하는지, 어떤 이유로 어떤 피드백을 받고 싶습니다.


IsInvalidated필요 하지 않습니다 private set?
James

2
@ 제임스 : 인터페이스 선언이나 구현에서 반드시 그런 것은 아닙니다 :class Foo : IFoo { private bool isInvalidated; public bool IsInvalidated { get { return isInvalidated; } } public void Invalidate() { isInvalidated = true; } }
stakx

인터페이스의 @James 속성은 동일한 구문을 사용하더라도 자동 속성과 동일하지 않습니다.
svick

궁금한 점이 있습니다. 인터페이스가 그러한 동작을 강요하기 위해 "힘을 가지고"있습니까? 이 동작을 각 구현에 위임해서는 안됩니까? 사물을 해당 수준으로 시행하려면 주어진 인터페이스를 구현하지 않고 하위 유형이 상속받을 수 있도록 추상 클래스를 만들어야한다고 생각합니다. (그런데, 내 근거가 이해가 되나요?)
heltonbiker

@heltonbiker 불행히도, 대부분의 언어 (나는 알고있다)는 타입에 대해 그러한 제약을 표현하는 것을 허용하지 않지만 분명히 해야한다 . 이것은 구현이 아니라 사양의 문제입니다. 내 선택에있어, 빠진 것은 언어로 클래스 불변량사후 조건을 직접 표현할 수 있다는 것 입니다. InvalidStateException예를 들어 , 우리는 예를 들어 걱정할 필요가 없지만 이것이 이론적으로 가능한지 확실하지 않습니다. 그래도 좋을 것입니다.
proskor

답변:


2

1과 2보다 솔루션 3을 선호합니다.

솔루션 1의 내 문제는 : Invalidate방법 이 없으면 어떻게됩니까 ? IsValid를 반환 하는 속성을 가진 인터페이스를 가정 합니다 DateTime.Now > MyExpirationDate. SetInvalid여기에 명시적인 방법이 필요하지 않을 수 있습니다 . 방법이 여러 속성에 영향을주는 경우 어떻게됩니까? 연결 유형 IsOpenIsConnected속성을 가정하십시오. 둘 다 Close메소드의 영향을받습니다 .

해결 방법 2 : 이벤트의 유일한 요점은 개발자에게 비슷한 이름의 속성이 각 호출마다 다른 값을 반환 할 수 있음을 알리는 것이라면 강력하게 조언 할 것입니다. 인터페이스를 짧고 명확하게 유지해야합니다. 또한 모든 구현에서 해당 이벤트를 발생시킬 수있는 것은 아닙니다. IsValid위의 예제를 다시 사용하겠습니다 MyExpirationDate.에 도달하면 타이머를 구현하고 이벤트를 시작해야합니다 . 결국 해당 이벤트가 공용 인터페이스의 일부인 경우 인터페이스 사용자는 해당 이벤트가 작동 할 것으로 예상합니다.

이 솔루션에 대해 말한 것은 나쁘지 않습니다. 메소드 또는 이벤트가 존재하면 유사한 이름의 특성이 각 호출에서 다른 값을 리턴 할 수 있음을 나타냅니다. 내가 말하는 것은 그들 자신만으로는 항상 그 의미를 전달하기에 충분하지 않다는 것입니다.

솔루션 3 은 내가 갈 것입니다. aviv가 언급했듯이 이것은 C # 개발자에게만 효과가 있습니다. C # -dev로서 나에게 IsInvalidated속성이 아니라는 사실 은 "단순한 접근자가 아니라 여기에 무슨 일이 일어나고있다"는 의미를 즉시 전달합니다. 그러나 이것이 모든 사람에게 적용되는 것은 아니며 MainMa가 지적했듯이 .NET 프레임 워크 자체는 일관성이 없습니다.

솔루션 3을 사용하려면 컨벤션을 선언하고 팀 전체가 따르도록 권장합니다. 문서 도 필수적입니다. 내 의견으로는 실제로 값을 변경하는 것을 암시하는 것은 매우 간단합니다. " 값이 여전히 유효한 경우 true를 반환 합니다. ", " 연결이 이미 열려 있는지 여부를 나타냅니다. "vs. " 는이 개체 유효한 경우 true를 반환합니다 ." 문서에서 더 명시 적으로 아프지 않을 것입니다.

그래서 내 조언은 다음과 같습니다.

해결책 3을 협약으로 선언하고이를 준수하십시오. 속성 값이 변경되는지 여부를 속성 문서에서 명확히하십시오. 간단한 속성을 사용하는 개발자는 속성이 변경되지 않는 것으로 간주합니다 (예 : 객체 상태가 수정 될 때만 변경). 그것과 같은 소리는 속성이 될 수있는 방법을 발생 개발자 ( Count(), Length(), IsOpen()) 그가 someting의가에 가서 (희망) 정확히 방법은 무엇과 작동 방식을 이해하는 방법 문서를 읽어 알고있다.


이 질문에 대한 답은 매우 좋았으며 그 중 하나만 받아 들일 수있는 것은 유감입니다. 나는 다른 답변에서 제기 된 일부 요점에 대한 좋은 요약을 제공하고 내가하기로 결정한 것이 많거나 적기 때문에 귀하의 답변을 선택했습니다. 답변을 게시 한 모든 사람에게 감사합니다!
stakx

8

네 번째 해결책이 있습니다 : 문서에 의존하십시오 .

string클래스가 불변 이라는 것을 어떻게 알 수 있습니까? MSDN 설명서를 읽었으므로 간단히 알 수 있습니다. StringBuilder반면에 문서는 다시 말해주기 때문에 변경이 가능합니다.

/// <summary>
/// Represents an index of an element stored in the database.
/// </summary>
private interface IX
{
    /// <summary>
    /// Gets the immutable globally unique identifier of the index, the identifier being
    /// assigned by the database when the index is created.
    /// </summary>
    Guid Id { get; }

    /// <summary>
    /// Gets a value indicating whether the index is invalidated, which means that the
    /// indexed element is no longer valid and should be reloaded from the database. The
    /// index can be invalidated by calling the <see cref="Invalidate()" /> method.
    /// </summary>
    bool IsInvalidated { get; }

    /// <summary>
    /// Invalidates the index, without forcing the indexed element to be reloaded from the
    /// database.
    /// </summary>
    void Invalidate();
}

세 번째 솔루션은 나쁘지 않지만 .NET Framework는 따르지 않습니다 . 예를 들면 다음과 같습니다.

StringBuilder.Length
DateTime.Now

속성이지만 매번 일정하게 유지 될 것으로 기대하지는 않습니다.

.NET Framework 자체에서 지속적으로 사용되는 것은 다음과 같습니다.

IEnumerable<T>.Count()

방법이지만 다음과 같습니다.

IList<T>.Length

속성입니다. 첫 번째 경우, 작업을 수행하려면 예를 들어 데이터베이스 쿼리와 같은 추가 작업이 필요할 수 있습니다. 이 추가 작업에는 다소 시간 이 걸릴 수 있습니다 . 속성의 경우 짧은 시간 이 걸릴 것으로 예상됩니다 . 실제로 목록의 수명 동안 길이가 변경 될 수 있지만 실제로는 반환 할 것이 없습니다.


클래스를 사용하면 코드를 보면 객체의 수명 기간 동안 속성 값이 변경되지 않음을 분명히 알 수 있습니다. 예를 들어

public class Product
{
    private readonly int price;

    public Product(int price)
    {
        this.price = price;
    }

    public int Price
    {
        get
        {
            return this.price;
        }
    }
}

가격은 동일하게 유지됩니다.

안타깝게도 인터페이스에 동일한 패턴을 적용 할 수 없으며 이러한 경우 C #이 표현력이 충분하지 않으면 문서 (XML 문서 및 UML 다이어그램 포함)를 사용하여 격차를 해소해야합니다.


“추가 작업 금지”지침조차도 절대적으로 일관되게 사용되지는 않습니다. 예를 들어, Lazy<T>.Value처음 액세스 할 때 계산하는 데 시간이 오래 걸릴 수 있습니다.
svick

1
제 생각에는 .NET 프레임 워크가 솔루션 3의 규칙을 따르는 지 여부는 중요하지 않습니다. 개발자가 사내 유형 또는 프레임 워크 유형으로 작업하고 있는지 알 수있을 정도로 똑똑 할 것으로 기대합니다. 모르는 개발자는 문서를 읽지 않으므로 솔루션 4도 도움이되지 않습니다. 솔루션 3이 회사 / 팀 컨벤션으로 구현되면 다른 API도 그것을 따르는 지 여부는 중요하지 않습니다 (물론 감사하겠습니다).
enzi

4

내 2 센트 :

옵션 3 : C #이 아닌 사람은 그다지 실마리가 아닙니다. 그러나 당신이 가져온 인용으로 판단하면, 숙련 된 C # 프로그래머에게는 이것이 무언가를 의미한다는 것이 분명해야합니다. 그래도 이것에 관한 문서를 추가해야합니다.

옵션 2 : 변경 될 수있는 모든 것에 이벤트를 추가 할 계획이 아니라면 가지 마십시오.

다른 옵션:

  • 인터페이스 IXMutable의 이름을 변경하여 상태를 변경할 수 있음이 분명합니다. 예제에서 변경할 수없는 유일한 값은 id입니다. 일반적으로 변경 가능한 객체에서도 변경할 수없는 것으로 가정합니다.
  • 언급하지 않았습니다. 인터페이스는 우리가 원하는대로 동작을 설명하는 데 좋지 않습니다. 부분적으로는 언어에서 "이 메서드는 항상 특정 개체에 대해 동일한 값을 반환합니다"또는 "이 메서드를 인수 x <0으로 호출하면 예외가 발생합니다."와 같은 것을 설명 할 수 없기 때문입니다.
  • 언어를 확장하고 구문 -Annotations / Attributes에 대한보다 정확한 정보를 제공하는 메커니즘이 있다는 것을 제외하고 . 때로는 약간의 작업이 필요하지만 메소드에 대해 임의로 복잡한 것을 지정할 수 있습니다. "이 메소드 / 프로퍼티의 값은 오브젝트의 수명 기간 동안 변경 될 수 있습니다"라는 주석을 찾거나 발명하여 메소드에 추가하십시오. 구현 메소드를 "상속"하기 위해 몇 가지 트릭을 수행해야 할 수도 있으며 사용자는 해당 주석에 대한 문서를 읽어야 할 수도 있지만 힌트 대신 원하는 말을 정확하게 말할 수있는 도구가 될 것입니다 그것에.

3

또 다른 옵션은 인터페이스를 분할하는 것입니다. 호출 Invalidate()할 때마다 IsInvalidated동일한 값을 반환해야 한다고 가정하면 true호출 IsInvalidated한 동일한 부분을 호출 할 이유가없는 것 같습니다 Invalidate().

따라서 무효화 를 확인 하거나 원인 을 제시하지만 두 가지를 모두 수행하는 것은 합리적이지 않은 것으로 보입니다. 따라서 Invalidate()작업을 포함하는 하나의 인터페이스를 첫 번째 부분에 제공하고 다른 인터페이스를 확인하기위한 다른 인터페이스 를 제공하는 것이 합리적 IsInvalidated입니다. 첫 번째 인터페이스의 서명에서 인스턴스가 무효화 될 것이라는 것이 분명하기 때문에 나머지 질문 (두 번째 인터페이스의 경우)은 실제로 매우 일반적입니다. 주어진 유형을 변경할 수 없는지 여부를 지정하는 방법 .

interface Invalidatable
{
    bool IsInvalidated { get; }
}

나는 이것이 질문에 직접 대답하지는 않지만 적어도 어떤 유형을 불변 으로 표시하는 방법에 대한 질문으로 줄입니다. 이는 일반적이며 이해되는 문제입니다. 예를 들어, 달리 정의하지 않는 한 모든 유형을 변경할 수 있다고 가정하면 두 번째 인터페이스 ( IsInvalidated)가 변경 가능 하다는 결론을 내릴 수 IsInvalidated있으며 때때로 값을 변경할 수 있습니다 . 반면에 첫 번째 인터페이스가 다음과 같다고 가정합니다 (의사 구문).

[Immutable]
interface IX
{
    Guid Id { get; }
    void Invalidate();
}

이 불변 표시되기 때문에, 당신은 알고 이드가 변경되지 않습니다. 물론 invalidate를 호출하면 인스턴스 상태가 변경되지만 이 인터페이스를 통해이 변경 내용을 관찰 는 없습니다 .


나쁜 생각이 아닙니다! 그러나 [Immutable]변이를 일으키는 유일한 방법을 포함 하는 인터페이스를 표시하는 것은 잘못이라고 주장합니다 . Invalidate()메소드를 Invalidatable인터페이스 로 옮길 것 입니다.
stakx

1
@stakx 동의하지 않습니다. :) 나는 당신이 불변성순도 의 개념을 혼동한다고 생각합니다 (부작용이 없음). 실제로, 제안 된 인터페이스 IX의 관점에서, 관찰 가능한 돌연변이는 전혀 없다. 에 전화 할 때마다 Id매번 같은 값을 얻게됩니다. 동일하게 적용됩니다 Invalidate()(반환 유형이이므로 아무것도 얻지 못합니다 void). 따라서 유형은 변경할 수 없습니다. 물론 부작용 이 있지만 인터페이스를 통해 관찰 할 수 IX없으므로 클라이언트에 영향을 미치지 않습니다 IX.
proskor

우리는 그것이 IX불변 이라는 것에 동의 할 수 있습니다 . 그리고 그들은 부작용입니다; 클라이언트가 신경 쓰지 않아도 IX되거나 ( 경우의 경우 ) 부작용이 무엇인지를 알 수 있도록 두 개의 분리 된 인터페이스간에 분산 Invalidatable됩니다. 그런데 왜 움직이지 Invalidate까지 Invalidatable? 그것은 IX불변 순수를 만들며 , Invalidatable더 불변하거나 더 불순 해지지 않을 것입니다 (이미 모두 이미 있기 때문에).
stakx

(실제 시나리오에서 인터페이스 분할의 특성은 아마도 기술적 인 문제보다는 실제 사용 사례에 더 의존 할 것이지만, 여기에서 귀하의 추론을 완전히 이해하려고합니다.)
stakx

1
@stakx 우선, 인터페이스의 의미를 좀 더 명확하게 만드는 추가 가능성에 대해 질문했습니다. 내 생각은 문제를 불변성의 문제로 줄이고 인터페이스를 분할해야한다는 것입니다. 그러나 하나의 인터페이스의 불변을 만들기 위해 필요한 분할은도 있기 때문에, 감각을 만들 것이었다뿐만 아니라 모두를 호출 할 이유가없는 Invalidate()IsInvalidated인터페이스의 같은 소비자 . 호출 Invalidate()하면 무효화되었다는 것을 알아야합니다. 왜 확인해야합니까? 따라서 서로 다른 목적으로 두 개의 고유 한 인터페이스를 제공 할 수 있습니다.
proskor
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.