Go Learning Diary: Entrada 1

Finalmente me organicé para empezar a aprender Go. Como esperaba, decidí comenzar a practicar de inmediato para mejorar en el uso del idioma. Se me ocurrió un "trabajo de laboratorio" en el que planeo consolidar varios aspectos del lenguaje, sin olvidar la experiencia existente de desarrollo en otros lenguajes, en particular, varios principios arquitectónicos, incluido SOLID y otros. Estoy escribiendo este artículo en el curso de la implementación de la idea en sí, expresando mis principales pensamientos y consideraciones sobre cómo hacer esta o aquella parte del trabajo. Entonces, este no es un artículo tipo lección en el que trato de enseñarle a alguien cómo y qué hacer, sino más bien un registro de mis pensamientos y razonamientos para la historia, para que haya algo a lo que referirse más adelante cuando trabaje en los errores.

Introductorio

La esencia del laboratorio es llevar un diario de los gastos en efectivo utilizando una aplicación de consola. La funcionalidad es preliminar de la siguiente manera:

  • el usuario puede hacer un nuevo registro de gastos tanto para el día actual como para cualquier día del pasado, especificando la fecha, el monto y el comentario

  • también puede hacer selecciones por fechas, obteniendo el monto total gastado en la salida

Formalización

Entonces, de acuerdo con la lógica empresarial, tenemos dos entidades: un registro de gastos separado ( Gastos ) y la entidad general Diario , que personifica el diario de gastos en su conjunto. El gasto consta de campos como fecha , suma y comentario . El diario todavía no consta de nada y simplemente personifica el diario en sí como un todo, de una forma u otra, que contiene un conjunto de objetos de gastos y, en consecuencia, permite obtenerlos / modificarlos para diversos fines. Sus campos y métodos adicionales se verán a continuación. Dado que estamos hablando de una lista secuencial de registros, especialmente ordenados por fechas, se sugiere una implementación en forma de lista enlazada de entidades. Y en este caso el objetoEl diario solo puede hacer referencia al primer elemento de la lista. También necesita agregar métodos básicos para manipular elementos (agregar / quitar, etc.), pero no debe exagerar con llenar este objeto para que no asuma demasiado , es decir, no contradiga el principio de responsabilidad única (Single responsabilidad - la letra S en SÓLIDO). Por ejemplo, no debe agregar métodos para guardar el diario en un archivo o leerlo. Así como cualquier otro método específico de análisis y recopilación de datos. En el caso de un archivo, esta es una capa separada de arquitectura (almacenamiento) que no está directamente relacionada con la lógica empresarial. En el segundo caso, las opciones para usar el diario se desconocen de antemano y pueden variar mucho., lo que conducirá inevitablemente a constantes cambios en el Diario , lo cual es muy indeseable. Por lo tanto, toda la lógica adicional estará fuera de esta clase.

Más cerca del cuerpo, es decir, la realización

En total, tenemos las siguientes estructuras, si aterrizamos aún más y hablamos de una implementación específica en Go:

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

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

Es mejor trabajar con listas vinculadas con una solución genérica como el paquete contenedor / lista . Estas definiciones de estructura deben colocarse en un paquete separado, que llamaremos gastos : creemos un directorio dentro de nuestro proyecto con dos archivos: Expense.go y 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. , , : , . , . , :)

PD El repositorio con el proyecto se encuentra en https://github.com/Amegatron/golab-expenses . La rama maestra contendrá la versión más reciente del trabajo. Las etiquetas ( etiquetas ) marcarán la última confirmación realizada de acuerdo con cada artículo. Por ejemplo, la última confirmación según este artículo (entrada 1) se etiquetaría como stage_01 .




All Articles