Solid-笔记

132 阅读5分钟

SOLID

solid原则核心是描述软件的中层结构目标,致力于实现:

  1. 是软件更容易被扩展。
  2. 使软件更容易被理解。
  3. 使软件更容易被复用。

开闭原则

O 来自于 “Open–closed principle(开放-关闭原则)” 的首字母,它认为:“类应该对扩展开放,对修改封闭。”这是一个从字面上很难理解的原则,它同样有着另外一种说法:“你应该可以在不修改某个类的前提下,扩展它的行为。” 一个最简单的例子,Python中的sorted函数。

>>> l = [5, 3, 2, 4, 1]
>>> sorted(l)
[1, 2, 3, 4, 5]

现在,假如我们想改变 sorted 函数的排序逻辑。比如,让它使用所有元素对 3 取余后的结果来排序。我们是不是需要去修改 sorted 函数的源码?当然不用,只需要在调用 sort 函数时,传入自定义的排序函数 key 参数就行了。

# 按照元素对 3 的余数排序,能被 3 整除的 9 排在了最前面,随后是 1 和 8
>>> sorted(l, key=lambda i: i % 3)
[9, 1, 8]

通过上面的例子,我们可以认为: sorted 函数是一个符合“开放-关闭原则”的绝佳例子,因为它:

  • 对扩展开放:你可以通过传入自定义 key 函数来扩展它的行为
  • 对修改关闭:你无需修改 sort 函数本身 mp.weixin.qq.com/s/801_toG7h…

案例

案例一

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
}

改造一:

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
}

改造二:

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
}

案例二

通过type定义func来实现

type DataReader func(source string) ([]byte, error)

func ReadFromFile(fileName string) ([]byte, error) {
	data, err := ioutil.ReadFile(fileName)
	if err != nil {
		return nil, err
	}

	return data, nil
}

func ReadFromLink(link string) ([]byte, error) {
	resp, err := http.Get(link)
	if err != nil {
		return nil, err
	}

	data, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	return data, nil
}

func GetCities(reader DataReader, source string) ([]City, error) {
	data, err := reader(source)
	if err != nil {
		return nil, err
	}

	var cities []City
	err = yaml.Unmarshal(data, &cities)
	if err != nil {
		return nil, err
	}

	return cities, nil
}

levelup.gitconnected.com/practical-s…

案例三

策略模式,也符合开闭原则

package strategy

import "fmt"

type Payment struct {
	context  *PaymentContext
	strategy PaymentStrategy
}

type PaymentContext struct {
	Name, CardID string
	Money        int
}

func NewPayment(name, cardid string, money int, strategy PaymentStrategy) *Payment {
	return &Payment{
		context: &PaymentContext{
			Name:   name,
			CardID: cardid,
			Money:  money,
		},
		strategy: strategy,
	}
}

func (p *Payment) Pay() {
	p.strategy.Pay(p.context)
}

type PaymentStrategy interface {
	Pay(*PaymentContext)
}

type Cash struct{}

func (*Cash) Pay(ctx *PaymentContext) {
	fmt.Printf("Pay $%d to %s by cash", ctx.Money, ctx.Name)
}

type Bank struct{}

func (*Bank) Pay(ctx *PaymentContext) {
	fmt.Printf("Pay $%d to %s by bank account %s", ctx.Money, ctx.Name, ctx.CardID)

}

单一职责原则

案例一

行为逻辑里有存入DB以及发送Email两步,拆成3部分。1)负责存入DB。2)负责发送Email。3)负责存入DB以及发送Email的逻辑。

type EmailService struct {
	db           *gorm.DB
	smtpHost     string
	smtpPassword string
	smtpPort     int
}

func NewEmailService(db *gorm.DB, smtpHost string, smtpPassword string, smtpPort int) *EmailService {
	return &EmailService{
		db:           db,
		smtpHost:     smtpHost,
		smtpPassword: smtpPassword,
		smtpPort:     smtpPort,
	}
}

