Opening Thoughts

This is the fourth installment in our series dedicated to the SOLID acronym — a collection of guidelines that help us build code that is simpler to scale and easier to adjust, without needing to overhaul large portions of the application when requirements shift.

The principles we are covering are:

Now, let's turn our attention to the Interface Segregation Principle.

Understanding Interface Segregation

Angular & Interface Segregation Principle — figure 1

Credit: https://blog.larapulse.com/clean-code/solid-in-simple-words

The illustration above makes it clear — no client should be forced to depend on methods it never calls. Put simply, favour a collection of smaller, focused interfaces over a single, sprawling one.

How does this translate into real code?

Consider a typical scenario. Suppose our app has a view like this:

Angular & Interface Segregation Principle — figure 2

Here we have a table displaying a list of users, showing each user's name and status. The underlying model looks like this:

export type User = {
    id: UserId;
    name: string;
    status: UserStatus;
}

When a user double-clicks a row, the app navigates to a detailed view of that user:

Angular & Interface Segregation Principle — figure 3

That detail page reveals far more information than the list. One tempting approach is to reuse a single model for both views, relying on optional properties:

export type User = {
    id: UserId;
    name: string;
    status: UserStatus;
    profilePhoto?: ResourceUrl;
    email?: string;
    roles?: UserRole[];
}

But this creates its own set of issues. For starters, the list view's model now carries fields that never appear in the list. Then there's the broader problem: sprinkle optional fields throughout the codebase and you quickly lose track of where a field is mandatory versus where it's just a nice-to-have. And when a refactor comes along — say, a change in the server's response format — you won't know which fields are safe to alter or remove.

The cleaner solution is to define distinct models for the list and detail views. This eliminates the need for optional fields and gives each component a precise understanding of the data it works with.

export type UserUiListItem = {
    id: UserId;
    name: string;
    status: UserStatus;
}

For the detail view, we have its own model:

export type UserUiDetail = UserUiListItem & {
    profilePhoto: ResourceUrl;
    email: string;
    roles: UserRole[];
}

With this separation, we're honouring the Interface Segregation Principle.

Let's explore another practical case. Picture an application for managing both emails and users. It has two primary views. The first is an admin panel, where authorised users can:

  • remove
  • create
  • modify user records.

This panel is restricted to a select group of users. The second view is a message filter page. Here, we only need to fetch a list of users or pull up a specific user's details. This is the view most people use regularly. Both pages rely on a service to talk to the server:

export interface UserResource {
    create(user: User): Observable<void>;
    delete(id: EntityUid): Observable<void>;
    getAll(): Observable<User[]>;
    getOne(id: EntityUid): Observable<User | undefined>;
    update(user: User): Observable<void>;
}

Notice that the message filter page only makes use of two methods — "getAll" and "getOne". The remaining methods are irrelevant to it. Applying the principle that "more, smaller interfaces are better than one big one", we should split this into separate interfaces. One interface covers the operations available to the general user base:

export interface StandardUserResource {
    getAll(): Observable<User[]>;
    getOne(id: EntityUid): Observable<User | undefined>;
}

The other interface groups together actions that require elevated privileges:

export interface PrivilagedUserResource {
    create(user: User): Observable<void>;
    delete(id: EntityUid): Observable<void>;
    update(user: User): Observable<void>;
}

Now, instead of injecting the concrete implementation everywhere, we can inject these narrower interfaces depending on what each view needs:

@Component()
export class MessageFilterComponent {
    constructor(
        @Inject(STANDARD_USER_RESOURCE) private standardUserResource: StandardUserResource
    ) {}
}
@Component()
export class AdminComponent {
    constructor(
        @Inject(PRIVILAGED_USER_RESOURCE) private privilagedUserResource: PrivilagedUserResource,
        @Inject(STANDARD_USER_RESOURCE) private standardUserResource: StandardUserResource
    ) {}
}

This way, each component is only exposed to the methods it actually calls. By doing so, we are in line with the Interface Segregation Principle.

Our next piece will tackle the final element of SOLID — the Dependency Inversion Principle.