isfanazha / solid-principle-go

Solid Principle with Go

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

SOLID Principle

The SOLID principle are a set of five designs guidelines that help developers create more maintainable, flexible, and scalable software.

Here's an overview of SOLID principles:

1. Single Responsibility Principle (SRP)

A class should have one, and only one, reason to change.

Example Case:
  • Before Implementation
type OrderServiceNotSOP struct {
    Orders []entity.Order
}

func (o *OrderServiceNotSOP) PlaceOrder(order entity.Order) {
    o.Orders = append(o.Orders, order)
}

func (o *OrderServiceNotSOP) GenerateInvoice(order entity.Order) {
    fmt.Printf("Generate Invoice For Order ID: %s, User ID: %d and Total Amount: %f", order.OrderID, order.UserID, order.TotalAmount)
}

OrderServiceNotSOP has two responsibilities: handling orders and generating invoices.

  • After Implementation
type OrderServiceSOP struct {
    Orders []entity.Order
}

func (o *OrderServiceSOP) PlaceOrder(order entity.Order) {
    o.Orders = append(o.Orders, order)
}

type InvoiceGeneratorSOP struct{}

func (i *InvoiceGeneratorSOP) GenerateInvoice(order entity.Order) {
    fmt.Printf("Generate Invoice For Order ID: %s, User ID: %d and Total Amount: %f", order.OrderID, order.UserID, order.TotalAmount)
}

Now, the OrderServiceSOP is solely responsible for managing orders, while the InvoiceGeneratorSOP takes care of generating invoices.

2. Open/Closed Principle (OCP)

Software entities should be open for extension but closed for modification.

Example Case:
  • Before Implementation
func ApplyDiscount(product entity.Product, isVIP bool) float64 {
    if isVIP {
        return product.Price * 0.20 // 20% discount for VIP
    }

    return product.Price * 0.10 // 10% discount otherwise
}

Imagine you have a simple discount system that applies a 10% discount to all products. Later, you want to add a special 20% discount for VIP customers.

  • After Implementation
type DiscountRule interface {
    ApplyDiscount(product entity.Product) float64
}

type StandardDiscount struct{}

func (sd StandardDiscount) ApplyDiscount(product entity.Product) float64 {
    return product.Price * 0.10 // 10% discount
}

type VIPDiscount struct{}

func (vd VIPDiscount) ApplyDiscount(product entity.Product) float64 {
    return product.Price * 0.20 // 20% discount for VIP
}

func CalculatePrice(product entity.Product, rule DiscountRule) float64 {
    return product.Price - rule.ApplyDiscount(product)
}

Now, when you want to add a new discount rule, you simply create a new type that implements the DiscountRule interface. The existing code doesn't have to change, adhering to OCP.

3. Liskov Substitution Principle (LSP)

Subtypes must be substitutable for their base types without altering the correctness of the program.

Example Case:
type ProductNotLSP interface {
    ApplyDiscount()
}

type DiscountedProductNotLSP struct {
    Name  string
    Price float64
}

func (dp *DiscountedProductNotLSP) ApplyDiscount() {
    dp.Price *= 0.90 // 10% discount
}

type RegularProductNotLSP struct {
    Name  string
    Price float64
}

func (rp *RegularProductNotLSP) ApplyDiscount() {
    // No discount for regular products
}

func ApplyDiscountsNotLSP(products []ProductNotLSP) {
    for _, product := range products {
        product.ApplyDiscount()
    }
}

The issue here is that this design doesn't make it clear which products are eligible for a discount, leading to confusion and potential errors. If a new category is added, the discount logic might need to change in many places.

  • After Implementation
type Discountable interface {
    ApplyDiscount()
}

type ProductLSP struct {
    Name  string
    Price float64
}

type DiscountedProductLSP struct {
    ProductLSP
}

func (dp *DiscountedProductLSP) ApplyDiscount() {
    dp.Price *= 0.90 // 10% discount
}

func ApplyDiscounts(products []Discountable) {
    for _, product := range products {
        product.ApplyDiscount()
    }
}

