Dependency Injection, The Best Pattern

CodeAesthetic
4 Aug 202313:15

Summary

TLDRThis script discusses the concept of dependency injection, a software design pattern that simplifies the management of dependencies between components. It illustrates how dependency injection can lead to more maintainable and testable code by decoupling components and allowing for easy swapping of implementations. The video uses a real-world example of an attachment service to demonstrate how dependency injection can handle multiple storage locations, virus scanning, image scaling, preview generation, and encryption, making the system highly configurable and easier to test.

Takeaways

  • 🎯 Dependency injection (DI) is a fundamental concept in software design that allows for more flexibility and testability.
  • 🔍 DI simplifies the process of passing dependencies between classes, making the code more modular and easier to manage.
  • 📦 By using DI, you can easily swap out implementations of a service without changing the code that depends on it.
  • 🔄 The script demonstrates how DI can be used to handle multiple storage locations for attachments in a business application.
  • 🔧 The example shows how DI can simplify complex configurations, such as handling different file upload scenarios.
  • 🔐 DI enables the use of mock or fake implementations for testing, allowing for isolated and controlled testing of individual components.
  • 🔄 The script illustrates the use of interfaces and factories to create a flexible and configurable architecture for handling file uploads.
  • 🔒 For security, the script mentions the importance of encrypting files before uploading them to a cloud service like Amazon S3.
  • 🔄 The concept of dependency injection is not just for production code; it also greatly aids in the testing process by allowing for controlled environments.
  • 🔧 The script encourages the use of dependency injection to promote clean, maintainable, and testable code.
  • 🔗 The video script is a practical guide to implementing and understanding the benefits of dependency injection in a real-world application.

Q & A

  • What is dependency injection and why is it considered powerful?

    -Dependency injection is a design pattern where an object receives its dependencies from an external source rather than creating or asking for them. It's powerful because it allows for greater flexibility, easier testing, and better separation of concerns.

  • How does dependency injection simplify the process of handling multiple storage locations in the attachment service example?

    -By using dependency injection, the attachment service can create and inject the appropriate storage implementation based on the user's company without having to modify the upload code. This makes the code cleaner, more maintainable, and easier to understand.

  • What are the benefits of using an interface for attachment storage in the script's example?

    -Using an interface for attachment storage allows the system to be more modular and configurable. It enables the creation of different implementations for various storage solutions without changing the core upload functionality.

  • How does dependency injection facilitate easier testing of the system?

    -Dependency injection makes it possible to inject mock or fake implementations of dependencies during testing. This allows for isolated testing of individual components without the need to interact with real external systems, simplifying the testing process.

  • What is the role of a factory in the dependency injection architecture described in the script?

    -A factory in the described architecture is used to create and provide the appropriate instances of dependencies based on certain conditions or inputs. For example, it decides which preview generator to create based on the file type being uploaded.

  • Why is it important to separate concerns in software design, as illustrated by the script?

    -Separating concerns is crucial for maintainability, scalability, and testability. It allows developers to focus on specific parts of the system, making it easier to understand, modify, and extend the codebase.

  • How does the script's example demonstrate the use of dependency injection for different stages of the upload process?

    -The example shows how each stage of the upload process, such as virus scanning, image scaling, preview generation, and encryption, can be handled by separate components that are injected into the request handler. This allows for a clean and configurable architecture.

  • What is the advantage of using a shared interface for different security scanners in the script's example?

    -A shared interface for security scanners allows the system to switch between different scanners without modifying the code that uses them. This makes it easier to update or change the scanner implementation as needed.

  • How does the script's example handle the need for different encryption keys per user?

    -The example injects a KeyService into the AesEncryption component, which provides the appropriate user-specific key for encryption. This ensures that each user's data is encrypted with a unique key.

  • What is the purpose of the 'Aesthetic' points mentioned at the end of the script?

    -The 'Aesthetic' points are likely a gamification element to encourage viewers to engage with the content, such as by reconfiguring the attachment service and sharing their results on the creator's site.

Outlines

00:00

🔧 Dependency Injection in Practice

This paragraph discusses the concept of dependency injection, a software design pattern that allows for more modular and flexible code. It explains how dependency injection simplifies the process of integrating different services, such as an attachment service in a business app, by decoupling the code that uses a service from the service itself. The example given involves handling multiple storage locations for attachments, depending on the client's preference, and how dependency injection can manage this complexity without resorting to extensive conditional logic or tightly coupled code.

