DEV Community

loading...
Cover image for Import considered harmful

Import considered harmful

kleeut profile image Klee Thomas ・2 min read

The import (and require) statement is incredibly powerful. Being able to separate out code into multiple files and packages has allowed the TypeScript (and JavaScript) ecosystem to flourish. But... It's a two edged sword. Our code tends to over use it (I certainly was and I expect you are as well). This over use is leading to lower code quality and harder to maintain code bases.

I have come to believe that use of the import keyword to bring in concrete classes and function should be considered harmful.

Using the import statement to access classes and functions from another file couples these files together tightly. Coupling code together is always a risk, code that is coupled together has to change together. Decoupling code into components that have one reason to change is a key element ensuring that your code adheres to the Single Responsibility Principle (code should have one reason to change) and the Open Closed Principle (Classes/Functions should be open for extension but closed for modification). Use of import to bring code from one file into another file breaks both of these principles.

Seemingly contradictory is that keeping code in a single file and failing to split it out into individual files also violates the Single Responsibility and Open Closed Principles. Which means that the import keyword is required and important. What is key is how it should be used. It should be used sparingly and intentionally.

My preference is to use it at a high level near to where the application is initialised or a request is handled. From this point the imported code can be used to instantiate classes, and curry functions. These can than be composed together, passing in functionality that may have otherwise been accessed using this import keyword. This composition creates testable, extendable, components. Adjusting the functionality of these components is now a matter of changing what is passed into the component rather than modifying the code of the component.

TypeScript has made composition easier from a developer experience point of view than it was using standard JavaScript. TypeScript has given us the ability to know and enforce the structure of the code that is passed into a function by having that function depend explicitly on an interface or type definition. This makes it easier to open functions for extension by decomposing code into smaller child functions and extracting them into their own files.

Using the import keyword to bring in classes, functions and objects will bind your code together in a way that will lead to it being harder to maintain. To ensure that code is more maintainable restrict the use of import for classes, objects, and functions to one place. Using import to depend on types and interfaces throughout the code is encouraged.

Discussion (2)

pic
Editor guide
Collapse
bsheldrick profile image
Brad Sheldrick • Edited

I'm confused why you think import breaks the Single Responsibility Principle and Open/Closed Principle.

If I have a class that does one thing and it requires imported modules to do this (composition), how has the import broken SRP?

If I import a class and then inherit from that class (extending), how has the import broken OCP?

What you're advocating for is the Dependency Inversion Principle (and I encourage it as well) but the concrete implementation will likely be achieved by the use of imports.

Imports is literally how dependency declaration works in JS/TS. To suggest this is harmful is akin to saying module dependency is harmful in any language.

You're going to have dependencies...this is unavoidable. How you manage dependencies is the real challenge. Internal dependencies comes down to how you organise your project code files. Third party package dependencies creates a long lived binding where the need should be carefully considered.

Collapse
kleeut profile image
Klee Thomas Author

Thanks for the reply. I appreciate that you took the time to read my post and respond.

What I'm trying to argue for is a form of dependency injection pattern, to my understanding this is different to the Dependency Inversion Principle from the SOLID principles. Both are important to me.

This dependency injection pattern sees dependency resolution done in one place so that the rest of the application is able to act on dependencies passed in rather than doing it's dependency resolution directly.

This helps to implement the Open Closed Principle because the files that are using the dependencies can rely on interfaces rather than concretes. The way I have seen this used is to pass in different implementations that vastly change the behaviour depending on run time data. In particular using local file system in place of S3 when running locally, or calling out to a different data store when building a multi-tenant application. Because the dependency was passed in the logic doesn't need to be aware of the underlying mechanism for working with data. These files are then open for extension, by passing in a different implementation based on run time information, but closed for modification, in that they don't need to be changed or copied to change the applications functionality.

As for the Single Responsibility Principle, if you take it as a class/file/function should have one reason to change not just should do one thing then I think the above example is reasonable as well.

I love talking about this stuff so if you'd like to discuss this further I'm happy to. I'm still learning so there is always a good chance I'll find out that my opinions aren't right.

Thanks again for the comment.