In object-oriented design, objects encapsulate state and expose behavior that operates on that state. Consequently, if an object must exhibit different behaviors at various stages of its lifecycle, it can be beneficial to represent these transitions by changing its type.
For example, consider a system that manages books. A newly created but unsaved book might be represented by a NewBook type. Once persisted, it is represented by a Book type, through which all subsequent interactions occur. If the book is later removed from the system, it could be represented by a DeletedBook type.
Access to instances of these types should be carefully managed. A NewBook can be instantiated directly using the new keyword, as it represents an object that does not yet exist in persistent storage. In contrast, a Book instance should be retrieved from a repository, ensuring that only books that have been saved are represented in this way. Similarly, a DeletedBook might be created by invoking a Delete method on an existing Book instance or retrieved from the repository.
Each type exposes only the behaviors that make sense within its respective state. A NewBook does not provide a Delete method, as deleting an unsaved book is meaningless. The Delete method exists solely on the Book type, allowing a book to be removed when necessary (though whether deletion is permitted at any given time depends on business rules). Meanwhile, the DeletedBook type does not expose a Delete method, as the operation is no longer relevant. In fact, a DeletedBook might expose no methods at all, unless the system supports some form of “resurrection,” in which case it might provide a method to restore it. Otherwise, a DeletedBook exists solely for historical reference within the repository.
This approach aligns with the principle of preventing objects from exposing operations that would be invalid for their current state. Rather than relying on validation and exception throwing and handling to prevent illegal actions, a more robust design choice is to avoid exposing such behaviors altogether. The most effective way to enforce this is by transitioning an object to a different type when its state changes. Naturally, language-specific mechanisms must be implemented to facilitate these transitions, such as copying relevant data between objects and ensuring state consistency.