Не отдавайте свои внутренние детали? [С++]

Я читаю книгу под названием "Стандарт кодирования на C++". Херб Саттер, Андрей Александреску и в главе 42 этой книги - пример: (глава короткая, поэтому я беру на себя смелость и вставляю ее часть)

Рассмотрим:

 class Socket {
 public:
   // … constructor that opens handle_, destructor that closes handle_, etc. …
   int GetHandle() const {return handle_;} // avoid this - (1) <-why this is bad code?
                                           // and why there is a comment to avoid such code??
 private:
   int handle_; // perhaps an OS resource handle
 };

Скрытие данных - это мощное устройство абстракции и модульности (см. пункты 11 и 41). Но прятать данные, а затем раздавать ручки к нему самопровозглашает себя, точно так же, как запирать ваш дом и оставлять ключи в замке. Это происходит потому, что:

У клиентов теперь есть два способа реализовать функциональность: они могут использовать абстракцию вашего класса (Socket) или напрямую манипулировать реализацией, на которую опирается ваш класс (дескриптор C-стиля сокета). В последнем случае объект не знает о значительных изменениях в ресурсе, который, по его мнению, принадлежит ему. Теперь класс не может надежно обогащать или приукрашивать функциональность (например, проксирование, протоколирование, сбор статистики), поскольку клиенты могут обойти украшенную контролируемую реализацию и любые инварианты, которые он считает добавлением, что делает правильную обработку ошибок практически невозможной (см. Пункт 70).

Класс не может изменить базовую реализацию своей абстракции, потому что клиенты зависят от него: если Socket позже будет обновлен, чтобы поддерживать другой протокол с другим набором примитивов низкого уровня, вызывая код, который извлекает базовый дескриптор_ и неправильно обрабатывает его будет тихо нарушена.

Класс не может принудительно применять свои инварианты, потому что вызывающий код может изменять состояние без ведома класса: например, кто-то может закрыть дескриптор, используемый объектом Socket, не проходя через функцию-член Socket, тем самым делая объект недействительным.

Клиентский код может хранить дескрипторы, возвращаемые вашим классом, и попытаться использовать их после того, как ваш код класса сделал их недействительными.

это резюме из этой книги:

Не пользуйтесь слишком много: избегайте возврата дескрипторов во внутренние данные, управляемые вашим классом, поэтому клиенты не будут неуправляемо изменять состояние, которое ваш объект считает собственником.

В основном я прошу:

  • Почему строка, отмеченная мной как (1), указана в качестве примера плохого кода (я всегда думал, что возвращающие указатели или ссылка - плохая идея, но возврат по значению - это нормально. Здесь они говорят, что возвращение по значению - плохая идея тоже?)

  • Возможно ли, что есть & отсутствует и что они на самом деле означают - не возвращать внутренние данные по ссылке или указателям?

Спасибо.

Ответ 1

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

Ответ 2

Проблема заключается не в деталях низкого уровня (этот код отлично подходит для С++).

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

Ответ 3

Дело не в том, что вы возвращаетесь по значению, и это нормально, дело в том, что вы возвращаете дескриптор ресурса.

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

Если ресурс является файлом, например, ваш класс должен иметь метод write() и read(), который читает и записывает в/из файла.

Ответ 4

Обратите внимание на объявление handle_:

  int handle_; // perhaps an OS resource handle

Даже если вы возвращаете int по значению из точки зрения С++, с точки зрения ОС этот дескриптор является "ссылкой" на некоторый ресурс ОС.

Ответ 5

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

2) Я сомневаюсь, что им не хватает ссылки по причинам, указанным выше.

Ответ 6

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

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

Ответ 7

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

Ответ 8

Вот фон на ручках вообще:

http://www.anvir.com/handle.htm

Ручки - это непрозрачные ссылки на ресурсы (т.е. расположение памяти), и только подсистема, которая дала вам дескриптор, знает, как дескриптор связан с физическим указателем. Это ни значение, ни указатель, ни ссылка, это просто псевдоним для ресурса, который вы используете с API, который знает, что с ним.

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

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

Примером этого может быть библиотека Microsoft MFC С++, где класс CWnd имеет аксессуар, который возвращает окно HWND (т.е. дескриптор):

http://msdn.microsoft.com/en-us/library/d64ehwhz(VS.71).aspx

Ответ 9

"Совместное измененное состояние."

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

Рассмотрим альтернативный API, который обнаружил метод Close(), а не GetHandle(). Если вы никогда не разоблачаете дескриптор, класс затем гарантирует, что он будет единственным, кто закроет дескриптор. Рукоятка становится частным состоянием класса.

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

Ответ 10

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