SOLID
solid原则核心是描述软件的中层结构目标,致力于实现:
- 是软件更容易被扩展。
- 使软件更容易被理解。
- 使软件更容易被复用。
开闭原则
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)
}