SOLID: Open/Closed Principle — With Real Life Example

// User.ts
export default class User {
id: number;
firstName: string;
lastName: string;
email: string;
password: string;
googleId?: string;

constructor(
firstName: string,
lastName: string,
email: string,
password: string,
googleId?: string
) {
this.id = this.createId();
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.password = password;
this.googleId = googleId;
}

private createId = (): number => {
return Math.floor(Math.random() * 1000);
};
}
// IUserService.ts
import User from "./User";

export default interface IUserService {
users: Array<User>;

addUser(user: User): void;

getUserByMail(mail: string): Promise<User | undefined>;
getUserByGoogleId(googleId: string): Promise<User | undefined>;

setUserGoogleId(user: User, googleId: string): void;
}
// UserService.ts
import IUserService from "./IUserService";
import User from "./User";

export default class UserService implements IUserService {
users: Array<User> = [];

constructor() {}

addUser = (user: User): void => {
this.users.push(user);
};

getUserByGoogleId = async (googleId: string): Promise<User | undefined> => {
return this.users.find((u) => u.googleId === googleId);
};

getUserByMail = async (mail: string): Promise<User | undefined> => {
return this.users.find((u) => u.email === mail);
};

setUserGoogleId = (user: User, googleId: string): void => {
user.googleId = googleId;
};
}
// AuthCredential.ts
export interface AuthCredential {
email?: string;
password?: string;
googleId?: string;
}
// IAuthProvider.ts
import UserService from "../../user/UserService";
import { AuthCredential } from "../credential/AuthCredential";
import User from "../../user/User";

export default interface IAuthProvider {
userService: UserService;

login(credentials: AuthCredential): Promise<User>;
}
// IAuthService.ts
import IAuthProvider from "./provider/IAuthProvider";
import User from "../user/User";
import { AuthCredential } from "./credential/AuthCredential";

export default interface IAuthService {
provider: IAuthProvider;

login: (credentials: AuthCredential) => Promise<User>;
}
// AuthService.ts
import IAuthService from "./IAuthService";
import IAuthProvider from "./provider/IAuthProvider";
import { AuthCredential } from "./credential/AuthCredential";
import User from "../user/User";

export default class AuthService implements IAuthService {
provider: IAuthProvider;

constructor(provider: IAuthProvider) {
this.provider = provider;
}

login = async (credentials: AuthCredential): Promise<User> => {
return this.provider.login(credentials);
};
}
// BaseAuthProvider.ts
import IAuthProvider from "./IAuthProvider";
import IUserService from "../../user/IUserService";
import { AuthCredential } from "../credential/AuthCredential";
import User from "../../user/User";

export default class BaseAuthProvider implements IAuthProvider {
userService: IUserService;

constructor(userService: IUserService) {
this.userService = userService;
}

login = async (credentials: AuthCredential): Promise<User> => {
this.checkMailAddressIsValid(credentials);
this.checkPasswordIsValid(credentials);
const user = await this.getUserIsThere(credentials.email!);
this.checkPasswordsIsMatch(user, credentials.password!);
return user;
};

private checkMailAddressIsValid = (credentials: AuthCredential): void => {
if (!credentials.email)
throw new Error("Please provide a valid email address");
};

private checkPasswordIsValid = (credentials: AuthCredential): void => {
if (!credentials.password)
throw new Error("Please provide a valid password");
};

private getUserIsThere = async (mail: string): Promise<User> => {
const user: User | undefined = await this.userService.getUserByMail(mail);
if (!user) throw new Error("User not found");
return user;
};

private checkPasswordsIsMatch = (user: User, password: string): void => {
if (user.password !== password) throw new Error("Wrong password");
};
}
// AuthGoogleProvider.ts
import UserService from "../../../user/UserService";
import BaseAuthProvider from "../BaseProvider";
import IAuthProvider from "../IAuthProvider";
import { AuthCredential } from "../../credential/AuthCredential";
import User from "../../../user/User";

export default class AuthGoogleProvider implements IAuthProvider {
userService: UserService;

constructor(userService: UserService) {
this.userService = userService;
}

login = async (credentials: AuthCredential): Promise<User> => {
this.checkGoogleIdIsValid(credentials);
return this.getUserIsThere(credentials.googleId!);
};

private checkGoogleIdIsValid = (credentials: AuthCredential): void => {
if (!credentials.googleId)
throw new Error("Please provide a valid Google ID");
};

private getUserIsThere = async (googleId: string): Promise<User> => {
const user: User | undefined = await this.userService.getUserByGoogleId(
googleId
);
if (!user) throw new Error("User not found");
return user;
};
}
// app.ts
import User from "./user/User";
import UserService from "./user/UserService";
import BaseAuthProvider from "./auth/provider/BaseProvider";
import AuthGoogleProvider from "./auth/provider/google/AuthGoogleProvider";
import AuthService from "./auth/AuthService";

(async () => {
const userService = new UserService();

const baseProvider = new BaseAuthProvider(userService);
const googleProvider = new AuthGoogleProvider(userService);

const authService = new AuthService(baseProvider);
const authServiceSec = new AuthService(googleProvider);

const sami = new User(
"Sami Salih",
"İbrahimbaş",
"info@ssibrahimbas.com",
"1234"
);
const elon = new User("Elon", "Musk", "info@tesla.com", "1234");
userService.addUser(sami);
userService.addUser(elon);
userService.setUserGoogleId(elon, "YouDon'tHaveEnoughMoneyToBuyMe");

await authService.login({
email: "info@ssibrahimbas.com",
password: "1234",
}); // success
await authServiceSec.login({
googleId: "IWillBuyYouToo", // User Not Found
});
})();

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Sami Salih İbrahimbaş

Sami Salih İbrahimbaş

Hello, I am Sami Salih İbrahimbaş. I am a Mid Level Backend Stack Developer living and working in Turkey.