RegularProductLSP is excluded from the discountable behavior by not implementing the Discountable interface. This design clearly separates products that can have discounts from those that cannot, reducing confusion and making the code easier to maintain and extend. Let's breakdown the code based on the definition:

  • Base types (Interface): Discountableinterface.
  • Struct (Possible Subtype): ProductLSP struct.
  • Embedded Struct (Subtype): The DiscountedProductLSP struct embeds ProductLSP. This effectively means DiscountedProductLSP inherits properties from ProductLSP and then adds its own functionality, specifically the ApplyDiscount() method, making it adhere to the Discountable interface. Hence, DiscountedProductLSP is a subtype of the Discountable interface.
Liskov Substitution Principle in Action:

Given the LSP, you should be able to replace an instance of the Discountable interface (base type) with an instance of DiscountedProductLSP (subtype) without affecting the program's correctness.

func ApplyDiscounts(products []Discountable) {
    for _, product := range products {
        product.ApplyDiscount()
    }
}

You can pass a slice of Discountable items, and as long as every item in that slice adheres to the Discountable interface (i.e., it has an ApplyDiscount() method), the function will work correctly. Here, DiscountedProductLSP can be used as a substitutable type for Discountable because it implements the required method.

This means that, if in the future you introduce another product type with a different discount strategy (say, a ClearanceProduct), as long as it implements the Discountable interface, you can pass it to the ApplyDiscounts function without any issues. This is the essence of the Liskov Substitution Principle.

4. Interface Segregation Principle (ISP)

No client should be forced to depend on interfaces it does not use.

Example Case:
  • Before Implementation
type UserManagerNotISP interface {
    Register(username, password string) error
    Login(username, password string) (*entity.User, error)
    UpdateProfile(userID int, profile entity.Profile) error
    DeleteAccount(userID int) error
    GetUserOrders(userID int) ([]entity.Order, error)
}

UserManagerNotISP interface handles everything related to a user - registration, login, profile updating, account deletion, and fetching orders. If a component is only interested in handling the registration, it still has to know about other methods, which is unnecessary.

  • After Implementation
type UserRegistrar interface {
    Register(username, password string) error
}

type UserAuthenticator interface {
    Login(username, password string) (*entity.User, error)
}

type UserProfileManager interface {
    UpdateProfile(userID int, profile entity.Profile) error
}

type UserAccountManager interface {
    DeleteAccount(userID int) error
}

type UserOrderViewer interface {
    GetUserOrders(userID int) ([]entity.Order, error)
}

With this breakdown, every component or module in the system can depend solely on the aspects it truly requires, which makes the application cleaner, more maintainable, and potentially less error-prone.

5. Dependency Inversion Principle (DIP)

High-level modules should not depend on low-level modules. Both should depend on abstractions.

Example Case:
  • Before Implementation
type EmailNotifierNotDIP struct {
    // some fields for email configuration
}

func (en *EmailNotifierNotDIP) SendEmail(to string, subject string, body string) {
    // logic to send an email
}

type OrderProcessorNotDIP struct {
    notifier EmailNotifierNotDIP
}

func (op *OrderProcessorNotDIP) CompletePurchase(userEmail string, product string) {
    // logic to complete purchase
    op.notifier.SendEmail(userEmail, "Purchase Completed", "Thank you for purchasing "+product)
}

The OrderProcessor is tightly coupled with the EmailNotifier. If we decide to notify the user through another medium, like SMS or push notifications, this design would require significant changes to OrderProcessor.

  • After Implementation
type Notifier interface {
    Notify(to string, subject string, message string)
}

type EmailNotifier struct {
    // some fields for email configuration
}

func (en *EmailNotifier) Notify(to string, subject string, message string) {
    // logic to send an email
}

type SMSNotifier struct {
    // some fields for SMS configuration
}

func (sn *SMSNotifier) Notify(to string, subject string, message string) {
    // logic to send an SMS (Note: In reality, SMS might not have a "subject", this is just for illustrative purposes)
}

type OrderProcessor struct {
    notifier Notifier // depends on the interface, not concrete implementation
}

func (op *OrderProcessor) CompletePurchase(userContact string, product string) {
    // logic to complete purchase
    op.notifier.Notify(userContact, "Purchase Completed", "Thank you for purchasing "+product)
}

The OrderProcessor depends on the abstraction (Notifier) and not on the concrete implementation (EmailNotifier or SMSNotifier). If we decide to change our notification method, or even introduce a new one, we can do so easily without altering OrderProcessor — we simply introduce a new implementation of the Notifier interface.

About

Solid Principle with Go


Languages

Language:Go 100.0%