Что автор подразумевает под приведением ссылки на интерфейс к любой реализации?

17

Я в настоящее время в процессе попытки мастер C #, так что я читаю адаптивный код через C # по Gary McLean Hall .

Он пишет о шаблонах и анти-шаблонах. В части реализации и интерфейсов он пишет следующее:

Разработчики, которые плохо знакомы с концепцией программирования интерфейсов, часто испытывают трудности с тем, чтобы оставить то, что находится за интерфейсом.

Во время компиляции любой клиент интерфейса не должен знать, какую реализацию интерфейса он использует. Такое знание может привести к неверным предположениям, которые связывают клиента с конкретной реализацией интерфейса.

Представьте себе общий пример, в котором класс должен сохранять запись в постоянном хранилище. Для этого он по праву делегирует интерфейс, который скрывает детали используемого механизма постоянного хранения. Однако было бы неправильно делать какие-либо предположения о том, какая реализация интерфейса используется во время выполнения. Например, приведение ссылки на интерфейс к любой реализации всегда плохая идея.

Это может быть языковой барьер или отсутствие у меня опыта, но я не совсем понимаю, что это значит. Вот что я понимаю:

У меня есть свободное время для веселого проекта на 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: да. В разработке. Ты прав. Исправлю вопрос. Спасибо
Маршалл
Интересный факт: F # имеет лучшую историю, чем C # в этом отношении - он избавляется от неявных реализаций интерфейса, поэтому всякий раз, когда вы хотите вызвать метод интерфейса, вам необходимо выполнить переход к типу интерфейса. Это очень ясно показывает, когда и как вы используете интерфейсы в своем коде, и делает программирование интерфейсов намного более укоренившимся в языке.
scrwtp
3
Это немного не по теме, но я думаю, что автор неправильно диагностирует проблему, с которой сталкиваются люди, впервые знакомые с этой концепцией. На мой взгляд, проблема в том, что люди, плохо знакомые с этой концепцией, не знают, как создавать хорошие интерфейсы. Очень легко создать слишком специфические интерфейсы, которые на самом деле не обеспечивают какой-либо общности (что вполне может иметь место ISomeClass), но также легко сделать слишком общие интерфейсы, для которых невозможно написать полезный код, для которого на данный момент единственные варианты должны переосмыслить интерфейс и переписать код или убрать.
Дерек Элкинс покинул ЮВ

Ответы:

37

Абстрагирование вашего класса в интерфейс - это то, что вы должны рассмотреть в том и только в том случае, если вы намереваетесь написать другие реализации упомянутого интерфейса или существует большая вероятность сделать это в будущем.

Так что, возможно , SomeClassи ISomeClassявляется плохим примером, поскольку это было бы как иметь OracleObjectSerializerкласс и IOracleObjectSerializerинтерфейс.

Более точным примером будет что-то вроде OracleObjectSerializerа IObjectSerializer. Единственное место в вашей программе, где вы заботитесь о том, какую реализацию использовать, это когда создается экземпляр. Иногда это дополнительно отделено с помощью заводского шаблона.

Везде в вашей программе следует использовать IObjectSerializerне заботясь о том, как она работает. Давайте на секунду предположим, что у вас есть SQLServerObjectSerializerреализация в дополнение к OracleObjectSerializer. Теперь предположим, что вам нужно установить какое-то специальное свойство, и этот метод присутствует только в OracleObjectSerializer, а не в SQLServerObjectSerializer.

Есть два пути: неправильный путь и принцип подстановки Лискова .

Неправильный путь

Неправильный способ и сам экземпляр, упомянутый в вашей книге, состоит в том, чтобы взять экземпляр IObjectSerializerи привести его к типу, OracleObjectSerializerа затем вызвать метод, setPropertyдоступный только для OracleObjectSerializer. Это плохо, потому что, хотя вы можете знать, что экземпляр является OracleObjectSerializer, вы вводите в свою программу еще один момент, когда вам важно знать, что это за реализация. Когда эта реализация изменится, и, вероятно, рано или поздно, если у вас будет несколько реализаций, в лучшем случае вам потребуется найти все эти места и внести правильные корректировки. В худшем случае вы приводите IObjectSerializerэкземпляр к a OracleObjectSerializerи получаете сбой среды выполнения в рабочей среде.