05:05

🛠️ Modularity and Upgradability

The second paragraph delves into the practical applications of dependency injection for various stages of a file upload process, including virus scanning, image scaling, preview generation, and encryption. It highlights how dependency injection allows for easy integration of new tools or services, such as switching from Threat Protect to Synergy Security Scanner, and how it simplifies the process of adding new features or supporting new file types. The paragraph also touches on the ease of testing and maintaining a system designed with dependency injection in mind.

10:06

🔍 Testing and Isolation

The final paragraph emphasizes the benefits of dependency injection for testing and isolating components of a system. It explains how interfaces and dependency injection enable the use of mock or fake implementations for testing purposes, allowing developers to verify the functionality of individual components without relying on the actual dependencies. The paragraph also introduces an interactive element, inviting viewers to experiment with the attachment service code and change the dependency injection configuration, offering a hands-on learning experience.

Mindmap

Keywords

💡Dependency Injection

Dependency Injection is a software design pattern that allows a program to supply the dependencies of a class at runtime rather than hard-coding them within the class. In the video, it is used to manage the complexity of different storage locations and services, making the code more modular and testable. It is exemplified by the way the attachment service is built, where the storage method is injected into the request handler, abstracting away the details of where the files are stored.

💡Business App

A business app, as mentioned in the video, is an application designed for professional use, often facilitating communication and collaboration among coworkers. The video's example includes a chat feature and the ability to send pictures and files, which are handled by an attachment service. This term is central to understanding the context in which Dependency Injection is being applied.

💡Attachment Service

The Attachment Service is a component of the business app that handles the storage, retrieval, and processing of attachments like files and images. It is responsible for uploading files to a storage location, such as Amazon S3 or an SFTP server, depending on the user's company preferences. The video discusses how Dependency Injection is used to manage the different storage options for this service.

💡S3

S3, or Simple Storage Service, is a part of Amazon Web Services that provides scalable storage for data. In the video, it is the default storage location for the attachment service. The concept is important because it represents one of the potential storage locations that the service must handle, and Dependency Injection is used to manage this and other storage options.

💡SFTP

SFTP, or Secure File Transfer Protocol, is a secure method of transferring files over a network. It is one of the alternative storage locations that the attachment service must support, as some companies do not want their data stored permanently on S3. The video uses SFTP as an example of how Dependency Injection can help manage different storage protocols.

💡WebDAV

WebDAV, or Web-based Distributed Authoring and Versioning, is an extension of the HTTP protocol that allows users to collaboratively edit and manage files on remote web servers. In the video, it is mentioned as another storage protocol that the attachment service might need to support for certain clients. This term illustrates the complexity that Dependency Injection helps to simplify.

💡Factory

A Factory, in the context of the video, is a design pattern that provides an interface for creating objects in a super class, but allows subclasses to alter the type of objects that will be created. The video mentions using a factory to decide which preview generator to create based on the file type being uploaded. This concept is crucial for understanding how Dependency Injection can be used to dynamically create and inject components into a system.

💡Interface

An Interface in programming is a contract or a set of methods that a class promises to implement. It is used in the video to define the contract for the attachment storage and the various services like virus scanning, image scaling, and encryption. Interfaces are key to Dependency Injection as they allow for loose coupling and the ability to swap out implementations without affecting the rest of the system.

💡Mock Implementation

A Mock Implementation is a simulated version of a system component used during testing. It mimics the behavior of the real component but is designed to be more controllable and easier to work with in a testing environment. The video discusses using mock implementations for testing purposes, such as a fake S3 or a mock encryption service, which are injected into the system to isolate and test specific parts of the code.

💡Configuration

Configuration in the video refers to the settings and parameters that determine how the attachment service behaves, such as which storage location to use or how to handle different file types. Configuration is passed into the constructors of the storage implementations, which is a common practice in Dependency Injection to set up the necessary dependencies for an object.

💡Request Handler

A Request Handler in the video is the part of the system that processes incoming requests, such as uploading an attachment. It is injected with the appropriate storage implementation, which it then uses to perform the upload. The request handler is a central component in the Dependency Injection process, as it receives the dependencies it needs to fulfill its responsibilities.

💡Testability

