저자는 구현에 대한 인터페이스 참조를 캐스트한다는 것은 무엇을 의미합니까?


17

현재 C #을 마스터하는 중이므로 Gary McLean Hall의 C #통해 Adaptive Code를 읽고 있습니다.

그는 패턴과 반 패턴에 대해 씁니다. 구현 대 인터페이스 부분에서 그는 다음과 같이 씁니다.

인터페이스 프로그래밍 개념에 익숙하지 않은 개발자는 종종 인터페이스의 배후에있는 것을 놓기가 어렵다.

컴파일 타임에 인터페이스의 모든 클라이언트는 사용중인 인터페이스의 구현을 모릅니다. 이러한 지식은 클라이언트를 인터페이스의 특정 구현에 연결하는 잘못된 가정으로 이어질 수 있습니다.

클래스가 영구 저장소에 레코드를 저장해야하는 일반적인 예를 상상해보십시오. 이를 위해 사용 된 영구 스토리지 메커니즘의 세부 사항을 숨기는 인터페이스에 올바르게 위임합니다. 그러나 어떤 인터페이스 구현이 런타임에 사용되고 있는지에 대한 가정은 옳지 않습니다. 예를 들어, 인터페이스 참조를 구현에 캐스트하는 것은 항상 나쁜 생각입니다.

언어 장벽이거나 경험이 부족할 수 있지만 그 의미를 이해하지 못합니다. 내가 이해하는 것은 다음과 같습니다.

C #을 연습 할 자유 시간 재미있는 프로젝트가 있습니다. 거기에는 수업이 있습니다.

public class SomeClass...

이 수업은 많은 장소에서 사용됩니다. C #을 배우면서 인터페이스를 사용하여 추상화하는 것이 더 낫다는 것을 읽었습니다.

public interface ISomeClass <- Here I made a "contract" of all the public methods and properties SomeClass needs to have.

public class SomeClass : ISomeClass <- Same as before. All implementation here.

그래서 나는 모든 클래스 참조에 들어가서 ISomeClass로 대체했습니다.

내가 쓴 구성을 제외하고 :

ISomeClass myClass = new SomeClass();

이것이 잘못되었음을 올바르게 이해하고 있습니까? 그렇다면 왜 그렇습니까? 대신 어떻게해야합니까?


25
예제에서 인터페이스 유형의 객체를 구현 유형으로 캐스팅하는 곳은 없습니다. 구현 유형의 무언가를 인터페이스 변수에 할당하고 있는데, 이는 완벽하고 정확합니다.
Caleth

1
"내가 작성한 생성자에서 무엇을 의미 ISomeClass myClass = new SomeClass();합니까? 만약 당신이 정말로 그것을 의미한다면, 그것은 당신이 원하는 것이 아니라 생성자에서의 재귀입니다. 희망적으로 당신은"건설 ", 즉 생성자 자체가 아닌 할당, 즉 아마도 의미합니다. ?
Erik Eidt

@ 에릭 : 예. 건설 중. 당신이 올바른지. 질문을 바로 잡을 것입니다. 감사합니다
Marshall

재미있는 사실 : F #은 C #보다 더 나은 스토리를 가지고 있습니다. 이는 암묵적인 인터페이스 구현을 없애기 때문에 인터페이스 메서드를 호출 할 때마다 인터페이스 유형으로 업 캐스트해야합니다. 이를 통해 코드에서 인터페이스를 사용하는시기와 방법을 매우 명확하게 알 수 있으며 언어에 훨씬 더 세분화 된 인터페이스를 프로그래밍 할 수 있습니다.
scrwtp

