Шаблон итератора - почему важно не раскрывать внутреннее представление?

24

Я читаю C # Design Pattern Essentials . В настоящее время я читаю о шаблоне итератора.

Я полностью понимаю, как реализовать, но я не понимаю важность или вижу вариант использования. В книге приведен пример, где кто-то должен получить список объектов. Они могли бы сделать это, выставив публичную собственность, такую ​​как IList<T>или Array.

Книга пишет

Проблема с этим заключается в том, что внутреннее представление в обоих этих классах подвергалось внешним проектам.

Что такое внутреннее представление? Факт это arrayили IList<T>? Я действительно не понимаю, почему это плохо для потребителя (программиста, вызывающего это) знать ...

Затем в книге говорится, что этот шаблон работает, выставляя его GetEnumeratorфункцию, поэтому мы можем вызывать GetEnumerator()и выставлять «список» таким образом.

Я предполагаю, что этот шаблон имеет место (как и все) в определенных ситуациях, но я не вижу, где и когда.

MyDaftQuestions
источник
1
Дополнительное чтение: en.m.wikipedia.org/wiki/Law_of_Demeter
Жюль
4
Потому что реализация может захотеть перейти от использования массива к использованию связанного списка, не требуя изменений в классе потребителя. Это было бы невозможно, если бы потребитель знал точный возвращаемый класс.
MTilsted
11
Это не плохо для потребителя знать , это плохо для потребителя, на которого можно положиться . Мне не нравится термин «скрытие информации», потому что он заставляет вас думать, что информация «личная», как ваш пароль, а не личная, как внутри телефона. Внутренние компоненты скрыты, потому что они не имеют отношения к пользователю, а не потому, что они являются своего рода секретом. Все, что вам нужно знать, это набрать здесь, поговорить здесь, послушать здесь.
Капитан Мэн
Предположим , что у нас есть хороший «интерфейс» , Fкоторый дает мне знания (методы) a, bи c. Все хорошо, может быть много разных вещей, но только Fдля меня. Думайте о методе как о «ограничениях» или предложениях контракта, которые Fобязывают классы делать. Предположим, что мы добавили dхотя, потому что нам это нужно. Это добавляет дополнительное ограничение, каждый раз, когда мы делаем это, мы навязываем все больше и больше F. В конце концов (в худшем случае) может быть только одна вещь, 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

Сокрытие реализации является основным принципом ООП и хорошей идеей во всех парадигмах, но это особенно важно для итераторов (или как их там называют на этом конкретном языке) в языках, которые поддерживают ленивую итерацию.

Проблема с представлением конкретного типа итераций - или даже интерфейсов, подобных 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 из них!

Enter Enumerators - версия шаблона итератора для 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>. Опять же, есть дополнительная гибкость.

Например, все linq-to-objects зависят только от IEnumerable<T>. В то время как он ускоряет поиск некоторых конкретных реализаций, IEnumerable<T>он делает это способом тестирования и использования, который всегда может вернуться к простому использованию GetEnumerator()и IEnumerator<T>реализации, которая возвращается. Это дает ему гораздо больше гибкости, чем если бы он был построен поверх массивов или списков.

Джон Ханна
источник
Хороший четкий ответ. Очень уместно, что вы держите это вкратце, и поэтому следуйте вашим советам в первом предложении. Браво!
Даниэль Холлинрейк
0

Формулировка мне нравится использовать зеркала @CaptainMan комментарий «s: „Это хорошо для потребителя , чтобы знать внутренние детали Это плохо для клиента. Необходимость знать внутренние детали.“

Если клиент должен ввести arrayили IList<T>в своем коде, когда эти типы были внутренними деталями, потребитель должен понять их правильно. Если они ошибаются, потребитель может не скомпилировать или даже получить неверные результаты. Это приводит к тому же поведению, что и другие ответы «всегда скрывайте детали реализации, потому что вы не должны их раскрывать», но начинает копаться в преимуществах отсутствия разоблачения. Если вы никогда не раскрываете детали реализации, ваш клиент никогда не сможет связать себя, используя ее!

Мне нравится думать об этом в этом свете, потому что он раскрывает концепцию сокрытия реализации под оттенки смысла, а не драконовское «всегда скрывайте детали реализации». Существует множество ситуаций, когда выставление реализации полностью допустимо. Рассмотрим случай с драйверами устройств реального времени, где возможность пропускать прошлые уровни абстракции и вставлять байты в правильные места может быть разницей между выбором времени или нет. Или рассмотрите среду разработки, в которой у вас нет времени на создание «хороших API-интерфейсов», и вам нужно использовать их немедленно. Часто разоблачение базовых API в таких средах может быть разницей между установлением крайнего срока или нет.

