开发者

How to restrict a class/struct so that only certain predefined objects of it can exist?

Suppose your program needs to keep track of, say, months of the year. Each month has a name and a length in days. Obviously, this is information that you want to define at compile time, and you w开发者_JAVA技巧ant to limit your program so that no other month information can be defined during runtime. And of course you want to conveniently access month data without complicated method calls. The typical use cases for this information would loosely be along these lines:

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;

Whereas things that shouldn't fly include:

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

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

(I deliberately made the code as vague as possible to signify that I'm not limited to any particular implementation approach.)

What's the most elegant way of solving this problem? I'm adding my own aswer for public review, but other solutions are welcome.


How about something like:

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);
...


Make Month's constructor private, and expose a static function call getMonth.

In short, make Month singleton! Well this is what based on what I understand from your question.

--

Edit:

I would like to improve your implementation. As Months is not required, so I removed it:

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);


If you don't need state, the best is to use an enum. Otherwise, you need a variation of Singleton where there is a number of predefined instances of the class, rather than just one. The key to this is declaring the constructor private, so that no external parties can instantiate the class, and define all needed instances of the class as static members.

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(...);


I think there are a couple of ways to look at this:

1) Month is an enumerated type, with 12 elements representing properties of the 12 months of the Gregorian calendar. Since C++ doesn't explicitly offer enumerated class types, we'll fake it with either:

  • private constructors and public access to instances (an array, static data members, globals, or function(s) that take a month number or name and return a pointer/reference). This is a modified Singleton.

  • public constructors that validate the name or month number, and fill in the number of days from internal knowledge held by the class. This gives the class value semantics.

Note that for this purpose a modified Singleton (12-ton) might not actually be right. You say in your title, "only certain predefined objects can exist", but in your code you write Month m = GetRandomMonth();, which creates a new Month object, named m. So there aren't only certain predefined objects, you're creating one right there. It looks like you want to use months by value, not just by reference to predefined objects.

To do this you need Month to have an accessible copy constructor, and you'll probably want an assignment operator too. That means it's not a Twelveleton (meaning, restricted to 12 objects of the type), it's just that there are only 12 possible distinct, non-equal values. Consider by analogy the type char - there are only 256 possible values (on my implementation), but I can easily create more than 256 objects: char x[257] = {0};.

2) Month is a general type representing a month. There are only 12 values of this type actually used in the Gregorian calendar (13 if you use a different value for February in leap-years), but if you want to create a Month("Jabruapril", 42) (fictional), or a Month("Nisan", 30) (Hebrew), or a Month("December", 30) (Roman calendar prior to Julian reform), because you think the defined properties the class will help you with what you're doing, then you're welcome to. Checking that a month is a valid Gregorian month, and getting hold of Gregorian months, is a separate concern from creating months in general.

Each of (1) and (2) is potentially the right design.

If there's a lot of logic built in to the Month class that assumes Gregorian calendar, then there's unlikely to be any use for (2), it will just give the wrong answers if you try to use the class in a Hebrew calendar. Since the Gregorian calendar won't ever change (we all sincerely hope), there's not a lot of mileage in making the class testable with cases that aren't valid Gregorian months. I honestly can't predict how the calendar would change if it did change, so I can't write tests to ensure my code is ready for change. So (1) is probably all you'll ever want.

If Month doesn't do a great deal of itself, it's just something that gets plugged together to make up a year and the calendar intelligence lies elsewhere, then (2) could be useful - you or someone else will re-use it to implement other calendars. In principle I'm in favour of offering users of my classes as much flexibility as possible, although in practice sometimes it introduces a burden of unit-testing on the class that's not justified.


Here's my own solution:

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;
}

This seems to work well enough, though I can't iterate over months. That could be fixed however by placing the Month objects in an array and defining the individual months as references to the array elements.


Here is a solution with an enum class (C++11 required). I use only two months to keep code short. This design is not using the object oriented paradigm and ends up much simpler, even beginners can understand this. C++ is a multi-paradigm language. It pays off to go the for simplest solution for a given problem instead of focussing on one particular paradigm.

Note: this is the first answer which properly considers the fact, that February has a variable number of days depending on the year. The number of days per month cannot be a static property of the month "object", because it depends on the year. One has to use method or a function to get the days per month.

#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

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