3
이것은 약간의 주제가 아니지만 저자가 개념에 익숙하지 않은 사람들의 문제를 잘못 진단한다고 생각합니다. 내 생각에 문제는 개념을 처음 접하는 사람들이 좋은 인터페이스를 만드는 방법을 모른다는 것입니다. 실제로 일반성을 제공하지 않는 너무 구체적인 인터페이스를 만드는 것은 매우 쉽습니다 ( ISomeClass하지만 잘 일어날 수 있음 ) . 유일한 옵션에 대해 유용한 코드를 작성할 수 없는 너무 일반적인 인터페이스를 만드는 것도 쉽습니다 인터페이스를 다시 생각하고 코드를 다시 작성하거나 다운 캐스트해야합니다.
데릭 엘 킨스는 SE를

답변:


37

클래스를 인터페이스로 추상화하는 것은 해당 인터페이스의 다른 구현을 작성하려고하거나 미래에 그렇게 할 가능성이있는 경우에만 고려해야합니다.

그래서 아마 SomeClassISomeClass그것이 가진 같은 것이기 때문에, 나쁜 예입니다 OracleObjectSerializer클래스와 IOracleObjectSerializer인터페이스를.

더 정확한 예는 같은 것 OracleObjectSerializer하고를 IObjectSerializer. 어떤 구현을 사용할지 신경 쓰는 유일한 프로그램 위치는 인스턴스가 생성 된 시점입니다. 때때로 이것은 팩토리 패턴을 사용하여 더 분리됩니다.

프로그램의 다른 곳에서는 IObjectSerializer작동 방식을 신경 쓰지 않아야 합니다. 이제 SQLServerObjectSerializer추가 구현 이 있다고 가정 해 봅시다 OracleObjectSerializer. 이제 설정할 특수 속성을 설정해야하고 해당 메소드가 SQLServerObjectSerializer가 아닌 OracleObjectSerializer에만 존재한다고 가정하십시오.

그것에 대해 두 가지 방법이 있습니다 : 잘못된 방법과 Liskov 대체 원칙 접근법.

잘못된 방법

잘못된 방법과 책에서 참조되는 인스턴스는 인스턴스를 가져 와서 IObjectSerializer캐스팅 OracleObjectSerializer한 다음에서 setProperty사용할 수 있는 메소드를 호출하는 것입니다 OracleObjectSerializer. 인스턴스가이라는 것을 알고 있지만OracleObjectSerializer 프로그램에서 구현이 무엇인지 알고 싶은 또 다른 요점을 소개 하기 때문에 이것은 나쁩니다 . 해당 구현이 변경 될 때 최상의 구현 시나리오 인 여러 구현이있는 경우 조만간이 모든 위치를 찾아서 올바르게 조정해야합니다. 최악의 시나리오에서는 IObjectSerializer인스턴스를에 캐스팅하고 OracleObjectSerializer프로덕션에서 런타임 오류가 발생합니다.

Liskov 대체 원리 접근

Liskov는 제대로 수행 setProperty한 경우 구현 클래스 와 같은 메소드가 필요하지 않아야한다고 말했습니다 OracleObjectSerializer. 클래스 OracleObjectSerializer를로 추상화하면 IObjectSerializer해당 클래스를 사용하는 데 필요한 모든 메소드를 포함해야 하며 , 그렇지 않으면 추상화에 문제가 있습니다 ( 예를 들어 Dog클래스를 IPerson구현으로 구현 하려고 시도 ).

올바른 방법은에 setProperty방법을 제공하는 것 IObjectSerializer입니다. 비슷한 방법 SQLServerObjectSerializer이이 setProperty방법을 통해 이상적으로 작동합니다 . 더 나은 방법으로, Enum각 구현이 해당 열거 형을 자체 데이터베이스 용어와 동등한 것으로 변환 하는 위치를 통해 특성 이름을 표준화합니다 .

간단히 말해서, 사용하는 ISomeClass것은 절반입니다. 생성을 담당하는 메소드 외부에 캐스트 할 필요는 없습니다. 그렇게하는 것은 거의 심각한 디자인 실수입니다.


