Finalmente me organizei para começar a aprender Go. Como esperado, decidi começar a praticar imediatamente para melhorar o uso do idioma. Eu propus um “trabalho de laboratório” no qual pretendo consolidar vários aspectos da linguagem, sem esquecer a experiência existente de desenvolvimento em outras linguagens, em particular - vários princípios arquitetônicos, incluindo SOLID e outros. Estou escrevendo este artigo no decorrer da implementação da própria ideia, expressando minhas principais reflexões e considerações sobre como fazer esta ou aquela parte do trabalho. Portanto, este não é um artigo do tipo lição em que tento ensinar a alguém como e o que fazer, mas apenas um registro dos meus pensamentos e raciocínios para a história, para que houvesse algo a que se referir mais tarde ao trabalhar nos erros.
Introdutório
A essência do laboratório é manter um diário das despesas de caixa usando um aplicativo de console. A funcionalidade é preliminar da seguinte forma:
o usuário pode fazer um novo registro de despesas tanto para o dia atual quanto para qualquer dia no passado, especificando a data, valor e comentário
também pode fazer seleções por datas, obtendo o valor total gasto na saída
Formalização
Portanto, de acordo com a lógica de negócios, temos duas entidades: um registro de despesas separado ( Despesa ) e um Diário de entidade geral , que personifica o diário de despesas como um todo. A despesa consiste em campos como data , soma e comentário . O Diário ainda não consiste em nada e simplesmente personifica o próprio diário como um todo, de uma forma ou de outra contém um conjunto de objetos Despesa e, consequentemente, permite que sejam obtidos / modificados para diversos fins. Seus outros campos e métodos serão vistos abaixo. Como estamos falando de uma lista sequencial de registros, especialmente ordenados por datas, sugere-se uma implementação na forma de uma lista vinculada de entidades. E neste caso o objetoO diário só pode se referir ao primeiro item da lista. Ele também precisa adicionar métodos básicos para manipular elementos (adicionar / remover, etc.), mas você não deve exagerar no preenchimento desse objeto para que ele não assuma muito , ou seja, não contradiga o princípio da responsabilidade única (Única responsabilidade - a letra S em SÓLIDO). Por exemplo, você não deve adicionar métodos para salvar o diário em um arquivo ou ler a partir dele. Bem como quaisquer outros métodos específicos de análise e coleta de dados. No caso de um arquivo, esta é uma camada separada de arquitetura (armazenamento) que não está diretamente relacionada à lógica de negócios. No segundo caso, as opções de uso do diário não são conhecidas com antecedência e podem variar muito., o que inevitavelmente levará a mudanças constantes no Diário , o que é muito indesejável. Portanto, toda lógica adicional estará fora desta classe.
Mais perto do corpo, ou seja, a realização
No total, temos as seguintes estruturas, se pousarmos ainda mais e falarmos de uma implementação específica em Go:
//
type Expense struct {
Date time.Date
Sum float32
Comment string
}
//
type Diary struct {
Entries *list.List
}
É melhor trabalhar com listas vinculadas com uma solução genérica , como o pacote de contêiner / lista . Essas definições de estrutura devem ser colocadas em um pacote separado, que chamaremos de despesas : vamos criar um diretório dentro do nosso projeto com dois arquivos: Expense.go e 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 O repositório com o projeto está localizado em https://github.com/Amegatron/golab-expenses . O branch master conterá a versão mais recente do trabalho. Tags ( tags ) marcarão o último commit feito de acordo com cada artigo. Por exemplo, o último commit de acordo com este artigo (entrada 1) será marcado como stage_01 .