ついにGoを学び始めるために自分自身を組織しました。さすがに、言語を上手に使うために、すぐに練習を始めることにしました。私は、他の言語、特にSOLIDなどのさまざまなアーキテクチャの原則での開発の既存の経験を忘れずに、言語のさまざまな側面を統合することを計画している「ラボ作業」を思いつきました。私はこの記事をアイデア自体の実装の過程で書いています。これを行う方法や作業のその部分についての私の主な考えと考慮事項を表明しています。ですから、これはレッスンのような記事ではなく、誰かに方法と方法を教えようとするものではなく、自分の考えと歴史の理由のログにすぎないので、後で間違いに取り組むときに参照することがありました。
入門
研究所の本質は、コンソールアプリケーションを使用して現金支出の日記をつけることです。機能は次のように予備的なものです。
ユーザーは、日付、金額、コメントを示す、当日と過去の両方の新しい費用レコードを作成できます。
また、日付で選択して、出力で費やされた合計金額を取得することもできます
形式化
したがって、ビジネスロジックによれば、2つのエンティティがあります。個別の費用レコード(Expense)と、支出日記全体を具体化する一般エンティティDiaryです。費用は、日付、合計、コメントなどのフィールドで構成されます。日記はまだ何も構成されておらず、費用オブジェクトのセットを含む何らかの方法で日記自体を全体として単純に擬人化するため、さまざまな目的でそれらを取得/変更できます。そのさらなるフィールドと方法を以下に示します。特に日付順に並べられたレコードの順次リストについて話しているので、エンティティのリンクされたリストの形式での実装はそれ自体を示唆しています。そしてこの場合、オブジェクト日記はリストの最初の項目のみを参照できます。また、要素を操作するための基本的なメソッド(追加/削除など)を追加する必要がありますが、このオブジェクトを埋めすぎて、あまり多くを引き受けないようにする必要があります。つまり、単一の責任の原則と矛盾しないようにします(単一責任-SOLIDの文字S)。たとえば、日記をファイルに保存したり、ファイルから読み取ったりするメソッドを追加しないでください。分析およびデータ収集の他の特定の方法と同様に。ファイルの場合、これはビジネスロジックに直接関係しないアーキテクチャ(ストレージ)の独立したレイヤーです。 2番目のケースでは、日記を使用するためのオプションが事前にわかっておらず、大きく異なる可能性があります。、これは必然的に日記の絶え間ない変化につながりますが、これは非常に望ましくありません。したがって、追加のロジックはすべてこのクラスの外にあります。
体に近い、つまり実現
さらに着陸してGoでの特定の実装について話すと、合計で次の構造になります。
//
type Expense struct {
Date time.Date
Sum float32
Comment string
}
//
type Diary struct {
Entries *list.List
}
コンテナ/リストパッケージなどの一般的なソリューションを使用して、リンクされたリストを操作することをお勧めします。これらの構造定義は、別のパッケージに入れる必要があります。これを費用と呼びます。プロジェクト内に、Expense.goとDiary.goの2つのファイルを使用してディレクトリを作成しましょう。
/ , / . , : ( ), - -, , , . . , , . : 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にあります。マスターブランチには、最新バージョンの作品が含まれます。ラベル(タグ)は、各記事に従って行われた最後のコミットをマークします。たとえば、この記事による最後のコミット(エントリ1)にはstage_01のタグが付けられます。