Writing Code That Lasts Forever (Droidcon NYC 2018)

Writing Code That Lasts Forever (Droidcon NYC 2018)

Video: https://www.youtube.com/watch?v=YZstpc2939s
Code: https://github.com/swankjesse/maintainability

Developers are perpetually fighting yesterday’s code. We need to conquer our immortal fears and build programs that evolve gracefully.

In this talk we’ll:
🗿 Discuss code that anticipates the future
🗿 Determine when to adopt frameworks and when not to!
🗿 Defy object-oriented principles
🗿 Speak about how good English leads to good Kotlin
🗿 Learn who will leave a legacy of code, and who will leave because of legacy code

This talk addresses some timeless problems in software development. Attendees will gain a permanent understanding of how to write maintainable code.

69252b3de5cb7f464c09301d9a6b0401?s=128

Jesse Wilson

August 28, 2018
Tweet

Transcript

  1. @jessewilson https://github.com/swankjesse/maintainability Writing Code That Lasts Forever

  2. Forever?

  3. • Maintainable: it’s ready for changes • Lasts Forever: it’s

    always ready for changes
  4. 1.0 1.1 1.2 1.3 1.4 1.5 1.6

  5. • There are many talks on maintainability – this one’s

    mine! • Not comprehensive • Ideas I think are important • Or that you might not have seen elsewhere 7 + 7 Strategies
  6. Incentives STRATEGY #1

  7. 1. Figure out what you value 2. Adjust incentives to

    reinforce those values
  8. • Don’t tell your team “write more tests” • Do

    find & remove barriers to writing tests
  9. Incremental Changes STRATEGY #2

  10. • Long-lived feature branches are bad! • Cherry-picking bug fixes

    to the release branch is bad • But releasing ½ of a feature is also bad! • Branch by abstraction: features can be turned on & off at runtime
  11. • master is always stable & shippable! • Per-feature betas

    • Decouple features & releases Why Branch By Abstraction?
  12. enum class BiometricAuth { NONE, FINGERPRINT, FINGERPRINT_OR_IRIS }

  13. interface Experimenter { fun <T : Enum<T>> getBucket(experiment: KClass<T>): T

    }
  14. class AuthScreen { @Inject lateinit var experimenter: Experimenter fun showBiometricAuth()

    { when (experimenter.getBucket(BiometricAuth::class)) { NONE -> skipBiometricAuth() FINGERPRINT -> showOldFingerprintAuth() FINGERPRINT_OR_IRIS -> showNewBiometricAuth() } } }
  15. Cash Debug Settings SendCashSuccessAnimation FADE WIPE SHREK_AND_FIONA ✔ BiometricAuth NONE

    FINGERPRINT FINGERPRINT_OR_IRIS ✔ NotificationSounds CHACHING BEEPS_AND_BOOPS BATMAN ✔ 15:17 https://github.com/JakeWharton/u2020
  16. class AuthTest { @Inject lateinit var authScreen: AuthScreen @Inject lateinit

    var experimenter: FakeExperimenter @Test fun testNewBiometricAuth() { experimenter.setDefault(BiometricAuthentication.FINGERPRINT_OR_IRIS) ... } }
  17. Experiments Admin BiometricAuth NONE FINGERPRINT FINGERPRINT_OR_IRIS 5 95 0 SAVE

    SendCashSuccessAnimation FADE WIPE SHREK_AND_FIONA 100 0 0 SAVE https://admin.cashapp.square/experiments Experiments Admin
  18. • Deploy server + web code every day (or more

    frequently!) • Release mobile apps every 2 weeks • Didn’t finish your feature in time?
 Another release is imminent! Release Trains
  19. 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9

    1.10 1.11 1.12 1.13 1.14 1.15
  20. Learning Organizations STRATEGY #3

  21. • What features should we build? • How should they

    work? • What architecture should we use? • Which libraries should be adopt? Research & Development
  22. Perfect is Too Slow & Too Difficult

  23. • Try lots of things & see what works •

    Lower the cost of making mistakes • Tight feedback loops! Learning Means Making Mistakes
  24. Tools & Leverage STRATEGY #4

  25. • Example: gRPC • gRPC is an alternative to REST

    + JSON. It makes it faster and easier to define server endpoints Infrastructure Investments
  26. • Set up time: • REST: 0 hours (done) •

    gRPC: 80 hours • gRPC: 1 hour • Each new endpoint: • REST: 8 hours Number of Endpoints 0 4 8 12 16 Hours Invested 0 16 32 48 64 80 96
  27. • If it takes 8 hours to add an endpoint,

    you’ll avoid adding endpoints when you need ’em • But if it only takes 1 hour you’ll add lots! Induced Demand! Number of Endpoints 0 4 8 12 16 Hours Invested 0 16 32 48 64 80 96
  28. • Faster compiler? compile more! • Easier to write tests?

    test more! • Less severe errors? experiment more!
  29. • Slow code reviews? Fewer pull requests! Suppressed Demand

  30. Know Your Problem Domain STRATEGY #5

  31. • A good rule in general • But it’s even

    better if you know what you’re going to need! “You Ain’t Gonna Need It” YAGNI
  32. • Understanding the business is a superpower • Understanding the

    users’ needs is a superpower • You’ll be able to anticipate what You’re Gonna Need!
  33. Modeling STRATEGY #6

  34. • Bad models make everything difficult • Good models get

    out of your way • Models frame how you think about a problem • Changing models is a lot of work! Modeling is Critical
  35. • Users, your revenue team, your growth team, your legal

    team, etc. • Good models fit the stakeholders’ needs • And potentially recognize when they’re in conflict Stakeholders
  36. • Don’t ask male/female if you just want pronouns •

    If you collect a country without a particular purpose, you’re oversimplifying The World Isn’t Discrete
  37. Value Objects, Service Objects, & Glue STRATEGY #7

  38. • They’re data & can be encoded using JSON, SQL,

    etc. • Preferably immutable • Don’t do anything fancy: no I/O, no RxJava, no threading • Kotlin has built-in support via data classes • Might be simple enough to not need tests! • Examples: Paint, SendMoneyRequest, SettingsModel Value Objects
  39. • Do work & have side-effects (screen, file system, network)

    • May have injected dependencies of other service objects • May be worth splitting into interface + implementation • Test these! • Offer fake implementations too • Examples: Animator, SendMoneyService, HistoryDatabase Service Objects
  40. • The application’s plumbing • Awkward to test • Examples:

    activities, dagger modules, event listeners Glue
  41. • Architecture patterns (MVC, MVVM, etc.) specify how to combine

    these to create UIs • Virtual DOM is powerful because it uses value objects for views • Primary benefit of React/Native • And it’s why I’m excited about Domic 3 Kinds of Objects & Android https://github.com/lyft/domic
  42. English, then Code STRATEGY #8

  43. Writing is thinking Why is writing so hard?

  44. • Tell you what is scope and what’s out of

    scope • android.graphics.Paint is a bad name because it includes too much: text alignment, stroke width, and text measurement • Use a thesaurus! Naming Matters
  45. Shipley’s Law STRATEGY #9

  46. Less code is better code ∴ No code is the

    best code Wil Shipley
  47. The Type System Works for You STRATEGY #10

  48. • JavaScript programmers: types are a lot of work for

    what you get • Java programmers: I need separate types for OnItemClickListener and OnItemLongClickListener
  49. interface GeoLocationService { fun latest(): GeoLocation }

  50. interface GeoLocationService { fun latest(): GeoLocation /** * For testing

    only. This doesn't actually teleport the device * to a new location. It merely overrides what [latest] returns. */ fun teleport(newLocation: GeoLocation) }
  51. Organize By Feature STRATEGY #11

  52. Organizing by Technology com.roundsapp models persistence services viewmodels views fragments

    activities • Redundant • For example, your views already have “View” in the class name
  53. Organizing by Feature com.roundsapp gamesummary scorekeeping gamelist creategame • Simplifies

    extracting features into submodules • Rewriting a feature? Start by copy- pasting its package! • Retiring a feature? Easy to find what to delete
  54. Building Blocks STRATEGY #12

  55. Dimetrodon Diplodocus Triceratops Stegosaurus Velociraptor Brachiosaurus Ankylosaurus Edmontosaurus Pterodactyl Dinosaur

    Extinct! 15:24
  56. Stegosaurus Diplodocus Velociraptor Triceratops Brachiosaurus Ankylosaurus Edmontosaurus Pterodactyl Tyrannosaurus Dinosaur

    Extinct! UNDO 15:24
  57. Toast.makeText(getActivity(), "Dinosaur Extinct!", Toast.LENGTH_LONG).show()

  58. public class UndoToast { public interface OnDismissListener { void onDismiss(View

    view, Parcelable token); } private final Context mContext; private final View mView; private final TextView mTextView; private Style mStyle; private OnDismissListener mOnDismissListener; public UndoToast(@NonNull Context context) { this.mContext = context; this.mStyle = new Style(); this.mStyle.type = Style.TYPE_STANDARD; final LayoutInflater layoutInflater = (LayoutInflater) context .getSystemService(Context.LAYOUT_INFLATER_SERVICE); this.mView = onCreateView(context, layoutInflater, Style.TYPE_STANDARD); this.mTextView = (TextView) this.mView.findViewById(R.id.message); } protected UndoToast(@NonNull Context context, @Style.Type int type) { this.mContext = context; this.mStyle = new Style(); this.mStyle.type = type; final LayoutInflater layoutInflater = (LayoutInflater) context .getSystemService(Context.LAYOUT_INFLATER_SERVICE); this.mView = onCreateView(context, layoutInflater, type); this.mTextView = (TextView) this.mView.findViewById(R.id.message); } protected UndoToast(@NonNull Context context, @NonNull Style style, @Style.Type int type) { this.mContext = context; this.mStyle = style; this.mStyle.type = type; final LayoutInflater layoutInflater = (LayoutInflater) context .getSystemService(Context.LAYOUT_INFLATER_SERVICE); this.mView = onCreateView(context, layoutInflater, type); this.mTextView = (TextView) this.mView.findViewById(R.id.message); } protected UndoToast(@NonNull Context context, @NonNull Style style, @Style.Type int type, @IdRes int viewGroupID) { this.mContext = context; this.mStyle = style; this.mStyle.type = type; // TYPE_BUTTON styles are the only ones that look different from the styles set by the Style() constructor if (type == Style.TYPE_BUTTON) { this.mStyle.yOffset = BackgroundUtils.convertToDIP(24); this.mStyle.width = FrameLayout.LayoutParams.MATCH_PARENT; } final LayoutInflater layoutInflater = (LayoutInflater) context .getSystemService(Context.LAYOUT_INFLATER_SERVICE); this.mView = onCreateView(context, layoutInflater, type); this.mTextView = (TextView) this.mView.findViewById(R.id.message);
  59. • An inventory of classes that can be composed to

    build things • Rearrange ’em to solve new problems Maintainable APIs work like Lego
  60. val request = Request.Builder() .url("https://lego.com/") .build() val call = client.newCall(request)

    call.execute().use { println(it.body().string()) } 1 2 3 404 flickr.com/photos/blakespot
  61. Policy & Precedence STRATEGY #14

  62. • Code that implements product decisions • Can be visible:

    character limit of a Tweet • Or hidden: you can’t send the same tweet twice • Or implicit: the call times out after 30 seconds and is retried twice Policy
  63. • No size limit on your profile photo? The universe

    will impose its own limit and you will be sad No Policy is a Policy
  64. • In school we’re taught to be avoid magic numbers

    • In practice: don’t bury policy Maintainable Policies
  65. @Inject @Named("base") lateinit var baseUrl: HttpUrl @Inject lateinit var client:

    OkHttpClient fun upload(file: File, endpoint: String): Boolean { val options = BitmapFactory.Options() options.inJustDecodeBounds = true BitmapFactory.decodeFile(file.path!!, options) require(options.outWidth < 5000 && options.outHeight < 5000) val request = Request.Builder() .url(baseUrl.resolve(endpoint)!!) .post(RequestBody.create(MediaType.parse(options.outMimeType), file)) .build() // Extend timeouts for large uploads. See IMAGINARY-3211. val uploadClient = client.newBuilder() .writeTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .build() val call = uploadClient.newCall(request) ...
  66. class ImageUploadPolicy { fun checkBounds(width: Int, height: Int) { require(width

    < 5000 && height < 5000) } }
  67. require(options.outWidth < 5000 && options.outHeight < 5000) @Inject @Named("base") lateinit

    var baseUrl: HttpUrl @Inject lateinit var client: OkHttpClient fun upload(file: File, endpoint: String): Boolean { val options = BitmapFactory.Options() options.inJustDecodeBounds = true BitmapFactory.decodeFile(file.path!!, options) val request = Request.Builder() .url(baseUrl.resolve(endpoint)!!) .post(RequestBody.create(MediaType.parse(options.outMimeType), file)) .build() // Extend timeouts for large uploads. See IMAGINARY-3211. val uploadClient = client.newBuilder() .writeTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .build() ...
  68. require(options.outWidth < 5000 && options.outHeight < 5000) @Inject @Named("base") lateinit

    var baseUrl: HttpUrl @Inject lateinit var client: OkHttpClient fun upload(file: File, endpoint: String): Boolean { val options = BitmapFactory.Options() options.inJustDecodeBounds = true BitmapFactory.decodeFile(file.path!!, options) val request = Request.Builder() .url(baseUrl.resolve(endpoint)!!) .post(RequestBody.create(MediaType.parse(options.outMimeType), file)) .build() // Extend timeouts for large uploads. See IMAGINARY-3211. val uploadClient = client.newBuilder() .writeTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .build() ... @Inject lateinit var imageUploadPolicy: ImageUploadPolicy
  69. @Inject @Named("base") lateinit var baseUrl: HttpUrl @Inject lateinit var client:

    OkHttpClient fun upload(file: File, endpoint: String): Boolean { val options = BitmapFactory.Options() options.inJustDecodeBounds = true BitmapFactory.decodeFile(file.path!!, options) val request = Request.Builder() .url(baseUrl.resolve(endpoint)!!) .post(RequestBody.create(MediaType.parse(options.outMimeType), file)) .build() // Extend timeouts for large uploads. See IMAGINARY-3211. val uploadClient = client.newBuilder() .writeTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .build() ... @Inject lateinit var imageUploadPolicy: ImageUploadPolicy
  70. @Inject @Named("base") lateinit var baseUrl: HttpUrl @Inject lateinit var client:

    OkHttpClient fun upload(file: File, endpoint: String): Boolean { val options = BitmapFactory.Options() options.inJustDecodeBounds = true BitmapFactory.decodeFile(file.path!!, options) val request = Request.Builder() .url(baseUrl.resolve(endpoint)!!) .post(RequestBody.create(MediaType.parse(options.outMimeType), file)) .build() // Extend timeouts for large uploads. See IMAGINARY-3211. val uploadClient = client.newBuilder() .writeTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .build() ... @Inject lateinit var imageUploadPolicy: ImageUploadPolicy imageUploadPolicy.checkBounds(options.outWidth, options.outHeight)
  71. class NetworkingModule : AbstractModule() { /** Extend timeouts for large

    uploads. See IMAGINARY-3211. */ @Provides @Named("image_uploads") fun provideImageUploadsHttpClient(client: OkHttpClient): OkHttpClient { return client.newBuilder() .writeTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .build() } ... }
  72. @Inject @Named("base") lateinit var baseUrl: HttpUrl @Inject fun upload(file: File,

    endpoint: String): Boolean { val options = BitmapFactory.Options() options.inJustDecodeBounds = true BitmapFactory.decodeFile(file.path!!, options) @Inject lateinit var imageUploadPolicy: ImageUploadPolicy imageUploadPolicy.checkBounds(options.outWidth, options.outHeight) lateinit var client: OkHttpClient val request = Request.Builder() .url(baseUrl.resolve(endpoint)!!) .post(RequestBody.create(MediaType.parse(options.outMimeType), file)) .build() // Extend timeouts for large uploads. See IMAGINARY-3211. val uploadClient = client.newBuilder() .writeTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .build() ... val call = .newCall(request) uploadClient
  73. @Named("image_uploads") @Inject @Named("base") lateinit var baseUrl: HttpUrl @Inject fun upload(file:

    File, endpoint: String): Boolean { val options = BitmapFactory.Options() options.inJustDecodeBounds = true BitmapFactory.decodeFile(file.path!!, options) @Inject lateinit var imageUploadPolicy: ImageUploadPolicy imageUploadPolicy.checkBounds(options.outWidth, options.outHeight) val request = Request.Builder() .url(baseUrl.resolve(endpoint)!!) .post(RequestBody.create(MediaType.parse(options.outMimeType), file)) .build() // Extend timeouts for large uploads. See IMAGINARY-3211. val uploadClient = client.newBuilder() .writeTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .build() ... val call = .newCall(request) client lateinit var client: OkHttpClient
  74. @Named("image_uploads") @Inject @Named("base") lateinit var baseUrl: HttpUrl @Inject fun upload(file:

    File, endpoint: String): Boolean { val options = BitmapFactory.Options() options.inJustDecodeBounds = true BitmapFactory.decodeFile(file.path!!, options) @Inject lateinit var imageUploadPolicy: ImageUploadPolicy imageUploadPolicy.checkBounds(options.outWidth, options.outHeight) val request = Request.Builder() .url(baseUrl.resolve(endpoint)!!) .post(RequestBody.create(MediaType.parse(options.outMimeType), file)) .build() ... val call = .newCall(request) client lateinit var client: OkHttpClient
  75. • Many systems all trying to make the same decisions

    • Example: Android themes Precedence I wrote some new features for you! Come again? BUT THE CODE IS ALREADY WRITTEN! I can’t hear you 15:33
  76. • Difficult to predict what will happen • Avoid ’em

    Precedence Rules are Clumsy! I wrote some new features for you! Come again? BUT THE CODE IS ALREADY WRITTEN! I can’t hear you 15:33
  77. Finish Transitions Get Cake STRATEGY #15

  78. 2009

  79. 2009

  80. 2009 Apache HTTP client

  81. 2009 Apache HTTP client org.json

  82. 2009 Apache HTTP client org.json AsyncTask

  83. 2010 Apache HTTP client org.json AsyncTask

  84. 2010 Apache HTTP client org.json AsyncTask Gson

  85. Apache HTTP client org.json AsyncTask HttpURLConnection Gson 2011

  86. Apache HTTP client org.json AsyncTask HttpURLConnection Gson Volley Callbacks 2013

  87. Apache HTTP client org.json AsyncTask HttpURLConnection Gson Volley Retrofit Callbacks

    RxJava OkHttp 2 2014
  88. Apache HTTP client org.json AsyncTask HttpURLConnection Gson Volley Retrofit Retrofit

    2 Callbacks RxJava OkHttp 3 OkHttp 2 2015
  89. Apache HTTP client org.json AsyncTask HttpURLConnection Gson Volley Retrofit Retrofit

    2 Callbacks RxJava OkHttp 3 Moshi OkHttp 2 2016
  90. Apache HTTP client org.json AsyncTask HttpURLConnection Gson Volley Retrofit Retrofit

    2 Callbacks RxJava OkHttp 3 Moshi OkHttp 2 Moshi-Kotlin 2018
  91. Apache HTTP client org.json AsyncTask HttpURLConnection Gson Volley Retrofit Retrofit

    2 Callbacks RxJava OkHttp 3 Moshi OkHttp 2 Moshi-Kotlin
  92. Retrofit 2 RxJava OkHttp 3 Moshi-Kotlin

  93. • Android SDK, build tools, server APIs, libraries • In-flight

    transitions are friction! Life in Transition
  94. • Plan to celebrate with your team when you finish

    a transition:
 cake, or team lunch, t-shirts, stickers, or Vegas • Keep your eyes on the prize when coding is a grind
  95. Takeaways FIN

  96. 1. People respond to incentives 2. Branch by abstraction 3.

    Build learning into your process 4. Infrastructure induces demand 5. Embrace your problem domain 6. Understand stakeholders 7. Minimize glue 8. Writing is thinking 9. Less code is better 10. The type system is just a tool 11. Organize by feature 12. Create building blocks 14. Policy good; precedence bad 15. Eat Cake
  97. @jessewilson https://github.com/swankjesse/maintainability Thanks