-
Notifications
You must be signed in to change notification settings - Fork 1
SOLID Principles
This document will serve to be a guideline for design patterns within va-enp-api
.
Code should follow the SOLID principles:
- Single Responsibility
- Functions should exist for a single purpose and as soon as they start to do more than one thing they are broken apart.
- Open-closed
- Class functions are a great example. Uses the class member or instance variables
- Liskov Substitution
- OOP principle, intended to allow things inheritance to work effectively with interfaces ("contracts").
- Interface Segregation
- A good example of this is how we have a
PushModel
and intend to haveSmsModel
andEmailModel
. The interface should not be overloaded to make sms requests require a "subject" field, which is required by email.
- A good example of this is how we have a
- Dependency Inversion
- High level functionality should not depend on low level functionality.
- A route needs to add or collect data from the database and could do so directly, but that is tightly coupled. DI says that the route should interact with an interface to collect the data it needs.
Non-single responsibility Example:
def process_the_thing(data):
try:
value_one = data['value_one']
value_two = data['value_two']
except: KeyError
logger.exception('Unable to proccess the thing')
raise NonRetryable
thing_one.update(value_one)
thing_two.v2 = value_two
This trivial example has a parsing and update section. These are two separate things necessary to end up at the same result: a processed thing.
These should always be broken apart.
def parse_the_thing(data):
try:
value_one = data['value_one']
value_two = data['value_two']
except: KeyError
logger.exception('Unable to proccess the thing')
raise NonRetryable
return Thing(value_one, value_two)
def update_the_thing(thing):
thing_one.update(value_one)
thing_two.v2 = value_two
def process_the_thing(data):
thing = parse_the_thing(data)
logger.debug('the thing processed is: {}', thing)
update_the_thing(thing)
Easier to maintain, extend, and test! Simpler to piece together what the function process_the_thing
is accomplishing. Then if you need to understand the parsing, you can drill into that.
A real example of this principle being violated is in the notification-api
repo, the process_pinpoint_results
method does all of the parsing in-method, that is a good example of something that could/should be broken apart.
The ability to extend code with no modification to existing code.
One aspect of open/closed is inheritance. BaseDao
below has the serialize functionality.
def BaseDao(Base):
def serialize():
...
Let's extend it to allow to represent a templates
object and allow it to add template rows.
def BaseDao(Base):
def serialize():
...
def TemplateDao(BaseDao):
async def add(data):
...
We can extend this class by adding a get
method, which has no bearing on BaseDao
nor add
:
def TemplateDao(BaseDao):
async def add(data):
...
async def get(id):
...
BaseDao
contained a way to serialize the data of that object.
def BaseDao(Base):
def serialize():
...
So following Liskov Substitution, we should be able to do the following:
template = TemplateDao.add(template_data)
print(template.serialize())
Every derived class should be capable of using the serialize
method.
This principle involves ensuring clients do not have to implement downstream methods or deal with downstream parameters.
The notification-api
has a good example of Interface Segregation violation in the send_sms
methods, where they require parameters be passed that are never used.
An example of how to avoid stepping over the line is the va-enp-api
send_notification
method. We want to ensure all notifications are handled in the same manner, so regardless of the intent to send an sms
, we want to ensure we call send_notification
which then calls send_sms
. This does not violate Interface Segregation because the sms
request does not have to implement anything related to send email
or push
notifications. It also does not need to include email
or push
parameters, nor additional sms
parameters for various providers.
This principle covers how different modules/classes within the application interact. Database is probably the most common example of this. In some APIs you may directly interface with the database inside the route which is putting all mechanics and utilities in the policy layer of an application.
We want to separate out the different layers (conceptually). Routes for example handle the following layers:
- Authentication
- Validation (2-step process)
- Parsing
- Doing the thing (composed of more layers)
- Returning a response
- Any background tasks
Authentication
, Validation
, and Doing the thing
all reach out to the database. We want all database calls handled in the same way (success or failure), so it makes sense to create a database utility layer to facilitate the other layers.
Wikipedia states it well:
By dictating that both high-level and low-level objects must depend on the same abstraction, this design principle inverts the way some people may think about object-oriented programming.
This can be difficult for developers, so please reach out!
By properly using polymorphism and dataclasses/models we are able to avoid many of the pitfalls seen with our current notification-api
.