写点什么

架构设计之路 -1

用户头像
Dnnn
关注
发布于: 2020 年 12 月 04 日

编程范式



结构化编程(Structured programming)



  • 1968年最新提出

  • 它采用子程序、程式码区块(英语:block structures)、for循环以及while循环等结构,来取代传统的 goto。希望借此来改善计算机程序的明晰性、品质以及开发时间,并且避免写出面条式代码。





  • 上图可以发现,虽然没有了goto,但是优秀的我们还是以写出完美的面条式代码。



面向对象式编程 (Object Oriented Programming)



  • 1966 年被提出

  • 面向对象程序设计方法是尽可能模拟人类的思维方式,使得软件的开发方法与过程尽可能接近人类认识世界、解决现实问题的方法和过程,也即使得描述问题的问题空间与问题的解决方案空间在结构上尽可能一致,把客观世界中的实体抽象为问题域中的对象。





  • 一个很好的例子:编年体史书、纪传体史书



函数式编程 (Functional Programming)



  • 函数式编程对于我们是一个比较神秘的编程范式,很少接触到,或者没有发现我们自己接触到。

  • 1958年LISP 基于函数式编程思想被设计出来

  • 函数式编程最大的特点就是没有赋值语句。不允许修改变量的值。





go语言中 函数式编程例子
package test
import (
"fmt"
"testing"
)
func adder() func(int) int {
sum := 0
return func(v int) int {
sum += v
return sum
}
}
func Test_A(t *testing.T) {
a := adder()
for i := 0; i < 10; i++ {
var s int
s = a(i)
fmt.Printf("0 + 1 + ... + %d = %d\n", i, s)
}
}
  • 我们自己代码里的函数式编程

// MatchAnyEventOf matches if any of several matchers matches.
func MatchAnyEventOf(types ...EventType) EventMatcher {
return func(e EventMessage) bool {
for _, t := range types {
if MatchEvent(t)(e) {
return true
}
}
return false
}
}
// MatchEvent matches a specific event type, nil events never match.
func MatchEvent(t EventType) EventMatcher {
return func(e EventMessage) bool {
return e != nil && e.EventType() == t
}
}



package main
import (
"fmt"
)
func main() {
list := []int32{111, 2, 34, 5, 6, 7}
fmt.Println(loop(list,
func(a, b int32) int32 {
if a >= b {
return a
}
return b
},
))
fmt.Println(loop(list,
func(a, b int32) int32 {
if a <= b {
return a
}
return b
},
))
}
func loop(list []int32, f func(a, b int32) int32) int32 {
var m int32
if len(list) != 0 {
m = list[0]
}
for _, k := range list {
m = f(k, m)
}
return m
}



我这里介绍了三种编程范式,它们都从某一方面限制和规范了程序员的能力。

没有一个范式是增加能力的。也就是说每个方式的目的都是设置了限制。

三个范式没有好坏之分,在开发中也没有强制的规定。一切都是以整洁开发为目的。

这三个范式都是在1958年到1968年被提出来的,后续再也没有出现过新的范式。



从编程范式的角度,在这里想起了一段话



二十多岁的程序员特别喜欢并发、框架、协议、内核、中间件之类的名词,还有这些年的大数据、机器学习、VR、区块链之类的新技术。在我不算长的十多年的编程经历里也学过很多很多新的技术,但是不管什么技术都只能维持一段时间的快感:大部分是浮于表面的技术本质没有变化,少数几个本质技术跟几十年前没有什么区别……而对于“优雅的设计”的追求却一直能够带给我精神上的满足感:我一直在追求设计出高内聚、低耦合、易于扩展和维护的系统;代码简洁而高效,考虑周全又没有一丝多余,一切都是恰到好处的感觉:不仅能够应对当前的需求,还能够顺应这段代码、这个系统、这个组织,甚至于自己和这个行业的未来。



