+9

Tập tành SOLID với Golang: Open Closed Principle

Lời mở đầu:

Để trở thành một Senior Developer, chắc hẳn mỗi chúng ta sẽ cần nắm vững một vài Principle. Đối với tôi, một thứ giúp tôi nâng tầm được bản thân trong năm qua đó là việc học và áp dụng SOLID Principle với ngôn ngữ Golang. Chính vì thế, hôm nay tôi muốn chia sẻ những gì mà mình đã tìm hiểu và thực hành Principle này với hi vọng sẽ giúp mọi người có thêm một góc nhìn về SOLID.

Hôm nay chúng ta sẽ cùng nhau tìm hiểu về chữ O trong SOLID Principle. Trong công việc hằng ngày, đôi khi chúng ta đã vô tình lướt qua nhau, đôi khi mình đã áp dụng Open/Closed Principle nhưng chúng ta lại không hề hay biết. Đó chính là design parttern Strategy. Tuy nhiên, Strategy cũng chỉ là một thể hiện của OCP (Open/Closed Principle).

Không để mọi người phải đợi lâu nữa. Chúng ta sẽ đi vào những luận điểm để hiểu rõ hơn về nguyên tắc này nhé:

Khi chúng ta không tuân thủ Open/Closed Principle:

Để hiểu về định nghĩa cụ thể của OCP các bạn có thể truy cập vào đây để tìm hiểu chi tiết. Chắc hẳn ở đây chúng ta đều rất đau đầu mỗi khi nhận yêu cầu mới của khách hàng hoặc những CR (change request) ảnh hưởng rất nhiều đến hệ thống hiện tại. Vậy làm cách nào để có thể thêm tính năng mới mà lại không ảnh hưởng hoặc không cần sửa đổi hoặc có cách nào để mở rộng nó mà không thay đổi chính nó không? Nghe có vẻ rất mâu thuẫn, nhưng hãy cùng xem ví dụ dưới đây để có cái nhìn về giả thuyết này nhé:

import (
	"net/http"

	"github.com/ahmetb/go-linq"
	"github.com/gin-gonic/gin"
)

type PermissionChecker struct {
	//
	// some fields
	//
}

func (c *PermissionChecker) HasPermission(ctx *gin.Context, name string) bool {
	var permissions []string
	switch ctx.GetString("authType") {
	case "jwt":
		permissions = c.extractPermissionsFromJwt(ctx.Request.Header)
	case "basic":
		permissions = c.getPermissionsForBasicAuth(ctx.Request.Header)
	case "applicationKey":
		permissions = c.getPermissionsForApplicationKey(ctx.Query("applicationKey"))
	}

	var result []string
	linq.From(permissions).
		Where(func(permission interface{}) bool {
			return permission.(string) == name
		}).ToSlice(&result)

	return len(result) > 0
}

func (c *PermissionChecker) getPermissionsForApplicationKey(key string) []string {
	var result []string
	//
	// extract JWT from the request header
	//
	return result
}

func (c *PermissionChecker) getPermissionsForBasicAuth(h http.Header) []string {
	var result []string
	//
	// extract JWT from the request header
	//
	return result
}

func (c *PermissionChecker) extractPermissionsFromJwt(h http.Header) []string {
	var result []string
	//
	// extract JWT from the request header
	//
	return result
}

Các bạn sẽ thấy PermissionChecker Nó kiểm tra xem có quyền truy cập một số tài nguyên, phụ thuộc vào context của application do package Gin cung cấp. Ở đây, chúng ta có phương thức HasPermission, nó sẽ kiểm tra quyền với tên cụ thể được liên kết với dữ liệu bên trong context. Việc truy xuất quyền từ context sẽ có thể khác nhau tuỳ thuộc vào việc người dùng sẽ lựa chọn phương thức xác thực gì (có thể sẽ là JWT Token, Basic Auth hay API key), và trong chương trình của chúng ta cần xử lý tất cả những phần đó để có thể trích xuất được dữ liệu do client gửi tới. Nếu các bạn chú ý tới SRP (Single Responsibility Principle), PermissionChecker sẽ chịu trách nhiệm quyết định xem có được sự cho phép truy cập hay không và nó không ảnh hưởng gì tới context.

