RUBBER, MEET ROAD. ROAD, MEET RUBBER
Now, it’s time for us to make some fundamental decisions about the SDK we’ll be designing.
STATE-HEAVY VS. STATE-LITE
For example, the app will have two distinct states: Peripheral and Central. Should these be reflected as a single API, with a set state, or should we actually provide two APIs, with the app loading the appropriate one for the state?
Let’s do a “Pro/Con” analysis:
- Simpler to Program
We can simply write one set of functionality. There will be a lot of common structure between the two states. Also, a monolithic codebase can be easier to subject to configuration management, and assign responsible developers.
- Easier to Debug/Test
As there will be one entrypoint, and very distinct state, testing at the code level should be simpler. Mocking should also be easier.
- Smaller Code Footprint
As we’ll probably be sharing a lot of code, the size of the SDK will be smaller.
- Easier to Document
Documentation is always a huge part of writing an SDK. It will be easier to write a single document that will re-use a lot of text between the two states.
- Easier to Use?
Note the question mark. That’s debatable. For some folks, it is easier to understand a single entrypoint/multiple state driver, while others are more likely to understand a lighter-weight, more aggregate, driver. This will also appear in the “Cons” list.
- Higher Quality (for Developer)
This is also debatable, as a single-entrypoint driver will have a “lot more under the hood” than a dual-entrypoint driver, but it will all be in one place, and there are likely to be less of what I call “trouble nodes.”
“Trouble nodes” are places where a branch happens, a function call is made, where a closure occurs, etc. Basically, it’s wherever the context changes. These are places where we can expect the unexpected.
If we have a single set of code, then testing in one state can often benefit the other state.
When we write an SDK, quality is God. It must be the absolute best quality possible, so we like it when a pattern helps us to have a better-quality SDK.
- Familiar Pattern
This is pretty much the “classic” pattern for SDKs. Single entrypoint, different states, and different behaviors, dependent upon the state.
- More Flexible and Adaptable
This is a debatable point, and will also appear in the “Cons” list. The structure of the SDK is such, that when we have changes (like new models), we can adapt to these changes fairly rapidly.
- More Difficult to Understand
A stateful system is always a bit harder to understand than a stateless system. If we have a single entity that behaves differently, depending on the value of a property, then it can be easy to get mixed up.
- More Code Overhead
Since the same code is being applied, dependent upon state, there is a necessary amount of overhead that is dedicated to state management.
- Lower Quality (for User)
I know I have “Higher Quality” in the “Pros” list, but we also have the issue of the same code, behaving differently, depending upon the state. That’s usually something we’d like to avoid. It can be difficult to test, and another way we can expect the unexpected. The quality hit is also likely to be on the user end, as they may not use the SDK properly, based on its state.
- Uglier Code
Almost by definition, stateful code is uglier than stateless code. It’s “multipurpose,” so there’s code branches and functions that need to have different meanings, depending on state (like having a “device cached” flag that means a lot more for a Central, than a Peripheral). Also, there are the issues that can be caused by properties shared across the context, being misapplied (like having a loop that will always only execute once for Peripheral, but possibly multiple times for Central).
- Harder to Manage
With a stateful system, we need to keep track of the state. Usually, the system itself will report/track its state, but the code that we write will also have to be stateful. We will need to write “multipurpose” code.
- More Difficult to Use
That should actually be obvious from the above. The onus is on the user to make sure they know the behavior for their selected state.
- More Flexible and Adaptable
Flexibility is also a potential quality hit. Not just “bug count” quality, but also the quality of the usability of the SDK. It is more difficult to explain a flexible system, and it can be more difficult to understand.
- Simpler to Use
When a code resource is “stateless,” it is generally “unipurpose,” so there’s no need to worry about the state it’s in when you use it.
- Easier to Understand
When we have one job, it’s easier to understand. Since we have an API that is dedicated to a single state, we just need to worry about the behavior under that single state.
- Higher Quality (for User)
Since the SDK is now simpler and more easily understood, the user code that implements it is likely to have a higher level of quality.
- Less Code Overhead
Since we don’t need to manage state, it means that we can lose a pretty significant amount of code that is dedicated to tracking and managing state.
- Prettier Code
Since we will be writing code to handle a single functionality, it will be smaller, lighter-weight, and easier to understand.
- Better Suited to the Need
Since stateless code tends to be smaller and more focused on a single state, it is usually better suited than stateful code to its single need.
- More “Atomic” Documentation
This may not be a “Pro” for all people, but I find it difficult to document a system that can switch between states. If I have discrete states, I can have dedicated documentation for each state.
- Less Code Reuse
We are likely to need to write code that does almost the same thing for each state, as opposed to one function that behaves differently, based on state. For example, if we have a function that counts devices on the other end of our connection, we could simply hardcode “
return 1” for the Peripheral state, and count the number of discovered devices for the Central state.
If we are supplying the same function for each state, the Peripheral version will be a simple “
return 1“, while the Central version will be a bit more complex. It’s almost as if we don’t need the trouble node at all for Peripherals.
- Lower Quality (for Developer)
The developer can’t take advantage of the same code paths being reused for different states. They now have more code paths to test, and they will need to repeat the test, with slight alterations for state.
That said, even though we need more testing, these tests aren’t as intense as they might need to be for a stateful implementation. If we do it right, this Con will disappear.
- Larger Code Footprint
Since there are now separate code paths, the same executable may need to be duplicated for each path, and each of the APIs will now need its own management overhead, so the SDK is likely to have a larger footprint.
- Unfamiliar Pattern
This is destined to change, as a stateless approach becomes more pervasive, but right now, people are still used to the “state machine” SDK model.
- Less Flexible
This is really by design. A stateless approach generally has bespoke code for a state, and isn’t designed to be modified for other states. It can be, but that is not required.
- Can’t Deal With Changing States Very Well
If you have a system where the states change a lot, then there needs to be a mechanism for transferring relevant information from one state to another.
A stateful SDK can simply have a shared context, but a stateless implementation needs to transfer the information whenever the state changes.
This can have drastic effect on some implementations. For example, one of the things that might need to be shared between states could be a cache. Cache management is generally not for the faint of heart.
- More “Atomic” Documentation
This can also be a “Con,” as we may need to repeat a lot of documentation, and make sure that we do things like match the vocabulary and glossary between the documentation branches for each state.
I’M CHOOSING TWO LUMPS
For our implementation, I’m choosing the “less-stateful,” two-API variant. I’ll explain:
- There Are Only Two States
If there are a number of states, the decision gets “murkier,” as that overhead and internal quality starts to become more of an issue.
- The States Don’t Change
Since we basically pick an app state on startup, and never change it, we don’t need to worry about state change overhead.
- I Can Make the Quality Work
I’m quite confident that I won’t have quality issues with the separate code paths. In fact, I hope to take advantage of the lighter-weight implementation to do better testing.
- It Will Act As A Teaching Aid
Since the main purpose of this exercise is to teach, having each state isolated will allow us to remain focused on the important stuff.
- Chaotic Systems Are Better Handled In A Stateless Manner
When I write device software, I’m generally writing ordered, predictable code, for a system that can’t be predicted.
The two major components of the system: Users, and other devices, are, by definition, outside my control.
Here’s an example: Say we are writing software to control the camera angle on a drone. It has a servo, which feeds back its position. We send it instructions to move to 47 degrees. The servo obliges, the camera moves to 47°, and the software knows where the camera is.
Now, we get a gust of wind, and the wind pushes the camera to 58°. If we have a stateful system, we could still assume the camera is at 47°, so we send a command to move to 52° (+5°). The problem is, is that the maximum travel of the gimbal is 60°, so we are rewarded by a sizzle and a puff of smoke from the drone.
If we had a system that didn’t assume that we knew where the camera position was, we could query it prior to sending the command, or, at the very least, have an observer set up, so we are informed when the camera angle changes.
History is full of terrible problems that occur, because state is assumed, and the assumption doesn’t meet reality. Not just in software, either. Study the history of war, and see how battles have been lost, because a commander thought they knew where the enemy’s troops were, and they were wrong.
In reality, the system won’t really be “stateless.” Core Bluetooth is highly stateful, under the hood, but this will allow users of the SDK to write less stateful code.
When I write SDKs, I ALWAYS give priority to the users. My SDKs are designed to abstract complexity and implementation details, and allow the user of the SDK to leverage the system in ways that I may not be able to imagine.
Now that we know the general structure of the SDK, let’s decide how it will be expressed.