DEV Community

Discussion on: Loosely coupled code: Babylon vs Three.js

Collapse
 
seanmay profile image
Sean May • Edited

I don't think that it is a conflation, at all.
I wouldn't think a Scene, in a general sense, is a renderable object, by name alone. I would think it contains / organizes renderable objects, but I would not expect the thing itself to be drawn and visible, innately, based upon its name. No more than I would expect a Stage to be shown, versus hosting the actors and sets, which are. The stage is necessary, but is a part of a play’s meta-structure, and should be virtually invisible to an engaged audience (and thus I wouldn't be rendering it for them to see).

Class inheritance is error-prone (logical errors) and the majority of researched authors/scholars warn to prefer other methods, lest your app end up with a gigantic god-blob object, or upon adding a new feature, you need to throw out your whole codebase, and redo your class taxonomy, due to the diamond problem, coupled with some unhealthy DRY obsession.

Quick example:

  • a Duck is a Bird
  • a Penguin is a bird

what should they inherit from so that

  • they operate in a single-inheritance system
  • they reuse all code so no method is written twice (penguins and ducks swim; ducks fly)
  • Bird operates as expected (don't have to catch bird.fly, because why even have a Bird in that case)
  • all subtypes of Bird adhere to Barbara Liskov’s subtyping assertion (that all subtypes, to be treated as valid subtypes, must be wholly substitutable for the supertype, such that the user is blind as to which it is given)

Now add an Ostrich.

With that understanding (and if you have a different understanding, that's fine... but this is the context I am working with, and I think it's pretty clear that I understand the taxonomic constraints of working with classes and deep hierarchy), I would expect a Scene to be able to handle different forms of renderable objects, so long as they conform to a known, common interface (again Liskov’s subtyping assertion... or Martin’s “Liskov Substitution Principle”). As it stands, you can not mix different objects, from different libraries, et cetera, because they rely on instance checks (because inheritance), and the only way to pass instance checks is to generate one of the same instance (or, in JS, to manually overwrite the constructor and proto chain) thus locking you into using only actual Object3D objects, and not other conformant objects.

This means that those objects and the scene are indeed tightly coupled. If I wanted a different type of external object to be renderable, well, I would literally need a completely different Scene, which, on its face, doesn't make any sense... and would mean that I need a completely different renderer, because I changed my scene, because I wanted to draw this one other object that I loaded using some other library.
Again, that sounds like my play needing a different stage, if I want to import a set from somewhere else; even if I am willing and able to modify the set to fit my current stage.

That is a prime example of tight coupling.

And yes, this also affects code organization, but as you can hopefully see from my example, this isn't a mutually exclusive statement, and my argument wasn't remotely about organization, as code-interop (and the blast-radius of making a piece of code interoperable) has very little to do with file structure.

Thread Thread
 
neovance profile image
Devon Bagley

Scene is just the root of a traversable tree of child objects, and may or may not be renderable, but have the transform, position, rotation, etc context of it's parent. The renderer calls scene.traverse, and scene.traverseVisible respectively, with a callback function to build the render buffer each frame.

The problem with inheritance that you are supplying is flawed by design. Bird is a classification that is not based on purpose, or function to begin with. Bird is a classification of animal which share certain traits, but those traits aren't related to the ability to fly or swim. This particular problem can easily be solved using well known design patterns within which inheritance can play a role. Direct inheritance is not the only tool applied to all problems.

There is no instance check. Three.js merely looks for a truthy property on the object called isObject3D, and isScene, etc for each "type" and assumes that the object implements the interface, so it should be possible to use objects from other libraries using a simple adapter to implement the interface.

In any case, looking at the files being imported doesn't really tell you the whole story of what is tightly coupled or not. Babylon is typescript, and thus all of the types used must be imported for static analysis of the type hints.

Thread Thread
 
seanmay profile image
Sean May

A Scene also needs a "translateOnAxis" and a "setRotationFromQuaternion", et al, because it is an Object3D. It doesn't just possess the information, but rather also possesses all of the methods, because of inheritance.
It also has Observer / Pub-Sub features, because Scene is also an EventDispatcher, because Object3D is an EventDispatcher.

This isn't tightly-coupled to you? How is the cohesion of all of the features of Scene, for you? Does the abstract concept of a "scene", or a scene graph, or a render batch list, or whatever abstraction you're going to use, always come with event pub-sub, and its own built in quaternion methods?
I would say that is low cohesion. If you got a list of all properties and methods on Scene, or on the superclass of Scene or on the supersuperclass of Scene... I believe that they would not all make sense to be on one object, to solve one problem.

"it should be possible"... until somewhere down the stack, if it assumes that it has a Scene, that it can add an event listener, or fire an event on it... and yes, if I were to write a new Scene from scratch, then it needs to contain everything that a regular Scene would inherit down the line, if I expect it to conform.

"In any case, looking at the files being imported doesn't really tell you the whole story of what is tightly coupled or not."
It really does. In every case, if a JS file imports another JS file, then the importer is tightly coupled to the imported...
If A imports B, then A gives B the chance to run code and change global state, just by virtue of being called. That's tightly coupled. Moreover, assuming that you have C that you want to use instead of B... if A has imported B directly, then chances are that the methods of A are going to use B directly, rather than using the C you want it to use, because it has reference to B in its code...
That is prima facie tight-coupling with A having a hard dependency on B (such that C can't be used as substitute, without also rewriting A).

"Babylon is typescript, and thus all of the types used must be imported for static analysis of the type hints."
And? Types can live in their own .ts files, or be imported separately using the import type { } statement, or the new import { type X } expression. None of that involves importing any running code. Moreover, all TypeScript types are structural types, not nominal types. That means that any and all types/interfaces in TypeScript are happily polymorphic and swappable for any interface that matches. Thus, you don't even need the class or a base class for polymorphism in TS. You don't need classes to all declare that they implement an interface. They don't even need to be class instances at all.
Loose coupling would be if Scene wasn't an Object3D, didn't have a bunch of math methods, didn't have pub-sub... didn't load/initialize a bunch of math libs, et cetera, if you load the file directly (by having it load Object, and having that load the rest)...
...and rather, I could use my own implementation of objects or scenes, separate from the whole ThreeJS or Babylon framework that they're residing in.