visit
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]
Classes shouldn’t be forced to depend on methods they do not use
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();
// 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());