The Tester-Doer pattern is equivalent to the Try-Parse idiom; both are equivalent to Maybe.

This article is part of a series of articles about software design isomorphisms. An isomorphism is when a bi-directional lossless translation exists between two representations. Such translations exist between the Tester-Doer pattern and the Try-Parse idiom. Both can also be translated into operations that return Maybe.

Isomorphisms between Tester-Doer, Try-Parse, and Maybe.

Given an implementation that uses one of those three idioms or abstractions, you can translate your design into one of the other options. This doesn't imply that each is of equal value. When it comes to composability, Maybe is superior to the two other alternatives, and Tester-Doer isn't thread-safe.

Tester-Doer #

The first time I explicitly encountered the Tester-Doer pattern was in the Framework Design Guidelines, which is from where I've taken the name. The pattern is, however, older. The idea that you can query an object about whether a given operation would be possible, and then you only perform it if the answer is affirmative, is almost a leitmotif in Object-Oriented Software Construction. Bertrand Meyer often uses linked lists and stacks as examples, but I'll instead use the example that Krzysztof Cwalina and Brad Abrams use:

ICollection<int> numbers = // ...
if (!numbers.IsReadOnly)
    numbers.Add(1);

The idea with the Tester-Doer pattern is that you test whether an intended operation is legal, and only perform it if the answer is affirmative. In the example, you only add to the numbers collection if IsReadOnly is false. Here, IsReadOnly is the Tester, and Add is the Doer.

As Jeffrey Richter points out in the book, this is a dangerous pattern:

"The potential problem occurs when you have multiple threads accessing the object at the same time. For example, one thread could execute the test method, which reports that all is OK, and before the doer method executes, another thread could change the object, causing the doer to fail."
In other words, the pattern isn't thread-safe. While multi-threaded programming was always supported in .NET, this was less of a concern when the guidelines were first published (2006) than it is today. The guidelines were in internal use in Microsoft years before they were published, and there wasn't many multi-core processors in use back then.

Another problem with the Tester-Doer pattern is with discoverability. If you're looking for a way to add an element to a collection, you'd usually consider your search over once you find the Add method. Even if you wonder Is this operation safe? Can I always add an element to a collection? you might consider looking for a CanAdd method, but not an IsReadOnly property. Most people don't even ask the question in the first place, though.

From Tester-Doer to Try-Parse #

You could refactor such a Tester-Doer API to a single method, which is both thread-safe and discoverable. One option is a variation of the Try-Parse idiom (discussed in detail below). Using it could look like this:

ICollection<int> numbers = // ...
bool wasAdded = numbers.TryAdd(1);

In this special case, you may not need the wasAdded variable, because the original Add operation never returned a value. If, on the other hand, you do care whether or not the element was added to the collection, you'd have to figure out what to do in the case where the return value is true and false, respectively.

Compared to the more idiomatic example of the Try-Parse idiom below, you may have noticed that the TryAdd method shown here takes no out parameter. This is because the original Add method returns void; there's nothing to return. From unit isomorphisms, however, we know that unit is isomorphic to void, so we could, more explicitly, have defined a TryAdd method with this signature:

public bool TryAdd(T item, out Unit unit)

There's no point in doing this, however, apart from demonstrating that the isomorphism holds.

From Tester-Doer to Maybe #

You can also refactor the add-to-collection example to return a Maybe value, although in this degenerate case, it makes little sense. If you automate the refactoring process, you'd arrive at an API like this:

public Maybe<Unit> TryAdd(T item)

Using it would look like this:

ICollection<int> numbers = // ...
Maybe<Unit> m = numbers.TryAdd(1);

The contract is consistent with what Maybe implies: You'd get an empty Maybe<Unit> object if the add operation 'failed', and a populated Maybe<Unit> object if the add operation succeeded. Even in the populated case, though, the value contained in the Maybe object would be unit, which carries no further information than its existence.

To be clear, this isn't close to a proper functional design because all the interesting action happens as a side effect. Does the design have to be functional? No, it clearly isn't in this case, but Maybe is a concept that originated in functional programming, so you could be misled to believe that I'm trying to pass this particular design off as functional. It's not.

A functional version of this API could look like this:

public Maybe<ICollection<T>> TryAdd(T item)

An implementation wouldn't mutate the object itself, but rather return a new collection with the added item, in case that was possible. This is, however, always possible, because you can always concatenate item to the front of the collection. In other words, this particular line of inquiry is increasingly veering into the territory of the absurd. This isn't, however, a counter-example of my proposition that the isomorphism exists; it's just a result of the initial example being degenerate.

