Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Classes (WebIDL interfaces) should only be used when you have both data and behavior #11

Open
domenic opened this issue Apr 30, 2014 · 33 comments
Assignees
Labels
Agenda+ Status: Consensus to write We have TAG consensus about the principle but someone needs to write it (see "To Write" project) tc39-tracker Issues where TC39 input would be useful
Projects

Comments

@domenic
Copy link
Member

domenic commented Apr 30, 2014

I see a lot of classes that have only data, or have only behavior. Both of these cases should be represented by plain JS objects.


Edit by TAG: Current TAG thinking here

@marcoscaceres
Copy link
Contributor

I guess you will explain this a bit, but can you give me a quick tl;dr example/why behavior-only interfaces should be represented as JS objects? Not really getting what you mean.

@domenic
Copy link
Member Author

domenic commented Apr 30, 2014

Sure. If there's no state to mutate, then it's just a bunch of functions. (Note: functions, not methods.) And we represent a collection of named functions in JS by e.g. { foo: function () { ... }, bar: function () { ... } }, not by declaring a class with those functions as methods, and then creating an instance of that class, and then calling the instance's methods.

@marcoscaceres
Copy link
Contributor

Thanks for the clarification, @domenic. It would indeed be nice to be able to express this somehow in WebIDL.

There is something that does worry me tho: It's usually pretty clear when you are returning "data" objects, but I think it gets trickier to design objects that are purely behavioral - as it's easy to add accessor properties on them. Having them as classes makes it easier to allow others to extend them (either directly through the prototype or through extends proper).

@domenic
Copy link
Member Author

domenic commented May 1, 2014

I see what you're saying. However, in my experience, the behavior-only objects in web specs are usually pretty clear: they're often singletons, and often have only one or two functions. As such, a shared prototype is not useful anyway in those cases, reinforcing the idea that a class is not the right tool for the job.

@travisleithead travisleithead self-assigned this Dec 7, 2016
@dbaron
Copy link
Member

dbaron commented Jul 26, 2017

Should WebIDL namespaces be used for the behavior-only case?

@travisleithead
Copy link
Contributor

@dbaron that makes sense to me.

  • data-only objects => dictionary
  • behavior-only objects (functions only) => namespace
  • mix of behavior and data (where the object holds state) => class (interface)
@torgo torgo added the Status: In Progress We're working on it but ideas not fully formed yet. label Jul 26, 2017
@torgo torgo added Status: Consensus to write We have TAG consensus about the principle but someone needs to write it (see "To Write" project) and removed Status: In Progress We're working on it but ideas not fully formed yet. labels Apr 6, 2018
dbaron pushed a commit to dbaron/design-principles that referenced this issue Mar 5, 2020
Remove redundant suggestions
@kenchris kenchris self-assigned this Feb 3, 2021
@torgo torgo assigned LeaVerou and unassigned kenchris Feb 3, 2021
@torgo torgo added the Overtaken? This is an old issue that may no longer be relevant? label May 3, 2021
@cynthia cynthia added WRITEMEASAP and removed Overtaken? This is an old issue that may no longer be relevant? labels May 13, 2021
@cynthia cynthia self-assigned this May 13, 2021
@torgo torgo added this to the 2021-08-02-week milestone Aug 1, 2021
@LeaVerou
Copy link
Member

LeaVerou commented Aug 4, 2021

@domenic

I see a lot of classes that have only data, or have only behavior.

Could you please point us to these examples? It would be very helpful in writing this up.

@torgo
Copy link
Member

torgo commented Aug 4, 2021

So we discussed in today's call and we all agree we should write something. Probably this isn't a webidl specific thing. The principle is about the API shape. @LeaVerou will work on this with @cynthia and @atanassov .

@atanassov atanassov self-assigned this Aug 4, 2021
@marcoscaceres
Copy link
Contributor

