关于代码可读可维护问题的一些浅见

目录

1. 问题原由

2. Go中的枚举解决方案

2.1 从某热门开源项目源码说起

2.2 Go的枚举解决方案

2.3 期望的表达方法

3. 从编程语言设计的高度看待此问题


1. 问题原由

将数字和字符串关联并赋予一个程序内符号命名,是编程中经常遇到的问题。

如ErrorCode->Error Message, Id->Name等等

在java中,有枚举(enum)类型可以做到这一切。

然而很不幸的是,Go语言不提供枚举类型,则这一切需要我们自己用普通类型来实现。

2. Go中的枚举解决方案

2.1 从某热门开源项目源码说起

这是某热门区块链项目定义消息枚举值与消息含义的方法,比较取巧的方法是其使用了struct来封装这些枚举,使用变量可以通过代码提示,找到所有可能的枚举值。

然而这种写法最大的问题在于代码可读性差,由于字段名与字段值不在可识别的范围内,没法通过阅读代码找到每个字段对应的枚举值。

// type MethodNum uint64
var MethodsMiner = struct {
	Constructor              abi.MethodNum
	ControlAddresses         abi.MethodNum
	ChangeWorkerAddress      abi.MethodNum
	ChangePeerID             abi.MethodNum
	SubmitWindowedPoSt       abi.MethodNum
	PreCommitSector          abi.MethodNum
	ProveCommitSector        abi.MethodNum
	ExtendSectorExpiration   abi.MethodNum
	TerminateSectors         abi.MethodNum
	DeclareFaults            abi.MethodNum
	DeclareFaultsRecovered   abi.MethodNum
	OnDeferredCronEvent      abi.MethodNum
	CheckSectorProven        abi.MethodNum
	ApplyRewards             abi.MethodNum
	ReportConsensusFault     abi.MethodNum
	WithdrawBalance          abi.MethodNum
	ConfirmSectorProofsValid abi.MethodNum
	ChangeMultiaddrs         abi.MethodNum
	CompactPartitions        abi.MethodNum
	CompactSectorNumbers     abi.MethodNum
	ConfirmUpdateWorkerKey   abi.MethodNum
	RepayDebt                abi.MethodNum
	ChangeOwnerAddress       abi.MethodNum
	DisputeWindowedPoSt      abi.MethodNum
}{MethodConstructor, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24}

起码这段代码应该写成这个样子,才具备基本的可读性。 

当然,这样写也是有弊端的,需要增加新的协议类型,需要在很长的代码里跨越千山万水增加两行,不具备可维护性(如果改动很频繁的话,当然这个例子不太可能如此)。

// type MethodNum uint64
var MethodsMiner = struct {
	Constructor              abi.MethodNum
	ControlAddresses         abi.MethodNum
	ChangeWorkerAddress      abi.MethodNum
	ChangePeerID             abi.MethodNum
	SubmitWindowedPoSt       abi.MethodNum
	PreCommitSector          abi.MethodNum
	ProveCommitSector        abi.MethodNum
	ExtendSectorExpiration   abi.MethodNum
	TerminateSectors         abi.MethodNum
	DeclareFaults            abi.MethodNum
	DeclareFaultsRecovered   abi.MethodNum
	OnDeferredCronEvent      abi.MethodNum
	CheckSectorProven        abi.MethodNum
	ApplyRewards             abi.MethodNum
	ReportConsensusFault     abi.MethodNum
	WithdrawBalance          abi.MethodNum
	ConfirmSectorProofsValid abi.MethodNum
	ChangeMultiaddrs         abi.MethodNum
	CompactPartitions        abi.MethodNum
	CompactSectorNumbers     abi.MethodNum
	ConfirmUpdateWorkerKey   abi.MethodNum
	RepayDebt                abi.MethodNum
	ChangeOwnerAddress       abi.MethodNum
	DisputeWindowedPoSt      abi.MethodNum
}{
	Constructor:              MethodConstructor,
	ControlAddresses:         2,
	ChangeWorkerAddress:      3,
	ChangePeerID:             4,
	SubmitWindowedPoSt:       5,
	PreCommitSector:          6,
	ProveCommitSector:        7,
	ExtendSectorExpiration:   8,
	TerminateSectors:         9,
	DeclareFaults:            10,
	DeclareFaultsRecovered:   11,
	OnDeferredCronEvent:      12,
	CheckSectorProven:        13,
	ApplyRewards:             14,
	ReportConsensusFault:     15,
	WithdrawBalance:          16,
	ConfirmSectorProofsValid: 17,
	ChangeMultiaddrs:         18,
	CompactPartitions:        19,
	CompactSectorNumbers:     20,
	ConfirmUpdateWorkerKey:   21,
	RepayDebt:                22,
	ChangeOwnerAddress:       23,
	DisputeWindowedPoSt:      24,
}

