Loading...
Loading...
By continuing to use the platform, you accept the terms of the Privacy Policy and the use of cookies.
SOLID is set of five object-oriented design principles that help write code easily maintainable, testable and extensible. These principles were formulated by Robert C. Martin, also known as "Uncle Bob".
Each class or module should have single responsibility and, accordingly, single reason to change.
Why?
Example (JS)
// Bad: class does too many things — saves data and logs
class UserService {
saveUser(user) {
// ...code to save to database
Logger.log(`User ${user.name} saved`);
}
}
// Better: extract logging logic into separate class/service
class UserService {
constructor(logger) {
this.logger = logger;
}
saveUser(user) {
// ...code to save to database
this.logger.log(`User ${user.name} saved`);
}
}
class Logger {
log(message) {
console.log(`[LOG]: ${message}`);
}
}
Classes and modules should be open for extension, but closed for modification.
Simply put — modules should be designed so they need to be changed as rarely as possible, and functionality can be extended by creating new entities and composing them with old ones.
Why?
// Bad: each time we add new condition to code
function getDiscount(user) {
if (user.type === 'regular') {
return 5;
}
if (user.type === 'vip') {
return 10;
}
// ...
}
// Better
class RegularUser {
getDiscount() {
return 5;
}
}
class VipUser {
getDiscount() {
return 10;
}
}
function showDiscount(user) {
// Don't change logic, just call method
return user.getDiscount();
}
Here we can easily add new user by implementing their class with own getDiscount method. At same time showDiscount function won't need to be changed.
Child classes should be fully replaceable with their parent classes. If somewhere parent type object is expected, we should be able to substitute child type object without breaking program.
Simply put — child classes shouldn't contradict base class. For example, they can't provide interface narrower than base. Child behavior should be expected for functions that use base class.
Why?
// Bad: Square breaks Rectangle behavior
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
setWidth(width) {
this.width = width;
}
setHeight(height) {
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Rectangle {
// Violates LSP: when changing width or height
// other dimension should also change, but this breaks parent logic
setWidth(width) {
this.width = width;
this.height = width;
}
setHeight(height) {
this.width = height;
this.height = height;
}
}
In this case, if we expect Rectangle in code and call setWidth and setHeight methods, for Square we'll get behavior that doesn't match what base class assumes (square always has width equal to height).
Better — don't inherit Square from Rectangle, but either use common abstract class (e.g., Shape), or apply composition. This way we avoid breaking base logic with inheritance.
Clients (classes, modules) shouldn't depend on interfaces (or contracts) they don't use. If interface is "fat" and contains too many methods, better split it into several specialized interfaces.
Why?
// Bad: one class containing all methods
// (e.g., Animal with eat, walk, swim, fly)
class Animal {
eat() {}
walk() {}
swim() {}
fly() {}
}
// Dog class may not swim, and Bird class may not walk on ground
// etc. - we get empty or unused methods.
// Better: separate responsibilities
class Eatable {
eat() {}
}
class Walkable {
walk() {}
}
class Flyable {
fly() {}
}
// Now classes that need to fly simply extend Flyable,
// and those that need to walk — Walkable, and so on
class Dog extends Eatable {
// Additionally, if we want, we can inherit Dog from Walkable
// but not add Flyable and Swimable methods that it doesn't need
}
Dependencies should be built at abstraction level, not concrete implementations. High-level modules shouldn't depend on low-level modules directly — both types of modules depend on abstractions.
Why?
// Bad: class directly depends on concrete implementation
class UserService {
constructor() {
this.db = new MySQLDatabase();
}
getUsers() {
return this.db.query('SELECT * FROM users');
}
}
// Better: through abstraction (interface/contract)
class UserService {
constructor(database) {
// database is abstract contract that should be able to .query()
this.database = database;
}
getUsers() {
return this.database.query('SELECT * FROM users');
}
}
// Now we can substitute concrete implementation
class MySQLDatabase {
query(sql) {
// MySQL query
}
}
class MongoDatabase {
query(sql) {
// Mongo query
}
}
// In different environments/applications
// we can pass needed implementation to UserService constructor
const userService = new UserService(new MySQLDatabase());
Here UserService doesn't know concrete database implementation details, it works through abstraction provided by MySQLDatabase or MongoDatabase classes.