Ітераторська схема - чому важливо не піддавати внутрішнього представлення?


24

Я читаю основи дизайну моделей C # . Я зараз читаю про шаблон ітератора.

Я повністю розумію, як реалізувати, але не розумію важливості чи не бачу випадку використання. У книзі наведено приклад, де комусь потрібно отримати список об’єктів. Вони могли це зробити, оголивши публічну власність, таку як IList<T>або Array.

Книга пише

Проблема в цьому полягає в тому, що внутрішнє представництво в обох цих класах зазнало зовнішніх проектів.

Що таке внутрішнє представництво? Те, що це arrayчи IList<T>? Я дійсно не розумію, чому це погано для споживача (програміст називає це), щоб знати ...

Потім у книзі сказано, що ця модель працює, виявляючи її GetEnumeratorфункції, тому ми можемо зателефонувати GetEnumerator()та викрити "список" таким чином.

Я припускаю, що в таких моделях є місце (як і всі) в певних ситуаціях, але я не бачу, де і коли.


1
Подальше читання: en.m.wikipedia.org/wiki/Law_of_Demeter
Жуль

4
Тому що реалізація може захотіти змінитись від використання масиву до використання зв'язаного списку, не вимагаючи змін у споживчому класі. Це було б неможливо, якби споживач знав точний клас, що повернувся.
MTilsted

11
Споживачеві це не погано знати , це погано, на що споживач покладається . Мені не подобається термін приховування інформації, тому що це змушує вас вважати, що інформація є "приватною", як ваш пароль, а не приватною, як внутрішня частина телефону. Внутрішні компоненти приховані тому, що вони не мають значення для користувача, а не тому, що вони є якимось секретом. Все, що вам потрібно знати, - це набрати тут, поговорити тут, послухати тут.
Капітан Людина

Припустимо , що у нас є хороший «інтерфейс» , Fякий дає мені знання (методи) a, bі c. Все добре і добре, може бути багато речей, які відрізняються, але тільки Fдля мене. Розгляньте метод як "обмеження" або пункти договору, що Fзобов'язує їх виконувати класи. Припустимо, ми додамо dвсе-таки, бо нам це потрібно. Це додає додаткового обмеження, щоразу, коли ми робимо це, ми все більше накладаємо на Fs. Врешті-решт (найгірший випадок) лише одне може бути Fтаким, чого ми можемо також не мати. І Fстільки обмежує, що існує лише один спосіб зробити це.
Алек Тіл

@captainman, який чудовий коментар. Так, я розумію, чому "абстрагувати" багато речей, але поворот у пізнанні та покладанні - це, чесно кажучи .... Блискуче. І важлива відмінність, яку я не враховував, поки не прочитав ваш пост.
MyDaftQuestions

Відповіді:


56

Програмне забезпечення - це гра обіцянок та привілеїв. Ніколи не годиться обіцяти більше, ніж ви можете поставити, або більше, ніж потрібно вашому співробітнику.

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

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

Тепер, якщо вашому співробітнику потрібен доступ до ряду речей, щоб вони гарантовано не пропустили жодної з них, ви реалізуєте Iterableінтерфейс і викриєте лише ті методи, які цей інтерфейс оголошує. Таким чином, наступного року, коли у вашій стандартній бібліотеці з’явиться масово краща та ефективніша структура даних, ви зможете вимкнути базовий код і скористатися ним, не фіксуючи код свого клієнта скрізь . Є й інші переваги: ​​не обіцяти більше, ніж потрібно, але одна лише така велика, що на практиці ніякі інші не потрібні.


12
Exposing the concrete type Array usually creates many additional promises, e.g. that you can sort the collection by a function of your own choosing...- Блискуче. Я просто не думав про це. Так, це повертає "ітерацію" і тільки, ітерацію!
MyDaftQuestions

18

Приховування реалізації є основним принципом OOP і гарною ідеєю у всіх парадигмах, але це особливо важливо для ітераторів (або, як вони їх називають, певною мовою) на мовах, які підтримують ліниву ітерацію.

Проблема з викриттям конкретного типу ітерабелів - або навіть інтерфейсів на кшталт IList<T>- полягає не в об'єктах, які їх викривають, а в методах, які їх використовують. Наприклад, скажімо, у вас є функція друку списку Foos:

void PrintFoos(IList<Foo> foos)
{
    foreach (foo in foos)
    {
        Console.WriteLine(foo);
    }
}

Ви можете використовувати цю функцію лише для друку списків foo - але тільки якщо вони реалізовані IList<Foo>

IList<Foo> foos = //.....
PrintFoos(foos);

Але що робити, якщо ви хочете надрукувати кожен рівномірний елемент списку? Вам потрібно буде створити новий список:

IList<Foo> everySecondFoo = new List<T>();
bool isIndexEven = true;
foreach (foo; foos)
{
    if (isIndexEven)
    {
        everySecondFoo.Add(foo);
    }
    isIndexEven = !isIndexEven;
}
PrintFoos(everySecondFoo);

