Дизайн класса C ++: класс или функции в безымянном пространстве имен или метод закрытого класса?

Я расширяю существующий класс новой функциональности и сомневаюсь, какое дизайнерское решение использовать. Есть несколько, каждый из которых имеет свои плюсы и минусы. Мой случай таков: у меня есть заголовок файла, который имеет специальный формат, и я собираюсь прочитать и сохранить его. Существует класс с именем FileHeader, который уже реализует некоторую сериализацию из / в поток и некоторые другие функциональные возможности. Один пункт в моем списке задач — добавить определенную функцию отметки времени. Отметка времени должна считываться / храниться в секундах с 1 января 1994 года 00:00:00. Однако класс FileHeader хранит дату и время в двух отдельных переменных. Поэтому мне нужно написать преобразование из / в секунд в дату и время. Вопрос в том, где эта функциональность должна находиться. Я использую секундыPerDay (60 * 60 * 24) и dateOrigin (01.01.1994) в качестве констант.

Я вижу, есть следующие варианты:

А) реализовать преобразование как закрытые методы класса FileHeader. СекундыPerDay и DateOrigin будут статическими частными константами класса.

//fileheader.h
class FileHeader
{
private:
static const unsigned secondsPerDate = 60 * 60 * 24;
static const Date dateOrigin;
const Date &m_date;
const Time &m_time;
unsigned convertToSeconds() const; // convert m_date and m_time to seconds
void fromSeconds(unsigned secs); // convert and store to m_date and m_time
public:
void saveToStream(Stream &s) const;
void restoreFromStream(const Stream &s);
//... other methods
}

//fileheader.cpp
const Date FileHeader::dateOrigin = Date(1994, 1, 1);

Это довольно просто. Но что мне не нравится, так это то, что это добавляет больше ответственности и без того довольно тяжелому классу. Вы знаете правила: один класс = одна ответственность. Например, обслуживание будет сложно. Если кто-то решит изменить секунды на минуты или что-то в этом роде, он переписывает методы, но, если он не будет достаточно осторожным, вероятно, оставит статическую константу secondsPerDay, хотя она больше не нужна. И т.д. Кроме того, мне не нравится тот факт, что я должен был обновить заголовочный файл, хотя это влияет только на детали реализации.

Б) Выполните реализацию только в безымянном пространстве имен в файле .cpp и используйте обычные функции и статические переменные:

namespace
{
const unsigned secondsPerDay = 60 * 60 * 24;
const Date dateOrigin = Date(1994, 1, 1);
unsigned dateTimeToSeconds(const Date &d, const Time &t) ...
Date secondsToDate(unsigned secs) ...
Time secondsToTtime(unsigned secs) ...
}

методы сохранения и восстановления FileHeader должны затем вызывать эти функции. Ну, мне нравится больше. Заголовок я не испортил, класс FileHeader ответственности не вырос. Но если кто-то решит изменить алгоритм, чтобы использовать минуты вместо секунд, он мог бы изменить функции, но если не проявить осторожности, он оставит ненужную статическую переменную secondsPerDay, даже если она больше не нужна.

C) Используйте безымянное пространство имен в FileHeader.cpp и выделенный класс в нем.

namespace
{
class TimeConverter
{
private:
static const unsigned secondsPerDay = 60 * 60 * 24;
static const Date dateOrigin;
public:
static unsigned secondsFromDateTime(const Date &date, const Time &time) //implementation here...
static Date dateFromSeconds(unsigned seconds) //implementation here...
static Time timeFromSeconds(unsigned seconds) //implementation here...
};
const Date TimeConverter::dateOrigin = Date(1994, 1, 1);
}

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

m_date = TimeConverter::dateFromSeconds(secs);
m_time = TimeConverter::timeFromSeconds(secs);

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

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

  • Есть ли другой хороший и полезный вариант?

  • я пропускаю какой-то важный аспект или значение использования этих опций?

ОБНОВЛЕНИЕ: следуя совету из одного из ответов ниже, я также представляю вариант D:

namespace TimeConverter
{
const unsigned secondsPerDay = 60 * 60 * 24;
const Date dateOrigin = Date(1994, 1, 1);
unsigned secondsFromDateTime(const Date &date, const Time &time)
{
return (date - dateOrigin) * secondsPerDay + time.asSeconds();
}

Date dateFromSeconds(unsigned seconds)
{
return dateOrigin + seconds / secondsPerDay;
}

Time timeFromSeconds(unsigned seconds)
{
return Time(seconds % secondsPerDay);
}
}

и следующий вопрос — как D лучше, чем C и наоборот. Каковы плюсы и минусы?

3

Решение

Лично я бы выбрал вариант B. Если бы мне когда-нибудь понадобилось повторно использовать эту функциональность, я бы адаптировал ее к C. Но я думаю, что создание класса для каждой мелочи может привести к слишком большой суматохе и шаблону. Я предпочитаю абстрагироваться, когда мне нужно, а не раньше … вариация на YAGNI. Опция B определяет и использует функциональность в одном месте, что облегчает чтение. Кроме того, как вы сказали, он не загромождает заголовочный файл.

1

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

Если я правильно понял, модификации, которые вы хотите применить, являются только деталями реализации. Пользователь никогда не сможет его изменить.

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

//date time conversions function header
namespace foo
{
unsigned secondsPerDay();
unsigned secondsFromDateTime(
const Date &date,
const Time &time,
const Date& startOfTime);
Date dateFromSeconds(unsigned seconds, const Date& startOfTime);
Time timeFromSeconds(unsigned seconds, const Date& startOfTime);
}

Я ввожу функцию secondsPerDay вместо глобальной переменной только из соображений стиля и ясности. Я считаю, что разница в производительности ничтожна (скажет только профилирование).

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

Наконец, внутри вашего файла FileHeader.cpp вы включите заголовок и определите дату начала.

Заключительный комментарий к опциям C. Нет необходимости создавать класс с использованием только статического метода в C ++ (он нужен вам в java, например, где свободные функции не разрешены). Именованное пространство имен — это способ реализации C ++.

1

Определенно, не соглашайтесь с A. Если вы сделаете его закрытым членом, это все еще часть интерфейса класса, который просто загромождает его.

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

Я бы пошел с D. Я бы вытащил его в свой собственный файл .h и .cpp, чтобы сделать написание модульных тестов удобным, а затем #include его только в файле .cpp, так как это деталь реализации вашего класса, а не часть интерфейса.

1
По вопросам рекламы [email protected]