paint-brush
Clean Code: Interface Segregation, Dependency Inversion, SOLID Principles in TS [Part 5]by@alenaananich
980 reads
980 reads

Clean Code: Interface Segregation, Dependency Inversion, SOLID Principles in TS [Part 5]

by Alena AnanichNovember 16th, 2023
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Explore advanced clean code principles in TypeScript, delving into the Interface Segregation Principle, emphasizing the importance of segregating interfaces to avoid unnecessary dependencies. Additionally, unravel the Dependency Inversion Principle, showcasing the significance of abstracting high-level modules and reducing code coupling for improved maintainability and flexibility.
featured image - Clean Code: Interface Segregation, Dependency Inversion, SOLID Principles in TS [Part 5]
Alena Ananich HackerNoon profile picture


Previous Parts:

Table of Contents

  1. Interface Segregation Principle
  2. Dependency Inversion Principle


In this article, we continue considering SOLID principles and practical examples of bad and clean architectural solutions.


1. Interface Segregation Principle

Classes shouldn’t be forced to depend on methods they do not use


The basics of this principle suggest that if any classes/objects do not implement any parts of the interface these fields/methods should be moved to the other interface.


Let’s take a look at an example:

enum Facets {
  CompanyFacet: 'companyFacet',
  TopicFacet: 'topicFacet'
}

// Bad
interface Facet {
  id: string;
  countNews: number;
  countDocs: number;
  selected: boolean;
  companyName?: string;
  topicName?: string;
}

function checkFacetType(facet: Facet): string {
  if (facet.companyName) {
    return Facet.CompanyFacet;
  }

  if (facet.topicName) {
    return Facet.TopicFacet;
  }
}


In the current example, the company facet doesn’t have the field topicName, and vice versa. So we need to move fields companyName and topicName to specified interfaces. The same rule will be for classes and methods. If any inherited class will not realize or use any parent method this method should be moved to a separated class/interface.


// Better
export interface Facet {
  id: string;
  countNews: number;
  countDocs: number;
  selected: boolean;
}

interface CompanyFacet extends Facet {
  companyName: string;
}

interface CompanyFacet extends Facet {
  topicName: string;
}



2. Dependency Inversion Principle


  • High-level modules should not depend on low-level modules. Both should depend on abstractions.
  • Abstractions should not depend upon details. Details should depend on abstractions.



// Bad
class DataBaseClient {
  request(): Promise<IClient> {
    return Promise.resolve();
  }
}

class LocalStorageClient {
  getItem(): IClient {
    return localStorage.getItem('any');
  }
}

class ClientRequest {
  private request: DataBaseClient;

  constructor() {
    this.request = new DataBaseClient();
  }

  get(): IClient {
    this.request.request();
  }
}

const client = new ClientRequest().get();


Here we have class ClientRequest (high-level module) with which we request the client info from the backend. In this class, we have a strict dependency on DataBaseClient (low-level module). If we decide to start grabbing clients from localStorage we will need to rewrite the realization of ClientRequest class, substitute DataBaseClient to LocalStorageClient and follow the new interface. It causes a lot of risk because we changed the realization of high-level class.


// We change DataBaseClient to LocalStorageClient 
class ClientRequest {
  private request: LocalStorageClient;

  constructor() {
    this.request = new LocalStorageClient();
  }

  get(): IClient {
    this.request.getItem();
  }
}

const client = new ClientRequest().get();


To reduce code coupling and make high-level classes independent from low-level classes we need to pass an abstract parameter as a dependency in a high-level module following a particular interface.


// Better
interface Client {
  get(): IClient;
}

class DataBaseClient implements Client {
  get(): IClient{
    return Promise.resolve().then(/../);
  }
}

class LocalStorageClient implements Client {
  get(): IClient{
    return localStorage.getItem('any');
  }
}

class ClientRequest {
  constructor(client: Client ) {}

  get(): IClient {
    return client.get();
  }
}

const client = new ClientRequest(new LocalStorageClient()).get();


Here we created an interface Client. Our high-level class ClientRequest takes client parameters following the Client interface with the method get. And we substitute dependency ClientRequest without code rewriting. So now high-level class ClientRequest doesn’t depend on low-level classes LocalStorageClient or DataBaseClient .


// Change the dependency 
const request = new ClientRequest(new LocalStorageClient().get());


Conclusion

With this article, we are done considering clean code principles in TypeScript. In the next article we will take a look at clean code approaches in Angular using TypeScript and following the already discussed TS principles.