Slide 1

Slide 1 text

abstract fun codeReadabilitySession5() fun Function()

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

Topics Make the function responsibility/flow clear Function > Introduction

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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