func (s *EmailService) Send(from string, to string, subject string, message string) error {
	email := EmailGorm{
		From:    from,
		To:      to,
		Subject: subject,
		Message: message,
	}

	err := s.db.Create(&email).Error
	if err != nil {
		log.Println(err)
		return err
	}
	
	auth := smtp.PlainAuth("", from, s.smtpPassword, s.smtpHost)
	
	server := fmt.Sprintf("%s:%d", s.smtpHost, s.smtpPort)
	
	err = smtp.SendMail(server, auth, from, []string{to}, []byte(message))
	if err != nil {
		log.Println(err)
		return err
	}

	return nil
}
type EmailGorm struct {
	gorm.Model
	From    string
	To      string
	Subject string
	Message string
}

type EmailRepository interface {
	Save(from string, to string, subject string, message string) error
}

type EmailDBRepository struct {
	db *gorm.DB
}

func NewEmailRepository(db *gorm.DB) EmailRepository {
	return &EmailDBRepository{
		db: db,
	}
}

func (r *EmailDBRepository) Save(from string, to string, subject string, message string) error {
	email := EmailGorm{
		From:    from,
		To:      to,
		Subject: subject,
		Message: message,
	}

	err := r.db.Create(&email).Error
	if err != nil {
		log.Println(err)
		return err
	}

	return nil
}

type EmailSender interface {
	Send(from string, to string, subject string, message string) error
}

type EmailSMTPSender struct {
	smtpHost     string
	smtpPassword string
	smtpPort     int
}

func NewEmailSender(smtpHost string, smtpPassword string, smtpPort int) EmailSender {
	return &EmailSMTPSender{
		smtpHost:     smtpHost,
		smtpPassword: smtpPassword,
		smtpPort:     smtpPort,
	}
}

func (s *EmailSMTPSender) Send(from string, to string, subject string, message string) error {
	auth := smtp.PlainAuth("", from, s.smtpPassword, s.smtpHost)

	server := fmt.Sprintf("%s:%d", s.smtpHost, s.smtpPort)

	err := smtp.SendMail(server, auth, from, []string{to}, []byte(message))
	if err != nil {
		log.Println(err)
		return err
	}

	return nil
}

type EmailService struct {
	repository EmailRepository
	sender     EmailSender
}

func NewEmailService(repository EmailRepository, sender EmailSender) *EmailService {
	return &EmailService{
		repository: repository,
		sender:     sender,
	}
}

func (s *EmailService) Send(from string, to string, subject string, message string) error {
	err := s.repository.Save(from, to, subject, message)
	if err != nil {
		return err
	}

	return s.sender.Send(from, to, subject, message)
}

案例二

拆分function

import "github.com/dgrijalva/jwt-go"

func extractUsername(header http.Header) string {
	raw := header.Get("Authorization")
	parser := &jwt.Parser{}
	token, _, err := parser.ParseUnverified(raw, jwt.MapClaims{})
	if err != nil {
		return ""
	}

	claims, ok := token.Claims.(jwt.MapClaims)
	if !ok {
		return ""
	}

	return claims["username"].(string)
}
func extractUsername(header http.Header) string {
	raw := extractRawToken(header)
	claims := extractClaims(raw)
	if claims == nil {
		return ""
	}
	
	return claims["username"].(string)
}

func extractRawToken(header http.Header) string {
	return header.Get("Authorization")
}

func extractClaims(raw string) jwt.MapClaims {
	parser := &jwt.Parser{}
	token, _, err := parser.ParseUnverified(raw, jwt.MapClaims{})
	if err != nil {
		return nil
	}

	claims, ok := token.Claims.(jwt.MapClaims)
	if !ok {
		return nil
	}
	
	return claims
}

levelup.gitconnected.com/practical-s…

接口隔离原则

案例一

将接口拆分为更小粒度,调用方使用到什么方法就用什么interface。

ype User interface {
	AddToShoppingCart(product Product)
	IsLoggedIn() bool
	Pay(money Money) error
	HasPremium() bool
	HasDiscountFor(product Product) bool
	//
	// some additional methods
	//
}

type Guest struct {
	cart ShoppingCart
	//
	// some additional fields
	//
}

