Що означає автор, передаючи посилання інтерфейсу на будь-яку реалізацію?


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
Ніде у вашому прикладі ви не кидаєте об'єкт типу інтерфейсу до типу реалізації. Ви присвоюєте щось інтерфейс типу змінної інтерфейсу, що ідеально добре і правильно.
Калет

1
Що ви маєте на увазі "в конструкторі, де я писав ISomeClass myClass = new SomeClass();? Якщо ви це справді маєте на увазі, це рекурсія в конструкторі, ймовірно, не те, що ви хочете. Сподіваємось, ви маєте на увазі" побудова ", тобто розподіл, можливо, але не в самому конструкторі, правильно ?
Ерік Ейдт

@Erik: Так. У будівництві. Ви праві. Виправить питання. Спасибі
Маршалл

Приємний факт: F # має кращу історію, ніж C # у цьому відношенні - це усуває неявні реалізації інтерфейсу, тому щоразу, коли ви хочете викликати метод інтерфейсу, вам потрібно перейти на тип інтерфейсу. Це робить дуже зрозумілим, коли і як ви використовуєте інтерфейси у своєму коді, а також робить програмування інтерфейсів набагато більш вбудованими в мову.
scrwtp

3
Це трохи поза темою, але я думаю, що автор неправильно діагностує проблему, яку мають нові люди в концепції. На мою думку, проблема полягає в тому, що люди, які не знають про концепцію, не знають, як зробити хороші інтерфейси. Дуже легко зробити занадто специфічні інтерфейси, які насправді не забезпечують загальності (що, можливо, трапляється з цим ISomeClass), але також легко зробити занадто загальні інтерфейси, для яких неможливо написати корисний код, на який момент єдині параметри повинні переосмислити інтерфейс і переписати код або перейти вниз.
Дерек Елкінс покинув SE

Відповіді:


37

Обмеження вашого класу інтерфейсом - це те, що ви повинні враховувати, якщо і лише якщо ви збираєтесь писати інші реалізації цього інтерфейсу або існує велика можливість цього зробити в майбутньому.

Тож можливо SomeClassі ISomeClassє поганим прикладом, тому що це було б як мати OracleObjectSerializerклас та IOracleObjectSerializerінтерфейс.

Більш точним прикладом може бути щось на кшталт OracleObjectSerializerі IObjectSerializer. Єдине місце у вашій програмі, де вам важливо, яку реалізацію використовувати, - це коли створюється екземпляр. Іноді це додатково відокремлюється за допомогою заводського зразка.

Повсюдно у вашій програмі слід використовувати IObjectSerializerне піклуючись про те, як вона працює. Нехай на секунду припустимо, що крім вас також є SQLServerObjectSerializerреалізація OracleObjectSerializer. Тепер припустимо, що вам потрібно встановити якесь особливе властивість, і цей метод присутній лише в OracleObjectSerializer, а не в SQLServerObjectSerializer.

Є два шляхи для цього: неправильний шлях та принцип підходу Ліскова .

Невірний спосіб

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

Принцип принципу заміни Лісков

Лісков сказав, що вам ніколи не потрібні такі методи, як 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. Є деякі незначні переваги, такі як можливість замінити реалізацію в коді, змінивши лише виклик конструктора або переназначивши цю змінну пізніше на іншу реалізацію, але, якщо не існує деінде, який вимагає введення змінної в інтерфейс, а не Реалізація цієї схеми робить ваш код схожим на те, що інтерфейси використовувались лише за допомогою "rote", не виходячи з реального розуміння переваг інтерфейсів.


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;  

Ви можете зробити те ж саме і надати щось, що реалізує інтерфейс для певної реалізації цього інтерфейсу, але не слід, тому що це призведе до помилки або несподіваної поведінки, якщо використовується інша реалізація, ніж ви очікуєте.


1
"Кастинг - це перетворення чогось одного типу в інший." - Ні. Кастинг явно перетворює щось із одного типу в інший. (Зокрема, "cast" - це назва синтаксису, який використовується для вказівки цього перетворення.) Неявні перетворення - це не касти. "Під час трансляції може бути визначена конкретна конверсія, але за замовчуванням - просто повторно інтерпретувати біти." - Звичайно, ні. Існує багато конверсій, як неявних, так і явних, які передбачають значні зміни в бітових моделях.
hvd

@hvd Я вніс виправлення щодо явності кастингу. Коли я сказав, що за замовчуванням - це просто переінтерпретувати біти, я намагався висловити, що якщо ти повинен створити свій власний тип, то у випадках, коли команда автоматично визначається, коли ти передаєш її іншому типу, біти будуть повторно інтерпретовані . У Animal/ Giraffeприкладі вище, якщо ви зробите, Animal a = (Animal)g;що біти будуть переосмислені, (будь-які дані, пов'язані з Giraffe, трактуються як "не частина цього об'єкта").
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 та об'єкт, 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.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.