Using Efficiency to Disambiguate Design Decisions
I actually let computational efficiency resolve my design decisions which might sound bad when designing interfaces for humans to use, but I use it to resolve cases of ambiguity where the pros and cons of two or more design decisions make it difficult to decide which trade-offs we accept when only considering human factors.
Consider this as a basic example:
// Locks or unlocks a mutex.
void Mutex::lock(bool on);
In practice I don't think this is so bad. It would reduce the number of methods the Mutex
provides, potentially making its interface a little bit easier to comprehend, the implementation of above function should still be reasonably straightforward, and the client code, even when they don't need to branch, looks reasonably straightforward with a very symmetrical use pattern:
mutex.lock(true);
// do stuff
mutex.lock(false);
We can then compare it to the straightforward alternative:
// Locks the mutex.
void Mutex::lock();
// Unlocks the mutex.
void Mutex::unlock();
At which point our interface gets a little bit bigger but with two functions that are even simpler individually than the one before. At this point our client code would look like this:
mutex.lock();
// do stuff
mutex.unlock();
... which, in my humble opinion, is not a hands-down improvement across the board. It's not necessarily that much more straightforward in terms of the maintainability of Mutex
or in terms of the client code using it. Of course it yields two simpler functions in exchange for one modestly complex, but the intellectual overhead of maintaining and using code is often as much proportional to the number of functions there are as the complexity of what the functions do.
From a maintenance standpoint an interface that offers 100 simpler
methods averaging 5 LOC each isn't always going to be easier to
maintain than one with 30 methods averaging 17 LOC each. The former
isn't necessarily going to be easier to use and comprehend either.
There's a balancing act between the simplicity of each and every thing
and the number of things that have to exist. A system with 100
uber-simple things isn't always easier to maintain and use than one
with 30 slightly meatier things that still have clear, comprehensible
responsibilities, especially if the former version has more
interactions between each and every thing.
But then if I let computational efficiency guide my decisions, it resolves the design for me:
// Locks the mutex.
void Mutex::lock();
// Unlocks the mutex.
void Mutex::unlock();
... because at this point the two functions can be implemented without branching, and in many use cases the client might not benefit at all from the branching involved with accepting a boolean parameter. So voila, there's my final design.
Minimalism
The design I least like is one that tries to provide both solutions:
// Locks the mutex.
void Mutex::lock();
// Unlocks the mutex.
void Mutex::unlock();
// Locks or unlocks a mutex.
void Mutex::lock(bool on);
... because this leads to the biggest interface possible with the most functions for clients to learn how to use and for the developers to maintain. Minimalism is thrown out the window.
People who embrace this type of mindset often end up designing interfaces whose functions and documentation scroll off the map in spite of the interface ultimately not doing that much.
Convenience of Use
Convenience of use is bottom priority for me. If using an interface requires two or three times as much trivial code as ideally possible if the interface provided more functions, I don't go back and change the design and add more functions to the interface. That violates the things I prioritize most which have more to do with minimalism and stability (designing things which find few or no reasons to change which is highly correlated with minimalism).
I'll defer any responsibility of making the interface more convenient to use in all possible scenarios to the people using them to come up with their own convenience helper library or whatever instead of convoluting my designs with their efforts towards convenience. Don't touch my minimalist interfaces and add 20 functions to make them more convenient and blow up the amount of code required to unit test unless there's actually crucial functionality missing which have nothing to do with convenience.
So anyway, that's how I go about thinking about it. It might be handy for some people. For you, I'd say don't worry if the code that uses your design has to write a bunch of if/else
statements. Let them come up with a way to make that more convenient on their own. Even if you're both the user and designer of your interface, be a schizo.
Don't let all the little temptations you get from using your interface to crunch code down usage code to a minimum leak into the design of the interface. From the design side, be like Linus Torvalds maintaining the Linux kernel. You don't want to just accept random pull requests from everyone who wants to streamline their code using the kernel. Look more for reasons not to change/expand the design than to constantly change and expand the design. That is the mindset required of a minimalist. Focus on providing genuine needs of the users, not the endless wishlist of wants, since just flawlessly covering the needs is difficult enough.
Polymorphism
Of course you can also use polymorphic solutions. That could include like a table of function pointers or delegates or functors or whatever your language provides in this context. Instead of:
boolean b = ...;
if (b)
myFunctionTrue();
else
myFunctionFalse();
You might do:
boolean b = ...; // some external input, like from a file
functions[b](); // use the boolean as a key for a table
// of functions
Or something to this effect:
boolean b = ...; // some external input, like from a file
objects[b].some_method(); // use the boolean to determine what object
// with overriden functionality to use
// or potentially even create and use through a
// factory.
But for the lower object-oriented solution especially, you shouldn't use inheritance and polymorphism as a low-level branching mechanism. Use it when the abstractions you model actually make sense and provide benefits at a scale much wider than avoiding an if/else
. Take it easy with that stuff.
myFunctionTrue
andmyFunctionFalse
does not use any boolean parameters tpo determine behavior.