@LeaVerou, I put together a gist with all the IDL in wpt/interfaces/*.idl... should hopefully help with finding examples there:
https://gist.github.com/marcoscaceres/1ef8f3eb3b37b75ecce8aea61c336e98

Looking quickly.... MediaError seems like a bad one (should have been a JSError maybe).

There are situations where using an interface (for data) is unavoidable, like with AudioTrack, VideoTrack, GeolocationCoordinates, because they are used types for attributes, and WebIDL doesn't allow Dictionary on attributes.

Anyway, hopefully the above helps with finding examples!

@LeaVerou LeaVerou added this to Unassigned in To Write Sep 16, 2021
@LeaVerou LeaVerou moved this from Unassigned to leaverou in To Write Sep 16, 2021
@RByers
Copy link

RByers commented Nov 25, 2022

Any update on this? I like the principle of avoiding unnecessary interfaces, and appreciate the TAG working on some guidelines to help improve consistency across the platform here.

Note that this just came up in a debate around exposing the Reporting API interfaces.

@annevk
Copy link
Member

annevk commented Dec 9, 2022

@LeaVerou those methods end up consuming the response's body.

@marcoscaceres
Copy link
Contributor

marcoscaceres commented Dec 12, 2022

@LeaVerou, a couple of examples:

  • GeolocationCoordinate - pure data.
  • Interesting case study: GamePad - currently pure data, but there is talk of making those into "live objects" that could receive events - that is, it would inherit from EventTarget (so maybe it was ok to make it into an interface... thought it would have been ok to convert it to an interface later, as everything is instanceof object).
  • ContactAddress - pure data. Should probably have just been an object.

There are some problems though, like there was a recent case where an APIs wanted to use the dictionary PaymentCurrencyAmount as an attribute value, which wouldn't be possible.

So, it's important to also thing about when/where a data object might be used (and if it will only ever be a return type). Otherwise, you can end up with (somewhat) clunky, but not tooooo baaaaaad, .getWhateverData()-like methods.

@annevk
Copy link
Member

annevk commented Dec 12, 2022

I think these days we would have used a dictionary for GeolocationPosition and GeolocationCoordinates though.

@marcoscaceres
Copy link
Contributor

Oh, yes, I should have included GeolocationPosition too. Accidental omission.

@RByers
Copy link

RByers commented Dec 19, 2022

Another example where I may have screwed up: InputDeviceCapabilities. It really is just pure data, but we made it an interface and put an attribute on UIEvent for reading it. If it weren't for that attribute, I would propose trying to change that one into a dictionary too.

@LeaVerou
Copy link
Member

LeaVerou commented Apr 20, 2023

Discussing with in a breakout with @cynthia, we realized we both think the guidance should be more nuanced:

First, we do agree that classes with only behavior don't make sense, that's just a collection of functions.

We are not so sure about objects that are only data. First, many of these objects may eventually evolve to contain behavior. Once you have state, it's easy to come up with methods to mutate it or make computations on it, and whether you have these from the get go or not should not dictate such a fundamental architectural decision.

We are definitely in agreement that "input-only" objects (e.g. options dictionaries) should always be plain objects (WebIDL dictionaries). For example, WebRTC APIs are terrible offenders here. Also, APIs accepting objects that are only or mainly data, should also accept plain objects, for better developer ergonomics (e.g. the headers parameter in fetch() is a good example).

However, class instances can be useful when objects are returned from an API and thus, can be passed around, as an object being an instance of a class is an implicit contract that it follows a certain schema (yes, that can be manipulated in JS, but everything can be). It's far easier for a developer to do e.g. obj instanceof GeolocationCoordinate than obj.latitude && obj.longtitude && obj.altitude && .... Furthermore, classes do no harm in this case, as they can be used as plain objects anyway (and should include suitable serialization rules to serialize as the equivalent plain object).

Another argument against that came up against using classes for data objects is namespace pollution, but this can be mitigated by these classes being hung on to the relevant APIs, rather than the global scope. E.g. GeolocationCoordinate could have been Geolocation.Coordinate.

In terms of defining or editing principles, some that could come out of this could be:

(Note: We use the term "Data-centric class" to describe a class whose instances primarily contain data, and potentially some auxiliary methods, or no methods at all)

  • Do not define classes that only have functions
  • Accept plain objects (WebIDL dictionaries) whenever possible (i.e. whether there is a corresponding data-centric class or not)
  • Data-centric classes should include a constructor signature that accepts a single plain object
  • Define JSON serialization rules for data-centric classes that return the equivalent plain object
  • Prefer defining data-centric classes as static properties on the APIs that they are related with (e.g. Geolocation.Coordinate instead of a global GeolocationCoodinate) (to be discussed more)
@LeaVerou LeaVerou added the tc39-tracker Issues where TC39 input would be useful label Apr 21, 2023
@ptomato
Copy link

ptomato commented Apr 21, 2023

Re. "Do not define classes that only have functions (e.g. never another Math object).", there is some discussion from a TC39 meeting that may be relevant:

In this discussion, objects such as Math, JSON, Temporal were termed "namespace objects" although it's still pending to write a definition in ECMA-262 of what that term exactly does and doesn't encompass. To my eye they don't seem like classes, at least for my mental model of what a class is: for example, you can't create an instance of Math.

@ljharb
Copy link

ljharb commented Apr 21, 2023

Additionally, although https://github.com/tc39/proposal-first-class-protocols is still stage 1 and too early to base design principles around, I'd personally find obj implements GeoLocationCoordinate as a protocol far more elegant and reliable than obj instanceof GeoLocationCoordinate with a class that otherwise shouldn't really be a class.

@bathos
Copy link

bathos commented Apr 22, 2023

However, class instances can be useful when objects are returned from an API and thus, can be passed around, as an object being an instance of a class is an implicit contract that it follows a certain schema (yes, that can be manipulated in JS, but everything can be).

What can’t be is private host-controlled state associated with the object, so an operation that takes (SomeInterface or SomeInterfaceInitDictionary) is guaranteed to be faster when the input is a real SomeInterface instance — there are no JS-observable conversion steps to perform at all for that Web IDL union type with that input. In ES as opposed to Web IDL, this can end up a bit like the difference between using new Intl.SomethingFormat(...) and something.toLocaleString(...); the former means interpreting the formatting options only once even if you then format many times, while the latter API must interpret the formatting options every time you format.

Unfortunately, it doesn’t seem like (many?) Web IDL APIs take advantage of that? For example new Request({ headers: reusableBonaFideHeadersInstance }) still does all the JS observable slow path stuff using the mutable public API rather than just consuming the private state which is already known to be valid. I would think that it’s desirable to use the (SomeInterface or SomeInterfaceInit) union pattern in those cases to take advantage of those invariants, and that the design principles might even want to say as much, but perhaps there’s a reason this has been avoided to date?

@torgo torgo modified the milestones: 2023-04-03-week, 2023-06-05-week Jun 4, 2023
@LeaVerou
Copy link
Member

LeaVerou commented Jun 5, 2023

Additionally, although tc39/proposal-first-class-protocols is still stage 1 and too early to base design principles around, I'd personally find obj implements GeoLocationCoordinate as a protocol far more elegant and reliable than obj instanceof GeoLocationCoordinate with a class that otherwise shouldn't really be a class.

Agreed, but how do you define "shouldn't really be a class"? I don't think whether it has methods or not is a good heuristic. Perhaps whether it could conceivably have methods in the future, but that's very handwavy.

@LeaVerou
Copy link
Member

LeaVerou commented Jun 5, 2023

In today’s breakout we decided to draft some text for these:

  • Accept plain objects (WebIDL dictionaries) whenever possible (i.e. whether there is a corresponding data-centric class or not)
  • Data-centric classes should include a constructor signature that accepts a single plain object
  • Define JSON serialization rules for data-centric classes that return the equivalent plain object

There doesn’t yet seems to be enough consensus for this:

  • Prefer defining data-centric classes as static properties on the APIs that they are related with (e.g. Geolocation.Coordinate instead of a global GeolocationCoodinate) (to be discussed more)

Whereas this doesn't seem to be much of a problem in existing APIs (as mentioned by @ptomato namespace objects are fine):

  • Do not define classes that only have functions (e.g. never another Math object).
@ljharb
Copy link

ljharb commented Jun 5, 2023

Math isn't a class, so I'm a bit confused why that's a problem. We also have JSON, Reflect, and Atomics that are all conceptually identical to Math.

@LeaVerou
Copy link
Member

LeaVerou commented Jun 5, 2023

Math isn't a class, so I'm a bit confused why that's a problem. We also have JSON, Reflect, and Atomics that are all conceptually identical to Math.

Yeah, that was a poor example, namespace objects are obviously fine. Basically, we don't want a constructible class, which holds no state but only behavior. We couldn't find any examples of this, but some people thought it may be good to document it anyway, but it's pretty low priority.

@bathos
Copy link

bathos commented Jun 5, 2023

DOMImplementation might be a good example of a historical interface that shouldn’t have existed. Each instance is 1:1 with a Document instance but it has no state of its own and represents nothing.

@domenic
Copy link
Member Author

domenic commented Jun 6, 2023

DOMParser is another.

@LeaVerou
Copy link
Member

LeaVerou commented Jun 6, 2023

Oh, excellent examples, thank you both!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Agenda+ Status: Consensus to write We have TAG consensus about the principle but someone needs to write it (see "To Write" project) tc39-tracker Issues where TC39 input would be useful