Akhirnya mengatur diri saya untuk mulai belajar Go. Seperti yang diharapkan, saya memutuskan untuk mulai berlatih segera untuk menjadi lebih baik dalam menggunakan bahasa. Saya datang dengan "pekerjaan laboratorium" di mana saya berencana untuk mengkonsolidasikan berbagai aspek bahasa, sambil tidak melupakan pengalaman perkembangan yang ada dalam bahasa lain, khususnya - berbagai prinsip arsitektur, termasuk SOLID dan lainnya. Saya menulis artikel ini dalam proses implementasi gagasan itu sendiri, menyuarakan pemikiran dan pertimbangan utama saya tentang bagaimana melakukan ini atau bagian pekerjaan itu. Jadi ini bukan artikel seperti pelajaran, di mana saya mencoba untuk mengajari seseorang bagaimana dan apa yang harus dilakukan, melainkan hanya sebuah log pemikiran dan penalaran saya untuk sejarah, sehingga ada sesuatu yang bisa dirujuk nanti ketika melakukan kesalahan.
Pengantar
Inti dari laboratorium adalah membuat buku harian pengeluaran kas menggunakan aplikasi konsol. Fungsinya adalah awal sebagai berikut:
pengguna dapat membuat catatan pengeluaran baru baik untuk hari ini dan untuk hari mana pun di masa lalu, menentukan tanggal, jumlah dan komentar
itu juga dapat membuat pilihan berdasarkan tanggal, mendapatkan jumlah total yang dibelanjakan pada keluaran
Formalisasi
Jadi, menurut logika bisnis, kami memiliki dua entitas: catatan pengeluaran ( Beban ) terpisah dan Buku Harian entitas umum , yang mempersonifikasikan buku harian pengeluaran secara keseluruhan. Beban terdiri dari bidang-bidang seperti tanggal , jumlah dan komentar . Diary belum terdiri dari apa pun dan hanya mempersonifikasikan buku harian itu sendiri secara keseluruhan, dengan satu atau lain cara berisi sekumpulan objek Pengeluaran , dan, karenanya, memungkinkannya untuk diperoleh / dimodifikasi untuk berbagai keperluan. Bidang dan metodenya lebih lanjut akan dilihat di bawah ini. Karena kita berbicara tentang daftar rekaman berurutan, terutama yang diurutkan berdasarkan tanggal, implementasi dalam bentuk daftar entitas tertaut menunjukkan dirinya sendiri. Dan dalam hal ini objeknyaBuku harian hanya dapat merujuk ke item pertama dalam daftar. Ia juga perlu menambahkan metode dasar untuk memanipulasi elemen (tambah / hapus, dll.), Tetapi Anda tidak boleh berlebihan dengan mengisi objek ini sehingga tidak memakan terlalu banyak , yaitu, tidak bertentangan dengan prinsip tanggung jawab tunggal (Single tanggung jawab - huruf S di SOLID). Misalnya, Anda tidak boleh menambahkan metode untuk menyimpan buku harian ke file atau membacanya. Serta metode analisis dan pengumpulan data khusus lainnya. Dalam kasus file, ini adalah lapisan terpisah dari arsitektur (penyimpanan) yang tidak terkait langsung dengan logika bisnis. Dalam kasus kedua, opsi untuk menggunakan buku harian tidak diketahui sebelumnya dan bisa sangat bervariasi., yang pasti akan menyebabkan perubahan konstan di Buku Harian , yang sangat tidak diinginkan. Oleh karena itu, semua logika tambahan akan berada di luar kelas ini.
Lebih dekat ke tubuh, yaitu realisasi
Secara total, kami memiliki struktur berikut, jika kami lebih banyak mendarat dan berbicara tentang implementasi spesifik di Go:
//
type Expense struct {
Date time.Date
Sum float32
Comment string
}
//
type Diary struct {
Entries *list.List
}Lebih baik bekerja dengan daftar tertaut dengan solusi umum seperti paket wadah / daftar . Definisi struktur ini harus ditempatkan dalam paket terpisah, yang akan kita sebut biaya : mari buat direktori di dalam proyek kita dengan dua file: Expense.go dan 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 Repositori dengan proyek ini terletak di https://github.com/Amegatron/golab-expenses . Cabang master akan berisi versi terbaru dari karya tersebut. Labels ( tag ) akan menandai komit terakhir yang dibuat sesuai dengan setiap artikel. Misalnya, komit terakhir menurut artikel ini (entri 1) akan diberi tag stage_01 .