[Программирование, Проектирование и рефакторинг, Go] Дневник изучения Go: запись 1
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Наконец-то организовал себя, чтобы начать изучать Go. Как и полагается, решил сразу приступить к практике, дабы лучше освоиться с языком. Придумал себе "лабораторную работу", в которой планирую закреплять различные аспекты языка, не забывая при этом уже имеющийся опыт разработки на других языках, в частности - различные архитектурные принципы, включая SOLID и другие. Статью эту я пишу по ходу реализации самой идеи, озвучивая основные свои мысли и рассуждения о том, как сделать ту или иную часть работы. Так что это не статья по типу урока, где я пытаюсь научить кого-то как и что делать, а скорее просто лог моих мыслей и рассуждений для истории, чтобы было потом на что сослаться, делая работу над ошибками.ВводнаяСуть лабораторки в том, чтобы вести дневник денежных расходов при помощи консольного приложения. Функционал предварительно заключается в следующем:
- пользователь может внести новую запись о расходе как за текущий день, так и за какой-либо день в прошлом, указав дату, сумму и комментарий
- он также может делать выборки по датам, получив на выходе общую потраченную сумму
ФормализацияИтак, по бизнес-логике у нас есть две сущности: отдельная запись о расходах (Expense) и общая сущность Diary, олицетворяющая дневник трат в целом. Expense состоит из таких полей как date, sum и comment. Diary пока ни из чего не состоит и просто олицетворяет сам дневник в целом, тем или иным образом содержа в себе набор объектов Expense, и соответственно позволяет их получить/модифицировать для различных целей. Его дальнейшие поля и методы будут видны далее. Поскольку мы говорим о последовательном списке записей, тем более упорядоченного по датам, напрашивается реализация в виде связанного списка сущностей. И в этом случае объект Diary может ссылаться всего лишь на первый элемент списка. В него также нужно добавить основные методы для манипуляции с с элементами (добавление/удаление и т.д.), но перебарщивать с наполнением этого объекта не стоит, чтобы он не брал на себя слишком многое, то есть не противоречил принципу единственной ответственности (Single responsibility - буква S в SOLID). Например, в него не стоит добавлять методы сохранения дневника в файл или чтения из него. Равно как и какие-либо другие специфические методы по анализу и сбору данных. В случае с файлом - это отдельный слой архитектуры (хранение), не связанный напрямую с бизнес-логикой. Во втором случае варианты использования дневника заранее неизвестны и могут сильно изменяться, что неминуемо приведет к постоянным изменениям в Diary, что очень нежелательно. Поэтому вся дополнительная логика будет вне этого класса.Ближе к телу, то есть реализацииИтого имеем следующие структуры, если приземляться еще больше и говорить уже о конкретной реализации в Go:
// структура самой записи в дневнике
type Expense struct {
Date time.Date
Sum float32
Comment string
}
// Сам дневник
type Diary struct {
Entries *list.List
}
Работать со связанными списками лучше обобщенным решением, которое предоставляет, например, пакет container/list. Данные определения структур стоит вынести в отдельный пакет, который назовем expenses: создадим директорию внутри нашего проекта с двумя файлами: Expense.go и 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. Что, впрочем, не отменяет полезности проделанной работы для меня самого: лишний раз убедиться, что основные архитектурные принципы применимы независимо от языка. А также проверить себя на то, что в состоянии их применять в совершенно новом для себя языке. Ну а насколько примененные решения окажутся грамотными и полезными для дальнейшей разработки, покажет время :)P.S. Репозиторий с проектом находится по адресу https://github.com/Amegatron/golab-expenses. Ветка master будет содержать самую последнюю версию работы. Метками (тэгами) буду отмечать последний коммит, сделанный в соответствии с каждой статьей. Например, последний коммит в соответствии с данной статьёй (запись 1) будет помечен тэгом stage_01.
===========
Источник:
habr.com
===========
Похожие новости:
- [JavaScript, Программирование, Разработка веб-сайтов] 200 теоретических вопросов по JavaScript
- [Программирование, Учебный процесс в IT, Карьера в IT-индустрии, Конференции] Бесплатные онлайн-мероприятия по разработке (15 сентября – 23 сентября)
- [Визуализация данных, Программирование микроконтроллеров, Разработка для интернета вещей, Умный дом, Интернет вещей] SCADA «BortX» с поддержкой языка управления в рамках ANSI /ISA-88 для ESP8266
- Google открыл код ветроэнергетической платформы Makani
- [Программирование микроконтроллеров] Продолжение очередной статьи: STM32 для начинающих. Интерфейсы
- [Программирование микроконтроллеров] Очередная статья: STM32 для начинающих
- [C++, Анализ и проектирование систем, ООП, Программирование, Программирование микроконтроллеров] Micro Property — минималистичный сериализатор двоичных данных для embedded систем
- [Программирование, Фриланс] Какой язык программирования выбрать
- [JavaScript, Node.JS, Программирование, Разработка веб-сайтов] Руководство по Express.js. Часть 3 (перевод)
- [Java, Программирование] Что нового в Java 15? (перевод)
Теги для поиска: #_programmirovanie (Программирование), #_proektirovanie_i_refaktoring (Проектирование и рефакторинг), #_go, #_solid, #_design_patterns, #_go, #_golang, #_learning, #_personalnyj_blog (персональный блог), #_programmirovanie (
Программирование
), #_proektirovanie_i_refaktoring (
Проектирование и рефакторинг
), #_go
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 21:53
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Наконец-то организовал себя, чтобы начать изучать Go. Как и полагается, решил сразу приступить к практике, дабы лучше освоиться с языком. Придумал себе "лабораторную работу", в которой планирую закреплять различные аспекты языка, не забывая при этом уже имеющийся опыт разработки на других языках, в частности - различные архитектурные принципы, включая SOLID и другие. Статью эту я пишу по ходу реализации самой идеи, озвучивая основные свои мысли и рассуждения о том, как сделать ту или иную часть работы. Так что это не статья по типу урока, где я пытаюсь научить кого-то как и что делать, а скорее просто лог моих мыслей и рассуждений для истории, чтобы было потом на что сослаться, делая работу над ошибками.ВводнаяСуть лабораторки в том, чтобы вести дневник денежных расходов при помощи консольного приложения. Функционал предварительно заключается в следующем:
// структура самой записи в дневнике
type Expense struct { Date time.Date Sum float32 Comment string } // Сам дневник type Diary struct { Entries *list.List } type DiarySaveLoad interface {
Save(diary *expenses.Diary) Load() *expenses.Diary } 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 } 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 } 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
func Create(date time.Time, sum float32, comment string) Expense {
return Expense{Date: date.Truncate(time.Second), Sum: sum, Comment: comment} } =========== Источник: habr.com =========== Похожие новости:
Программирование ), #_proektirovanie_i_refaktoring ( Проектирование и рефакторинг ), #_go |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 21:53
Часовой пояс: UTC + 5