Comments (10)
Hi @hbarcelos. Thanks for the feedback. Your last paragraph really hits the nail on the head. The point of the video and the accompanying code is to teach people the "rules" and like anything, once you master the "rules" you start finding reasonable places to break them.
There's a reasonable argument to be made for just importing most, if not all, the dependencies of isQuestionable
. Ultimately, the Dependency Inversion Principle (and all of the Clean Architecture model and SOLID principles) are about managing risk. In the context of software development when we talk about risk, we're talking about the likelihood and impact of future change. In the last few years, I've developed a mental model of risk that looks like this:
likelihood of change x impact of change = degree of risk
The higher the risk, the more likely I am to take steps to mitigate that risk. In the specific case you mentioned, the risk is relatively low so the dependency injection is probably overkill.
The other thing to keep in mind is that there is a direct correlation between the size of your codebase (and team) and the impact of change. As your app and team grow the impact of changes increases, so you need to re-calculate the risk and refactor accordingly.
With respect to libraries like axios-mock-adapter
I tend to be extremely wary of such dependencies because they are maintained independently of axios
and yet tightly coupled to axios
which tends to be a toxic combination.
I try to avoid mocks as much as possible. When unit testing http requests I tend to decouple the request itself from the code that builds the request and the code that processes the response, that way I can unit test those independently and then write an e2e test that does the real http request. This eliminates the need for any mocking. See buildModerationApiCommand
and normalizeModerationApiResponse
for example. These are pure functions that are easy and fast to test.
from comments-api.
Thanks for writing such a thoughtful response @hbarcelos . I really enjoy these kinds of conversations.
In terms of how to decide when to define our own interfaces, I look at it this way. We can divide the features or capabilities of our app into two categories: commodity features and differentiating features.
Commodity features are those features which are common to most, if not all, applications (e.g. logging, error handling, network communication, database communication, routing...).
Differentiating features are those features which are unique to our application. Most of the time, the differentiating features are directly related to the value proposition of our application and in commercial software, the differentiating features are what customers are paying for.
When it comes to commodity features, there is little value in trying to define our own Interface. Ideally, we would adopt an open standard and prefer libraries that conform to that standard, of course the problem with standards is there's almost never just one:
So, we usually end up picking a popular library (e.g. axios
) and just adopting its interface knowing that, if we need to swap it out in the future, we can write an adapter that maps the new Interface to the Interface our module is expecting. Of course, this is easier to do if we used DI from the start.
For differentiating features, it's another story. I would argue there is always value in defining our own Interfaces here. That's why I'm generally quite resistant to the idea of using a third party Framework to build my Entities or Use Cases. For example, I would never use a Mongoose Model as a Business Entity, nor would I use Mongoose to enforce business rules. If I used something like Mongoose, the Models it generates would only be used to interact with the database and would be kept far, far away from my Business Logic.
from comments-api.
Reopening so that itβs easier to discover this conversation.
from comments-api.
likelihood of change x impact of change = degree of risk
I liked this! I think it's something I already do, but not deliberately. That's a good mental model to have, so thanks for that π.
About axios-mock-adapter
, I must say I agree with you. I myself tend to follow the same approach most of the time. Usually I only mock when it's inevitable (e.g.: when calling a paid or limited-usage service). However, I do that in a lower level, using nock
, just to avoid the kind of hassle you've mentioned.
I only brought that up because it's usually an argument people make:
Oh, if you don't inject dependencies, you cannot/it's too difficult to test!
While this might be true for other less dynamic languages, JavaScript gives you a lot of freedom.
The other thing to keep in mind is that there is a direct correlation between the size of your codebase (and team) and the impact of change. As your app and team grow the impact of changes increases, so you need to re-calculate the risk and refactor accordingly.
I agree. However this is what makes it difficult to know exactly when you need to define your "interfaces" in the inner circles and implement them in the adjacent ones.
For example, I believe it would be completely unnecessary having to create an interface for an HTTP Client in our controllers and then write adapters for axios
, request
, got
and so on. Usually it's better just to choose one of them and stick to it.
However, problems will arise if the chosen module somehow has a severe security breach that can't/won't be fixed anytime soon. Then you're going to have a bad time trying adapt everything to the replacement module's API.
For example, your normalizeModerationApiResponse
is coupled to axios
API, because it assumes the response has a data
property:
export function normalizeModerationApiResponse (response) {
return (
!response.data.Classification ||
response.data.Classification.ReviewRecommended
)
}
If we switch to request-promise
or got
, the property will change to body
. So we'd have to remember changing this function as well to make that work. Imagine now the code-base is huge and there are many normalizeModerationApiResponse
-like functions. Chances of forgetting to update one of those are pretty high (hence the need for a good E2E test suite).
When it comes to that, probably what we'd have to do in such case is write an adapter from axios
API to request-promise
or got
. The problem now is that we end-up with the same problem axios-mock-adapter
has,
[...] because they are maintained independently of axios and yet tightly coupled to axios which tends to be a toxic combination.
There is another problem regarding request-promise
. Its default behavior is to return only the response body for successful requests, so we will have to remember to properly configure it with the resolveWithFullResponse
option.
Damn! These things are really hard π .
from comments-api.
@arcdev1 thank you very much for taking the time to discuss this matter with me. Really appreciate it.
So, we usually end up picking a popular library (e.g. axios) and just adopting its interface knowing that, if we need to swap it out in the future, we can write an adapter that maps the new Interface to the Interface our module is expecting. Of course, this is easier to do if we used DI from the start.
I tend to use a slightly different approach, which I call the symlink.
Whenever I need something like an HTTP client, I define a file in a common folder (e.g.: src/shared
) and do as follows:
// src/shared/http-client.js
module.exports = require('axios');
// or
export { default } from 'axios';
Now all files depending on an HTTP client will require this module instead of requiring axios
directly.
If I ever need to change axios
into something else, I can change the dependency here and export the adapter. I don't even need to touch another module.
This way I don't need to bother with injecting dependencies.
That's why I'm generally quite resistant to the idea of using a third party Framework to build my Entities or Use Cases. For example, I would never use a Mongoose Model as a Business Entity, nor would I use Mongoose to enforce business rules.
π π π Amen to that!
There has been more occasions than I would like to count where I tried to explain that to colleagues only to hear back "Oh, ok! But doing this with mongoose is easier" π€¦ββοΈ
If I used something like Mongoose, the Models it generates would only be used to interact with the database and would be kept far, far away from my Business Logic.
Another problem is that if you ever decide that is better to ditch Mongo, you just can't. You are stuck with it forever, unless you do a full rewrite of your application (which will probably go south at some point).
That's why I believe 90% of what people call vendor lock-in are just poor architectural decisions :v.
Anyway, to not extend this discussion here any further, I'll close this issue.
Do you happen to know any place we could have these kind of high level discussions? Slack? Gitter? Anything?
from comments-api.
Do you happen to know any place we could have these kind of high level discussions? Slack? Gitter? Anything?
If you've subscribed to my newsletter (devmastery.com) you'll see an invite to our Discord Server in my regular email blasts and I'm happy to have conversations exactly like this on there.
You'll also find a couple hundred devs there with different backgrounds and experience so it would be a great place to start this kind of chat.
from comments-api.
Niice... that's sound very interesting.
I just subscribed to your newsletter.
There is a little bug in it though:
After clicking the confirmation email link, I'm redirected to https://devmastery.com/signup/thank-you.html. This gives me a 404 error.
from comments-api.
Thanks for the heads up on the bug. Will fix.
from comments-api.
However, problems will arise if the chosen module somehow has a severe security breach that can't/won't be fixed anytime soon. Then you're going to have a bad time trying adapt everything to the replacement module's API.
axios/axios#2131 *cough
Now all files depending on an HTTP client will require this module instead of requiring
axios
directly.
If I ever need to changeaxios
into something else, I can change the dependency here and export the adapter. I don't even need to touch another module.This way I don't need to bother with injecting dependencies.
Oh, I saw this approach somewhere β¦ can't find it right now β¦ the idea was to have a single place where all dependencies are exported as properties of an Object. This way, one's own code does not have to import them everywhere / updates can happen in a single place.
from comments-api.
the idea was to have a single place where all dependencies are exported as properties of an Object.
I think this approach takes things too far. Problem here is that you'll have non-related dependencies all scattered trough a single file that might become huge.
Just imagine yourself and your team updating this big chunk of a file all at the same time. Merge conflicts will drive you crazy. The bureaucracy of adding a new dependency will quickly escalate.
I believe this strategy should be used with caution, in a more fine-grained fashion.
from comments-api.
Related Issues (20)
- Classes and encapsulation HOT 1
- Design patterns HOT 1
- authentication and authorization HOT 2
- race condition HOT 1
- How should I include Transactions, without breaking this source code architecture? HOT 3
- can one use case be dependent on another usecase. HOT 2
- Content Moderator Timeout Issue HOT 1
- Concurrent database writes HOT 5
- Typo in src/index.js: app.put instead of app.patch HOT 2
- How to support api error codes and subcodes HOT 1
- What's the point HOT 2
- Implment Abstracts for model and collection HOT 1
- Validation in entity HOT 3
- Classes vs. factory functions? HOT 3
- How should the add-comment use-case be if using classes in js? HOT 2
- VS Code debugging HOT 2
- .env variable undefined at the time of running data-access module HOT 1
- sampledotenv needs "DM_API_ROOT" HOT 3
- Why you use IIFE in db access file ? HOT 1
- Is Id considered to be in the framework layer?
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
π Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. πππ
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google β€οΈ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from comments-api.