Avoiding the Pitfalls of Using “new” Keyword

Discuss how to avoid creating dependent objects manually in order to improve maintainability using dependency injection and abstraction.

Shawn Shi
Geek Culture
6 min readFeb 12, 2023

--

Diagram by author. Icons from flaticon.com.

Background

You probably see the “new” keyword every day. In simple cases, e.g., creating instances for data transfer object (DTO) or plain old class object (POCO), it is perfectly good practice to use the “new” keyword. However, in classes where complex behaviour or state exists, using “new” keyword to manually initialize instances of dependency classes can lead to tight coupling between the classes, making the code harder to maintain, test, and extend. This is because the class (A) that creates the instance of another class (B) is now responsible for managing its lifetime and dependencies. If the implementation of the other class (B) changes, the class (A) that creates it must also be changed, which can result in a ripple effect throughout the codebase.

Dependency injection is a better approach for managing class instances and their lifetimes, as it decouples the creation and management of instances from the rest of the code. This allows for improved maintainability, testability, and flexibility, as we’ll see in the following sections of this article.

Goal

Sending emails is a common requirement for many web applications. In this article, we will use sending emails with SendGrid as an example to demonstrate how to improve the code implementation in an ASP.NET Core web application. We will go through 3 steps and explain the benefits of each step:

  1. Simplest implementation using “new” keyword
  2. Use dependency injection
  3. Use an interface to abstract out the business logic

Simplest way (BAD way)

If we just copy-paste the code from the SendGrid Quick Start guide, our controller action for sending email might look something like this below:

public class EmailController : Controller
{
[HttpPost]
public async Task SendEmail(string toEmail, string subject, string message)
{
// Using "new" keyword to instantiate the SendGridClient
var client = new SendGridClient("your-sendgrid-api-key");

// Prepare email and send
EmailAddress from = new EmailAddress("test@example.com", "Example User");
EmailAddress to = new EmailAddress(toEmail, "Recipient");
SendGridMessage msg = MailHelper.CreateSingleEmail(from, to, subject, message, message);

await client.SendEmailAsync(msg);
}
}

Problems here:

  1. Obviously, the EmailController is taking care of two things, managing its dependency, SendGridClient instance, and also managing request and response, which is the only thing the controller is supposed to do. This breaks the single responsibility design principle.
  2. Imaging there are 4 more other places where you would send emails, such as account registration, password reset, etc., you would see 4 more references to “new SendGridClient(“your-sendgrid-api-key”)”. What if you need to rotate your api key? Now you have 5 places to update every time.
  3. If ever our relationship with SendGrid turns sour and we want to replace it with MailChimp, we have to update lots of code in the 5 places.

Use Dependency Injection

Dependency Injection is a software design pattern that allows the creation of dependent objects outside of a class and provides those objects to a class through constructor parameters. By using dependency injection, we can make our code more maintainable and testable.

In our sample web application, instead of manually initializing the SendGrid client, we can use dependency injection to initialize it. To do this, we need to add the following code to the Startup.cs file to configure the SendGrid service:

public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddSingleton(x => new SendGridClient("your-api-key");
}

And in the HomeController class, add a constructor to inject the SendGrid client and store it in a private field.

public class EmailController : Controller
{
private readonly SendGridClient _sendGridClient;

// Constructor to resolve dependency objects
public EmailController(SendGridClient sendGridClient)
{
_sendGridClient = sendGridClient;
}

[HttpPost]
public async Task SendEmail(string toEmail, string subject, string message)
{
// No "new" keyword to instantiate the SendGridClient

// Prepare email and send
EmailAddress from = new EmailAddress("test@example.com", "Example User");
EmailAddress to = new EmailAddress(toEmail, "Recipient");
SendGridMessage msg = MailHelper.CreateSingleEmail(from, to, subject, message, message);

await client.SendEmailAsync(msg);
}
}

With these changes:

  • we no longer need to manually initialize the SendGrid client. Instead, it is injected into the controller class using dependency injection, allowing for better maintainability and testability of the code. This solves our problem #1.
  • If we need to rotate the API key, we only need to update the startup class. This also solves the problem #2.
  • However, the EmailController still knows a little bit about how SendGridClient works, for example, it knows to create instances for EmailAddress, SendGridMessage, which are only relevant if we are using SendGrid. We still have problem #3 to solve.

Use Abstraction

By using an interface, we can make our code more flexible and easier to maintain. An interface is a blueprint for classes that allows us to define a set of methods and properties to expose. By implementing an interface, a class agrees to provide implementations for all of the methods and properties defined in the interface.

In our web application, we can define an interface called IEmailService with a single method to send an email. To do this, we need to create a new file called IEmailService.cs and add the following code:

using System.Threading.Tasks;
using SendGrid;

public interface IEmailService
{
Task SendEmailAsync(string toEmail, string subject, string message);
}

We also need to create a new class called SendGridEmailService.cs that implements the IEmailServiceinterface:

using SendGrid;
using System.Threading.Tasks;

public class SendGridEmailService: IEmailService
{
private readonly SendGridClient _sendGridClient;

public SendGridEmailService(SendGridClient sendGridClient)
{
_sendGridClient = sendGridClient;
}

public async Task SendEmailAsync(string toEmail, string subject, string message)
{
EmailAddress from = new EmailAddress("test@example.com", "Example User");
EmailAddress to = new EmailAddress(toEmail, "Recipient");
SendGridMessage msg = MailHelper.CreateSingleEmail(from, to, subject, message, message);
await _sendGridClient.SendEmailAsync(msg);
}
}

And in the Startup.cs file, we need to add the following code to configure the SendGrid service:

public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
// Register a singleton SendGridClient instance,
// which will last the lifetime of the application
services.AddSingleton(x => new SendGridClient("your-api-key"));
// Register our IEmailService implementation using SendGrid
services.AddSingleton<IEmailService, SendGridEmailService>();
}

In the controller class, we need to modify the constructor to inject the `IEmailService` interface:

public class EmailController : Controller
{
private readonly IEmailService _emailService;

// Constructor to resolve dependency objects
public EmailController(IEmailService emailService)
{
_emailService = emailService;
}

[HttpPost]
public async Task SendEmail(string toEmail, string subject, string message)
{
// No "new" keyword to instantiate the SendGridClient

// No Prepare email and send

await _emailService.SendEmailAsync(toEmail, subject, message);
}
}

With these changes:

  • our controller now has NO knowledge on how an email should be sent, and has NO idea who is our email provider. If we want to change the email service provider, we only need to modify the implementation of the `IEmailSender` interface and update the configuration in the Startup.cs file. That allows us to easily swap out SendGrid with a different email service provider, if needed. Now our problem #3 is solved.

Benefits Summary

By using dependency injection and an interface, we can make our code more maintainable, testable, and flexible.

  • Improved maintainability: DI decouples the creation of dependent objects from the class that uses them, making it easier to maintain and test the code.
  • Improved testability: Dependency injection allows us to easily replace the SendGrid client with a mock object for testing purposes.
  • Improved flexibility: By using an interface, we can easily swap out SendGrid implementation of our IEmailSerivce with a different email service provider, without changing the implementation of the controller class. This is because our email controller class even has no idea that SendGrid is used, all it knows is that the abstraction of IEmailService.
    Conclusion

In conclusion, by using dependency injection and an interface to replace using “new” keyword to instantiating dependency objects, we can make our application more maintainable, testable, and flexible.

Many thanks for reading! Cheers!

--

--

Shawn Shi
Geek Culture

Senior Software Engineer at Microsoft. Ex-Machine Learning Engineer. When I am not building applications, I am playing with my kids or outside rock climbing!