Testability refers to how easy it is to write tests for a piece of software. The video emphasizes that Dependency Injection makes the system more testable by allowing for the injection of mock or fake implementations, which isolate the components being tested from their dependencies. This is a significant advantage of using Dependency Injection, as it simplifies the testing process and improves the overall quality of the code.

Highlights

Dependency injection is a fundamental concept in software development that simplifies the process of managing dependencies between code components.

It allows for the decoupling of code by passing dependencies into a class rather than having the class create or find them itself.

This approach can lead to more maintainable and testable code due to its modular nature.

A business app example is provided, where users can chat, send pictures, and files, with attachments being handled by a separate service.

The attachment service uses dependency injection to handle multiple storage locations, such as S3 and SFTP, based on the user's company preferences.

The use of dependency injection simplifies the upload process by encapsulating the complexity of different storage solutions behind a single interface.

The concept of a factory is introduced to further abstract the creation of storage instances based on company configuration.

Dependency injection enables the easy swapping of components, such as virus scanners, without altering the core code of the application.

The use of interfaces and dependency injection allows for the easy integration of new features, such as support for new file types.

It also facilitates the use of mock implementations for testing, isolating components and making it easier to verify their behavior in isolation.

The architecture described is highly configurable, allowing for changes like switching from Threat Protect to Synergy Security Scanner with minimal code changes.

The video concludes with an invitation for viewers to experiment with the provided attachment service code and modify its dependency injection configuration.

The speaker emphasizes the importance of trying new things and learning through experimentation, which is reflected in the approach to teaching dependency injection.

The video serves as both an educational resource and a practical guide, offering viewers the opportunity to apply the concepts learned to a real-world scenario.

Transcripts

00:00

Dependency injection is a term

00:01

I don't love because it sounds a lot more fancy than it is.

00:05

Dependency injection is simply when you have a piece of code

00:08

which uses another piece of code, and instead of using that code

00:11

directly, it's passed in instead.

00:14

When you pass something in to be used, we call it injection.

00:17

We inject the dependent code into the code that uses it.

00:21

While this part is quite simple, it unlocks

00:23

some very powerful side effects that we're going to cover.

00:27

We have a business app where users can chat with their coworkers.

00:30

They can also send pictures and files to each other.

00:33

When a user sends something, the file gets uploaded to our attachment service.

00:37

The attachment service is responsible for storing, retrieving and processing

00:41

all attachments.

00:43

We're

00:43

going to build up this whole service using dependency injection

00:46

and we'll see what it enables us to do.

00:58

When a user sends a message

00:59

with an attachment, the message text gets sent to our standard chat service.

01:03

We want people to receive their messages almost real time.

01:07

So this service is all about speed.

01:09

The attachment, on the other hand, gets uploaded to our attachment service.

01:13

There's an end point in our note service

01:14

/attachment/upload that the app connects to and uploads a file.

01:18

The attachment gets stored on the disk temporarily, processed in a few ways

01:22

we'll talk about and then uploaded to its final destination.

01:26

The default storage location is an S3, a part of Amazon's Web services.

01:31

It's a simple storage service that lets you put up files and pull them down.

01:35

We have some code here that takes the uploaded file

01:38

and then uploads it to S3.

01:40

Unfortunately, simple and elegant doesn't always like to co-exist with business.

01:44

While S3 is nice and straightforward and most of our clients are okay with it,

01:48

we have a few firms that don't want us to permanently store their data.

01:52

This means we actually need our service to handle multiple storage locations.

01:56

Then, depending on which company a user is from,

01:59

we need to put their attachments in the right place.

02:02

Most of these picky clients just give us an SFTP server to connect to,

02:05

but one really wanted us to use WebDav.

02:09

Our first thought might be to simply extend our upload code with some

02:12

if statements

02:20

and then have the caller of the upload method pass in where to upload the file.

02:24

This is awkward for a few reasons.

02:26

First, this one class has a ton of responsibility, making the code pretty ugly.

02:31

The code for SFTP is intermingled with the code

02:33

from AWS and WebDav, even though they're pretty different.

02:37

There's a lot of paths the code can take, and that makes the code

02:40

harder to understand.

02:43

Second, using the class isn't very simple.

02:47

We have this one upload function

02:49

which needs a bunch of info for where we're going to upload the attachment.

02:53

But what info it needs is very different depending on where it's going.

02:57

If it's AWS, we need the AWS keys.

03:00

If it's SFTP

03:01

we need the address and private key and WebDav needs a URL and auth key.