在形成了“追求优雅设计”的世界观之后,一切就都不一样了:产品经理的每一个“膈应”的需求都是对自己优雅设计的挑战。程序变得越来越灵活和抽象,细碎的问题渐渐变得不再需要改代码了。未来每一次的修改都在自己的掌控中,三四年前的某个设计可能因为今天的需求而继续产生价值。这是一种开了上帝视角的快感。

如何设计一个好的类



1. 创建一个类的理由



  • 为现实世界建模

  • 为抽象对象建模

  • 降低复杂度,隐藏实现细节,隐藏复杂度

  • 隔离复杂度,限制变动的影响范围

  • 让参数传递更加顺畅

  • 让代码更易于重用



2. 应该避免的类



  • 避免创建万能类

  • 消除无关紧要的类

  • 避免用动词命名的类

3. 坏味道

3.1 抽象



  • 类是否有一个中心目的?

  • 类的命名是否恰当?其名字是否表达了其中心目的?

  • 类的提供的服务是否足够完整,能让其他类无须动用其内部数据?

  • 是否从类中出去无关信息?

  • 是否考虑过把类进一步分解为组件?是否以尽可能将其分解?



3.2 封装



  • 是否把类的成员的可访问性降到最小?

  • 是否避免暴露类的中的数据成员?

  • 在编程语言所许可的范围内,类是否以尽可能地对其他类隐藏自己的实现细节。

  • 类是否避免对其使用者,包括派生类如何使用它做了假设。

  • 类是否不依赖于其他类?他是松耦合的吗?



3.3 继承



  • 继承是否建立一个"IS A 的关系"?也就是说派生类是否遵循了LSP(里氏替换原则)?

  • 派生类是否避免了“覆盖”不可覆盖的方法?

  • 基类中所有的数据成员是否被定义为private的了?



3.4 跟实现相关的其他问题



  • 类中是否有只有大约7个或者更少的数据成员?

  • 是否把类直接或间接调用其他类的子程序数量减少到最少?

  • 类是否只在绝对必要时才与其他类互相协作?

  • 是否在构造函数中初始化了所有数据成员?



3.5 语言相关的问题



  • 你是否研究过所用编程语言里和类相关的各种特有问题?

4. GSX order类演化

  • 原有设计

type OrderAggregate struct {
xcqrs.BaseAggregate
Number int64 //订单number
StudentNumber int64 // 学生number
ClazzNumber int64 // 班级编号
ClazzType ClazzType //购买班级类型,细胞班级或联报班级
ClazzCategory ClazzCategory //班级类型
CellCourseNumber int64 //细胞课number,如果购买的细胞班级,则此值不为空,联报班级则为空
CellSubclazzNumber int64 //辅导班number,如果购买的细胞班级,则此值不为空
Status Status // 订单状态
ExpireTime string // 订单过期时间
PlaybackDeadlineTime string // 每个订单对应的课程都有观看期限,只有在观看期限内才能学习课程
PayPrice int32 // 用户实际支付的金额
RefundPrice int32 // 退款金额
RefundNotLearn int32 // 0退款后可以继续学习回放,1不能看
RefundTime string // 退款时间
MasterSubjectID int32 // 主营三级科目
AddressSet int32 //订单地址设置情况 0:正常状态 1:订单需要邮寄地址,但并未填写地址 2:订单需要邮寄地址并填写
PayChannel PayChannel // 支付方式
PayTime string // 支付时间
ChildOrders map[string]*ChildOrder //联报订单的子订单 map结构 <OrderNumber,ChildOrder>
IsTransferClazzRefund bool //是否为转班退款订单
ParentNumber int64 //父订单号
TransferClazzRefundPrice int32 //转班退款金额
ShareKey string //来源
CreateTime string //创建时间
LessonCount int32 //课节数统计
}



  • 新的设计

