7

Suppose I have a constructor that performs an expensive IO operation that takes a noticeable amount of time. I don't like it for a few reasons (first of all, it's simply wrong, but there are practical considerations too)

What should I do?

I see two options

  1. Extract the expensive operation into a separate method. That method may return some async wrapper (CompletableFuture, Mono). The clients that call the constructor should be updated to include the new method call after constructor calls (in case they relied on a fully initialized state)
MyClass myClass = MyClass();
CompletableFuture<MyClass /* or some specific component that is expensively initialized*/> = myClass.initAsync();
  1. Remove the constructor from the public API altogether, replace it with a factory method returning an async wrapper. I would have to identify all the places the "expensive" state is used and move that logic to the callbacks passed to the async wrapper. It feels more right
MyClass.createAsync().thenAccept(/* consumer code */)`)

What do you think?

4
  • There's also a small issue with Swing being not thread-safe (it's a Swing application). I guess that means the callback should have another callback inside of it (i.e. SwingUtilities.invokeLater(..)). But I guess it's a different story Commented Sep 2 at 10:48
  • 4
    This is what the builder pattern is for
    – slebetman
    Commented Sep 3 at 2:20
  • There's always the minimal option: keep the existing code, but clearly document the fact (in the constructor's JavaDoc, and also in any relevant project documentation).  Probably not the best option, but at least it avoids surprise, and allows callers to take it into account.
    – gidds
    Commented Sep 3 at 23:24
  • @gidds I'm not sure most people are in the habit of reading a constructor's doc before calling it Commented Sep 4 at 3:54

5 Answers 5

24
  1. Don't put i/o in constructor. It leads to so much trouble, e.g. how will you test that? There are some valid cases for such thing in other languages (e.g. RAII in C++ and Rust) but I don't think it applies to Java (which I don't know that well), just like it doesn't apply to C# (which I do know well).
  2. Don't make "init" methods. That's even worse. It leads to situation where you actually can have invalid objects, if you forget to init them. Ultimately this will lead to trouble, as someone will use such class without init at some point. Murphy's law, man. Don't trust in developers' discipline if programing language alone can solve your problem.
  3. Use builders/factories that return valid objects. These can do any i/o and async stuff under the hood, anything you want. Ultimately it is just a way of bypassing constructor limitations. But it has some other advantages (e.g. caching).
  4. As an alternative pass i/o to constructor as lazy dependency. Which does i/o only when you use the object, not during construction. I recommend builders/factories though. Because their behavior is easier to predict and debug.
8
  • Thank you. Does it mean you endorse the second option? Commented Sep 2 at 11:28
  • @SergeyZolotarev yes, except you don't have to remove constructor from the public API. Depends on concrete case.
    – freakish
    Commented Sep 2 at 11:29
  • 6
    Option 4 works best if you can delay the I/O operations. If performing I/O is necessary just to have a valid object, then some sort of factory or builder does the trick just fine. Commented Sep 2 at 15:35
  • Where is the difference in testing the constructor versus testing the factory method? Commented Sep 2 at 21:52
  • 6
    @PaŭloEbermann factory can return various objects. It can return Result objects, it can return errors, it can return async wrappers, etc. Constructors are limited: either the object or exception (which btw should not be used for error flow IMO). In particular constructor cannot return async wrappers, which typically indicate i/o. It's not about testing, but rather what you can do with it.
    – freakish
    Commented Sep 3 at 6:21
20

This is very similar to an ancient StackOverflow question. I suggest reading my answer there, as it hasn't changed.

Arguing that the constructor must not be slow is a mindset that comes from real-time systems (e.g. those with GUIs that shouldn't block), but it is not a hard-and-fast rule.

At a minimum, your constructor needs to get the object configured to the point that its invariants are true.

Be careful of premature optimization. Be careful of blanket application of good rules one domain into all domains.

Overall, it depends.

11
  • 8
    Nice, no one says why IO and compute is bad in constructor, so other answers read cargo-cultish. Have a +1.
    – Basilevs
    Commented Sep 3 at 7:30
  • @Basilevs In this case, because asynchronous IO is preferred and that can't be done in a constructor. But I agree with the general sentiment of this answer, +1!
    – Bergi
    Commented Sep 3 at 9:06
  • 1
    @Bergi, are you sure, that still holds with VirtualThreads finalized in Java 21?
    – Basilevs
    Commented Sep 4 at 6:37
  • 1
    @jeancallisti IO does not defeat anything - either is succeeds and object is initialized, or it is not and Java object is not created. If object is created, it is created deterministically. If it fails to construct, determinism is irrelevant.
    – Basilevs
    Commented Sep 4 at 9:47
  • 1
    I think you are all overcomplicating it. If the boss says "we need a cross-platform command-line equivalent to wc that reads just one file, by tomorrow", my solution will NOT use Async IO nor any types of thread, and will read the file immediately in the primary class's constructor, and, yes, will be unit-tested despite that. Commented Sep 5 at 1:14
3

This is a scenario where dependency injection is a good idea. Instead of doing the IO in the constructor, do the IO separately and then provide the handle (File handle, Buffer handle, whatever...) of that IO job to your object's constructor.


Advantages:

  1. You are not anymore doing a heavy task in the constructor. This results in quick object construction;
  2. Unit testing the IO job will be easy since it can be moved to another class whose object you will be passing to your constructor;
  3. Unit testing your constructor code will be easy because you won't have to wait for the constructor to run IO, and you can provide a mock object to the constructor for testing; and,
  4. Most importantly, any possible IO or network operation failure can now be gracefully handled instead of having an object left in a garbage state.
2
2

Consume the completed operation

Oddthinking's diagnosis is correct:

At a minimum, your constructor needs to get the object configured to the point that its invariants are true.

If it is impossible to create a valid object (with true invariants) from the constructor's parameters without performing expensive I/O, then I'd say that the constructor's parameters are wrong.

Modify the constructor so that it takes the completed form of the expensive operation, and then provide an async helper method (or factory) which both does the I/O and constructs the object, to replace existing calls to the constructor.

This has the following advantages:

  • It doesn't require you to hide any constructors - the constructor is a real legitimate constructor which produces a valid object if called.
  • It decouples the I/O from the business logic. If you later add another way of getting the data for the object, you don't have to change the code of the object itself.
  • If you need to construct multiple copies of the object from the same source data, you'll only need to read the source data once.
  • It's unit-testable (if the data object itself can be constructed in your tests)
7
  • Thank you. But then all that async stuff may bubble up to main(). You have an expensively created component at the bottom of composition hierarchy which is then passed up to create another object and so on. Then, you realize you never deal with "real" objects, only with Monos or CompletableFutures. Does this scenario scare you? Commented Sep 3 at 17:30
  • 1
    @SergeyZolotarev - I'm not sure I understand the concern. If the object's constructor takes the result of the I/O operation, then you must await its completion before calling the constructor. We are dealing with real objects and not futures. The future object is a temporary state that allows us to control when we do the I/O, and it doesn't need to be kept around longer than necessary.
    – Tim C
    Commented Sep 3 at 17:55
  • As for whether the scenario "scares" me - I use async operations every day in my work (in C# though, not Java). Futures do not scare me, because I know how and when to resolve them to real objects. If you'd rather not deal with the async, the helper method which does the I/O and then returns the result can be synchronous if you like, and then its performance characteristics will be identical to the constructor.
    – Tim C
    Commented Sep 3 at 18:00
  • If Object A needs Object B, Object B needs Object C (for initialization), and Object C requires an IO operation, then its factory method is going to return an async wrapper. If you don't block (generally, you shouldn't), it now means initialization of Object B is now expensive too, as well as initialization of Object A. Their factory methods now return async wrappers of their respective objects. So a single IO operation propagates drastic API changes all the way up. And since it's pretty likely that a composition tree would have at least one IO operation, everything is now async wrappers Commented Sep 4 at 3:00
  • 1
    @SergeyZolotarev - "If you don't block (generally, you shouldn't)." I think this is where we disagree. Right now, the code is blocking, because it does the I/O synchronously during the constructor. If the code right now is fine, then blocking is fine. Creating an async factory makes it possible to change instances of that construction to non-blocking on a case-by-case basis. It does not require you to make that change everywhere all at once. async is a tool, not a lifestyle.
    – Tim C
    Commented Sep 4 at 16:36
-1

The main focal point here is that constructors are not the right place for any kind of expensive operation, let alone async IO calls.

  1. Extract the expensive operation into a separate method

The main point of concern here is that what you're really doing is breaking the constructor in two parts. That introduces as many problems as it appears to solve, because now you have a second method that everyone should remember to always run right after the constructor. If they don't, then your object will not be in the initialized state that it should be post-initialization.

  1. Remove the constructor from the public API altogether, replace it with a factory method returning an async wrapper.

MyClass.createAsync().thenAccept(/* consumer code */)

I agree with the overarching goal here. However, you seem to be taking the static route by calling MyClass.createAsync().

Instead, I would opt for an instanced factory instead of a static class method. In other words, create a MyClassFactory which has a CreateMyClass method (names are obviously open for improvement based on context that I don't have in this question), which performs the async work and then calls the MyClass constructor with the already received results from those asynchronous operations.

This achieves the best of both worlds:

  • Your constructor remain to be inexpensive and synchronous, focused only on initializing the instance and nothing more.
  • Your expensive logic can still be encapsulated in an appropriate class (i.e. whose responsibility it is to perform this task)
  • The factory is instanced and therefore can be both injected as a dependency and mocked as needed.
5
  • Thank you. I'm not sure injecting factories is good practice, though Commented Sep 3 at 5:47
  • @SergeyZolotarev: Why would factories not be an injectable dependency? (as an exception to the more general guideline of dependency inversion)
    – Flater
    Commented Sep 3 at 7:01
  • 2
    A static MyClass::createAsync could be injected as well, should the need arise - why do you need a separate class when a method suffices?
    – Bergi
    Commented Sep 3 at 9:05
  • @Bergi I assume the dependency would be a type equivalent to what Func<T> in C# is (i.e. a lambda/delegate). Yeah, that would work on a technical level, but it does mean you lose out on stronger typing. I see no reason to say it's impossible to do it that way and it'll probably be all right for trivial factory logic, but factory patterns are well established concepts and I wouldn't consider having a FooFactory alongside a Foo as an unnecessarily complex implementation. Generally speaking when you get to the point of needing a factory, the construction becomes a responsibility on its own.
    – Flater
    Commented Sep 3 at 22:37
  • @Bergi: To put it differently, the opinion on whether this factory method belongs in the same class or another ones hinges on whether you consider this to be a separate responsibility or not. This can be contextual. In OP's case, it seems like there is an asynchronous orchestration happening before the instance can properly be initialized - that starts sounding like it's independent enough to warrant a separate factory. But that's an inference based on OP's question (no code snippet to judge), so reasonable people can disagree here.
    – Flater
    Commented Sep 3 at 22:39

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