Code Quality

22 posts

lineOriginal article

Code Quality Improvement Techniques Part 1 (opens in new tab)

The Null Object Pattern is a design technique that replaces null values with objects representing "empty" or "invalid" states to simplify code and provide functional fallbacks. While it effectively streamlines logic for collections and general data flows, using it when error conditions must be explicitly distinguished can lead to hidden bugs and reduced type safety. Developers should generally prefer statically verified types, such as Optionals or language-native nullable types, unless the error case can be seamlessly integrated into the happy-path logic. ### Benefits of the Null Object Pattern * **Code Simplification:** By returning an empty list or a "null object" instead of a literal `null`, callers can avoid repetitive null-check boilerplate. * **Functional Continuity:** It allows for uninterrupted processing in functional chains, such as using `.asSequence().map().forEach()`, because the "empty" object still satisfies the required interface. * **Fallback Provisioning:** The pattern is useful for converting errors into safe fallback values, such as displaying an "Unknown User" profile image rather than crashing or requiring complex conditional UI logic. ### Risks of Silent Failures and Logic Errors * **Bypassing Compiler Safety:** Unlike nullable types in Kotlin or Swift, which force developers to handle the `null` case, a custom null object (e.g., `UserModel.INVALID`) allows code to compile even if the developer forgets to check the object's validity. * **Identity vs. Equivalence:** Implementing the pattern requires caution regarding how the object is compared. If a null object is checked via reference identity (`==`) but the class lacks a proper `equals` implementation, new instances with the same "empty" values may not be recognized as invalid. * **Debugging Difficulty:** When a null object is used inappropriately, the program may continue to run with dummy data. This makes bugs harder to detect compared to a runtime error or a compile-time type mismatch. ### Best Practices for Type Safeness * **Prefer Static Verification:** When boundary conditions or errors must be handled differently than the "happy path," use `Optional`, `Maybe`, or native nullable types to ensure the compiler enforces error handling. * **Criteria for Use:** Reserve the Null Object Pattern for cases where the error logic is identical to the normal logic, or when multiple "empty" candidates exist that cannot be easily resolved through static typing. * **Runtime Errors as a Tool:** In dynamic or non-nullable contexts, a runtime error is often preferable to silent execution with an invalid object, as it provides a clear signal that an unexpected state has been reached. ### Recommendation To maintain high code quality, utilize the Null Object Pattern primarily for collections and UI fallbacks. For core business logic where the presence of data is critical, rely on type-safe mechanisms that force explicit handling of missing values, thereby preventing invalid states from propagating silently through the system.

lineOriginal article

Code Quality Improvement Techniques Part 1 (opens in new tab)

Effective naming in software development should prioritize the perspective of the code's consumer over the visual consistency of class declarations. By following natural grammatical structures, developers can reduce ambiguity and ensure that the purpose of a class or variable is immediately clear regardless of context. Ultimately, clear communication through grammar is more valuable for long-term maintenance than aesthetic symmetry in the codebase. ### Prefixing vs. Postfixing for Class Names When splitting a large class like `SettingRepository` into specific modules (e.g., Account, Security, or Language), the choice of where to place the modifier significantly impacts readability. * Postfixing modifiers (e.g., `SettingRepositorySecurity`) might look organized in a file directory, but it creates grammatical confusion when the class is used in isolation. * A developer encountering `SettingRepositorySecurity` in a constructor might misinterpret it as a "security module belonging to the SettingRepository" rather than a repository specifically for security settings. * Prefixing the modifier (e.g., `SecuritySettingRepository`) follows standard English grammar, clearly identifying the object as a specific type of repository and reducing the cognitive load for the reader. ### Handling Multiple Modifiers and the "Sandwich" Effect In cases where a single prefix is insufficient, such as defining the "height of a send button in portrait mode," naming becomes more complex. * Using only prefixes (e.g., `portraitSendButtonHeight`) can be ambiguous, potentially being read as the "height of a button used to send a portrait." * To resolve this, developers can use a "modifier sandwich" by moving some details to the end using prepositions like "for," "of," or "in" (e.g., `sendButtonHeightForPortrait`). * While prepositions are helpful for variables, they should generally be avoided in class or struct names to ensure that instance names derived from the type remain concise. * Developers should also defer to platform-specific conventions; for example, Java and Kotlin often omit prepositions in standard APIs, such as using `currentTimeMillis` instead of `currentTimeInMillis`. When naming any component, favor the clarity of the person reading the implementation over the convenience of the person writing the definition. Prioritizing grammatical correctness ensures that the intent of the code remains obvious even when a developer is looking at a single line of code.

lineOriginal article

Code Quality Improvement Techniques Part (opens in new tab)

