Avoiding the Hazards of Dependencies - Part 2

Mitigating risks & future-proofing

Avoiding the Hazards of Dependencies - Part 2

Mitigating risks & future-proofing


About the author

Chris Griffith has been a game developer for over 19 years and a mobile developer since 2010. He’s produced dozens of apps and games for brands like Match.com, LEGO, Microsoft, Kraft, Anheuser-Busch and PepsiCo.

Chris has been a member of the PullRequest network since April 2018.

Be sure to check out other great work from Chris:


images/hazards-of-dependencies-part-2.jpg

In my last article, I talked about the questions I feel every software development team should ask before incorporating 3rd-party dependencies into your apps. If those questions can be answered to the team’s satisfaction, does that mean you should accept the risks involved and plow ahead? Not necessarily. In this article, I’ll share some best practices for mitigating many of the risks involved with using other peoples' software in your own. Note that my specific perspective is that of a mobile engineer, and the code examples below are in Swift, but in principle, these suggestions are widely applicable.

Defense #1: Abstraction

One of the most significant challenges posed by a 3rd party library is what to do in the event you need to remove it. If you heavily make use of a framework in your code that’s managed by someone else, you’re taking on the liability of changes and breakages. If a library suddenly becomes unavailable or you cease to be able to use it for legal reasons, or even if a new version is released with a completely incompatible API, you may find yourself needing to make hundreds of time-consuming and costly changes.

Let’s use Firebase as an example since it’s common and has a simple API for doing analytics event tracking. It’s also a framework that, by its nature, will be used throughout an app. The following example of how you track an event is from the Firebase documentation:

Analytics.logEvent("share_image", parameters: [
  "name": name as NSObject,
  "full_text": text as NSObject
  ])

Seems straightforward enough, right? It could be very tempting to copy/paste this code throughout your app, on every screen, and boom, you’ve got analytics. However, every time you use this code, you’re inserting a volatile line that might have to change if you had to pull Firebase out of your app. You need a way to insert a layer of protection between yourself and external software. What if there was a way we could call any given method in the Firebase SDK no more than one time in our code? Then we’d not only have only a handful of changes to make if we removed it, but they’d probably all be in a single file. Enter the concept of abstraction.

We can create a class known as a proxy, which simply relays commands to Firebase in a single place. This class will be part of our app’s code and will be the single point of entry for the SDK. It could look something like this:

import Firebase
import Analytics

class AnalyticsProxy {
  
  static func setup() {
    FirebaseApp.configure()
  }

  static func logEvent(_ name: String, parameters: [AnyObject: AnyObject]) {
    Analytics.logEvent(name, parameters: parameters)
  }
}

Notice how, for this example, we have mirrored Firebase’s API exactly. That’s not prescriptive, and in fact, we might want to make a more robust version of the logEvent method that captures additional data to be processed here. Using this system, we could support multiple analytics packages with ease. Every time the proxy’s logEvent method is called, we can create a request for each different package. Any time you need more functionality from the SDK, simply add a method to this class and call it instead, forwarding the command. Now the only component the rest of your app relies upon is the proxy. You could easily comment out the body of each of these methods to remove analytics entirely from your app in less than a minute.

A package like Firebase lends itself particularly well to this approach because most of its commands are “fire and forget,” meaning you don’t need to worry about return values or waiting for responses. The SDK manages server communication itself. What about other packages, such as one that makes doing layout constraints easier, or the very popular TTTAttributedLabel? These introduce new types and overrides and extensions for existing types. Integration of these elements is more invasive, but there are still techniques to help isolate them. One approach is with type aliases. A type alias is simply defining a new type as a pointer to an existing one. Both Kotlin and Swift support the concept of aliases natively.

typealias MyAppAttributedLabel = TTTAttributedLabel

All this code does is create a mapping so that whenever I use the type MyAppAttributedLabel the compiler is actually creating a reference to the original type. So throughout my code I can use something like:

var label: MyAppAttributedLabel
label.doSomethingTTTALWouldDo()