func (g *Guest) AddToShoppingCart(product Product) {
	g.cart.Add(product)
}

func (g *Guest) IsLoggedIn() bool {
	return false
}

func (g *Guest) Pay(Money) error {
	return errors.New("user is not logged in")
}

func (g *Guest) HasPremium() bool {
	return false
}

func (g *Guest) HasDiscountFor(Product) bool {
	return false
}

type NormalCustomer struct {
	cart   ShoppingCart
	wallet Wallet
	//
	// some additional fields
	//
}

func (c *NormalCustomer) AddToShoppingCart(product Product) {
	c.cart.Add(product)
}

func (c *NormalCustomer) IsLoggedIn() bool {
	return true
}

func (c *NormalCustomer) Pay(money Money) error {
	return c.wallet.Deduct(money)
}

func (c *NormalCustomer) HasPremium() bool {
	return false
}

func (c *NormalCustomer) HasDiscountFor(Product) bool {
	return false
}

type PremiumCustomer struct {
	cart     ShoppingCart
	wallet   Wallet
	policies []DiscountPolicy
	//
	// some additional fields
	//
}

func (c *PremiumCustomer) AddToShoppingCart(product Product) {
	c.cart.Add(product)
}

func (c *PremiumCustomer) IsLoggedIn() bool {
	return true
}

func (c *PremiumCustomer) Pay(money Money) error {
	return c.wallet.Deduct(money)
}

func (c *PremiumCustomer) HasPremium() bool {
	return true
}

func (c *PremiumCustomer) HasDiscountFor(product Product) bool {
	for _, p := range c.policies {
		if p.IsApplicableFor(c, product) {
			return true
		}
	}
	
	return false
}

type UserService struct {
	//
	// some fields
	//
}

func (u *UserService) Checkout(ctx context.Context, user User, product Product) error {
	if !user.IsLoggedIn() {
		return errors.New("user is not logged in")	
	}
	
	var money Money
	//
	// some calculation
	//
	if user.HasDiscountFor(product) {
		//
		// apply discount
		//
	}
	
	return user.Pay(money)
}
type User interface {
	AddToShoppingCart(product Product)
	//
	// some additional methods
	//
}

type LoggedInUser interface {
	User
	Pay(money Money) error
	//
	// some additional methods
	//
}

type PremiumUser interface {
	LoggedInUser
	HasDiscountFor(product Product) bool
	//
	// some additional methods
	//
}

type Guest struct {
	cart ShoppingCart
	//
	// some additional fields
	//
}

func (g *Guest) AddToShoppingCart(product Product) {
	g.cart.Add(product)
}

type NormalCustomer struct {
	cart   ShoppingCart
	wallet Wallet
	//
	// some additional fields
	//
}

func (c *NormalCustomer) AddToShoppingCart(product Product) {
	c.cart.Add(product)
}

func (c *NormalCustomer) Pay(money Money) error {
	return c.wallet.Deduct(money)
}

type PremiumCustomer struct {
	cart     ShoppingCart
	wallet   Wallet
	policies []DiscountPolicy
	//
	// some additional fields
	//
}

func (c *PremiumCustomer) AddToShoppingCart(product Product) {
	c.cart.Add(product)
}

func (c *PremiumCustomer) Pay(money Money) error {
	return c.wallet.Deduct(money)
}

func (c *PremiumCustomer) HasDiscountFor(product Product) bool {
	for _, p := range c.policies {
		if p.IsApplicableFor(c, product) {
			return true
		}
	}

	return false
}

type UserService struct {
	//
	// some fields
	//
}

func (u *UserService) Checkout(ctx context.Context, user User, product Product) error {
	loggedIn, ok := user.(LoggedInUser)
	if !ok {
		return errors.New("user is not logged in")
	}

	var money Money
	//
	// some calculation
	//
	if premium, ok := loggedIn.(PremiumUser); ok && premium.HasDiscountFor(product)  {
		//
		// apply discount
		//
	}

	return loggedIn.Pay(money)
}

levelup.gitconnected.com/practical-s…