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

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

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

Munetoshi Ishikawa

May 11, 2023
Tweet

More Decks by Munetoshi Ishikawa

Transcript

  1. Brushup: Function 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
  2. 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 Dependency > Introduction
  3. What "dependency" is 1/2 Example: Class X depends on class

    Y - X has an instance of Y as a property - A method of X takes Y as a parameter or returns Y - X calls a method of Y - X inherits Y Y appears in X X depends on Y, roughly speaking Dependency > Introduction
  4. What "dependency" is 2/2 Is the word "dependency" only used

    for classes? No It may also be used for instances, functions, modules Some dependencies are difficult to find at a glance Dependency > Introduction
  5. Contents of Dependency I & II Dependency I: - Coupling

    Dependency II: - Direction - Redundancy - Explicitness Dependency > Introduction
  6. Contents of Dependency I & II Dependency I: - Coupling

    Dependency II: - Direction - Redundancy - Explicitness Dependency > Coupling
  7. Coupling 1/2 Indicates strength of dependency A weaker coupling is

    preferred The original definition7 is for dependencies between functions It is applied to dependencies between classes in this talk 7 Glenford J. Myers. 1975. Reliable software though composite design. Litton Educational Publishing, Inc. Dependency > Coupling
  8. Coupling 2/2 Content coupling Common coupling Control coupling Stamp coupling

    Data coupling Message coupling strong weak attention required acceptable External coupling Dependency > Coupling
  9. Topics Content coupling Common coupling Control coupling Stamp coupling Data

    coupling Message coupling strong weak attention required acceptable External coupling Dependency > Coupling > Content coupling
  10. Content coupling Depends on the internal workings - Jump into

    internal code directly - Modify internal status directly - Overwrite behavior directly It is not easy to do this with modern programming languages However, a similar behavior can be created Dependency > Coupling > Content coupling
  11. Equivalent to content coupling - Allows illegal usage - Shares

    internal mutable state Dependency > Coupling > Content coupling
  12. Equivalent to content coupling - Allows illegal usage - Shares

    internal mutable state Dependency > Coupling > Content coupling
  13. Allows illegal usage: Bad example class Caller { fun callCalculator()

    { calculator.calculate() ... Dependency > Coupling > Content coupling
  14. Allows illegal usage: Bad example class Caller { fun callCalculator()

    { calculator.parameter = 42 calculator.calculate() val result = calculator.result ... Dependency > Coupling > Content coupling
  15. Allows illegal usage: Bad example class Caller { fun callCalculator()

    { calculator.parameter = 42 calculator.prepare() calculator.calculate() calculator.tearDown() val result = calculator.result ... Dependency > Coupling > Content coupling
  16. Allows illegal usage: Bad points Easy to misuse calculator -

    Call prepare()/tearDown() before/after calling calculate() - Pass/get a value by parameter/result property. Not robust to implementation change Dependency > Coupling > Content coupling
  17. Allows illegal usage: How to fix - Encapsulate stateful call

    sequence - Pass/receive data as arguments or a return value Dependency > Coupling > Content coupling
  18. Allows illegal usage: Fixed code example class Caller { fun

    callCalculator() { val result = calculator.calculate(42) ... class Calculator { fun calculate(type: Int): Int { prepare() ... tearDown() return ... Dependency > Coupling > Content coupling
  19. Allows illegal usage: What to check Do not apply design

    concepts/principals too much - e.g., command/query separation Dependency > Coupling > Content coupling
  20. Equivalent to content coupling - Allows illegal usage - Shares

    internal mutable state Dependency > Coupling > Content coupling
  21. Shares mutable state: Bad example class UserListPresenter(val userList: List<UserData>) {

    fun refreshViews() { /* ... */ } class Caller { val userList: MutableList<UserData> = mutableListOf() val presenter = UserListPresenter(userList) ... fun addUser(newUser: UserData) { userList += newUser presenter.refreshViews() Dependency > Coupling > Content coupling
  22. Shares mutable state: Bad points UserListPresenter depends on an external

    mutable value - Causes unexpected state update by other reference holders - No limitation of modifying the list (e.g., addition/deletion) Easy to create an illegal state and hard to find the root cause - A reference to the presenter is not required to update it Dependency > Coupling > Content coupling
  23. Shares mutable state: How to fix - Make values immutable,

    copy arguments, or apply copy-on-write - Limit interfaces to update the state Dependency > Coupling > Content coupling
  24. Shares mutable state: Fixed code example class UserListPresenter { val

    userList: MutableList<UserData> = mutableListOf() fun addUsers(newUsers: List<UserData>) { userList += newUsers // Update views with `userList` } No need to worry about whether newUsers is mutable or not (if addUsers is atomic) Dependency > Coupling > Content coupling
  25. Shares mutable state: What to check Make a mutable value

    ownership clear Options: - Modify only by owner's interface - Create manager class of mutable data Dependency > Coupling > Content coupling
  26. Content coupling: Summary Don't rely on internal working - Remove

    illegal usage - Make mutable data ownership clear Dependency > Coupling > Content coupling
  27. Topics Content coupling Common coupling Control coupling Stamp coupling Data

    coupling Message coupling strong weak attention required acceptable External coupling Dependency > Coupling > Common/external coupling
  28. Common/external coupling Shares mutable global data - Pass data by

    global variables - Depend on a mutable singleton - (Use shared resources such as a file system or network) Common coupling: Shares data structure instance External coupling: Shares basic/primitive value Dependency > Coupling > Common/external coupling
  29. Common/external coupling Shares mutable global data - Pass data by

    global variables - Depend on a mutable singleton - (Use shared resources such as a file system or network) Dependency > Coupling > Common/external coupling
  30. Pass data by global variables: Bad example var parameter: Int?

    = null var result: Int? = null class Calculator { fun calculate() { result = parameter + ... fun callCalculator() { parameter = 42 calculator.calculate() Dependency > Coupling > Common/external coupling
  31. Pass data by global variables: Bad points No information/limitation of

    call relationships - Breaks other simultaneous calls - Tends to cause an illegal state Dependency > Coupling > Common/external coupling
  32. Pass data by global variables: How to fix Pass/receive values

    as function arguments or a return value Dependency > Coupling > Common/external coupling
  33. Pass data by global variables: Fixed code example class Calculator

    { fun calculate(parameter: Int): Int = parameter + ... fun callCalculator() { val result = calculator.calculate(42) Dependency > Coupling > Common/external coupling
  34. Common/external coupling Shares mutable global data - Pass data by

    global variables - Depend on a mutable singleton - (Use shared resources such as a file system or network) Dependency > Coupling > Common/external coupling
  35. Depend on a mutable singleton: Bad example val USER_DATA_REPOSITORY =

    UserDataRepository() class UserListUseCase { suspend fun invoke(): List<User> = withContext(...) { val result = USER_DATA_REPOSITORY.query(...) ... Dependency > Coupling > Common/external coupling
  36. Depend on a mutable singleton: Bad points Singleton is hard

    to manage - Unmanaged lifecycle, scope, and references - Low flexibility for specification change - Bad testability Dependency > Coupling > Common/external coupling
  37. Depend on a mutable singleton: How to fix Pass an

    instance as a constructor parameter ( Dependency injection) The constructor caller can manage the instance Dependency > Coupling > Common/external coupling
  38. Depend on a mutable singleton: Fixed code example class UserListUseCase(

    val userDataRepository: UserDataRepository ) { suspend fun invoke(): List<User> = withContext(...) { val result = userDataRepository.query(...) ... Dependency > Coupling > Common/external coupling
  39. Common/external coupling: Note Apply the same discussion to a smaller

    scope - module, package, class... Example: Avoid passing values using instance properties Dependency > Coupling > Common/external coupling
  40. Common/external coupling in a class: Bad example class Klass {

    var parameter: Int? = null fun firstFunction() { parameter = 42 secondFunction() } fun secondFunction() { /* Use `parameter` here... */ } Dependency > Coupling > Common/external coupling
  41. Common/external coupling in a class: Fixed code class Klass {

    fun firstFunction() { val argument = 42 secondFunction(argument) } fun secondFunction(parameter: Int) { /* Use `parameter` here... */ } Dependency > Coupling > Common/external coupling
  42. Common/external coupling in a class: Summary Avoid using mutable singletons

    or global variables - Unmanageable, not robust, hard to test To avoid common/external coupling: - Pass/receive values as arguments or a return value - Use dependency injection Dependency > Coupling > Common/external coupling
  43. Topics Content coupling Common coupling Control coupling Stamp coupling Data

    coupling Message coupling strong weak attention required acceptable External coupling Dependency > Coupling > Control coupling
  44. Control coupling Determines "what to do" using arguments Often unavoidable

    in software development Control coupling should be mitigated in some cases Dependency > Coupling > Control coupling
  45. Control coupling to be mitigated - Conditional branches with unnecessarily

    large scope - How to fix - Conditional branches with little behavior relevance - How to fix 1 - How to fix 2 Dependency > Coupling > Control coupling
  46. Control coupling to be mitigated - Conditional branches with unnecessarily

    large scope - How to fix - Conditional branches with little behavior relevance - How to fix 1 - How to fix 2 Dependency > Coupling > Control coupling
  47. Branches with large scope fun updateView(isError: Boolean) { if (isError)

    { resultView.isVisible = true errorView.isVisible = false iconView.image = CROSS_MARK_IMAGE } else { resultView.isVisible = false errorView.isVisible = true iconView.image = CHECK_MARK_IMAGE } } Dependency > Coupling > Control coupling
  48. Branches with large scope: What is wrong - Need to

    read all conditional branches to understand the behavior - Not robust to implementation change Dependency > Coupling > Control coupling
  49. Control coupling to be mitigated - Conditional branches with unnecessarily

    large scope - How to fix: Split by object, not condition - Conditional branches with little behavior relevance - How to fix 1 - How to fix 2 Dependency > Coupling > Control coupling
  50. Branches with large scope: How to fix Split by object,

    not condition (introduced last session) - Split scope of control coupling Confirm that target objects are common for each condition - updateIf... may be applicable when partially different Dependency > Coupling > Control coupling
  51. Branches with large scope: Fixed code example fun updateView(isError: Boolean)

    { if (isError) { resultView.isVisible = true errorView.isVisible = false iconView.image = CROSS_MARK_IMAGE } else { resultView.isVisible = false errorView.isVisible = true iconView.image = CHECK_MARK_IMAGE } } Dependency > Coupling > Control coupling
  52. Branches with large scope: Fixed code example fun updateView(isError: Boolean)

    { resultView.isVisible = !isError errorView.isVisible = isError iconView.image = getIconImage(isError) } fun getIconImage(isError: Boolean): Image = if (!isError) CHECK_MARK_IMAGE else CROSS_MARK_IMAGE Dependency > Coupling > Control coupling
  53. Control coupling to be mitigated - Conditional branches with unnecessarily

    large scope - How to fix: Split by object, not condition - Conditional branches with little behavior relevance - How to fix 1 - How to fix 2 Dependency > Coupling > Control coupling
  54. Branches with little relevance: Bad example 1/2 sealed class DataType

    { class UserName(...) : DataType() class BirthDate(...) : DataType() class ProfileImage(...) : DataType() } Dependency > Coupling > Control coupling
  55. Branches with little relevance: Bad example 2/2 fun updateUserView(dataType: DataType)

    = when(dataType) { is DataType.UserName -> { val userName = getUserName(dataType.userId) userNameView.text = userName } is DataType.Birthday -> { val birthDate = ... birthDayView.text = format(birthDate, ...) } is DataType.ProfileImage -> { ... } } Dependency > Coupling > Control coupling
  56. Branches with little relevance: Bad points - Need to read

    all branches to understand the behavior - Not easy to map caller condition to callee behavior updateUserView(DataType.ProfileImage(...)) fun updateUserView(dataType: DataType) = when(dataType) { is DataType.UserName -> { ... } // Need to check the condition... is DataType.Birthday -> { ... } is DataType.ProfileImage -> { ... } // Finally found Dependency > Coupling > Control coupling
  57. Control coupling to be mitigated - Conditional branches with unnecessarily

    large scope - How to fix: Split by object, not condition - Conditional branches with little behavior relevance - How to fix 1: Remove conditional branches - How to fix 2 Dependency > Coupling > Control coupling
  58. Remove conditional branches Split function for each condition Applicable if

    a condition is statically determined - If not, causes another control coupling on the caller side Dependency > Coupling > Control coupling
  59. Remove conditional branches: Applicable example Caller location determines the condition

    class Caller1 { fun caller1() = presenter.updateUserView(DataType.UserName(...)) } class Caller2 { fun caller2() = presenter.updateUserView(DataType.BirthDate(...)) } Dependency > Coupling > Control coupling
  60. Remove conditional branches: Fixed code example fun updateUserView(dataType: DataType) =

    when(dataType) { is DataType.UserName -> { val userName = getUserName(dataType.userId) userNameView.text = userName } is DataType.Birthday -> { val birthDate = ... birthDayView.text = format(birthDate, ...) } is DataType.ProfileImage -> { ... } } Dependency > Coupling > Control coupling
  61. Remove conditional branches: Fixed code example fun updateUserNameView(...) { val

    userName = getUserName(...) userNameView.text = userName } fun updateBirthdayView(...) { val birthDate = ... birthDayView.text = format(birthDate, ...) } fun updateProfileImage(...) { ... Dependency > Coupling > Control coupling
  62. Control coupling to be mitigated - Conditional branches with unnecessarily

    large scope - How to fix: Split by object, not condition - Conditional branches with little behavior relevance - How to fix 1: Remove conditional branches - How to fix 2: Replace with other structures Dependency > Coupling > Control coupling
  63. Replace with other structures Create classes/values to encapsulate the behavior

    Example: Strategy pattern - Replace branches with polymorphism or parameters - Implement by enum, abstract class, callback object, etc. - Other methods preferred because of the structure complexity Dependency > Coupling > Control coupling
  64. Replace with other structures: Fixed code example enum class Binder(val

    viewId: ViewId) { fun updateView(holder: ViewHolder) = setContent(holder.getView(viewId)) abstract fun setContent(view: View) Dependency > Coupling > Control coupling
  65. Replace with other structures: Fixed code example enum class Binder(val

    viewId: ViewId) { USER_NAME(USER_NAME_VIEW_ID) { override fun setContent(...) = ... } BIRTHDAY(BIRTHDAY_VIEW_ID) { override fun setContent(...) = ... ... fun updateView(holder: ViewHolder) = setContent(holder.getView(viewId)) abstract fun setContent(view: View) Dependency > Coupling > Control coupling
  66. Replace with other structures: Fixed code example Consider the scope

    of encapsulation - Do not add feature-specific logic into a widely-used class - Split logic from model classes if required Dependency > Coupling > Control coupling
  67. Control coupling: Summary Avoid branches with unnecessarily large scope or

    little relevance Options to mitigate control coupling - When target objects are common: Split by object, not condition - When condition is statically determined: Split function - Otherwise: Replace with other structures (e.g., strategy pattern) Dependency > Coupling > Control coupling
  68. Topics Content coupling Common coupling Control coupling Stamp coupling Data

    coupling Message coupling strong weak attention required acceptable External coupling Dependency > Coupling > Stamp coupling
  69. Stamp coupling Passes/receives instances of data structures May be acceptable

    even if there are unused properties: - To use polymorphism, strategy pattern, etc - To clarify the meaning or simplify arguments or return values Dependency > Coupling > Stamp coupling
  70. Stamp coupling: Example updateUserView(userData) fun updateUserView(userData: UserData) { // Some

    property of `userData` are used userNameView.text = userData.fullName profileImage.setImageUrl(userData.profileImageUrl) // `userData.mailAddress` and `.phoneNumber` are not used } Dependency > Coupling > Stamp coupling
  71. Topics Content coupling Common coupling Control coupling Stamp coupling Data

    coupling Message coupling strong weak attention required acceptable External coupling Dependency > Coupling > Data coupling
  72. Data coupling Passes/receives only basic type values8 fun updateUserView(fullName: String,

    profileImageUrl: String) { userNameView.text = fullName profileImage.setImageUrl(profileImageUrl) } updateUserView(userData.fullName, userData.profileImageUrl) 8 Strictly speaking, String can be considered as a list/array of characters Dependency > Coupling > Data coupling
  73. Data coupling: Caution Stamp coupling is sometimes preferred over data

    coupling - To clarify meaning of parameters - To create parameters or return value type safe fun updateUserView(fullName: String, profileImageUrl: String) = ... updateUserView(imageUrl, fullName) // !!! updateUserView(itemName, itemImageUrl) // !!! updateUserView(userA.fullName, userB.imageUrl) // !!! Dependency > Coupling > Data coupling
  74. Topics Content coupling Common coupling Control coupling Stamp coupling Data

    coupling Message coupling strong weak attention required acceptable External coupling Dependency > Coupling > Message coupling
  75. Message coupling Calls a function without any parameters or return

    value - e.g., calling an idempotent function such as Closable.close - Note the possibility of content coupling at another place fun notifyUserDataUpdated() { userNameView.text = ... profileImage.setImageUrl(...) } notifyUserDataUpdated() Dependency > Coupling > Message coupling
  76. Coupling: Summary Use weaker coupling - Stamp coupling or data

    coupling preferred Coupling to be aware of: - Content coupling: Depends on internal working - Common/external coupling: Shares mutable global data - Control coupling: Determines "what to do" using arguments Dependency > Coupling > Summary