Yonel Ceruto

Posted on

# Why OOP is Your Old New Best Friend: The Date Distance Problem

Calculating the distance between two dates is relatively straightforward. There are multiple ways. Think about it for a moment... If you like functional programming, your first thought might look like this:

``````function date_distance(string \$origin, string \$destination): int
{
return strtotime(\$destination) - strtotime(\$origin);
}
``````

That's it! Definition and implementation. It gets the job done! Simple, right? But hold on... simple isn't always right!

Trying to reuse this function feels like using someone else's toothbrush: it might seem okay in a hurry, but you're not really sure what issues it might cause.

Developers usually read just the first few lines:

``````function date_distance(string \$origin, string \$destination): int
``````

If that definition is self-explanatory, nobody cares about the implementation. But is it? Is the return value in seconds, milliseconds, or something else? What about the date arguments, `\$origin` and `\$destination`? They seem to be dates, but it's not entirely clear. Confusing, right?

Ensuring clarity on what every function does, what inputs it needs, and what it returns isn't easy. Even for you, because trust me, you'll eventually forget the details of your own code.

Wouldn't it be better to have a specific return type called `Seconds`? Absolutely! It's not just any random number, and that distinction matters. We want to clearly indicate that the distance will be in `Seconds`. Let's update the function to reflect this:

``````function date_distance(DateTime \$origin, DateTime \$destination): Seconds
``````

So, instead of any datetime string, we ask for a `DateTime` object representing a valid datetime. And instead of returning just any integer, we return the number of `Seconds`.

``````class Seconds
{
public function __construct(private int \$distance) {}

// ...
}

function date_distance(DateTime \$origin, DateTime \$destination): Seconds
{
\$distance = \$destination->getTimestamp() - \$origin->getTimestamp();

return new Seconds(\$distance);
}
``````

The implementation looks a lot like the first version, with small changes. We're using objects and methods instead of scalar values and functions! The responsibility of parsing the datetime is handled by requiring a `DateTime` instance. Nice! And I'm completely sure about the return type, no doubts, it's in seconds.

The `Seconds` object isn't just a data container; it includes behavior through methods. You can add methods for time unit conversions like `toMinutes()` or `toDays()`, and maybe an `asAbsolute()` method to always return a positive distance. Super!

``````function date_distance(DateTime \$origin, DateTime \$destination): Seconds
{
\$distance = \$destination->getTimestamp() - \$origin->getTimestamp();

return new Seconds(\$distance);
}
``````

Any other improvements? Let's look at the usage snippet:

``````\$seconds = date_distance(\$dateA, \$dateB);
``````

In our heads, it reads as "Compute the date distance between date A and date B in seconds," which is correct, but it could simply be "Compute the date A distance to date B in seconds." Still pondering? There's a principle that has always helped me reason about object-oriented solutions: "Tell, Don't Ask."

"Tell, Don't Ask" principle states that Object-orientation combines data with behavior (methods). Instead of requesting data from an object, we should tell the object what to do.

In this sense, we should never create a global function to solve a problem unless there is no relevant Object concept around it. So, this is what we want to achieve now:

``````\$seconds = \$dateA->distanceTo(\$dateB);
``````

The `distanceTo()` method doesn't exist in the `DateTime` class, but we can extend `DateTime` and create our own `DatePoint` class:

``````class DatePoint extends DateTime
{
public function distanceTo(self \$destination): Seconds
{
\$distance = \$destination->getTimestamp() - \$this->getTimestamp();

return new Seconds(\$distance);
}
}
``````

Note the `self \$destination` argument and the `\$this->getTimestamp()` call. This approach keeps the function relevant to the `DatePoint` object, ensuring that the calculation of the distance between two dates is neatly encapsulated within the object-oriented design.

This way, we leverage the full power of object-oriented programming by bundling data and behavior together.

As a challenge, I invite you to develop a complete solution that covers all the issues explained here, including validation constraints around the origin and destination dates.

If you enjoyed this content, please leave a like—it motivates me to write more. Also, share your feedback if you have a different perspective or want to elaborate on this topic.

Ciao!

david duymelinck

I understand you want to demonstrate the power of OOP.

I think the function is fine if you change the name to date_strings_to_seconds.I know it is better to make a function name more generic for future use, but sometimes more context is better.

I love return type objects, but naming an object Seconds just to increase the readabilty of the code feels like a step to far for me. I would use the DateInterval class.

Another thing with your type hinted example is the prep work the code needs to do before calling the function after the change.

``````// from
function date_distance('+ 1 day', 'now');
// to
function date_distance(new  DateTime('+1 day'), new DateTime());
``````

If the function is used many times, this is going to be a painful refactor.

While OOP is a good tool, don't use it for everything.

Michael

Thank you. Very valuable and well written.
I often see a lot of code in the second version in projects. It's great that you share something like this with us. keep up the good work.