Определение интерфейса с пользовательскими типами данных: как мне сохранить это удобочитаемым?

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

struct MyDataType
{
std::wstring name;
std::wstring purpose;
}

DLL_EXPORT int DoSomething(MyDataType instruction);

В целях удобства обслуживания я постепенно переключаю эту библиотеку на более стандартный стиль интерфейса:

struct MyLibraryInterface
{
virtual int DoSomething(MyDataType instruction) = 0;
}

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

Способ 1

//MyLibraryInterface.h
namespace MyLibraryInterface_v1_Types
{
struct MyDataType
{
std::wstring name;
std::wstring purpose;
}
}
#include "MyLibraryInterface_v1_Types_Backend.private.h" //this header would contain the required "paperwork" to make my datatype work with the 3rd-party lib

struct MyLibraryInterface_v1
{
virtual int DoSomething(MyDataType instruction) = 0;
}

Преимущества:

  • Тот, кому нужно использовать мою библиотеку, просто должен #include один заголовок

  • Пользовательское пространство имен хранит типы данных отдельно от определений функций

Недостатки:

  • #include на полпути через заголовок выглядит неряшливо и неуместно (хотя я заметил, что некоторые заголовки MS используют эту технику)

  • Настраивается ли собственное пространство имен или я просто сбиваю с толку пользователей? (Сама инкапсуляция определенно необходима, так как я мог бы изменить определение этого типа данных в будущем. Но я не знаю, нужно ли мне собственное пространство имен или мне просто нужно поместить объявление типа данных в объявление структуры, которая будет используй это.)

Способ 2

//MyLibraryInterface.h
namespace MyLibraryInterface_v1_Types
{
struct MyDataType
{
std::wstring name;
std::wstring purpose;
}
}
//3rd-party "paperwork" is directly placed here

struct MyLibraryInterface_v1
{
virtual int DoSomething(MyDataType instruction) = 0;
}

Преимущества:

  • Не выглядит грязно #include на полпути через мой заголовок

Недостатки:

  • Сторонние документы теперь отображаются в заголовке, который будут использовать мои пользователи (им не нужно это видеть, и я бы предпочел, чтобы их не было, по косметическим причинам или из-за простоты понимания)

  • Такое ощущение, что я вообще не пользуюсь мощью пространства имен типа данных, так как сторонний код остается свободно плавающим в коде моей библиотеки и не инкапсулируется

Способ 3

//MyLibraryInterface.h
struct MyLibraryInterface_v1
{
struct MyDataType
{
std::wstring name;
std::wstring purpose;
}

virtual int DoSomething(MyDataType instruction) = 0;
}

#include "MyLibraryInterface_v1_Types_Backend.private.h" //this header would contain the required "paperwork" to make my datatype work with the 3rd-party lib

Преимущества:

  • Избавляется от пользовательского пространства имен
  • Меньше путаницы, потому что пользователи теперь имеют в виду MyLibraryInterface_v1::MyDataType вместо MyLibraryInterface_v1_Types::MyDataType, что является более интуитивным, если они вызывают функцию в MyLibraryInterface_v1 тем не мение

Недостатки:

  • #include в самом низу заголовка выглядит действительно плохой

  • Смешивание типов данных и объявлений функций кажется мне немного сомнительным

Способ 4

//MyLibraryInterface_v1_Types.h
namespace MyLibraryInterface_v1_Types
{
struct MyDataType
{
std::wstring name;
std::wstring purpose;
}
}
//3rd-party paperwork can be directly placed here, immediately following the definition of the custom datatype//MyLibraryInterface.h
#include "MyLibraryInterface_v1_Types.h" /* this header, as defined above, holds the definitions of the custom datatypes this library will use. It also includes the 3rd-party paperwork required to make those datatypes work. It can't be a private header, though, because users will need to access it to use the custom types. */

struct MyLibraryInterface_v1
{
virtual int DoSomething(MyDataType instruction) = 0;
}

Преимущества:

  • Сторонние документы оформляются напрямую с декларациями типов данных, которые в них нуждаются

Недостатки:

  • Пользователям может быть трудно найти или использовать пользовательские типы данных

  • Кажется довольно не интуитивным, так как типы данных находятся как в отдельном заголовке, так и в отдельном пространстве имен


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


Обновите немного больше информации:

Сторонняя библиотека, которую я использую, оборачивает мой интерфейс в struct для меня. Так что я смогу создать объект MyLibraryInterface*сторонняя библиотека позволит мне получить доступ к реализации этого интерфейса из указанной библиотеки DLL, а затем я могу вызвать MyLibraryObj->DoSomething(), По сути, это вариант pImpl.

