$30 off During Our Annual Pro Sale. View Details »

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.

Jesse Wilson

August 28, 2018
Tweet

More Decks by Jesse Wilson

Other Decks in Technology

Transcript

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

    View Slide

  2. Forever?

    View Slide

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

    View Slide

  4. 1.0 1.1 1.2 1.3 1.4 1.5 1.6

    View Slide

  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

    View Slide

  6. Incentives
    STRATEGY #1

    View Slide

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

    View Slide

  8. • Don’t tell your team “write more tests”
    • Do find & remove barriers to writing tests

    View Slide

  9. Incremental Changes
    STRATEGY #2

    View Slide

  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

    View Slide

  11. • master is always stable & shippable!
    • Per-feature betas
    • Decouple features & releases
    Why Branch By Abstraction?

    View Slide

  12. enum class BiometricAuth {
    NONE,
    FINGERPRINT,
    FINGERPRINT_OR_IRIS
    }

    View Slide

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

    View Slide

  14. class AuthScreen {
    @Inject lateinit var experimenter: Experimenter
    fun showBiometricAuth() {
    when (experimenter.getBucket(BiometricAuth::class)) {
    NONE -> skipBiometricAuth()
    FINGERPRINT -> showOldFingerprintAuth()
    FINGERPRINT_OR_IRIS -> showNewBiometricAuth()
    }
    }
    }

    View Slide

  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

    View Slide

  16. class AuthTest {
    @Inject lateinit var authScreen: AuthScreen
    @Inject lateinit var experimenter: FakeExperimenter
    @Test
    fun testNewBiometricAuth() {
    experimenter.setDefault(BiometricAuthentication.FINGERPRINT_OR_IRIS)
    ...
    }
    }

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  20. Learning Organizations
    STRATEGY #3

    View Slide

  21. • What features should we build?
    • How should they work?
    • What architecture should we use?
    • Which libraries should be adopt?
    Research & Development

    View Slide

  22. Perfect is Too Slow
    & Too Difficult

    View Slide

  23. • Try lots of things & see what works
    • Lower the cost of making mistakes
    • Tight feedback loops!
    Learning Means
    Making Mistakes

    View Slide

  24. Tools & Leverage
    STRATEGY #4

    View Slide

  25. • Example: gRPC
    • gRPC is an alternative to REST + JSON. It makes it faster and
    easier to define server endpoints
    Infrastructure Investments

    View Slide

  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

    View Slide

  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

    View Slide

  28. • Faster compiler? compile more!
    • Easier to write tests? test more!
    • Less severe errors? experiment more!

    View Slide

  29. • Slow code reviews? Fewer pull requests!
    Suppressed Demand

    View Slide

  30. Know Your Problem Domain
    STRATEGY #5

    View Slide

  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

    View Slide

  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!

    View Slide

  33. Modeling
    STRATEGY #6

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  37. Value Objects,
    Service Objects,
    & Glue
    STRATEGY #7

    View Slide

  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

    View Slide

  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

    View Slide

  40. • The application’s plumbing
    • Awkward to test
    • Examples: activities, dagger modules, event listeners
    Glue

    View Slide

  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

    View Slide

  42. English, then Code
    STRATEGY #8

    View Slide

  43. Writing is thinking
    Why is writing so hard?

    View Slide

  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

    View Slide

  45. Shipley’s Law
    STRATEGY #9

    View Slide

  46. Less code is better code
    ∴ No code is the best code
    Wil Shipley

    View Slide

  47. The Type System
    Works for You
    STRATEGY #10

    View Slide

  48. • JavaScript programmers: types are a lot of work for what you get
    • Java programmers: I need separate types for
    OnItemClickListener and OnItemLongClickListener

    View Slide

  49. interface GeoLocationService {
    fun latest(): GeoLocation
    }

    View Slide

  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)
    }

    View Slide

  51. Organize By Feature
    STRATEGY #11

    View Slide

  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

    View Slide

  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

    View Slide

  54. Building Blocks
    STRATEGY #12

    View Slide

  55. Dimetrodon
    Diplodocus
    Triceratops
    Stegosaurus
    Velociraptor
    Brachiosaurus
    Ankylosaurus
    Edmontosaurus
    Pterodactyl
    Dinosaur Extinct!
    15:24

    View Slide

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

    View Slide

  57. Toast.makeText(getActivity(), "Dinosaur Extinct!", Toast.LENGTH_LONG).show()

    View Slide

  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);

    View Slide

  59. • An inventory of classes that can be composed to build things
    • Rearrange ’em to solve new problems
    Maintainable APIs work like Lego

    View Slide

  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

    View Slide

  61. Policy & Precedence
    STRATEGY #14

    View Slide

  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

    View Slide

  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

    View Slide

  64. • In school we’re taught to be avoid magic numbers
    • In practice: don’t bury policy
    Maintainable Policies

    View Slide

  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)
    ...

    View Slide

  66. class ImageUploadPolicy {
    fun checkBounds(width: Int, height: Int) {
    require(width < 5000 && height < 5000)
    }
    }

    View Slide

  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()
    ...

    View Slide

  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

    View Slide

  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

    View Slide

  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)

    View Slide

  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()
    }
    ...
    }

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  77. Finish Transitions
    Get Cake
    STRATEGY #15

    View Slide

  78. 2009

    View Slide

  79. 2009

    View Slide

  80. 2009
    Apache HTTP client

    View Slide

  81. 2009
    Apache HTTP client org.json

    View Slide

  82. 2009
    Apache HTTP client org.json AsyncTask

    View Slide

  83. 2010
    Apache HTTP client org.json AsyncTask

    View Slide

  84. 2010
    Apache HTTP client org.json AsyncTask
    Gson

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  92. Retrofit 2 RxJava
    OkHttp 3 Moshi-Kotlin

    View Slide

  93. • Android SDK, build tools, server APIs, libraries
    • In-flight transitions are friction!
    Life in Transition

    View Slide

  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

    View Slide

  95. Takeaways
    FIN

    View Slide

  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

    View Slide

  97. @jessewilson
    https://github.com/swankjesse/maintainability
    Thanks

    View Slide