Interface Segregation
Overview
Zestor follows the Interface Segregation Principle by splitting its functionality into focused interfaces. This allows you to pass only the access level your code needs.
Available Interfaces
// Reader provides read-only access
type Reader[T any] interface {
Get(kind, key string) (val T, ok bool, err error)
List(kind string, filter ...FilterFunc[T]) (map[string]T, error)
Count(kind string) (int, error)
Keys(kind string) ([]string, error)
Values(kind string) ([]KeyValue[T], error)
GetAll() (map[string]map[string]T, error)
}
// Writer provides write access
type Writer[T any] interface {
Set(kind, key string, value T) (created bool, err error)
SetFn(kind, key string, fn func(v T) (T, error)) (changed bool, err error)
SetAll(kind string, values map[string]T) error
Delete(kind, key string) (existed bool, prev T, err error)
}
// Watcher provides watch access
type Watcher[T any] interface {
Watch(kind string, opts ...WatchOption[T]) (r <-chan *Event[T], cancel func(), err error)
}
// ReadWriter combines Reader and Writer
type ReadWriter[T any] interface {
Reader[T]
Writer[T]
}
// Store is the full interface
type Store[T any] interface {
Reader[T]
Writer[T]
Watcher[T]
Close() error
Dump() string
}
Why Interface Segregation?
1. Principle of Least Privilege
Pass only the access your code needs:
// This function can't accidentally modify data
func generateReport(r store.Reader[User]) Report {
users, _ := r.List("users")
// r.Set(...) ← Compile error! Reader has no Set
return buildReport(users)
}
2. Clearer Function Signatures
The interface type documents what the function does:
// Obviously read-only
func countActiveUsers(r store.Reader[User]) int
// Obviously writes data
func importUsers(w store.Writer[User], users []User) error
// Obviously watches for changes
func streamEvents(w store.Watcher[User], out chan Event)
3. Easier Testing
Smaller interfaces are easier to mock:
type mockReader struct {
users map[string]User
}
func (m *mockReader) Get(kind, key string) (User, bool, error) {
u, ok := m.users[key]
return u, ok, nil
}
func (m *mockReader) List(kind string, filters ...store.FilterFunc[User]) (map[string]User, error) {
return m.users, nil
}
// ... only need to implement Reader methods
Usage Examples
Read-Only Service
type ReportService struct {
store store.Reader[User]
}
func NewReportService(r store.Reader[User]) *ReportService {
return &ReportService{store: r}
}
func (s *ReportService) GetUserCount() int {
count, _ := s.store.Count("users")
return count
}
func (s *ReportService) GetAdmins() []User {
users, _ := s.store.List("users", func(k string, u User) bool {
return u.Role == "admin"
})
// Convert to slice...
return result
}
Write-Only Importer
type UserImporter struct {
store store.Writer[User]
}
func NewUserImporter(w store.Writer[User]) *UserImporter {
return &UserImporter{store: w}
}
func (i *UserImporter) Import(users map[string]User) error {
return i.store.SetAll("users", users)
}
func (i *UserImporter) Delete(key string) error {
_, _, err := i.store.Delete("users", key)
return err
}
Watch-Only Event Processor
type EventProcessor struct {
store store.Watcher[User]
}
func NewEventProcessor(w store.Watcher[User]) *EventProcessor {
return &EventProcessor{store: w}
}
func (p *EventProcessor) ProcessEvents(ctx context.Context) error {
ch, cancel, err := p.store.Watch("users")
if err != nil {
return err
}
defer cancel()
for {
select {
case event, ok := <-ch:
if !ok {
return nil
}
p.handleEvent(event)
case <-ctx.Done():
return ctx.Err()
}
}
}
Read-Write Without Watch
type SyncService struct {
store store.ReadWriter[User]
}
func NewSyncService(rw store.ReadWriter[User]) *SyncService {
return &SyncService{store: rw}
}
func (s *SyncService) Upsert(key string, user User) error {
existing, ok, _ := s.store.Get("users", key)
if ok && existing.Email == user.Email {
return nil // No change needed
}
_, err := s.store.Set("users", key, user)
return err
}
Passing Store as Different Interfaces
The memStore implementation satisfies all interfaces, so you can pass it wherever needed:
func main() {
// Create full Store
s := gomap.NewMemStore[User](store.StoreOptions[User]{})
defer s.Close()
// Pass as Reader
reportSvc := NewReportService(s)
// Pass as Writer
importer := NewUserImporter(s)
// Pass as Watcher
processor := NewEventProcessor(s)
// Pass as ReadWriter
syncSvc := NewSyncService(s)
// All use the same underlying store instance
}
Best Practices
Use the narrowest interface possible — If you only read, accept
ReaderDocument access patterns — The interface type serves as documentation
Consider splitting large functions — If a function needs both read and write, consider if it can be split
Use
ReadWriterfor CRUD — When you need read and write but not watchAccept interfaces, return concrete types — Functions should accept interfaces but constructors can return the full
Store