2

I have a class Transaction that can have one of two states, Unpublished and Published. I would like to be able to limit what methods can be called on this class based on its state such that the compiler throws an error if the wrong method is called. Every method changes the state in a predictable way, so there is never any ambiguity at compile time about what state the object is in. I've seen this called an "Indexed Monad", but I'm not sure whether that is actually what this is called.

Here's a (contrived) example:

class Transaction
{
    public enum State
    {
        Unpublished,
        Published
    }

    public State curState = State.Unpublished;

    public void GetData()
    {
        if (curState == State.Unpublished)
        {
            throw new InvalidOperationException("cannot get data from unpublished transaction!");
        }

        Console.WriteLine("got data from transaction");
    }

    public void Publish()
    {
        if (curState == State.Published)
        {
            throw new InvalidOperationException("cannot publish already published transaction!");
        }

        Console.WriteLine("published transaction");
        curState = State.Published;

    }
}

Transaction myTransaction = new Transaction();
myTransaction.GetData(); // Runtime error!

The problem with this is that someone could create a Transaction object and try to call the GetData() method before calling Publish(), and the compiler wouldn't complain, even though it should be possible to infer what methods are available at compile time.

Is the only way to do this to return a new object of a different class in the Publish() method? If so, is there any way to stop someone from trying to call Publish() again on the old object, like this:

UnpublishedTransaction myTransactionVariable = new UnpublishedTransaction();
PublishedTransaction myNewTransactionVariable = myTransactionVariable.Publish(); // Return a new object of a different class that doesn't have Publish().
myTransactionVariable.Publish(); // Runtime Error! Can't publish twice. Or, even worse, return another object that represents the same underlying transaction.
myNewTransactionVariable.GetData(); // *Can* get data from published transaction.

Even if there is, this way of doing it looks ugly since I have to create a new variable for the object returned by Publish().

Am I doomed to throwing runtime errors, hoping that other developers are good at predicting the state of the object and read the documentation?

Caleb Keller
  • 553
  • 3
  • 19
  • 1
    you as you have found you can do some stuff at compile time, but you will eventually run out of options and have to rely on runtime errors. You need good system tests and code coverage, thats the way to go to detect coding erros in large systems – pm100 Feb 25 '23 at 02:00
  • 1
    Look up "type state pattern". In your case there are two states, so you would model this as two separate classes (types). – Paul Dempsey Feb 25 '23 at 02:03
  • There is nothing stopping anyone from calling the same method on the same object a second time. How about designing it so that calling `Publish` a second time does nothing, or just logs a warning? I feel this is similar to how calling `Dispose` multiple times does nothing. – Sweeper Feb 25 '23 at 02:09
  • @Sweeper That would definitely work, I was just wondering if there's a way to prevent this at compile time so I don't have to write edge cases into the methods themselves. Thanks! – Caleb Keller Feb 25 '23 at 02:17
  • You can start learning [how to write an analyzer](https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/tutorials/how-to-write-csharp-analyzer-code-fix). – shingo Feb 25 '23 at 04:27

1 Answers1

2

In short, the answer is no, but if you want a compile-time solution, there's a way to do it.

Basically, what you're talking about is a finite state machine.

A simplistic state machine (there are more sophisticated ways using a library such as Stateless) would be to model each state and only offer the methods you allow on each state. Taking your code, you can do something like

public class InitialState // or name the class Transaction if you like
{
    public PublishedState Publish()
    {
        Console.WriteLine("published transaction");
        return new PublishedState(...);
    }
}

public class PublishedState
{
    public FinalState GetData()
    {
        Console.WriteLine("got data from transaction");
        return new FinalState();
    }
}

public class FinalState
{}

In this model

  • each state only presents the valid methods to call. This means you prevent out of order calls, and you prevent duplicate calls (if desired).
  • each method returns a state that allows only its valid methods
  • each method performs the action and then the state change
  • no state change enum is necessary, and so no base class is necessary. The state is now modeled as a class, not an enum that needs checking at runtime.

So, if you initially allow your users to only get an InitialState, then, at compile time, they can only call the methods you allow.

If you want to make it more sophisticated and share code between states or share transitions, you can add base classes or interfaces into the mix.

If you want to lock down the states further, for example preventing a developer from instantiating a PublishedState first, then make all your classes internal with a public interface, or keep the classes public but make their constructor internal.

Side Note: Incidentally, InvalidOperationException occurs in many cases in the BCL (loosely, the System libraries). It's quite honestly a shortcut, and an antipattern. That said, it can be a reasonable shortcut, but should generally be avoided. Statefulness in APIs is discouraged, but can sometimes be hard to maintain, especially if those APIs are APIs in the "web" sense of the term.

Kit
  • 20,354
  • 4
  • 60
  • 103
  • Thanks! Out of curiosity, does the Stateless library you mentioned allow the available methods to be limited at compile time like this, or not? I looked over their documentation, but didn't see anything that indicated either way. – Caleb Keller Feb 25 '23 at 21:24
  • Effectively it does, in the sense that Stateless uses lambdas that can be supplied from a single class or wherever you want, but the client-side doesn't know anything about that. As long as you're using their API, you're not exposing "yours". – Kit Feb 26 '23 at 19:05