Applying config injection to the model layer

Revisiting our register package, we see that it has references to both config and logging:

// Registerer validates the supplied person, calculates the price in 
// the requested currency and saves the result.
// It will return an error when:
// -the person object does not include all the fields
// -the currency is invalid
// -the exchange rate cannot be loaded
// -the data layer throws an error.
type Registerer struct {
}

// get price in the requested currency
func (r *Registerer) getPrice(ctx context.Context, currency string) (float64, error) {
converter := &exchange.Converter{}
price, err := converter.Do(ctx, config.App.BasePrice, currency)
if err != nil {
logging.L.Warn("failed to convert the price. err: %s", err)
return defaultPersonID, err
}

return price, nil
}

Our first step is to define an interface that will supply the dependencies we need:

// Config is the configuration for the Registerer
type Config interface {
Logger() *logging.LoggerStdOut
BasePrice() float64
}

Do you see anything wrong with this? The first thing that jumps out is the fact that our Logger() method returns a pointer to a logger implementation. This will work, but it's not very future proof or testable. We could define a logging interface locally and decouple ourselves entirely from the logging package. This would mean, however, that we would have to define a logging interface in most of our packages. Theoretically, this is the best option, but it is not very practical. Instead, we could define one logging interface and have all of the packages depend upon that. While this will mean that we still remained coupled to the logging package, we will rely on an interface that seldom changes, rather than an implementation that is far more likely to change.

The second potential issue is the naming of the other method, BasePrice(), because it's somewhat generic, and a potential source of confusion later on. It is also the name of the field in the Config struct but Go will not allow us to have a member variable and method with the same name, so we will need to change that.

After updating our config interface, we have the following:

// Config is the configuration for the Registerer
type Config interface {
Logger() logging.Logger
RegistrationBasePrice() float64
}

We can now apply config injection to our Registerer, giving us the following:

// NewRegisterer creates and initializes a Registerer
func NewRegisterer(cfg Config) *Registerer {
return &Registerer{
cfg: cfg,
}
}

// Config is the configuration for the Registerer
type Config interface {
Logger() logging.Logger
RegistrationBasePrice() float64
}

// Registerer validates the supplied person, calculates the price in
// the requested currency and saves the result.
// It will return an error when:
// -the person object does not include all the fields
// -the currency is invalid
// -the exchange rate cannot be loaded
// -the data layer throws an error.
type Registerer struct {
cfg Config
}

// get price in the requested currency
func (r *Registerer) getPrice(ctx context.Context, currency string) (float64, error) {
converter := &exchange.Converter{}
price, err := converter.Do(ctx, r.cfg.RegistrationBasePrice(), currency)
if err != nil {
r.logger().Warn("failed to convert the price. err: %s", err)
return defaultPersonID, err
}

return price, nil
}

func (r *Registerer) logger() logging.Logger {
return r.cfg.Logger()
}

I have also added a convenience method, logger(), to reduce the code from r.cfg.Logger() to just r.logger(). Our service and tests are currently broken, so we have more changes to make.

To get the tests going again, we need to define a test configuration and update our tests. For our test configuration, we could use mockery and create a mock implementation, but we are not interested in validating our config usage or adding extra code to all of the tests in this package to configure the mock. Instead, we are going to use a stub implementation that returns predictable values. Here is our stub test config:

// Stub implementation of Config
type testConfig struct{}

// Logger implement Config
func (t *testConfig) Logger() logging.Logger {
return &logging.LoggerStdOut{}
}

// RegistrationBasePrice implement Config
func (t *testConfig) RegistrationBasePrice() float64 {
return 12.34
}

And add this test config to all of our Registerer tests, as shown in the following code:

registerer := &Registerer{
cfg: &testConfig{},
}

Our tests are running again, but strangely, while our service compiles, it would crash with a nil pointer exception if we were to run it. We need to update the creation of our Registerer from the following: 

registerModel := &register.Registerer{}

 We change it to this:

registerModel := register.NewRegisterer(config.App)

This leads us to the next problem. The config.App struct does not implement the methods we need. Adding these methods to config, we get the following:

// Logger returns a reference to the singleton logger
func (c *Config) Logger() logging.Logger {
if c.logger == nil {
c.logger = &logging.LoggerStdOut{}
}

return c.logger
}

// RegistrationBasePrice returns the base price for registrations
func (c *Config) RegistrationBasePrice() float64 {
return c.BasePrice
}

With these changes, we have severed the dependency link between the registration package and the config package. In the Logger() method we have illustrated previously, you can see we are still using the logger as a singleton, but instead of being a global public variable, which would be prone to data races, it's now inside the config object. On the surface, this might not seem like it made any difference; however, the data races we were primarily concerned about were during testing. Our object now relies on an injected version of the logger and is not required to use the global public variable.

Here, we examine our updated dependency graph to see where to go next:

We are down to three links into the config package; that is, those from the main, data, and exchange packages. The link from the main package cannot be removed, hence, we can ignore that. So, let's look into the data package.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.139.83.199