type TradeAggregate struct {
xcqrs.BaseAggregate
Number int64 `cqrs:"id"` //订单number
StudentNumber int64 // 学生number
CreatorID int64 // 员工ID
Goods Goods // 购买商品
PayType PayType // 支付类型
Price Price // 交易价格
Status Status // 状态
CancelType CancelType // 取消类型
RefundType RefundType // 退款类型
Purchase Purchase // 支付信息
ShareKey ShareKey // 分享信息
TradeFlows []PriceFlow // 交易价格流水
AddressSet int32 //地址是否设置,0不需要,1需要未设置,2需要已设置
ExpireTime *xtype.DateTime // 订单过期时间
CreateTime *xtype.DateTime // 记录创建时间
UpdateTime *xtype.DateTime // 记录更新时间
}



// Goods 购买商品.
type Goods struct {
//0-默认 1-普通课程交易 2-物流交易 3-题库交易 4-联报课程交易 5-选课单交易
Type GoodsType
//课程交易时为number 物流交易时为关联的订单number
Number int64
//商品下单时的快照.
Snapshot GoodsInfo //已废弃,勿用,请用数组
//商品下单时的快照.
Snapshots []GoodsInfo
}



// Price 价格.
type Price struct {
//原价
OriginPrice int32
//成单金额,单位为分
PayPrice int32
//1-现金 2-学币
CurrencyType CurrencyType
}
  • 具有了很好的抽象力度。

  • 封装性明显提升。

  • 代码清晰度明显提升。

  • 一点建议

  • 类中所有字段全是可导出的,破坏了封装性

  • TradeAggregate 主类中的字段过多可以继续拆分。



高质量子程序(函数、方法)



  • 我们为什么要创建子程序呢?

1. 创建一个子程序的理由



  • 降低复杂度

func (e EventHandler) onPaperCorrectedEvent(ctx context.Context, tx *gorm.DB, event *events.PaperCorrectedEvent) error {
key1 := fmt.Sprintf(config.CachePaperRank, event.ExamNumber)
res := xrds.Trace(ctx, e.redis).ZRange(key1, 0, 5)
//时间常量 "2029-12-31 00:00:00"
var endTime int64 = 1893340800
correctTime, err := xtype.DateTimeFrom(event.CorrectTime)
if err != nil {
return err
}
//时间得分(时间常量-答题时间)
timeScore := endTime - correctTime.Unix()
stringTimeScore := strconv.FormatInt(timeScore, 10)
//补齐位数
fmtStringTimeScore := fmt.Sprintf("%0*s", 9, stringTimeScore)
//最终分数(分数做整数位,时间戳做小数位)
//stringScore :=
floatScore, err := strconv.ParseFloat(event.FirstCorrectData.Score, 64)
if err != nil {
return err
}
stringScore := strconv.FormatFloat(floatScore*1000, 'f', -1, 64)
stringFinalScore := stringScore + "." + fmtStringTimeScore
finalScore, err := strconv.ParseFloat(stringFinalScore, 64)
if err != nil {
return err
}
key := fmt.Sprintf(config.CachePaperRank, event.ExamNumber)
if err = xrds.Trace(ctx, e.redis).ZAdd(key, redis.Z{
Score: finalScore,
Member: event.PaperNumber,
}).Err(); err != nil {
xlog.Ctx(ctx).Errorw("缓存作业排行榜信息失败", "err", err)
} else {
xlog.Ctx(ctx).Infow("缓存作业排行榜信息", "paperNumber", event.PaperNumber)
}
return nil
}

优化一下

func (e EventHandler) onPaperCorrectedEvent(ctx context.Context, event *events.PaperCorrectedEvent) error {
if event.ExamEntityType != vov2.EXAM_ENTITY_TYPE_CLAZZ_LESSON {
return nil
}
if event.PaperStatus != vov2.PAPER_STATUS_DONE {
return nil
}
return e.addRank(ctx, event.ExamNumber, event.PaperNumber, event.Score, event.CorrectTime)
}

引入中间、易懂的抽象(把需要写注释的地方写成子程序)

把一段代码放入一个命名恰当的子程序中,是说明这段代码用意最好的方法

