26、开发交互式终端工具:Pomodoro 计时器应用

开发交互式终端工具: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
  1. 初始化 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
  1. 创建 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
  1. 创建并编辑 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[扩展与优化]

这个流程图展示了从应用程序的初始化到最终的扩展优化的整个开发过程。

通过不断地学习和实践,我们可以利用这些技术开发出更多功能强大、用户友好的交互式终端应用程序。希望本文对您在开发类似应用程序时有所帮助。

【四轴飞行器】非线性三自由度四轴飞行器模拟器研究(Matlab代码实现)内容概要:本文围绕非线性三自由度四轴飞行器的建模与仿真展开,重点介绍了基于Matlab的飞行器动力学模型构建与控制系统设计方法。通过对四轴飞行器非线性运动方程的推导,建立其在三维空间中的姿态与位置动态模型,并采用数值仿真手段实现飞行器在复杂环境下的行为模拟。文中详细阐述了系统状态方程的构建、控制输入设计以及仿真参数设置,并结合具体代码实现展示了如何对飞行器进行稳定控制与轨迹跟踪。此外,文章还提到了多种优化与控制策略的应用背景,如模型预测控制、PID控制等,突出了Matlab工具在无人机系统仿真中的强大功能。; 适合人群:具备一定自动控制理论基础和Matlab编程能力的高校学生、科研人员及从事无人机系统开发的工程师;尤其适合从事飞行器建模、控制算法研究及相关领域研究的专业人士。; 使用场景及目标:①用于四轴飞行器非线性动力学建模的教学与科研实践;②为无人机控制系统设计(如姿态控制、轨迹跟踪)提供仿真验证平台;③支持高级控制算法(如MPC、LQR、PID)的研究与对比分析; 阅读建议:建议读者结合文中提到的Matlab代码与仿真模型,动手实践飞行器建模与控制流程,重点关注动力学方程的实现与控制器参数调优,同时可拓展至多自由度或复杂环境下的飞行仿真研究。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值