Це досить довго, але за допомогою LINQ ми можемо це зробити з одноклассником, який насправді є більш читабельним:

PrintFoos(foos.Where((foo, i) => i % 2 == 0).ToList());

Тепер ви помітили це .ToList()в підсумку? Це перетворює ледачий запит у список, тому ми можемо передати його PrintFoos. Для цього потрібно виділити другий список і два пункти на елементи (один у першому списку для створення другого списку, а другий у другому списку для його друку). Крім того, що робити, якщо у нас це є:

void Print6Foos(IList<Foo> foos)
{
    int counter = 0;
    foreach (foo in foos)
    {
        Console.WriteLine(foo);
        ++ counter;
        if (6 < counter)
        {
            return;
        }
    }
}

// ........

Print6Foos(foos.Where((foo, i) => i % 2 == 0).ToList());

Що робити, якщо foosє тисячі записів? Нам доведеться пройти всі вони та виділити величезний список, щоб лише надрукувати 6 з них!

Введіть перечислювачі - версію C # в ітераторі. Замість того, щоб наша функція приймала список, ми приймаємо його Enumerable:

void Print6Foos(Enumerable<Foo> foos)
{
    // everything else stays the same
}

// ........

Print6Foos(foos.Where((foo, i) => i % 2 == 0));

Тепер Print6Foosможна ліниво повторювати перші 6 пунктів списку, і нам не потрібно торкатися решти його.

Не піддаючи внутрішнього представництва ключове значення тут. Коли ми Print6Foosприйняли список, ми повинні були надати йому список - те, що підтримує випадковий доступ - і тому нам довелося виділити список, оскільки підпис не гарантує, що він буде лише повторювати його. Приховуючи реалізацію, ми можемо легко створити більш ефективний Enumerableоб’єкт, який підтримує лише те, що функція насправді потребує.


6

Викриття внутрішнього представництва практично ніколи не є хорошою ідеєю. Це не тільки робить код важче міркувати, але і складніше в обслуговуванні. Уявіть, що ви вибрали будь-яку внутрішню репресію - скажімо, IList<T>- і викрили цю внутрішню. Кожен, хто використовує ваш код, може отримати доступ до Списку, і код може покладатися на внутрішнє представництво як "Список".

З будь-якої причини ви вирішили змінити внутрішнє представництво IAmWhatever<T>на якийсь пізніший час. Замість того, щоб просто змінити внутрішній клас вашого класу, вам доведеться змінити кожен рядок коду та методу, спираючись на тип внутрішнього представлення IList<T>. Це громіздко і в кращому випадку схильне до помилок, коли ви єдиний, хто використовує клас, але може порушити код інших людей, використовуючи ваш клас. Якщо ви просто відкрили набір загальнодоступних методів, не піддаючи жодних внутрішніх справ, ви могли змінити внутрішнє представлення без будь-якого рядка коду поза вашим класом, зауваживши, працюючи так, ніби нічого не змінилося.

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


6

Чим менше ви кажете, тим менше вам доводиться говорити.

Чим менше ви просите, тим менше вам потрібно сказати.

Якщо ваш код лише відкритий IEnumerable<T>, який підтримує лише GetEnumrator()його, його можна замінити будь-яким іншим кодом, який може підтримувати IEnumerable<T>. Це додає гнучкості.

Якщо ваш код використовує лише IEnumerable<T>його, він може підтримуватися будь-яким кодом, який реалізується IEnumerable<T>. Знову ж таки, є додаткова гнучкість.

Наприклад, всі зв'язки до об'єктів залежать лише від IEnumerable<T>. Хоча це швидкі шляхи, деякі конкретні його реалізації IEnumerable<T>роблять це тестовим способом, який завжди може бути відступом від простого використання GetEnumerator()та IEnumerator<T>повернення. Це дає йому набагато більшу гнучкість, ніж якби вона була побудована на вершині масивів чи списків.


Приємна чітка відповідь. Дуже вдалий у тому, що ви тримаєте це коротко, а тому дотримуйтесь ваших порад у першому реченні. Браво!
Даніель Холлінрак

0

Формулювання Я люблю використовувати дзеркала в коментарі @CaptainMan: "Споживачеві добре знати внутрішні деталі. Клієнту погано потрібно знати внутрішні деталі".

Якщо замовник має ввести arrayабо ввести IList<T>його код, коли ці типи були внутрішніми реквізитами, споживач повинен їх правильно виправити. Якщо вони помиляються, споживач може не збирати або навіть отримувати неправильні результати. Це спричиняє таку ж поведінку, що й інші відповіді "завжди приховувати деталі реалізації, тому що ви не повинні їх викривати", але починає копати переваги, не викриваючи їх. Якщо ви ніколи не виставляєте детальну інформацію про реалізацію, ваш клієнт ніколи не може покласти себе на користь, використовуючи її!

