Oftentimes systems that are created using a microservices architecture end up being assembled and installed as monoliths. Sometimes this is required as a transitional step, when the operations department isn’t quite yet ready to install and support such a fragmented system. It’s also common for startups, who usually have to deal with limited financial resources, to use this approach. Packing a large amount of microservices into a monolith allows teams to significantly reduce the amount of containers needed to get the system up and running. Such a system can easily be broken up into microservices in the future, when the startup is ready to support an increasing number of clients.
Direct clients are key to creating microservice-based monoliths. A direct client uses direct calls to the microservice’s service from the shared address space, bypassing external interfaces in the process. On this step, we are going to create such a client. We’ll be placing our code in the src/version1 folder.
First off, let’s define an interface for our clients to implement. This interface should contain a list of all the methods that are provided by our microservice’s API. As a result, we get the following code:
/version1/IBeaconClientV1.go
package clients1
import (
"context"
data1 "github.com/pip-services-samples/service-beacons-go/data/version1"
cquery "github.com/pip-services4/pip-services4-go/pip-services4-data-go/query"
)
type IBeaconsClientV1 interface {
GetBeacons(ctx context.Context, filter cquery.FilterParams,
paging cquery.PagingParams) (*cquery.DataPage[data1.BeaconV1], error)
GetBeaconById(ctx context.Context, beaconId string) (*data1.BeaconV1, error)
GetBeaconByUdi(ctx context.Context, udi string) (*data1.BeaconV1, error)
CalculatePosition(ctx context.Context, siteId string, udis []string) (*data1.GeoPointV1, error)
CreateBeacon(ctx context.Context, beacon data1.BeaconV1) (*data1.BeaconV1, error)
UpdateBeacon(ctx context.Context, beacon data1.BeaconV1) (*data1.BeaconV1, error)
DeleteBeaconById(ctx context.Context, beaconId string) (*data1.BeaconV1, error)
}
/src/version1/IBeaconClientV1.py
from typing import Optional, List, Any
from pip_services4_data.query import PagingParams, DataPage, FilterParams
from pip_services4_components.context import IContext
from src.data.version1 import BeaconV1
class IBeaconsClientV1:
def get_beacons_by_filter(self, context: Optional[IContext], filter: FilterParams,
paging: PagingParams) -> DataPage:
raise NotImplementedError('Method from interface definition')
def get_beacon_by_id(self, context: Optional[IContext], id: str) -> dict:
raise NotImplementedError('Method from interface definition')
def get_beacon_by_udi(self, context: Optional[IContext], udi: str) -> dict:
raise NotImplementedError('Method from interface definition')
def calculate_position(self, context: Optional[IContext], site_id: str, udis: List[str]) -> Any:
raise NotImplementedError('Method from interface definition')
def create_beacon(self, context: Optional[IContext], entity: BeaconV1) -> dict:
raise NotImplementedError('Method from interface definition')
def update_beacon(self, context: Optional[IContext], entity: BeaconV1) -> dict:
raise NotImplementedError('Method from interface definition')
def delete_beacon_by_id(self, context: Optional[IContext], id: str) -> dict:
raise NotImplementedError('Method from interface definition')
Let’s start writing our direct client. This will be a class that implements the interface we defined above, that has our service set as a dependency in the service, and that will call the service’s methods when asked to. To learn more about the referencing and linking mechanisms, be sure to read The Component References. Ultimately, this will just be a wrapper class for the container. The direct client’s code is listed below:
/version1/BeaconsDirectClientV1.go
package clients1
import (
"context"
data1 "github.com/pip-services-samples/service-beacons-go/data/version1"
logic "github.com/pip-services-samples/service-beacons-go/service"
cref "github.com/pip-services4/pip-services4-go/pip-services4-components-go/refer"
cquery "github.com/pip-services4/pip-services4-go/pip-services4-data-go/query"
clients "github.com/pip-services4/pip-services4-go/pip-services4-rpc-go/clients"
)
type BeaconsDirectClientV1 struct {
clients.DirectClient
service logic.IBeaconsService
}
func NewBeaconsDirectClientV1() *BeaconsDirectClientV1 {
c := &BeaconsDirectClientV1{
DirectClient: *clients.NewDirectClient(),
}
c.DependencyResolver.Put(context.Background(), "service", cref.NewDescriptor("beacons", "service", "*", "*", "1.0"))
return c
}
func (c *BeaconsDirectClientV1) SetReferences(ctx context.Context, references cref.IReferences) {
c.DirectClient.SetReferences(ctx, references)
service, ok := c.Service.(logic.IBeaconsService)
if !ok {
panic("BeaconsDirectClientV1: Cant't resolv dependency 'service' to IBeaconsService")
}
c.service = service
}
func (c *BeaconsDirectClientV1) GetBeacons(ctx context.Context,
filter cquery.FilterParams, paging cquery.PagingParams) (*cquery.DataPage[data1.BeaconV1], error) {
timing := c.Instrument(ctx, "beacons.get_beacons")
result, err := c.service.GetBeacons(ctx, filter, paging)
timing.EndTiming(ctx, err)
return &result, err
}
func (c *BeaconsDirectClientV1) GetBeaconById(ctx context.Context,
beaconId string) (*data1.BeaconV1, error) {
timing := c.Instrument(ctx, "beacons.get_beacon_by_id")
result, err := c.service.GetBeaconById(ctx, beaconId)
timing.EndTiming(ctx, err)
return &result, err
}
func (c *BeaconsDirectClientV1) GetBeaconByUdi(ctx context.Context,
udi string) (*data1.BeaconV1, error) {
timing := c.Instrument(ctx, "beacons.get_beacon_by_udi")
result, err := c.service.GetBeaconByUdi(ctx, udi)
timing.EndTiming(ctx, err)
return &result, err
}
func (c *BeaconsDirectClientV1) CalculatePosition(ctx context.Context,
siteId string, udis []string) (*data1.GeoPointV1, error) {
timing := c.Instrument(ctx, "beacons.calculate_position")
result, err := c.service.CalculatePosition(ctx, siteId, udis)
timing.EndTiming(ctx, err)
return &result, err
}
func (c *BeaconsDirectClientV1) CreateBeacon(ctx context.Context,
beacon data1.BeaconV1) (*data1.BeaconV1, error) {
timing := c.Instrument(ctx, "beacons.create_beacon")
result, err := c.service.CreateBeacon(ctx, beacon)
timing.EndTiming(ctx, err)
return &result, err
}
func (c *BeaconsDirectClientV1) UpdateBeacon(ctx context.Context,
beacon data1.BeaconV1) (*data1.BeaconV1, error) {
timing := c.Instrument(ctx, "beacons.update_beacon")
result, err := c.service.UpdateBeacon(ctx, beacon)
timing.EndTiming(ctx, err)
return &result, err
}
func (c *BeaconsDirectClientV1) DeleteBeaconById(ctx context.Context,
beaconId string) (*data1.BeaconV1, error) {
timing := c.Instrument(ctx, "beacons.delete_beacon_by_id")
result, err := c.service.DeleteBeaconById(ctx, beaconId)
timing.EndTiming(ctx, err)
return &result, err
}
src/version1/BeaconsDirectClientV1.py
from typing import Optional, List, Any
from pip_services4_components.context import IContext
from pip_services4_data.query import PagingParams, FilterParams
from pip_services4_components.refer import Descriptor
from pip_services4_rpc.clients import DirectClient
from .IBeaconsClientV1 import IBeaconsClientV1
from ...data.version1 import BeaconV1
class BeaconsDirectClientV1(DirectClient, IBeaconsClientV1):
def __init__(self):
super(BeaconsDirectClientV1, self).__init__()
self._dependency_resolver.put('service', Descriptor('beacons', 'service', '*', '*', '1.0'))
def get_beacons_by_filter(self, context: Optional[IContext], filter: FilterParams, paging: PagingParams) -> dict:
timing = self._instrument(context, 'beacons.get_beacons')
result = self._service.get_beacons_by_filter(context, filter, paging)
timing.end_timing()
return result
def get_beacon_by_id(self, context: Optional[IContext], id: str) -> dict:
timing = self._instrument(context, 'beacons.get_beacon_by_id')
result = self._service.get_beacon_by_id(context, id)
timing.end_timing()
return result
def get_beacon_by_udi(self, context: Optional[IContext], udi: str) -> dict:
timing = self._instrument(context, 'beacons.get_beacon_by_udi')
result = self._service.get_beacon_by_udi(context, udi)
timing.end_timing()
return result
def calculate_position(self, context: Optional[IContext], site_id: str, udis: List[str]) -> Any:
timing = self._instrument(context, 'beacons.calculate_position')
result = self._service.calculate_position(context, site_id, udis)
timing.end_timing()
return result
def create_beacon(self, context: Optional[IContext], entity: BeaconV1) -> dict:
timing = self._instrument(context, 'beacons.create_beacon')
result = self._service.create_beacon(context, entity)
timing.end_timing()
return result
def update_beacon(self, context: Optional[IContext], entity: BeaconV1) -> dict:
timing = self._instrument(context, 'beacons.update_beacon')
result = self._service.update_beacon(context, entity)
timing.end_timing()
return result
def delete_beacon_by_id(self, context: Optional[IContext], id: str) -> dict:
timing = self._instrument(context, 'beacons.delete_beacon_by_id')
result = self._service.delete_beacon_by_id(context, id)
timing.end_timing()
return result
Now that we’re done writing the client, we should test it. To be sure that our code works as intended, we need to perform some functional testing. Let’s start with creating, in a separate class, a set of tests that will be common to all our clients. This will help us simplify the process of testing multiple clients, as well as make sure that they all work the same. We’ll place the code for our tests in the test/version1 folder. The code for this class can be found in the repositories:
Now, let’s test the direct client. To do this, create an instance of the direct client and pass it as a parameter to our set of tests. An example implementation of the tests can be found in the example’s:
Run the tests using the testing methods that are standard for the programming language you are using. All tests should pass successfully.This finishes the development of the Direct client. Move on to Step 4. Designing an HTTP Client.