immutability

3 posts

line

Code Quality Improvement Techniques Part (opens in new tab)

LY Corporation’s technical review highlights that making a class open for inheritance imposes a "tax" on its internal constraints, particularly immutability. While developers often use inheritance to create specialized versions of a class, doing so with immutable types can allow subclasses to inadvertently or intentionally break the parent class's guarantees. To ensure strict data integrity, the post concludes that classes intended to be immutable should be made final or designed around read-only interfaces rather than open for extension. ### The Risks of Open Immutable Classes * Kotlin developers often wrap `IntArray` in an `ImmutableIntList` to avoid the overhead of boxed types while ensuring the collection remains unchangeable. * If `ImmutableIntList` is marked as `open`, a developer might create a `MutableIntList` subclass that adds a `set` method to modify the internal `protected valueArray`, violating the "Immutable" contract of the parent type. * Even if the internal state is `private`, a subclass can override the `get` method to return dynamic or state-dependent values, effectively breaking the expectation that the data remains constant. * These issues demonstrate that any class with a "fundamental" name should be carefully guarded against unexpected inheritance in different modules or packages. ### Establishing Safe Inheritance Hierarchies * Mutable objects should not inherit from immutable objects, as this inherently violates the immutability constraints established by the parent. * Conversely, immutable objects should not inherit from mutable ones; this often leads to runtime errors (such as `UnsupportedOperationException`) when a user attempts to call modification methods like `add` or `set` on an immutable instance. * The most effective design pattern is to use a "read-only" (unmodifiable) interface as a common parent, similar to how Kotlin distinguishes between `List` and `MutableList`. * In this structure, mutable classes can inherit from the read-only parent without issue (adding new methods), and immutable classes can inherit from the read-only parent while adding stricter internal constraints. To maintain high code quality and prevent logic errors, developers should default to making classes final when immutability is a core requirement. If shared functionality is needed across different types of lists, utilize composition or a shared read-only interface to ensure that the "immutable" label remains a truthful guarantee.

line

Code Quality Improvement Techniques Part (opens in new tab)

Effective code review communication relies on a "conclusion-first" approach to minimize cognitive load and ensure clarity for the developer. By stating proposed changes or specific requests before providing the underlying rationale, reviewers help authors understand the primary goal of the feedback immediately. This practice improves development productivity by making review comments easier to parse and act upon without repeated reading. ### Optimizing Review Comment Structure * Place the core suggestion or requested code change at the very beginning of the comment to establish immediate context. * Follow the initial request with a structured explanation, utilizing headers or numbered lists to organize multiple supporting arguments. * Clearly distinguish between the "what" (the requested change) and the "why" (the technical justification) to prevent the intended action from being buried in a long technical discussion. * Use visual formatting to help the developer quickly validate the logic behind the suggestion once they understand the proposed change. ### Immutability and Data Class Design * Prefer the use of `val` over `var` in Kotlin `data class` structures to ensure object immutability. * Using immutable properties prevents bugs associated with unintended side effects that occur when mutable objects are shared across different parts of an application. * Instead of reassigning values to a mutable property, utilize the `copy()` function to create a new instance with updated state, which results in more robust and predictable code. * Avoid mixing `var` properties with `data class` features, as this can lead to confusion regarding whether to modify the existing instance or create a copy. ### Property Separation by Lifecycle * Analyze the update frequency of different properties within a class to identify those with different lifecycles. * Decouple frequently updated status fields (such as `onlineStatus` or `statusMessage`) from more stable attributes (such as `userId` or `accountName`) by moving them into separate classes. * Grouping properties by their lifecycle prevents unnecessary updates to stable data and makes the data model easier to maintain as the application scales. To maintain high development velocity, reviewers should prioritize brevity and structure in their feedback. Leading with a clear recommendation and supporting it with organized technical reasoning ensures that code reviews remain a tool for progress rather than a source of confusion.

line

Code Quality Improvement Techniques Part (opens in new tab)

The builder pattern is frequently overused in modern development, often leading to code that is less robust than it appears. While it provides a fluent API, it frequently moves the detection of missing mandatory fields from compile-time to runtime, creating a "house of sand" that can collapse unexpectedly. By prioritizing constructors and factory functions, developers can leverage the compiler to ensure data integrity and build more stable applications. ### Limitations of the Standard Builder Pattern * In a typical builder implementation, mandatory fields are often initialized as nullable types and checked for nullity only when the `.build()` method is called. * This reliance on runtime checks like `checkNotNull` means that a developer might forget to set a required property, leading to an `IllegalStateException` during execution rather than a compiler error. * Unless the platform or a specific library (like an ORM) requires it, the boilerplate of a builder often hides these structural weaknesses without providing significant benefits. ### Strengthening Foundations with Constructors and Defaults * Using a class constructor or a factory function is often the simplest and most effective way to prevent bugs related to missing data. * In languages like Kotlin, the need for builders is further reduced by the availability of default parameters and named arguments, allowing for concise instantiation even with many optional fields. * If a builder must be used, mandatory arguments should be required in the builder's own constructor (e.g., `Builder(userName, emailAddress)`) to ensure the object is never in an invalid state. ### Managing Creation State and Pipelines * Developers sometimes pass a builder as an "out parameter" to other functions to populate data, which can obscure the flow of data and reduce readability. * A better approach is to use functions that return specific values, which are then passed into a final constructor, keeping the logic functional and transparent. * For complex, multi-stage creation logic, defining distinct types for each stage—such as moving from a `UserAccountModel` to a `UserProfileViewComponent`—can ensure that only valid, fully-formed data moves through the pipeline. ### Appropriate Use of Terminal Operations * The builder-like syntax is highly effective when implementing "terminal operations," where various transformations are applied in an arbitrary order before a final execution. * This pattern is particularly useful in image processing or UI styling (e.g., `.crop().fitIn().colorFilter()`), where it serves as a more readable alternative to deeply nested decorator patterns. * In these specific cases, the pattern facilitates a clear sequence of operations while maintaining a "last step" (like `.createBitmap()`) that signals the end of the configuration phase. Prioritize the use of constructors and factory functions to catch as many errors as possible during compilation. Reserve the builder pattern for scenarios involving complex terminal operations or when dealing with restrictive library requirements that demand a specific instantiation style.