-
|
- SOLID Principles |
Hardcoding. Have your cake and eat it
One of the core principles of software that is drummed into new developers is 'Don't Hardcode anything'. What if I told you that it's OK to hardcode things? What if I told you that in some circumstances hardcoding is the RIGHT way to proceed?.
As a junior developer, one of the key principles that were drummed into me was that hardcoding values is a BAD thing (amongst many other principles). But then more often than not, the pressures of deadlines, project managers and product owners don’t afford us the luxury of building an engineered solution the first time around. So we revert to simply hardcoding just to keep everyone happy and deliver a feature. What no one ever mentions is that there are two ways to hardcode values: the RIGHT way and the WRONG way.
I’ve seen developers new in their career, and even some more experienced fall for this trap. This scenario can play out in many different ways, perhaps it’s a Proof of Concept that “will just get thrown away”. Between you and me, rarely is code simply throw away, I’ve heard of too many POC’s that find their way into production.
Put simply, If somethings worth doing its worth doing right. Now, I’m not advocating wasting time implementing an over-engineered solution to prove a concept, or when time constraints (and PMs) don’t allow. What I am saying though is that when done correctly, we can have our cake and eat it too.
The WRONG way
Consider the following scenario: A junior developer has been tasked to implement a shipping cost calculator. Because of the time constraints, and the small target market the Product Owner is insisting that that solution must be simple, and hardcoding is fine, because “we can throw it away and do it properly when we have time”.
Being a junior, and somewhat naive developer they go ahead and implement the solution as follows:
public class ShippingService
{
//As per requirements from Product Owner, Hardcoding countries here, as
//These won't change anytime soon, and we don't have time to create a new database,
//and service and the added maintenance overhead.
public Dictionary<string,decimal> countries = new Dictionary<string, decimal>
{
{"au",15M},
{"us", 10M}
};
public decimal CalculateShipping(string country)
{
if (countries.TryGetValue(country.ToLower(), out var cost))
{
return cost;
}
throw new Exception("Country not implemented yet");
}
}
Clearly, the developer knows this isn’t the right thing to do, so they spent more time writing a comment justifying their actions… Now, what they don’t realise is that: a. The sales team has sold this feature to another 10 regions b. The Product Owner has identified another 10 different usages for country data, and not all of them are in the shipping service. Oh, and they’ll only tell you about them 1 at a time, over the next few months, to not overwhelm you. c. The backlog is so jam-packed there won’t be any time to go back and refactor any of the Country data anytime soon. Especially when the countries dictionary is so heavily utilised.
The RIGHT way
Luckily a more seasoned developer has had a chance to review the pull-request and offers the following solution:
public class Country
{
public string Name {get;set;}
public decimal ShippingRate {get;set;}
}
public interface ICountryData
{
Country GetCountryByCode(string code);
}
public class CountryData: ICountryData
{
//Hardcoded for now, Ideally provided by database
public Dictionary<string,Country> countries = new Dictionary<string, Country>
{
{"au",new Country{Name = "Australia", ShippingRate=15M}},
{"us",new Country{Name = "United States", ShippingRate=10M}}
};
public Country GetCountryByCode(string code)
{
if (countries.TryGetValue(code, out var country))
{
return country;
}
return null;
}
}
public class ShippingService
{
private ICountryData _countryData;
public ShippingService(ICountryData countryData)
{
_countryData = countryData;
}
public decimal CalculateShipping(string country)
{
var result = _countryData.GetCountryByCode(country);
if (result == null)
{
return result.ShippingRate;
}
throw new Exception("Country not implemented yet");
}
}
Notice that this extra work doesn’t cost much more to write. It doesn’t avoid the hardcoding. It doesn’t worry about databases or external dependencies. It just does the job, in a more ideal way.
Why is this a better solution
Adhering more closely to the SOLID principles we are working toward a more complete solution, as opposed to just solving today’s problem.
We are adhering to the Single Responsibility Principle, whereby the Shipping service isn’t concerned about the storage of country data.
We use the Interface Segregation Principle, along with Dependency Inversion Principle for the ‘ICountryData’ so that at any point in the future (when the budget allows) replacing the implementation won’t affect any of the calling code.
Importantly, the ICountryData interface, and even the Country class, aren’t fully defined just yet. That’s OK. No doubt these contracts will grow and evolve as the system grows. At least now we have somewhere to add this information.
Conclusion
When used in this fashion, hardcoding data can aid in the rapid prototyping of very complex systems. Instead of spending time and effort working on complex algorithms, and storage mechanisms etc, we should be looking to abstract out the complexity behind an interface and hardcode the simplest results. This way, we can move on with the rest of the system, without being blocked by some complexity that is not part of our immediate concern. Kick the can down the road, so to speak.
-
|
- SOLID Principles |