//时间常量 "2029-12-31 00:00:00"
var endTime int64 = 1893340800
correctTime, err := xtype.DateTimeFrom(event.CorrectTime)
if err != nil {
return err
}
//时间得分(时间常量-答题时间)
timeScore := endTime - correctTime.Unix()
stringTimeScore := strconv.FormatInt(timeScore, 10)
//补齐位数
fmtStringTimeScore := fmt.Sprintf("%0*s", 9, stringTimeScore)
//最终分数(分数做整数位,时间戳做小数位)
//stringScore :=
floatScore, err := strconv.ParseFloat(event.FirstCorrectData.Score, 64)
if err != nil {
return err
}
stringScore := strconv.FormatFloat(floatScore*1000, 'f', -1, 64)
stringFinalScore := stringScore + "." + fmtStringTimeScore
finalScore, err := strconv.ParseFloat(stringFinalScore, 64)



优化一下

func (e EventHandler) addRank(ctx context.Context, examNumber int64, paperNumber int64, score string, correctTime string) error {
rankScore, err := e.getRandScore(correctTime, score)
if err != nil {
return err
}
err = xrds.Trace(ctx, e.redis).ZAdd(config.GetCachePaperRankKey(examNumber), redis.Z{
Score: rankScore,
Member: paperNumber,
}).Err()
if err != nil {
xlog.S(ctx).Errorw("缓存作业排行榜信息失败", "err", err)
} else {
xlog.S(ctx).Infow("缓存作业排行榜信息", "paperNumber", paperNumber)
}
return nil
}
func (e EventHandler) getRandScore(correctTimeString string, score string) (float64, error) {
correctTime, err := xtype.DateTimeFrom(correctTimeString)
if err != nil {
return 0, errors.New("获取批改时间失败")
}
floatScore, err := strconv.ParseFloat(score, 64)
if err != nil {
return 0, errors.New("获取分数失败")
}
stringScore := strconv.FormatFloat(floatScore*1000, 'f', -1, 64)
stringFinalScore := stringScore + "." + vo.GetPaperRankScore(correctTime.Unix())
finalScore, err := strconv.ParseFloat(stringFinalScore, 64)
return finalScore, err
}
  • 避免代码重复

母庸质疑,这是创建子程序最好的理由,我们kit 库就是这个目的。

  • 隐藏顺序

把数据处理的顺序隐藏起来是一个好主意,否则散落在代码里的顺序会带来后续调整的负担。

func (s *SendXMQListener) Handle(ctx context.Context, tx *gorm.DB, msg EventMessage) {
//-------------------------
_, err = s.client.SendMessage(ctx, &xmqv1.SendMessageRequest{
Message: &xmqv1.Message{
Topic: s.o.topic,
Tag: string(msg.EventType()),
Key: strconv.FormatInt(msg.AggregateID(), 10),
ShardingKey: strconv.FormatInt(msg.AggregateID(), 10),
Body: string(payload),
},
})
if err != nil {
xlog.L(ctx).Error("发送Event到XMQ失败", zap.Error(err))
return
}
//-------------------------
err = xdb.Trace(ctx, tx).Where("event_identifier = ?", msg.EventID()).Delete(DomainEventProduceEntry{}).Error
return
}
  • 提高可移植性

可以用子程序来隔离程序中不可移植的部分,从而明确识别未来的移植工作。不可移植的部分包括语言提供的非标准功能,对硬件的依赖,以及对操作系统的依赖等。

此处APP和QT会比较多

  • 简化复杂的布尔判断

为了理解流程,通常没有必要去研究那些复杂的布尔判断的细节。把这些复杂的判断放入函数中,提高代码的可读性。

1.这样就可以把细节放到一边。

2.一个具有描述性的函数名字可以概括出判断的目的。



if consumerCfg.ReconsumeTimes > MinSourceRecTime && msg.ReconsumeTimes > consumerCfg.ReconsumeTimes {
...
} else if consumerCfg.ReconsumeTimes <= MinSourceRecTime && msg.ReconsumeTimes > MinRetryTimeForEmail {
...
}

优化一下

