Skip to content

Commit

Permalink
docs: README updates
Browse files Browse the repository at this point in the history
  • Loading branch information
bryanchriswhite committed Jun 30, 2023
1 parent 05f8f91 commit 8a3155b
Showing 1 changed file with 178 additions and 106 deletions.
284 changes: 178 additions & 106 deletions shared/modules/doc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,47 +28,22 @@ This document outlines how we structured the code by splitting it into modules,

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt).

### Class Diagram Overview

```mermaid
classDiagram
### Module

class IntegrableModule {
<<interface>>
+GetBus() Bus
+SetBus(bus Bus)
}
A module is an abstraction (go interface) of a self-contained unit of functionality that carries out a specific task/set of tasks with the idea of being reusable, modular, replacable, mockable, testable and exposing a clear and concise API.

class ModuleFactoryWithOptions {
<<interface>>
Create(bus Bus, options ...ModuleOption) (Module, error)
}
A module MAY implement 0..N common interfaces.

class Module {
<<interface>>
}
class InjectableModule {
<<interface>>
GetModuleName() string
}
class InterruptableModule {
<<interface>>
Start() error
Stop() error
}
Each *shared* module MUST be registered with the module registry such that it can be retrieved via the bus.
You can find additional details in the [Modules in detail](#submodules-in-detail) section below.

Module --|> InjectableModule
Module --|> IntegrableModule
Module --|> InterruptableModule
Module --|> ModuleFactoryWithOptions
### Module mock

class Submodule {
<<interface>>
}
A mock is a stand-in, a fake or simplified implementation of a module that is used for testing purposes.
It is used to simulate the behaviour of the module and to verify that the module is interacting with other modules correctly.
Mocks are generated using `go:generate` directives together with the [`mockgen` tool](https://pkg.go.dev/github.com/golang/mock#readme-running-mockgen).o

Submodule --|> InjectableModule
Submodule --|> IntegrableModule
```
A global search of `mockgen` in the codebase will provide examples of where and how it's used.

### Shared module interfaces

Expand All @@ -81,33 +56,6 @@ There are some interfaces that are common to multiple modules and we followed th
These interfaces that can be embedded in modules are defined in `shared/modules/module.go`.
GoDoc comments will provide you with more information about each interface.

### Factory interfaces

In order to formalize and support the generalization of modules and submodule while still providing flexibility, we defined a set of generic factory interfaces which can be embedded or used directly to enforce consistent constructor semantics.
These interfaces are [defined in `shared/modules/factory.go`](../factory.go) and outlined convenience:

- `ModuleFactoryWithOptions`: Specialized factory interface for modules conforming to the "typical" signature above. Useful when creating modules that only require optional runtime configurations.
- `FactoryWithConfig`: A generic type, used to create a module with a specific configuration type.
- `FactoryWithOptions`: The generic form used to define `ModuleFactoryWithConfig`.
- `FactoryWithConfigAndOptions`: Another generic type which combines the capabilities of the previous two. Suitable for creating modules that require both specific configuration and optional runtime configurations.

### Module

A module is an abstraction (go interface) of a self-contained unit of functionality that carries out a specific task/set of tasks with the idea of being reusable, modular, replacable, mockable, testable and exposing a clear and concise API.

A module MAY implement 0..N common interfaces.

