1

When I have a class A in a module amod as follows

// module amod

type AConfig = {
  x: number;
  // ...
};

class A {
  constructor(public config: AConfig) {}
  // ...
}

export {A, type AConfig}

and want to decare a class B in a separate module bmod that makes use of A, what would be the better approach?

Approach 1:

// module bmod

import {A, type AConfig} from 'amod';

class B {
  protected _a: A;
  constructor(aConfig: AConfig) {
    this._a = new A(aConfig);
  }
}

Approach 2:

// module bmod

import {type A} from 'amod';

class B {
  protected _a: A;
  constructor(a: A) {
    this._a = a;
  }
}

So far, I always went with approach 2 because it seemed to me as if it only had advantages. For example, I can give the same instance a to multiple instances of B, or use decorators.

However, I now understood that there is also at least one big disadvantage of approach 2: Any module cmod that wants to make use of B must not only depend on bmod but also on amod (to create the instance of A passed to the B constructor), which can easily introduce subtle bugs due to different versions of amod between bmod and cmod and thus requires careful version management. That would not be necessary for approach 1.

Is there a common design for this problem, or at least a best practice?

5
  • This seems like a language-specific problem. In other languages you can only have one version of amod
    – user253751
    Nov 8, 2022 at 16:32
  • I have presented the problem here in TypeScript with Node.js (i.e., JavaScript) in mind, but it should be relevant for all systems that use isolated dependencies.
    – Remirror
    Nov 8, 2022 at 17:40
  • Not a full answer, but #1 has the drawback of seeming to communicate that it does something "extra" with the given aConfig besides just creating an A, because otherwise why wouldn't it just accept the A?
    – Quelklef
    Nov 8, 2022 at 20:04
  • If you had to re-write this question in a single statement. What would be the question?
    – Laiv
    Nov 9, 2022 at 15:26
  • Well...its title? :-)
    – Remirror
    Nov 10, 2022 at 9:41

1 Answer 1

4

You are right to be considerate of what dependencies your classes expose.

However, I think you have a similar issue in both cases: In both your approaches, users of B are forced to transitively depend on amod when it seems like you might not want them to.

Instead, consider a trick I often find myself using for design questions like these:
Think of the class you are designing (B) on its own terms, not in terms of other classes it might use. What's actually important to this class on its own? What does it really need? Questions like these can lead you to design with lower coupling and higher cohesion, and to just make more clean and useful classes in general.

Applying those questions to this situation you could get the following answers:

  1. "You really need an implementation of A interface in order to create B, where you could use any A implementation. Or you want to compose B through dependency injection of A interface."
  2. "You should only need XYZ knowledge to create B, it can deal with its own internal implementation."

.. there may, of course, be more nuanced answers but these are good enough to give you the idea.

If your answer is similar to 1 then passing A to B's constructor is fine, though you could abstract it to make B depend on a A interface instead of concrete A if you want.

If your answer is similar to 2 then just pass the knowledge that B needs to its constructor. Then B is free to use whatever hidden implementation it wants without the user needing knowledge of it.

That knowledge you pass to B could be AConfig (which again may be an interface) but you need to ask yourself if that's really what B needs. Rather you could just pass BConfig data to B and then internally configure A (or whatever) from that data without the user needing knowledge of amod at all.

Hope this gives you a fresh point of view for designing these classes.

2
  • It is 1. in my case. Still, I'm reluctant to let cmod depend on bmod and amod exactly because the transitive dependency (cmod->bmod->amod) you mention may be to a different version than the additional direct dependency (cmod->amod) then.
    – Remirror
    Nov 8, 2022 at 20:49
  • @Remirror If the user truly must provide an A to create a B then there's not really a way around cmod depending on amod and it should be a reasonable dependency. Eg. user_mod -> calendar_mod -> date_mod is completely reasonable. But the fact that you considered providing just AConfig made me suspect you could hide the A dependency inside B and cmod doesn't really need to know about it.
    – xtratic
    Nov 9, 2022 at 14:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.