1
당신이 캐스트에려고하는 경우에, 나에게 보인다 IObjectSerializer에게 OracleObjectSerializer당신이 그것이 무엇인지는 것을 "알고"있기 때문에, 당신은 당신의 미래를 포함 할 수있는,이 코드를 유지할 수 다른 사람과 자신에게 정직 (그리고 더 중요하게해야한다 자체) 및 OracleObjectSerializer생성 된 위치에서 사용 된 위치까지 사용하십시오. 이를 통해 특정 구현에 대한 종속성을 도입하고 있음을 알 수 있으며이를 수행하는 데 수반되는 작업과 추악 자체가 무언가 잘못되었다는 강력한 힌트가됩니다.
KRyan

당신이 정말로 어떤 이유로 경우 (그리고 않는 특정 구현에 의존해야, 그것은이 무슨 일을하고 의도와 목적을하고 있다는 것을 많은 뚜렷해집니다. 이것은 물론 일이 결코 "해야" 그리고 그것이 일어나고있는 것처럼 보일 수도있는 99 %의 시간은 실제로는 아니고 당신은 사물을 고쳐야하지만, 100 % 확실하거나 사물을 따라야 할 것은 없습니다.)
KRyan

@KRyan 절대적으로. 추상화는 필요한 경우에만 사용해야합니다. 필요하지 않을 때 추상화를 사용하면 코드를 이해하기가 더 어려워집니다.

29

허용되는 답변은 정확하고 매우 유용하지만 다음과 같이 요청한 코드 줄을 간단히 설명하겠습니다.

ISomeClass myClass = new SomeClass();

대체로 이것은 끔찍한 일이 아닙니다. 가능할 때마다 피해야 할 것은 다음과 같습니다.

void someMethod(ISomeClass interface){
    SomeClass cast = (SomeClass)interface;
}

코드가 외부에서 인터페이스를 제공하지만 내부적으로 특정 구현으로 캐스트하는 경우 "이 구현 만 가능하다는 것을 알고 있기 때문에" 그것이 사실이더라도 인터페이스를 사용하여 구현에 캐스트함으로써 실제 유형의 안전을 자발적으로 포기함으로써 추상화를 사용하는 척 할 수 있습니다. 누군가가 코드를 나중에 작업하고 인터페이스 매개 변수를 허용하는 메서드를 보는 경우 해당 인터페이스의 모든 구현이 전달할 수있는 유효한 옵션이라고 가정 할 것입니다. 특정 방법이 필요한 매개 변수에 관한 것임을 잊었을 때. 인터페이스에서 특정 구현으로 캐스트해야 할 필요가 있다고 생각되면 인터페이스, 구현, 또는이를 참조하는 코드가 잘못 설계되어 변경되어야합니다. 예를 들어, 전달 된 인수가 특정 클래스 인 경우에만 메소드가 작동하면 매개 변수는 해당 클래스 만 승인해야합니다.

이제 생성자 호출을 다시 살펴보십시오.

ISomeClass myClass = new SomeClass();

캐스팅으로 인한 문제는 실제로 적용되지 않습니다. 이 중 어느 것도 외부에 노출 된 것으로 보이지 않으므로 이와 관련된 특별한 위험이 없습니다. 본질적으로이 코드 라인 자체는 인터페이스가 처음부터 추상화되도록 설계된 구현 세부 사항이므로 외부 관찰자는 해당 기능에 관계없이 동일한 방식으로 작동하는 것을 볼 수 있습니다. 그러나 이것은 인터페이스의 존재로부터 아무것도 얻지 못합니다. 귀하의이 myClass유형을 가지고 ISomeClass있지만 항상 특정 구현이 할당 이후는 어떤 이유가 없습니다,SomeClass. 생성자 호출 만 변경하거나 나중에 해당 변수를 다른 구현으로 다시 할당하여 코드에서 구현을 교체 할 수있는 것과 같은 사소한 잠재적 이점이 있지만, 변수가 아닌 인터페이스에 변수를 입력해야하는 다른 곳이없는 한 이 패턴을 구현하면 인터페이스의 이점에 대한 실제 이해가 아니라 코드가 인터페이스 만 사용 된 것처럼 보이게됩니다.