03:05

So we're kind of forced

03:06

to have a bunch of these

03:06

optional variables that need to be filled out in certain cases,

03:10

and then comments to tell you which ones to fill out.

03:12

This makes it pretty easy for the caller to make a mistake.

03:15

And finally, the part of the code that actually calls upload over here

03:19

needs to have all of this destination specific context to perform the upload.

03:24

But really, at this phase, it just wants to upload.

03:27

The part of the code that knows best which company a user is from

03:30

and can deduce where the file should go is up here at the beginning of the request.

03:34

But right now, we're forced to pass all of this information around.

03:38

Let's see what happens if we use dependency injection instead.

03:41

Let's create an interface that represents our attachment storage,

03:44

which contains a key upload method

03:46

that does what the request handler wants to do: Upload an attachment.

03:50

Then we create three different implementations of the storage interface.

03:54

The configuration for each is passed into their constructor.

03:59

There's no

03:59

more optional variables that sometimes need to be set.

04:03

We require exactly these values and you get an error if you forget one.

04:07

So now, once the user is authenticated and we know which company they're from,

04:11

we create the storage that the request handler should use.

04:15

Instead of the configuration needing to be passed all the way to the request

04:18

handler, only the storage is passed to, or injected into, the request handler.

04:24

It's not aware of which storage is passed in

04:26

or where the file is going, it just knows that it can call upload.

04:30

That said, this construction code is still a little

04:33

too complex to put here, so let's see if we can clean this up.

04:37

If we look at the input here, it's really just this company configuration

04:41

and the output is the storage which conforms to the storage interface.

04:46

So we can just move this out to a factory.

04:51

Great.

04:52

But saving to the final storage destination

04:55

is the last step of the process.

04:57

We have all these other stages that the upload goes through.

05:04

We run each upload through a virus scanner.

05:07

This checks the files for signatures of obvious viruses.

05:11

Then if the file’s an image, we scale it down

05:13

to a max width of 2500 pixels.

05:17

This is what we display to the user when they click on an image

05:19

because it uses less bandwidth and loads faster.

05:24

Then the file goes through preview generation.

05:26

This is basically the thumbnail that pops up underneath an attachment in the chat

05:30

so the user can see what the attachment is without fully downloading it.

05:35

Then the last step is encryption.

05:38

If we're storing the files on Amazon's S3, we pre encrypt the files

05:41

before sending them up.

05:42

That way, if there's a security breach at Amazon,

05:45

we can say they're encrypted

05:46

when we have to send out one of those: ‘We were hacked, btw’, emails.

05:50

So let's see how we

05:51

can make each of these requirements fit cleanly into our service.

05:55

For the virus scanner, we currently use a scanner called Threat Protect.

05:59

However, Synergy Security Scanner has much better

06:02

detection KPIs and our plan is to switch to it.

06:05

But sadly, we haven't finalized the deal with Synergy We're only allowed to test

06:09

with it in our development environment, not in production.

06:12

No worries.

06:13

We can create a shared interface for our two scanners

06:20

and on initialization we pick one

06:26

and inject

06:27

that one into the request handler.

06:32

When we launch in development mode, synergy security is initialized

06:36

and in production the old Threat Protect scanner is created.

06:40

Our request handler just scans the files but doesn't know which

06:43

scanner is doing it.

06:45

For image scaling,

06:46

we use the sharp library in order to inject that,

06:50

we simply wrap it up in an image scaler interface.

06:54

The interface also contains a method

06:56

which tells us if an attachment supports scaling.

07:00

We injected into our upload, request and only scale

07:04

if the attachment supports it.

07:08

Preview generation is the most complex given how many types of attachments

07:11

there could be.

07:13

We have an interface that represents the different preview generators.

07:16

It takes the input file and then returns the preview image.

07:19

We have one implementation that handles document files like word docs, slides,

07:23

etc. one for videos which extracts a thumbnail from the video

07:27

and one for images.

07:29

But the image one can thankfully just reuse our image scaler.

07:33

We just inject the same image scaler from before into the image preview generator.

07:38

So we have all these preview generator classes,

07:41

but we only need one at a time depending on which file type is being uploaded.

07:45

The upload request shouldn't need to worry about these details.

07:49

We’ll inject a factory which takes on the burden of deciding

07:52

which preview generator to create.

08:08

The factory takes in the mime type of the upload and then returns

