NoteTube

The 8 Golden Rules of Writing Clean Functions - Clean Code In Kotlin
16:55

The 8 Golden Rules of Writing Clean Functions - Clean Code In Kotlin

Philipp Lackner

8 chapters8 takeaways10 key terms8 questions

Overview

This video outlines eight essential principles for writing clean, maintainable, and understandable Kotlin functions. These principles, while demonstrated in Kotlin, are broadly applicable to software development. They focus on improving code readability and reducing cognitive load by emphasizing single responsibility, avoiding side effects, adhering to the DRY principle, minimizing parameters, failing fast, using early returns, maintaining appropriate levels of detail, and judiciously using extension functions. Adhering to these rules leads to more robust, flexible, and easier-to-debug codebases.

How was this?

Save this permanently with flashcards, quizzes, and AI chat

Chapters

  • Functions should perform only one specific task or have a single reason to change.
  • Violating SRP leads to functions that are hard to understand, increasing cognitive load.
  • Multiple responsibilities in one function make code inflexible and difficult to reuse.
  • Breaking down complex functions into smaller, single-purpose functions improves readability and maintainability.
Ensuring functions have a single responsibility makes them easier to understand, test, and reuse, preventing the accumulation of complex, unmanageable code.
A `registerUser` function that handles validation, password hashing, database saving, and email sending is broken down into separate functions for each task, making the overall process clearer and each part reusable.
  • Side effects are actions a function takes that affect the outside world beyond its return value (e.g., database writes, analytics events).
  • Functions should ideally be pure, meaning they only compute and return a value based on their inputs.
  • Unintended side effects make it difficult to reason about function behavior and can lead to bugs.
  • Separate functions for actions with side effects from those that only perform calculations.
Minimizing side effects makes functions predictable and easier to test, as their behavior is solely determined by their inputs and outputs, not by hidden external changes.
An `applyDiscount` function that calculates a discount and also marks a coupon as redeemed and tracks an analytics event. This is refactored into a `calculateTotal` function and an `orderService.placeOrder` function which handles the redemption and tracking.
  • Avoid duplicating code throughout your project.
  • Repeated code increases the effort required for maintenance, testing, and debugging.
  • Extracting repeated logic into a single function or utility makes updates easier and less error-prone.
  • Centralizing logic ensures consistency and reduces the chance of introducing bugs through manual copying.
Adhering to the DRY principle reduces code volume, simplifies updates, and minimizes the risk of inconsistencies and errors that arise from maintaining identical logic in multiple places.
Multiple formatting functions for different receipt elements (order line, footer, refund) all using the same number formatting logic. This logic is extracted into a single `formatPrice` function.
  • Functions with too many parameters increase cognitive load and are harder to use correctly.
  • Group related parameters into data classes or objects.
  • Passing a single object representing related data is more readable and maintainable than passing many individual parameters.
  • This approach makes the function signature cleaner and clarifies the data requirements.
Reducing the number of parameters by using data classes makes function signatures cleaner, improves readability, and reduces the likelihood of errors caused by incorrect parameter order or missing values.
A `createInvoice` function that initially takes many individual parameters (customer details, address, money amount) is refactored to accept `Customer`, `Address`, and `Money` objects, which encapsulate these related data points.
  • Detect and report errors as early as possible within a function.
  • Throw exceptions immediately when an error condition is identified.
  • This prevents unnecessary computation and avoids executing code that will ultimately fail.
  • Early failure detection simplifies debugging by pinpointing the source of the error quickly.
Failing fast ensures that errors are identified and handled at the earliest possible moment, preventing wasted computation and making it easier to diagnose and fix problems.
In a transaction import function, checks for invalid data or conditions should throw exceptions immediately, rather than proceeding with processing that will eventually fail.
  • Use early returns to exit a function as soon as a specific condition is met, especially for error or exit cases.
  • This pattern reduces deep nesting of `if-else` statements, making code flatter and more readable.
  • It clearly delineates the different exit paths of a function.
  • Code following early returns can assume that preceding conditions have been met.
Employing early returns simplifies control flow, eliminates excessive indentation, and makes it much easier to follow the logic and understand the conditions under which a function will return different results.
A `signUp` function first checks for invalid email, then existing user, then password strength, returning an error immediately for each. This avoids deeply nested `if-else` blocks.
  • Functions should operate at a level of abstraction consistent with their purpose.
  • High-level functions should orchestrate actions using other functions, avoiding low-level implementation details.
  • Low-level details (like credit card validation logic) should be encapsulated in separate, lower-level functions.
  • This principle ensures that functions clearly represent the task they are intended to perform at their given abstraction layer.
Matching the level of detail in a function to its abstraction layer makes the code's intent clearer and prevents overly complex, low-level logic from cluttering high-level operations.
A `checkoutService.placeOrder` function should call other high-level functions like `validateCard`, `calculateTotal`, and `submitToAPI`, rather than containing the raw credit card validation code itself.
  • Extension functions in Kotlin allow adding functionality to existing classes without modifying them.
  • Use extension functions when the receiver object (the class being extended) is the 'owner' of the operation.
  • The receiver object should be the primary subject or target of the function's action.
  • Avoid using extension functions when the receiver is merely a data carrier or not the true owner of the behavior.
Judicious use of extension functions makes code more readable and idiomatic by clearly indicating which object is the primary focus of an operation, enhancing code organization and expressiveness.
An `appendRowToFile` function is better implemented as a file extension function (`file.appendCsvRow(values)`) because the file is the object being modified, rather than passing the file as a parameter to a standalone function.

Key takeaways

  1. 1Clean code is about making software easier to understand, maintain, and evolve.
  2. 2Functions should be small, focused, and perform a single, well-defined task.
  3. 3Minimize side effects to make functions predictable and easier to test.
  4. 4Avoid repeating code by extracting common logic into reusable functions.
  5. 5Group related data into objects or data classes to simplify function parameters.
  6. 6Handle errors as early as possible and use early returns to simplify control flow.
  7. 7Match the level of abstraction in functions to their intended purpose.
  8. 8Use Kotlin's extension functions thoughtfully to improve code readability when the receiver is the true owner of the functionality.

Key terms

Single Responsibility Principle (SRP)Cognitive LoadSide EffectDRY PrincipleData ClassFail FastEarly ReturnAbstraction LayerExtension FunctionReceiver Object

Test your understanding

  1. 1What does it mean for a function to have a single responsibility, and why is this important?
  2. 2How can side effects in a function lead to unexpected behavior or bugs?
  3. 3Why is duplicating code detrimental to software maintenance, and what is the DRY principle?
  4. 4What is the recommended approach for handling functions with many related parameters?
  5. 5How does the 'fail fast' principle help in debugging and error handling?
  6. 6What are the benefits of using early returns in function design?
  7. 7How can you ensure a function operates at the appropriate level of detail or abstraction?
  8. 8Under what circumstances should you use Kotlin's extension functions?

Turn any lecture into study material

Paste a YouTube URL, PDF, or article. Get flashcards, quizzes, summaries, and AI chat — in seconds.

No credit card required