Looking for maybe some design advice here. I've run into this issue a couple of times now and feel like my approaches have been sub par up until now.
I've got for example a method that I want to create called GetConversation. Now GetConversation will return the same data whether you give it an id or an alternate address parameter. Just a matter of preference or whatever data you have available that makes it a little more flexible and easy to use. This is great but the problem I'm running into is that these are both string parameters. Ideally it would look something like this,
public object GetConversation(string id)
{
// Get Conversation by id
}
public object GetConversation(string address)
{
// Get Conversation by address
}
Now of course this is going to fail because of ambiguity. The compiler doesn't know which function to access when the user drills into GetConversation.
This brings me to my second approach at a flexible solution,
public object GetConversation(string id, string address)
{
// Get Conversation by id or address
if (id != null)
{
// Get by id
}
else if (address != null)
{
// Get by address
}
return null;
}
But now I am starting to make a preference of always searching by id first. Isn't necessarily a bad thing but I feel like this just isn't the most cohesive solution and I have to explicitly set a return outside of my checks for failures on both checks.
Any thoughts on a better approach?
In this particular scenario, I would rename the methods to be GetConversationById
and GetConversationByAddress
, to avoid the ambiguity.
If the scenario was slightly different, and the methods were genuine overloads with different implementations, I'd use a strategy pattern with an abstract factory to create the strategies.
An alternative approach you may wish to consider is to introduce new types1. So you would have an Address
type and an Id
type.
At a minimum, you'd expect these new types to support (explicit) conversions to/from string
and implement all of the expected equality and comparison functions2. You then use these types for this function and 1) you're allowed, because now the types are different and 2) It's now clear how the caller makes clear which one they're calling:
var conv = thing.GetConversation((Address)"here");
Now, if that's the only place you'll use these types, it probably doesn't seem worth the effort - but are there more uses for these types? Do Id
s or Address
es have more structure beyond merely being string
s? (Of course, if it turns out, say, the Address
es must always be valid Uri
s then it turns out that we didn't need to do all of this work anyway, since somebody already wrote that class for us). If so, we might consider static TryParse
methods to allow users to safely test strings.
The more places you can use these types, the fewer times you'll accidentally pass an id
when an address
was expected, and vice versa.
You also have the opportunity to consider which operations of the underlying type you wish to expose through your wrappers. Sure, concatenating and splitting strings makes sense, but would it make sense to concatenate two id
s or two address
es together? (Or, even more bizarre, concatenating an id
and an address
?). So you only expose the operations that make sense for these new types, and you're steering your consumers into making fewer possible misuses. By providing the explicit operator back to string
, there's always a "relief valve" if a consumer does find a need to do something you hadn't thought useful.
1There are some overheads in introducing such wrappers. So I'd not suggest doing this on the hot path for something performance sensitive, but in most situations this would not apply.
2Yes, it does get tedious writing all of the usual boilerplate to implement IEquatable<T>
and IComparable<T>
but you can make a snippet for this in VS.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With