Open Close Principle (OCP)
Tags: go, solidprinciples
Published: 2023-09-08 04:30:55

Robert C. Martin menjelaskan:
> Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.

Open for extension artinya kode dapat dapat diperluas sesuai kebutuhan. Closed for modification artinya kode tidak dapat dirubah. Dengan kata lain, kita dapat menambah fungsionalitas pada sistem tanpa mengubah kode yang sudah ada.

Dave Cheney menjelaskan:
> The Open / Closed Principle encourages you to compose simple types into more complex ones using embedding.

Agar lebih jelasnya, perhatikan kode yang ada pada contoh SRP sebelumnya:

```go
package main

import (
    "fmt"
)

// User adalah tipe data yang mewakili informasi pengguna.
type User struct {
    ID       int
    Username string
    Email    string
}

// UserRepository bertanggung jawab untuk berinteraksi dengan basis data pengguna.
type UserRepository struct{}

func (us *UserRepository) GetUserByUsername(username string) (User, error) {
    // Logika untuk mengambil pengguna berdasarkan username
    return User{}, nil
}

func (us *UserRepository) Save() error {
    // Logika untuk menyimpan ke basis data
    return nil
}

// UserValidator bertanggung jawab untuk memvalidasi data pengguna.
type UserValidator struct{}

func (uv *UserValidator) Validate() error {
    // Logika validasi pengguna seperti validasi email, kata sandi, dsb.
    return nil
}

// UserService adalah layanan yang berfungsi sebagai penghubung antara klien dan data pengguna.
type UserService struct {
    Repository UserRepository
    Validator  UserValidator
}

// CreateUser membuat pengguna baru dengan menggunakan Repository dan Validator.
func (us *UserService) CreateUser(user User) error {
    // Validasi pengguna menggunakan Validator
    if err := us.Validator.Validate(); err != nil {
        return err
    }

    // Simpan pengguna ke basis data menggunakan Repository
    if err := us.Repository.Save(); err != nil {
        return err
    }

    return nil
}

func (us *UserService) GetUserByID(userID int) (User, error) {
	// GetUserByID mengambil pengguna berdasarkan ID dari Repository.
    return us.Repository.GetUserByID(userID)
}

func main() {
    // Inisialisasi layanan pengguna
    userService := UserService{
        Repository: UserRepository{}, // Contoh sederhana, seharusnya menggunakan koneksi basis data nyata.
        Validator:  UserValidator{},  // Contoh sederhana, seharusnya ada validasi yang lebih lengkap.
    }

    // Membuat pengguna baru
    newUser := User{ID: 1, Username: "john_doe", Email: "john@example.com"}
    err := userService.CreateUser(newUser)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }

    // Mengambil pengguna berdasarkan ID
    user, err := userService.GetUserByID(1)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }

    fmt.Println("User:", user)
}
```

Kita asumsikan kode di atas menggunakan basis data MySQL. Jika ada perubahan untuk mengganti basis data menjadi Oracle akan ada beberapa kemungkinan.

1. Mengubah seluruh kode agar dapat berinteraksi dengan basis data Oracle
2. Menambah method `GetUserByIDAtOracle` dan `SaveAtOracle`

Agar dapat menerapkan OCP, kode harus dibuat seperti berikut:

```go
package main

import (
	"fmt"
)

// User adalah tipe data yang mewakili informasi pengguna.
type User struct {
	ID       int
	Username string
	Email    string
}

type UserRepository interface {
	GetUserByUsername(username string) (User, error)
	Save() error
}

// UserRepository bertanggung jawab untuk berinteraksi dengan basis data pengguna.
type UserMySQLRepository struct{}

func (r *UserMySQLRepository) GetUserByUsername(username string) (User, error) {
	// Logika untuk mengambil pengguna berdasarkan username
	return User{}, nil
}

func (r *UserMySQLRepository) Save() error {
	// Logika untuk menyimpan ke basis data
	return nil
}

// UserValidator bertanggung jawab untuk memvalidasi data pengguna.
type UserValidator struct{}

func (uv *UserValidator) Validate() error {
	// Logika validasi pengguna seperti validasi email, kata sandi, dsb.
	return nil
}

// UserService adalah layanan yang berfungsi sebagai penghubung antara klien dan data pengguna.
type UserService struct {
	Repository UserRepository
	Validator  UserValidator
}

// CreateUser membuat pengguna baru dengan menggunakan Repository dan Validator.
func (us *UserService) CreateUser(user User) error {
	// Validasi pengguna menggunakan Validator
	if err := us.Validator.Validate(); err != nil {
		return err
	}

	// Simpan pengguna ke basis data menggunakan Repository
	if err := us.Repository.Save(); err != nil {
		return err
	}

	return nil
}

// GetUserByUsername mengambil pengguna berdasarkan username.
func (us *UserService) GetUserByUsername(username string) (User, error) {
	return us.Repository.GetUserByUsername(username)
}

func main() {
	// Inisialisasi layanan pengguna
	userService := UserService{
		Repository: &UserMySQLRepository{}, // Contoh sederhana, seharusnya menggunakan koneksi basis data nyata.
		Validator:  UserValidator{},        // Contoh sederhana, seharusnya ada validasi yang lebih lengkap.
	}

	// Membuat pengguna baru
	newUser := User{ID: 1, Username: "john_doe", Email: "john@example.com"}
	err := userService.CreateUser(newUser)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	// Mengambil pengguna berdasarkan username
	user, err := userService.GetUserByUsername("myUser")
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	fmt.Println("User:", user)
}
```

