Previous Parts:
Clean Code: Naming and Code Composition in TypeScript [Part 2]
Clean Code: Single Responsibility, Open/Closed, Liskov Substitution SOLID Principles in TS [Part 4]
In this article, we continue considering SOLID principles and practical examples of bad and clean architectural solutions.
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;
}
- 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());
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.