Diário de Aprendizagem: Artigo 1

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 .




All Articles