1
Apache Math는 Cartesian3D Source, 286 행 으로이를 수행하는데, 이는 실제로 성가시다.
J_F_B_M

1
이것은 원래 질문을 다루는 실제로 정답입니다.
Benjamin Gruenbaum

2

나쁜 예를 들어 코드를 표시하는 것이 더 쉽다고 생각합니다.

public interface ISomeClass
{
    void DoThing();
}

public class SomeClass : ISomeClass
{
    public void DoThing()
    {
       // Mine for BitCoin
    }

}

public class AnotherClass : ISomeClass
{
    public void DoThing()
    {
        // Mine for oil
    }
    public Decimal Depth;
 }

 void main()
 {
     ISomeClass task = new SomeClass();

     task.DoThing(); //  This is good

     Console.WriteLine("Depth = {0}", ((AnotherClass)task).Depth); <-- The task object will not have this field
 }

문제는 코드를 처음 작성할 때 해당 인터페이스의 구현이 하나뿐이므로 캐스팅이 계속 작동하지만 앞으로는 다른 클래스를 구현 한 다음 (예에서 볼 수 있듯이) 사용중인 개체에 존재하지 않는 데이터에 액세스하십시오.


왜 안녕하세요? 당신이 얼마나 잘 생겼는지 아는 사람 있나요?
Neil

2

명확성을 위해 캐스팅을 정의하겠습니다.

캐스팅은 한 유형에서 다른 유형으로 무언가를 강제로 변환합니다. 일반적인 예는 부동 소수점 숫자를 정수 유형으로 캐스팅하는 것입니다. 캐스팅 할 때 특정 변환이 지정 될 수 있지만 기본값은 단순히 비트를 재 해석하는 것입니다.

다음은 이 Microsoft 문서 페이지 에서 전송하는 예입니다 .

// Create a new derived type.  
Giraffe g = new Giraffe();  

// Implicit conversion to base type is safe.  
Animal a = g;  

// Explicit conversion is required to cast back  
// to derived type. Note: This will compile but will  
// throw an exception at run time if the right-side  
// object is not in fact a Giraffe.  
Giraffe g2 = (Giraffe) a;  

당신은 할 수 구현하는 인터페이스의 특정 구현에 대한 인터페이스,하지만 당신이 그 같은 일 캐스트 뭔가 할 안된다 예상보다 다른 구현을 사용하는 경우 그 오류 또는 예기치 않은 동작이 발생하기 때문입니다.


1
"캐스팅은 어떤 유형에서 다른 유형으로 무언가를 변환하는 것입니다." -아니요. 캐스팅은 한 유형에서 다른 유형으로 무언가를 명시 적으로 변환하고 있습니다. 특히 "cast"는 해당 변환을 지정하는 데 사용되는 구문의 이름입니다. 암시 적 변환은 캐스트되지 않습니다. "캐스팅 할 때 특정 변환이 지정 될 수 있지만 기본값은 단순히 비트를 재 해석하는 것입니다." -- 확실히. 비트 패턴을 크게 변경하는 암시 적 및 명시 적 변환이 많이 있습니다.
hvd

@ hvd 나는 캐스팅의 명시 성과 관련하여 수정했습니다. 기본적으로 단순히 비트를 해석하는 것이라고 말했을 때, 자신의 유형을 만들려면 캐스트가 자동으로 정의되는 경우 다른 유형으로 캐스트 할 때 비트가 해석된다고 표현하려고했습니다. . 위의 Animal/ Giraffe예제 Animal a = (Animal)g;에서 비트를 해석하면 기린 관련 데이터가 "이 개체의 일부가 아닌 것으로 해석됩니다"가 해석됩니다.
Ryan1729