2.2 Go的枚举解决方案

如下是一种常见的Error Code定义方法。

然而试想,如果有1万个code和text的对应关系需要维护,在这种代码框架下要实现一一对应,并且不出错,几乎就不是人能干的活了。

package error

import (
	"fmt"
)

const (
	SUCCESS = 0
	ERROR1  = 1
	ERROR2  = 2
)

var codeTable = map[int]string{
	SUCCESS: "success",
	ERROR1:  "error1",
	ERROR2:  "error2",
}

func ErrorMsg(code int) string {
	if text, ok := codeTable[code]; ok {
		return text
	}
	return fmt.Sprintf("unknown error code %d", code)
}

2.3 在go中实现枚举注册器

go中可以为内置类型定义方法,是一个很自然的表达方式。

package code

import (
	"fmt"
)

type Code int

func (c Code) Int() int {
	return int(c)
}

func NewCodeSet(name string) *CodeSet {
	s := &CodeSet{
		name: name,
		code: make(map[int]string),
        text: make(map[string]int),
	}
	return s
}

type CodeSet struct {
	name string
	code map[int]string
	text map[string]int
}

func (s *CodeSet) tryInit() {
	if s.code == nil {
		s.code = make(map[int]string)
		s.text = make(map[string]int)
	}
}

// MustRegister regist a code with text, it will painic if code is duplicate
func (s *CodeSet) MustRegister(code int, text string) Code {
	c, err := s.Register(code, text)
	if err != nil {
		panic(err)
	}
	return c
}

// Register regist a code with text, it returns error if code is duplicate
func (s *CodeSet) Register(code int, text string) (Code, error) {
	s.tryInit()
	if old, ok := s.code[code]; ok {
		return 0, fmt.Errorf("[%s] duplicate code {%d, %s}, old text: %s", s.name, code, text, old)
	}
	if old, ok := s.text[text]; ok {
		return 0, fmt.Errorf("[%s] duplicate text {%d, %s}, old code: %d", s.name, code, text, old)
	}
	s.code[code] = text
	s.text[text] = code
	return Code(code), nil
}

func (s *CodeSet) Text(code Code) (string, error) {
	if text, ok := s.code[code.Int()]; ok {
		return text, nil
	}
	return "", fmt.Errorf("unknown code (%d)", code)
}

func (s *CodeSet) Code(text string) (Code, error) {
	if code, ok := s.text[text]; ok {
		return Code(code), nil
	}
	return 0, fmt.Errorf("unknown text (%d)", text)
}

2.4 完美的枚举关系描述代码

枚举定义代码如下。可以看到,程序标识符,枚举值,枚举名都出现在同一行。

后续就算有一万个枚举加进来,三者的对应关系都只与其对应的代码行有关。

程序可读性和可维护性可谓完美。

package code

var (
	Sunday    = MustRegister(0, "SUN")
	Monday    = MustRegister(1, "MON")
	Tuesday   = MustRegister(2, "TUE")
	Wednesday = MustRegister(3, "WED")
	Thursday  = MustRegister(4, "THU")
	Friday    = MustRegister(5, "FRI")
	Saturday  = MustRegister(6, "SAT")
)

var weekdaySet = NewCodeSet("weekday")

// mustRegister register an error code with text that don't allow duplicate code
func MustRegister(code int, text string) Code {
	return weekdaySet.MustRegister(code, text)
}

func MustGetCode(s string) Code {
	c, err := weekdaySet.Code(s)
	if err != nil {
		panic(err)
	}
	return c
}

func MustGetText(c Code) string {
	s, err := weekdaySet.Text(c)
	if err != nil {
		panic(err)
	}
	return s
}

3. 从编程语言设计的高度看待此问题

如上所述方法,在go中实现枚举值与枚举名对应,可以说已经满足我们对代码可读性,可维护性的所有诉求,唯一美中不足,是这一切都是发生在“运行时”。

然而,根据现代编程语言发展理论,当参数code,text都是字面常量的情况下,函数MustRegister是完全有能力成为constexpr的,如C++11定义的那样,就算是函数,只要满足constexpr的特性,就完全可以在“编译期”求值。

这样,当出现了code重复,text重复这些问题的时候,编译的时候,就能够抛出错误,而不需要等到运行时发现这些低级错误。

Go语言出于简洁的考虑,很多强大的现代编程语言理论都没有相应特性支持。

然而我们作为代码编写者,应该时常去思考一个问题,如果让我们自己去实现自己想要的编程语言特性,那么我们日常实现的那些代码,应该怎样优化才能达到信达雅的哲学高度。

因为虽然Go是一门优秀的编程语言,但是它也并不是完美的。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值