Мені подобається думати про це в цьому світлі, оскільки це відкриває концепцію приховування реалізації до відтінків сенсу, а не драконівського "завжди приховувати деталі реалізації". Існує безліч ситуацій, коли викриття реалізація цілком справедлива. Розглянемо випадок драйверів пристроїв у режимі реального часу, коли можливість пропускати минулі шари абстракції та байкати в потрібні місця може бути різницею між тим, як зробити час чи ні. Або врахуйте середовище розробки, де у вас немає часу зробити "хороші API", і вам потрібно використовувати речі негайно. Часто розкриття базових API в таких середовищах може бути різницею між тим, чи зробити термін, чи ні.

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

Тематичне дослідження, яке я бачу знову і знову, - це те, де розробник робить API, який не дуже обережно приховує деталі реалізації. Другий розробник, використовуючи цю бібліотеку, виявляє, що в API відсутні деякі ключові функції, які вони хотіли б. Замість того, щоб написати запит на функцію (який може мати поворотну кількість місяців), вони замість цього вирішують зловживати своїм доступом до прихованих деталей реалізації (що займає секунди). Це само по собі не погано, але часто розробник API ніколи не планував того, щоб бути частиною API. Пізніше, коли вони вдосконалюють API та змінюють цю основну деталь, вони виявляють, що вони прикуті до старої реалізації, оскільки хтось використовував її як частину API. Найгірша версія цього - коли хтось використовував ваш API для надання орієнтованої на клієнта функції, а тепер ваша компанія ' s зарплата прив'язана до цього недосконалого API! Просто викривши деталі впровадження, коли ви недостатньо знали своїх клієнтів, виявилося достатньою мотузкою, щоб зав'язати петлю навколо вашого API!


1
Re: "Або врахуйте середовище розробки, де у вас немає часу для створення" хороших API-програм ", і вам потрібно використовувати речі негайно": Якщо ви опинитесь у такому середовищі, і чомусь не хочете виходити ( або не впевнені, чи так це зробити), я рекомендую книгу Death March: Повне керівництво розробника програмного забезпечення для виживання проектів "Місія неможлива" . Він має чудові поради з цього приводу. (Також рекомендую змінити свою думку про те, щоб не
виходити

@ruakh Я вважав, що завжди важливо зрозуміти, як працювати в умовах, коли у вас немає часу на створення хороших API. Відверто кажучи, наскільки ми любимо підхід до башти зі слонової кістки, справжній код - це те, що насправді платить рахунки. Чим раніше ми можемо це визнати, тим швидше ми можемо почати наближатись до програмного забезпечення як балансу, врівноважуючи якість API та продуктивний код, щоб відповідати нашому точному середовищу. Я бачив занадто багато молодих розробників, захоплених у безладді ідеалів, не усвідомлюючи, що їм потрібно їх розгинати. (Я також був тим молодим розробником .. як і раніше одужав після 5 років дизайну "хорошого API")
Cort Ammon - Відновіть Моніку

1
Реальний код оплачує рахунки, так. Гарний дизайн API - це те, як ви гарантуєте, що надалі зможете генерувати реальний код. Crappy код перестає платити за себе, коли проект стає неможливим і неможливо виправити помилки, не створюючи нових. (І в будь-якому випадку, це помилкова дилема. Чого потрібний хороший код - це не стільки часу, скільки хребта .)
ruakh

1
@ruakh Що я знайшов, це те, що можна зробити хороший код без "хороших API". Якщо вам надається можливість, хороші API-файли є приємними, але ставитися до них як до необхідності викликає більше чвар, ніж варто. Поміркуйте: API для людських тіл є жахливим, але ми все ще вважаємо, що вони досить хороші подвиги техніки =)
Корт Аммон - Відновити Моніку

Інший приклад, який я наведу, - це той, який був натхненником для аргументу щодо створення часу. В одній із груп, з якими я працюю, були деякі драйвери для пристроїв, які вони потребували розробити, з жорсткими вимогами в реальному часі. У них було 2 API для роботи. Одне було добре, інше - менше. Хороший API не міг зробити терміни, оскільки він приховував реалізацію. Менший API виявив реалізацію, і це дозволить використовувати специфічні для процесора методи для створення робочого продукту. Хороший API був ввічливо поставлений на полицю, і вони продовжували відповідати вимогам термінів.
Корт Аммон - Відновіть Моніку

0

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

Припустимо, у вас є джерело даних, який ви представляєте як масив, ви можете повторити його за допомогою регулярного циклу.

Тепер скажіть, що ви хочете зробити рефактор. Ваш начальник попросив, щоб джерело даних був об'єктом бази даних. Крім того, він хотів би мати можливість витягувати дані з API, або хеш-мапу, або пов'язаного списку, або обертового магнітного барабана, або мікрофона, або підрахунок якостих стружок у реальному часі, знайдених у Зовнішній Монголії.

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

Якщо у вас 1000 класів, які намагаються отримати доступ до об'єкта даних, то тепер у вас є велика проблема рефакторингу.

Ітератор - це стандартний спосіб доступу до джерела даних на основі списку. Це загальний інтерфейс. Використання його зробить ваш код джерелом даних агностичним.

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