Try-Parse #

Another idiom described in the Framework Design Guidelines is the Try-Parse idiom. This seems to be a coding idiom more specific to the .NET framework, which is the reason I call it an idiom instead of a pattern. (Perhaps it is, after all, a pattern... I'm sure many of my readers are better informed about how problems like these are solved in other languages, and can enlighten me.)

A better name might be Try-Do, since the idiom doesn't have to be constrained to parsing. The example that Cwalina and Abrams supply, however, relates to parsing a string into a DateTime value. Such an API is already available in the base class library. Using it looks like this:

bool couldParse = DateTime.TryParse(candidate, out DateTime dateTime);

Since DateTime is a value type, the out parameter will never be null, even if parsing fails. You can, however, examine the return value couldParse to determine whether the candidate could be parsed.

In the running commentary in the book, Jeffrey Richter likes this much better:

"I like this guideline a lot. It solves the race-condition problem and the performance problem."
I agree that it's better than Tester-Doer, but that doesn't mean that you can't refactor such a design to that pattern.

From Try-Parse to Tester-Doer #

While I see no compelling reason to design parsing attempts with the Tester-Doer pattern, it's possible. You could create an API that enables interaction like this:

DateTime dateTime = default(DateTime);
bool canParse = DateTimeEnvy.CanParse(candidate);
if (canParse)
    dateTime = DateTime.Parse(candidate);

You'd need to add a new CanParse method with this signature:

public static bool CanParse(string candidate)

In this particular example, you don't have to add a Parse method, because it already exists in the base class library, but in other examples, you'd have to add such a method as well.

This example doesn't suffer from issues with thread safety, since strings are immutable, but in general, that problem is always a concern with the Tester-Doer anti-pattern. Discoverability still suffers in this example.

From Try-Parse to Maybe #

While the Try-Parse idiom is thread-safe, it isn't composable. Every time you run into an API modelled over this template, you have to stop what you're doing and check the return value. Did the operation succeed? Was should the code do if it didn't?

Maybe, on the other hand, is composable, so is a much better way to model problems such as parsing. Typically, methods or functions that return Maybe values are still prefixed with Try, but there's no longer any out parameter. A Maybe-based TryParse function could look like this:

public static Maybe<DateTime> TryParse(string candidate)

You could use it like this:

Maybe<DateTime> m = DateTimeEnvy.TryParse(candidate);

If the candidate was successfully parsed, you get a populated Maybe<DateTime>; if the string was invalid, you get an empty Maybe<DateTime>.

A Maybe object composes much better with other computations. Contrary to the Try-Parse idiom, you don't have to stop and examine a Boolean return value. You don't even have to deal with empty cases at the point where you parse. Instead, you can defer the decision about what to do in case of failure until a later time, where it may be more obvious what to do in that case.

Maybe #

In my Encapsulation and SOLID Pluralsight course, you get a walk-through of all three options for dealing with an operation that could potentially fail. Like in this article, the course starts with Tester-Doer, progresses over Try-Parse, and arrives at a Maybe-based implementation. In that course, the example involves reading a (previously stored) message from a text file. The final API looks like this:

public Maybe<string> Read(int id)

The protocol implied by such a signature is that you supply an ID, and if a message with that ID exists on disc, you receive a populated Maybe<string>; otherwise, an empty object. This is not only composable, but also thread-safe. For anyone who understands the universal abstraction of Maybe, it's clear that this is an operation that could fail. Ultimately, client code will have to deal with empty Maybe values, but this doesn't have to happen immediately. Such a decision can be deferred until a proper context exists for that purpose.

From Maybe to Tester-Doer #

Since Tester-Doer is the least useful of the patterns discussed in this article, it makes little sense to refactor a Maybe-based API to a Tester-Doer implementation. Nonetheless, it's still possible. The API could look like this:

public bool Exists(int id)

public string Read(int id)

Not only is this design not thread-safe, but it's another example of poor discoverability. While the doer is called Read, the tester isn't called CanRead, but rather Exists. If the class has other members, these could be listed interleaved between Exists and Read. It wouldn't be obvious that these two members were designed to be used together.

Again, the intended usage is code like this:

string message;
if (fileStore.Exists(49))
    message = fileStore.Read(49);

This is still problematic, because you need to decide what to do in the else case as well, although you don't see that case here.

The point is, still, that you can translate from one representation to another without loss of information; not that you should.

From Maybe to Try-Parse #

Of the three representations discussed in this article, I firmly believe that a Maybe-based API is superior. Unfortunately, the .NET base class library doesn't (yet) come with a built-in Maybe object, so if you're developing an API as part of a reusable library, you have two options:

  • Export the library's Maybe<T> type together with the methods that return it.
  • Use Try-Parse for interoperability reasons.
This is the only reason I can think of to use the Try-Parse idiom. For the FileStore example from my Pluralsight course, this would imply not a TryParse method, but a TryRead method:

public bool TryRead(int id, out string message)

This would enable you to expose the method in a reusable library. Client code could interact with it like this:

string message;
if (!fileStore.TryRead(50, out message))
    message = "";

This has all the problems associated with the Try-Parse idiom already discussed in this article, but it does, at least, have a basic use case.

Isomorphism with Either #

At this point, I hope that you find it reasonable to believe that the three representations, Tester-Doer, Try-Parse, and Maybe, are isomorphic. You can translate between any of these representations to any other of these without loss of information. This also means that you can translate back again.

While I've only argued with a series of examples, it's my experience that these three representations are truly isomorphic. You can always translate any of these representations into another. Mostly, though, I translate into Maybe. If you disagree with my proposition, all you have to do is to provide a counter-example.

There's a fourth isomorphism that's already well-known, and that's between Maybe and Either. Specifically, Maybe<T> is isomorphic to Either<Unit, T>. In Haskell, this is easily demonstrated with this set of functions:

toMaybe :: Either () a -> Maybe a
toMaybe (Left ()) = Nothing
toMaybe (Right x) = Just x
 
fromMaybe :: Maybe a -> Either () a
fromMaybe Nothing = Left ()
fromMaybe (Just x) = Right x

Translated to C#, using the Church-encoded Maybe together with the Church-encoded Either, these two functions could look like the following, starting with the conversion from Maybe to Either:

// On Maybe:
public static IEither<UnitT> ToEither<T>(this IMaybe<T> source)
{
    return source.Match<IEither<UnitT>>(
        nothing: new Left<UnitT>(Unit.Value),
        just: x => new Right<UnitT>(x));
}

Likewise, the conversion from Either to Maybe:

// On Either:
public static IMaybe<T> ToMaybe<T>(this IEither<UnitT> source)
{
    return source.Match<IMaybe<T>>(
        onLeft: _ => new Nothing<T>(),
        onRight: x => new Just<T>(x));
}

You can convert back and forth to your heart's content, as this parametrised xUnit.net 2.3.1 test shows:

[Theory]
[InlineData(42)]
[InlineData(1337)]
[InlineData(2112)]
[InlineData(90125)]
public void IsomorphicWithPopulatedMaybe(int i)
{
    var expected = new Right<Unitint>(i);
    var actual = expected.ToMaybe().ToEither();
    Assert.Equal(expected, actual);
}

I decided to exclude IEither<Unit, T> from the overall theme of this article in order to better contrast three alternatives that may not otherwise look equivalent. That IEither<Unit, T> is isomorphic to IMaybe<T> is a well-known result. Besides, I think that both of these two representations already inhabit the same conceptual space. Either and Maybe are both well-known in statically typed functional programming.

Summary #

The Tester-Doer pattern is a decades-old design pattern that attempts to model how to perform operations that can potentially fail, without relying on exceptions for flow control. It predates mainstream multi-core processors by decades, which can explain why it even exists as a pattern in the first place. At the time people arrived at the pattern, thread-safety wasn't a big concern.

The Try-Parse idiom is a thread-safe alternative to the Tester-Doer pattern. It combines the two tester and doer methods into a single method with an out parameter. While thread-safe, it's not composable.

Maybe offers the best of both worlds. It's both thread-safe and composable. It's also as discoverable as any Try-Parse method.

These three alternatives are all, however, isomorphic. This means that you can refactor any of the three designs into one of the other designs, without loss of information. It also means that you can implement Adapters between particular implementations, should you so desire. You see this frequently in F# code, where functions that return 'a option adapt Try-Parse methods from the .NET base class library.

While all three designs are equivalent in the sense that you can translate one into another, it doesn't imply that they're equally useful. Maybe is the superior design, and Tester-Doer clearly inferior.

Next: Builder isomorphisms.



Wish to comment?

You can add a comment to this post by sending me a pull request. Alternatively, you can discuss this post on Twitter or somewhere else with a permalink. Ping me with the link, and I may respond.

Published

Monday, 15 July 2019 07:35:00 UTC

Tags



"Our team wholeheartedly endorses Mark. His expert service provides tremendous value."
Hire me!
Published: Monday, 15 July 2019 07:35:00 UTC