2

I have an application, which contains a logger. The logger already exists and is ready to be used, so it can be passed directly to the constructor.

class App {
    private readonly logger: Logger;

    public constructor(logger: Logger) {
        this.logger = logger;
    }

    public run() {
        // do stuff
    }
}

function main() {
    const logger = new Logger();
    const app = new App(logger);
    app.run();

    return 0;
}

main();

Now I want to add configuration, so I can modify its behavior without having to rebuild the application (e.g., send requests to a certain URL without hardcoding the URL). I don't want my constructors to throw, though. I also want to be able to reload at runtime, so thats another restriction leading to a 2-phase init.

 class App {
     private readonly logger: Logger;
+    private readonly configManager: ConfigManager;

-    public constructor(logger: Logger) {
+    public constructor(logger: Logger, configsDir: string) {
         this.logger = logger;
+        this.configManager = new ConfigManager(configsDir);
     }

+    public loadConfigs(configsDir: string) {
+        // It needs a separate initialization step, because reading files
+        // might fail and I don't want my constructors to throw
+        return this.configManager.loadConfigs();
+    }

     public run() {
         // do stuff
     }
 }

 function main() {
     const logger = new Logger();
-    const app = new App(logger);
+    const app = new App(logger, "./run/configs");
+    if (!app.loadConfigs()) return 1;
     app.run();

     return 0;
 }

 main();

I have a widget/module/part that will make use of the configuration. Unfortunately, because the configuration uses a 2-phase init, it cannot go in the constructor, and must wait for the second init. And because the init is not in the constructor, it cannot be readonly. Additionally, because Typescript cannot be certain that the variable is initialized, it must either be declared as T | undefined or asserted to be non-null with the ! operator.

 class App {
     private readonly logger: Logger;
     private readonly configManager: ConfigManager;
+    private widget!: Widget; // The exclamation mark asserts its definition

     public constructor(logger: Logger, configsDir: string) {
         this.logger = logger;
         this.configManager = new ConfigManager(configsDir);
+        // Can't init the widget here, because the config has not loaded yet
+        // this.widget = new Widget(this.configManager.getWidgetSettings());
     }

+    public loadWidget() {
+        // Because the init is not in the constructor, the variable cannot
+        // be `readonly`.
+        this.widget = new Widget(this.configManager.getWidgetSettings());
+    }

     public loadConfigs(configsDir: string) {
         // It needs a separate initialization step, because reading files
         // might fail and I don't want my constructors to throw
         return this.configManager.loadConfigs();
     }

     public run() {
         // do stuff
     }
 }

 function main() {
     const logger = new Logger();
     const app = new App(logger, "./run/configs");
     if (!app.loadConfigs()) return 1;
+    app.loadWidget();
     app.run();

     return 0;
 }

 main();

Here I've gone with the assert route, since it looks nicer than throwing a bunch of if (!this.configManager) throw new Error()s around. Then I need to pray that I remember to load everything and in the correct order.

What was originally a simple and straightforward "make the app run itself" turned into "pass everything in the constructor and run" and is now further degrading into a maze.

Is there a better design I can use to avoid all of the following:

  • throwing constructors
  • 2-phase initialization
  • repetitive definition checks
  • asserting

In any case, I will definitely catch mistakes since it crashes, but ideally I can avoid them entirely. I can't figure out a way to avoid all those at the same time, but maybe there's a less convoluted design I haven't thought of.

1
  • 2
    Use generic Result<TOk, TErr> objects, with static "constructor methods" and private true constructors.
    – freakish
    Commented Sep 23 at 4:39

3 Answers 3

4

There's a lot going on here, both in your identification of the problem and your responses to it.

I don't want my constructors to throw, though.

Constructor should generally not throw (other than argument null exceptions which are inherently applicable to the purpose of a constructor), because constructors shouldn't contain any meaningful behavior.

I'm pointing this out because it contrasts with:

Now I want to add configuration, so I can modify its behavior.

Your concern about the constructor throwing arises from you introducing this configuration, which modifies behavior. This suggests that you're building your constructor in a way that it should execute certain behaviors; which goes against the initial "no behavior in constructors" point I made earlier which is the main reason for the "don't throw in a constructor" guideline to begin with.

There's a contradiction here with you trying to introduce behavior in a constructor while at the same time trying to adhere to a guideline that bases itself on the instruction to not do so. One of these is going to have to give.

public constructor(logger: Logger, configsDir: string) {
    this.logger = logger;
    this.configManager = new ConfigManager(configsDir);
}

You implemented DI correctly with the logger by having the consumer inject it as a dependency. You then regressed on that by not injecting the ConfigManager.

const logger = new Logger();
const app = new App(logger);

if (!app.loadConfigs()) 
    return 1;

app.run();

It is inherently pointless to avoid having throwing behavior in a constructor, if your solution to that problem is then that you put it in a second method and then mandate that this second method should be run in the same breath as the constructor.

This smells like semantical redefinition just to be able to adhere to the letter of the rule (it's not in the constructor, is it?) while blatantly ignoring the spirit of the rule (no real behavior during the initialization stage, which generally happens in the constructor).

Other than missing the point of the rule you're trying to adhere to; the solution you're implementing in pursuit of it violates a bunch of other guidelines, chief among which is "tell, don't ask".

Your refactoring is also incomplete, as it appears you're not actually setting configsDir even though you've added it to the constructor signature:

enter image description here

It is unclear to me what language you're using and whether this would result in a compiler error, unexpected behavior, or if the language uses a default value for an unmentioned parameter.

It's also unclear to me if not seeing it work (since you're not setting it) is the source of some failures that you've been seeing and trying to subsequently fix with e.g. a more elaborate solution (I can't confirm it but it does feel this way, looking at your code).


