Code Quality Improvement Techniques Part 1 (opens in new tab)
Effective code design often involves shifting the responsibility of state verification from the caller to the receiving object. By internalizing "if-checks" within the function that performs the action, developers can reduce boilerplate, prevent bugs caused by missing preconditions, and simplify state transitions. This encapsulation ensures that objects maintain their own integrity while providing a cleaner, more intuitive API for the rest of the system.
Internalizing State Verification
- Instead of the caller using a pattern like
if (!receiver.isState()) { receiver.doAction() }, the check should be moved inside thedoActionmethod. - Moving the check inside the function prevents bugs that occur when a caller forgets to verify the state, which could otherwise lead to crashes or invalid data transitions.
- This approach hides internal state details from the caller, simplifying the object's interface and focusing on the desired outcome rather than the prerequisite checks.
- If "doing nothing" when a condition isn't met is non-obvious, developers should use descriptive naming (e.g.,
markAsFriendIfNotYet) or clear documentation to signal this behavior.
Leveraging Return Values for Conditional Logic
- When a caller needs to trigger a secondary effect—such as showing a UI popup—only if an action was successful, it is better to return a status value (like a
Boolean) rather than using higher-order functions. - Passing callbacks like
onSucceededinto a use case can create unnecessary dependency cycles and makes it difficult for the caller to discern if the execution is synchronous or asynchronous. - Returning a
Booleanto indicate if a state change actually occurred allows the caller to handle side effects cleanly and sequentially. - To ensure the caller doesn't ignore these results, developers can use documentation or specific compiler annotations to force the verification of the returned value.
To improve overall code quality, prioritize "telling" an object what to do rather than "asking" about its state and then acting. Centralizing state logic within the receiver not only makes the code more robust against future changes but also makes the intent of the calling code much easier to follow.