New technologies emerge one after another, and what remains after the wave fades are some classic design ideas.

On the front end, there’s jQuery, which used to be well known, and more recently, vue.js. There are many reasons why both are popular, but one thing they have in common is the elegance of their API design.

So this time I want to talk about a big topic — the art of API design.

Discuss the domain of content

This article is not a jQuery API appreciation, and when we talk about API design, we are not limited to discussing how a framework should design exposed methods. As the basic collaborative means of dividing and dividing the complex logic of the program world, the design of the generalized API touches every aspect of our daily development.

The most common API exposure approaches are Function Signiture and Attributes; When it comes to front-end IO, we need to pay attention to the data structure of the communication interface (JSON Schema). If there is asynchronous communication, there is also the question of how Events or messages are designed; Even when you rely on a Package, the Package name itself is the interface. Have you ever come across a weird Package name and made fun of it?

In short, “API design” isn’t just about the designer of the framework or library, it’s about every developer.

grasp

The core question is, how do we judge the design of an API as “good”? In my opinion, in a word, easy to use.

What is “easy to use”? My understanding is that it is easy to use as long as it is close enough to everyday human language and thinking and does not require additional brain thinking.

Don ‘t make me think.

Specifically, I’ve made the following points based on numerous (negative and positive) cases I’ve encountered over the years. In order of request, from lowest to highest:

  • Standard: morphology and grammar
    • The correct spelling
    • Accurate terminology
    • Pay attention to singular and plural
    • Don’t get the part of speech wrong
    • Handle abbreviations
    • Use the right tenses and voice
  • Advanced: Semantics and usability
    • Single responsibility
    • Avoid side effects
    • Design function parameters properly
    • Use function overloading wisely
    • Make the return value predictable
    • Curing glossary
    • Follow a consistent API style
  • Excellence: systems and the big picture
    • Version control
    • Ensure downward compatibility
    • Design extension mechanisms
    • Controls the level of abstraction of the API
    • Convergence API set
    • Divergent API set
    • Develop an API support strategy

(This article focuses on JavaScript as a language example.)

Standard: morphology and grammar

A high-level language is not much different from a natural language (English), so the correct use of morphology and grammar is a programmer’s basic literacy. This is especially important when it comes to API code that users call.

But in fact, due to the generally poor command of English in Asia… So the reality is not good — if proper lexical and grammatical usage were the threshold for compliance, many apis would fall short.

The correct spelling

It goes without saying that spelling a word correctly is the bottom line. However, API typos are still common, even in a large company like Alibaba.

There was a JSON interface (MTOP) that returned a set of store data to render in a front-end template:

// json
[
  {
    "shopBottom": {
      "isTmall": "false",
      "shopLevel": "916",
      "shopLeveImg": "//xxx.jpg"
    }
  }
]
Copy the code

At first glance, it looked boring, but I was debugging for about half a day and couldn’t render the store’s “store Level logo image”, the shopLevelImg field. What went wrong?

If you have eyes, you may have noticed that the interface is giving you the field name shopLeveImg, which is missing an L, and that this detail is hard to see with the naked eye in the light of the I after it.