Kode di atas ditambahkan interface `UserRepository`, lalu struct `UserMySQLRepository` akan mengimplementasikan method yang ada pada interface tersebut. Kode `UserMySQLRepository` berisi kode spesifik untuk berinteraksi dengan basis data MySQL. Saat menginisialisasi Jika akan mengubah basis data menjadi Oracle, maka kode akan menjadi seperti berikut:

```go
package main

import (
	"fmt"
)

// User adalah tipe data yang mewakili informasi pengguna.
type User struct {
	ID       int
	Username string
	Email    string
}

type UserRepository interface {
	GetUserByUsername(username string) (User, error)
	Save() error
}

// UserRepository bertanggung jawab untuk berinteraksi dengan basis data pengguna.
type UserMySQLRepository struct{}

func (r *UserMySQLRepository) GetUserByUsername(username string) (User, error) {
	// Logika untuk mengambil pengguna berdasarkan username
	return User{}, nil
}

func (r *UserMySQLRepository) Save() error {
	// Logika untuk menyimpan ke basis data
	return nil
}

type UserOracleRepository struct{}

func (r *UserOracleRepository) GetUserByUsername(username string) (User, error) {
	// Logika untuk mengambil pengguna berdasarkan username
	return User{}, nil
}

func (r *UserOracleRepository) Save() error {
	// Logika untuk menyimpan ke basis data
	return nil
}


// UserValidator bertanggung jawab untuk memvalidasi data pengguna.
type UserValidator struct{}

func (uv *UserValidator) Validate() error {
	// Logika validasi pengguna seperti validasi email, kata sandi, dsb.
	return nil
}

// UserService adalah layanan yang berfungsi sebagai penghubung antara klien dan data pengguna.
type UserService struct {
	Repository UserRepository
	Validator  UserValidator
}

// CreateUser membuat pengguna baru dengan menggunakan Repository dan Validator.
func (us *UserService) CreateUser(user User) error {
	// Validasi pengguna menggunakan Validator
	if err := us.Validator.Validate(); err != nil {
		return err
	}

	// Simpan pengguna ke basis data menggunakan Repository
	if err := us.Repository.Save(); err != nil {
		return err
	}

	return nil
}

// GetUserByID mengambil pengguna berdasarkan ID dari Repository.
func (us *UserService) GetUserByUsername(username string) (User, error) {
	return us.Repository.GetUserByUsername(username)
}

func main() {
	// Inisialisasi layanan pengguna
	userService := UserService{
		Repository: &UserOracleRepository{}, // Contoh sederhana, seharusnya menggunakan koneksi basis data nyata.
		Validator:  UserValidator{},        // Contoh sederhana, seharusnya ada validasi yang lebih lengkap.
	}

	// Membuat pengguna baru
	newUser := User{ID: 1, Username: "john_doe", Email: "john@example.com"}
	err := userService.CreateUser(newUser)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	// Mengambil pengguna berdasarkan username
	user, err := userService.GetUserByUsername("myUser")
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	fmt.Println("User:", user)
}
```

Kode di atas ditambahkan struct bernama `UserOracleRepository`, yang didalamnya berisi kode spesifik untuk berinteraksi dengan basis data oracle. Struct `UserOracleRepository` dimasukan saat inisialiasi `UserService`.

Dengan OCP kita dapat menambah fungsionalitas tanpa mengubah kode yang sudah ada.

## Referensi
- https://blog.cleancoder.com/uncle-bob/2013/03/08/AnOpenAndClosedCase.html
- https://blog.cleancoder.com/uncle-bob/2014/05/12/TheOpenClosedPrinciple.html
- https://en.wikipedia.org/wiki/Open–closed_principle
- https://dave.cheney.net/2016/08/20/solid-go-design