Я в настоящее время в процессе попытки мастер 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();
Я правильно понимаю, что это неправильно? Если да, то почему, и что я должен делать вместо этого?
источник
ISomeClass myClass = new SomeClass();
? Если вы действительно это имеете в виду, это рекурсия в конструкторе, возможно, не то, что вы хотите. Надеюсь, вы имеете в виду« конструирование », т.е. распределение, возможно, но не в самом конструкторе, верно ?ISomeClass
), но также легко сделать слишком общие интерфейсы, для которых невозможно написать полезный код, для которого на данный момент единственные варианты должны переосмыслить интерфейс и переписать код или убрать.Ответы:
Абстрагирование вашего класса в интерфейс - это то, что вы должны рассмотреть в том и только в том случае, если вы намереваетесь написать другие реализации упомянутого интерфейса или существует большая вероятность сделать это в будущем.
Так что, возможно ,
SomeClass
иISomeClass
является плохим примером, поскольку это было бы как иметьOracleObjectSerializer
класс иIOracleObjectSerializer
интерфейс.Более точным примером будет что-то вроде
OracleObjectSerializer
аIObjectSerializer
. Единственное место в вашей программе, где вы заботитесь о том, какую реализацию использовать, это когда создается экземпляр. Иногда это дополнительно отделено с помощью заводского шаблона.Везде в вашей программе следует использовать
IObjectSerializer
не заботясь о том, как она работает. Давайте на секунду предположим, что у вас естьSQLServerObjectSerializer
реализация в дополнение кOracleObjectSerializer
. Теперь предположим, что вам нужно установить какое-то специальное свойство, и этот метод присутствует только в OracleObjectSerializer, а не в SQLServerObjectSerializer.Есть два пути: неправильный путь и принцип подстановки Лискова .
Неправильный путь
Неправильный способ и сам экземпляр, упомянутый в вашей книге, состоит в том, чтобы взять экземпляр
IObjectSerializer
и привести его к типу,OracleObjectSerializer
а затем вызвать метод,setProperty
доступный только дляOracleObjectSerializer
. Это плохо, потому что, хотя вы можете знать, что экземпляр являетсяOracleObjectSerializer
, вы вводите в свою программу еще один момент, когда вам важно знать, что это за реализация. Когда эта реализация изменится, и, вероятно, рано или поздно, если у вас будет несколько реализаций, в лучшем случае вам потребуется найти все эти места и внести правильные корректировки. В худшем случае вы приводитеIObjectSerializer
экземпляр к aOracleObjectSerializer
и получаете сбой среды выполнения в рабочей среде.Лисков Подстановка Принцип подхода
Лисков сказал, что вам никогда не понадобятся такие методы, как
setProperty
в классе реализации, как в случае с my,OracleObjectSerializer
если все сделано правильно. Если вы абстрагируете классOracleObjectSerializer
вIObjectSerializer
, вы должны охватить все методы, необходимые для использования этого класса, и если вы не можете, то что-то не так с вашей абстракцией (пытаясь сделатьDog
класс работать какIPerson
реализация).Правильный подход заключается в том, чтобы предоставить
setProperty
методIObjectSerializer
. Подобные методы вSQLServerObjectSerializer
идеале будут работать через этотsetProperty
метод. Более того, вы стандартизируете имена свойств черезEnum
где каждая реализация переводит это перечисление в эквивалент для своей собственной терминологии базы данных.Проще говоря, использование -
ISomeClass
это только половина. Вам никогда не нужно приводить его вне метода, ответственного за его создание. Это почти наверняка серьезная ошибка проектирования.источник
IObjectSerializer
чтобы ,OracleObjectSerializer
потому что вы «знаете» , что это то , что она есть, то вы должны быть честным с самими собой (и что более важно, с теми , кто может поддерживать этот код, который может включать в свое будущее сам), и использоватьOracleObjectSerializer
весь путь от того, где он создан, до того, где он используется. Это делает общедоступным и ясным, что вы вводите зависимость от конкретной реализации - и работа и уродство, связанные с этим, сами по себе становятся сильным намеком на то, что что-то не так.Принятый ответ является правильным и очень полезным, но я хотел бы кратко остановиться на строке кода, о которой вы спрашивали:
Вообще говоря, это не страшно. То, что следует избегать, когда это возможно, будет делать это:
Если ваш код предоставляется интерфейсом извне, но внутренне преобразует его в конкретную реализацию, «потому что я знаю, что это будет только эта реализация». Даже если это в конечном итоге окажется правдой, используя интерфейс и приводя его к реализации, вы добровольно отказываетесь от реальной безопасности типов, просто чтобы притворяться, что используете абстракцию. Если кто-то еще позже поработает над кодом и увидит метод, который принимает параметр интерфейса, то он предположит, что любая реализация этого интерфейса является допустимой опцией для передачи. Это может даже быть вами немного ниже после того, как вы забыли, что конкретный метод заключается в том, какие параметры ему нужны. Если вам когда-нибудь понадобится преобразовать интерфейс в конкретную реализацию, то интерфейс, реализация, или код, который ссылается на них, неправильно разработан и должен измениться. Например, если метод работает только тогда, когда переданный аргумент является определенным классом, то параметр должен принимать только этот класс.
Теперь, оглядываясь назад на ваш вызов конструктора
проблемы от кастинга на самом деле не применяются. Ничто из этого, по-видимому, не подвергается внешнему воздействию, поэтому с ним не связано никакого особого риска. По сути, эта строка кода сама по себе является деталью реализации, которую интерфейсы изначально разрабатывают для абстрагирования, поэтому внешний наблюдатель увидит, что она работает одинаково, независимо от того, что они делают. Тем не менее, это также не получает ничего от существования интерфейса. У вас
myClass
есть типISomeClass
, но у него нет никаких причин, так как ему всегда назначается конкретная реализация,SomeClass
, Есть некоторые незначительные потенциальные преимущества, такие как возможность поменять реализацию в коде, изменив только вызов конструктора, или переназначить эту переменную позже другой реализации, но если нет другого места, где требуется, чтобы переменная была введена в интерфейс, а не реализация этого шаблона делает ваш код похожим на интерфейсы, которые используются только наизусть, а не из реального понимания преимуществ интерфейсов.источник
Я думаю, что проще показать ваш код на плохом примере:
Проблема в том, что когда вы изначально пишете код, вероятно, есть только одна реализация этого интерфейса, так что приведение будет работать, просто в будущем вы можете реализовать другой класс, а затем (как показывает мой пример) вы попробуйте получить доступ к данным, которых нет в используемом вами объекте.
источник
Просто для ясности, давайте определимся с кастингом.
Приведение - это принудительное преобразование чего-либо из одного типа в другой. Типичным примером является приведение числа с плавающей запятой к целочисленному типу. Конкретное преобразование может быть указано при приведении, но по умолчанию просто интерпретировать биты.
Вот пример кастинга с этой страницы документации Microsoft .
Вы можете сделать то же самое и привести что-то, что реализует интерфейс к конкретной реализации этого интерфейса, но этого не следует делать, потому что это приведет к ошибке или неожиданному поведению, если будет использоваться реализация, отличная от той, на которую вы рассчитывали.
источник
Animal
/Giraffe
, если бы вы сделалиAnimal a = (Animal)g;
это, биты были бы переосмыслены (любые данные, специфичные для жирафа, были бы интерпретированы как «не являющиеся частью этого объекта»).Мои 5 центов:
Все эти примеры в порядке, но они не являются примерами из реального мира и не показывают реальных намерений.
Я не знаю C #, поэтому приведу абстрактный пример (смесь между Java и C ++). Надеюсь, что все в порядке.
Предположим, у вас есть интерфейс
iList
:Теперь предположим, что есть много реализаций:
Можно придумать много разных реализаций.
Теперь предположим, что у нас есть следующий код:
Это ясно показывает наше намерение, которое мы хотим использовать
iList
. Конечно, мы больше не можем выполнятьDynamicArrayList
определенные операции, но нам нужноiList
.Рассмотрим следующий код:
Теперь мы даже не знаем, что такое реализация. Этот последний пример часто используется при обработке изображений, когда вы загружаете какой-то файл с диска, и вам не нужен его тип файла (gif, jpeg, png, bmp ...), но все, что вам нужно, это сделать некоторые манипуляции с изображениями (flip, масштаб, сохранить как png в конце).
источник
У вас есть интерфейс 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.
источник