终于组织了自己,开始学习Go。正如预期的那样,我决定立即开始练习,以更好地使用该语言。我提出了一项“实验室工作”,其中我计划合并该语言的各个方面,同时不要忘记使用其他语言进行开发的现有经验,尤其是-各种体系结构原理,包括SOLID等。我在实现构想本身的过程中写了这篇文章,表达了我对如何完成这一部分或那部分工作的主要想法和考虑。因此,这不是一篇像一堂课的文章,我试图在此教别人如何做,而是教我自己的思想和对历史的推理记录,以便以后在进行错误工作时可以参考。
介绍性
该实验室的本质是使用控制台应用程序保存费用日记。该功能初步如下:
用户可以指定当日,金额和注释,记录当日和过去任意一天的新费用记录
它还可以按日期进行选择,从而获得在输出中花费的总金额
正规化
因此,根据业务逻辑,我们有两个实体:一个单独的费用记录(Expense)和一个通用实体Diary,该实体将整个支出日记拟人化。费用由日期,金额和注释之类的字段组成。日记尚未包含任何内容,只是简单地以一种或另一种方式将日记本身整体化,包含一组Expense对象,并因此可以出于各种目的而对其进行获取/修改。其更多领域和方法将在下面看到。由于我们正在谈论的是记录的顺序列表,尤其是按日期排序,因此以实体链接列表的形式实现的方法本身就是一种建议。在这种情况下,对象日记只能引用列表中的第一项。它也需要补充的基本方法操作元件(添加/删除等),但你不应该填补这个对象,使其太过火没有考虑太多,也就是说,它并不矛盾单一职责原则(单责任-SOLID中的字母S)。例如,您不应添加将日记保存到文件或从中读取文件的方法。以及任何其他特定的分析和数据收集方法。对于文件,这是体系结构(存储)的独立层,与业务逻辑不直接相关。在第二种情况下,使用日记的选项是事先未知的,并且可能会有很大差异。,这将不可避免地导致恒定的变化在日记,这是非常不可取的。因此,所有其他逻辑将不在此类之内。
贴近身体,即实现
总体而言,如果我们投入更多精力并谈论Go中的特定实现,我们将具有以下结构:
//
type Expense struct {
Date time.Date
Sum float32
Comment string
}
//
type Diary struct {
Entries *list.List
}
最好使用诸如容器/列表包之类的通用解决方案来处理链表。这些结构定义应该放在一个单独的包中,我们将其称为支出:让我们在项目内部创建一个包含两个文件的目录:Expense.go和Diary.go。
/ , / . , : ( ), - -, , , . . , , . : Save(d *Diary)
Load() (*Diary)
. : DiarySaveLoad, expenses/io:
type DiarySaveLoad interface {
Save(diary *expenses.Diary)
Load() *expenses.Diary
}
, /, / (, , - - URL , ). , . , (Liskov substitution - L SOLID), . -, / , : Save Load . , , , , , , DiarySaveLoadParameters, /, . . (Interface segregation - I SOLID), , .
, : FileSystemDiarySaveLoad. , “ ”, - / :
package io
import (
"expenses/expenses"
"fmt"
"os"
)
type FileSystemDiarySaveLoad struct {
Path string
}
func (f FileSystemDiarySaveLoad) Save(d *expenses.Diary) {
file, err := os.Create(f.Path)
if err != nil {
panic(err)
}
for e := d.Entries.Front(); e != nil; e = e.Next() {
buf := fmt.Sprintln(e.Value.(expenses.Expense).Date.Format(time.RFC822))
buf += fmt.Sprintln(e.Value.(expenses.Expense).Sum)
buf += fmt.Sprintln(e.Value.(expenses.Expense).Comment)
if e.Next() != nil {
buf += "\n"
}
_, err := file.WriteString(buf)
if err != nil {
panic(err)
}
}
err = file.Close()
}
:
func (f FileSystemDiarySaveLoad) Load() *expenses.Diary {
file, err := os.Open(f.Path)
if err != nil {
panic(err)
}
scanner := bufio.NewScanner(file)
entries := new(list.List)
var entry *expenses.Expense
for scanner.Scan() {
entry = new(expenses.Expense)
entry.Date, err = time.Parse(time.RFC822, scanner.Text())
if err != nil {
panic(err)
}
scanner.Scan()
buf, err2 := strconv.ParseFloat(scanner.Text(), 32)
if err2 != nil {
panic(err2)
}
entry.Sum = float32(buf)
scanner.Scan()
entry.Comment = scanner.Text()
entries.PushBack(*entry)
entry = nil
scanner.Scan() // empty line
}
d := new(expenses.Diary)
d.Entries = entries
return d
}
“ ”, / . , , expenses/io/FileSystemDiarySaveLoad_test.go:
package io
import (
"container/list"
"expenses/expenses"
"math/rand"
"testing"
"time"
)
func TestConsistentSaveLoad(t *testing.T) {
path := "./test.diary"
d := getSampleDiary()
saver := new(FileSystemDiarySaveLoad)
saver.Path = path
saver.Save(d)
loader := new(FileSystemDiarySaveLoad)
loader.Path = path
d2 := loader.Load()
var e, e2 *list.Element
var i int
for e, e2, i = d.Entries.Front(), d2.Entries.Front(), 0; e != nil && e2 != nil; e, e2, i = e.Next(), e2.Next(), i+1 {
_e := e.Value.(expenses.Expense)
_e2 := e2.Value.(expenses.Expense)
if _e.Date != _e2.Date {
t.Errorf("Data mismatch for entry %d for the 'Date' field: expected %s, got %s", i, _e.Date.String(), _e2.Date.String())
}
// Expense ...
}
if e == nil && e2 != nil {
t.Error("Loaded diary is longer than initial")
} else if e != nil && e2 == nil {
t.Error("Loaded diary is shorter than initial")
}
}
func getSampleDiary() *expenses.Diary {
testList := new(list.List)
var expense expenses.Expense
expense = expenses.Expense{
Date: time.Now(),
Sum: rand.Float32() * 100,
Comment: "First expense",
}
testList.PushBack(expense)
//
// ...
d := new(expenses.Diary)
d.Entries = testList
return d
}
, , . , /: , , . go test expenses/expenses/io -v
FAIL :
Data mismatch for entry 0 for the 'Date' field: expected 2020-09-14 04:16:20.1929829 +0300 MSK m=+0.003904501, got 2020-09-14 04:16:00 +0300 MSK
: . , time.Now, . : / RFC822, , , . . , , , , ( ), . . , . SOLID, , (Open-closed principle - O SOLID). , . , -, . , , , - , , Expense. , Go , expenses:
func Create(date time.Time, sum float32, comment string) Expense {
return Expense{Date: date.Truncate(time.Second), Sum: sum, Comment: comment}
}
, Expense ( :D), : Load FileSystemDiarySaveLoad, ( getSampleDiary). . , , , , time.RFC3339Nano . , , , .
. :) , / , , . :) , Diary, . . ( container/list) - "" Diary, - . () Diary, , , . .
, Go, , - Go. , , : , . , . , :)
PS该项目的存储库位于https://github.com/Amegatron/golab-expenses。master分支将包含该作品的最新版本。标签(tag)将标记根据每个文章进行的最后一次提交。例如,根据本文的最后一次提交(条目1)将被标记为stage_01。