Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Code readability: Session 5 (ver. 2, En)

Code readability: Session 5 (ver. 2, En)

Munetoshi Ishikawa

May 11, 2023
Tweet

More Decks by Munetoshi Ishikawa

Other Decks in Programming

Transcript

  1. abstract fun codeReadabilitySession5()
    fun Function()

    View Slide

  2. Brushup: State
    Simplify state for readability and robustness
    - Do not make it an objective
    Avoid non-orthogonal relationships
    - Replace with function or use sum-type
    Care about state transition
    - Immutability, idempotence, and cycle

    View Slide

  3. Contents of this lecture
    - Introduction and Principles
    - Natural language: Naming, Comments
    - Inner type structure: State, Function
    - Inter type structure: Dependency I, Dependency II
    - Follow-up: Review
    Function > Introduction

    View Slide

  4. "Function" in this session
    Also includes
    - Subroutine
    - Procedure
    - Method
    - Computed property
    - Constructor, initialization block
    - ...
    Function > Introduction

    View Slide

  5. What a readable function is
    Predictable
    - Consistent with the name
    - Easy to write documentation
    - Few error cases or limitations
    Function > Introduction

    View Slide

  6. Topics
    Make the function responsibility/flow clear
    Function > Introduction

    View Slide

  7. Topics
    - Make responsibility clear
    - Split if it is hard to summarize
    - Split if it has both return value and side-effect
    - Make flow clear
    - Perform definition-based programming
    - Focus on a happy path
    - Split by object, not condition
    Function > Introduction

    View Slide

  8. Topics
    - Make responsibility clear
    - Split if it is hard to summarize
    - Split if it has both return value and side-effect
    - Make flow clear
    - Perform definition-based programming
    - Focus on a happy path
    - Split by object, not condition
    Function > Responsibility > Check with a short summary

    View Slide

  9. Split the function if it is hard to summarize
    Try to summarize what the function does
    Function > Responsibility > Check with a short summary

    View Slide

  10. Function summary: Example 1/2
    Question: How can we summarize this code?
    messageView.text = messageData.contentText
    senderNameView.text = messageData.senderName
    timestampView.text = messageData.sentTimeText
    Example answer:
    "Bind/update message layout with a new message data"
    Function > Responsibility > Check with a short summary

    View Slide

  11. Function summary: Example 2/2
    Question: How can we summarize this code?
    messageView.text = messageData.contentText
    doOnTransaction {
    messageDatabase.insertNewMessage(messageData)
    }
    Example answer?:
    "Bind a new message and save it"
    "Perform actions on a new message received"
    Function > Responsibility > Check with a short summary

    View Slide

  12. Function responsibility and summary
    Hard to summarize = May need to split the function
    fun bindMessageViewData(messageData: MessageData) { ... }
    fun saveMessageDataToDatabase(messageData: MessageData) { ... }
    Exceptions:
    - Interface for other layers/modules
    - Entry point of event handling
    Function > Responsibility > Check with a short summary

    View Slide

  13. Topics
    - Make responsibility clear
    - Split if it is hard to summarize
    - Split if it has both return value and side-effect
    - Make flow clear
    - Perform definition-based programming
    - Focus on a happy path
    - Split by object, not condition
    Function > Responsibility > Command and query

    View Slide

  14. Command-query separation6
    Separate a function by command and query
    - Command: Modifies the receiver or parameters
    - Query: Return without any modification
    6 Bertrand Meyer. 1988. Object-oriented Software Construction. Prentice-Hall
    Function > Responsibility > Command and query

    View Slide

  15. Command-query separation: Example 1/2
    class IntList(vararg elements: Int) {
    infix fun append(others: IntList): IntList = ...
    }
    val a = IntList(1, 2)
    val b = IntList(3, 4)
    val c = a append b
    Question: What is the expected value of a, b and c?
    Function > Responsibility > Command and query

    View Slide

  16. Command-query separation: Example 2/2
    val a = IntList(1, 2)
    val b = IntList(3, 4)
    val c = a append b
    Expected: a={1, 2}, b={3, 4}, c={1, 2, 3, 4}
    Surprising result: a={1, 2, 3, 4}, b={3, 4}, c={1, 2, 3, 4}
    Function append should not modify a or b
    because it returns a result
    Function > Responsibility > Command and query

    View Slide

  17. Command-query separation: Drawbacks 1/3
    Command-query separation is not an objective
    - May cause strong coupling (will explain during the next session)
    - May make an unnecessary state
    Function > Responsibility > Command and query

    View Slide

  18. Command-query separation: Drawbacks 2/3
    Bad code example
    class UserDataStore {
    private var latestOperationResult: Result = NO_OPERATION
    // Command
    fun saveUserData(userData: UserData) = ...
    // Query
    val wasLatestOperationSuccessful: Boolean get() = ...
    }
    Variable
    latestOperationResult
    may become a cause of a bug
    Function > Responsibility > Command and query

    View Slide

  19. Command-query separation: Drawbacks 3/3
    Good code example
    class UserDataRequester {
    /**
    * Saves a given user data to ...
    * Then, returns true if the operation successful, otherwise false.
    */
    fun saveUserData(userData: UserData): Boolean = ...
    }
    No need to keep the latest result when returning it directly
    Function > Responsibility > Command and query

    View Slide

  20. Responsibility on return value and modification
    Do not modify when returning a "main" result
    = Acceptable to return a "sub" result with modification
    - Main result: conversion/calculation result, new instance, ...
    - Sub result: error value, metadata of modification (stored size), ...
    (Documentation is mandatory)
    Acceptable if the interface is a de facto standard
    e.g., Queue.poll(): E, Iterator.next(): E
    Function > Responsibility > Command and query

    View Slide

  21. Make responsibility clear: Summary
    Try to split a function if it is hard to summarize
    - Try to write documentation anyway
    Do not modify when returning a "main" result
    - Fine to return a "sub" result with modification
    - Fine if the interface is a de facto standard
    Function > Responsibility > Summary

    View Slide

  22. Topics
    - Make responsibility clear
    - Split if it is hard to summarize
    - Split if it has both return value and side-effect
    - Make flow clear
    - Perform definition-based programming
    - Focus on a happy path
    - Split by object, not condition
    Function > Flow

    View Slide

  23. Function with clear flow
    Important point:
    Can understand the code overview only by scanning through
    - Able to skip reading details
    - Easy to find the important part
    - No need to cover all conditional branches
    Confirm with writing the short summary
    Function > Flow

    View Slide

  24. Topics
    - Make responsibility clear
    - Split if it is hard to summarize
    - Split if it has both return value and side-effect
    - Make flow clear
    - Perform definition-based programming
    - Focus on a happy path
    - Split by object, not condition
    Function > Flow > Definition-based programming

    View Slide

  25. Definition-based programming 1/2
    Style that frequently uses named variables and functions
    Objective:
    - Clarify code that can be skipped
    - Make abstraction level high
    - Reduce the need to go back and read
    Function > Flow > Definition-based programming

    View Slide

  26. Definition-based programming 2/2
    Replace hard-to-read elements
    with named variables and functions
    Hard-to-read elements:
    - Nest (parameter, function, class, control flow)
    - Anonymous function, object literal
    - Method chain
    Function > Flow > Definition-based programming

    View Slide

  27. Parameter nest: Issues 1/2
    Question: Why is the following code difficult to read?
    showDialogOnError(
    presenter.updateSelfProfileView(
    repository.queryUserModel(userId)
    )
    )
    Function > Flow > Definition-based programming

    View Slide

  28. Parameter nest: Issues 2/2
    Answer:
    - The important function call is placed inside the nest
    - It requires to understand all return values
    - The direction of code flow is reversed (bottom to top)
    Function > Flow > Definition-based programming

    View Slide

  29. Parameter nest: How to fix
    Use a named local variable for return values
    val userModel = repository.queryUserModel(userId)
    val viewUpdateResult =
    presenter.updateSelfProfileView(UserModel)
    showDialogOnError(viewUpdateResult)
    Function > Flow > Definition-based programming

    View Slide

  30. Method chain with lambda: Issues 1/2
    Question: Why is the following code difficult to read?
    return queryTeamMemberIds(teamId)
    .map { memberId ->
    ... // Convert to UserModel
    ...
    }
    .filter {
    val userStatus = ...
    userStatus.isOnline
    }
    .map { it.emailAddress }
    Function > Flow > Definition-based programming

    View Slide

  31. Method chain with lambda: Issues 2/2
    Answer:
    Need to read details to understand behavior
    - Receiver for each method call (= return value for each call)
    - Behavior detail of each lambda
    Function > Flow > Definition-based programming

    View Slide

  32. Method chain with lambda: How to fix 1/2
    Option 1: Split the chain with named local variables
    val teamMembers = queryTeamMemberIds(teamId).map { ...
    ...
    }
    val onlineTeamMembers = teamMembers.filter { ...
    ...
    }
    return onlineTeamMembers.map { it.emailAddress }
    It may still be difficult to read if there are long lambdas
    Function > Flow > Definition-based programming

    View Slide

  33. Method chain with lambda: How to fix 2/2
    Option 2: Replace lambdas with named private functions
    return queryTeamMemberIds(teamId)
    .map(::toUserModel)
    .filter(::isOnline)
    .map { it.emailAddress }
    ...
    private fun toUserModel(userId: UserId): UserModel { ... }
    private fun isOnline(userModel: UserModel): Boolean { ... }
    Function > Flow > Definition-based programming

    View Slide

  34. Definition-based programming: Pitfall
    Extracting may make the code less readable
    - Unnecessary state
    - Unnecessary strong coupling (will explain at the next session)
    Function > Flow > Definition-based programming

    View Slide

  35. Pitfalls of extraction: Original code
    val userNameTextView: View = ...
    val profileImageView: View = ...
    init {
    // Complex userNameTextView initialization code
    userNameTextView...
    // Complex profileImageView initialization code
    profileImageView...
    }
    Extract complex initialization code
    Function > Flow > Definition-based programming

    View Slide

  36. Pitfalls of extraction: Bad example
    var userNameTextView: View? = null
    var profileImageView: View? = null
    init {
    initializeUserNameTextView()
    initializeProfileImageView()
    }
    private fun initializeUserNameTextView() {
    userNameTextView = ...
    private fun initializeProfileImageView() {
    profileImageView = ...
    Function > Flow > Definition-based programming

    View Slide

  37. Pitfalls of extraction: What is wrong
    - Unnecessary mutability and nullability
    - Unsafe to call initialize... methods multiple times
    - Function name initialize... is ambiguous
    Optimize the scope of the extracted methods
    Function > Flow > Definition-based programming

    View Slide

  38. Pitfalls of extraction: Good example
    Extract view creation and initialization logic
    val userNameTextView: View = createUserNameTextView()
    val profileImageView: View = createProfileImageView()
    private fun createUserNameTextView(): TextView =
    ...
    private fun createProfileImageView(): ImageView =
    ...
    Function > Flow > Definition-based programming

    View Slide

  39. Topics
    - Make responsibility clear
    - Split if it is hard to summarize
    - Split if it has both return value and side-effect
    - Make flow clear
    - Perform definition-based programming
    - Focus on a happy path
    - Split by object, not condition
    Function > Flow > Focus on a happy path

    View Slide

  40. Happy path and unhappy path
    Happy path: Achieves the main objective of a function
    Unhappy path: Does not achieve the main objective of a function
    Function > Flow > Focus on a happy path

    View Slide

  41. Happy path and unhappy path: Example
    String.toIntOrNull()
    Happy path: Returns an integer value
    e.g, "-1234", "0", "+001", "-2147483648"
    Unhappy path: Returns null
    e.g., "--0", "text", "", "2147483648", "1.0"
    Function > Flow > Focus on a happy path

    View Slide

  42. Function flow and happy path
    Firstly, filter unhappy paths to focus on a happy path
    Apply early return
    Function > Flow > Focus on a happy path

    View Slide

  43. Early return: Bad example
    if (isNetworkAvailable()) {
    val queryResult = queryToServer()
    if (queryResult.isValid) {
    // Do something with queryResult...
    } else {
    showInvalidResponseDialog()
    }
    } else {
    showNetworkUnavailableDialog()
    }
    Function > Flow > Focus on a happy path

    View Slide

  44. Early return: What is wrong
    - Hard to find the main case logic
    - The relation between error condition and handling logic
    Function > Flow > Focus on a happy path

    View Slide

  45. Early return: Good example
    if (!isNetworkAvailable()) {
    showNetworkUnavailableDialog()
    return
    }
    val queryResult = queryToServer()
    if (!queryResult.isValid) {
    showInvalidResponseDialog()
    return
    }
    // Do something with queryResult...
    Function > Flow > Focus on a happy path

    View Slide

  46. Early return: Caution 1/2
    Avoid early return with only some branches of when/switch
    May overlook the early return and the corresponding condition
    val messageText = when (type) {
    FOO -> "this is FOO"
    BAR -> "this is BAR"
    null -> return
    }
    Function > Flow > Focus on a happy path

    View Slide

  47. Early return: Caution 1/2
    Avoid early return with only some branches of when/switch
    May overlook the early return and the corresponding condition
    if (type == null) {
    return
    }
    val messageText = when (type) {
    FOO -> "this is FOO"
    BAR -> "this is BAR"
    }
    Function > Flow > Focus on a happy path

    View Slide

  48. Early return: Caution 2/2
    Avoid making an unnecessary unhappy path
    Typical case: Iteration of an empty collection
    if (list.isEmpty) {
    return listOf()
    }
    return list
    .filter { ... }
    .map { ... }
    Function > Flow > Focus on a happy path

    View Slide

  49. Topics
    - Make responsibility clear
    - Split if it is hard to summarize
    - Split if it has both return value and side-effect
    - Make flow clear
    - Perform definition-based programming
    - Focus on a happy path
    - Split by object, not condition
    Function > Flow > Split by object

    View Slide

  50. Axis to break down function 1/2
    Multiple conditions and target objects
    Example: Show account state
    - Conditions: Two account types (premium, free)
    - Objects: Two views (background, icon)
    Function > Flow > Split by object

    View Slide

  51. Axis to break down function 2/2
    Question: On which axis should the function be split?
    Condition
    Object
    Premium account Free account
    Background color
    Account icon
    Red Gray
    “Twemoji” ©Twitter, Inc and other contributors (Licensed under CC-BY 4.0) https://twemoji.twitter.com/
    Function > Flow > Split by object

    View Slide

  52. Bad example of "split by condition first"
    fun bindViewData(accountType: AccountType) {
    when (accountType) {
    PREMIUM -> updateViewsForPremium()
    FREE -> updateViewsForFree()
    }
    }
    private fun updateViewsForPremium() {
    backgroundView.color = PREMIUM_BACKGROUND_COLOR
    accountTypeIcon.image = resources.getImage(PREMIUM_IMAGE_ID)
    }
    Function > Flow > Split by object

    View Slide

  53. What is wrong with "split by condition" 1/2
    Can write only ambiguous short summary
    /**
    * Updates views depending whether the account type is free or premium.
    */
    Function > Flow > Split by object

    View Slide

  54. What is wrong with "split by condition" 2/2
    No guarantee that all the combinations are covered
    when (accountType) {
    PREMIUM -> updateViewsForPremium()
    FREE -> updateViewsForFree()
    BUSINESS -> updateViewsForBusiness()
    }
    private fun updateViewsForBusiness() {
    backgroundView.color = PREMIUM_BACKGROUND_COLOR
    // [BUG] Forgot to update the account icon
    Function > Flow > Split by object

    View Slide

  55. Split by object: Good example 1/2
    fun bindViewData(accountType: AccountType) {
    updateBackgroundViewColor(accountType)
    updateAccountTypeImage(accountType)
    }
    private fun updateBackgroundViewColor(accountType: AccountType) {
    backgroundView.color = when (accountType) {
    PREMIUM -> PREMIUM_BACKGROUND_COLOR
    FREE -> FREE_BACKGROUND_COLOR
    }
    }
    Function > Flow > Split by object

    View Slide

  56. Split by object: Good example 2/2
    Easy to write a meaningful summary
    /**
    * Updates background color and icon according to a given account type.
    */
    Ensures that all the combinations are covered
    Can conduct further refactoring
    e.g., Make extracted functions reference transparent
    Function > Flow > Split by object

    View Slide

  57. Split by object: Further refactoring
    fun bindViewData(accountType: AccountType) {
    backgroundView.color = getBackgroundColorInt(accountType)
    accountTypeIcon.image = getAccountTypeIconImage(accountType)
    }
    private fun getBackgroundColorInt(accountType: AccountType): Int =
    when (accountType) {
    PREMIUM -> PREMIUM_BACKGROUND_COLOR
    FREE -> FREE_BACKGROUND_COLOR
    }
    Function > Flow > Split by object

    View Slide

  58. Early return VS split by object
    Does "early return" not represent "split by condition"?
    Strategy 1:
    - Merge unhappy paths into a happy path
    Strategy 2:
    - Apply early return first
    - Apply "split by object" for the happy path
    Function > Flow > Split by object

    View Slide

  59. Function flow: Summary
    Perform definition-based programming:
    Replace nest/chain/literal with named variables/functions
    Focus on a happy path:
    Apply early return
    Split by object, not condition:
    Make a function or block for each target object
    Function > Flow > Summary

    View Slide

  60. Summary
    Check whether it is easy to write a short summary
    Function responsibility:
    - Do not modify when returning a main result
    Function flow:
    - Make code understandable by scanning it
    - Definition-based programming, early return, split by object
    Function > Summary

    View Slide