Each *shared* module MUST be registered with the module registry such that it can be retrieved via the bus.
You can find additional details in the [Modules in detail](#submodules-in-detail) section below.

### Module mock

A mock is a stand-in, a fake or simplified implementation of a module that is used for testing purposes.
It is used to simulate the behaviour of the module and to verify that the module is interacting with other modules correctly.
Mocks are generated using `go:generate` directives together with the [`mockgen` tool](https://pkg.go.dev/github.com/golang/mock#readme-running-mockgen).o

A global search of `mockgen` in the codebase will provide examples of where and how it's used.

### Base module

A base module is a module that implements a common interface, exposing the most basic logic.
Expand All @@ -121,45 +69,83 @@ You can find the base modules in the `shared/modules/base_modules` package.
A submodule is a self-contained unit of functionality which is composed by a module and is necessary for that module to function.
Each module SHOULD be registered with the module registry such that it can be retrieved via the bus.

### Example Submodule Class Diagram
### Shared submodule

A shared submodule is any submodule which is a depencency of more than one module.
Shared submodules MUST define their respective interface type in the `shared/modules` package to avoid import cycles.

_NOTE: "interface types" of "shared submodules" are distinctly different from "shared module interfaces"._

### Class Diagram Legend

```mermaid
classDiagram
direction LR
class concreteType
class InterfaceType {
<<interface>>
InterfaceMethod()
}
concreteType --|> InterfaceType : realizes interface
concreteType ..|> InterfaceType : realizes one of (this)
concreteType ..|> InterfaceType : realizes one of (or this)
```

### Module, Submodule & Shared Interfaces

```mermaid
classDiagram
direction TB
class IntegrableModule {
<<interface>>
+GetBus() Bus
+SetBus(bus Bus)
GetBus() Bus
SetBus(bus Bus)
}
class ModuleFactoryWithOptions {
<<interface>>
Create(bus Bus, options ...ModuleOption) (Module, error)
}
class Module {
<<interface>>
}
class InjectableModule {
<<interface>>
GetModuleName() string
}
class InterruptableModule {
<<interface>>
Start() error
Stop() error
}
Module --|> InjectableModule
Module --|> IntegrableModule
Module --|> InterruptableModule
Module --|> ModuleFactoryWithOptions
class Submodule {
<<interface>>
}
Submodule --|> InjectableModule
Submodule --|> IntegrableModule
```

class ExampleSubmoduleInterface {
<<interface>>
...
}
class exampleSubmodule
class exampleSubmoduleFactory {
<<interface>>
Create(...) (ExampleSubmoduleInterface, error)
}
### Factory interfaces

exampleSubmodule --|> ExampleSubmoduleInterface
ExampleSubmoduleInterface --|> Submodule : (embed)
ExampleSubmoduleInterface --|> exampleSubmoduleFactory :(embed)
%% exampleSubmodule --|> exampleSubmoduleFactory
```
In order to formalize and support the generalization of modules and submodule while still providing flexibility, we defined a set of generic factory interfaces which can be embedded or used directly to enforce consistent constructor semantics.
These interfaces are [defined in `shared/modules/factory.go`](../factory.go) and outlined convenience:

- `ModuleFactoryWithOptions`: Specialized factory interface for modules conforming to the "typical" signature above. Useful when creating modules that only require optional runtime configurations.
- `FactoryWithConfig`: A generic type, used to create a module with a specific configuration type.
- `FactoryWithOptions`: The generic form used to define `ModuleFactoryWithConfig`.
- `FactoryWithConfigAndOptions`: Another generic type which combines the capabilities of the previous two. Suitable for creating modules that require both specific configuration and optional runtime configurations.

## Code Organization

Expand All @@ -175,10 +161,24 @@ classDiagram

## (Sub)Modules in detail

We structured the code so that each module has its interface (and supporting interfaces, if any) defined in the `shared/modules` package, where the file containing the module interface follows the naming convention `[moduleName]_module.go`.
#### Shared (sub)module interfaces

Each module (and shared submodule) defined its module interface (and supporting interfaces, if any) defined in the `shared/modules` package, where the file containing the module interface follows the naming convention `[moduleName]_module.go`.
You can start by looking at the interfaces of the modules we already implemented to get a better idea of what a module is and how it should be structured.
You might notice that these files include `go:generate` directives, these are used to generate the module mocks for testing purposes.

#### Construction parameters & non-(sub)module dependencies

Modules and submodules MAY have zero or more REQUIRED and/or OPTIONAL construction parameters. Often (sub)modules are defined in terms of such parameters to allow for flexibility in their creation and/or usage in different scenarios (e.g. identity, actor type, etc.).

_NOTE: Would-be dependencies/parameters which are modules or submodules should be retrieved via the [module registry](#modules-registry) instead._

| | Module | Submodule |
|------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
| **has no construction parameters** | Constructor MUST receive and SHOULD call option function arguments. | Submodule SHOULD use a factory interface type based on `Factory`. |
| **has REQUIRED construction parameters** | Parameter arguments MUST be represented as primitive/serializable types & consolidated into a protobuf type to be included in the runtime module's config. Subsequently referenced via the bus. High-level objects MAY be derived in the module constructor. | Parameter arguments MUST be consolidated and passed as a single "config" type using a factory interface type based on `FactoryWithConfig`. |
| **has OPTIONAL construction parameters** | Constructor MUST receive and SHOULD call option function arguments. | Constructor MUST receive option function arguments using a factory interface type based on `FactoryWithOptions`. |

### Module creation

Modules MUST implement the `ModuleFactoryWithConfig` flavor of [factory interfaces](#factory-interfaces), enforcing the following constructor signature:
Expand All @@ -190,19 +190,30 @@ Where `options` is an (optional) variadic argument that allows for the passing o
This is useful to configure the module at creation time.

Each module constructor MUST receive this variadic option argument as all modules share this interface.
Additionally, each module MUST consider these options (i.e. loop over and call them) as support may be added for new options at any time.
Additionally, each module SHOULD consider these options (i.e. loop over and call them) as support may be added for new options at any time.

See `ModuleFactoryWithOptions`, also defined in `shared/modules/factory.go`.

### Module options vs configs
### Module configs & options

_(see: [constructor parameters](#construction-parameters--non-submodule-dependencies))_

Modules MAY depend on **optional** values; i.e. not necessary in order for a module to function properly.

An option function type MUST be defined for each optional value dependency.
Such option functions receive the module for option assignment by implementing `ModuleOption`.
Such option functions receive the module for option assignment by implementing `ModuleOption`:

```go
type WithFooOption func(foo string) ModuleOption {
return func(m InjectableModule) {
m.(*FooModule).SetFoo(foo)
}
}
```

Module MAY depend on some **required** values; i.e. necessary in order for a module to function properly.
Any such required values MUST be composed into a single config struct which can be received via the respective factory interface type.

Any such required values MUST be composed into a single "config" struct which can be received via the respective factory interface type.

Such a config type SHOULD implement a `#IsValid()` method which is likely called in the module constructor.

Expand Down Expand Up @@ -230,61 +241,122 @@ Depending on your submodule's requirements, you should choose the most suitable

Each submodule factory interface type SHOULD return an interface type for the submodule; however it MAY be appropriate to return a concrete type in some cases.

### Submodule options
```mermaid
classDiagram
direction TB
Submodules MAY depend on **optional** values.
class IntegrableModule {
<<interface>>
+GetBus() Bus
+SetBus(bus Bus)
}
class InjectableModule {
<<interface>>
GetModuleName() string
}
An option function type MUST be defined for each optional value dependency.
Such option functions MUST receive the submodule for option assignment (similar to `ModuleOption`) in accordance with the respective factory interface type.
class Submodule {
<<interface>>
}
Option functions SHOULD be written in terms of interface submodule types but MAY instead be written in terms of concrete submodule types if they are only intended or only make sense to be used with a specific concrete submodule type.
Submodule --|> InjectableModule
Submodule --|> IntegrableModule
Submodules SHOULD NOT consider options when none exist, this should be reflected in the respective factory interface type applied (i.e. no variadic options argument).
class ExampleSubmoduleInterface {
<<interface>>
...
}
class exampleSubmodule
class exampleSubmoduleFactory {
<<interface>>
Create(...) (ExampleSubmoduleInterface, error)
}
exampleSubmodule --|> ExampleSubmoduleInterface
ExampleSubmoduleInterface --|> Submodule : (embed)
ExampleSubmoduleInterface --|> exampleSubmoduleFactory :(embed)
exampleSubmoduleFactory ..|> Factory
exampleSubmoduleFactory ..|> FactoryWithConfig
exampleSubmoduleFactory ..|> FactoryWithOptions
exampleSubmoduleFactory ..|> FactoryWithConfigAndOptions
```

### Submodule configs
### Submodule configs & options

_(see: [constructor parameters](#construction-parameters--non-submodule-dependencies))._

#### Configs

Submodules MAY also depend on some **required** values; i.e. necessary in order for a submodule to function properly.
Any such values MUST be composed in to a single config struct in accordance with the respective factory interface type.

#### Options

Submodules MAY depend on **optional** values; i.e. not necessary in order for a submodule to function properly.

An option function type MUST be defined for each optional value dependency.
Such option functions MUST receive the submodule for option assignment (similar to `ModuleOption`) in accordance with the respective factory interface type.

Option functions SHOULD be written in terms of interface submodule types but MAY instead be written in terms of concrete submodule types if they are only intended or only make sense to be used with a specific concrete submodule type.

Submodules SHOULD NOT consider options when none exist, this should be reflected in the respective factory interface type applied (i.e. no variadic options argument).

### Comprehensive Submodule Example:
```go
var _ MySubmoduleInterface = &mySubmodule{}
// Enforce the submodule interface type
var _ ExampleSubmoduleInterface = &exampleSubmodule{}

type MySubmoduleInterface interface {
// Submodule interface type, embedding the submodule &
// submodule factory interface types.
type ExampleSubmoduleInterface interface {
modules.Submodule
mySubmoduleFactory
exampleSubmoduleFactory
}

type mySubmoduleConfig struct {
// "Config" struct for the submodule.
type exampleSubmoduleConfig struct {
someRequiredValue func(data []byte) error
}
type mySubmoduleOption func(*mySubmodule)
type mySubmoduleFactory = FactoryWithConfigAndOptions[
MySubmoduleInterface,
mySubmoduleConfig,
mySubmoduleOption

// Option function type for the submodule.
type exampleSubmoduleOption func(*exampleSubmodule)

// Submodule factory interface type.
type exampleSubmoduleFactory = FactoryWithConfigAndOptions[
ExampleSubmoduleInterface,
exampleSubmoduleConfig,
exampleSubmoduleOption
]

type mySubmodule struct {
// Concrete submodule type.
type exampleSubmodule struct {
someRequiredValue func(data []byte) error
someOptionalValue string
}

func (*mySubmodule) Create(bus modules.Bus, cfg mySubmoduleConfig, opts ...mySubmoduleOption) (MySubmoduleInterface, error) {
sub := &mySubmodule{
// Create implements exampleSubmoduleFactory.
func (*exampleSubmodule) Create(
bus modules.Bus,
cfg exampleSubmoduleConfig,
opts ...exampleSubmoduleOption
) (ExampleSubmoduleInterface, error) {
// Consider config values
sub := &exampleSubmodule{
someRequiredValue: cfg.someRequiredValue,
}

// Apply option functions.
for _, opt := range opts {
opt(sub)
}
return sub, nil
}

func WithSomeOption(someOption string) mySubmoduleOption {
return func(s *mySubmodule) {
// WithSomeOption returns an option function which assigns
// `someOption` to the submodule.
func WithSomeOption(someOption string) exampleSubmoduleOption {
return func(s *exampleSubmodule) {
s.someOptionalValue = someOption
}
}
Expand Down

0 comments on commit 8a3155b

Please sign in to comment.