Sharing is Caring - Patterns for JavaScript Library Design
[Applause] MAGGIE: Hi, everybody.
How are you doing today?
You are hitting the end of the conference, it's like the tired part, but we are going
to be okay.
So I'm here to talk to you about API design for JavaScript libraries and how you can do
some sharing, so I will say up on the screen are my two kids and the oldest, Dalton, really
likes to tell you that caring is sharing when he wants to eat the food on your dinner plate.
So who am I?
I really think that you need know nothing other than semicolons, tabs, all the code
in the slide is semicolons, tabs.
In all seriousness I work for Microsoft, I'm a crisis management engineer, so if you are
hosted on Azure and your things are not working, I am the person who is on the phone being
like: everyone wake up!
I am a maintainer of Moment.js, there are two of us here today, I don't know if some
of you saw Matt's talk earlier, but I am a maintainer of Moment.js and in addition I
am the JS Foundation's representative to TC3 ... 9 and I represent the JS Foundation which
is awesome and supports JavaScript projects in the ecosystem.
I will tell you, if you have made a JavaScript commit to an open source project we represent
you so if you would like representation, we are your representation as an open source
committer, and on that note I am the champion of the date re-work proposal that was talked
about earlier, so if you are interested in what's happening in JavaScript date, I am
working on that, but today that is not what we are talking about.
Today what we are talking about is libraries.
Is anybody here besides me a library author?
Okay, all right.
I am going to bring up some libraries and if it's one of yours and I got it wrong, you
shout real loud: you got that library wrong, Maggie.
So what is a library?
There are a lot of definitions for the word library, but what I am going to go with is:
a library is a bit of code that's useful when it's packaged up and distributed to other
people.
This could be internal or external, so there are tons of external libraries that we know
about, like LoDash, jQuery, Request and I think everybody in the room nearly knows every
one of those, right?
Internally there's a tendency to make internal libraries.
I know at Microsoft we have millions of internal libraries for the purposes of logging and
analytics.
Oh my gosh, I add internal libraries to everything for that.
For the purpose of this talk I do want to define what is not a library.
I am going to say things like Express or Angular or webpack are not libraries for the purpose
of this talk.
Instead, Express or Angular would be a framework.
They do a whole lot more than provide some useful code.
They tell you how you should code.
Webpack on the other hand would be a full tool suite so I'm not going to consider those
things libraries.
One other thing I am going to mention about this talk is that I'm going to get out examples
of some of the libraries I have listed at the top, and we might think of those libraries
as like lame, like it's from 2007, guys, but the bottom line is these libraries have survived
a really long time.
So we are really into the new hotness in JavaScript but for the purposes of this talk I have stuck
to libraries that we are all npm installing several years later because longevity is good.
Call me lame, it's okay, I can live with it.
So here is what we think a library is like.
Whose kids behave like this all the time, like happy and playing?
All right, so here is what having a library is actually like.
Does anybody have this toy besides me?
This is a toy only a grandparent would give you.
There's two kids, some buttons and some whipped cream.
[Laughter] What is what having a library is like out
in the open source space.
Like that looked really fun, and then boom.
Kept in the open source space it actually looked leek this.
Tim Wood, awesome guy, he was like: I'm going to share, I am going to care - and here we
are four years later with 2,400 closed issues and 176 open.
That's what having a library is like.
And your best defence against that is making it well.
So what makes a library good?
Small size.
I never want to hear about it again.
Great code!
Your users don't use your code.
They are never going to look at it.
Encourages functional programming practices: it's big right now, isn't it?
Wait, wait, this is JavaScript, the amazing mocha ... tool chain that I spent 12 weeks
assembling, that made my library good.
Guys, ease of use.
Ease of use.
Nobody wants to learn your library.
They won't.
Those 2,400 odd GitHub issues, 50% of them are, "I didn't read your documentation".
It's okay to make it simple.
You don't have to get into fancy patterns.
Your users are just going to go: what's partial application?
So I am going to break this out into four areas.
One: invocation.
How do I invoke my library?
How do I actually call it?
Two, configuration.
This is a huge part of any library, it doesn't do what I want it to do unless it changes
its configuration this way.
Three, defaults.
What should the default behaviours of my library be?
And finally errors, which are no easy thing to deal with.
So invocation.
Basically for any library there's two kind of simple ways to invoke it.
One is static invocation, right?
You just call a function.
So here we have two great libraries that I think everyone here has probably touched,
a request from Node and this is aesthetic invocation, I want to get Google.com and I
get a call back.
It's just static.
Or good old 27 million downloads a month LoDash, who here hasn't used it - I am asking for
filter, and I get back a filtered list.
Awesome.
Static invocation is a great pattern.
I would not shy away from it.
For logging libraries this is the way to go.
It's easy, you just say dot log, dot error, it's easy for people to figure out, they don't
have to think too hard about what's going on so this is my go-to if I want to write
a simple library.
It has a drawback though.
Here is LoDash without using any of the chaining or functional features.
If I want to get the sum of some odd numbers doubled.
We are going down - like this almost looks like a Christmas tree but it's not, it's just
LoDash.
All right, so when you start getting into this kind of trap with your library then you
are probably going to move to doing something like a factory function.
Now, many libraries have done really well with the factory function.
Up here, I have Q - everybody has used Q, right?
Oh yes, it returns promises, doesn't it?
Q fundamentally is a library that makes promises, and then you can do what you will with those
promises.
JQuery, good old ubiquitous jQuery makes jQuery objects.
Who learnt that when they first started coding ten years ago?
And Moment, my library.
By the way, I am going to shred Moment in my examples.
I will tell you of my pain.
But Moment again is a factory library.
You invoke Moment and you get back a Moment object.
And these kinds of things allow for some really, really good patterns to happen.
A factory function is what's going to allow you to go into a chaining API or at least
is one very good way to do that, so here we can see Q, again we are doing F call, which
is going to give us a promise back, and then fundamentally we can chain on another promise
and chain on another promise because we just keep on getting promises all the way down,
so this is a super helpful way to invoke a library, to have it give an object back and
then have that give an object back.
There are some problems with chaining but on the whole it works well for a lot of people.
Here is chaining API, here I'm adding three days to the current time, then going to the
start, and then going to a year ago.
God knows why I would want to do that but people have.
It reads really easy, I'm not questioning where is this invocation in the world, I'm
just cruising along.
JQuery, again like for all that we like to rip on jQuery for the bad programming practices,
man does this get our work done?
I still bring in jQuery if I have a static ASVC web page.
Why not?
So the next set of patterns that I would like to look at are configuration patterns.
Almost every library in the world is going to need some form of configuration, and it
can get pretty difficult for people to figure out how to do.
So we will take a quick look at Moment.
This is Moment like circa 2012.
It was great.
You had a date string and you could either pass it into the Moment constructor and let
the Moment constructor deal with it or you could specify a format to make sure that the
constructor got the right thing.
This is beautiful.
This was easy.
Moment 2017.
Constructor.
Here, you can construct with an array, you can construct a Moment from another Moment
object that will give you a copy, you can construct a Moment from a date, I guess that
makes sense; you can do a date string with a format in the English language or any other
language; you can use strict mode which will force you to match the pattern that you are
supplying; you could combine language in strict mode, oh, multiple formats, maybe you are
expecting four or five formats, I am going to throw an array in there and I still need
to support language and strict mode.
Whoa!
Like that was intense.
By the way, having this overloaded constructor where your constructor is expecting like a
billion options, I think - so Moment uses ES6 modules and I think to actually parse
out this constructor it happens in about ten files that are all about 100 lines of code
each, just to parse this madness it's like type checking and then what does this actually
mean?
This isn't that great.
I am going to give some credit here to my colleagues on the ECMA402 Committee.
They have been putting together the new internationalisation API.
Has anybody used those?
Oh yeah, the international API so the standards said how are we going to do configuration
because localisation takes a heck of a lot of configuration and they came up with this
really simple paradigm, and for all that it isn't fancy, I love it.
I think it's really going to serve JavaScript's users over time, and it's this.
I am going to create a new date time format here.
I must know the locale for the format.
The locale is required, right?
So I put the required parameter here, and then everything else I need to know I put
in an options object.
Now, this is used the world over, in a million libraries, and it's used because it works.
I'm easily specifying hour, minute, second and time zone formats without having a mess
in my constructor, without having to chain defaults to the global object.
None of that is happening.
So options, objects, it seems simple, it seems almost stupid for me to say but they are going
to get you a long way in cleaning up constructors like I showed you with Moment.
The other thing they do and again good old jQuery, you are beautiful - you really were
- is they have simple business logic.
If you go into options object less than then say you want the user to be able to define
a behaviour in the library you are going to be able to do something like that with this,
here we have jQuery AJAX requests and if I want to file a 404 I can parse a custom function.
This is beautiful.
This is easy for the user.
So basically, when it comes to configuration, do required parameters at the beginning of
your constructor and then tie the options object to the end.
This is going to be the easiest way for anybody to invoke your library.
Defaults.
Now, defaults are a fascinating topic, and they are difficult to get right.
Let's look at this.
This is a clean HTTP request with a built in no JS APIs.
I assume a lot of people here have done this, the plain no library HTTP request and some
interesting stuff is going on.
What I want to do here is I want to get the Moment, so I am going to Tim R Wood/Moment,
okay, great, I assume I get requests and a few things get crazy here, I actually do like
continuous update stream, response body, but I want to call it this, I get a re-direct.
301, move permanently.
That's absolutely true because Tim moved the Moment repo to the Moment org years ago, but
now because I get a re-direct I am going to have to start this whole process again and
go look for the re-direct link that I got.
That's kind of a pain.
Request.
Anybody here a Request user?
This is a good library, it does a lot of good stuff for us but a big one is it will automatically
follow a re-direct so here when I kick off my request for Tim R Wood Moment, I actually
get back the repository I wanted with the data about it.
It just automatically followed the re-direct.
Now, what this is is best by default.
When was the last time that you got a re-direct link and didn't want to follow it?
Right.
I am sure it has happened and I am sure you can configure requests to not re-direct but
like seriously, this is like 95% of the time, 99% of the time, you are going to follow the
re-direct, so if there's an obvious right best answer like that's like well over 90%
case, then do it!
Right?
But here is a flip side, I am going to go back to Moment, nothing like trashing the
library that I love so much.
Here I have Moment and I'm parsing in this date string, let's pan out 1025 and I get
out 1725.
Why?
So here is what is actually going on with Moment.
Time is complicated and when I parse in 1025 to the base Moment constructor I get back
1725 because it's converting it to local time, minus 5 here, and then I came to Berlin time,
right?
If, in fact, I had wanted UTC, I would use the UTC constructor.
If I had wanted to stay in minus 5 I would use parse zone.
If I had wanted a different time zone, for instance New York, then I would use the time
zone constructor.
There's no good default here.
Like, honestly, are any of those like the thing that you do all the time, are any of
those a 95% case?
No.
And this single API flaw has caused more support issues in Moment.js than any other thing by
about three orders of magnitude.
People are like: why isn't this the day that I thought it was going to be?
And all they really had to do was this: instead of having that default just Moment paren constructor,
if we had made people choose and made them say Moment.local, they would have gone, "Oh,
local time", and it would have saved us I don't know how many hours.
The next time we ran a major version for me to deprecate the major constructor will be
all of 30 minutes of work and it is happening.
We are not living like this anymore.
Get to update all the docs.
Then you get to update your code.
So default only when there is a best answer.
If there are several likely behaviours, don't lock yourself into the trap of answering support
requests on all of them.
So the last thing I want to talk about is errors.
So errors at one time in JavaScript, we were like in happy fun land, like we are on the
bus, we will just ... the browser and it will be really great, then we will hit F12 and
it will be great, then this happens.
This is directly out of the Node.js docs.
Exception must be handled or the no process will be handled immediately.
Who has had this happen?
Oh yeah.
So we were like: you are going to use it on a server and then the error is going to happen
and we are going to crash the server?
Oh God!
And for a long time people got this idea that
libraries should never throw errors.
Never throw errors in a library, you will kill things.
But that's difficult too, so I actually like any good software engineer went to Twitter
and was like: does Node give any official guidance about this error thing several years
in?
And it exploded into a massive Twitter conversation that lasted like four hours.
And the only thing back I really got from Node is that Miles cares a lot about errors.
He wanted me to tell you, so Miles cares a lot about errors.
But the general consensus on the thread can be summed up as: throw an obvious developer
error.
So let's break that one down.
Here is Moment doing actually a pretty good job at its thing.
This is a date and it's probably user input, and user input is always potentially junk,
right?
So we don't want to every time a user enters a date, if that date isn't in the format that
we want, start exploding Node servers, so instead what happens is we very politely take
your input and we say: oh, you have tried to format this; invalid date.
And then maybe your one user with the bad input sees invalid date but at least we haven't
taken out your Node process.
So this is done.
Bad user input doesn't crash.
But then we have this other thing going on.
You need to get on Moment to get the hours date part, and this looks great, it gets us
back 13, and must be some time in the early afternoon that I ran this, but this is weird.
I misspell hours, I put in hurs, and I get back a Moment object.
I would completely expect to get a Moment object from that method invocation.
At the end of the day this wouldn't be deployed onto a server in production.
This is a developer time error.
And it's one that's potentially very difficult to find.
You will go digging through your code and you will be like: where is it?
Why do I see JSON where I should see a string?
What the heck?
And you will get all the way down to the library and you will be like: those people! [Laughter]
So let's see an example of this actually being really done well.
Who here - this is immutable, people used Immutable from Facebook?
Sure.
Good library.
What Immutable does is it makes collections.
Here we are going to make a map and any time we change this map it will make a new map
and this is this library doing it very well.
We are making a map, ABC, 123, then we set B and the first map still has 2 and the second
map has 50 so that's what it does.
Okay.
But it does something awesome here.
When I try to make a map of the number 1, which is like impossible, because like how
do you map the number 1, it actually tells me: hey, developer, we expected an array or
an iterable object.
Could you hand that over?
[Laughter] This is good.
When it comes to errors, if it's about the parser user input then you are going to want
to try to suppress as best you can for Node.js but if you can tell the dead fat finger something,
help them out and undo the fat finger.
In conclusion, just make stuff easy to use.
Don't get fancy, don't spend a lot of time thinking about: oh, what are the functional
paradigms I can use here and how beautiful can my code be and what tool chain can I have?
At the end of the day, some have lived for years not for anything other than the fact
that people picked them up and were able to use them quickly.
So put your investment as a library author, whether internal or external, right there.
For invocation, static your factory.
One of them is going to work out for you.
Chaining may really enhance your problem domain.
Objects for configuration, everybody knows them and they really do clean things up.
Defaults, careful, careful.
When there is an obvious right answer, then pick that to be your default, but don't pick
an arbitrary default.
Do not do it.
And finally, throw for those obvious developer areas that make it so your users aren't sitting
there complaining about how you are a horrible person and how they couldn't find this mysterious
bug in your code.
After that, just share.
Get out on GitHub, through your company get people contributing to your library internally
and be friendly and be open to new ideas.
All right, well, thanks, everybody.
I love questions, so ... [Applause] >> Wasn't that fantastic?
Come on, more rounds of applause, please.
Woo!
[Applause]
Không có nhận xét nào:
Đăng nhận xét