Hello,
I’m new to the Typescript world and I’m trying to learn some techniques.
Let’s imagine that I want to implement a Logger in my application with Winston. Later, I want to use Bunyan instead of Winston. Problem: All files that use the old Logger should be updated.
Note: It seems that Winston and Bunyan use the same functions. Let’s assume that this is not the case.
Should I create an interface (ILogger), then classes for each implementation (LoggerImplWinston and LoggerImplBunyan)? Any other options for you?
At the same time, I am also trying to use the Inversion of Control (Inversify) and Hexagonal architecture (Interfaces, application, domain, infrastructures) …
Thus, I’m french, so keep things as simple as possible
Thank you in advance.
Top comments (4)
I've done more Java than TS, but I believe they're similar here: if you want to be able to choose between logging frameworks (or whatever) at runtime, then typically you'd create either an abstract base class or an interface to wrap the real logger instance.
Also, as an implementation detail, you'd probably want it to be able to wrap multiple logger instances and dispatch to all of them at once.
However, you should weigh the work of maintaining this wrapper against the likelihood that you actually would swap out your logging framework, and the fact that in all likelihood doing so will amount to little more than a find-replace job.
Separate from whether or not your wrap the logger instances, you may want to consider using aspect-oriented programming in some form -- depending on how much stuff you have to log, it may or may not make maintenance easier.
Thank you for your answer. So, if I understand correctly, I can create an interface:
I implement it like this:
Gist link : gist.github.com/AlashaHonore/d1b0d...
And create a decorator to make maintenance easier.
However, what do you mean by “[…] as an implementation detail, you’re probably going to be able to wrap multiple logger instances and dispatch to all of them at once”?
Another question: Should I do the same thing (IConfig interface + ConfigImplConvict implementation) for the configuration of the project? And use dependency injection instead of decorator?
I understand that this is not necessary in all projects. In fact, the one I am working on seems simple to me. For the logs, I could use a decorator by directly using the real instance of the logger.
Thank you again for your help.
Only that, so long as you're writing a logger class, it might be interesting to add the ability to write to multiple destinations at once, using a single instance.
Hard to answer without knowing what the project is and what your goals are. If it's a learning exercise, then I might try multiple solutions and see how each differs. But without a concrete need motivating them, I don't think you can say that one would be better.
Otherwise, it sounds like the scope might be small enough that no matter what you do you're not going to run into maintenance problems. Design patterns exist to solve specific problems (and specifically in languages not dynamic enough to allow them to be solved more simply). So more isn't necessarily better.
If the goals for this project are centered around an app with specific behavior, my advice would be to focus on the behavior and not over-think the supporting stuff. For instance, if you have a real need for logging, let that guide your decisions about how to implement it. You can always add more logging code, but if you add and later remove it, you can't get that time back.
A simple config solution might just be a class that acts as a namespace for some constants. You may find later that you want instances, so that it's easy to pick a stage at runtime ("dev", "prod", "test", etc.). Or you might prefer to have a data file that's separate from the source code, so that you can add secrets without having to put them in version control. (Or so you don't have to make the type checker happy whenever your config changes.)
For context, most of my professional experience has been with dynamic languages, and I prefer metaprogramming to patterns (where possible). But my overall experience has been that it's not hard to write a decorator, but it is hard to finish an app, so I would try not to get bogged down in places where a naive design choice will be cheap to adjust. Especially since static typing makes refactoring that much less error-prone.
Regarding dependency inversion: spotting violations is easy enough -- wherever a class depends on another class, make it depend instead on a passed-in instance instead. But I would first ask what it buys you -- it's good to depend on abstractions, but until you actually reuse a piece of code, it can be hard to know what the right abstraction will be, at least in my experience.
Thank you for the valuable advice. I will put them into practice :)