Как ограничить класс / структуру, чтобы могли существовать только определенные предопределенные объекты? - PullRequest
3 голосов
/ 19 февраля 2011

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

Month m = GetRandomMonth();
if ( m == FEBRUARY )
    CreateAppointment(2011, m, 28);

// Iterating over all months would be optional, but a nice bonus
for (/* each month m*/)
    cout << "Month " << m.name << " has " << m.num_days << " days." << endl;

В то время как вещи, которые не должны летать, включают:

Month m = Month("Jabruapril", 42);  // should give compiler error

Month m = MonthService().getInstance().getMonthByName("February");  // screw this

(Я намеренно сделал код как можно более расплывчатым, чтобы показать, что я не ограничен каким-либо конкретным подходом к реализации.)

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

Ответы [ 6 ]

5 голосов
/ 19 февраля 2011

Как насчет чего-то вроде:

class Month
{
public:
    static const Month JANUARY;
    static const Month FEBRUARY;
    ...

private:
    Month(const std::string &name, int days) : name(name), days(days) {}

    const std::string   name;
    const int           days;
};

const Month Month::JANUARY = Month("January", 31);
const Month Month::FEBRUARY = Month("February", 28);
...
1 голос
/ 19 февраля 2011

Я думаю, что есть несколько способов взглянуть на это:

1) Month - это перечислимый тип с 12 элементами, представляющими свойства 12 месяцев григорианского календаря. Поскольку C ++ явно не предлагает перечислимые типы классов, мы подделаем его либо:

  • частные конструкторы и открытый доступ к экземплярам (массив, статические члены данных, глобальные переменные или функции (и), которые принимают номер или имя месяца и возвращают указатель / ссылку). Это модифицированный синглтон.

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

Обратите внимание, что для этой цели модифицированный синглтон (12-тонный) на самом деле не может быть правильным Вы говорите в своем заголовке «могут существовать только определенные предопределенные объекты», но в своем коде вы пишете Month m = GetRandomMonth();, который создает новый Month объект с именем m. Так что есть не только определенные предопределенные объекты, вы создаете один прямо здесь. Похоже, вы хотите использовать месяцы по значению, а не только по ссылке на предопределенные объекты.

Для этого вам нужно Month, чтобы иметь доступный конструктор копирования, и вам, вероятно, также понадобится оператор присваивания. Это означает, что это не Twelveleton (то есть, ограничено 12 объектами типа), просто есть только 12 возможных различных, не равных значений . Рассмотрим по аналогии тип char - есть только 256 возможных значений (в моей реализации), но я могу легко создать более 256 объектов: char x[257] = {0};.

2) Month - это общий тип, представляющий месяц. В григорианском календаре есть только 12 значений этого типа , фактически используемых (13, если вы используете другое значение для февраля в високосных годах), но если вы хотите создать Month("Jabruapril", 42) (вымышленный), или Month("Nisan", 30) (иврит), или Month("December", 30) (римский календарь до юлианской реформы), потому что вы думаете, что определенные свойства класса помогут вам в том, что вы делаете, тогда вы можете. Проверка того, что месяц является действительным григорианским месяцем, и овладение григорианскими месяцами - это отдельная задача от создания месяцев в целом.

Каждый из (1) и (2) потенциально является правильным дизайном.

Если в класс Month встроена большая логика, предполагающая использование григорианского календаря, маловероятно, что будет использоваться (2), он просто даст неправильные ответы, если вы попытаетесь использовать класс в Еврейский календарь. Поскольку григорианский календарь никогда не изменится (мы все искренне надеемся), не так много времени нужно для того, чтобы сделать класс тестируемым в случаях, которые не являются действительными григорианскими месяцами. Я, честно говоря, не могу предсказать, как изменится календарь, если он действительно изменится, поэтому я не могу писать тесты, чтобы убедиться, что мой код готов к изменениям. Так что (1) это, вероятно, все, что вы когда-либо захотите.

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

1 голос
/ 19 февраля 2011

Сделайте конструктор Month закрытым и предоставьте статический вызов функции getMonth.

Короче, сделай Month синглтон! Ну, это то, что основано на том, что я понимаю из вашего вопроса.

-

Edit:

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

class Month
{
public:
    string name;
    int num_days;
public:
    static const Month JANUARY;
    static const Month FEBRUARY;
private:
    Month(string n, int nd) : name(n), num_days(nd) {}
};

const Month Month::JANUARY = Month("January", 31);
const Month Month::FEBRUARY = Month("February", 28);
1 голос
/ 19 февраля 2011

Если вам не нужно указывать состояние, лучше всего использовать enum. В противном случае вам понадобится вариация Singleton , где есть не один, а несколько предопределенных экземпляров класса. Ключом к этому является объявление конструктора private, чтобы никакие внешние стороны не могли создать экземпляр класса и определить все необходимые экземпляры класса как статические члены.

class Month {
  public:
    static const Month JANUARY(...);
    ...
    static const Month DECEMBER(...);

    // public API
private:
  Month(...);

    // private members
};

const Month Month::JANUARY = Month(...);
...
const Month Month::DECEMBER = Month(...);
0 голосов
/ 14 января 2019

Вот решение с классом enum (требуется C ++ 11).Я использую только два месяца, чтобы код был коротким.Этот дизайн не использует объектно-ориентированную парадигму и в итоге оказывается намного проще, даже начинающие могут это понять.C ++ - это мультипарадигмальный язык.Лучше всего найти простейшее решение для данной проблемы вместо того, чтобы сосредоточиться на одной конкретной парадигме.

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

#include <ostream>
#include <cassert>
#include <iostream>

enum class Month {
  January, February
};

// conversion to stream to get name as string, for example
inline std::ostream& operator<<(std::ostream& os, Month m) {
  switch (m) {
    case Month::January: os << "January"; break;
    case Month::February: os << "February"; break;
    default: assert(false); // never arrive here
  }
  return os;
}

inline unsigned leap_days(int year) {
  return 0; // TODO, returns extra days for leap years
}

inline unsigned days(Month m, int year) {
  switch (m) {
    case Month::January: return 31;
    case Month::February: return 27 + leap_days(year);
  }
  assert(false); // never arrive here
  return 0;
}

// allows to iterate over months with a range-based for loop
// like so: for(Month m : months) { ... }
constexpr Month months[] = { Month::January, Month::February };

int main() {
    for (Month m : months) {
        std::cout << "month " << m << " has " << days(m, 2019) << std::endl;
    }
}
0 голосов
/ 19 февраля 2011

Вот мое собственное решение:

class Month
{
public:
    string name;
    int num_days;
private:
    Month(string n, int nd) : name(n), num_days(nd) {}
    friend class Months;
};

class Months
{
public:
    static const Month JANUARY;
    static const Month FEBRUARY;
    // ...
private:
    Months() {}
};

const Month Months::JANUARY = Month("January", 31);
const Month Months::FEBRUARY = Month("February", 28);
// ...

bool operator==(const Month& lhs, const Month& rhs)
{
    return lhs.name == rhs.name;
}

int main()
{
    cout << Months::JANUARY.name << " " << Months::JANUARY.num_days << endl;
    Month m = Months::FEBRUARY;
    if ( m == Months::FEBRUARY )
        cout << m.name << " " << m.num_days << endl;
    return 0;
}

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

...