Applying the Single Responsibility Principle is a fundamental practice for maintaining high code quality, but over-fragmenting logic can inadvertently lead to architectural complexity. While splitting classes aims to increase cohesion, it can also scatter business constraints and force callers to manage an overwhelming number of dependencies. This post explores the "responsibility of assigning responsibility," arguing that sometimes maintaining a slightly larger, consolidated class is preferable to creating fragmented "Ravioli code." ### Initial Implementation and the Refactoring Drive The scenario involves a dynamic "Launch Button" that can fire rockets, fireworks, or products depending on its mode. * The initial design used a single `LaunchButtonBinder` that held references to all possible `Launcher` types and an internal enum to select the active one. * To strictly follow the Single Responsibility Principle, developers often attempt to split this into two parts: a binder for the button logic and a selector for choosing the mode. * The refactored approach utilized a `LaunchBinderSelector` to manage multiple `LaunchButtonBinder` instances, using an `isEnabled` flag to toggle which logic was active. ### The Problem of Scattered Constraints and State While the refactored classes are individually simpler, the overall system becomes harder to reason about due to fragmented logic. * **Verification Difficulty:** In the original code, the constraint that "only one thing launches at a time" was obvious in a single file; in the refactored version, a developer must trace multiple classes and loops to verify this behavior. * **State Redundancy:** Adding an `isEnabled` property to binders creates a risk of state synchronization issues between the selector’s current mode and the binders' internal flags. * **Information Hiding Trade-offs:** Attempting to hide implementation details often forces the caller to resolve all dependencies (binders, buttons, and launchers) manually, which can turn the caller into a bloated "God class." ### Avoiding "Ravioli Code" Through Balanced Design The pursuit of granular responsibilities can lead to "Ravioli code," where the system consists of many small, independent components but lacks a clear, cohesive structure. * The original implementation’s advantage was that it encapsulated all logic related to the launch button's constraints in one place. * When deciding to split a class, developers must evaluate if the move improves the overall system or simply shifts the burden of complexity to the caller. * Effective design requires balancing individual class cohesion with the overhead of inter-module coupling and dependency management. When refactoring for code quality, prioritize the clarity of the overall system over the dogmatic pursuit of small classes. If splitting a class makes it harder to verify business constraints or complicates the caller's logic significantly, it may be better to keep those related responsibilities together.

lineOriginal article

Code Quality Improvement Techniques Part (opens in new tab)

The "Clone Family" anti-pattern occurs when two parallel inheritance hierarchies—such as a data model tree and a provider tree—share an implicit relationship that is not enforced by the type system. This structure often leads to type-safety issues and requires risky downcasting to access specific data types, increasing the likelihood of runtime errors during code modifications. To resolve this, developers should replace rigid inheritance with composition or utilize parametric polymorphism to explicitly link related types. ## The Risks of Implicit Correspondence Maintaining two separate inheritance trees where individual subclasses are meant to correspond to one another creates several technical hurdles. * **Downcasting Requirements:** Because a base provider typically returns a base data model type, developers must manually cast the result to a specific subclass (e.g., `as FooDataModel`), which bypasses compiler safety. * **Lack of Type Enforcement:** The constraint that a specific provider always returns a specific model is purely implicit; the compiler cannot prevent a provider from returning the wrong model type. * **Fragile Architecture:** As the system grows, ensuring that "Provider A" always maps to "Model A" becomes difficult to audit, leading to potential bugs when new developers join the project or when the hierarchy is extended. ## Substituting Inheritance with Composition When the primary goal of inheritance is simply to share common logic, such as fetching raw data, using composition or aggregation is often a superior alternative. * **Logic Extraction:** Shared functionality can be moved into a standalone class, such as an `OriginalDataProvider`, which is then held as a private property within specific provider classes. * **Direct Type Returns:** By removing the shared parent class, each provider can explicitly return its specific data model type without needing a common interface. * **Decoupling:** This approach eliminates the "Clone Family" entirely by removing the need for parallel trees, resulting in cleaner and more modular code. ## Leveraging Parametric Polymorphism In scenarios where a common parent class is necessary—for example, to manage a collection of providers within a shared lifecycle—generics can be used to bridge the two hierarchies safely. * **Generic Type Parameters:** By defining the parent as `ParentProvider<T>`, the base class can use a type parameter for its return values rather than a generic base model. * **Subclass Specification:** Each implementation (e.g., `FooProvider : ParentProvider<FooDataModel>`) explicitly defines its return type, allowing the compiler to enforce the relationship. * **Flexible Constraints:** Developers can still utilize type bounds, such as `ParentProvider<T : CommonDataModel>`, to ensure that the generics adhere to a specific interface while maintaining type safety for callers. When designing data providers and models, avoid creating parallel structures that rely on implicit assumptions. Prioritize composition to simplify the architecture, or use generics if inheritance is required, ensuring that the relationships between classes remain explicit and verifiable by the compiler.

lineOriginal article

Code Quality Improvement Techniques Part (opens in new tab)

