Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

Forever?

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

1.0 1.1 1.2 1.3 1.4 1.5 1.6

Slide 5

Slide 5 text

• 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

Slide 6

Slide 6 text

Incentives STRATEGY #1

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

Incremental Changes STRATEGY #2

Slide 10

Slide 10 text

• 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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

enum class BiometricAuth { NONE, FINGERPRINT, FINGERPRINT_OR_IRIS }

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

• 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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

Learning Organizations STRATEGY #3

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

Perfect is Too Slow & Too Difficult

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

Tools & Leverage STRATEGY #4

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

• 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

Slide 27

Slide 27 text

• 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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

• Slow code reviews? Fewer pull requests! Suppressed Demand

Slide 30

Slide 30 text

Know Your Problem Domain STRATEGY #5

Slide 31

Slide 31 text

• 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

Slide 32

Slide 32 text

• Understanding the business is a superpower • Understanding the users’ needs is a superpower • You’ll be able to anticipate what You’re Gonna Need!

Slide 33

Slide 33 text

Modeling STRATEGY #6

Slide 34

Slide 34 text

• 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

Slide 35

Slide 35 text

• 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

Slide 36

Slide 36 text

• 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

Slide 37

Slide 37 text

Value Objects, Service Objects, & Glue STRATEGY #7

Slide 38

Slide 38 text

• 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

Slide 39

Slide 39 text

• 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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

• 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

Slide 42

Slide 42 text

English, then Code STRATEGY #8

Slide 43

Slide 43 text

Writing is thinking Why is writing so hard?

Slide 44

Slide 44 text

• 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

Slide 45

Slide 45 text

Shipley’s Law STRATEGY #9

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

The Type System Works for You STRATEGY #10

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

interface GeoLocationService { fun latest(): GeoLocation }

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

Organize By Feature STRATEGY #11

Slide 52

Slide 52 text

Organizing by Technology com.roundsapp models persistence services viewmodels views fragments activities • Redundant • For example, your views already have “View” in the class name

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

Building Blocks STRATEGY #12

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

Policy & Precedence STRATEGY #14

Slide 62

Slide 62 text

• 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

Slide 63

Slide 63 text

• No size limit on your profile photo? The universe will impose its own limit and you will be sad No Policy is a Policy

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

@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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

@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

Slide 73

Slide 73 text

@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

Slide 74

Slide 74 text

@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

Slide 75

Slide 75 text

• 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

Slide 76

Slide 76 text

• 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

Slide 77

Slide 77 text

Finish Transitions Get Cake STRATEGY #15

Slide 78

Slide 78 text

2009

Slide 79

Slide 79 text

2009

Slide 80

Slide 80 text

2009 Apache HTTP client

Slide 81

Slide 81 text

2009 Apache HTTP client org.json

Slide 82

Slide 82 text

2009 Apache HTTP client org.json AsyncTask

Slide 83

Slide 83 text

2010 Apache HTTP client org.json AsyncTask

Slide 84

Slide 84 text

2010 Apache HTTP client org.json AsyncTask Gson

Slide 85

Slide 85 text

Apache HTTP client org.json AsyncTask HttpURLConnection Gson 2011

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

Retrofit 2 RxJava OkHttp 3 Moshi-Kotlin

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

• 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

Slide 95

Slide 95 text

Takeaways FIN

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

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