Однако, по мере того как вы оставляете эти особые случаи позади и смотрите на более общие случаи, становится все более и более важным скрывать реализацию. Выявление деталей реализации похоже на веревку. При правильном использовании вы можете делать с ним все, например, масштабировать высокие горы. Используется неправильно, и это может связать вас в петлю. Чем меньше вы знаете о том, как будет использоваться ваш продукт, тем более осторожным вы должны быть.

Ситуационное исследование, которое я вижу снова и снова, - это то, где разработчик создает API, который не очень тщательно скрывает детали реализации. Второй разработчик, используя эту библиотеку, обнаруживает, что в API отсутствуют некоторые ключевые функции, которые они хотели бы. Вместо того чтобы писать запрос функции (который может иметь срок обработки в месяцы), они вместо этого решают злоупотребить своим доступом к скрытым деталям реализации (что занимает секунды). Это само по себе неплохо, но часто разработчик API никогда не планировал, что это будет частью API. Позже, когда они улучшают API и изменяют эти базовые детали, они обнаруживают, что они прикованы к старой реализации, потому что кто-то использовал его как часть API. Худшая версия этого - когда кто-то использовал ваш API для обеспечения ориентированной на клиента функции, а теперь ваша компания ». s paycheck привязан к этому некорректному API! Просто, раскрыв подробности реализации, когда вы недостаточно знаете о своих клиентах, оказалось, что достаточно веревки, чтобы связать петлю вокруг вашего API!

Корт Аммон - Восстановить Монику
источник
1
Re: «Или подумайте о среде разработки, в которой у вас нет времени на создание« хороших API-интерфейсов », и вам нужно что-то использовать немедленно»: если вы оказались в такой среде и по какой-то причине не хотите выходить ( или я не уверен, что так и есть), я рекомендую книгу « Марш смерти»: Полное руководство разработчика программного обеспечения по выживанию в проектах «Миссия невыполнима» . У него есть отличный совет по этому вопросу. (Также я рекомендую изменить свое мнение о том, чтобы не
бросать
@ruakh Я обнаружил, что всегда важно понимать, как работать в среде, где у вас нет времени на создание хороших API. Честно говоря, как бы нам ни нравился подход к башне из слоновой кости, реальный код - это то, что фактически оплачивает счета. Чем раньше мы сможем это признать, тем скорее мы начнем подходить к программному обеспечению как к балансу, балансирующему качество API с продуктивным кодом, чтобы соответствовать нашей конкретной среде. Я видел слишком много молодых разработчиков, запутавшихся в беспорядке идеалов, не понимающих, что им нужно сгибать их. (Я также был молодым разработчиком ... все еще выздоравливаю после 5 лет разработки "хорошего API")
Корт Аммон - Восстановить Монику
1
Реальный код оплачивает счета, да. Хороший дизайн API - это гарантия того, что вы по-прежнему сможете генерировать реальный код. Crappy код перестает окупать себя, когда проект становится неуправляемым, и становится невозможно исправлять ошибки без создания новых. (И в любом случае, это ложная дилемма. Для хорошего кода требуется не столько время, сколько основа .)
ruakh
1
@ruakh Я обнаружил, что можно создать хороший код без «хороших API». Если есть возможность, хорошие API-интерфейсы хороши, но рассматривать их как необходимость вызывает больше раздоров, чем стоит. Подумайте: API для человеческих тел ужасен, но мы все еще думаем, что они довольно хорошие маленькие инженерные подвиги =)
Cort Ammon - Восстановить Монику
Другой пример, который я привел, был вдохновляющим для аргумента о выборе времени. В одной из групп, с которыми я работаю, были некоторые драйверы устройств, которые им нужно было разработать, с жесткими требованиями в реальном времени. У них было 2 API для работы. Один был хорош, другой - меньше. Хороший API не может определить время, потому что он скрывает реализацию. Меньший API раскрыл реализацию, и при этом вы можете использовать специфичные для процессора методы для создания рабочего продукта. Хороший API был вежливо положен на полку, и они продолжали отвечать требованиям сроков.
Cort Ammon - Восстановить Монику
0

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

Предположим, у вас есть источник данных, который вы представляете как массив, вы можете перебирать его с помощью обычного цикла for.

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

Хорошо, если у вас есть только один цикл for, вы можете легко изменить свой код, чтобы получить доступ к объекту данных так, как он ожидает к нему доступ.

Если у вас есть 1000 классов, пытающихся получить доступ к объекту данных, теперь у вас большая проблема с рефакторингом.

Итератор - это стандартный способ доступа к источнику данных на основе списка. Это общий интерфейс. Использование этого сделает ваш код независимым от источника данных.

superluminary
источник