if c.needDrop(ctx, cfg, msg.ReconsumeTimes) {
...
} else if c.needSendWarn(ctx, cfg, msg.ReconsumeTimes) {
...
}
func (c *Consumer) needDrop(ctx context.Context, consumerConf config.Consumer, currentRetryTime int) bool {
if consumerConf.ReconsumeTimes <= MinSourceRecTime {
xlog.S(ctx).Info("没有开启丢弃功能,原因是ReconsumeTimes <= MinSourceRecTime")
return false
}
return currentRetryTime > consumerConf.ReconsumeTimes
}
func (c *Consumer) needSendWarn(ctx context.Context, consumerConf config.Consumer, currentRetryTime int) bool {
if currentRetryTime <= MinRetryTimeForEmail {
return false
}
if len(consumerConf.Emails) == 0 {
return false
}
if currentRetryTime%100 != 0 {
return false
}
return true
}



2. 好的名字

  • 避免使用无意义的,模糊或表述不清楚的动词

有时一个子程序仅有的问题就是名字表达不清晰,而程序本身没有问题

if !course.Category(clazz.Category).IsOpen() {
return nil, errors.WithStack(orderErr)
}
------
if !course.Category(clazz.Category).IsPublicClass() {
return nil, errors.WithStack(orderErr)
}



  • 准确使用对仗词

  • add / romove

  • begin/end

  • create / destory

  • fisrt / last

  • get / put

  • increment / decrement

  • insert / delete

  • lock / unlock

  • min / max

  • next / previous

  • old / new

  • open / close

  • show /hide

  • start /stop

  • up /down

3. 坏味道

3.1 大局事项



  • 创建子程序的理由充分吗?

  • 一个程序中所有适于单独提出的部分是不是已经被提出到单独的子程序中了?

  • 子程序的名字是否描述了他所有做的事情?

  • 子程序是否只做一件事情,并且做好?(原子性)



3.2 参数传递事宜



  • 子程序中的参数是否超过7个?

  • 是否用到了每一个参数?(不要预留没有用的参数)

  • 子程序是否避免把输入参数当做工作变量?

  • 那么它是否在所有可能的情况下都能返回一个合法值?

防御式编程





1. 关于输入



  • 检查所有来源与外部数据的值

  • 检查子程序所有输入的值

  • 决定如何处理错误的输入值



2. 断言





  • 大声的喊出系统存在一个bug。(响亮的报错!)

  • 断言来处理绝对不应该发生的状况。

  • 喊出来的信息尽量的详细一些,能明确bug类型。



3. 错误处理



  • 健壮性

意味着要不断尝试采取某些措施,以保证程序可以持续的运行下去。哪怕有事做出一些不够准确的结果。

  • 正确性

意味着要不断尝试采取某些措施,以保证程序可以持续的运行下去。哪怕有事做出一些不够准确的结果。

4. 错误处理技术

  • 返回中立值

有时处理错误的最佳做法就是继续执行操作,并简单返回一个没有错误的数值。比如说数值计算可以返回0.字符串可以返回空。

func (m *SubclazzPaperIntrosRequest) GetLessonNumber() int64 {
if m != nil {
return m.LessonNumber
}
return 0
}
func (m *VideoObjectDataRequest) GetObjectNames() []string {
if m != nil {
return m.ObjectNames
}
return nil
}
  • 换用下一个正确的数据,或者前次相同的数据

这种处理手法多用于流式数据中,比如天气预报。

  • 换用接近的合法值

有些情况下,可以选择返回最接近的合法值。比如行课的数据。

if roomTotalTime == 0 {
studyPercent = 0
} else {
studyPercent = (studyTotalTime * 60 / float64(roomTotalTime)) * 100
}
if studyPercent > 100 {
studyPercent = 100
}

把警告信息记录到日志文件里

使用这个可以结合其他方法,把错误记录在日志中,然后让程序继续运行,但是使用日志要小心敏感信息是否被记录

当错误发送生时显示出错误信息

要注意不要告诉系统潜在的攻击者太多的东西。攻击者可能利用错误信息来发现如何攻击这个系统。

关闭程序

有些程序一旦出错就要关闭程序,比如在数据迁移时,出现问题就要及时停止脚本。