Misspelled words are so common, for example:

  • For a library called Toast, the name in package.json is written as taost. As a result, the package could not be found in NPM.
  • A property in the factory method mistakenly wrote panel as a pannel. Causes the code to not run when initialized with the correct property name.
  • A URL (www.ruanyifeng.com/blog/2017/0… Entertainment is written as entainment… This is not a big deal, but the URL published after the change can not be changed, leaving a typo ugly.

Notice that these spelling errors often occur in string scenarios. Unlike variable names, the IDE cannot check whether the words in the string are scientific or consistent with some variable names, so we need to be especially careful about this when dealing with apis that need to be exposed; On the other hand, paying more attention to the TYPo hints (word misspellings) of the IDE would also help a lot.

Accurate terminology

As we know, the meanings of Chinese and English words do not correspond one by one. Sometimes a Chinese meaning can be explained by different English words, so we need to choose appropriate and accurate words to describe it.

For example, “message” in Chinese can be translated into “message”, “notification”, “news” and so on. Although these different words can all mean “message,” there are subtle differences in their usage and context:

  • Message: Generally refers to the message communicated between two parties. It is the content carrier. And they often come in pairs. Such as postMessage() and receiveMessage().
  • Notification: Often used for short notifications, and now even for iOS/Android notifications. Such as new NotificationManager().
  • News: A longer news message, heavier than notification. Such as getTopNews ().
  • Feed: A word that has been around since the days of RSS subscriptions, RSS is dying out, but the word feed is being used in more ways than one. Its meaning is understood but not expressed. Such as fetchWeitaoFeeds (). So, even if the Chinese meaning is roughly the same, use the exact words so that the reader can more easily understand the API’s role and context.

There’s a positive case for React. React has two methods called:

React.createClass({
  getDefaultProps: function() {
    // return a dictionary
  },
  getInitialState: function() {
    // return a dictionary either
  }
});
Copy the code

They are used to define the initialization component information and return the same type of value. However, the method name is default and initial respectively. Why not use one method?

The reason has to do with the React mechanism:

  • Props refers to an Element’s properties, either without an attribute value that was later assigned to it, or with a default value that was later overridden. So this behavior, default is a reasonable modifier.
  • State is a specific state in the Component state machine, and since it is described as a state machine, the relationship between states is switched. So for the initial state, I’m going to use initial. This small detail gives you a glimpse of the React mechanism itself, and it shows the wisdom of the API designers.

In addition, I recently came across a set of event apis like this:

// event name 1
page.emit('pageShowModal');

// event name 2
page.emit('pageCloseModal');

Copy the code

These two events are clearly a pair of positive and antisense actions. In the case above, show is used for “show window” and close is used for “close window”, which is a very intuitive literal translation. Instead, the words should come in pairs: show & hide, open & close.

Therefore, it must be emphasized here that positive and antonyms appearing in pairs should not be mixed. Other words that often come in pairs in the programming world are:

  • in & out
  • on & off
  • previous & next
  • forward & backward
  • success & failure

In short, we can try to expand our English vocabulary and use appropriate words, which will help us to accurately describe the API.

Pay attention to singular and plural

All data structures such as arrays, collections, and lists are named in the plural form:

var shopItems = [ // ... ] ; export function getShopItems() { // return an array } // fail export function getShopItem() { // unless you really return a non-array }Copy the code

The reality is often surprisingly bad. Recently, WHEN I changed a project, I encountered something like this:

class MarketFloor extends Component {
  state = {
    item: [
      {}
    ]
  };
}
Copy the code

The item here is really an array, even though it has only one member inside it. So you should name it items or itemList, but not a singular item anyway.

Also be careful to be consistent in the style of the complex numbers, either all -s or all -list.

Conversely, we don’t use plural numbers when we’re talking about things like dictionaries and maps!

// fail
var EVENT_MAPS = {
  MODAL_WILL_SHOW: 'modalWillShow',
  MODAL_WILL_HIDE: 'modalWillHide',
  // ...
};
Copy the code

Although this data structure appears to be a collection of key-value pairs, “map” already contains this meaning and does not need to be modified with complex numbers.

Don’t get the part of speech wrong

Another silly mistake to make is to confuse parts of speech, i.e., nouns, verbs, adjectives…

asyncFunc({
  success: function() {},
  fail: function() {}
});
Copy the code

Success is a very popular word in the programming world, but some students may get it confused and use it as a verb. In the above cases, the parts of speech of the paired words should be the same. Here they should be written succeed and fail. Of course, in this context, it’s best to follow convention and use the noun combination success and failure.

The whole part of speech of this pair is as follows:

  • Noun: success, failure
  • V. Succeed, fail
  • Adj. Successful, failed
  • Do you want to do STH successfully?

Note that if some words do not have corresponding parts of speech, consider adopting other forms to achieve the same meaning.

So even though most of us know that methods are named with verbs, attributes are named with nouns, and Boolean types are named with adjectives (or their equivalent), unfamiliarity with the parts of speech of certain words can lead to the final API naming problem, which can be awkward.

Handle abbreviations

The last thing to note about morphology is abbreviations. There’s a lot of confusion about acronyms (DOM, SQL) uppercase or lowercase, or just uppercase, and what to do in the hump format…

The easy and confusing thing to do for this problem is to capitalize all the letters of the acronym. (If there are clear industry practices for a locale, follow them.)

// before
export function getDomNode() {}

// after
export function getDOMNode() {}

Copy the code

In early versions of the classic front-end library KISSY, DOM was named DOM in the API and changed to DOM under the hump; In later versions, the DOM is written in all caps.

Another example of abbreviations is for long words, such as BTN (button), CHK (checkbox), and TPL (template). This depends on the specific language specification/development framework specification. If nothing is decided and there is no industry convention, it is always safe to write all the words.

Use the right tenses and voice

Because we generally call an API like “call an instruction,” syntactically, a function name is imperative and tenses use the present simple tense.

But in some cases, we need to use other tenses (continuous, past, future). For example, when we talk about life cycles, event nodes.

In some component systems, life cycles are definitely involved. Let’s look at how the React API is designed:

export function componentWillMount() {}
export function componentDidMount() {}
export function componentWillUpdate() {}
export function componentDidUpdate() {}
export function componentWillUnmount() {}

Copy the code

React divides several key lifecycle nodes (mount, update, unmount…). , describing these node fragments in the future and past tense, exposing the API. React uses componentDidMount instead of componentMounted. React uses componentDidMount instead of componentMounted. React uses componentDidMount instead of componentMounted.

Similarly, we need to consider using appropriate tenses when designing our event apis, especially if we want to provide a fine-grained event slice. Or, introduce prepositions like before and after to simplify:

// will render
Component.on('beforeRender', function() {});

// now rendering
Component.on('rendering', function() {});

// has rendered
Component.on('afterRender', function() {});

Copy the code

The other aspect is about voice, that is, the choice of active voice and passive voice. The best rule of thumb is to avoid the passive voice whenever possible. Because passive voice can seem confusing and unintuitive, we’re going to convert the API for passive voice to active voice.

Written in code, it looks like this:

// passive voice, make me confused
object.beDoneSomethingBy(subject);

// active voice, much more clear now
subject.doSomething(object);
Copy the code

Advanced: Semantics and usability

There are so many lexical and grammatical points, but it is only standard level. Making sure the API is available and semantic is what makes it “usable”.

Whether it is the friendly parameter setting or the sweet syntax sugar, it reflects the programmer’s humanistic care.

Single responsibility

Single responsibility is a well-known principle in software engineering. However, it is easier to know than to carry out. On the one hand, it may be difficult to divide “responsibility” in specific business logic, and on the other hand, some students have not formed the habit of implementing this principle.

From small function-level apis to large packages, it is important to maintain a single core responsibility.

// fail
component.fetchDataAndRender(url, template);

// good
var data = component.fetchData(url);
component.render(data, template);
Copy the code

As above, separate out the two independent things that are mixed up in a big blob of functions to ensure a single function level responsibility.

Further, (assuming) fetchData itself is better encapsulated by another class, split the original Component class (Component) and separate out fetching responsibilities that do not belong to it:

class DataManager {
  fetchData(url) {}
}

class Component {
  constructor() {
    this.dataManager = new DataManager();
  }
  render(data, template) {}
}

// more code, less responsibility
var data = component.dataManager.fetchData(url);
component.render(data, template);
Copy the code

The same is true at the file level, where only one class is written to a file, keeping the responsibility of the file simple (this is a natural rule for many languages, of course).

Finally, depending on the specific degree of business correlation, it is decided whether to make a package from a cluster of files or split into multiple files.

Avoid side effects

Strictly “programming without side effects” occurs almost exclusively in purely functional programs, and side effects are unavoidable in real-world OOP programming scenarios. Therefore, “avoiding side effects” here mainly refers to:

The function itself behaves stably and predictably. The function runs without unexpected contamination of the external environment. For pure functions with no side effects, the same parameters can always be executed with the same result. This idempotence makes the final result of a function predictable no matter how many times it is run in any context — which makes users very comfortable. Don’t worry about the details of the function’s logic, whether it should be called at a particular time, keeping track of how many times it is called, and so on. Let’s hope we don’t design apis in this case:

// return x.x.x.1 while call it once
this.context.getSPM();

// return x.x.x.2 while call it twice
this.context.getSPM();
Copy the code

In this case, getSPM() is used to get the unique SPM code for each link (SPM is ali’s common buried point statistics scheme). But the usage is weird: each call returns a different SPM string, so when we need to get more than one SPM, we say:

var spm1 = this.context.getSPM();
var spm2 = this.context.getSPM();
var spm3 = this.context.getSPM();

Copy the code

While implementationally understandable — this function maintains a counter that returns a self-incremented SPM D bit at a time — this implementation doesn’t match up at all with the getter function named idempotently. In other words, this makes the API unpredictable.

How to modify it? One way to do this is not to change the internal implementation of this function, but to change the API to a generator-like style, using an interface like spmGenerater.next () to get the self-incrementing SPM code.

Alternatively, if you want to keep the name, you can change the function signature to getSPM(spmD), accept a custom SPM D bit, and then return the entire SPM code. This also makes the invocation more explicit.

In addition to the function’s internal operation needs to be predictable, once it causes unexpected pollution to the outside, then the impact will be greater, and more hidden.

There are generally two ways to pollute the outside: one is to modify the variables of the outside scope directly inside the function body, or even global variables; The second is indirectly affecting the external environment by modifying arguments if the argument is a data structure of reference type.

There have been cases where operations on global variables have caused the entire container to collapse, so we won’t expand it here.

How to prevent such side effects? Essentially, you need to control read and write permissions. Such as:

  • Module sandbox mechanism, which strictly limits module changes to external scope;
  • Perform access control on key members, freeze write permissions, and so on.

Design function parameters properly

For a Function, the Function Signature is more important than the Function itself. Function name, parameter setting, return value type, these three elements constitute the complete function signature. Among them, parameter setting is the most frequent and most concerned part for users.

So how do you elegantly design the entry parameters of a function? My understanding is as follows:

Optimize parameter order. The more relevant the parameter is, the more advanced it will be.

This makes sense, the more relevant the parameter, the more important it is to come first. There are two more implications of this, namely, the postponement of omitted parameters and the setting of default values for omitted parameters. For some languages (such as C++), if you want to omit an argument in a call, you must define a default value for it, and arguments with default values must be postpended. This is mandatory at the compile level. For other flexible languages (such as JS), it is also a best practice to postposition the saving arguments.

// bad
function renderPage(pageIndex, pageData) {}

renderPage(0, {});
renderPage(1, {});

// good
function renderPage(pageData, pageIndex = 0) {}

renderPage({});
renderPage({}, 1);
Copy the code

The second point is to control the number of parameters. The user cannot remember too many entry parameters, so parameters are omitted if they can be omitted, or further, parameters of the same type are merged.

Merging parameters is particularly common in JS because of the ease with which composite data structures such as Object can be created. It is common to package many configuration items into a single configuration object:

// traditional
$.ajax(url, params, success);

// or
$.ajax({
  url,
  params,
  success,
  failure
});
Copy the code

The benefits of this are:

  • The user still needs to remember the parameter names, but does not need to care about the order of the parameters.
  • Don’t worry about long parameter lists. When parameters are merged into a dictionary structure, you can add as many parameters as you want without worrying about which ones need to be left behind.

Of course, everything has pros and cons, because of the lack of order, it is impossible to highlight which is the most core parameter information; In addition, setting the default values of the parameters can be more tedious than the parameter list form. Therefore, you need to design function parameters in an optimal way, for the same purpose: ease of use.

Use function overloading wisely

When it comes to API design, especially function design, there is always a mechanism: overload.

Overloading is a cool feature for strongly typed languages that can drastically reduce the number of function names and avoid namespace contamination. For weakly typed languages, however, since type-binding is not required at compile time, functions can pass as many arguments as they want at call time… So overloading becomes very subtle here. Here’s a look at when to reload and when not to reload.

Element getElementById(String: id)

HTMLCollection getElementsByClassName(String: names)

HTMLCollection getElementsByTagName(String: name)
Copy the code

These three functions are classic DOM apis, and I was thinking about these two things when I was learning them (switching from Java thinking to JS thinking) :

  • Why design the name for such a complex structure as getSomethingBySomething instead of using getSomething as an overload?
  • Only getElementById of these three functions is singular. Why not return HTMLCollection and make the function name plural to maintain consistency? Of the two problems, if the second problem can be solved, the structure of the three functions will be exactly the same and the first problem can be considered.

Let’s start with problem number two. Digging a little deeper into DOM, you know that ID must be unique for the entire DOM, so in theory getElementsById (note the complex number) will always return a Collection of only 0 or 1 members, This way the user will always call var Element = getElementsById(ID)[0], which is ridiculous. So the DOM API is well designed.

Since there is no solution to problem two, of course these three functions cannot be overloaded. To say the least, even if problem two can be solved, there is another problem: their entry parameters are the same, String! In strongly typed languages, arguments of the same type and order and return values cannot be overridden at all. Because the compiler cannot execute different logic through any one valid feature!

Therefore, do not choose overloading if entry parameters cannot be distinguished effectively.

Of course, there’s an odd way around it:

// fail
function getElementsBy(byWhat, name) {
  switch(byWhat) {
    case 'className':
      // ...
    case 'tagName':
      // ...
  }
}

getElementsBy('tagName', name);
getElementsBy('className', name);
Copy the code

A style similar to overloading, but actually branching logic at run time… As you can see, the total amount of information in the API has not decreased. Sure enough, this style can be useful in certain situations, but it’s not recommended in most cases.

Similar to the above style, this is done:

// get elements by tag-name by default
HTMLCollection getElements(String: name)

// if you add a flag, it goes by class-name
HTMLCollection getElements(String: name, Boolean: byClassName)

Copy the code

“Flag bits as a means of overloading” — often seen in early Microsoft apis, you can’t code a Boolean bit out of the documentation, and you have no idea what the Boolean bits are for, which greatly reduces the development experience and readability of the code.

There are so few scenarios that can be reloaded! Not really. There’s one scenario where reloading seems to work well for me: batch processing.

Module handleModules(Module: module)

Collection<Module> handleModules(Collection<Module>: modules)

Copy the code

When a user is often faced with dealing with an indefinite number of objects or objects, he may need to think and decide when to use singular handleModules and when to use plural handleModules. Overloading this type of operation into one (mostly seen in setter operations), while supporting both individual and batch processing, reduces the cognitive burden on users.

So, overload when appropriate, or prefer “multiple functions with the same name structure.” The principle is the same, as long as the logic is correct, the user burden should be reduced as much as possible.

By the way, the three getElements apis eventually evolved back to the same function: querySelector(selectors).

Make the return value predictable

Functions are easy to use in two ways: entry and exit. This section covers the exit: the function return value.

For getter-type functions, the immediate purpose of the call is to get the return value. So we want to keep the type of the return value consistent with the expectation of the function name.

// expect 'a.b.c.d'
function getSPMInString() {

  // fail
  return {
    a, b, c, d
  };
}
Copy the code

In this regard, the new ES2015 feature “deconstruction assignment” should be used with caution.

For setter-type functions, the call is expected to execute a series of instructions and then achieve some side effects, such as saving files, overwriting variable values, and so on. So most of the time we chose to return undefined/void — not always the best choice.

Recall that when we call an operating system command, the system always returns an “exit code”, which allows us to know how the execution of the system command turned out, without having to do anything else to verify that the operation actually worked. Therefore, creating such a return value style may increase robustness to some extent.

Another option is to have the setter API always return this. Return this to create a style of “chaining” to simplify code and increase readability:

$('div')
  .attr('foo', 'bar')
  .data('hello', 'world')
  .on('click', function() {});

Copy the code

One final anomaly is functions that execute asynchronously. Due to the asynchronous nature, callback can only be used to continue the operation of a value that takes some time to return. It’s especially necessary to wrap them with promises. Return a Promise for all asynchronous operations, making the overall API style more predictable.

Curing glossary

As mentioned in the previous lexical section, even if we try our best to use the right words, there are still some awkward situations in which we can’t make a choice.

For example, we often see PIC and image mixed with path and URL. The two groups of words are very similar in meaning (of course, strictly speaking path and URL are clearly different, so we’ll ignore them here), and if you’re not careful, you’ll get 4 combinations…

  • picUrl
  • picPath
  • imageUrl
  • imagePath
  • Worse is imgUrl, picUri, picURL…

So, produce a glossary from the beginning, including how abbreviations are case-sensitive, whether there are any custom abbreviations, and so on. A glossary may look like this:

Standard terminology meaning Prohibited non-standard words
pic The picture image, picture
path The path URL, url, uri
on The binding event bind, addEventListener
off Unbundling event unbind, removeEventListener
emit Triggering event fire, trigger
module The module mod

It is not only in public apis that the glossary specification is followed, it is best to follow the glossary in local variables and even strings.

page.emit('pageRenderRow', {
  index: this.props.index,
  modList: moduleList
});
Copy the code

For example, in this case I came across recently, I wrote modList and moduleList at the same time, which is a little strange.

In addition, for some invented, business-specific words, if they cannot be succinctly translated in English, use pinyin directly:

  • Taobao Taobao
  • Micro tao Weitao
  • There’s a Jiyoujia extremely

Here, do not translate “micro tao” into MicroTaobao… Of course, special words have English names except, such as Tmall.

Follow a consistent API style

This is kind of a review section. Many sections of morphology, syntax, and semantics all point to the same point: consistency.

Consistency minimizes information entropy.

Okay, that’s either a quote or I just made it up. All in all, consistent performance significantly reduces the user’s learning costs and produces accurate expectations of the API.

In terms of morphology, refine the glossary, keep the same words globally, avoid the emergence of different but similar words. In grammar, follow the uniform grammatical structure (subject-verb-object order, active and passive voice), avoid arbitrary sentences. Semantically, the proper use of function overloading provides predictable and even consistent type of function entry and exit. It can even be consistent in more detail, just to give a few examples:

  • Type logs in either Chinese or English.
  • Asynchronous interfaces either all use callbacks, or they all change to promises.
  • OnDoSomething = func or Object. on(‘ doSomething ‘, func).
  • All setter operations must return this.

It’s better to make the same spelling mistake for a word than to make the same spelling mistake once.

Yes, consistency, can’t be stressed enough.

Excellence: systems and the big picture

Whether it is large enough to be released to the industry or small enough to be used across departments within a company, a set of apis, once exposed, is a product, and the callers are the users. A small detail may affect the appearance of the whole product, and a small change may cause the collapse of the whole product. Therefore, we must stand in the overall level, even consider the entire technical environment, systematically grasp the design of the API within the whole system, reflect the big picture.

Version control

80% of development projects are bad at version control: arbitrary version names, empty and weird commit information, unplanned feature updates… It obviously takes a while for people to develop a disciplined development demeanor, but at least one thing needs to happen first:

The API guarantees forward compatibility when the large version number does not change.

The first <major> bit in <major>.<minor>.<patch>

This change represents a major change to the API as a whole, possibly incompatible, so users should be cautious about making dependency changes to the larger version; On the other hand, if the API has incompatible changes, it means that the large version number must be changed, otherwise the user is prone to routine update dependency and the entire system will not work, or worse, will cause online failure.

If the situation does not improve, users will choose never to upgrade dependencies, leading to more potential problems. Over time, these products (libraries, middleware, WHATEVER) will eventually be discarded.

So hopefully API providers won’t lock large versions to 0 in the future.

Ensure downward compatibility

If we don’t want to cause customers trouble with updates, the first thing we need to do is make sure our API is backward compatible.

API changes either to provide new functionality or to pay for bad design… Specifically, the change is nothing more than: increase, delete, modify three aspects.

First, delete. Do not remove publicly published apis, no matter how badly they were written. If you must delete, make sure “Deprecated” is used correctly:

For some poor API that you don’t want to keep, don’t just delete it, mark it as @deprecated and put it in the next minor update (say, from 1.0.2 to 1.1.0).

/**
* @deprecated
*/
export function youWantToRemove(foo, bar) {}

/**
* This is the replacement.
*/
export function youWantToKeep(foo) {}
Copy the code

Also, it is clearly stated in Changelog that these apis will be removed (not recommended, but still available).

After that, in the next big release (such as 1.1.0 through 2.0.0), remove the parts marked @deprecated and indicate in Changelog that they have been removed.

Then there are the API changes. If we were just fixing bugs, refactoring implementations, or adding small features, there would be nothing to talk about. But if you want to overhaul an API… For example, rework the entry parameters, rewrite the business logic, and so on.

  • Make sure the original API complies with the “single responsibility” principle, and if not, modify it.
  • Add a brand new API to fulfill the new requirements! Since our apis all follow a “single responsibility,” the need to completely change the API meant that the new requirements and the original responsibility were no longer matched, and it was better to simply add a new API.
  • Choose to keep or remove the old API as the case may be, and enter the “remove API” process described above.

Finally, the new API. In fact, even if you add code without deleting code, the whole thing is not necessarily backward compatible. Here’s a classic positive case:

// modern browsers
document.hidden == false;

// out-of-date browsers
document.hidden == undefined;
Copy the code

A new API for browsers to mark “current document visible”. An intuitive design would be to add property names like document.visible… The problem is that, logically, documents are visible by default, meaning document.visible defaults to true, and older browsers that don’t support this new property return Document. visible == undefined, a falsy value. Therefore, if the user simply writes:

if (document.visible) {
  // do some stuff
}
Copy the code

If you do feature checking, you get the wrong conditional branch in older browsers… On the other hand, if judged by the Document. hidden API, it is backward compatible.

Design extension mechanisms

There is no doubt that while maintaining downward compatibility, apis need to have a corresponding extension mechanism for sustainable development — on the one hand, developers can add features themselves, and on the other hand, users can participate in the ecosystem.

Technically, interfaces can be extended in many ways: extend, mixin, decorate… There is no right or wrong choice, because different extension methods are suitable for different scenarios: logically there is a derivation relationship, and the need to use the base class behavior and custom behavior, use heavyweight inheritance; It’s just extending some behavior function, but there’s no logical parent-child relationship at all, use combination; Decoration is more applied to a given interface, packaging it into a variety of new interfaces applicable to different scenarios……

On the other hand, for different programming languages, due to different language characteristics… Static, dynamic, etc., are more suitable for some expansion methods. So, what kind of expansion method to use, or depends on the situation.

In the JS world, there are some classic technology products, their extension has even formed an ecology, such as:

  • JQuery. $.fn. CustomMethod = function() {}; . This simple mixin approach has provided thousands of plugins for jQuery, and much of jQuery’s own API is itself built around this writing.
  • The React. React themselves have dealt with all the component instantiation, life cycle, such as rendering and update trival matters, as long as developers to inherit from a component class based on the React.Com ponent. This is a classic approach for a Component system.
  • Gulp. Compared to Webpack, which was a big hit in the last two years, I think Gulp is more like the logic of a building system — defining various “tasks” and then connecting them with “pipes”. A Gulp plugin is also pure, accepting file streams, returning file streams, and so on.
  • Koa. For mainstream HTTP servers, the middleware design is pretty much the same: accept the previous request and return a new response. For Koa, which is inherently promise-oriented, the middleware style is more like Gulp, except that one is a file stream and the other is an HTTP stream.

It’s not just big frameworks that need to think about extensibility; designing extensible apis should become a fundamental way of thinking. Like this living business example:

// json
[
  {
    "type": "item",
    "otherAttrs": "foo"
  },
  {
    "type": "shop",
    "otherAttrs": "bar"
  }
]

// render logic
switch(feed.type) {
  case 'item':
    console.log('render in item-style.');
    break;
  case 'shop':
    console.log('render in shop-style.');
    break;
  case 'other':
  default:
    console.log('render in other styles, maybe banner or sth.');
    break;
}
Copy the code

Render a set of feeds based on different types: product module, store module, or whatever. One day, there was a new requirement to support rendering tmall store module (display more Tmall logos, etc.), so a type = ‘tmallShop’ was directly added to JSON interface — this interface modification is very simple and intuitive, but not good. Without changing the front-end code, the tmallShop type goes into the default branch by default, resulting in weird rendering results.

Considering the relationship between tmallShop and shop is an inheritance, tmallShop can be used as an ordinary shop, performing all the logic of the latter. In Java terms:

// a tmallShop is a shop
Shop tmallShop = new TmallShop();
tmallShop.doSomeShopStuff();
Copy the code

Reflecting this logic into the JSON interface, it makes sense to add a subType field that marks tmallShop, while its type remains shop. In this way, even if the original front-end code is not modified at all, it still works, except that it cannot render some of the characteristics of the Tmall store.

Here is a very similar positive case, which is the MODULE JSON Schema designed by ABS Building system (site building system produced by Taobao FED) :

// json
[
  {
    "type": "string",
    "format": "enum"
  }, {
    "type": "string",
    "format": "URL"
  }
]
Copy the code

Again, type is the main type, and the extension field is changed to format to accommodate some of the extension features. In practice, it is also very convenient to add all kinds of new data structure logic.

Controls the level of abstraction of the API

What are the prerequisites for an API to be extensible? The interface is abstract enough. In this way, we can add all kinds of specific attributive and decorate more functions. Here’s an example in everyday language:

// abstract
I want to go to a place.
// when
{Today, Tomorrow, Jan. 1st} I want to go to a place.
// where
I want to go to {mall, cafe, bed}.

// concrete, no extends any more
Today I want to go to a cafe for my business.
Copy the code

So, when you design your API, be abstract, don’t get bogged down in concrete implementations, don’t get bogged down in concrete requirements, think big.

Consider a practical example: A React Native page framework wants to expose an event called “scroll to second screen” so that the page developer can listen for this event and better control the loading strategy of page resources (e.g. rendering is loaded by default on the first screen and the rest of the resources are loaded on the second screen).

However, due to some implementation reasons, the page framework can not use the page offset to accurately tell “scroll to the second screen”, but only “the first module of the second screen appears”. So this incident is not designed for secondScreenReached, and become the secondScreenFirstModuleAppear… Although secondScreenFirstModuleAppear not precisely defined secondScreenReached, but direct exposure to the specific API is too bad, the question is:

  • The user is relying on a very, very specific API, creating an additional information burden for the user. “The first module of the second screen appears!” It’s weird, the user doesn’t care about the module at all, all the user cares about is whether he gets to the second screen.
  • Once the page framework can truly achieve “scroll to the second screen” by page displacement, if we expose the high abstraction of secondScreenReached, then we only need to change the concrete implementation of this interface. On the contrary, we secondScreenFirstModuleAppear exposure is very specific, can only turn notify users: “you don’t have to rely on the incident now, change to our new secondScreenReached!”

Yes, the higher the level of abstraction is generally the better, making the API business-neutral, more generic, and easy to extend. However, it’s best for an abstraction geek like me to learn to control the level of abstraction of the interface and keep it at the right level instead of doing endless abstractions.

“SecondScreenReached”, “targetScreenReached”, “secondScreenReached”, “targetScreenReached”, “secondScreenReached”, “targetScreenReached”, “secondScreenReached”, “targetScreenReached” Is it more flexible and elegant? No –

  • It is important to take into account the specific business requirement scenarios when abstracting, and there is no need to pull out implementation paths that will never be reached. In this example, no one cares about the events on screen 3 or 4.
  • Too high an abstraction can easily create too many layers, introducing additional costs of coupling, communication, and communication between different layers, which can become a new headache. For users, it is also an additional information burden.

For a particular business, the more abstract an interface is, the more general it is, and the more concrete it is, the more it solves a particular problem. So, think clearly about the scope of the scenarios that apis are designed for, avoid lazy design, avoid over design.

Convergence API set

For a whole system of apis, the user is faced with the whole set, rather than a few of the individual apis. We should ensure that the apis in the collection are in the same abstraction dimension, and appropriately merge the API, reduce the information of the whole collection, and make subtraction as appropriate.

The product begins to do subtraction, is gentle to the user.

Convergence approximate meaning of parameters and local variables. There’s nothing wrong with the following set of apis, but it must be ominously intuitive for OCD:

export function selectTab(index) {}

export function highlightTab(tabIndex) {}

export function gotoPage(index) {}
Copy the code

Index, tabIndex, maybe pageIndex? To be sure, the naming of function parameters and local variables has no direct impact on the end user, but these inconsistencies are still reflected in the API documentation and can cause confusion for the developers themselves. So pick a fixed naming style and stick with it! If you forget, refer back to the “Solidified Glossary” section above.

Convergence approximates the function of duty. Exposing too many interfaces to users is not a good thing, but does merging different functions undermine the “single responsibility” principle?

No, because “single responsibility” itself depends on a specific level of abstraction. The following example is similar to, but different from, the previous example in “Using function overloading wisely.”

// a complex rendering process
function renderPage() {

  // too many APIs here
  renderHeader();
  renderBody();
  renderSidebar();
  renderFooter();
}

// now merged
function renderPage() {
  renderSections([
    'header', 'body', 'sidebar', 'footer'
  ]);
}

// call renderSection
function renderSections(sections) {}

// and the real labor
function renderSection(section) {}
Copy the code

Similarly, avoid exposing too many similar apis, and take advantage of abstractions to combine them to reduce the user’s stress.

For a scenario with a clear inheritance tree, convergence APIs are more natural and significant — Polymorphism is used to build Consistent APIs.

// bad: type-checking here
function travelToTexas(vehicle) {
  if (vehicle instanceof Bicycle) {
    vehicle.pedal(this.currentLocation, new Location('texas'));
  } else if (vehicle instanceof Car) {
    vehicle.drive(this.currentLocation, new Location('texas'));
  }
}

// cool
function travelToTexas(vehicle) {
  vehicle.move(this.currentLocation, new Location('texas'));
}
Copy the code

There’s a guy who’s taken the API to extremes: jQuery’s $(). Isn’t this style one of the killer features of jQuery back then?

Stop giving me foo() and bar() if $() will do it for me.

Convergence approximation function package. One level up, we can even merge similar packages.

In the Rax system of Taobao FED (RN-like framework), there are basic component labels, such as <Image> (in @ali/ rax-Components), <Link> (in @ali/ Rax-Components), as well as some package with enhanced functions. Such as <Picture> (in @ali/rax-picture), <Link> (in @ali/rax-spmlink).

In this case, the latter is an enhanced version of the former with more features. In practice, it is recommended to use <Picture> instead of <Image>. In this environment, the exposure of basic apis like <Image> becomes a nuisance. Consider incorporating the functionality of the enhanced package completely into the base component, i.e. <Picture> into <Image>, leaving the user with a single, standard component API.

Divergent API set

It sounds ridiculous, why should a collection of apis converge and diverge? Is it just for symmetry in the outline?

Of course not. This section exists because I have a case I have to mention that doesn’t fit in any other paragraph but here… No, to get back to the point, we do sometimes need to diverge the SET of apis, providing several seemingly close apis to guide the user. Because — as ridiculous as it sounds — in some cases, the API is not enough, but the user doesn’t realize that the API is not enough, and instead chooses to mix and abuse it. Look at this example:

// the func is used here requestAnimationFrame(() => { // what? trigger an event? emitter.emit('moduleDidRenderRow'); }); / /... and there requestAnimationFrame(() => { // another one here, I guess rendering? this.setState({ // ... }); });Copy the code

While refactoring a set of code, I saw that the code was filled with requestAnimationFrame(), a relatively new global API that executes an incoming function with a delay of close to 60 FPS, similar to setTimeout() optimized for a particular scenario, But it was originally intended to animate frames, not weird scenes.

After digging deeper into the code logic, I realized that the purpose of this call was to “delay doing something” and avoid blocking the main render thread. In this case, however, it is better to call setTimeout() directly to delay the operation. It’s not very semantic, but at least it’s better than pretending to be an animation. Worse, as far as I know the misuse of requestAnimationFrame() is not only present in this refactoring, BUT I’ve seen it in at least three different libraries — all of which have nothing to do with animation.

One possible inference is that requestAnimationFrame(callback) is called without specifying timeout milliseconds, whereas setTimeout(callback, timeout) is required. The former seems cooler to many users.

So, there are some apis out there that seem to be “folk remedies” : I don’t know why, but… Just use it!

In fact, the most appropriate solution for the above scenario is to use a newer API called requestIdleCallback(callback). The NAME of this API sounds very semantic: perform operations when the thread is idle. This perfectly fits the requirements of the scenario above, and it also comes with low-level optimizations.

Of course, because the API is relatively new, not all platforms support it yet. Even so, we can do our own polyfill for the interface first:

// simple polyfill
export function requestIdleCallback(callback) => {
  callback && setTimeout(callback, 1e3 / 60);
};
Copy the code

Another classic example of abuse is “Generator/yield” in ES2015.

The Generator mechanism, which was originally used in very limited scenarios, has been ingeniously adapted and packaged as a solution for asynchronous code synchronization. This is creative, of course, but semantically weak, making the code very difficult to read and causing maintenance problems. Instead, just use Promise.

Thankfully, the new VERSION of ES has a new asynchronous code keyword “async/await”, which really solves the problem of asynchronous code synchronization at the syntactic level, and the new version of Node.js already supports this syntax.

Therefore, we, as API developers, have to provide enough context-appropriate apis to guide our users and prevent them from doing unexpected “clever” things.

Develop an API support strategy

We say that a set of public apis is a product. The product must have a specific audience, whether it’s a global developer, or just a cross-departmental colleague; Products also have a shelf life, or life cycle.

For the target user group, we need to formulate the API support strategy:

  • How long is the support cycle for each large release.
  • Whether there is a long-term stable VERSION of API support. (Long – term Support)
  • How to upgrade from an older version.

Older versions are probably still running, but the maintainers have no time or energy to deal with these relics, so it’s better for developers and users to point out that certain versions are no longer maintained. Of course, don’t forget to provide upgrade documentation to guide existing users on how to migrate to the new version. It would also be better to identify the end of the life of the previous version as soon as we start a new version and inform users in advance.

There is also a technical caveat: it is best to have clear isolation between large versions. For a complex technical product, API is only the ultimate interface directly facing users, and there are specific environments, tool sets, dependency packages and other supports behind it, which cannot be mixed with each other.

For example, KISSY, once a classic front-end library. KISSY 6 relies heavily on TNPM (NPM of Alibaba’s internal network) and DEF Suite (front end tool suite of Taobao FED), although there are few API changes compared to the previous version 1.4. But you still can’t use version 6 directly in the old environment… This reduces the flexibility of free composition to some extent, but in fact, as the complexity of business problem scenarios increases, the solution itself will need to be more customized. Therefore, it is the status of the industry to package the environment, tools and other upstream and downstream associations together with the code into a whole technical solution.

So, isolate the large version, develop a good API support strategy, make our products more professional, so that users do not have to worry about.

conclusion

The above, is my experience since the realization of some “way”, three into the class, dozens of subdivision points, I do not know whether to give the reader you bring a little inspiration.

But in reality, the road is simple. I have always believed that programming and writing are not much different from each other

  • Logic and abstraction.
  • Domain knowledge.
  • Language sense.

Writing code is like writing, and designing aN API is like making an outline. Write frequently, think frequently, understand predecessors’ pattern, routine, learn some popular library design methods, master English, improve language sense…… I’m sure you can all design great apis.

Finally, here are the classic principles of API design:

Think about future, design with flexibility, but only implement for production.

Reprinted from: taobaofed.org/blog/2017/0…