DEV Community

Michiharu Ono
Michiharu Ono

Posted on

Getting Started with Object-Oriented Design (Part 2): Design Principles and Design Patterns

In the previous article, we have discussed the fundamental goal of Object-Oriented Design (OOD), which is to manage the relationships between objects, allowing the system to adapt efficiently to future changes.

This naturally leads us to the next question: How do we effectively manage the relationships between objects? 🤔

The truth is, there’s no one-size-fits-all approach to this. Every system and context is different, and there are many ways to go about it.

Thankfully, there are some incredibly useful tools to simplify this process 😌  —tools crafted by industry pioneers like the Gang of Four. We owe these visionaries a great deal for sharing their wisdom, which has laid the groundwork for many of the techniques we rely on today.

With that in mind, I'd like to briefly introduce two practical concepts that can help you manage object relationships more effectively: Design Principles and Design Patterns.

They will help set you on the path toward building scalable systems or at least nudge you in the right direction. By applying these concepts, you'll not only enhance the quality of your code but also make your development process smoother and more predictable.


Understanding “Design Principles”

Design principles are general guidelines or best practices that help you make better decisions when writing code. These principles aren’t really specific solutions to problems, but rather broad rules that you choose to follow when applies.

Some popular design principles include:

1. Single Responsibility Principle (SRP)

This principle is about keeping each class focused on a single responsibility(*some might argue but it does not necessarily mean a single class must have only a single public method). This makes the code easier to understand and modify.

2. Open-Closed Principle (OCP)

This principle ensures your code can accommodate new functionality without altering existing code. This reduces the risk of introducing bugs when extending the app. For instance, by using inheritance or composition, you can add features while maintaining the integrity of the original class.

3. Liskov Substitution Principle (LSP)

You should be able to use a subclass anywhere you'd use its parent class without breaking anything. In other words, a subclass should honor the expectations set by its parent class, ensuring it behaves consistently and predictably without introducing errors or unexpected results.

4. Interface Segregation Principle (ISP)

This principle ensures that classes aren’t forced to implement methods they don’t need by promoting the use of smaller, more focused interfaces. In essence, it advocates for splitting large, "fat" interfaces into smaller, specific ones to reduce unnecessary dependencies and improve modularity.

5. Dependency Inversion Principle (DIP)

This states that high-level modules should not depend on low-level modules; instead, both should depend on abstractions. Abstractions should not depend on details, but details should depend on abstractions.

This concept might be self-explanatory at first, but don’t worry—let’s break it down with an example.

For instance, in a coffee maker system, the high-level module is the coffee maker, and the low-level module is the brewing method (e.g., drip, espresso).

If you apply the Dependency Inversion Principle (DIP), the coffee maker depends on an abstraction of the brewing method (an interface or abstract class that defines the brewing process), not on a specific implementation (such as a DripBrew or EspressoBrew class). This allows you to swap out different brewing methods without altering the core coffee maker logic. The coffee maker just needs to know how to interact with the abstraction, making the system more flexible and maintainable.

6. DRY (Don’t Repeat Yourself)

Duplicated code can lead to inconsistencies and make maintenance more difficult. Abstract common functionality into reusable components or methods. This reduces redundancy and ensures that changes only need to be made in one place.

7. Law of Demeter

The Law of Demeter is often summarized as "Only talk to your immediate friends." This principle emphasizes minimizing an object's knowledge of other objects' internal structures or properties. Instead of relying on deep method chaining (obj.getSomething().doAnotherThing()), delegate responsibilities to direct collaborators.

It is important to note that there are MANY MORE of these principles! You don’t really need to perfectly memorize them all but it is nice to know to stop your hands and think about it once in a while. (By the way, if you have every heard of SOLID, it is about the principles from 1 to 5.)

Also, make sure not to overemphasize one principle over another. ⚠️ For example, if you care too much about DRY principle for example, you might compromise other areas that may affect managing the relationships between objects.


Understanding “Design Patterns”

When it comes to object-oriented software design, principles like Single Responsibility and Open-Closed lay the groundwork for creating maintainable systems.

But what about recurring challenges that developers face across projects?

This is where design patterns come into play!

What Are Design Patterns?

Design patterns are proven, reusable solutions to common problems encountered in software design, providing you with practical templates for solving particular challenges.

Think of design patterns as ready-to-use blueprints that you can implement when you face a certain type of problem. These patterns help you avoid reinventing the wheel 🔄 and offer solutions that have already been tested and refined over time.

While design principles provide guidance on how to design, design patterns give you specific techniques or approaches to address particular issues.

Here are five of the commonly used design patterns in object-oriented programming. (There are many more!)

1. Singleton Pattern

Purpose: Ensures that a class has only one instance and provides a global point of access to that instance.

Example Use Case: Managing shared resources like configuration settings where only one instance should exist across the application.

2. Observer Pattern

Purpose: Allows one object (the "subject") to notify other objects (the "observers") about state changes, creating a one-to-many relationship.

Example Use Case: Used for situations where multiple parts of the system need to respond to changes in another part, such as updating UI elements when a model changes.

3. Factory Method Pattern

Purpose: Defines an interface for creating objects, allowing subclasses to alter the type of objects that will be created.

Example Use Case: Creating instances of different types of objects dynamically, for example, when deciding which type of user (admin, guest, etc.) to create based on conditions.

4. Decorator Pattern

Purpose: Adds new functionality to an object dynamically without altering its structure.

Example Use Case: Extending the functionality of existing objects, like adding additional presentation logic to a model object, often used for enhancing UI components.

5. Service Object Pattern

Purpose: Encapsulates business logic into separate classes, improving maintainability and keeping controllers thin.

Example Use Case: Handling complex logic or operations that do not belong in controllers or models, such as processing payments.

Design patterns are like trusty blueprints for tackling common challenges in software design, but they’re not always the right tool for the job. It’s easy to get excited and want to use a design pattern everywhere—I’ve definitely been guilty of this myself 🙋‍♂️

However, overusing them can add unnecessary complexity, so it’s important to step back and consider whether a pattern truly fits your specific problem. When applied thoughtfully, design patterns can be game-changers, helping you build code that’s maintainable, scalable, and clear.


Wapping up

Design principles and patterns are essential tools in a developer's toolkit 🛠️, offering both guidance and practical solutions for creating maintainable, adaptable systems. They’re not always applicable, but when used strategically and thoughtfully, they can simplify complexity and enhance your code's structure.

Always remember to assess your context 🔍 before applying any principle or pattern—what works for one problem might not suit another. With a balanced approach, you can leverage these concepts to build systems that stand the test of time, making both your life as a developer and your codebase far more enjoyable to work with! 😊

Top comments (0)