Эта сторонняя библиотека также автоматически упаковывает любые типы STL и любые пользовательские типы данных, чтобы их можно было использовать в нескольких компиляторах, поэтому мой std::wstring использование здесь абсолютно безопасно. Однако библиотека требует, чтобы я предоставил определенную информацию для установки как обернуть пользовательские типы. Я должен предоставить эту информацию о настройке где-то после того, как каждый пользовательский тип определен, что исключает «нормальный» шаблон размещения #include с частной информацией о настройке вверху моего заголовка интерфейса. Я также не могу полностью удалить частную информацию о настройках из заголовка интерфейса; Любой, кто вызывает мою библиотеку через этот интерфейс, должен будет использовать стороннюю библиотеку для этого, и он должен будет снова предоставить объявление интерфейса, чтобы библиотека знала, что она ищет в данной DLL. Все, что я могу сделать, — это попытаться сделать работу частной установки как можно более аккуратной и ненавязчивой, а в идеале пометить ее как нечто, что пользователям моей библиотеки никогда не понадобится или не захочет работать напрямую.

Кроме того, у меня есть возможность поместить свои пользовательские типы данных в интерфейс struct или в свои namespace, Я играл с ними прямо в struct сначала, но так как некоторые из этих типов данных являются постоянными данными (enum classes) казалось немного неаккуратным, чтобы поместить их в struct с объявлениями функций. namespace «чувствуется» чище, но с другой стороны, функции и типы данных будут обрабатываться по-разному (myLibraryObj->DoSomething() против MyLibraryInterface_v1_Types::MyDataType) и, следовательно, может быть менее интуитивным, чем держать все в struct (myLibraryObj->DoSomething(), MyLibraryInterface_v1::MyDataType).

0

Решение

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

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

Напомним, что #include это чисто текстовый механизм, просто заменяющий текстовый блок на директиву. Таким образом, если вы включите внешний заголовок внутри detail пространство имен, оно не появится ни в глобальном пространстве имен, ни в пространстве имен библиотеки верхнего уровня. Включая внешние определения таким образом, вы можете явно предоставлять только то, что вам нужно, из внешнего заголовка; все остальное экранировано внутри detail иначе.

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

namespace library {
namespace detail {
#include <whatever>
namespace external_library {
class exposed {} ;
class hidden {} ;
}
}
typedef detail::external_library::exposed external_type ;
class my_type {} ;
}
library::my_type foo ;
library::external_type bar ;

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

1

Другие решения

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

Это абсолютно необходимо предоставить подробности реализации, такие как сторонняя библиотека, которую вы упоминаете своим конечным пользователям? Вы должны принять во внимание, что, когда они #include ваши заголовки, они также будут #includeВ таком случае производительность компиляции будет снижаться, и, самое главное, некоторые идентификаторы, используемые пользователями, могут конфликтовать с идентификаторами сторонних библиотек. Альтернативой было бы объявить все последние внутри detail пространство имен, как это делают библиотеки Boost. Тем не менее, не самый чистый выбор на мой взгляд. Разве вы не можете переместить настройку, связанную с вашими пользовательскими типами данных, из заголовков? Это может быть невозможно, если нужно что-то сделать с типом, но дать ему шанс. (Вы слышали о Pimpl идиома?)

1

Я бы применил следующие правила для развития библиотеки:

  1. Хорошая идея — использовать пространство имен, сделать его короче. Я бы порекомендовал не более 5 букв. Таким образом, если у потребителя библиотеки одинаковое имя типа (я сталкивался с такими случаями в моей карьере), он сможет различить ваши типы и его.
  2. Поместите все свои определения в это пространство имен — типы + интерфейсы.
  3. Не включайте частные заголовки в публичный. Включите частные заголовки в исходные файлы.
  4. Использование STL в вашем интерфейсе (wstring) ограничит использование клиентом вашей библиотеки того же компилятора (VC или gcc) и той же его версии.
    а. Я бы проверил, какой компилятор будут использовать ваши клиенты, и использую этот компилятор для вашей библиотеки. Сколько клиентов у вас будет? Это крупные предприятия или маленькие?
    б. Другой вариант — предоставить 2 версии вашей библиотеки — VC и gcc.
    с. В случае, если вам придется поддерживать слишком много типов и версий компиляторов в будущем — лучше работать с простым типом — массивом wchar_t (но я думаю, что это наименее предпочтительный вариант здесь)
0
По вопросам рекламы [email protected]