Crafting Code Brilliance: Demystifying Generic Classes for Improved Code Maintainability

In the ever-evolving landscape of software development, writing maintainable and high-quality code is paramount. As projects grow in complexity, developers often find themselves grappling with challenges such as code duplication, redundant patterns, and decreased maintainability. In this article, we'll explore the importance of generic classes and how they contribute to improved maintainability and code quality. Additionally, we'll emphasize the significance of analyzing code before diving into implementation, and provide a practical example to illustrate these concepts.

The Challenge of Code Duplication

Code duplication is a common problem that arises in software development. As developers create services and repositories for different entities, it's not uncommon to encounter repetitive patterns in the code. This redundancy can lead to maintenance nightmares, making it difficult to update, enhance, or debug the codebase. the simple example we can mention here is CRUD SERVICES

The Power of Generic Classes

One effective solution to mitigate code duplication is the use of generic classes. By creating generic repositories and services, developers can abstract away common functionalities, making them applicable to various entities. This approach promotes a cleaner and more maintainable codebase, as changes or updates can be made in a single location, affecting all instances uniformly.

The Importance of Analysis Before Implementation

While the idea of generic classes is powerful, it's essential to carefully analyze the codebase before implementing generic solutions. Not all entities or components may conform to a one-size-fits-all approach. Understanding the specific requirements and nuances of the domain is crucial to creating effective generic patterns.

Example 1: Generic Repository and Service in Spring Boot

Let's consider a scenario where we have entities such as Professor and Course in a Spring Boot application.


class Professor extend JpaRepository<T, ID> {}
class Controller extend JpaRepository<T, ID> {}*

Instead of creating separate repositories and services for each entity like presented above, we can create generic counterparts like so :

javaCopy code
// Generic Repository
public interface CustomJpaRepository<T, ID> extends JpaRepository<T, ID> {
    Optional<T> findById(ID id);
}

// Generic Service
public abstract class CustomEntityService<T, ID> {

    @Autowired
    private CustomJpaRepository<T, ID> repository;

    @Transactional(readOnly = true)
    public Optional<T> getEntity(final ID id) {
        return repository.findById(id);
    }

    // Additional generic methods for listing, deleting, creating, and updating entities
    // ...

    @Transactional
    public T updateEntity(ID entityId, T newEntity) {
        T entity = repository.findById(entityId)
                .orElseThrow(() -> new NoSuchElementException("Entity with ID " + entityId + " not found."));
        // Perform entity update logic here if needed
        return repository.save(entity);
    }
}

This CustomJpaRepository is an interface extending the Spring Data JPA JpaRepository. It introduces a generic method findById to retrieve an entity by its ID. The use of generics (<T, ID>) allows this repository to be applied to various entities, providing a consistent interface for accessing data.

The CustomEntityService is an abstract class that utilizes the generic repository. It autowires the repository, allowing it to access common CRUD operations such as findById and introduces additional generic methods for listing, deleting, creating, and updating entities. The use of @Transactional ensures that these operations are performed within a transaction, and any exception during the update is appropriately handled.

The super keyword is used in the constructor of the CustomEntityService class to invoke the constructor of the extended class (Object in this case) before executing the code in the subclass. This is essential for proper initialization and ensures that the repository is correctly injected.

Example 2: Generic API service in Angular

ApiService Generic Class

The ApiService class is designed to handle CRUD operations for a generic type T that extends IResource. The class is initialized with essential dependencies such as HttpClient, endpoint details, an API serializer, and the authentication service.

export class ApiService<T extends IResource> {
  // ... (constructor, private fields)

  public create(item: T): Observable<T> {
    // Implementation for creating an item
  }

  public update(item: T): Observable<T> {
    // Implementation for updating an item
  }

  getOne(id: number): Observable<T> {
    // Implementation for getting a single item by ID
  }

  getAll(params: any = null): Observable<T[]> {
    // Implementation for getting all items with optional parameters
  }

  delete(id: any) {
    // Implementation for deleting an item by ID
  }

  // ... (additional methods)

  protected convertData(data: any): T[] {
    // Implementation for converting data to generic type
  }

  getCustomHeaders(): HttpHeaders {
    // Implementation for setting custom headers, including authorization
  }
}

You may review a concrete example from a personal project that I’ve made during my free time :

For example, let’s take a look at these two services

UserService and LogRequestService

In this example, UserService and LogRequestService are concrete implementations of the ApiService, specifying the concrete types Iuser and ILogRequest, respectively. Each class is injected with necessary dependencies during initialization.

The use of super in the constructor of these classes is crucial. It calls the constructor of the extended class (ApiService), ensuring proper initialization of the superclass before executing the subclass-specific code. This enables the correct setup of essential dependencies for handling API requests.

@Injectable()
export class UserService extends ApiService<Iuser> {
  constructor(httpClient: HttpClient, nbAuthService: NbAuthService) {
    super(
      httpClient,
      environment.api_url,
      'workspace/user',
      new UsertSerializer(),
      nbAuthService
    );
  }
}

@Injectable()
export class LogRequestService extends ApiService<ILogRequest> {
  constructor(httpClient: HttpClient, nbAuthService: NbAuthService) {
    super(
      httpClient,
      environment.api_url,
      environment.logrequest_api.logrequest,
      new LogRequestSerializer(),
      nbAuthService
    );
  }

  // Additional methods specific to LogRequestService
}

Conclusion

In conclusion, the strategic use of generic classes can significantly enhance the maintainability and quality of code. By carefully analyzing the codebase and identifying common patterns, developers can create reusable and adaptable solutions. The generic repository and service pattern exemplified in the Spring Boot application showcases the power of abstraction and how it can simplify complex code structures. As developers, it's imperative to embrace a thoughtful approach to coding, considering not only the immediate requirements but also the long-term maintainability and scalability of the software.