hvd의 말에도 불구하고 사람들은 종종 암시 적 변환과 관련하여 "캐스트"라는 용어를 사용합니다. 예를 들어 https://www.google.com/search?q="implicit+cast"&tbm=bks를 참조 하십시오 . 기술적으로는 다른 사용자가 다르게 사용할 때 혼동하지 않는 한 명시 적 전환을 위해 '캐스트'라는 용어를 예약하는 것이 더 정확하다고 생각합니다.
ruakh

0

내 5 센트 :

이러한 모든 예는 괜찮지 만 실제 사례는 아니며 실제 의도를 나타내지 않았습니다.

나는 C #을 모른다. 그래서 추상적 인 예제를 줄 것이다 (Java와 C ++의 혼합). 괜찮 으시길 바랍니다.

인터페이스가 있다고 가정하십시오 iList.

interface iList<Key,Value>{
   bool add(Key k, Value v);
   bool remove(Element e);
   Value get(Key k);
}

이제 많은 구현이 있다고 가정하십시오.

  • DynamicArrayList-평평한 배열을 사용하여 끝에 삽입 및 제거가 빠릅니다.
  • LinkedList-이중 링크 목록을 사용하여 앞과 끝에 빠르게 삽입합니다.
  • AVLTreeList-AVL 트리를 사용하여 모든 작업을 빠르게 수행하지만 많은 메모리를 사용합니다
  • SkipList-SkipList를 사용하여 AVL Tree보다 느리게 모든 것을 수행하지만 메모리는 적게 사용합니다.
  • HashList-HashTable 사용

많은 다른 구현을 생각할 수 있습니다.

이제 다음 코드가 있다고 가정하십시오.

uint begin_size = 1000;
iList list = new DynamicArrayList(begin_size);

우리가 사용하고 싶은 의도를 분명히 보여줍니다 iList. 더 이상 DynamicArrayList특정 작업 을 수행 할 수 없지만 iList.

다음 코드를 고려하십시오.

iList list = factory.getList();

이제 우리는 구현이 무엇인지조차 모릅니다. 이 마지막 예는 디스크에서 파일을로드 할 때 이미지 처리에 자주 사용되며 파일 형식 (gif, jpeg, png, bmp ...)이 필요하지 않지만 이미지 조작 (플립, 스케일, 끝에 png로 저장).


0

ISomeClass 인터페이스와 ISomeClass를 구현하도록 선언 된 것 외에는 코드에서 아무것도 모르는 myObject 객체가 있습니다.

ISomeClass 인터페이스를 구현하는 SomeClass 클래스가 있습니다. ISomeClass를 구현하기로 선언했거나 ISomeClass를 구현하기 위해 직접 구현했음을 알고 있습니다.

myClass를 SomeClass로 전송하는 데 문제가 있습니까? 두 가지가 잘못되었습니다. 하나는 myClass가 SomeClass (SomeClass의 인스턴스 또는 SomeClass의 서브 클래스)로 변환 될 수 있다는 것을 모르기 때문에 캐스트가 잘못 될 수 있다는 것입니다. 둘째,이 작업을 수행 할 필요가 없습니다. iSomeClass로 선언 된 myClass를 사용하여 작업하고 ISomeClass 메소드를 사용해야합니다.

SomeClass 객체를 얻는 지점은 인터페이스 메서드가 호출되는 시점입니다. 어느 시점에서 인터페이스에 선언되어 있지만 SomeClass에서 구현되어 있으며 아마도 ISomeClass를 구현하는 다른 많은 클래스에서 myClass.myMethod ()를 호출합니다. 호출이 SomeClass.myMethod 코드로 끝나는 경우 self는 SomeClass의 인스턴스임을 알 수 있으며, 그 시점에서이를 SomeClass 객체로 사용하는 것이 절대적으로 정확합니다. 물론 실제로 SomeClass가 아닌 OtherClass의 인스턴스 인 경우 SomeClass 코드에 도달하지 않습니다.

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