A new class and a change to 1 line of code, and I was back up and running!Here's the extent of the new code. First a new class:
Then I just had to swap out one line of code to use the new provider:
And that's it. Let's look at some details (and planning) that made this possible.
Never a Good Time for Bad News
I sat down this afternoon to make some changes to my application that controls the lights and air conditioning in my apartment. Earlier in the week, I changed the persistent file format over from CSV to JSON (I'll write about this in a future article). Then I started working on a visual editor for the schedule to make things easier. I tried to continue that today when I came across my problem.
If you'd like a history of the application up to this point, check out this series of articles: Rewriting a Legacy Application (which also includes links to the GitHub project).
I fired up my editor application to see what it looked like, and it didn't come up. It just hung. This was a surprise since it was working just fine yesterday. But there is one thing that I don't have control over: the service call to get sunset and sunrise times.
After debugging through the application, I found the problem was the service. I tried to service directly and got the following message:
That's not something you like to come across. So I had to fix this before I could go on.
Alternate Calculation Available
Back when I was working on the time calculations, Matt Johnson mentioned a NuGet package that would possibly work for this. I figured that I would look into this if the service became an issue.
Well, the service became an issue. So, I took a closer look at Solar Calculator. And it turned out to be much easier to use than manually parsing the service data and converting things to local time.
I copied the sample code in the documentation and plugged in the numbers that I needed. The result was a pretty simple class (compared to the 80 lines of code I had in the provider with the service):
The SolarTimes object has what I need, and I just pass in the date and location (latitude/longitude) similar to the parameters of the service. But as you can see the parsing is minimal.
Note: I haven't looked at this code very closely; I was primarily focused on getting things working again so I could continue development. I'm pretty sure there is an easier way to do the time conversions (such as using "local" time rather than the time zone). But this code works and isn't horrible, so it gets to stay for now.
[Update: 5/6/2015: I did simplify this code a bit (and learned about DateTimeKind). Check it out: Fun with DateTime.]
Loose Coupling Saves the Day!
Writing a new class to do a piece of functionality is never the hard part. The hard part is trying to incorporate it into the application. Fortunately, I had planned for this.
Since I didn't have control over the service, I didn't want to rely on it too heavily. So when I created the sunset provider functionality, I first made sure that I had an interface for it.
Since I had this interface, that meant that I could program against the abstraction (the interface) in the main part of my code. (BTW, I really like Interfaces for exactly this reason.)
The other thing I did was use Dependency Injection. My schedule object has a dependency on a sunset provider -- it needs to know how to get the sunset and sunrise times. But rather than hard-coding that dependency, I inject the dependency through a property. (BTW, I really like Dependency Injection as well.)
That meant that when I did a "Find In Files" to search for the old provider (SunriseSunsetOrg), I found exactly 1 match. And it was really easy to change that out with the new provider (SolarCalculator):
This uses a pattern known as Property Injection. We have a class with an ISunsetProvider property. If we do nothing, then we get the default value (which is SolarCalculator in this case), but we're still able to assign a different value to this property for testing purposes.
Everything Else Still Works
So after changing this 1 line of code, the application now uses the new, working sunset provider. In addition, all of the unit tests still pass. (The reason the tests still pass is because they mocked the behavior of the service rather than calling it directly.)
If you don't believe that this is all that I changed, feel free to check out the GitHub commit: Added new sunrise/sunset provider (old service discontinued).
This shows 5 files modified: 2 files are project related from adding the new class and NuGet packages; 1 file is the new provider class; 1 file is the property injection update; and 1 file is a test console app to do a sanity check of the functionality.
When I put together an application (or add functionality), I pay close attention to the things that are likely to change in the future. In this case, I strongly suspected that I would want to change the sunset/sunrise calculation at some point. Because of this, I added a seam to my code -- a place to make it really easy to swap out one piece of functionality for another.
Adding the seam was pretty easy: just a combination of interfaces (abstraction) and dependency injection (loose coupling).
It feels really good to be able to take advantage of the loose coupling. Now I feel a lot better for having put it in to begin with. In addition, the new provider uses a calculation on the local machine so I no longer need a network connection to get that information.
Loose Coupling For the Win!