From 29895e43ab43600cfe75bf3bbf922a59a966559f Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Wed, 29 May 2019 17:16:57 -0400 Subject: [PATCH] program structure: wrote instantiate vs initialize section, pt2 almost done --- .../program-structure-and-composability.md | 165 ++++++++++++++++-- 1 file changed, 152 insertions(+), 13 deletions(-) diff --git a/_drafts/program-structure-and-composability.md b/_drafts/program-structure-and-composability.md index 7968405..0af4e99 100644 --- a/_drafts/program-structure-and-composability.md +++ b/_drafts/program-structure-and-composability.md @@ -120,7 +120,7 @@ var globalConns = map[string]*RedisConn{} func Get(name string) *RedisConn { if globalConns[name] == nil { - globalConns[name] = makeConnection(name) + globalConns[name] = makeRedisConnection(name) } return globalConns[name] } @@ -316,6 +316,8 @@ it is necessary to implement some helper functions. Here are their function signatures: ```go +// Package mctx + // NewChild creates and returns a new Context based off of the parent one. The // child will have a path which is the parent's path appended with the given // name. @@ -333,7 +335,9 @@ these functions have been implemented in a package called `mctx`. Here's an example of how `mctx` might be used in the `redis` component's code: ```go -func NewRedis(ctx context.Context, defaultAddr string) *RedisConn { +// Package redis + +func NewConn(ctx context.Context, defaultAddr string) *RedisConn { ctx = mctx.NewChild(ctx, "redis") ctxPath := mctx.Path(ctx) paramPrefix := strings.Join(ctxPath, "-") @@ -357,10 +361,12 @@ In our above example, the two `redis` components' parameters would be: The prefix joining stuff will probably get annoying after a while though, so let's invent a new package, `mcfg`, which acts like `flag` but is aware of -`mctx`. Then `NewRedis` is reduced to: +`mctx`. Then `redis.NewConn` is reduced to: ```go -func NewRedis(ctx context.Context, defaultAddr string) *RedisConn { +// Package redis + +func NewConn(ctx context.Context, defaultAddr string) *RedisConn { ctx = mctx.NewChild(ctx, "redis") addrParam := flag.String(ctx, "-addr", defaultAddr, "Address of redis instance to connect to") // finish setup @@ -372,29 +378,29 @@ func NewRedis(ctx context.Context, defaultAddr string) *RedisConn { Sharp-eyed gophers will notice that there's a key piece missing: When is `mcfg.Parse` called? When does `addrParam` actually get populated? Because you can't create the redis connection until that happens, but that can't happen -inside `NewRedis` because there might be other things after `NewRedis` which -want to set up parameters. To illustrate the problem, let's look at a simple -program which wants to set up two `redis` components: +inside `redis.NewConn` because there might be other things after `redis.NewConn` +which want to set up parameters. To illustrate the problem, let's look at a +simple program which wants to set up two `redis` components: ```go func main() { - // Create the root context, and empty Context. + // Create the root context, an empty Context. ctx := context.Background() // Create the Contexts for two sub-components, foo and bar. ctxFoo := mctx.NewChild(ctx, "foo") ctxBar := mctx.NewChild(ctx, "bar") - // Now we want to try to create a redis instances for each component. But... + // Now we want to try to create a redis sub-component for each component. // This will set up the parameter "--foo-redis-addr", but bar hasn't had a // chance to set up its corresponding parameter, so the command-line can't // be parsed yet. - fooRedis := redis.NewRedis(ctxFoo, "127.0.0.1:6379") + fooRedis := redis.NewConn(ctxFoo, "127.0.0.1:6379") // This will set up the parameter "--bar-redis-addr", but, as mentioned - // before, NewRedis can't parse command-line. - barRedis := redis.NewRedis(ctxBar, "127.0.0.1:6379") + // before, redis.NewConn can't parse command-line. + barRedis := redis.NewConn(ctxBar, "127.0.0.1:6379") // If the command-line is parsed here, then how can fooRedis and barRedis // have been created yet? Creating the redis connection depends on the addr @@ -404,4 +410,137 @@ func main() { We will solve this problem in the next section. -## Init vs. Start +### Instantiation vs Initialization + +Let's break down `redis.NewConn` into two phases: instantiation and initialization. +Instantiation refers to creating the component on the component structure and +having it declare what it needs in order to initialize. After instantiation +nothing external to the program has been done; no IO, no reading of the +command-line, no logging, etc... All that's happened is that the empty shell of +a `redis` component has been created. + +Initialization is the phase when that shell is filled. Configuration parameters +are read, startup actions like the creation of database connections are +performed, and logging is output for informational and debugging purposes. + +The key to making effective use of this dichotemy is to allow _all_ components +to instantiate themselves before they initialize themselves. By doing this we +can ensure that, for example, all components have had the chance to declare +their configuration parameters before configuration parsing is done. + +So let's modify `redis.NewConn` so that it follows this dichotemy. It makes +sense to leave instantiation related code where it is, but we need a mechanism +by which we can declare initialization code before actually calling it. For +this, I will introduce the idea of a "hook". + +A hook is, simply a function which will run later. We will declare a new +package, calling it `mrun`, and say that it has two new functions: + +```go +// Package mrun + +// WithInitHook returns a new Context based off the passed in one, with the // +given hook registered to it. +func WithInitHook(ctx context.Context, hook func()) context.Context + +// Init runs all hooks registered using WithInitHook. Hooks are run in the order +// they were registered. +func Init(ctx context.Context) +``` + +With these two functions we are able to defer the initialization phase of +startup by using the same Contexts we were passing around for the purpose of +denoting component structure. One thing to note is that, since hooks are being +registered onto Contexts within the component instantiation code, the parent +Context will not know about these hooks. Therefore it is necessary to add the +child component's Context back into the parent. To do this we add two final +functions to the `mctx` package: + +```go +// Package mctx + +// WithChild returns a copy of the parent with the child added to it. Children +// of a Context can be retrieved using the Children function. +func WithChild(parent, child context.Context) context.Context + +// Children returns all child Contexts which have been added to the given one +// using WithChild, in the order they were added. +func Children(ctx context.Context) []context.Context +``` + +Now, with these few extra pieces of functionality in place, let's reconsider the +most recent example, and make a program which creates two redis components which +exist independently of each other: + +```go +// Package redis + +// NOTE that NewConn has been renamed to WithConn, to reflect that the given +// Context is being returned _with_ a redis component added to it. + +func WithConn(parent context.Context, defaultAddr string) (context.Context, *RedisConn) { + ctx = mctx.NewChild(parent, "redis") + + // we instantiate an empty RedisConn instance and parameters for it. Neither + // has been initialized yet. They will remain empty until initialization has + // occurred. + redisConn := new(RedisConn) + addrParam := flag.String(ctx, "-addr", defaultAddr, "Address of redis instance to connect to") + + ctx = mrun.WithInitHook(ctx, func() { + // This hook will run after parameter initialization has happened, and + // so addrParam will be usable. redisConn will be usable after this hook + // has run as well. + *redisConn = makeRedisConnection(*addrParam) + }) + + // Now that ctx has had configuration parameters and intialization hooks + // instantiated into it, return both it and the empty redisConn instance + // back to the parent. + return mctx.WithChild(parent, ctx), redisConn +} + +//////////////////////////////////////////////////////////////////////////////// + +// Package main + +func main() { + // Create the root context, an empty Context. + ctx := context.Background() + + // Create the Contexts for two sub-components, foo and bar. + ctxFoo := mctx.NewChild(ctx, "foo") + ctxBar := mctx.NewChild(ctx, "bar") + + // Add redis components to each of the foo and bar sub-components. The + // returned Contexts will be used to initialize the redis components. + ctxFoo, redisFoo := redis.WithConn(ctxFoo, "127.0.0.1:6379") + ctxBar, redisBar := redis.WithConn(ctxBar, "127.0.0.1:6379") + + // Add the sub-component contexts back to the root, so they can all be + // initialized at once. + ctx = mctx.WithChild(ctx, ctxFoo) + ctx = mctx.WithChild(ctx, ctxBar) + + // Parse will descend into the Context and all of its children, discovering + // all registered configuration parameters and filling them from the + // command-line. + mcfg.Parse(ctx) + + // Now that configuration has been initialized, run the Init hooks for each + // of the sub-components. + mrun.Init(ctx) + + // At this point the redis components have been fully initialized and may be + // used. For this example we'll copy all keys from one to the other. + keys := redisFoo.Command("KEYS", "*") + for i := range keys { + val := redisFoo.Command("GET", keys[i]) + redisBar.Command("SET", keys[i], val) + } +} +``` + +### Full example + +## Part 3: Annotations, Logging, and Errors