开发交互式终端工具:Pomodoro 计时器应用
1. 技能拓展与回顾
在继续深入之前,我们可以拓展一些技能和技术。以下是一些建议:
- 给列表命令添加
--active
标志,用于仅显示未完成的活跃任务。
- 利用所学原理创建一个命令行工具,从互联网上的 API 查询数据,例如 Movie DB 或 Open Weather API。
回顾之前的内容,我们使用了 Cobra 框架和
net/http
包开发了一个与远程 REST API 交互的命令行应用程序,还使用
encoding/json
包解析 JSON 数据。同时,我们探索并应用了多种测试 API 服务器和命令行客户端实现的技术,通过结合单元测试、模拟响应和测试服务器,可以进行持续的本地测试,并偶尔进行集成测试,以确保应用程序在不同环境中可靠运行。
2. 开发交互式 Pomodoro 计时器应用
Pomodoro 是一种时间管理方法,通过定义短时间间隔来专注任务,随后进行短休息和长休息,以帮助人们休息和重新安排任务优先级。通常,Pomodoro 间隔为 25 分钟,短休息为 5 分钟,长休息为 15 分钟。
我们将设计并实现一个直接在终端运行的交互式 CLI 应用程序,相较于完整的 GUI 应用程序,交互式 CLI 应用使用更少的资源,依赖项也更少,更具可移植性。例如系统监控应用(如 top 或 htop)和交互式磁盘实用工具(如 ncdu)。
对于 Pomodoro 应用程序,我们将实现 Repository 模式来抽象数据源,将业务逻辑与数据解耦,这样可以根据需求实现不同的数据存储。例如,我们将先实现一个内存数据存储,后续可以实现由 SQL 数据库支持的存储。
2.1 初始化 Pomodoro 应用程序
以下是初始化应用程序的步骤:
1. 创建目录结构:
$ mkdir -p $HOME/pragprog.com/rggo/interactiveTools/pomo
$ cd $HOME/pragprog.com/rggo/interactiveTools/pomo
- 初始化 Go 模块:
$ cd $HOME/pragprog.com/rggo/interactiveTools/pomo
$ go mod init pragprog.com/rggo/interactiveTools/pomo
接下来,我们创建
pomodoro
包来包含创建和使用 Pomodoro 计时器的业务逻辑。
1. 创建子目录并切换到该目录:
$ mkdir -p $HOME/pragprog.com/rggo/interactiveTools/pomo/pomodoro
$ cd $HOME/pragprog.com/rggo/interactiveTools/pomo/pomodoro
-
创建
interval.go文件,并添加以下内容:
package pomodoro
import (
"context"
"errors"
"fmt"
"time"
)
// Category constants
const (
CategoryPomodoro = "Pomodoro"
CategoryShortBreak = "ShortBreak"
CategoryLongBreak = "LongBreak"
)
// State constants
const (
StateNotStarted = iota
StateRunning
StatePaused
StateDone
StateCancelled
)
type Interval struct {
ID int64
StartTime time.Time
PlannedDuration time.Duration
ActualDuration time.Duration
Category string
State int
}
type Repository interface {
Create(i Interval) (int64, error)
Update(i Interval) error
ByID(id int64) (Interval, error)
Last() (Interval, error)
Breaks(n int) ([]Interval, error)
}
var (
ErrNoIntervals = errors.New("No intervals")
ErrIntervalNotRunning = errors.New("Interval not running")
ErrIntervalCompleted = errors.New("Interval is completed or cancelled")
ErrInvalidState = errors.New("Invalid State")
ErrInvalidID = errors.New("Invalid ID")
)
type IntervalConfig struct {
repo Repository
PomodoroDuration time.Duration
ShortBreakDuration time.Duration
LongBreakDuration time.Duration
}
func NewConfig(repo Repository, pomodoro, shortBreak, longBreak time.Duration) *IntervalConfig {
c := &IntervalConfig{
repo: repo,
PomodoroDuration: 25 * time.Minute,
ShortBreakDuration: 5 * time.Minute,
LongBreakDuration: 15 * time.Minute,
}
if pomodoro > 0 {
c.PomodoroDuration = pomodoro
}
if shortBreak > 0 {
c.ShortBreakDuration = shortBreak
}
if longBreak > 0 {
c.LongBreakDuration = longBreak
}
return c
}
以下是这些代码的功能说明:
- 定义了 Pomodoro 间隔的类别和状态常量。
- 创建了
Interval
结构体来表示 Pomodoro 间隔。
- 定义了
Repository
接口,包含创建、更新、查询等方法。
- 定义了一些错误值,用于表示特定的错误情况。
- 定义了
IntervalConfig
结构体和
NewConfig
函数,用于配置 Pomodoro 间隔的持续时间。
2.2 处理 Interval 类型的函数和方法
接下来,我们实现一些处理
Interval
类型的函数和方法。
首先是内部的非导出函数:
func nextCategory(r Repository) (string, error) {
li, err := r.Last()
if err != nil && err == ErrNoIntervals {
return CategoryPomodoro, nil
}
if err != nil {
return "", err
}
if li.Category == CategoryLongBreak || li.Category == CategoryShortBreak {
return CategoryPomodoro, nil
}
lastBreaks, err := r.Breaks(3)
if err != nil {
return "", err
}
if len(lastBreaks) < 3 {
return CategoryShortBreak, nil
}
for _, i := range lastBreaks {
if i.Category == CategoryLongBreak {
return CategoryShortBreak, nil
}
}
return CategoryLongBreak, nil
}
type Callback func(Interval)
func tick(ctx context.Context, id int64, config *IntervalConfig, start, periodic, end Callback) error {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
i, err := config.repo.ByID(id)
if err != nil {
return err
}
expire := time.After(i.PlannedDuration - i.ActualDuration)
start(i)
for {
select {
case <-ticker.C:
i, err := config.repo.ByID(id)
if err != nil {
return err
}
if i.State == StatePaused {
return nil
}
i.ActualDuration += time.Second
if err := config.repo.Update(i); err != nil {
return err
}
periodic(i)
case <-expire:
i, err := config.repo.ByID(id)
if err != nil {
return err
}
i.State = StateDone
end(i)
return config.repo.Update(i)
case <-ctx.Done():
i, err := config.repo.ByID(id)
if err != nil {
return err
}
i.State = StateCancelled
return config.repo.Update(i)
}
}
}
func newInterval(config *IntervalConfig) (Interval, error) {
i := Interval{}
category, err := nextCategory(config.repo)
if err != nil {
return i, err
}
i.Category = category
switch category {
case CategoryPomodoro:
i.PlannedDuration = config.PomodoroDuration
case CategoryShortBreak:
i.PlannedDuration = config.ShortBreakDuration
case CategoryLongBreak:
i.PlannedDuration = config.LongBreakDuration
}
if i.ID, err = config.repo.Create(i); err != nil {
return i, err
}
return i, nil
}
这些函数的功能如下:
-
nextCategory
函数根据 Repository 中的最后一个间隔,确定下一个间隔的类别。
-
Callback
类型定义了一个回调函数,用于在间隔执行期间执行特定任务。
-
tick
函数控制间隔计时器,根据不同的情况更新间隔的状态和持续时间。
-
newInterval
函数创建一个新的 Pomodoro 间隔,并根据配置设置其类别和持续时间。
然后是
Interval
类型的 API 函数:
func GetInterval(config *IntervalConfig) (Interval, error) {
i := Interval{}
var err error
i, err = config.repo.Last()
if err != nil && err != ErrNoIntervals {
return i, err
}
if err == nil && i.State != StateCancelled && i.State != StateDone {
return i, nil
}
return newInterval(config)
}
func (i Interval) Start(ctx context.Context, config *IntervalConfig, start, periodic, end Callback) error {
switch i.State {
case StateRunning:
return nil
case StateNotStarted:
i.StartTime = time.Now()
fallthrough
case StatePaused:
i.State = StateRunning
if err := config.repo.Update(i); err != nil {
return err
}
return tick(ctx, i.ID, config, start, periodic, end)
case StateCancelled, StateDone:
return fmt.Errorf("%w: Cannot start", ErrIntervalCompleted)
default:
return fmt.Errorf("%w: %d", ErrInvalidState, i.State)
}
}
func (i Interval) Pause(config *IntervalConfig) error {
if i.State != StateRunning {
return ErrIntervalNotRunning
}
i.State = StatePaused
return config.repo.Update(i)
}
这些函数的功能如下:
-
GetInterval
函数尝试从 Repository 中获取最后一个间隔,如果该间隔无效,则创建一个新的间隔。
-
Start
方法用于启动 Pomodoro 间隔,根据间隔的状态进行相应的处理。
-
Pause
方法用于暂停正在运行的 Pomodoro 间隔。
3. 使用 Repository 模式存储数据
我们将使用 Repository 模式实现一个 Pomodoro 间隔的数据存储,将数据存储实现与业务逻辑解耦,以便后续可以灵活修改或切换数据库。
首先,我们已经在前面定义了
Repository
接口,包含以下方法:
| 方法名 | 功能 |
| ---- | ---- |
|
Create
| 在数据存储中创建/保存一个新的 Interval |
|
Update
| 更新数据存储中 Interval 的详细信息 |
|
Last
| 从数据存储中检索最后一个 Interval |
|
ByID
| 根据 ID 从数据存储中检索特定的 Interval |
|
Breaks
| 从数据存储中检索指定数量的 Break 类型的 Interval |
接下来,我们实现一个内存数据存储:
1. 创建目录并切换到该目录:
$ mkdir -p $HOME/pragprog.com/rggo/interactiveTools/pomo/pomodoro/repository
$ cd $HOME/pragprog.com/rggo/interactiveTools/pomo/pomodoro/repository
-
创建并编辑
inMemory.go文件:
package repository
import (
"fmt"
"sync"
"pragprog.com/rggo/interactiveTools/pomo/pomodoro"
)
type inMemoryRepo struct {
sync.RWMutex
intervals []pomodoro.Interval
}
func NewInMemoryRepo() *inMemoryRepo {
return &inMemoryRepo{
intervals: []pomodoro.Interval{},
}
}
func (r *inMemoryRepo) Create(i pomodoro.Interval) (int64, error) {
r.Lock()
defer r.Unlock()
i.ID = int64(len(r.intervals)) + 1
r.intervals = append(r.intervals, i)
return i.ID, nil
}
func (r *inMemoryRepo) Update(i pomodoro.Interval) error {
r.Lock()
defer r.Unlock()
if i.ID == 0 {
return fmt.Errorf("%w: %d", pomodoro.ErrInvalidID, i.ID)
}
r.intervals[i.ID-1] = i
return nil
}
func (r *inMemoryRepo) ByID(id int64) (pomodoro.Interval, error) {
r.RLock()
defer r.RUnlock()
i := pomodoro.Interval{}
if id == 0 {
return i, fmt.Errorf("%w: %d", pomodoro.ErrInvalidID, id)
}
i = r.intervals[id-1]
return i, nil
}
func (r *inMemoryRepo) Last() (pomodoro.Interval, error) {
r.RLock()
defer r.RUnlock()
i := pomodoro.Interval{}
if len(r.intervals) == 0 {
return i, pomodoro.ErrNoIntervals
}
return r.intervals[len(r.intervals)-1], nil
}
func (r *inMemoryRepo) Breaks(n int) ([]pomodoro.Interval, error) {
r.RLock()
defer r.RUnlock()
data := []pomodoro.Interval{}
for k := len(r.intervals) - 1; k >= 0; k-- {
if r.intervals[k].Category == pomodoro.CategoryPomodoro {
continue
}
data = append(data, r.intervals[k])
if len(data) == n {
return data, nil
}
}
return data, nil
}
以下是这些代码的功能说明:
-
inMemoryRepo
结构体表示内存数据存储,使用
sync.RWMutex
来防止并发访问冲突。
-
NewInMemoryRepo
函数创建一个新的
inMemoryRepo
实例。
- 实现了
Repository
接口的所有方法,包括创建、更新、查询等操作。
通过以上步骤,我们完成了 Pomodoro 计时器应用程序的业务逻辑和内存数据存储的实现。接下来,我们可以对这个应用程序进行测试,确保其功能的正确性。
以下是一个简单的 mermaid 流程图,展示了 Pomodoro 间隔的状态转换:
graph LR
A[StateNotStarted] -->|Start| B[StateRunning]
B -->|Pause| C[StatePaused]
B -->|Finish| D[StateDone]
B -->|Cancel| E[StateCancelled]
C -->|Resume| B
这个流程图展示了 Pomodoro 间隔从未开始状态到运行状态,再到暂停、完成或取消状态的转换过程。
以上就是关于开发交互式 Pomodoro 计时器应用程序的详细内容,涵盖了业务逻辑的实现和内存数据存储的设计。通过这些步骤,我们可以构建一个功能完整的 Pomodoro 应用程序,并根据需要进行扩展和优化。
开发交互式终端工具:Pomodoro 计时器应用
4. 测试 Pomodoro 应用程序
在完成业务逻辑和数据存储的实现后,对 Pomodoro 应用程序进行测试是确保其功能正确性的重要步骤。虽然原文未详细给出测试代码,但我们可以根据已有的代码结构和功能,设计一些基本的测试用例。
4.1 测试
Interval
相关功能
以下是一些可能的测试用例示例:
package pomodoro
import (
"context"
"testing"
"time"
)
// 模拟 Repository 接口的实现,用于测试
type mockRepo struct {
intervals []Interval
}
func (m *mockRepo) Create(i Interval) (int64, error) {
i.ID = int64(len(m.intervals)) + 1
m.intervals = append(m.intervals, i)
return i.ID, nil
}
func (m *mockRepo) Update(i Interval) error {
for k, v := range m.intervals {
if v.ID == i.ID {
m.intervals[k] = i
return nil
}
}
return ErrInvalidID
}
func (m *mockRepo) ByID(id int64) (Interval, error) {
for _, v := range m.intervals {
if v.ID == id {
return v, nil
}
}
return Interval{}, ErrInvalidID
}
func (m *mockRepo) Last() (Interval, error) {
if len(m.intervals) == 0 {
return Interval{}, ErrNoIntervals
}
return m.intervals[len(m.intervals)-1], nil
}
func (m *mockRepo) Breaks(n int) ([]Interval, error) {
data := []Interval{}
for k := len(m.intervals) - 1; k >= 0; k-- {
if m.intervals[k].Category != CategoryPomodoro {
data = append(data, m.intervals[k])
if len(data) == n {
return data, nil
}
}
}
return data, nil
}
func TestGetInterval(t *testing.T) {
repo := &mockRepo{}
config := NewConfig(repo, 25*time.Minute, 5*time.Minute, 15*time.Minute)
interval, err := GetInterval(config)
if err != nil {
t.Errorf("GetInterval returned an error: %v", err)
}
if interval.ID == 0 {
t.Errorf("Expected a valid interval ID, got 0")
}
}
func TestStartInterval(t *testing.T) {
repo := &mockRepo{}
config := NewConfig(repo, 25*time.Minute, 5*time.Minute, 15*time.Minute)
interval, _ := GetInterval(config)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
err := interval.Start(ctx, config, func(i Interval) {}, func(i Interval) {}, func(i Interval) {})
if err != nil {
t.Errorf("Start interval returned an error: %v", err)
}
}
func TestPauseInterval(t *testing.T) {
repo := &mockRepo{}
config := NewConfig(repo, 25*time.Minute, 5*time.Minute, 15*time.Minute)
interval, _ := GetInterval(config)
// 先启动间隔
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
_ = interval.Start(ctx, config, func(i Interval) {}, func(i Interval) {}, func(i Interval) {})
err := interval.Pause(config)
if err != nil {
t.Errorf("Pause interval returned an error: %v", err)
}
}
这些测试用例的功能如下:
-
TestGetInterval
测试
GetInterval
函数是否能正确获取或创建一个新的间隔。
-
TestStartInterval
测试
Start
方法是否能正确启动一个间隔。
-
TestPauseInterval
测试
Pause
方法是否能正确暂停一个正在运行的间隔。
4.2 测试
Repository
接口实现
对于
inMemoryRepo
的测试,我们可以编写以下测试用例:
package repository
import (
"pragprog.com/rggo/interactiveTools/pomo/pomodoro"
"testing"
)
func TestInMemoryRepo(t *testing.T) {
repo := NewInMemoryRepo()
// 创建一个新的间隔
interval := pomodoro.Interval{
PlannedDuration: 25 * pomodoro.time.Minute,
Category: pomodoro.CategoryPomodoro,
State: pomodoro.StateNotStarted,
}
id, err := repo.Create(interval)
if err != nil {
t.Errorf("Create interval returned an error: %v", err)
}
if id == 0 {
t.Errorf("Expected a valid interval ID, got 0")
}
// 更新间隔
interval.ID = id
interval.State = pomodoro.StateRunning
err = repo.Update(interval)
if err != nil {
t.Errorf("Update interval returned an error: %v", err)
}
// 根据 ID 查询间隔
retrievedInterval, err := repo.ByID(id)
if err != nil {
t.Errorf("ByID returned an error: %v", err)
}
if retrievedInterval.State != pomodoro.StateRunning {
t.Errorf("Expected interval state to be running, got %d", retrievedInterval.State)
}
// 查询最后一个间隔
lastInterval, err := repo.Last()
if err != nil {
t.Errorf("Last returned an error: %v", err)
}
if lastInterval.ID != id {
t.Errorf("Expected last interval ID to be %d, got %d", id, lastInterval.ID)
}
// 查询 Break 类型的间隔
breaks, err := repo.Breaks(1)
if err != nil {
t.Errorf("Breaks returned an error: %v", err)
}
}
这些测试用例验证了
inMemoryRepo
实现的
Repository
接口的各个方法的正确性。
5. 总结与展望
通过以上步骤,我们完成了一个交互式 Pomodoro 计时器应用程序的开发,包括业务逻辑的实现、内存数据存储的设计以及基本的测试。这个应用程序利用了 Go 语言的强大功能,结合 Repository 模式,实现了业务逻辑与数据存储的解耦,提高了代码的可维护性和可扩展性。
以下是整个开发过程的关键步骤总结:
1.
初始化应用程序
:创建目录结构和 Go 模块,定义 Pomodoro 间隔的相关常量、结构体和接口。
2.
实现业务逻辑
:编写处理
Interval
类型的函数和方法,包括确定间隔类别、控制计时器、创建和管理间隔等。
3.
实现数据存储
:使用 Repository 模式,实现一个内存数据存储,确保数据的一致性和并发访问的安全性。
4.
进行测试
:设计并编写测试用例,验证业务逻辑和数据存储的正确性。
展望未来,我们可以对这个应用程序进行进一步的扩展和优化:
-
添加更多功能
:例如添加统计功能,记录每个 Pomodoro 间隔的完成情况和效率;添加通知功能,在间隔结束时提醒用户。
-
切换数据存储
:可以将内存数据存储切换为 SQL 数据库,如 SQLite 或 PostgreSQL,以实现数据的持久化存储。
-
优化用户界面
:通过使用终端界面库,如 Bubble Tea 或 Termui,为用户提供更友好的交互界面。
以下是一个 mermaid 流程图,展示了整个开发过程的主要步骤:
graph LR
A[初始化应用程序] --> B[实现业务逻辑]
B --> C[实现数据存储]
C --> D[进行测试]
D --> E[扩展与优化]
这个流程图展示了从应用程序的初始化到最终的扩展优化的整个开发过程。
通过不断地学习和实践,我们可以利用这些技术开发出更多功能强大、用户友好的交互式终端应用程序。希望本文对您在开发类似应用程序时有所帮助。
超级会员免费看
55

被折叠的 条评论
为什么被折叠?