The "Set Discount" technique improves code quality by grouping related mutable properties into a single state object rather than allowing them to be updated individually. By restricting state changes through a controlled interface, developers can prevent inconsistent configurations and simplify the lifecycle management of complex classes. This approach ensures that dependent values are updated atomically, significantly reducing bugs caused by race conditions or stale data. ### The Risks of Fragmented Mutability When a class exposes multiple independent mutable properties, such as `isActive`, `minImportanceToRecord`, and `dataCountPerSampling`, it creates several maintenance challenges: * **Order Dependency:** Developers might accidentally set `isActive` to true before updating the configuration properties, causing the system to briefly run with stale or incorrect settings. * **Inconsistent Logic:** Internal state resets (like clearing a counter) may be tied to one property but forgotten when another related property changes, leading to unpredictable behavior. * **Concurrency Issues:** Even in single-threaded environments, asynchronous updates to individual properties can create race conditions that are difficult to debug. ### Consolidating State with SamplingPolicy To resolve these issues, the post recommends refactoring individual properties into a dedicated configuration class and using a single reference to manage the state: * **Atomic Updates:** By wrapping configuration values into a `SamplingPolicy` class, the system ensures that the minimum importance level and sampling interval are always updated together. * **Representing "Inactive" with Nulls:** Instead of a separate boolean flag, the `policy` property can be made nullable. An `inactive` state is naturally represented by `null`, making it impossible to "activate" the recorder without providing a valid policy. * **Explicit Lifecycle Methods:** Replacing property setters with methods like `startRecording()` and `finishRecording()` forces a clear transition of state and ensures that counters are reset consistently every time a new session begins. ### Advantages of Restricting State Transitions Moving from individual property mutation to a consolidated interface offers several technical benefits: * **Guaranteed Consistency:** It eliminates the possibility of "half-configured" states because the policy is replaced as a whole. * **Simplified Thread Safety:** If the class needs to be thread-safe, developers only need to synchronize a single reference update rather than coordinating multiple volatile variables. * **Improved Readability:** The intent of the code becomes clearer to future maintainers because the valid combinations of state are explicitly defined by the API. When designing components where properties are interdependent or must change simultaneously, you should avoid providing public setters for every field. Instead, provide a focused interface that limits updates to valid combinations, ensuring the object remains in a predictable state throughout its lifecycle.

lineOriginal article

Code Quality Improvement Techniques Part (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 the `doAction` method. * 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 `onSucceeded` into 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 `Boolean` to 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.

lineOriginal article

Code Quality Improvement Techniques Part 1 (opens in new tab)

Maintaining a clear separation of concerns between software layers requires avoiding implicit dependencies where one layer relies on the specific implementation details of another. When different components share "hidden" knowledge—such as a repository fetching extra data specifically to trigger a UI state—the code becomes fragile and difficult to maintain. By passing explicit information through data models, developers can decouple these layers and ensure that changes in one do not inadvertently break the other. ### The Risks of Implicit Layer Dependency When layers share implicit logic, such as a repository layer knowing the specific display requirements of the UI, the architecture becomes tightly coupled and prone to bugs. * In the initial example, the repository fetches `MAX + 1` items specifically because the UI needs to display a "+" sign if more items exist. * This creates a dependency where the UI logic for displaying counts relies entirely on the repository's internal fetching behavior. * Code comments that explain one layer's behavior in the context of another (e.g., `// +1 is for the UI`) are a "code smell" indicating that responsibilities are poorly defined. ### Decoupling Through Explicit State The most effective way to separate these concerns is to modify the data model to carry explicit state information, removing the need for "magic numbers" or leaked logic. * By adding a boolean property like `hasMoreItems` to the `StoredItems` model, the repository can explicitly communicate the existence of additional data. * The repository handles the logic of fetching `limit + 1`, determining the boolean state, and then truncating the list to the correct size before passing it up. * The UI layer becomes "dumb" and only reacts to the provided data; it no longer needs to know about the `MAX_COUNT` constant or the repository's fetching strategy to determine its display state. ### Strategic Placement of Logic and Constants Determining where constants like `ITEM_LIST_MAX_COUNT` should reside is a key architectural decision that impacts code reuse and clarity. * **Business Logic Layer:** Placing such constants in a dedicated Domain or Use Case layer is often the best approach for maintaining a clean architecture. * **Model Classes:** If a separate logic layer is too complex for the project scale, the constant can be housed within the model class (e.g., using a companion object in Kotlin). * **Dependency Direction:** Developers must ensure that functional logic does not leak into generic data models, as this can create confusing dependencies where a general-purpose model becomes tied to a specific feature's algorithm. Effective software design relies on components maintaining a "proper distance" from one another. To improve code quality, favor explicit flags and clear data contracts over implicit assumptions about how different layers of the stack will interact.