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

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

110

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

Munetoshi Ishikawa

May 11, 2023
Tweet

Transcript

  1. Brushup: Coupling 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" by arguments
  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. Contents of Dependency I & II Dependency I: - Coupling

    Dependency II: - Direction - Redundancy - Explicitness Dependency > Direction
  4. Dependency direction Keep dependencies acyclic as far as possible Class

    A Class C Class B cyclic Class A Class C Class B acyclic Dependency > Direction
  5. Preferred dependency direction - caller → callee - concrete →

    abstract - complex → simple - mutable → immutable - unstable → stable - algorithm → data model Dependency > Direction
  6. Preferred dependency direction - caller → callee - concrete →

    abstract - complex → simple Dependency > Direction
  7. Preferred dependency direction - caller → callee - concrete →

    abstract - complex → simple Dependency > Direction > Caller to callee
  8. Caller and callee: Bad example 1/2 class MediaViewPresenter { fun

    getVideoUri(): Uri = ... fun playVideo() { videoPlayerView.play(this) ... class VideoPlayerView { fun play(presenter: MediaViewPresenter) { val uri = presenter.getVideoUri() Dependency > Direction > Caller to callee
  9. Caller and callee: Bad example 2/2 MediaViewPresenter fun getVideoUri VideoPlayerView

    fun play calls play calls getVideoUri Dependency > Direction > Caller to callee
  10. Caller and callee: Bad points Difficult to understand the behavior

    The call sequence of playVideo is complex Large impact on modification Changes in MediaViewPresenter affect VideoPlayerView Hard to write tests Mock object of VideoPlayerView will be complicated Dependency > Direction > Caller to callee
  11. Caller and callee: How to fix Remove unnecessary callback -

    Pass values as arguments - Extract small classes Dependency > Direction > Caller to callee
  12. Caller and callee: How to fix Remove unnecessary callback -

    Pass values as arguments - Extract small classes Dependency > Direction > Caller to callee
  13. Pass values as arguments class MediaViewPresenter { fun getVideoUri(): Uri

    = ... fun playVideo() { videoPlayerView.play(this) ... class VideoPlayerView { fun play(presenter: MediaViewPresenter) { val uri = presenter.getVideoUri() Dependency > Direction > Caller to callee
  14. Pass values as arguments class MediaViewPresenter { fun getVideoUri(): Uri

    = ... fun playVideo() { videoPlayerView.play(getVideoUri()) ... class VideoPlayerView { fun play(videoUri: Uri) { ... Dependency > Direction > Caller to callee
  15. Caller and callee: How to fix Remove unnecessary callback -

    Pass values as arguments - Extract small classes Dependency > Direction > Caller to callee
  16. Extract small classes Cannot pass value directly for asynchronous evaluation

    Extract depended logic as a class Dependency > Direction > Caller to callee
  17. Extract small classes: Step 1 Create a named class for

    the callback MediaViewPresenter VideoPlayerView VideoUriProvider fun getVideoUri Dependency > Direction > Caller to callee
  18. Extract small classes: Step 2 Move the named class out

    of the outer class MediaViewPresenter VideoPlayerView VideoUriProvider fun getVideoUri Dependency > Direction > Caller to callee
  19. Caller and callee: How to fix Remove unnecessary callback -

    Pass values as arguments - Extract small classes Dependency > Direction > Caller to callee
  20. Caller and callee: Exception Dependency on a caller may be

    acceptable for some cases - Asynchronous call: thread, event listener - Higher order function as a utility: List.forEach Dependency > Direction > Caller to callee
  21. Mitigating asynchronous calls Limit purpose/lifecycle/scope of a callback - promise

    pattern, future object Use language features or libraries for asynchronous calls - coroutine, actor, async/await Dependency > Direction > Caller to callee
  22. Preferred dependency direction - caller → callee - concrete →

    abstract - complex → simple Dependency > Direction > Concrete to abstract
  23. Concrete and abstract Do not depend on inheritances = Do

    not check type of this or self Dependency > Direction > Concrete to abstract
  24. Concrete and abstract: Bad example open class IntList { fun

    addElement(value: Int) { if (this is ArrayIntList) { ... } else { ... ... class ArrayIntList: IntList() { ... Dependency > Direction > Concrete to abstract
  25. Concrete and abstract: Bad points Ambiguous scope of the responsibility

    Not robust against modification - For inheritance specification change - To add a new inheritance Dependency > Direction > Concrete to abstract
  26. Concrete and abstract: How to fix Remove downcast with overriding

    open class IntList { open fun addElement(value: Int) { ... class ArrayIntList: IntList() { override fun addElement(value: Int) { ... Dependency > Direction > Concrete to abstract
  27. Preferred dependency direction - caller → callee - concrete →

    abstract - complex → simple Dependency > Direction > Complex to simple
  28. Complex and simple: Bad example class UserData( val userId: UserId,

    ... ) class UserDataRequester { fun obtainFromServer(): UserData { ... Dependency > Direction > Complex to simple
  29. Complex and simple: Bad example class UserData( val userId: UserId,

    ... val requester: UserDataRequester ) class UserDataRequester { fun obtainFromServer(): UserData { ... Dependency > Direction > Complex to simple
  30. Complex and simple: Bad points Hard to manage scope/lifecycle/references of

    complex code - Simple: Scope/lifecycle can be _small to large - Complex: Scope/lifecycle must be managed strictly Dependency on complex code may cause: - Resource leaks - implicit preconditions for simple code Dependency > Direction > Complex to simple
  31. Complex and simple: How to fix Just remove dependency of

    "simple to complex" - Pass a complex class instance directly if required class UserData( val userId: UserId, ... ) Dependency > Direction > Complex to simple
  32. Complex and simple: Exceptions It is sometimes necessary to depend

    on complex types - Mediator pattern - Adapter/facade - Data binder Limit objective/scope as far as possible Dependency > Direction > Complex to simple
  33. Preferred dependency direction - caller → callee - concrete →

    abstract - complex → simple Dependency > Direction > Summary
  34. Dependency direction: Summary Maintain acyclic dependency - caller → callee

    - complex, concrete, mutable → simple, abstract, immutable Exceptions: asynchronous calls, mediator patterns, etc. Limit the objective/scope, even for the exceptions Dependency > Direction > Summary
  35. Contents of Dependency I & II Dependency I: - Coupling

    Dependency II: - Direction - Redundancy - Explicitness Dependency > Redundancy
  36. Redundant dependency Cascaded dependency - Nested & indirect dependency Redundant

    dependency set - N:M dependency Dependency > Redundancy
  37. Redundant dependency Cascaded dependency - Nested & indirect dependency Redundant

    dependency set - N:M dependency Dependency > Redundancy
  38. Cascaded dependency Do not depend on unrelated code only for

    getting a reference MessageTextPresenter TimestampPresenter has instance (only for MessageDataProvider) MessageDataProvider has instance depends indirectly Dependency > Redundancy > Cascaded dependency
  39. Cascaded dependency: Bad example class TimestampPresenter { val dataProvider: MessageDataProvider

    = ... fun invalidateViews() { // Update timestamp by `dataProvider` ... class MessageTextPresenter { val timestampPresenter: TimestampPresenter = ... fun invalidateViews() { val messageData = timestampPresenter.dataProvider... // Update message text view by `messageData` Dependency > Redundancy > Cascaded dependency
  40. Cascaded dependency: Bad points Unnecessary dependency MessageTextPresenter is unrelated with

    TimestampPresenter Indirect dependency MessageTextPresenter does not have MessageDataProvider directly Dependency > Redundancy > Cascaded dependency
  41. Cascaded dependency: How to fix Flatten nested dependencies by means

    of direct dependency MessageTextPresenter TimestampPresenter MessageDataProvider has instance Dependency > Redundancy > Cascaded dependency
  42. Cascaded dependency: Fixed code example class TimestampPresenter { val dataProvider:

    MessageDataProvider = ... fun invalidateViews() { // Update timestamp by `dataProvider` ... class MessageTextPresenter { val dataProvider: MessageDataProvider = ... fun invalidateViews() { // Update message text view by `dataProvider` Dependency > Redundancy > Cascaded dependency
  43. Redundant dependency Cascaded dependency - Nested & indirect dependency Redundant

    dependency set - N:M dependency Dependency > Redundancy > Dependency set
  44. Redundant dependency set Avoid duplication of "depending class set" MessageTextPresenter

    TimestampPresenter LocalMessageDataProvider RemoteMessageDataProvider Dependency > Redundancy > Dependency set
  45. Redundant dependency set: Bad points Fragile for modification - When

    a new depending/depended class is added - Because of code duplication Dependency > Redundancy > Dependency set
  46. Redundant dependency set: How to fix Create an intermediate layer

    to consolidate the dependency set MessageTextPresenter TimestampPresenter LocalMessageDataProvider RemoteMessageDataProvider MessageDataProvider Dependency > Redundancy > Dependency set
  47. Redundant dependency set: Note Do not create a layer if

    you do not need it now Keep "YAGNI" and "KISS" in mind Do not provide access to a hidden layer Never make a public property serverMessageDataProvider in MessageDataProvider Dependency > Redundancy > Dependency set
  48. Redundant dependency set: Summary - Do not depend on unrelated

    code only for getting a reference - Create an intermediate layer to consolidate the dependency set Dependency > Redundancy > Summary
  49. Contents of Dependency I & II Dependency I: - Coupling

    Dependency II: - Direction - Redundancy - Explicitness Dependency > Explicitness
  50. Implicit dependency 1/3 Dependency that is not discoverable only by

    class definitions interface Interface class Implementation : Interface class Caller(val interface: Interface) Caller may be affected by modification in Implementation Caller(Implementation()) Dependency > Explicitness
  51. Implicit dependency 2/3 Not discoverable by class diagram Caller Interface

    Implementation has instance inherits Dependency > Explicitness
  52. Implicit dependency 2/3 Use explicit dependencies instead of implicit dependencies

    To clarify affected scope of modification Different from "weak coupling" - Trying to use weak coupling may cause implicit dependency Dependency > Explicitness
  53. Implicit dependency: Example - Unnecessary abstraction - Implicit domain of

    variable Dependency > Explicitness > Unnecessary abstraction
  54. Unnecessary abstraction Caller may depend on a child class implicitly

    ProfilePresenter MessagePresenter StringProvider ProfileDataRepository MessageDataRepository Dependency > Explicitness > Unnecessary abstraction
  55. Unnecessary abstraction: Bad example interface StringProvider { fun queryString(id: Int):

    String } class ProfileDataRepository : StringProvider { /** Returns user name of the given [userId] */ override fun queryString(userId: Int): String = ... } class ProfilePresenter(val nameProvider: StringProvider, ...) { fun updateProfileView(userId: Int) { userNameView.text = nameProvider.queryString(userId) ... Dependency > Explicitness > Unnecessary abstraction
  56. Unnecessary abstraction: Bad points Time-consuming to track the call sequence

    Requires checking the constructor caller A wrong instance may be passed Compiles even if MessageDataRepository is passed Dependency > Explicitness > Unnecessary abstraction
  57. Unnecessary abstraction: How to fix Remove unnecessary subtyping Use child

    class directly with removing the parent/interface ProfilePresenter MessagePresenter ProfileDataRepository MessageDataRepository Dependency > Explicitness > Unnecessary abstraction
  58. Unnecessary abstraction: Fixed code example class ProfileDataRepository { fun queryUserName(userId:

    Int): String = ... } class ProfilePresenter(val repository: ProfileDataRepository, ...) { fun updateProfileView(userId: Int) { userNameView.text = repository.queryUserName(userId) ... Dependency > Explicitness > Unnecessary abstraction
  59. Unnecessary abstraction: Note 1/2 Clarify objective of subtyping - Polymorphism

    - Sum type: sealed class - Dependency inversion, implement separation: module/library Dependency > Explicitness > Unnecessary abstraction
  60. Unnecessary abstraction: Note 2/2 Avoid using subtyping only to make

    code common - Consider using aggregation or composition instead Consider using another method to achieve polymorphism - Parametric: generics, etc. - Adhoc: overloading, type-class, etc. Dependency > Explicitness > Unnecessary abstraction
  61. Implicit dependency: Example - Unnecessary abstraction - Implicit domain of

    variable Dependency > Explicitness > Implicit domain of variable
  62. Implicit domain of variable: Bad example Question: What is wrong

    with the following code? fun setViewBackgroundColor(colorString: String) { val colorCode = when(colorString) { "red" -> 0xFF0000 "green" -> 0x00FF00 else -> 0x000000 } view.setBackgroundColor(colorCode) } Dependency > Explicitness > Implicit domain of variable
  63. Implicit domain of variable: Bad points An implicit constraint between

    the caller and callee Need to know which value is accepted "blue", "RED", "FF0000" are invalid Fragile for modification Need to update the caller when a color is added/removed Dependency > Explicitness > Implicit domain of variable
  64. Implicit domain of variable: How to fix Create a model

    type representing the domain enum class BackgroundColor(val colorCode: Int) { RED(0xFF0000), GREEN(0x00FF00) } fun setViewBackgroundColor(color: BackgroundColor) { view.setBackgroundColor(color.colorCode) } Dependency > Explicitness > Implicit domain of variable
  65. Dependency explicitness: Summary Use explicit dependency - Make dependency visible

    on class definition/diagram - Do not be confused with weak coupling To make dependency explicit - Remove unnecessary abstraction layer - Create a model type representing the domain Dependency > Explicitness > Summary
  66. Summary ɹ Coupling: Mitigate content/common/external/control coupling Direction: Make dependency acyclic

    or encapsulate the cycle Redundancy: Avoid duplication of dependency Explicitness: Make dependency visible on the definition/diagram Dependency > Summary