[go: up one dir, main page]

DEV Community

Cover image for Difference between state and State
Caio Ferreira
Caio Ferreira

Posted on • Edited on

Difference between state and State

Photo by Annie Spratt on Unsplash

Today we will try to solve the ambiguity in the concept of state, highlighting the differences in the two main notions about it. The post describes the nature of each one, some use cases and how they fit in the object and functional paradigms.

Introduction

On the last couple of months, I dove into the topic of State Machines and how we can design UI’s with this concept in order to provide better semantic and predictability to our application. When reading and talking about it often I have to stop and clarify which of the two ideas about state I am referring to:

  • the idea of a collection of data at a point in time
  • the idea of the representation of an entity modeled as a state machine.

For the sake of comprehension, we will use state to refer to the first and State to the last.

Requirements

We will use Typescript for our yummy examples so some familiarity with it would be good.

The state as a travel bag

The first notion we became comfortable with when learning about state in software development is the entity “travel bag”. Basically, we see a state as a collection of data in a specific point in time. Throughout the application lifecycle, this data is manipulated and altered in order to reflect the business process. For example:

    class Pizza {
        private dough: Dough; // it's an enum that could be traditional or thin
        private ingredients: Array<Ingredient>;

        // entity controls
        private isBeingnPrepared: boolean;
        private isBaking: boolean;
        private baked: boolean;

        constructor() {
            this.isBeingnPrepared = true;
            this.isBaking = false;
            this.baked = false;
        }

        // getters and setters

        async public bakePizza(): void {
            const oven = new OvenService();

            try {
                this.isBeingnPrepared = false;
                this.isBaking = true;

                await oven.bake(this);

                this.baked = true;
            } catch (error) {
                throw error;
            }
        }
    }

From this point on, the pizza state, and hence the application state, has changed, because its data was updated. However, two booleans can be arranged in four different ways, and some of them are invalid states. In the object-oriented paradigm we would avoid this by encapsulating such data in an object and modeling its operations only through methods that guarantee atomic and consistent changes.

Until this use case, our model seems to be fine. But, the time comes to implement the next step in the pizzeria flow, the delivery.

    class Pizza {
        private dough: Dough;
        private ingredients: Array<Ingredient>;

        // entity controls
        private isBeingnPrepared: boolean;
        private isBaking: boolean;
        private baked: boolean;
        private isBeingDelivered: boolean;
        private hasBeenDelivered: boolean;

        constructor() {
            this.isBeingnPrepared = true;
            this.isBaking = false;
            this.baked = false;
            this.isBeingDelivered = false;
            this.hasBeenDelivered = false;
        }

            // getters and setters

            // bake behavior

        async public deliveryPizza() {
            if (!this.baked) {
                throw new PizzaNotBakedException();
            }

            const deliveryService = new DevelieryService();

            try {
                this.isBeingnPrepared = false;
                this.isBeingDelivered = true;

                await deliveryService.send(this);
            } catch (error) {
                throw error;
            }
        }

        public notifyDelivery(wasSuccessful) {
            if(wasSuccessful) {
                this.hasBeenDelivered = true;
            }
        }
    }

What raises a flag in this code is the use of a guard condition at the start of the delivery function that checks if the pizza is baked. If not, it throws an exception. This seems really simple, and if this were the only condition, it would be fine. But, a pizza could already be left for delivery, as such, we don’t want to try to send it again. So, we add another guard condition to our function:

    class Pizza {
        private dough: Dough;
        private ingredients: Array<Ingredient>;

        // entity controls
        private isBeingPrepared: boolean;
        private isBaking: boolean;
        private baked: boolean;
        private isBeingDelivered: boolean;
        private hasBeenDelivered: boolean;

            // constructor

            // getters and setters

            // bake behavior

        async public deliveryPizza() {
            if (!this.baked) {
                throw new PizzaNotBakedException();
            }

            if (this.isBeingDelivered) {
                throw new PizzaAlreadyLeftException();
            }

            const deliveryService = new DevelieryService();

            try {
                this.isBeingPrepared = false;
                this.isBeingDelivered = true;

                await deliveryService.send(this, this.notifyDelivery);
            } catch (error) {
                throw error;
            }
        }

            // notify delivery behavior
    }

If we elaborate all the scenarios which a pizza can be, this kind of implementation with lots of branches and conditions expressed by if/else statements grows exponentially. It increases our code cyclomatic complexity and diminishes maintainability as such code is more fragile, harder to read and understand.

It gets worse when this kind of conditional start to spread across the code, as in the bake function, which needs to be updated in order to not try to bake it again.

    class Pizza {
        private dough: Dough;
        private ingredients: Array<Ingredient>;

        // entity controls
        private isBeingPrepared: boolean;
        private isBaking: boolean;
        private baked: boolean;
        private isBeingDelivered: boolean;
        private hasBeenDelivered: boolean;

        // constructor

            // getters and setters

        async public bakePizza(): void {
            if (this.baked) {
                throw new PizzaAlreadyBakedException();
            }

            const oven = new OvenService();

            try {
                this.isBeingPrepared = false;
                this.isBaking = true;

                await oven.bake(this);

                this.baked = true;
            } catch (error) {
                throw error;
            }
        }

        // delivery behavior

            // notify delivery behavior
    }

