Go Learning Diary: Eintrag 1

Schließlich organisierte ich mich, um Go zu lernen. Wie erwartet habe ich beschlossen, sofort mit dem Üben zu beginnen, um die Sprache besser beherrschen zu können. Ich habe eine "Laborarbeit" entwickelt, in der ich verschiedene Aspekte der Sprache konsolidieren möchte , ohne dabei die vorhandenen Erfahrungen mit der Entwicklung in anderen Sprachen zu vergessen, insbesondere in verschiedenen Architekturprinzipien, einschließlich SOLID und anderen. Ich schreibe diesen Artikel im Zuge der Umsetzung der Idee selbst und spreche meine wichtigsten Gedanken und Überlegungen darüber aus, wie dieser oder jener Teil der Arbeit zu tun ist. Dies ist also kein Artikel wie eine Lektion, in der ich versuche, jemandem beizubringen, wie und was zu tun ist, sondern nur ein Protokoll meiner Gedanken und Überlegungen zur Geschichte, damit später bei der Arbeit an Fehlern auf etwas Bezug genommen werden kann.

Einleitend

Das Wesentliche des Labors besteht darin, mithilfe einer Konsolenanwendung ein Ausgaben-Tagebuch zu führen. Die Funktionalität ist wie folgt vorläufig:

  • Der Benutzer kann eine neue Spesenabrechnung sowohl für den aktuellen Tag als auch für jeden Tag in der Vergangenheit erstellen und dabei Datum, Betrag und Kommentar angeben

  • Es kann auch eine Auswahl nach Datum treffen, um den Gesamtbetrag zu erhalten, der für die Ausgabe ausgegeben wurde

Formalisierung

Also, nach Business - Logik, haben wir zwei Entitäten: eine separaten Kostensatz ( Aufwendungen ) und die allgemeinen Entity - Tagebuch , das die Ausgaben Tagebuch als Ganze verkörpert. Die Ausgaben bestehen aus Feldern wie Datum , Summe und Kommentar . Das Tagebuch besteht noch aus nichts und verkörpert einfach das Tagebuch selbst als Ganzes auf die eine oder andere Weise, die eine Reihe von Ausgabenobjekten enthält , und ermöglicht es dementsprechend, sie für verschiedene Zwecke zu erhalten / zu modifizieren. Die weiteren Felder und Methoden werden unten aufgeführt. Da es sich um eine sequentielle Liste von Datensätzen handelt, die insbesondere nach Datum geordnet sind, bietet sich eine Implementierung in Form einer verknüpften Liste von Entitäten an. Und in diesem Fall das ObjektTagebuch kann sich nur auf das erste Element in der Liste beziehen. Es müssen auch grundlegende Methoden zum Bearbeiten von Elementen hinzugefügt werden (Hinzufügen / Entfernen usw.), aber Sie sollten beim Füllen dieses Objekts nicht über Bord gehen, damit es nicht zu viel übernimmt , das heißt, es widerspricht nicht dem Prinzip der Einzelverantwortung (Einzelverantwortung) Verantwortung - der Buchstabe S in SOLID). Beispielsweise sollten Sie keine Methoden hinzufügen, um das Tagebuch in einer Datei zu speichern oder daraus zu lesen. Sowie andere spezifische Methoden zur Analyse und Datenerfassung. Bei einer Datei handelt es sich um eine separate Architekturschicht (Speicher) , die nicht direkt mit der Geschäftslogik zusammenhängt. Im zweiten Fall sind die Optionen für die Verwendung des Tagebuchs nicht im Voraus bekannt und können stark variieren., was unweigerlich zu ständigen Änderungen im Tagebuch führen wird , was sehr unerwünscht ist. Daher befindet sich die gesamte zusätzliche Logik außerhalb dieser Klasse.

Näher am Körper, also die Erkenntnis

Insgesamt haben wir folgende Strukturen, wenn wir noch mehr landen und über eine bestimmte Implementierung in Go sprechen:

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

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

Es ist besser, mit verknüpften Listen mit einer generischen Lösung wie dem Container- / Listenpaket zu arbeiten . Diese Strukturdefinitionen sollten in einem separaten Paket zusammengefasst werden, das wir als Ausgaben bezeichnen : Erstellen wir ein Verzeichnis in unserem Projekt mit zwei Dateien: Expense.go und 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 Das Repository mit dem Projekt befindet sich unter https://github.com/Amegatron/golab-expenses . Der Master - Zweig enthält die aktuellste Version der Arbeit. Tags ( Tags ) markieren das letzte Commit, das gemäß jedem Artikel ausgeführt wurde. Beispielsweise wird das letzte Commit gemäß diesem Artikel (Eintrag 1) mit stage_01 gekennzeichnet .




All Articles