for {
papers, err := trans.homeworkSvc.GetPapersByHomeworkNumber(ctx, getHomeworkNumbersFromRelation(relations), offset, limit)
if err != nil {
break
}
if len(papers) == 0 {
break
}
for _, p := range papers {
total++
err = trans.writeFile(getFileName(total, size), p.Number)
if err != nil {
break;
}
}
}

5. 错误处理的统一



确定一种通用的处理错误的方法,是架构层次的设计决策。



一旦确定了某种错误方法,就要确保始终如一的贯彻这一方法,如果你决定让高层次代码来处理错误,而低层次代码只需要简单的报错,那么就要确保高层次的代码真正的处理了错误。



go 语言是可以忽略函数的返回错误的 --------- 但是千万不要这么做。及时你认为这个函数无论如何不能犯错,也要去检查一下

防御式编程的全部重点就在于【防御那些你未曾预料到的错误】。

BAD CASE



func (svc KnowledgePointExamSubmitSvc) Handler(
ctx context.Context,
req *homeworkLogicV2PB.KnowledgePointExamSubmitRequest) (*homeworkLogicV2PB.KnowledgePointExamSubmitResponse, error) {
examDetail, _ := svc.homeworkRPC.ExamDetail(ctx, &homeworkDataV2PB.ExamDetailRequest{
Number: req.GetExamNumber(),
})
studentNumber, _ := auth.StudentNumberFromCtx(ctx)
...
masterDegreeInfo, _ := svc.homeworkRPC.KnowledgePointMasterDegree(ctx, &homeworkDataV2PB.KnowledgePointMasterDegreeRequest{
StudentNumber: studentNumber,
LessonNumbers: []int64{req.GetClazzLessonNumber()},
})
...
questionInfo, _ := svc.homeworkRPC.Questions(ctx, &homeworkDataV2PB.QuestionsRequest{
QuestionNumbers: questionNumbers,
})
var degreeInfo = notHandledDegreeInfo
// 【这里 如果 totalQuestionScore == 0 怎办?】
if totalUserAnswerScore/totalQuestionScore*100 >= 75 {
degreeInfo = handledDegreeInfo
}
return &homeworkLogicV2PB.KnowledgePointExamSubmitResponse{
Code: 0,
Msg: "",
Data: &homeworkLogicV2PB.KnowledgePointExamSubmitResponse_Data{
PaperNumber: paperCommitRes.PaperNumber,
HandleDegree: int32(math.Floor((totalUserAnswerScore/totalQuestionScore)*100 + 0.5)),
Solutions: solutionRes,
TipsInfo: tipsInfo,
DegreeInfo: degreeInfo,
},
}, nil
}



6. 异常



如果一个子程序中遇到了预料之外的情况,但是不知道该如何处理的话,他就可以抛出一个异常,就好比举起双手说“我不知道该怎么处理它-----我真希望有谁知道怎么办!”一样。



对于不知道前因后果的错误,可以把控制权交给系统中其他能更好解释错误的并采取措施的部分。

异常和继承有一点事相同的,审慎明智的使用可以降低复杂度,而草率的使用,只会然代码变得几乎无法理解。



  • 用异常通知程序的其他部分,发送了不可忽略的错误

  • 只有在真正例外的情况下使用异常

  • 不能用异常来推卸责任

  • 避免使用空 catch语句



7. 坏味道



  • 子程序是否保护自己免遭有害输入数据的破坏?

  • 你是否在高层设计中规定了是让错误处理倾向于健壮性或者稳定性?

  • 是否在开发阶段使错误不可以被忽略?

  • 是否考虑到SQL注入和XSS攻击?

  • 是否检查了所有错误?

  • 是否捕获了所有异常

  • 错误信息中是否泄漏了机密数据?

  • 错误信息是否暴露太多系统细节,会被攻击者攻击?



发布于: 2020 年 12 月 04 日阅读数: 17
用户头像

Dnnn

关注

还未添加个人签名 2019.07.09 加入

还未添加个人简介

评论

发布
暂无评论
架构设计之路-1