You need to model an expiration date. Which object will you use?
TL;DR: Model real word expiration dates with... expiration dates
Problems
Bijection Violation
Unexpected Behavior
Small Objects Missing
Solutions
- Honor the bijection and model correct behavior
Context
In many systems, the expiry date of a credit card is often represented by simply using a Date object.
This can lead to potential issues and misunderstandings, especially when dealing with operations such as comparisons and calculations based on the expiry date.
it's generally considered a better practice to represent the expiry date with an adequate object.
Sample Code
Wrong
import java.util.Date;
public class CreditCard {
private String cardNumber;
private Date expiryDate;
public CreditCard(String cardNumber, Date expiryDate) {
// Not a complete date
this.cardNumber = cardNumber;
this.expiryDate = expiryDate;
}
public boolean isExpired() {
Date currentDate = new Date();
return expiryDate.before(currentDate);
// How will it work?
// using the last day of the month?
}
}
public class CreditCard {
private String cardNumber;
private int expiryMonth;
private int expiryYear;
public CreditCard(String cardNumber, int expiryMonth, int expiryYear) {
this.cardNumber = cardNumber;
this.expiryMonth = expiryMonth;
this.expiryYear = expiryYear;
// No validations on number ranges?
}
public boolean isExpired(int currentMonth, int currentYear) {
return (expiryYear < currentYear) ||
(expiryYear == currentYear && expiryMonth < currentMonth);
}
// Inappropriate intimacy code smell
}
Right
class CreditCard {
private String number;
private MonthOfYear expiryDate;
// expiryDate is the role
// MonthOfYear is the type
}
class MonthOfYear {
private Month month;
private Year year;
// These are other small objects
public MonthOfYear(Month month, Year year) {
// You don't need to add validations since
// month is a valid month
// year is a valid year
this.month = month;
this.year = year;
}
public boolean isBeforeEndOfMonth(Date date) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
return (calendar.get(Calendar.YEAR) < year.value()) ||
(calendar.get(Calendar.YEAR) == year.value() &&
calendar.get(Calendar.MONTH) < month.value())
// Notice there are no days involved
}
// This protocol is just for MonthOfYears
public Day[] getDaysInMonth() { }
public boolean isLeapYear() { } // ...
public void iterateDays() { } // ...
}
Detection
[X] Manual
This is a design smell
Tags
- Primitive Obsession
Level
[x] Intermediate
This is not an obvious design decision
AI Generation
Actual AI assistants are not very good at creating these kinds of small objects
AI Detection
Gemini detected this is a possible smell
Conclusion
ExpiryDate explicitly separates month and year, making the code more readable and easier to understand the specific information needed for expiry.
It can also encapsulate logic specific to expiry dates, such as calculating days remaining, validating expiry, or iterating through days in the month.
While Date objects offer date manipulation functionalities, they don't inherently represent the specific concept of a credit card expiry.
Finding small objects with concrete behavior is always a difficult task.
Relations
Code Smell 177 - Missing Small Objects
Maxi Contieri ・ Nov 5 '22
More Info
The One and Only Software Design Principle
Maxi Contieri ・ Oct 13 '20
Disclaimer
Code Smells are my opinion.
Credits
Photo by CardMapr.nl on Unsplash
Thank you, Hernan Wilkinson for this tip
Standards are always out of date. That’s what makes them standards.
Alan Bennett
Software Engineering Great Quotes
Maxi Contieri ・ Dec 28 '20
This article is part of the CodeSmell Series.
Top comments (6)
That's kind of dumb question for this piece of code -> "Sample Code - Wrong".
I mean, it will work in the way the
Date
in Java works, no? It will compare seconds of the date, which is readable and also applied very well in case of Cards since they also have a expiration day.So why is it a code smell if this is just a design decision, these two design solutions you showed has its own cons and pros, while your examples are too abstract to make a clear judgement of a "code smell".
I'm generally reading every new "code smell" you're creating and this one is so doubtful.
Every time you make a design decision and break the bijection you open a door to a bad behavoir.
An expiration date IS NOT a date
you can model with a Date and will have ripple effect on wrong behavior sooner or later
I'm sorry, quite not understand, why it's bad to have a date for expiry?
I understand there cases where you may need comparing year and month separately, but isn't that a requirement rather than a design decision?
a Date has a day number
an expiring date hasn't
Since they have diferent responsibilities they should never be modeled with the same objects
The expiration date of a credit card is just less specific, but still IS a date.
So why they have different responsibilities?
Date
represents a date with millisecond precision.I understand that less precise date can't be mapped to more precise one, but what's the problem with vice versa situation?
I think this is exactly the case when it's tolerable to use
Date
since this is a standard for keeping dates.Can you provide an example that will lead to an unexpected scenario with using
Date
?it is Kind of a Date
but does not BEHAVES_AS_A Date. Therefore is not a subtype
Date represents a date with day precision.
DateTime represents a date with millisecond precision.
Credit Card Expiration Date represents a year and a month. no day involved.
it is tolerable, and a code smell bringing lots of problems
What happen if you model your expiration date and ask for dayOfMonth?
Expected: Unknown responsibility / Exception / Fail Fast