Although this kind of design serves several proposes, in special on more simple or data-centric scenarios, in fast evolution and process-centric domains it evolves on a mess of code execution paths and unsynchronized conditionals through different functions.

The state as an entity travel bag has a use and it is to carry the associated information to the model. Try to control the behavior of this entity through the same concept ends up overloading it with responsibility and creating a silent trap for our design.

The problem faced here is that the application architecture allows for invalid behavior through invalid states, and when it does eventually some use case will expose the bugs created by this freedom. Besides that, this approach takes the system invariants, in this case, the Pizza cooking flow, and scatter then inside many implementation points instead of enforcing them in the design.

Side note: if you are versed in Algebraic Data Types you can see this as a Product Type with cardinality which tends to infinity.

Representational State

Once we have the problem of control the entity information and behavior being done by the same construct, the state, our response could not be more simple: let’s break these responsibilities.

Therefore, we need a new pattern to handle our entity’s behavior.

But, the alternative pattern we propose when designing your application is not at all new. It is the State Pattern, describes in many ancient books about OO. And this books will tell you the same, that the State Pattern seeks to delegate an entity behavior to a specific implementation which is the current State and at the end of the method calculate the entity’s next State, which will now represent the entity, replacing its behaviors implementation on the fly. After all, this pattern is a translation of a state machine to the idiom of the nouns. An alternative implementation for our Pizza example can be as below:

    interface IPizza {
        bakePizza();
        deliveryPizza();
        notifyDelivery(wasSuccessful: boolean);
    }

    type Pizza = PreparingPizza | BakedPizza | DeliveringPizza | DeliveredPizza;

    class PreparingPizza implements IPizza {
        private dough: Dough; // it is an enum that could be traditional or thin
        private ingredients: Array<Ingredient>;

        constructor(dough: Dough, ingredients: Array<Ingredient>) {
            this.dough = dough;
            this.ingredients = ingredients;
        }

        // setters

        getDough() {
            return this.dough;
        }

        getIngredients() {
            return this.ingredients;
        }

        public async bakePizza(): Promise<BakedPizza> {
            const oven = new OvenService();

            try {
                await oven.bake(this);

                return new BakedPizza(this);
            } catch (error) {
                throw error;
            }
        }

        public async deliveryPizza() {
            throw new PizzaNotReadyForDelivery();
        }

        public notifyDelivery(wasSuccessful) {
            throw new PizzaNotReadyForDelivery();
        }
    }

    class BakedPizza implements IPizza {
        private dough: Dough;
        private ingredients: Array<Ingredient>;

        // constructor
        constructor(pizza: PreparingPizza) {
            this.dough = pizza.getDough();
            this.ingredients = pizza.getIngredients();
        }

        // getters and setters

        public async bakePizza(): Promise<BakedPizza> {
            throw new PizzaAlreadyBakedException();
        }

        public async deliveryPizza(): Promise<DeliveringPizza> {
            const deliveryService = new DevelieryService();

            try {
                await deliveryService.send(this);

                return new DeliveringPizza(this);
            } catch (error) {
                throw error;
            }
        }

        public notifyDelivery(wasSuccessful) {
            throw new PizzaNotLeftForDeliveryYey();
        }
    }

    class DeliveringPizza implements IPizza {
        private dough: Dough;
        private ingredients: Array<Ingredient>;

        // constructor

        // getters and setters

        public async bakePizza(): Promise<BakedPizza> {
            throw new PizzaAlreadyBakedException();
        }

        public async deliveryPizza(): Promise<DeliveringPizza> {
            throw new PizzaAlreadyLeftForDeliveryException();
        }

        public notifyDelivery(wasSuccessful) {
            if(wasSuccessful) {
                return new DeliveredPizza(this);
            }
        }
    }

    class DeliveredPizza implements IPizza {
        private dough: Dough;
        private ingredients: Array<Ingredient>;

        // constructor

        // getters and setters

        public async bakePizza(): Promise<BakedPizza> {
            throw new PizzaAlreadyBakedException();
        }

        public async deliveryPizza(): Promise<DeliveringPizza> {
            throw new PizzaAlreadyLeftForDeliveryException();
        }

        public notifyDelivery(wasSuccessful) {
            throw new PizzaAlreadyDeliveredException();
        }
    }

With this implementation, we enforce the domain invariants with our type system through the interface and the Pizza union type. With it we gain less cyclomatic complexity since we don’t have so many branches in our code and, by design, we don’t allow for invalid States to happen. Besides that, each State carries an internal data, its travel bag. As such, these patterns are not excluding, but rather composable.

In the trend on the front-end what we are usually seeing is more a functional paradigm approach to the state machines. The entity, represented as a state machine, is now just a different data structure for each State that can be interpreted by the pure functions that implement the domain behaviors. These functions than can internally delegate its call to others functions specialized in each State. This separation of the state machine implementation of the behavior is natural as it follows the idiom for functional architectures.

