I think that a lot of accidental complexity in software is produced by not picking the simplest abstraction for the job. Let me lead with an example: Consider this code from a code generator that generates C++ code:
std::ostringstream extra_properties;
if (!attribute.unit.empty())
{
extra_properties << fmt::format("\n properties.set_unit(\"{0}\");", attribute.unit);
}
if (!attribute.min_value.empty())
{
extra_properties << fmt::format("\n properties.set_min_value(\"{0}\");", attribute.min_value);
}
if (!attribute.max_value.empty())
{
extra_properties << fmt::format("\n properties.set_max_value(\"{0}\");", attribute.max_value);
}
It has a lot of ugly duplication: basically everything but the method names and values. So, how do we get rid of the duplication? Just a couple of years ago, I would probably have used a function for that:
void property_snippet(std::ostringstream& str, std::string const& method_name, std::string const& value)
{
if (value.empty())
return;
str << fmt::format("\n properties.{0}(\"{1}\");", method_name, value);
}
And then turn the call site code into:
property_snippet(extra_properties, "set_unit", attribute.unit);
property_snippet(extra_properties, "set_min_value", attribute.min_value);
property_snippet(extra_properties, "set_max_value", attribute.max_value);
Back then, I would have said that this is a definite improvement, but nowadays I am not so sure anymore. The call-site is a lot more concise, but we still have about half its code duplicated: the first half of each line. The additional function adds lots of complexity that is not necesarily offset by the gain at the call-site: the declaration with all the parameters. And the code gets separated, which is only really good if the function does a little bit more than this one.
This variant can, however, be made simpler with lambdas that capture extra_properties
instead of passing it each time. While that is a better solution, I would argue that function objects and capturing are not necessarily simple either, so this only makes second place.
Nowdays, my first go-to abstraction is an in-place list and a loop:
std::tuple<char const*, std::string> methods_and_values[] = {
{"set_unit", attribute.unit},
{"set_min_value", attribute.min_value},
{"set_max_value", attribute.max_value},
};
for (auto [method_name, value] : methods_and_values)
{
if (value.empty())
continue;
extra_properties << fmt::format("\n properties.{0}(\"{1}\");", method_name, value);
}
For me, this has the added benefit that is clearly separates the ‘inert’ data part of the code and the ‘active’ transformation. While this example is C++, this works in almost languages that I know of, even such arcane beasts as Xbase++.
I really like your thought process and the result. It also opens up the possibility to replace the loop with some functional construct like map…