Hi, Peter.
Thank you so much for your reply. I hadn't thought of that option. I like it! It makes it quite obvious which version of the object you are dealing with. With a little front-end detection work (to tell which version of the object you are dealing with), you can restore the object from storage, and re-create an object of the appropriate version. And, it will be easy to convert one version of an object to another version; just put a function in the package to upgrade the object.
I also like it because you just keep including the same package, without having to worry about which version of the package to include. The package can evolve as needed, and you can incrementally add new features.
I'm wondering if I can embed the version number inside the object, have methods on the object detect the object version, and implement the appropriate behavior based on the object version.
What do you think of this architecture?
package user
const (
latestVersion string = "v3"
)
type User struct {
VersionNumber string
Properties map[string]interface{}
}
// for functionality backward compatible to all versions
func (u *User) SomeAction (inputs...) (outputs..., error) {
switch u.VersionNumber {
case "v1": {code for v1 object}
case "v2": {code for v2 object}
case "v3": {code for v2 object}
}
}
// for old functionality that won’t work with the latest version
func (u *User) SomeOldAction (inputs...) (outputs..., error) {
switch u.VersionNumber {
case "v1": {code for v1 object}
case "v2": {code for v2 object}
case "v3": return errors.New("v3 not implemented.")
}
}
// for new functionality that won't work with the oldest version
func (u *User) SomeNewAction (inputs...) (outputs..., error) {
switch u.VersionNumber {
case "v1": return errors.New("v1 not implemented.")
case "v2": {code for v2 object}
case "v3": {code for v3 object}
}
}
func (u *User) UpgradeToVersion (newVersion string) (outputs..., error) }
if u.VersionNumber == latestVersion { return ... }
switch u.VersionNumber + ":" + newVersion {
case "v1:v2": {code to convert v1 to v2}
case "v2:v3": {code to convert v2 to v3}
}
}
The main issue I see is that it is more complicated, and complex is bad. Simple is good.
Another issue I see is that the use of maps makes it not safe for concurrency.
A possible solution would be to create a goroutine for each object, and serialize access via a channel.
If the goroutine needs to return an object to the caller, make a local copy of the official object, and return the local copy to the caller.
That way, the official object remains under the control of the goroutine.
Cons:
One goroutine per object would be a lot of goroutines and channels.
No obvious way to decide when to terminate a goroutine.
If you don't already know the correct channel for your object, finding the correct channel would be challenging.
Does anyone have any other ideas for making it safe for concurrency?
Does anyone see any other potential issues with it?