Playing Architect
Welcome to post #4 in our “From Zero to Open Source Hero” series! At the end of post three, we had configured our example Android project to include:
- an empty library module
- an empty application module that depends on this library module
With these modules in place, the architecture of our open source project is already well-defined at the highest level:
- the code that collectively forms the reusable component we will share with other developers must all be located in the library module;
- the code required to demonstrate and test this component in a live application must all be located in the application module
This separation of concerns is a solid first step toward making sure our library is structured appropriately.
In the remainder of this post, we’ll identify four user experience (UX) traits that characterize high-quality library projects and discuss the implications of these traits for our code. Thinking about these topics early on will help us establish guiding principles concerning the structure and evolution of our libraries that will ultimately lead to more focused and maintainable codebases. Remember: our users are the developers whose applications consume our libraries, and UX is therefore measured from their perspective!
Trait #1: Easy Integration
Developers are always weighing the time needed to understand and integrate existing libraries against the time required to build custom solutions. Help users evaluate your library by prioritizing quick and painless integration with existing code.
Suggestions
- Aim to keep your library’s required public API as lean as possible to flatten the learning curve for new users. In particular:
- Identify which components of the embedding app your library absolutely must know about, and which can be accessed transitively via the required components. Library setup methods should consume just the required components, with transitive access to secondary components handled within your library. Locating this component-access code within your library also leads to a drier ecosystem!
- Determine default attribute values and library behaviors up front. Minimize the amount of configuration that a user is absolutely required to provide. The closer your library is to plug-and-play, the better.
Trait #2: Selective Configuration
Solid defaults are a must, but almost every developer will need to make some adjustments to satisfy their requirements. Make these adjustments painless by allowing users to adjust each configurable option independently.
Suggestions
- Allow developers to override the default value of every configurable option individually. Yes, that means your public API just got a lot larger – however, this growth should mainly consist of predictably named setters so the increase in cognitive load is minimized.
- Employ the Builder pattern if you anticipate allowing users to construct configurations in code. This pattern is perfect when a type contains many optional fields. When parsing a built configuration, fill in any missing values with the sensible defaults you defined earlier.
- If your library configuration can be specified in more than one way (for example, Android views can be configured either using xml or programmatically), try to unify code that handles these alternate routes as early as possible. This helps maintain feature parity.
Trait #3: Selective Customization
Just like selective configuration, but on a grander scale! Support developers who need to make significant customizations by allowing them to replace individual default components with their own.
Suggestions
- Make sure the major components of your library are loosely coupled. Representing major architectural boundaries using appropriate abstractions and exposing these abstractions to users allows the maximum amount of customization while still guaranteeing interoperability with the rest of your library. Decide where these boundaries should lie early on, and be disciplined about respecting them in your own code!
- Plan for default components to be nothing more than implementations of the abstractions identified as part of the previous bullet point. This guarantees that they don’t accidentally rely on “insider knowledge” of your library – if this were to be the case, users might find themselves missing crucial bits of information when attempting to build custom versions.
Trait #4: Easy Contribution
No matter how well you plan your defaults and architectural boundaries, there will come a time when a user requests a feature or capability you never even considered. Help them help you by keeping your code legible and easy to navigate.
Suggestions
- Settle on style and naming conventions early (we will discuss how to enforce these conventions using CI in a later post in the series). If you don’t get this done right away, the effort required to “upgrade” your library to spec later will be significant. A consistent style will help potential contributors understand existing library code more quickly.
- Decide on a documentation strategy. All public APIs should be documented for library consumers to reference, but it’s also worth considering documenting more complex internal APIs to help future contributors.
- Allocate time to write tests as you go, TDD or not. Tests are living and expressive documentation that help communicate the expected behavior of your existing code. As a bonus, they’ll also help increase user confidence in the quality of your library.