Go Learning Diary: Entrée 1

Enfin je me suis organisé pour commencer à apprendre le Go. Comme prévu, j'ai décidé de commencer à pratiquer tout de suite afin de m'améliorer dans l'utilisation de la langue. J'ai imaginé un "travail de laboratoire" dans lequel je compte consolider différents aspects du langage, sans oublier l'expérience existante de développement dans d'autres langages, notamment - divers principes architecturaux, dont SOLID et d'autres. J'écris cet article au cours de la mise en œuvre de l'idée elle-même, exprimant mes principales réflexions et considérations sur la façon de faire telle ou telle partie du travail. Donc, ce n'est pas un article de type leçon où j'essaie d'enseigner à quelqu'un comment et quoi faire, mais plutôt juste un journal de mes pensées et de mon raisonnement pour l'histoire, de sorte qu'il y ait quelque chose auquel se référer plus tard lorsque je travaille sur les erreurs.

Introduction

L'essence du laboratoire est de tenir un journal des dépenses en espèces à l'aide d'une application console. La fonctionnalité est préliminaire comme suit:

  • l'utilisateur peut créer un nouvel enregistrement de dépenses à la fois pour la journée en cours et pour n'importe quel jour du passé, en précisant la date, le montant et le commentaire

  • il peut également faire des sélections par dates, obtenant le montant total dépensé à la sortie

Formalisation

Ainsi, selon la logique métier, nous avons deux entités: un enregistrement de dépenses distinct ( Dépenses ) et l'entité générale Diary , qui personnifie le journal de dépenses dans son ensemble. Les dépenses se composent de champs tels que la date , la somme et le commentaire . Le journal ne consiste encore en rien et personnifie simplement le journal lui-même dans son ensemble, d'une manière ou d'une autre contenant un ensemble d'objets de dépenses , et, en conséquence, permet de les obtenir / modifier à des fins diverses. Ses autres domaines et méthodes seront décrits ci-dessous. Puisqu'il s'agit d'une liste séquentielle d'enregistrements, notamment triés par dates, une implémentation sous la forme d'une liste chaînée d'entités se suggère. Et dans ce cas, l'objetLe journal ne peut faire référence qu'au premier élément de la liste. Il faut également ajouter des méthodes de base pour manipuler les éléments (ajouter / supprimer, etc.), mais il ne faut pas aller trop loin en remplissant cet objet pour qu'il n'en prenne pas trop , c'est-à-dire qu'il ne contredit pas le principe de responsabilité unique (Single responsabilité - la lettre S en SOLIDE). Par exemple, vous ne devez pas ajouter de méthodes pour enregistrer le journal dans un fichier ou en lire. Ainsi que toute autre méthode spécifique d'analyse et de collecte de données. Dans le cas d'un fichier, il s'agit d'une couche d'architecture distincte (stockage) qui n'est pas directement liée à la logique métier. Dans le second cas, les options d'utilisation du journal ne sont pas connues à l'avance et peuvent varier considérablement., ce qui entraînera inévitablement des changements constants dans l' agenda , ce qui est très indésirable. Par conséquent, toute logique supplémentaire sera en dehors de cette classe.

Plus proche du corps, c'est-à-dire la réalisation

Au total, nous avons les structures suivantes, si nous atterrissons encore plus et parlons d'une implémentation spécifique dans Go:

//     
type Expense struct {
  Date time.Date
  Sum float32
  Comment string
}

//  
type Diary struct {
  Entries *list.List
}

Il est préférable de travailler avec des listes liées avec une solution générique telle que le package conteneur / liste . Ces définitions de structure doivent être placées dans un package séparé, que nous appellerons dépenses : créons un répertoire dans notre projet avec deux fichiers: Expense.go et 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 Le référentiel avec le projet se trouve sur https://github.com/Amegatron/golab-expenses . La branche principale contiendra la version la plus récente de l'œuvre. Les étiquettes ( tags ) marqueront le dernier commit effectué conformément à chaque article. Par exemple, le dernier commit selon cet article (entrée 1) serait étiqueté stage_01 .




All Articles