Looking at separation of concerns / single-responsibility principle for encapsulation, the following questions come to mind:
- Why does the User object care if it is a legal age?
- Who should be responsible for determining if the user object is a legal age?
- The legal age for what?
- Who is responsible for making sure the time used in determining age is accurate, including time zone corrections if that matters?
Using the real-world "driving" example:
License (permission) to operate a motor vehicle is typically restricted by age, but the age in question varies by location. When a person applies for such a license, the relevant licensing authority typically does not simple ask the person "are you of legal age?" and take a "yes/no" answer. Instead, they typically want to make that determination themselves, based on the person's birthdate, some authoritative source for the current time, and the relevant laws (policy). The criteria for this determination is available to the person, so they can avoid wasting time going to the licensing authority in person and waiting for their application to be processed, but the person themself is not the one who grants that permission.
Like some other answers, this makes me think additional object types might help clear up responsibility in this interface.
Alexei Levenkov's answer before it was deleted, showed using a DateProvider (authoritative source of current time) and a Policy. If in your application, the answer to "Why does the User object care if it is a legal age?" is, as in our car example, "to avoid wasting resources on a request guaranteed to fail", then that policy class could include an accessor method providing its minimum age, or perhaps the earliest birthdate for which permission would be granted.
[EDIT]: That answer appears to have been deleted. The general idea for simple cases is something like this:
class DateProvider // or "interface DateProvider"
// if that is how your language of preference
// describes purley abstract classes with only
// pure virtual methods
{
public:
virtual Date current_date() const = 0;
// probably not necessary if your language declared as "interface"
virtual ~DateProvider() = default;
};
class AgePolicy
{
public:
AgePolicy(TimeDuration legal_age)
: minimum_age(legal_age)
{}
bool meets_age_requirement(User const & usr, DateProvider const & dp)
{
// assuming your Date and TimeDuration classes have
// proper operator overloads for this
return dp.current_date() - usr.get_birthday() >= this->get_minimum_age();
}
// optional, in case User really needs to know this
TimeDuration get_minimum_age() const {return minimum_age;}
private:
TimeDuration minimum_age;
};
To address supercat's comment about generalizing beyond age, you could instead set up the policy like:
class Policy
{
public:
virtual bool meets_requirement(User const &) const = 0;
virtual ~Policy() = default;
};
class AgePolicy : public Policy
{
// as previous AgePolicy, but now pass pointer or reference to
// DateProvider in constructor so it can saved as private data
// to be used in the method call
};
You could then have an implementation of Policy that aggregates other concrete Policy implementations (location, insurance, etc) and whose method returns the logical "and" of each policy.
The advantage of having a separate policy object is that it provides a single answer to "The legal age for what?" Maybe today you care about "renting a car in Spain", but in five years your company has offices in the US and China, and now you care about those, too. Being able to inject those policy differences can make that more straightforward to update.
I know you were concerned in the comments about leaking encapsulation of the date, but I think that is inherent in this interface. If I ask the local drivers license authority to renew my driver's license, they'll want my current license number; that's just part of that interface. As a small bit of what is essentially "plain data" that has no functionality or behavior attached to it directly (in the case of the license number, it is just a glorified integer), I think that's fine; no worse to return an integer than a bool.
Dates are a little more complicated than plain integers in that there are a variety of possible formats and reference points, but if necessary, I think handling those differences is better solved with some sort of adapter object to convert between formats (in the simplest case, that would be applied when data enters or exits your program from users, files, system calls, third-party libraries, etc. and a single consistent representation is used within your code).
If you are concerned about the date being completely publicly accessible, you might be able to make use of the pass-key pattern to make access a little more restricted.
The advantage of having a separate DateProvider interface is that it provides a single answer to: "Who should be responsible for determining if the user object is a legal age?" If you currently only care about your local time, you can make a concrete implementation that queries some authoritative server for your local time. If you want to trust code running elsewhere to provide its own time (knowing that computer users can change their system clocks), you can make a class that does that. If you want a test class that returns a random date each time to see if the system is robust against absurd inputs, you can make that.
That example could be further extended to include some sort of permitting authority object, that makes use of a particular policy and date provider via aggregation or composition to enforce the state or action of whatever it is the user object is trying to get permission to do. Now we have a single answer to "Who should care if the user object is a legal age?" This provides a place to make sure the right combination of DateProvider and Policy are used; if you are enforcing rules in Tokyo and New York, not only the age limits, but the local date may differ, and you don't want to get those mixed up.
Depending on what the user is trying to do with that permission (and the capabilities of the language involved), maybe the authority object simply calls some function if permission would be granted, or maybe it provides the user with a passkey. In this case, the policy class could be changed to take a date as its input instead of a user, if you wanted (in which case the policy allows for jokes like "my car is old enough to drink now"). Something like:
class Authority
{
public:
// in languages that don't garbage-collect, be wary of
// lifetime issues here
Authority(DateProvider *, Policy *);
// if you really want a bool
bool permits(User const &) const;
// see the link for possible implementation details of the PassKey;
// some other function or object that requires permission then
// takes the PassKey as an argument
PassKey request_permission(User const &) const;
// where Callable could be a function pointer in languages
// that support them, or an object with an overloaded
// call operator in languages that have those, or
// an interface with relevant method
void call_if_permitted(User const &, Callable) const
// private data members to store provider, policy
};
If we want a sanity check, we could also look at this from a data perspective. I know this is tagged object-oriented, but we're about to see how using other tools can suggest that we may be on the right track for that object-oriented design. Suppose we wanted to make these objects persistent by storing them in a database. Database normalization and object-relational mappings are topics unto themselves, but we'll keep it simple because we're only after a sanity check here.
- It would probably make sense to store the birthdate in the user table, because each user is expected to have exactly one birthday, that can be looked up based on who the user is (whatever that primary key may be).
- It probably does not make sense to store the current time in the table, because the duplicated data wastes space and provides an opportunity to fail to keep it updated properly, and has nothing to do with the primary key of the user table; that information probably belongs as the return value of some kind of subroutine or system call, maybe taking the relevant location to account for timezone differences if necessary.
- It probably does not make sense to have some table-wide variable in the user table for legal age, because that data has nothing to do with whatever the primary key for users is.
- It may, however, make sense to have some kind of policy table (say, minimum driving age by country) that stores the minimum driving age keyed to some code for the country.
- The authority, if used, might by keyed on a combination of local time zone and country code, allowing lookups into the policy table, and using the time zone with the date call.
Object-relational mapping in general is not always an easy problem, and some problems just won't look similar, or correct, from both perspectives, but if you find an organization that does look right both ways, that's a good sign.
Clock
you can pass, so unit tests can pass their own fixed one. Better abstraction than a fake now.getBirthDate
hasMinimumLegalAgeOn(when: Date)
. It's obvious it takes a date. :-)