Quick Implementation of DI/DI Container on SAP CAP: Theoretical Part

Introduction

This article is divided into two chapters: the theoretical part in Chapter 1 and the hands-on part in Chapter 2. It explains how to utilize Dependency Injection (DI) and DI containers (IoC containers) on CAP. This guide aims to serve as a reference for architectural design that enhances testability, maintainability, and modularity in Side-by-Side application development.

Note: While this article discusses the benefits of DI/DI containers, it is important to consider the potential drawbacks such as increased code volume, structural complexity, and performance overhead. Decide whether to apply this design pattern based on your project’s requirements.

Target Audience

This article assumes a basic understanding of CAP. If you are new to CAP, please refer to the SAP CAP Cookbook for more information. This content is intended for those who are new to using DI/DI containers, want to know how to utilize them in CAP projects, or want to try it out hands-on.

Issues with Simple Application Design

When developing a CAP application with a simple layered architecture, the typical implementation involves:

Creating a repository.Writing business logic in handlers.Defining services in CDS files and detailing the service classes based on them.

In this case, the handler instantiates the repository class, meaning the handler depends on the repository class.

Let’s understand this with an example code to fetch all book data.

import { Book } from “./book”;

class BookRepository {
async findAllBooks(): Promise<Book[]> {
return await SELECT.from(“Books”);
}
}

export { BookRepository };

Indeed, the readBooksHandler depends on the concrete BookRepository class.

import { BookRepository } from “./bookRepository”;

export const readBooksHandler = () => async () => {
// Instantiating BookRepository within readBooksHandler.
const bookRepository = new BookRepository();
const allBooks = await bookRepository.findAllBooks();
return allBooks;
};

If you want to replace it with a mock, you need to maintain the BookRepository.

What is Dependency Injection (DI)?

To solve the above problem, there is a design pattern called Dependency Injection (DI), which injects object dependencies from the outside.

Previously, readBooksHandler depended on the concrete BookRepository class, and now you will change it to depend on an interface, IBookRepository, for using DI. This allows the use of any concrete class that implements IBookRepository, whether it is BookRepository or MockBookRepository, thereby improving testability. Let’s see how to write the code.

First, define IBookRepository.

import { Book } from “./book”;

export interface IBookRepository {
findAllBooks(): Promise<Book[]>;
}

Rewrite BookRepository to implement IBookRepository.

import { IBookRepository } from “./iBookRepository”;
import { Book } from “./book”;

class BookRepository implements IBookRepository {
async findAllBooks(): Promise<Book[]> {
return await SELECT.from(“Books”);
}
}

export { BookRepository };

Pass bookRepository with IBookRepository interface as an argument in the readBooksHandler.

import { IBookRepository } from “./iBookRepository”;

export const readBooksHandler =
(bookRepository: IBookRepository) =>
async () => {
const books = await bookRepository.findAllBooks();
return books;
};

Instantiate BookRepository within CatalogService. This way, if you want to use a mock, you only need to replace it with MockBookRepository.

import { BookRepository } from “./bookRepository”;
import { readBooksHandler } from “./readBooksHandler”;
import cds from “@sap/cds”;

class CatalogService extends cds.ApplicationService {
async init() {
const bookRepository = new BookRepository();

// Use the following bookRepository if you want to use a mock.
// const bookRepository = new MockBookRepository();

this.on(“READ”, “Books”, readBooksHandler(bookRepository));
}
}

export default CatalogService;

Note: In this example, submitOrderHandler is defined as a function, but if you define it as a class, you can achieve DI by taking an object with IBookRepository as an abstract type in the constructor arguments.

The Necessity of DI Containers

Using DI makes the handler and repository loosely coupled, making it easy to replace with mocks. However, the service class, which calls the handler, now needs to handle the instantiation of the handler and the resolution of dependencies. This can become cumbersome when the dependencies within or between layers become complex, potentially leading to errors. This is where DI containers come in. They allow you to centralize the code for instantiating dependent objects and resolving dependencies in a DI container configuration file, making replacements immediate. For more details, refer to the hands-on part.

Conclusion

In this article,  I explained the issues with simple application design and the usefulness of DI/DI containers to solve those issues. Next, refer to “Quick Implementation of DI/DI Container on SAP CAP: Node.js Hands-on Part” and try writing the code yourself.

 