I come back to my original point of you regressing on the dependency injection; because if you hadn't regressed on this, it appears that your main concern here would have been a non-issue.

const logger = new Logger();

const configManager = new ConfigManager("configsDirValue");
configManager.loadConfigs();

const app = new App(logger, configManager);

app.run();

This follows the DI pattern that you had already been running with and completely bypasses the issue with constructors throwing exceptions by initializing the configManager before passing it down the dependency graph.

7
  • I think I originally started out with the idea of "I want the app to run itself, so the main function is empty", but then I ran into the throwing constructors problem, and moved things out into the main function, and thus the DI. I guess I'll have to think more about that then. Commented Sep 23 at 6:58
  • The configuration I mentioned was not to configure the constructor, it's for other stuff the application does. I've made it clearer in the question. Commented Sep 23 at 6:58
  • As for the bad constructor call, thanks to Javascript and the fact that I was using stubs when I wrote this question, missing arguments don't actually error out at runtime. Also, it was mostly written as an illustration that closely resembles my application, rather than the application itself. I've fixed that in the question too. Commented Sep 23 at 7:00
  • @404NameNotFound: in your real application, is the config dir really hardcoded, or can it be configured somehow? And if yes, may its value change during the application's lifetime?
    – Doc Brown
    Commented Sep 23 at 13:41
  • @DocBrown It can change depending on flags passed to it, e.g., node myapp.js --self-contained. Currently there's the option of using "standard locations" (e.g., ~/.config, %LOCALAPPDATA%, etc.) or "self-contained" (relative to the project dir), but I could add support for arbitrary locations (e.g., node myapp.js --config-dir $dir. I don't expect it to change during runtime, but there's nothing preventing it from changing I guess, though it probably shouldn't. Commented Sep 23 at 16:39
2

I don't want my constructors to throw

This is an understandable but doomed impulse. Just remember that no matter how you design constructors they can still throw out-of-memory exceptions. More on this later.

I also want to be able to reload at runtime, so thats another restriction leading to a 2-phase init.

Oh come on. That doesn't require a 2-phase init. That just requires a configuration load at startup that can be redone at any time.

@Flater has done a very good job laying out the basic Dependecy Injection pattern that solves this. It closely follows a pattern I use with individual classes. I avoid 2-phase init with every object. If an object exists it should be ready to use. My constructors set state and validate. That's it. No sneaky creating state. If validation succeeds then the object exists and is ready to use. Objects that work this way are so much easier to deal with. This is the discipline I use with constructors.

It is entirely possible to stick to that while dealing with your reload requirement. By following DI the IO error would happen before the constructor call. But you want to reload configuration at any time. Let's deal with that.

By following DI you actually have more options now.

If you're good restarting from scratch you can just construct a whole new app with the new configuration. If you need settings / state from the old app you can either DTO them to the new one or simply give the app a reloadConfig() method that reads the config file again. One that, yes, can throw IO exceptions.

Done this way the object is always in a useable state so you don't have to remember to call anything in any particular order. Makes it so much easier. It takes a bit of work setting this up but I'm too lazy not to do it.

What was originally a simple and straightforward "make the app run itself" turned into "pass everything in the constructor and run" and is now further degrading into a maze.

The way to solve that is with a name.

You want app to be able to run itself. But you've already created two different constructors. One of which takes an argument. So apparently there is more than one way for app to run itself.

This is a common situation. It'd be easy if everything could just build itself. But there is more than one way to build them. DI says just pass in the dependencies. But apply that dogmatically and it leads to a mess.

Instead, give these ways of building your app (or any object) names. This gives the choices of what to pass a coherent place to live. The name should make those choices unsurprising.

Since we're making sure you don't need to know the state of the app to use it you don't care how it was built when you use it.

This is where you create state. The arguments that need to be passed in to the constructor can come from a static builder, a factory method, really any construction pattern. Break each construction problem down into smaller chunks. Not everything has to be done flatly in main. We can use code for this. Just give each way a good name.

Or you can give up on that and use a DI container. They work too. Kinda.

1

You are looking at this from a coding / implementation point of view. That's the wrong view. Ask yourself: What should your application as a whole do if this dependency fails? Can it run successfully? Can it run in some restricted mode? Is there something you need to tell the user? Can you fix it? Can the user fix it, possibly by trying again in an hour, or by calling their IT guy? How do you minimise the damage caused by this?

Note that an assert at best writes something to a log file, or it does nothing, and then it either stops your application from running, or you pray that not too much goes wrong if you you continue running. Asserts should only be used for things that absolutely cannot happen, or for finding incorrect code during development. And if you have two hundred lines like "if (condition) throw ..." then you write a function func throwif (bool condition...) so your error handling doesn't interfere as much with your code, and you are a lot more flexible.

Your Answer

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

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