Сколько работы должен выполнить конструктор моего класса? - PullRequest
5 голосов
/ 18 января 2012

У меня есть класс, который представляет поток данных, он в основном читает или записывает в файл, но сначала данные шифруются / дешифруются, а также имеется базовый объект кодека, который обрабатывает доступ к носителю.

Я пытаюсь написать этот класс в RAII, и мне нужен чистый, красивый, удобный в использовании дизайн.

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

Прямо сейчас я делаю все это в конструкторе, что занимает много времени.Я подумываю о переносе крипто-инициализации (большей части работы) из ctor в отдельный метод (скажем, Stream::auth(key)), но опять же, это перенесет некоторую ответственность на пользователя класса, как онинеобходимо выполнить auth() до того, как вызовет все операции ввода-вывода.Это также означает, что я должен был бы поставить чек в вызовах ввода / вывода, чтобы убедиться, что auth() был вызван.

Как вы думаете, что является хорошим дизайном?

PS Iпрочитал похожий вопрос, но я не смог применить ответы по этому делу.Они в основном как "Это зависит" ...: - /

Спасибо

Ответы [ 4 ]

7 голосов
/ 18 января 2012

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

. Вы можете выбрать класс таким образом, чтобы оннаходится в каком-то «пустом» / «неактивном» состоянии после запуска конструктора, или вы можете перевести его непосредственно в «активное» состояние, в котором оно должно быть.

Как правило, оно должно бытьпредпочел, чтобы конструктор создал ваш класс.Обычно вы не считаете класс полностью «созданным», пока он фактически не будет готов к использованию, но исключения существуют.Однако имейте в виду, что в RAII одна из ключевых идей заключается в том, что класс не должен существовать , пока он не будет готов, запущен и пригоден для использования.Вот почему его деструктор выполняет очистку, и именно поэтому его конструктор должен выполнить настройку.

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

Думайте об этом с точки зрения инвариантов.На что я могу положиться, если мне дадут экземпляр вашего класса?Чем больше я могу с уверенностью предположить об этом, тем проще его использовать.Если может быть готовым к использованию, может находиться в каком-то «сконструированном, но не инициализированном» состоянии, а может быть в «очищенном, но не очищенном» состоянии.уничтожить "состояние, тогда его использование быстро становится болезненным.

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

Похоже, ваша проблема в том, что вы слишком много делаете в конструкторе.

Что если вы разделите работу на несколько меньшихклассы?Пусть кодек инициализируется отдельно, тогда я могу просто передать уже инициализированный кодек вашему конструктору.И все средства аутентификации и криптографии и тому подобное, возможно, могут быть перемещены в отдельные объекты, а затем просто переданы «этому» конструктору, как только они будут готовы.

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

2 голосов
/ 18 января 2012

По сути, это все сводится к тому, какой дизайн выбрать из следующих трех:

Дизайн

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


Дизайн 1

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

class SecureStream
{
public:
    SecureStream();
    void initialize(Stream&,const Key&) throw(InvalidKey,AlreadyInitialized);
    std::size_t get(      void*,std::size_t) throw(NotInitialized,IOError);
    std::size_t put(const void*,std::size_t) throw(NotInitialized,IOError);
};

Плюсы :

  1. Пользователи имеютконтроль, когда вызывать «тяжелый» процесс инициализации
  2. Объект может быть создан до того, как ключ существует.Это важно для таких сред, как COM, где все объекты должны иметь конструктор по умолчанию (CoCreateObject() не позволяет пересылать дополнительные аргументы конструктору объекта).Иногда все еще есть обходные пути, такие как строитель объект.

Минусы :

  1. Объекты должны быть проверены на наличиеустаревшее состояние перед использованием объекта.Этот может быть принудительно установлен объектом, возвращая код ошибки или выбрасывая исключение.Лично я ненавижу объекты, которые позволяют мне использовать их, и, похоже, просто игнорирую мои вызовы (например, неудавшийся std::ostream).

Дизайн 2

ЭтоRAII прим.Убедитесь, что объект пригоден для использования на 100% без дополнительных артефактов (например, вручную вызывая stream.initialize(...); для каждого экземпляра.

class SecureStream
{
public:
    SecureStream(Stream&,const Key&) throw(InvalidKey);
    std::size_t get(      void*,std::size_t) throw(IOError);
    std::size_t put(const void*,std::size_t) throw(IOError);
};

Pros :

  1. объект всегда можно считать действительным. Это намного проще в использовании.

Минусы :

  1. Конструктору может потребоваться много времени для выполнения.
  2. Все необходимые аргументы должны быть доступны при построении экземпляра. Это иногда было проблемой для меня, особенно если большинство других объектов в базе кода используютdesign # 1.

Design 3

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

class SecureStream
{
public:
    SecureStream(Stream&,const Key&);
    std::size_t get(      void*,std::size_t) throw(InvalidKey,IOError);
    std::size_t put(const void*,std::size_t) throw(InvalidKey,IOError);
private:
    void initialize() throw(InvalidKey);
};

Плюсы :

  1. Почти так же легко использовать, как дизайн # 1. Почти (см. ниже).

Минусы :

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

Обсуждение

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

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

В любом случае вы все равно можете получить преимуществадизайна # 3 с использованием динамического выделения класса в дизайне № 2.

Заключение

По сути, если единственная причина, по которой вы колеблетесь, это какой-то философский идеал, когда конструкторы выполняются быстро, я быпросто используйте чистый дизайн RAII.

2 голосов
/ 18 января 2012

Вы могли бы просто поставить проверку в вызовах ввода-вывода, чтобы увидеть, был ли вызван auth, и если он это сделал, то продолжить, если нет, то вызвать его.

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

0 голосов
/ 18 января 2012

Здесь нет строгого и быстрого правила, но в целом лучше избегать тяжелых конструкторов по двум причинам, которые приходят на ум (возможно, и по другим):

  • Порядок созданных объектовСписок инициализаторов может привести к незначительным ошибкам
  • Что делать с исключениями в конструкторе?Вам нужно будет обрабатывать частично построенные объекты в вашем приложении?
...