I've been looking on the web but I could not find any good advice on whether it could be considered a good practice to use constructors (new MyClass(...params).myMethod()) over classes with only static methods (MyClass.myMethod(...params)), or the opposite.
I would guess that the solution depends on the situation. If that is the case, could we draw some general guidance on when it is a good practice to use a constructor, and when to use the static methods approach?
@ilDon Welcome! Thanks for bringing this up. @BrunoScheufler @js-kyle @idori What do you think?
My general rule of thumb will probably be: whenever you represent an instance of something - use instances. Use static whenever your want factor/build an instance or to invoke a simple utility that unlikely to retain any state beyond the method level (e.g. logger)
Consider a 'ProductService' that has a few methods. Should we use it without instantiation, with static methods, we are at a risk of:
Limiting the possibilities for instance level data - what if we want each request to hold some contextual data (e.g. user id and roles) and pass between all classes, in a static service this won't be possible unless we pass it to any method, we can't save the context on the instance data. This a very tangible limitation
Harder to perform IOC (dependency injection) and mock for testing - most ioc/di framework perform on the instance level. Though It's doable to mock static dependencies (not set on the instance level), it's not as convenient
That said, static is just a bit faster (usually negligible in webapps) and whenever we use utility functions that are both frequently invoked (E.g. logger) and unlikely to contain instance data - static makes sense.
Also on the world of DDD, factories are usually implemented as static as it doesn't make sense to instantiate an instance for the sake of instantiating an instance...
Would love to hear @benjamingr take here...
@i0natan thanks for your thoughts, I also take a very similar approach :-)
Additionally, I find instantiation helpful when dealing with certain promises, or functions to be called by an event listener. By creating a new instance on each promise/event, I know that properties of each instance won't interfere with the others that might still be executing.
@ilDon Yes, sounds like a good example of interfering
Would love to hear @benjamingr take here...
Very simple, if a method needs to access an instance's state then it needs to be an object method obj.foo() - otherwise it should be a static method.
In general, anything except the main entry point should not own any mutable state. So anything static that accesses _any_ mutable state _must_ accept that thing as a parameter rather than access it as a global variable or a static variable (essentially the same thing).
That said, static is just a bit faster
This is not the case in V8 (and node), invoking a static method is exactly as fast as invoking an instance method. All method have context (even if it's empty) and a this - for static methods it's just the class itself (well, the constructor function).
Other than that what you said about not changing state beyond its own scope hits the nail pretty well.
In general, anything except the main entry point should not own any mutable state. So anything static that accesses any mutable state must accept that thing as a parameter rather than access it as a global variable or a static variable (essentially the same thing).
Very concise way of defining it 👍
This is not the case in V8 (and node), invoking a static method is exactly as fast as invoking an instance method
What about instantiation time?
@i0natan
What about instantiation time?
What about it?
@benjamingr
What about it?
Scenario A - 5000 requests/sec, each web request is using static objects, no "new" instantiation
Scenario B - 5000 requests/sec, each web request instantiates 5 "new" objects (e.g. ProductService, ProductDAL, etc)
Wouldn't B spend more time on instantiation of objects and more memory (though negligible)?
According to this jsperf it seems that the static version is faster, what do I miss?
That jsperf is bonkers, I would recommend never believing a jsperf - in particular one that is susceptible to DCE like that one.
That said, allocations aren't free of course - regardless of stuff being static or instance methods.
There is no additional overhead in doing stats.foo() and not MyService.foo(state) - allocating state itself is of course not free.
I'm not sure I'd bother caring about allocating a ProductService though one can always use a flyweight or some other pooling strategy to reduce allocations.
allocating state itself is of course not free
That what I meant. Not using static has an additional *minor operation. The rest resonate with me as well 👍 :]
@i0natan wait no! That fact has nothing to do with static/not-static it has to do with state. It doesn't matter if you're allocating a UsersProvider { id='15' } or passing an object {id: 15} to a static method in terms of memory. It's the extra state you keep around that takes the memory and not the static/instance stuff.
@benjamingr
CPU-wise - Can you share a link/other keywords on the allocation you refer to so I can do some further reading?
Memory-wise - going with the example above, are we in agreement that B will consume more memory (state being transferred across static or instance methods will have the same impact, however the instances themselves will consume a *bit more memory)?
Classes should be used when you have 1 to many instances with state that needs managing.
The use case for static methods on classes in javascript is not common (unlike Java).
I personally prefer the first, as the purpose of classes is simply to provide namespacing. And i get that from my favourite module system already. And i've had enough singletons for a lifetime (thanks java spring!).
Do you have a singleton with state? There are three solutions
All of this is mutable global state which has severe design implications when your app grows. There is a fourth alternative:
That way you don't have mutable global state since it is controlled by the main module which can decide to pass it around and you can easily track mutations and modifications.
It's also by far the easiest to test - even if you run async tests in parallel - and you don't have to remember to revert anything.
There is absolutely no difference (conceptually) between file scoped modules and global variable.
Singletons are a bad idea even in Java for a while....
The purpose of classes is not to provide name-spacing, it is to encapsulate state mutations and state in order to control and enforce how state is used in order to:
The use case for static methods on classes in javascript is not common (unlike Java).
That's only because Java doesn't have global variables and JavaScript does - that's the part static and singletons play because Java doesn't have an actual module system (well, it does now) so all objects are dumped to the package namespace and making a method/property static makes it effectively global.
@benjamingr 99% resonates me with a lot and well explained. About the 1%:
There is a fourth alternative, pass it around to modules that require it...
Not sure why this is not yet another variation of global-mutable. Say, we pass OrderService to some job executors (objects that perform custom order-related tasks) and one of them intentionally or as an innocent side-affect mutate the state, wouldn't it affect all the other and lead to the same tragic outcome? Agree that this has better mitigation capabilities (use IOC for example and change to on-demand to immutable OrderService using configuration) but still we deal here with mutable objects
@benjamingr @sloops77 - Putting the edge cases and implementation details aside, it looks like we share the understanding that classes should be used whenever possible (or any form of non-shared state), what exceptions do you see for this, when would you deviate and use a single instance of an object (e.g. DB connection provider)?
... and one of them intentionally or as an innocent side-affect mutate the state, wouldn't it affect all the other and lead to the same tragic outcome?
Mutable shared state still isn't great - but at least it's not global so if you want to pass that capability around you have to be explicit.
@benjamingr @sloops77 - Putting the edge cases and implementation details aside, it looks like we share the understanding that classes should be used whenever possible (or any form of non-shared state), what exceptions do you see for this, when would you deviate and use a single instance of an object (e.g. DB connection provider)?
I think the whole thing is entirely orthogonal to whether or not classes are used and you can write good OOP without using classes at all (and terrible OOP with classes).
The important thing isn't having the template or the sugar for binding state to behaviour - it's whether or not the state is globally accessible (from the perspective of the caller).
what exceptions do you see for this, when would you deviate and use a single instance of an object (e.g. DB connection provider)?
I would not make a "single DB provider" a global mutable shared object - both because I might want to introduce pooling at some point, because I might want to swap the DB at some point (when I split it to two databases) and because I want to easily control who has access to the database.
Note this is not about whether we have one of something (often the case), it's about whose concern it is to own and manage the life-cycle of that "one thing". Making it global is effectively saying "no one" (or - the life time of this object is tied to the application).
A caveat to this is that if you're writing short uncomplicated programs (for example, when teaching) then none of this is a big deal since if the problem you have to solve is easy you don't need to apply all design principles.
There are two questions here:
I do maintain however that:
@benjamingr
I would not make a "single DB provider" a global mutable shared object
I again agree with 99% and iterating the core message here - aim to keep everything separated and autonomous which implies encapsulated (make it also small, but well... this belong to another discussion...:))
My remark about DB access wasn't about the DAL/provider. I tried to outline with you, what are the cases for example, where using a global might seems reasonable. Not ideal, reasonable. One case that I propose is: Have a DAL with local state or no state (e.g. no static class), but all DALs must share the same DB connection so I would formalize this assertion and create a object - SequelizeDBConnection that provide that single Sequelize (or whatever) to the DALs
Thoughts?
The problem of globality only applies to global state - if there is no global state the fact something is accessible globally isn't a problem since nothing can be modified "elsewhere" anyway.
It is perfectly fine to have immutable (namespaced if possible!) globals.
That said, in your case there is global state (the SequelizeDBConnection) so I don't think it holds - I'd inject the instance instead so there is no violation of the single responsibility principle between the layers.
Also - what happens if tomorrow you want to use your DAL/provider in another project?
@benjamingr
By the "singletons pattern" i meant by using DI (hence my reference to spring - the classic java DI framework). This is exactly what you proceeded to describe after my post - you have a better way with words than me!
I dont agree that module scoping and global state is the same. I can't get access or mutate to a unexported variable defined in file scope from a module. And I can control access via exported functions. Your point about testing however is well taken - it's definitely an advantage.
It depends if you want to advocate for classes over functions, and for DI over the module system.
Avoiding mutation (global or local) and instead doing immutable transforms in my experience is much preferable. Of course there are performance implications but such systems are much easier to reason about.
@benjamingr
I'd inject the instance instead so there is no violation of the single responsibility principle between the layers.
What SRP violation? DAL just depends on some npm package (probably private one) that knows to provide the physical link to the DB. It has no idea what happens there
Also - what happens if tomorrow you want to use your DAL/provider in another project?
Same thing as I would wish to relocate the DAL without the SequelizeConnection - I'd have to take the code and also copy the dependencies (e.g. logger) to the target package.json. Same happens with the SequelizeConnection, I have not only to copy the DAL but also it dependeneices reference
By the "singletons pattern" i meant by using DI (hence my reference to spring - the classic java DI framework). This is exactly what you proceeded to describe after my post - you have a better way with words than me!
For what it's worth I'm really not a big fan of Spring or Spring DI.
I can't get access or mutate to a unexported variable defined in file scope from a module.
That's also true for global variables:
global.foo = (() => {
let foo = 15;
return { setFoo(newFoo) { foo = newFoo }, getFoo() { return foo }};
})();
Although really that point was mostly about lifecycle management.
Your point about testing however is well taken - it's definitely an advantage.
It's not just testing (testing just highlights it) - it's reuse in general, testing just takes your one-use-case code and adds a second use case. Anything else that tries to reuse the code or swap parts of it will run into the exact same architecture issues.
Avoiding mutation (global or local) and instead doing immutable transforms in my experience is much preferable. Of course there are performance implications but such systems are much easier to reason about.
If you avoid mutation then nothing happens. Programming is tastefully composing side effects :D The question is how to best control mutation and side effects - making them as local as possible and enforcing the state with invariants is certainly an effective tool. Immutable objects are for also prone to other issues "regular" state isn't (like stale/dead references).
@i0natan
What SRP violation? DAL just depends on some npm package (probably private one) that knows to provide the physical link to the DB. It has no idea what happens there
An object being in charge both of its creation/initialization/lifetime and _some other thing_ (being a DAL I'd presume)
I'd have to take the code and also copy the dependencies (e.g. logger) to the target package.json. Same happens with the SequelizeConnection, I have not only to copy the DAL but also it dependeneices reference
That's only if you want to use it in another node project - if you want to reuse it in the same project - or you ever want to reuse code between projects in any way you're pretty much stuck.
@benjamingr Not sure I got it, might be that I miscommunicated the DAL design, LMK whether your
comments still apply after you read my clarification here:
Is this what you had in mind?
It's not just testing (testing just highlights it) - it's reuse in general, testing just takes your one-use-case code and adds a second use case
Would you really prefer passing an object all over (design for something that isn't written in your requirements) instead of mocking the dependency magically using a sinon-like library? the trade-off is between cleaner design + dirty testing (sinon) vs over-design + clean testing. Isn't it so?
p.s. I don't stand 100% behind the idea to avoid passing objects, mostly thinking out loud that I would sacrifice some traditional software engineering for simplicity
Is this what you had in mind?
Yes, I would not pass that code review if the code is reasonably large. If you're doing something different though let's talk about it.
That code does "magic", it holds global state (in a singleton no less) which causes implicit dependencies. Because some day I'm going to write a unit test for some module that is magically using it 3 layers in - and it's going to drop my production database for a unit test and I'm going to cry.
If this article doesn't explain it well enough let me know - I got a few references I can find :)
Would you really prefer passing an object all over (design for something that isn't written in your requirements) instead of mocking the dependency magically using a sinon-like library?
Absolutely. As a counter:
SequelizeDBConnection? Let's say I have modules A and B with a dependency on the db package - A has version 1.2 of it and B has version 1.3 of it - I either have an error (and I have to do the refactoring above) - more than 1 database connection meaning it can't enforce its singularity or I have to very carefully monitor the dependencies of my dependents in every project that uses either A or B . How is this (pretty common) case handled?Of course, I would not _actually_ pass _everything_ around in real life - I would use an injector if the code is large-ish.
the trade-off is between cleaner design + dirty testing (sinon) vs over-design + clean testing. Isn't it so?
If your design breaks when you try to use your code in any but exactly-the-way it was intended that's not "dirty testing" that's "fragile code" :)
I'm not sure how the following is harder:
//db.mjs
export class DatabaseAdapter {}
//foo.mjs
export class Foo {
constructor(name, db) { this.db = db; this.name = name } // this is nicer if ts is allowed
query(sql) { return this.db.query(sql, name) }; // or use name some other way
}
//main.mjs
import { DatabaseAdapter } from './db'
import { Foo } from './foo'
const db = new DatabaseAdapter(require('./dbconfig.json')); // our 'singleton' but it's not global
const foo = new Foo('john', db);
foo.query('SELECT * FROM Blah');
Vs. the following (with mutable global shared state):
//db.mjs
export class DatabaseAdapter {} // adapter now needs to be aware of credentials or expose an init
export const instance = new DatabaseAdapter(require('./dbconfig'));
//foo.mjs
import { instance } from './db';
export class Foo {
constructor(name) { this.name = name }
query(sql) { return instance.query(sql, name) };
}
//main.mjs
import { Foo } from './foo'
const foo = new Foo('john', db);
foo.query('SELECT * FROM Blah');
But again - do whatever works for you, if you're doing a hackathon project, a project that doesn't get deployed to prod or something that needs to be "quick and dirty" use mutable global shared state as much as you want.
This is just my opinion, I am just one person, I recommend watching the talk I linked to (I can link to more).
@benjamingr I wouldn't come here to discuss 'quick and dirty' code for small apps. My goal is to discuss design patterns for the most challenging and largest application, I believe that my code serves this purpose and the same time I believe that it might not and I can learn and improve. Mostly from you.
That code does "magic", it holds global state (in a singleton no less)
But having a singleton is a technical requirement we can't avoid (Sequelize, for example, forces that) and your code also uses a singleton (./db would have to take care that only 1 real instance of Sequelize co-exists) - so our discussion is not about singleton rather about how to design them
Would you expect to have to refactor every single file that needs access to the database in case you need a second instance
That's exactly where my design excels-in. SequelizeDBConnection abstracts everything related to the connection === changes should take place in one object. I guess you're aware that practically it's not really 1 connection rather a typical pool implementation that rarely changes.
Would you expect every single piece of the code to be able to access and query the database?
Yes, as a typical js/node side effect. Also in your code anyone can grab a copy of ('./db'), what the difference?
If this article doesn't explain it well enough let me know
This article describes an utterly different *white-box case where A might alter its behavior based on B and C which are configurable- a good case to explicitly state the dependency. I'm describing a *black-box scenario where A uses B and no one in the world is invited or should know to know or alter this behavior. Its an internal implementation. Why tell the world about my implementation (encapsulation)?
I'm not the first to challenge Miško Hevery tendency for over structured dependency injection, the Angular philosophy of strict object dependencies design (DI) is far from being a consensus
What happens if you want to reuse code that uses the database but it's running a different version of SequelizeDBConnection
Sorry, I missed that point, kindly clarify. SequelizeDBConnection can manage version like any other npm package. Isn't your (./db) an npm package? if not, how would you reuse it across microservices?
Bottom line
I don't see a big difference between your example and mine except for the DI style you brought. While I kept it strict and simple -> A needs and uses B. You involved another orchestrator, main or DI, that defines that C knows that A needs B so it will take for that. Then with module D, E, F, G -> C will become one big piece of centralization.
What I heard from you is - I want to keep it very structured, explicit and ready for testing - it has a lot of value when the probability of a short-term issue is high. What I'm suggesting, is that we should not add any fancy design vehicle without an immediate reason and we shouldn't design our code for testing rather for the requirements and simplicity. There are many voices these days that go against the 'design for test' approach.
p.s. interesting discussion... :]
But having a singleton is a technical requirement we can't avoid (Sequelize, for example, forces that) and your code also uses a singleton (./db would have to take care that only 1 real instance of Sequelize co-exists) - so our discussion is not about singleton rather about how to design them
By singleton I mean an object in charge both of its usage and its lifetime - i.e. this. I am _not_ talking about "just having one of something". Effectively having global state (since the singleton's class is global and importable).
You can just have one instance of DB - anyone can import ./db.js but they can't get an instance (they can be passed one) - like in the example above.
That's exactly where my design excels-in. SequelizeDBConnection abstracts everything related to the connection === changes should take place in one object. I guess you're aware that practically it's not really 1 connection rather a typical pool implementation that rarely changes.
I don't understand, if there are two places that call getInstance() and suddenly they need a different instance - in your design I have to change every call site to something other than getInstance - like getSpecialInstance. With "passing things in" (or dependency injection) - the code at the usage site doesn't need to change at all - it's just passed a different dependency.
The fact this makes testing easy is just an indication it's on the right track.
Why tell the world about my implementation (encapsulation)?
I think you should look at don't look for things again. The problem with this sort of "black box" is that the dependency is implicit and mutation can happen from everywhere. If there are two places that access the database and do so internally and I want them to run in a transaction for example - I'm stuck.
Sorry, I missed that point, kindly clarify. SequelizeDBConnection can manage version like any other npm package. Isn't your (./db) an npm package? if not, how would you reuse it across microservices?
I mean if you have two packages you depend on that depend on it - it is now no longer a singleton :D
I don't see a big difference between your example and mine except for the DI style you brought. While I kept it strict and simple -> A needs and uses B. You involved another orchestrator, main or DI, that defines that C knows that A needs B so it will take for that. Then with module D, E, F, G -> C will become one big piece of centralization.
I encourage you to read more about it - I maintain there is a huge difference between mutating global shared state and passing things in - both for readability, refactorability, usability and other reasons :)
Namely, read about service locators vs dependency injection.
I have worked in projects where DI was only used, and others where importation was used.
I dont see such a big disparity in maintainability - in fact big DI projects are often very hard to maintain.
With DI projects often dev's are so frustrated without being able to import a thing, that they just willy nilly start adding more and more injected objects without really thinking. It makes it VERY difficult to distinguish between what is initialization state, and what is simply a dependency or a utility. Because injection is often co-located with the module containing the class (or even-worse magical via introspection of variable names). This pollutes the API. It makes it harder to read, harder to understand what is going on. You end up seeing comments explaining the dependencies, or an attempt to enforce via code review what "kind of" dependencies go where in the constructor's parameter list. Eg initialization state first, then services, then utility functions.
In either scenario (DI or import), the practical effort in updating the dependency from db to db2 is the similar. If you are importing modules you shouldn't add a function getSpecialInstance to the existing module - instead just implement the same api on a new module.
Plus furthermore - how many change databases in this way? It's kinda like the strawman of using an ORM to abstract away your db - yeah it sounds good in theory - but if you are changing db its probably away from a RDBMS to Cassandra or Redis or something. You are gonna change a lot more than simply the db reference. What this means is that DI isnt very useful in practical terms for something that changes extremely rarely, and the side effect of API pollution is something the devs live with everyday.
I favour using modules over objects in js - because objects are inherently dangerous because state is not easily protected. They can be manipulated by the unscrupulus programmer unless you restrict mutation via lint rules. Even worse is if multiple references to an object are shared, mutation can be non deterministic. That the object was imported or received via DI is not really the issue - all references of course get the update version unbeknownst to the other reference holders. In my experience these are the killer problems, not the usage of import over DI. That the reference is available via module import or injected doesnt really matter for this sort of problem.
Going back to the stale reference issue - practically speaking moving to immutability causes a change in programmer behaviour - they no longer keep references because someone else might have changed it. They start to use functions that return values instead. The only location state is kept is in the db (or caches).
Also if parrelizability of tests is important (going back to a previous discussion) - you can always use a form of currying where you explicitly split "dependencies" from "initialisation of state" in the module/function version.
Remember, Mishko's articles were written in a time where module systems in js barely existed. DI's popularity came from Java - that has lots of problems because they didnt have functions or a good module system.
I dont see such a big disparity in maintainability - in fact big DI projects are often very hard to maintain.
Do you think there might be a correlation between the project size and using DI? Larger projects are harder to maintain. In my personal experience Java projects are also harder to maintain 😅
Seriously asking - not saying your experience is any less valid than mine - but my experience is that the problem is usually Java and not DI as a concept.
It makes it VERY difficult to distinguish between what is initialization state, and what is simply a dependency or a utility. Because injection is often co-located with the module containing the class (or even-worse magical via introspection of variable names).
I'm definitely not a fan of magic. If something has no state and doesn't require configuration - there is no point in injecting it. If something has no mutable state - it still makes no sense to inject it. However for things that carry around mutable state - injecting definitely makes sense.
If developers are lazy and are abusing it in an "effectively global" way (all dependents assume that objects are singular and global) then as you said - it's abused and it's effectively global. DI is a tool to use in order to have less globals - you can configure an injector and make it behave effectively globally like you said though that's abuse.
You end up seeing comments explaining the dependencies, or an attempt to enforce via code review what "kind of" dependencies go where in the constructor's parameter list. Eg initialization state first, then services, then utility functions.
I hope that's not coming across as harsh but that sounds awful. If injection isn't obvious and doesn't just behave like passing parameters (that is, if you can't resolve the object yourself without an injector) then it makes no sense in my opinion. The whole point is that the object isn't concerned with the initialization of its dependencies. If that's broken it's not good code in my personal opinion.
In either scenario (DI or import), the practical effort in updating the dependency from db to db2 is the similar. If you are importing modules you shouldn't add a function getSpecialInstance to the existing module - instead just implement the same api on a new module.
But then you have to update every single file using the database (either to call the different method or use the new module - and then you'll need a third module to share the configuration).
You are gonna change a lot more than simply the db reference.
I gave a few concrete examples such as:
I can share other examples if you'd like - but it's something I've seen happen quite a few times and I'm only using DB as an example here.
I favour using modules over objects in js - because objects are inherently dangerous because state is not easily protected.
Yeah, Peer5 had that approach when I joined - that's why we have a bunch of rewire in our code :D
When code is global - then there is no clear sense of ownership enforced on your entities in your code and it's very easy to get "who is in charge of what" wrong. I realize this is a bigger concern for a complicated code base than a simple one (see my above "do whatever works for you" comment).
Even worse is if multiple references to an object are shared....
I'm not a fan of uncontrolled mutation (hopefully obviously!) - you can control mutation to regular objects (with closures, weakmaps, freeze or private class fields).
That said, while I think minimizing uncontrolled mutation is important - imagine the same scenario of a mutated object except now that global is a global :D That's what I want to avoid.
Going back to the stale reference issue - practically speaking moving to immutability causes a change in programmer behaviour
I've written (and am writing) a bunch of FP and I'm pretty fond of value semantics - immutability is not a silver bullet. I'm totally fine with a global or singleton object if it holds no mutable state :D If anything immutability just highlights how hacky code singletons or module scope create.
Also if parrelizability of tests is important (going back to a previous discussion) - you can always use a form of currying where you explicitly split "dependencies" from "initialisation of state" in the module/function version.
In functional languages (F# and Haskell) I use currying for DI all the time. It's the same pattern (of passing things in) except the state is carried in a closure scope and not in class state.
Remember, Mishko's articles were written in a time where module systems in js barely existed. DI's popularity came from Java - that has lots of problems because they didnt have functions or a good module system.
Java still doesn't have a module system (ok, they do, but it's pretty new). If anything I believe that Java gave DI a terrible name because it was used for glorified globals. Singletons came from Java too. Misko went on to write a fairly popular JavaScript framework that ships with DI by the way (not one I'm a big fan of personally) so it's not like it's uncommon :D
Do you think there might be a correlation between the project size and using DI?
Indeed that is what you argued for above, no?
Seriously asking - not saying your experience is any less valid than mine - but my experience is that the problem is usually Java and not DI as a concept.
i was refering to angular projects. My Java DI experiences are from so long ago that i don't even have any PTSD left.
I hope that's not coming across as harsh but that sounds awful.
Be harsh! It was awful. 😱. Understanding when and when not to use DI is tricky. As this discussion is proving. The reality was though that mixing serivces, with initialisation data for state, with utility functions ended up with this chaos. Constructors with 7-8 parameters all over the place + the injector configuration to keep in sync. Binding the services/utils to this because that was the only way it could be accessed in instance methods. Totally obfuscating what was the actual state that was being managed.
But then you have to update every single file using the database
yep, but with DI you need to update every injection point to the new reference. this more or less feels like the same to me. Plus module imports are (usually) easier to identify than instances of objects.
I gave a few concrete examples such as:
Sorry i missed them (long thread and all). Yep these are pretty good times to use DI. Personally i would do it only when needed because the problem set dictates it (e.g. building some reporting tool). I wouldn't do it everywhere (for the transactional services there is no point)
It's the same pattern (of passing things in) except the state is carried in a closure scope and not in class state.
but much clearer because you can separate the types of things being injected.
Misko went on to write a fairly popular JavaScript framework that ships with DI by the way (not one I'm a big fan of personally) so it's not like it's uncommon :D
Oh i used it! And it can work well, but once things got big - oh my golly was it hard to understand. It clearly attracts the Java / C# mob because they are more comfortable with these patterns of development in comparison to say React.
Hello there! 👋
This issue has gone silent. Eerily silent. ⏳
We currently close issues after 100 days of inactivity. It has been 90 days since the last update here.
If needed, you can keep it open by replying here.
Thanks for being a part of the Node.js Best Practices community! 💚
Most helpful comment
My general rule of thumb will probably be: whenever you represent an instance of something - use instances. Use static whenever your want factor/build an instance or to invoke a simple utility that unlikely to retain any state beyond the method level (e.g. logger)
Consider a 'ProductService' that has a few methods. Should we use it without instantiation, with static methods, we are at a risk of:
Limiting the possibilities for instance level data - what if we want each request to hold some contextual data (e.g. user id and roles) and pass between all classes, in a static service this won't be possible unless we pass it to any method, we can't save the context on the instance data. This a very tangible limitation
Harder to perform IOC (dependency injection) and mock for testing - most ioc/di framework perform on the instance level. Though It's doable to mock static dependencies (not set on the instance level), it's not as convenient
That said, static is just a bit faster (usually negligible in webapps) and whenever we use utility functions that are both frequently invoked (E.g. logger) and unlikely to contain instance data - static makes sense.
Also on the world of DDD, factories are usually implemented as static as it doesn't make sense to instantiate an instance for the sake of instantiating an instance...
Would love to hear @benjamingr take here...