In the event I needed to replace TTTAttributedLabel with something else, I can change MyAppAttributedLabel to be an actual type, or a protocol, or a type alias for a different underlying type. I still have to provide the missing functionality, but by making use of abstraction, I don’t have to locate every single instance of TTTAttributedLabel in the code and remove or change it.

Defense #2: Identify Bad Actors

Some 3rd-party frameworks fetch remote data to present in your app. These might be advertising SDKs, user support engines, or the aforementioned analytics packages. If these libraries aren’t open-source, what exactly do they send, and how often is opaque until an app runs? As developers, we always want to know what our apps are doing, but software like this can make it difficult. All the optimization you do to your app before shipping it to users can be undone by a 3rd-party library that is implemented poorly.

If you need to add packages like this to your project, it’s a good idea to have a simple set of tests to run on them to get an idea of their impact. Some key elements you want to look for when profiling are:

CPU usage

If a lot of threads and processes are continuously running and max out the CPU, the user will experience performance issues and notice other adverse effects like their overheating or battery draining faster than usual.

Memory usage

There’s a finite amount of memory your app can consume before it is shut down by the iOS; if an external SDK has a memory leak, you want to identify it quickly.

Disk usage

Even though device storage is getting larger every year, people can still run low on space. If a library in your app is caching excessive amounts of data to disk (and never deleting it), users may decide to remove it from their phones.

Network traffic

Advertising SDKs, in particular, can pull large videos over the user’s connection, and may also send various payloads back and forth over the lifecycle of an app. If there are many such libraries in your app they might be negatively impacting the performance of the network requests you make on behalf of the user. It’s important to know how much your app is taxing your users' data plans. Excessive network usage can also negatively impact battery life.

The first three of these can fairly easily be monitored in Xcode through the basic debug tools. To see the level of network traffic and precisely what types of payloads are being sent, I recommend a network proxying tool like Charles. With some setup, it will allow you to inspect every request coming from your app. To save time, you might want to perform these tests once you have all your 3rd-party dependencies so you can see the aggregate effect. But if you take the time to go through these steps as you add them, it will be easier to identify which ones are dragging your app down.

Defense #3: Adoption

If an open-source external framework either changes direction or gets abandoned, you can choose to “adopt” it. You can fork the repo or fully bring the project’s code into your own, taking full responsibility for maintaining it from that point forward. For self-contained libraries, this could be a better option than finding an alternative or having to extract it from the project. This isn’t an option with closed-source dependencies, but those are usually made by companies that maintain them rather than volunteers on GitHub. Also, be sure that the library’s license terms in question don’t prohibit this course of action.

Defense #4: Periodically Re-evaluate

Many early-stage companies will have teams as small as 1-2 people producing their apps. Because of limited resources, the decision to rely heavily on 3rd-party frameworks to supply needed functionality quickly may be the only practical course of action. But 2-3 years in, say when the company has raised a round of funding and the team has grown, it’s worth re-examining those decisions. Maybe a particular framework is more trouble to maintain and update than the value it provides; perhaps it would be better to replace it with a homegrown implementation.

It’s equally possible as apps change over time for dead code to build up; screens are abandoned or even deleted. However, if no one checks to see if a dependency is being used, it can linger on, still adding weight to the file size or other ill effects. I recommend as part of an app’s maintenance lifecycle (maybe even once a year) to build in a certain amount of time to reflect on what dependencies are being used and to quickly answer all the original questions again about the value they provide.

Conclusion

My intention in sharing this information is not to dissuade anyone from the use of 3rd-party dependencies in their projects. Rather, I hope it inspires you to exercise the same kind of rigor when evaluating them as you would to making a hiring decision. I advise you always to keep your apps' long-term sustainability in mind, not just short-term gains.


Be sure to check out other great work from Chris:

Photo by Max Pixel


About PullRequest

HackerOne PullRequest is a platform for code review, built for teams of all sizes. We have a network of expert engineers enhanced by AI, to help you ship secure code, faster.

Learn more about PullRequest

Chris Griffith headshot
by Chris Griffith

August 3, 2020