When I first started diving into the world of software development, I often found myself overwhelmed by all the buzzwords and concepts flying around. One of the concepts that seemed particularly daunting was SOLID principles. It felt like something only "serious" developers needed to worry about. But as I grew more comfortable with coding, I realized that these principles are less about being fancy and more about writing code that doesn’t make you want to pull your hair out after a few months.
So, here’s my take on SOLID principles in JavaScript—a no-nonsense, practical guide that I wish I had when I started.
The Single Responsibility Principle states that a class should have only one reason to change, meaning it should have only one job or responsibility.
Think of a barista at your favorite coffee shop. Their job is to make coffee. If they suddenly have to start fixing the espresso machine, serving pastries, and taking out the trash, things are going to get chaotic. Just like how the barista should focus on making coffee, your class should focus on doing one thing well.
Imagine you have a User class that handles user authentication, data validation, and database storage. It’s doing too much! By splitting these responsibilities into separate classes, you make your code easier to manage and maintain.
class UserAuthenticator { login(user) { // handle login } } class UserDataValidator { validate(user) { // validate user data } } class UserDatabase { save(user) { // save user to the database } }
The Open/Closed Principle states that software entities should be open for extension but closed for modification. In other words, you should be able to add new functionality without changing existing code.
Imagine your favorite gaming console. You can add new games, controllers, and accessories, but you don’t have to open it up and rewire it to do so. Similarly, you should be able to add new features to your code without changing its core structure.
Let’s say you have a Shape class with a method to calculate the area. If you need to add a new shape, like a triangle, you shouldn’t have to modify the existing class. Instead, extend it.
class Shape { area() { throw "Area method not implemented"; } } class Rectangle extends Shape { constructor(width, height) { super(); this.width = width; this.height = height; } area() { return this.width * this.height; } } class Circle extends Shape { constructor(radius) { super(); this.radius = radius; } area() { return Math.PI * this.radius * this.radius; } }
The Liskov Substitution Principle states that objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program.
Imagine renting a car. Whether you get a sedan or an SUV, you expect it to have the basic functionalities of a car: it should drive, steer, and stop. If your rental car required a completely different set of controls, you’d be in trouble! Similarly, subclasses should behave in a way that doesn’t break the expectations set by their parent class.
If you have a Bird class and a Penguin class that extends it, the Penguin should still behave like a Bird even if it can’t fly. It should still walk, eat, and maybe even swim.
class Bird { move() { console.log("Flies in the sky"); } } class Penguin extends Bird { move() { console.log("Swims in the water"); } } const myBird = new Bird(); const myPenguin = new Penguin(); myBird.move(); // Flies in the sky myPenguin.move(); // Swims in the water
The Interface Segregation Principle suggests that clients should not be forced to implement interfaces they don’t use. Instead of having one large interface, you should create smaller, more specific ones.
Imagine a restaurant where the chef also has to be the waiter, bartender, and dishwasher. It’s overwhelming and inefficient! Instead, each role should have its specific tasks. Similarly, your interfaces should be specialized and focused.
If you have a Worker interface that includes methods like buildHouse, paintHouse, and designHouse, a worker who only paints houses shouldn’t have to implement all the other methods. Break it down into smaller interfaces.
class Builder { build() { console.log("Building house..."); } } class Painter { paint() { console.log("Painting house..."); } } class Designer { design() { console.log("Designing house..."); } }
The Dependency Inversion Principle states that high-level modules should not depend on low-level modules. Both should depend on abstractions.
Think about how you plug your phone charger into a wall socket. You don’t need to know the details of the electrical wiring inside the walls—all you need is the interface (the socket) to power your device. Similarly, your code should depend on abstractions (interfaces), not concrete implementations.
If you have a LightBulb class that directly controls a Switch class, you’re creating a tight coupling. Instead, both should depend on an interface like PowerSource.
class LightBulb { turnOn(powerSource) { powerSource.provideElectricity(); console.log("Light is on"); } } class Switch { constructor(powerSource) { this.powerSource = powerSource; } operate() { this.powerSource.togglePower(); } } class PowerSource { provideElectricity() { console.log("Providing electricity"); } togglePower() { console.log("Toggling power"); } }
Mastering the SOLID principles is like learning to cook with a set of proven recipes. Once you understand them, you can whip up code that’s not just functional but elegant and easy to maintain. So next time you find yourself in a coding conundrum, remember: there’s a principle for that!
Happy coding! ?
The above is the detailed content of Making SOLID Simple: A JavaScript Guide to Clean Code Principles. For more information, please follow other related articles on the PHP Chinese website!