​ IntroductionThis article is divided into two chapters: the theoretical part in Chapter 1 and the hands-on part in Chapter 2. It explains how to utilize Dependency Injection (DI) and DI containers (IoC containers) on CAP. This guide aims to serve as a reference for architectural design that enhances testability, maintainability, and modularity in Side-by-Side application development.Note: While this article discusses the benefits of DI/DI containers, it is important to consider the potential drawbacks such as increased code volume, structural complexity, and performance overhead. Decide whether to apply this design pattern based on your project’s requirements.Target AudienceThis article assumes a basic understanding of CAP. If you are new to CAP, please refer to the SAP CAP Cookbook for more information. This content is intended for those who are new to using DI/DI containers, want to know how to utilize them in CAP projects, or want to try it out hands-on.Issues with Simple Application DesignWhen developing a CAP application with a simple layered architecture, the typical implementation involves:Creating a repository.Writing business logic in handlers.Defining services in CDS files and detailing the service classes based on them.In this case, the handler instantiates the repository class, meaning the handler depends on the repository class.Let’s understand this with an example code to fetch all book data.import { Book } from “./book”;

class BookRepository {
async findAllBooks(): Promise<Book[]> {
return await SELECT.from(“Books”);
}
}

export { BookRepository };Indeed, the readBooksHandler depends on the concrete BookRepository class.import { BookRepository } from “./bookRepository”;

export const readBooksHandler = () => async () => {
// Instantiating BookRepository within readBooksHandler.
const bookRepository = new BookRepository();
const allBooks = await bookRepository.findAllBooks();
return allBooks;
};If you want to replace it with a mock, you need to maintain the BookRepository.What is Dependency Injection (DI)?To solve the above problem, there is a design pattern called Dependency Injection (DI), which injects object dependencies from the outside.Previously, readBooksHandler depended on the concrete BookRepository class, and now you will change it to depend on an interface, IBookRepository, for using DI. This allows the use of any concrete class that implements IBookRepository, whether it is BookRepository or MockBookRepository, thereby improving testability. Let’s see how to write the code.First, define IBookRepository.import { Book } from “./book”;

export interface IBookRepository {
findAllBooks(): Promise<Book[]>;
}Rewrite BookRepository to implement IBookRepository.import { IBookRepository } from “./iBookRepository”;
import { Book } from “./book”;

class BookRepository implements IBookRepository {
async findAllBooks(): Promise<Book[]> {
return await SELECT.from(“Books”);
}
}

export { BookRepository };Pass bookRepository with IBookRepository interface as an argument in the readBooksHandler.import { IBookRepository } from “./iBookRepository”;

export const readBooksHandler =
(bookRepository: IBookRepository) =>
async () => {
const books = await bookRepository.findAllBooks();
return books;
};Instantiate BookRepository within CatalogService. This way, if you want to use a mock, you only need to replace it with MockBookRepository.import { BookRepository } from “./bookRepository”;
import { readBooksHandler } from “./readBooksHandler”;
import cds from “@sap/cds”;

class CatalogService extends cds.ApplicationService {
async init() {
const bookRepository = new BookRepository();

// Use the following bookRepository if you want to use a mock.
// const bookRepository = new MockBookRepository();

this.on(“READ”, “Books”, readBooksHandler(bookRepository));
}
}

export default CatalogService;Note: In this example, submitOrderHandler is defined as a function, but if you define it as a class, you can achieve DI by taking an object with IBookRepository as an abstract type in the constructor arguments.The Necessity of DI ContainersUsing DI makes the handler and repository loosely coupled, making it easy to replace with mocks. However, the service class, which calls the handler, now needs to handle the instantiation of the handler and the resolution of dependencies. This can become cumbersome when the dependencies within or between layers become complex, potentially leading to errors. This is where DI containers come in. They allow you to centralize the code for instantiating dependent objects and resolving dependencies in a DI container configuration file, making replacements immediate. For more details, refer to the hands-on part.ConclusionIn this article,  I explained the issues with simple application design and the usefulness of DI/DI containers to solve those issues. Next, refer to “Quick Implementation of DI/DI Container on SAP CAP: Node.js Hands-on Part” and try writing the code yourself.   Read More Technology Blogs by SAP articles 

#SAP

#SAPTechnologyblog

You May Also Like

More From Author