For this example, let's say we have three classes of user:
- Advanced Users
Users of this interface are granted unrestricted read/write access, with more implementation details being exposed - Tenant Users
Users of this interface are granted restricted read/write access (Ex. a tenant application, or a plugin implementation) - Read-Only Users
Users of this interface are granted read-only access (Ex. A read-only interface to be used for a custom UI/Dashboard/Debugger)
Notice that, for this particular example, Tenant
and ReadOnly
are effectively sub-interfaces of Advanced
.
On the other hand, Tenant
and ReadOnly
are not interfaces of each other.
Also worth noting: users of a particular interface will always keep that interface's access level, no matter which methods they call.
type AdvancedTable interface{
AddEntry(value1, value2 string) AdvancedEntry
GetEntry(uint32) AdvancedEntry
ID() uint32
}
type TenantTable interface{
AddEntry(value1, value2 string) TenantEntry
}
type ReadOnlyTable interface{
GetEntry(uint32) ReadOnlyEntry
ID() uint32
}
type AdvancedEntry interface{
GetValue1() string
SetValue1(string)
GetValue2() string
SetValue2(string)
Table() AdvancedTable
ID() uint32
}
type TenantEntry interface{
GetValue1() string
SetValue1(string)
GetValue2() string
SetValue2(string)
Table() TenantTable
}
type ReadOnlyEntry interface{
GetValue1() string
GetValue2() string
Table() ReadOnlyTable
ID() uint32
}
Note:
This is one possible implementation
Another likely alternative would involve renaming conflicting methods.
(i.e. - GetEntry(uint32) ReadOnlyEntry
-> GetReadOnlyEntry(uint32) ReadOnlyEntry
, etc.)
Such an implementation would reduce portability of the caller's code.
// implements AdvancedTable
type table struct {
id uint32
entries map[uint32]*entry
}
// implements AdvancedEntry
type entry struct {
table *table
id uint32
value1 string
value2 string
}
// implements ReadOnlyTable
type readOnlyTable struct {
table *table
}
// implements ReadOnlyEntry
type readOnlyEntry struct {
entry *entry
}
// implements TenantTable
type tenantTable struct {
table *table
}
// implements TenantEntry
type tenantEntry struct {
entry *entry
}
These must be implemented individually for each interface, because each interface is slightly different
// AdvancedTable implementation
func (t *table) AddEntry(value1, value2 string) AdvancedEntry {
e := &entry{table:table, id: generateUniqueID(), value1: value1, value2: value2})
t.entries[e.id] = e
return e
}
func (t *table) GetEntry(id uint32) AdvancedEntry {
return t.entries[id]
}
func (t *table) ID() uint32 {
return t.id
}
// AdvancedEntry implementation
func (e *entry) GetValue1() string {
return e.value1
}
func (e *entry) SetValue1(str string) {
e.value1 = str
}
func (e *entry) GetValue2() string {
return e.value2
}
func (e *entry) SetValue2(str string) {
e.value2 = str
}
func (e *entry) Table() AdvancedTable {
return e.table
}
func (e *entry) ID() uint32 {
return e.entry
}
// TenantTable implementation
func (tt tenantTable) AddEntry(value1, value2 string) TenantEntry {
e := &entry{table:table, id: generateUniqueID(), value1: value1, value2: value2})
tt.table.entries[e.id] = e
return TenantEntry{e}
}
// TenantEntry implementation
func (te tenantEntry) GetValue1() string {
return te.entry.value1
}
func (te tenantEntry) SetValue1(str string) {
te.entry.value1 = str
}
func (te tenantEntry) GetValue2() string {
return te.entry.value2
}
func (te tenantEntry) SetValue2(str string) {
te.entry.value2 = str
}
func (te tenantEntry) Table() TenantTable {
return tenantTable{te.entry.table}
}
// ReadOnlyTable implementation
func (rot readOnlyTable) GetEntry(id uint32) ReadOnlyEntry {
return rot.table[id]
}
func (rot readOnlyTable) ID() uint32 {
return rot.table.id
}
// ReadOnlyEntry implementation
func (roe readOnlyEntry) GetValue1() string {
return roe.entry.value1
}
func (roe readOnlyEntry) GetValue2() string {
return roe.entry.value2
}
func (roe readOnlyEntry) Table() ReadOnlyTable {
return readOnlyTable{roe.entry.table}
}
func (roe readOnlyEntry) ID() uint32 {
return roe.entry.id
}
This is significantly more concise
// implements AdvancedTable, TenantTable, & ReadOnlyTable
type table struct {
id uint32
entries map[uint32]*entry
}
// implements AdvancedEntry, TenantEntry, & ReadOnlyEntry
type entry struct {
table *table
id uint32
value1 string
value2 string
}
Single implementation for multiple interfaces
// Advanced-, Tenant-, & ReadOnly-Table implementation
func (t *table) AddEntry(value1, value2 string) *entry {
e := &entry{table:table, id: generateUniqueID(), value1: value1, value2: value2})
t.entries[e.id] = e
return e
}
func (t *table) GetEntry(id uint32) *entry {
return t.entries[id]
}
func (t *table) ID() uint32 {
return t.id
}
// Advanced-, Tenant-, & ReadOnly-Entry implementation
func (e *entry) GetValue1() string {
return e.value1
}
func (e *entry) SetValue1(str string) {
e.value1 = str
}
func (e *entry) GetValue2() string {
return e.value2
}
func (e *entry) SetValue2(str string) {
e.value2 = str
}
func (e *entry) Table() *table {
return e.table
}
func (e *entry) ID() uint32 {
return e.id
}
This is significantly shorter, clearer, and still maintains isolation.