The primary distinction between libraries and functions is the audience. The viewers of functions is most of the people. Libraries, then again, present providers to functions. Thus, their viewers is utility builders. In consequence, the primary consideration in designing a library is the developer expertise.
The primary elements of person expertise are UI, usability and efficiency. The developer expertise is considerably comparable. The equivalents for builders being APIs, integration and efficiency. Sure, efficiency is necessary in each facet of programming 🙂. This equivalence manifests within the design concerns that each element presents.
We are going to now have a look at every of those essential concerns:
“They are saying UI is sort of a joke — if you must clarify it, it’s not that good.” — Martin Leblanc
The identical rule applies to APIs. They need to be clear and easy.
Variables and strategies names ought to be expressive, with constant terminology.
Strategies ought to do precisely what their title suggests with no unwanted effects.
Place the strategies, variables and constants in locations the place a developer would look forward to finding them. Be certain they’re out there when wanted. Cut up APIs into completely different objects primarily based on their tasks. Expose globally out there APIs on a single static object. Place particular APIs on objects with the related accountability. If an API is simply out there in a sure context, maintain it in a devoted class.
A great API ought to be simple to make use of and onerous to abuse.
APIs need to be as onerous as potential to make use of in any method apart from meant. If a number of the APIs are usually not all the time out there, don’t expose them on a static object. As a substitute, expose them in an asynchronous method solely when they’re truly helpful. It will make it inconceivable to make use of them after they aren’t out there.
Right here’s an instance of an API of a library that gives video gamers:
One other limitation that APIs might have is being constrained to a sure scope or context. To stop abusing some of these APIs present them as extensions to the related object.
Yet another requirement that’s frequent to each UI and APIs design is transparency. It will be important that the appliance UI will present when the appliance is busy or some performance is unavailable. Equally, it’s necessary that the library API will let the developer know precisely what’s happening inside it.
- Expose states and occasions. There’s a vital distinction between state and occasions and deciding on the incorrect one will lead to an inconvenient API. A great rule of thumb is that if the developer can ask “is X presently Y?” then Y is a state of X, in any other case it’s in all probability an occasion.
- Be certain to offer a end result for asynchronous operations.
- Expose Errors. Software customers ought to be notified about errors with a well mannered normal message. Software builders then again ought to be notified with a message that ought to be as informative as potential. Clarify precisely what went incorrect and why. State potential actions if out there. Be certain to do it within the related place for the appliance developer to have the ability to take motion. Word that selecting your personal response to an error could also be surprising. Keep away from returning a default worth and delegate the response to the appliance. Replicate this habits within the API’s title.
Scale back throwing exceptions solely to probably the most primary misuse that makes the library ineffective. Deal with exceptions internally and keep away from crashing the host utility in any respect value.
APIs ought to be simple to increase.
- Keep away from utilizing strategies with many parameters as a result of including one other parameter is a breaking change. As a substitute, attempt to use the builder sample that may simply be prolonged in a backwards appropriate method. In Kotlin, use type safe builders to create a Domain Specific Language (DSL) and obtain a really handy API that can be very simple to increase.
** The total implementation of those APIs will be discovered here.
- Use enums as an alternative of booleans even should you solely have two choices as a result of enums are extendable.
The second developer expertise element is integration. The mixing, particularly of probably the most primary use case, should be as simple and intuitive as potential. Software builders will typically attempt to consider any potential library by implementing the fundamental integration. Whether it is too sophisticated they might determine to not use the library. As well as, the extra sophisticated the combination is, the extra seemingly it’s that the appliance developer will get it incorrect. Due to this fact, all the time go over the combination course of and see should you can carry out any of the combination steps internally. Merge integration steps each time potential. It’s higher to offer default settings and habits whereas maintaining them open for personalization if wanted.
Naming can be essential to assist information the method. See instance beneath:
Totally different improvement paradigms. Some builders desire declarative programming whereas others desire imperative. In follow it largely applies to states. Within the declarative programming model the developer wish to observe state modifications and react to them. Builders who use crucial programming will want to have the ability to question the state. Thus expose state as each variables and alter listeners. Kotlin’s StateFlows are an ideal strategy to assist each paradigms. Libraries that wish to preserve Java compatibility received’t be capable to keep away from listeners and variables.
The third element of the developer expertise is efficiency. It’s fairly apparent that efficiency should be nearly as good as potential. Any operation the library does will inevitably have some impression on the efficiency of the appliance. There are methods to make this impression simpler to deal with.
Let’s begin with initialization. There are some things to bear in mind about it.
The library mustn’t ever carry out any operations earlier than it’s initialized. It ought to present full management over the initialization course of and timing. Keep away from performing actions that the appliance developer is probably not conscious of or have management over. This rule will permit utility builders to combine the library with a characteristic flag or another situation understanding that when it’s off, the library has no impact. Frequent use circumstances are gradual launch, AB testing or disabling in case of an error that’s solely found after distribution.
Do solely the naked minimal throughout initialization. Transfer every other operation to a background thread as initialization is anticipated to be synchronous and run as early as potential — e.g. Software.onCreate() for Android.
Scale back the quantity of sources that the initialization consumes. Particularly cellular knowledge as it could devour the person’s knowledge plan. It’s frequent for a library to be initialized on each launch of the appliance however solely utilized in sure eventualities. So initialization ought to be slim and light-weight to keep away from losing sources when the library is initialized however not used.
If the library must carry out some knowledge or computation heavy operation, It’s higher to offer a separate API for it with a transparent title like “fetchImages”. Let utility builders determine when to make use of it — they know what’s greatest for his or her utility.
Pattern challenge. It’s essential for documentation to offer not solely written info and code samples but additionally a runnable challenge. This offers a a lot increased degree of confidence within the skill to efficiently combine the library. It’s additionally suggested to offer as many integration eventualities as potential. Present them within the type of a number of tasks,modules or flavors if crucial. Builders are inclined to comply with the saying that “code by no means lies, feedback (and documentation) generally do” and like to depend on a working pattern for integration.
There are a number of methods through which the precise improvement means of a library is completely different from the certainly one of an utility. The primary one is that in contrast to functions, libraries should be initialized. I’ve supplied some normal concerns for its design within the part about efficiency. Now I wish to present some extra sensible recommendation. Initialization is necessary largely to organize the library for motion. Use it to initialize providers which might be completely crucial for the library to perform like dependency injection. As well as, carry out operations that the library wish to carry out as quickly as potential like fetching configuration. That, clearly, should be executed on a separate thread and be as small as potential.
One other position of initialization is to obtain world configuration from the appliance which may be required all through your complete lifecycle of the library. On Android, it’s also used to acquire context.
On Android, a library can initialize itself utilizing a Content material Supplier as the great guys from Firebase describe in this blog post from 2016. Afterward Google launched the Androidx App Startup library to facilitate this technique and supply some further management. As talked about earlier than within the “Efficiency” part, I consider that despite the fact that this technique exists and could be very handy, libraries shouldn’t use it and I strongly suggest libraries to make use of guide initialization.
One factor that library builders ought to be extra conscious of than utility builders is code visibility. Making large elements of utility code public is a typical follow and is the reason (excuse) why it’s the default access modifier in Kotlin. This isn’t the case for library improvement. Any public code in a library could also be consumed by the host utility. Due to this fact the entry modifiers of each little bit of code require cautious consideration.
A typical supply of unintentional publicity of inside code is utilities and extensions. Pay further consideration to the scope through which they need to be legitimate.
One strategy to catch these earlier than a launch, apart from lint, is to go over the documentation that’s generated from the code and verify for something that doesn’t belong.
The quantity of management the developer has over the discharge cycle is completely different between an utility and a library. When releasing an utility after which realizing that modifications need to be made, the developer can all the time launch a brand new model. Adoption is probably not very quick however purchasers do replace ultimately and functions can request or even force the user to update. This isn’t the case for a library. As soon as a library goes stay, the developer has little or no management over it and over the builders who embed it into their functions. They could by no means improve from model 1.0.0. And even when they do improve it takes for much longer and is out of the library developer’s management. Hold as a lot management over the library as potential by offering distant configuration from the server. As talked about earlier than, this config ought to be synced to the library on initialization and cached for future runs. It ought to include parameters that may be simply modified and have a major impact on the performance of the library similar to characteristic flags, API URLs, and so on.
As well as, identical to utility builders, library builders additionally want to observe utilization and detect points. The primary and most evident answer that involves thoughts is to easily use a library for that. However there’s a catch. All of those options, that I do know of, are designed to work as a single tennant. Because of this if, for instance, a library integrates Google Analytics and the appliance that embeds that library additionally makes use of Google Analytics, solely certainly one of them can obtain knowledge. On this state of affairs the library is both blocking the host utility from receiving their analytics knowledge or it receives nothing. This limitation forces libraries to develop in-house options for each monitoring utilization and detecting points like crashes or ANRs. How to try this is an article in and of itself.
Right here’s a response I received from Firebase relating to utilization inside libraries:
“Firebase SDKs are usually not meant for library tasks. The options out there on Firebase had been built-in in an utility degree and never on a per module or per library foundation so, the use case for having this built-in on a library challenge isn’t potential or not supported.”
In contrast to functions that comply with the “don’t reinvent the wheel” precept, library builders ought to undertake the other strategy and maintain dependencies to a naked minimal. On Android the primary motive is that the appliance that will probably be utilizing the library might depend on the identical dependencies. On this case, just one occasion of the dependency will truly be current at runtime. Gradle will carry out a dependency resolution course of that may most probably find yourself in both the app or the library operating a special model of that dependency than the one it was developed with. This will result in surprising habits and crashes if the variations aren’t appropriate, particularly within the case of two completely different main variations. The exception to this rule are libraries that generate their product at construct time and don’t truly run any of their very own code at runtime. That is often the case with libraries that present database abstractions or libraries that carry out code manipulation utilizing annotation processing.
Including dependencies additionally will increase the scale of the library, generally considerably. A big measurement is a typical motive for utility builders to determine to not use a sure library.
We’re consistently being instructed to modularize our applications. There are many advantages to modularization, like shorter construct occasions, testability, code reuse, and extra. Google even ready different types of modules to make the lives of utility builders simpler. However what about library improvement? There appear to be no official pointers or assist for modularization of libraries.
In follow, there’s a large distinction between modules in functions and libraries. Whereas in functions modules are being packaged robotically into the apk (or bundle), this isn’t the case with jars (or aars). There are two methods to incorporate a couple of module in a compiled library.
One is to compile each dependency module to a jar (or aar) and import it as a file throughout the construct course of. It will end result within the code of the dependencies being compiled as a part of the unique module. This strategy makes sharing dependencies between two modules a giant drawback. It causes namespace collisions if the identical courses are current in two libraries on the identical time. This drawback could also be resolved by renaming the dependencies utilizing a course of referred to as shadowing however it leads to a couple of occasion of the identical code.
One other strategy to import a module is to publish it as a separate library and use it as a dependency. On this case, Gradle will deal with the model decision and the code could also be shared. Sadly it creates further challenges for model administration and prolongs the publishing course of.
Neither method is handy and each require a personalized challenge construction to have the ability to rely upon the code of the module in improvement and on the compiled module for publishing (relying on a file or a distant library). Additionally, they each complicate the publishing course of. As well as, including a brand new module requires extra work and the extra modules the challenge incorporates the extra sophisticated and fragile it turns into.
The underside line is that modularization of a library provides rather more work and complexity than modularization of an utility. I’d counsel utilizing modules for libraries solely with the intention to share code between completely different providers that can be utilized individually. On this case there’s no different selection and every service ought to be a separate library and due to this fact a separate module within the challenge. They will share code through the use of a shared module that’s revealed and consumed as a dependency by the providers (the second possibility introduced above). Such a module is often referred to as “basement”.
As talked about, this can even require the challenge to have at the very least two flavors — one for improvement and one for publishing. Within the improvement taste the providers rely upon the shared code as a module within the challenge. Within the publishing one they are going to rely upon the shared module as a Gradle dependency.
Discover that versioning of a library that has many modules and shares at the very least certainly one of them will be tough. They need to both use the identical model and be revealed all collectively or use a Bill of Materials (BOM) to assist completely different variations of the providers.
Use custom Gradle plugins to share configuration between the modules to keep away from duplicating the Gradle configuration.
My early makes an attempt at library modularization will be present in this article.
When creating an utility, it’s apparent that staying updated with the newest variations of the programming language, construct instruments, and dependencies is a greatest follow. It’s a lot much less apparent when creating a library. In library improvement it can be crucial that the code stays appropriate with as many functions as potential. Utilizing the newest variations might create compatibility points with older ones. For instance, focusing on a better Android SDK model than the host utility will lead to a construct exception.
One other compatibility subject that will come up is language compatibility. For instance, Kotlin’s suspending features and different options received’t be out there in a Java utility. Due to this fact, each launch should be examined towards the bottom supported setup and all supported languages.
I hope that this text sums up what for my part are a very powerful issues that utility builders ought to take note when approaching library improvement. I attempted to provide a quick presentation of the primary ones. Word that each one of them will be the subject of a separate article. Earlier than writing a library for the primary time, each developer ought to get a deeper understanding of the variations with the intention to keep away from making errors and ship a top quality product. Hope you discover it helpful.