Лисков Подстановка Принцип подхода

Лисков сказал, что вам никогда не понадобятся такие методы, как setPropertyв классе реализации, как в случае с my, 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
Это действительно правильный ответ, который касается исходного вопроса.
Бенджамин Грюнбаум
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
 }

Проблема в том, что когда вы изначально пишете код, вероятно, есть только одна реализация этого интерфейса, так что приведение будет работать, просто в будущем вы можете реализовать другой класс, а затем (как показывает мой пример) вы попробуйте получить доступ к данным, которых нет в используемом вами объекте.

Нил
источник
Привет, сэр. Кто-нибудь когда-нибудь говорил тебе, какой ты красивый?
Нил
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;  

Вы можете сделать то же самое и привести что-то, что реализует интерфейс к конкретной реализации этого интерфейса, но этого не следует делать, потому что это приведет к ошибке или неожиданному поведению, если будет использоваться реализация, отличная от той, на которую вы рассчитывали.

Ryan1729
источник
1
«Кастинг - это преобразование чего-то из одного типа в другой». - Нет. Приведение - это явное преобразование чего-то из одного типа в другой. (В частности, «cast» - это имя синтаксиса, используемого для определения этого преобразования.) Неявные преобразования не являются приведениями. «Конкретное преобразование может быть указано при приведении, но по умолчанию просто интерпретировать биты». - Конечно нет. Существует много преобразований, как явных, так и явных, которые включают значительные изменения в битовых комбинациях.
HVd
@hvd Теперь я внес исправление в отношении четкости приведения. Когда я сказал, что по умолчанию стоит просто переосмыслить биты, я пытался выразить, что если вы хотите сделать свой собственный тип, то в случаях, когда приведение автоматически определяется, когда вы приводите его к другому типу, биты будут интерпретироваться заново. , В приведенном выше примере Animal/ Giraffe, если бы вы сделали Animal a = (Animal)g;это, биты были бы переосмыслены (любые данные, специфичные для жирафа, были бы интерпретированы как «не являющиеся частью этого объекта»).
Ryan1729
Несмотря на то, что говорит HVD, люди очень часто используют термин «приведение» в отношении неявных преобразований; см., например, https://www.google.com/search?q="implicit+cast"&tbm=bks . Технически я считаю более правильным зарезервировать термин «приведение» для явных преобразований, если вы не запутаетесь, когда другие используют его по-другому.
Руах
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 ...), но все, что вам нужно, это сделать некоторые манипуляции с изображениями (flip, масштаб, сохранить как png в конце).

Ник
источник
0

У вас есть интерфейс ISomeClass и объект myObject, о котором вы ничего не знаете из своего кода, кроме того, что он объявлен для реализации ISomeClass.

У вас есть класс SomeClass, который, как вы знаете, реализует интерфейс ISomeClass. Вы знаете это, потому что он объявлен для реализации ISomeClass, или вы реализовали его самостоятельно для реализации ISomeClass.

Что не так с приведением myClass к SomeClass? Две вещи не так. Во-первых, вы действительно не знаете, что myClass - это то, что может быть преобразовано в SomeClass (экземпляр SomeClass или подкласс SomeClass), поэтому приведение может пойти не так. Второе, вам не нужно этого делать. Вы должны работать с myClass, объявленным как iSomeClass, и использовать методы ISomeClass.

Точка, в которой вы получаете объект SomeClass, - это когда вызывается метод интерфейса. В какой-то момент вы вызываете myClass.myMethod (), который объявлен в интерфейсе, но имеет реализацию в SomeClass и, конечно, возможно, во многих других классах, реализующих ISomeClass. Если вызов заканчивается в вашем коде SomeClass.myMethod, то вы знаете, что self является экземпляром SomeClass, и в этот момент абсолютно нормально и действительно правильно использовать его в качестве объекта SomeClass. Конечно, если это на самом деле экземпляр OtherClass, а не SomeClass, вы не получите код SomeClass.

gnasher729
источник