08:11

the right preview generator to use.

08:15

So now the upload can simply just ask the factory for the preview generator

08:18

and then use it.

08:22

And lastly, we have encryption.

08:25

We only have one implementation of encryption.

08:27

We use AES but the key is per user and comes from our key service.

08:33

So we inject our KeyService into the AesEncryption

08:37

and then the AesEncryption into the storage factory.

08:44

Then whenever we get a request for a company

08:47

that's configured to use AWS, the storage factory

08:50

injects the AesEncryption into the new instance of AWSstorage.

08:55

Then the upload request gets this final

08:57

constructed AWSstorage and simply calls upload without knowing

09:02

that there's this whole chain of connected functionality.

09:08

And now we have our complete architecture.

09:12

You can see that our service is configurable from this one spot,

09:16

which makes it super easy to change.

09:19

Once our deal with synergy security

09:21

goes through, it's just these two lines to change our service.

09:25

Want to add preview support for a new file type?

09:28

It just slots in like this.

09:29

No access to the key server when running the attachment service

09:33

on your local development box?

09:35

No worries.

09:36

We can just use a fixed key when running locally.

09:39

Injection basically just lets us pick and choose

09:42

from our compatible puzzle pieces and then slot them in when we need them.

09:46

You'll notice that the time

09:47

in which dependencies are injected varies a bit.

09:50

A few dependencies are resolved and injected right at startup,.

09:54

This is often the most common scenario in dependency injection.

09:59

But some dependencies that are chosen and injected when a request comes in.

10:03

In either case, the process is mostly the same.

10:06

We have some code that accomplishes something.

10:08

It lists the dependencies it needs, and so we fulfill those needs.

10:14

You might wonder why go through the hassle of creating interfaces

10:17

and injecting things when we only have one implementation?

10:20

Like we only have one implementation of encryption.

10:24

Well, there's one big thing we haven't talked about.

10:33

If you look at our architecture here, most of our components talk to each other

10:36

through these interfaces, which are injected in.

10:39

This means that each of these connection points we can control what is being used.

10:43

We've been using this to choose which implementation to use

10:46

in our production service, but we can also make them use no implementation.

10:50

We can use injection to inject fake or mock implementations instead,

10:54

which basically means we can slice and dice up our architecture

10:57

to isolate sections of code during testing.

11:00

Let's say we want to write a test for our AWS storage class.

11:04

We can use a fake S3

11:06

which we run locally that pretends to be the cloud service.

11:10

Then our test can call upload, and we can verify that a file got uploaded to S3.

11:15

But because of the encryption, we can't actually check the content

11:18

of the file and verify that it's correct and didn't get corrupted.

11:22

Not to worry.

11:23

Let's inject a mock encryption that basically just disables encryption.

11:27

When the AWS storage class asks us to encrypt a file

11:31

we'll just hand back a file that isn't actually encrypted.

11:34

Now our test is able to verify fully that uploading and only uploading works

11:39

because we've isolated it away from our dependencies.

11:43

What if we wanted to test encryption?

11:45

Well, we could mock out the key store to return a key that we control

11:49

instead of going all the way to the key service.

11:52

Or if we wanted to

11:53

integration test both our AWS code and encryption code together.

11:57

We could do that by injecting our fake key store into the real encryption

12:02

and then inject the real encryption into the AWS storage.

12:06

A key thing here is that this is easy to do.

12:09

A natural side effect of having nice code

12:12

is that it's easy to test without needing to hack around the code structure.

12:16

If you find yourself asking, how can I test a private method?

12:19

Or I need to set some internal variable in order to test.

12:22

That's a signal that you maybe need to pull some stuff out,

12:25

that you need to isolate some part of it by separating it and injecting

12:29

it instead.

12:37

I'm going to try something new with this video.

12:40

I truly think you only learn stuff by trying stuff.

12:43

So for those subscribed to my Patreon I'm going to start

12:46

including some light experiments with videos.

12:49

For this one, you can download the attachment service I wrote,

12:52

and I want you to reconfigure the service by changing the dependency injection.

12:57

And then you get to win some *Aesthetic* points

12:58

if you enter the results on the site.

Rate This

5.0 / 5 (0 votes)

Related Tags
Dependency InjectionSoftware DevelopmentCode SimplicityTestingScalabilityInterface ImplementationConfigurationModularityMockingIntegration