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. 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
  2. 8.

    • Don’t tell your team “write more tests” • Do

    find & remove barriers to writing tests
  3. 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
  4. 11.

    • master is always stable & shippable! • Per-feature betas

    • Decouple features & releases Why Branch By Abstraction?
  5. 14.

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

    { when (experimenter.getBucket(BiometricAuth::class)) { NONE -> skipBiometricAuth() FINGERPRINT -> showOldFingerprintAuth() FINGERPRINT_OR_IRIS -> showNewBiometricAuth() } } }
  6. 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
  7. 16.

    class AuthTest { @Inject lateinit var authScreen: AuthScreen @Inject lateinit

    var experimenter: FakeExperimenter @Test fun testNewBiometricAuth() { experimenter.setDefault(BiometricAuthentication.FINGERPRINT_OR_IRIS) ... } }
  8. 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
  9. 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
  10. 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
  11. 21.

    • What features should we build? • How should they

    work? • What architecture should we use? • Which libraries should be adopt? Research & Development
  12. 23.

    • Try lots of things & see what works •

    Lower the cost of making mistakes • Tight feedback loops! Learning Means Making Mistakes
  13. 25.

    • Example: gRPC • gRPC is an alternative to REST

    + JSON. It makes it faster and easier to define server endpoints Infrastructure Investments
  14. 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
  15. 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
  16. 28.

    • Faster compiler? compile more! • Easier to write tests?

    test more! • Less severe errors? experiment more!
  17. 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
  18. 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!
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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
  24. 40.

    • The application’s plumbing • Awkward to test • Examples:

    activities, dagger modules, event listeners Glue
  25. 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
  26. 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
  27. 48.

    • JavaScript programmers: types are a lot of work for

    what you get • Java programmers: I need separate types for OnItemClickListener and OnItemLongClickListener
  28. 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) }
  29. 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
  30. 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
  31. 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);
  32. 59.

    • An inventory of classes that can be composed to

    build things • Rearrange ’em to solve new problems Maintainable APIs work like Lego
  33. 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
  34. 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
  35. 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
  36. 64.

    • In school we’re taught to be avoid magic numbers

    • In practice: don’t bury policy Maintainable Policies
  37. 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) ...
  38. 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() ...
  39. 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
  40. 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
  41. 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)
  42. 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() } ... }
  43. 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
  44. 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
  45. 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
  46. 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
  47. 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
  48. 78.
  49. 79.
  50. 90.

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

    2 Callbacks RxJava OkHttp 3 Moshi OkHttp 2 Moshi-Kotlin 2018
  51. 91.

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

    2 Callbacks RxJava OkHttp 3 Moshi OkHttp 2 Moshi-Kotlin
  52. 93.

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

    transitions are friction! Life in Transition
  53. 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
  54. 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