What remains in both cases are the nature of the State as an entity’s representation. It works on its behalf and delimits the possible behaviors it can expose.

For example, a Pizza could never be in Baked and Delivered States at the same time. Now, it isn’t an implementation that guarantees that it is the design itself. Such abstractions, that models the domain, the heart of our product, couldn’t depend on implementation details to be valid, they must depend on the abstractions itself.

Side note: if you are versed in Algebraic Data Types you can see this as a Union Type with finite cardinality in the order of less then a dozen.

Evolving the abstraction

One could implement a State oriented design by using a simple enum, a proper state machine implementation or a more advanced concept, a statechart.

It is true that many domains can be modeled using the two first approaches to code a State, but sometimes we are faced with a high complexity scenario where this abstraction implementation would not scale with the development of the application.

For that reason that in 1987 David Harel proposed a new technique that expanded the grounds of the state machine definition, introducing tools like state hierarchy, parallelism, clustering, history, etc. He called it statecharts and it is a formalism that helps us scale the development of a State design, be implementing it thoroughly or just taking some tools.

I highly recommend reading more about statecharts as it can shift your mindset about how to approach problems.

Summary

Now we can differentiate state from State and avoid accidental complexity by using the right construct to model our domain. Its worth nothing if I don’t say that there is no silver bullet and these are tools to deliver a job. We have been experimenting with this design style on my team and it has been helpful since our scenario is really complex and fast pacing.

If you have any questions or want to discuss these and other topics more in deep please comment or you can reach me at Twitter, @caiorcferreira .

Thanks for reading!

References

Top comments (5)

Collapse
 
skittishsloth profile image
Matthew Cory

Very good article! The only further thing I'd recommend is to take it one step further and move away from the shared interface altogether - or at least away from the actionable methods. IOW, remove the methods that currently just throw exceptions in your last example. That way the type system enforces it, and there's no possible way someone will try to re-bake a delivered pizza.

Collapse
 
caiorcferreira profile image
Caio Ferreira

Thanks for the reply!

The decision to stick with the shared interface in principle is for educational reasons, as this implementation is the canonical way to implement the State pattern found on books about Design Patterns. Besides that, IMHO, removing the interface is a specific case decision, as this is the most semantic form to represent contracts and allows for abstractions to not depend on specific types, like the Pizza union type.
Finally, the goal of this design is to contribute to the whole architecture in its main mission: point the most correct way to extend the application. As such, it could be counterproductive to have really tight constraints enforced on the type system level. In my vision, the type system is another tool to indicate how to grow the implementation instead of a guard limiting the application's capabilities.

Collapse
 
skittishsloth profile image
Matthew Cory

I can see that. My personal preference is to prevent the need for exceptions as much as possible; if there's some way to keep the application from getting in an invalid state before runtime, I'd rather see that instead.

I don't necessarily agree that it would limit the application's future capabilities. If, for some reason, the company later decides that's it's perfectly reasonable to deliver an un-baked pizza, you'd still need to make appropriate changes - remove the exception throwing logic and create the appropriate state object. If that method hadn't been previously available, you'd add it (maybe extract a "DeliverablePizza" interface) and then work the logic in similarly.

The key difference is that, with the method available but just throwing an exception, someone can call it and wonder why it doesn't work. The Pizza interface contract provides for delivery - why doesn't this object obey it's contract? Without the method provided, there's no confusion, it simply isn't possible to deliver a pizza that isn't in a deliverable state.

Thread Thread
 
caiorcferreira profile image
Caio Ferreira

I think I probably focused on a different point than your first comment. I agree that avoid unnecessary exceptions that could lead to invalid states is better. In order to avoid it, one could still make this methods no-ops that log messages informing that they were called.

The point in using an interface is that in my team we usually see the software in a lifecycle of code -> stabilize -> protect -> experiment. In that way, introducing new methods, changing the interface, in a domain we already understand the behavior would make the need to introduce breaking changes to code that is already stabilized and protected instead of just replacing an implementation detail. Although, if it is a domain we don't have a grasp yet, this approach would certainly be used.

Along with this, interpreting the methods as commands send to the object, a specific command be accepted (i.e. the method being in the interface) but not processed (i.e. it is a no-op) is a common pattern when following CQS or in CQRS architecture, which aligns with the event-driven approach of this design.

It is really nice to have this kind of feedback once this was the main drive for me to start writing. Thank you.

Thread Thread
 
skittishsloth profile image
Matthew Cory

In some cases a no-op is fine - for a stretch I was a big fan of the null object pattern - but in this particular case I'd honestly rather see an exception than a no-op. I can see myself being that idiot coder who skipped the docs and is tearing his hair out wondering why my un-baked pizza isn't delivering.

I'm also seeing myself as increasingly old fashioned though - I'm not a fan of dynamic languages because I want this kind of type safety we're discussing - so I may not be able to fully see your side of it.