Giả sử chúng ta muốn mở rộng logic của uỷ quyền và thêm một số luồng mới, chẳng hạn như lưu trữ dữ liệu người dùng trong session hoặc sử dụng Digest Authorization. Trong trường hợp này, chúng ta sẽ cần phải sửa logic bên trong PermissionChecker. Việc sửa đổi logic đó có thể xảy ra một vài vấn đề như sau:

  • PermissionChecker giữ logic được xử lý ban đầu ở một nơi nào đó khác.
  • Bất kỳ sự điều chỉnh nào của logic ủy quyền , nơi mà có thể ở một module khác, đều yêu cầu sự sửa đổi trong PermissionChecker.
  • Để thêm một cách trích xuất ủy quyền mới, chúng ta luôn cần sửa đổi PermissionChecker.
  • Logic bên trong PermissionChecker chắc chắn sẽ thay nổi với một luồng ủy quyền mới.
  • Unit testing cho PermissionChecker sẽ bao gồm nhiều chi tiết kỹ thuật về trích xuất khác nhau của permission.

Và một lần nữa chúng ta cần phải cấu trúc lại. Bây giờ chúng ta sẽ cùng nhau học cách tuân thủ OCP

Khi chúng ta tuân thủ Open/Closed Principle:

OCP chỉ đơn giản chỉ ra cho chúng ta rằng cấu trúc phần mềm nên được mở để mở rộng nhưng đóng để sửa đổi. Với suy nghĩ như vậy, chúng ta có thể đưa ra một số hướng có thể cho code của chúng ta tuân thủ theo OCP. Đoạn mã code đó phải cung cấp một thứ gì đó để cho phép tiện ích mở rộng tới từ bên ngoài. Trong OOP, chúng được hỗ trợ mở rộng bằng cách sử dụng các cách triển khai khác nhau cho cùng một interface. Nói cách khác là chúng ta sử dụng Polymorphism.

type PermissionProvider interface {
	Type() string
	GetPermissions(ctx *gin.Context) []string
}

type PermissionChecker struct {
	providers []PermissionProvider
	//
	// some fields
	//
}

func (c *PermissionChecker) HasPermission(ctx *gin.Context, name string) bool {
	var permissions []string
	for _, provider := range c.providers {
		if ctx.GetString("authType") != provider.Type() {
			continue
		}

		permissions = provider.GetPermissions(ctx)
		break
	}

	var result []string
	linq.From(permissions).
		Where(func(permission interface{}) bool {
			return permission.(string) == name
		}).ToSlice(&result)

	return len(result) > 0
}

Trong đoạn code mẫu bên trên, chúng ta sẽ thấy xuất hiện thêm một interface PermissionProvider, nó sẽ giúp chúng ta tuân thủ OCP. Việc sửa lại PermissionChecker không ẩn đi phần thông tin trích xuất từ context. Thay vào đó, chúng ta tạo ra một interface mới. Cấu trúc này đại diện cho từng trích xuất logic khác nhau, ví dụ như: JWTPermissionProvider hoặc APIKeyPermissionProvider hay thậm chí là AuthBasicPermissionProvider. Mục đích chính của chúng ta, để mở rộng PermisionChecker mà không cần sửa đổi nó và như các bạn thấy đó bây giờ nó đã đáp ứng được yêu cầu mà chúng ta kỳ vọng.

Một ví dụ khác có thể giúp các bạn loại bỏ phần bên trong của PermissionProvider từ PermissionChecker.

type PermissionProvider interface {
	Type() string
	GetPermissions(ctx *gin.Context) []string
}

type PermissionChecker struct {
	//
	// some fields
	//
}

func (c *PermissionChecker) HasPermission(ctx *gin.Context, provider PermissionProvider, name string) bool {
	permissions := provider.GetPermissions(ctx)

	var result []string
	linq.From(permissions).
		Where(func(permission interface{}) bool {
			return permission.(string) == name
		}).ToSlice(&result)

	return len(result) > 0
}

Cá nhân tôi thích cách tiếp cận đầu tiên hơn. Tuy nhiên, cách tiếp cận thứ hai cũng hoàn toàn không vấn đề gì. Phụ thuộc vào từng bài toán cụ thể, các bạn có thể lựa chọn một cách tiếp cận phù hợp.

Tóm lại:

Open/Closed Principle là nguyên tắc thứ hai trong SOLID. Nó muốn gửi đến nhưng người lập trình viên chúng ta một thông điệp, hãy luôn luôn mở rộng cấu trúc code mà không thực sự sửa đổi chúng. Rất trừu tượng phải không các bạn, hi vọng với bài chia sẻ này của mình, các bạn sẽ có thêm một cái nhìn về các nguyên tắc để áp dụng và thực hành nhuần nhuyễn trong dự án của mình. Cảm ơn mọi người đã đọc bài của mình.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí