-
Notifications
You must be signed in to change notification settings - Fork 246
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(go): extension & overrides mechanism (#2717)
This implements and documents a mechanism for supporting class extension and overrides in go, where such concepts are not endemic. This uses a new "overriding" constructor generated for open classes, and a special tag users should use to specify which members are overridden on the base type. --- By submitting this pull request, I confirm that my contribution is made under the terms of the [Apache 2.0 license]. [Apache 2.0 license]: https://www.apache.org/licenses/LICENSE-2.0
- Loading branch information
1 parent
fb9abf9
commit 33f3b26
Showing
25 changed files
with
1,986 additions
and
639 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
1 change: 1 addition & 0 deletions
1
gh-pages/content/user-guides/lib-user/language-specific/.pages.yml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
--- | ||
title: Language Specific | ||
nav: | ||
- Go: go.md | ||
- Python: python.md |
202 changes: 202 additions & 0 deletions
202
gh-pages/content/user-guides/lib-user/language-specific/go.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
# Go | ||
|
||
!!! danger | ||
The **go** target is currently unstable and not suitable for production use. | ||
|
||
**Go** is not a common object-oriented language: the language currently only | ||
supports composition, not extension. On the other hand, the | ||
[*jsii type system*][type-system] includes *classes* and *interfaces*, which are | ||
typically associated with extension-based programming models. | ||
|
||
In rare circumstances, **Go** developers may find themselves in a situation | ||
where they must implement an *abstract base class*, extend some *class* in order | ||
to *override* a method or property, or implement a *jsii interface*. | ||
|
||
[type-system]: ../../../specification/2-type-system.md | ||
|
||
## Implementing *jsii interfaces* | ||
|
||
Implementing *jsii interfaces* leverages the idiomatic **go** way to implement | ||
interfaces: define all the necessary methods on the implementing **go** struct, | ||
and the value can be used naturally. | ||
|
||
There is a single restriction: all such implementation methods must be defined | ||
using a *pointer receiver*, or a runtime error may occur: | ||
|
||
Assuming you are consuming a *jsii* module that defines the following: | ||
|
||
```go | ||
package jsiimodule | ||
|
||
import ( | ||
"fmt" | ||
) | ||
|
||
type IGreeter interface { | ||
Greet(greetee string) | ||
} | ||
|
||
// ... | ||
func NewMajestyGreeter(greeter IGreeter) MajestyGreeter { | ||
// Omitted for brevity | ||
} | ||
|
||
// Does something with the IGreeter that was provided at construction time | ||
func (m MajestyGreeter) Announce(who string) { | ||
m.greeter.Greet(fmt.Sprintf("Your Royal Highness %s", who)) | ||
} | ||
``` | ||
|
||
You can implement this interface natively in go as: | ||
|
||
```go | ||
package main | ||
|
||
import ( | ||
"fmt" | ||
|
||
"example/jsiimodule" | ||
) | ||
|
||
type greeter struct { | ||
_ byte // padding | ||
} | ||
|
||
// IMPORTANT - this function has a pointer receiver! | ||
func (g *greeter) Greet(greetee string) { | ||
fmt.Printf("Hello, %s!\n", greetee) | ||
} | ||
|
||
func main() { | ||
g := &greeter{} | ||
|
||
// Simply pass the instance though, it "just works". | ||
mg := jsiimodule.NewMajestyGreeter(g) | ||
|
||
mg.Announce("Elizabeth II") | ||
} | ||
``` | ||
|
||
## Extending and overriding *classes* | ||
|
||
!!! important | ||
Leveraging extension and override goes against the design principles of the | ||
**go** programming language. We advise you avoid using this mechanism unless | ||
you have determined that there is no way to achieve the desired result with | ||
composition. | ||
|
||
In particular, if the only element you need to override on a *class* is it's | ||
*constructor*, you should simply *decorate* this constructor instead of | ||
using the extension and overrides mechanism. For example you can declare an | ||
AWS CDK construct (that does not declare new properties or methods) in the | ||
following way: | ||
|
||
```go | ||
package cdkapp | ||
|
||
import ( | ||
"github.com/aws/aws-cdk-go" | ||
"github.com/aws/aws-cdk-go/aws-s3" | ||
) | ||
|
||
// Optional: alias the type for clarity | ||
type CustomBucket s3.Bucket | ||
|
||
// Imagine this builds an S3 bucket with "special" defaults. It does not | ||
// accept s3.BucketProps, instead those are hard-coded in the constructor | ||
// itself. It could also accept a different properties object, to allow for | ||
// user settings? | ||
func NewCustomBucket(scope core.Construct, id string) CustomBucket { | ||
return s3.NewBucket(scope, id, s3.BucketProps{ | ||
// ... customized properties | ||
}) | ||
} | ||
``` | ||
|
||
*Classes* that are open for *extension* (including *abstract base classes*) have | ||
a special *overriding* constructor that can be used when building sub-classes. | ||
This *override* constructor is expected to be called from within the child | ||
*class* constructor that you are writing. This constructor is named using the | ||
following convention: `New<ClassName>_Override`, and receives the *overriding* | ||
struct instance as the first parameter. | ||
|
||
The **go** `struct` that *extends* the base *jsii class* must anonymously embed | ||
the *jsii class*' **go** interface, while specifying the `overrides:"..."` field | ||
tag. The value of the tag is a comma-separated list of **go** methods that are | ||
locally overridden (note: in the case of *property accessors*, the *getter* name | ||
is to be used, even if only the *setter* is overridden). | ||
|
||
Assuming the following abstract base class: | ||
|
||
```go | ||
package jsiimodule | ||
|
||
type AbstractBaseClass interface { | ||
// Those members have implementations provided, you *may* override them | ||
ConcreteMethod() bool | ||
ConcreteProperty() string | ||
SetConcreteProperty(v string) | ||
|
||
|
||
// Those members do not have implementations, you *must* implement them | ||
AbstractMethod() string | ||
AbstractReadonlyProperty() float64 | ||
} | ||
|
||
// NewAbstractBaseClass_Override initializes an overridden AbstractBaseClass | ||
// instance. The inst parameter receives the go struct that declares the | ||
// overrides, while the someString and someNumber are parameters to the abstract | ||
// base class' constructor. | ||
func NewAbstractBaseClass_Override(inst AbstractBaseClass, someString string, someNumber float64) { | ||
// Omitted for brevity | ||
} | ||
``` | ||
|
||
You can implement that abstract base class in go in the following way: | ||
|
||
```go | ||
package main | ||
|
||
import ( | ||
"fmt" | ||
"strings" | ||
|
||
"example/jsiimodule" | ||
) | ||
|
||
type childClass struct { | ||
// We implement AbstractMethod, AbstractReadonlyProperty, and override the setter for ConcreteProperty | ||
jsiimodule.AbstractBaseClass `overrides:"AbstractMethod,AbstractReadonlyProperty,ConcreteProperty"` | ||
|
||
// Our own storage | ||
stringValue string | ||
} | ||
|
||
// Provide your own constructor, which delegates to the base class' overriding | ||
// constructor. | ||
func NewChildClass(stringValue string, someString string, someNumber float64) jsiimodule.AbstractBaseClass { | ||
c := &childClass{stringValue: stringValue} | ||
|
||
// This will take care of setting childClass.AbstractBaseClass! | ||
jsiimodule.NewAbstractBaseClass_Override(c, someString, someNumber) | ||
|
||
return c | ||
} | ||
|
||
// Then implement the necessary members | ||
func (c *childClass) AbstractMethod() string { | ||
fmt.Println("childClass.AbstractMethod invoked!") | ||
return c.stringValue | ||
} | ||
|
||
func (c *childClass) AbstractReadonlyProperty() float64 { | ||
fmt.Println("childClass.ConcreteProperty read!") | ||
return 1337 | ||
} | ||
|
||
// And overrides those we decided to replace | ||
func (c *childClass) SetConcreteProperty(v string) { | ||
// We'll just up-case before delegating to the "super" implementation. | ||
c.AbstractBaseClass.SetConcreteProperty(strings.ToUpper(v)) | ||
} | ||
``` |
Oops, something went wrong.