I have an algorithm I am trying to implement that has steps 1 to 5. There are several different ways I could implement each step. Each calculation step is essentially just an astronomy calculation, and I could either depend on any one of the astronomy libraries or implement my own astronomy calculation.
My current design seems to be too heavily dependent on one library throughout all of the astronomy calculations, and I am concerned about what I will have to do when I have to change how I calculate a step. I would rather not have so much hardcoding, and I would like my development to be a bit easier in the future.
Is my dependency route overkill? What would be a good way to design this software? I just need an astronomy module with a collection of astronomy functions, but I feel like the Algorithm
should be insulated from how the astronomy module changes.
Current Design
algorithm.py
import astronomy
class Algorithm:
def __init__(self) -> None:
pass
def calculate(self, a: float, b: float, c: dt.datetime) -> dict[str, tuple]:
value1 = astronomy.calculation1(a, b, c)
value2 = astronomy.calculation2(a, b, c)
value3 = astronomy.calculation3(value1, value2)
value4 = astronomy.calculation4(value3)
value5 = astronomy.calculation5(value1)
return { ... }
astronomy.py
import skyfield
# import lots of other libraries
def calculation1(self, a, b, c):
"""
lots of skyfield specific code
"""
def calculation2(self, a, b, c):
"""
lots of skyfield specific code
"""
def calculation3(self, x, y):
"""
lots of skyfield specific code
"""
def calculation4(self, x):
"""
lots of skyfield specific code
"""
def calculation5(self, x):
"""
lots of skyfield specific code
"""
New Design with Dependency Injection
algorithm.py
import AstronomyCalculator
class Algorithm:
def __init__(self, astronomy_calculator) -> None:
self.ac = astronomy_calculator
def calculate(self, a: float, b: float, c: dt.datetime) -> dict[str, tuple]:
value1 = self.ac.calculation1(a, b, c)
value2 = self.ac.calculation2(a, b, c)
value3 = self.ac.calculation3(value1, value2)
value4 = self.ac.calculation4(value3)
value5 = self.ac.calculation5(value1)
return { ... }
astronomycalculator.py
import ABC
class AstronomyCalculator(ABC):
def __init__(self):
pass
def calculation1(a, b, c):
pass
def calculation2(a, b, c):
pass
def calculation3(value1, value2):
pass
def calculation4(value3):
pass
def calculation5(value1):
pass
skyfield.py
import skyfield
class Skyfield(AstronomyCalculator):
def __init__(self):
pass
def calculation1():
"""
lots of skyfield specific code
"""
astropy.py
import astropy
class Astropy(AstronomyCalculator):
def __init__(self):
pass
def calculation1():
"""
lots of astropy specific code
"""
custommath.py
import numpy as np
import pandas as pd
class CustomMath(AstronomyCalculator):
def __init__(self):
pass
def calculation1(a, b, c):
"""
handwritten calculations
"""
main.py
def main():
# Then I could change which astronomy calculator I use here
astronomy_utils = Skyfield()
algorithm = Algorithm(astronomy_utils)
results = algorithm.calculate(a,b,c)
EDIT:
To add further context about what the algorithm is doing, here is the calculate()
function:
class Algorithm:
def __init__(self) -> None:
pass
def calculate(self, latitude: float, longitude: float, local_time: dt.datetime) -> dict[str, tuple]:
for body in constants.CELESTIAL_INTERESTS:
upper_culmination_time = astronomy.meridian_transit_time(latitude, longitude, local_time, body)
rise_time = astronomy.rise_time(latitude, longitude, local_time, body)
set_time = astronomy.set_time(latitude, longitude, local_time, body)
ascending_positions_horizontal_reference_frame = astronomy.sample_ephemeride(body, latitude, longitude, rise_time, upper_culmination_time, samples = 100)
descending_positions_horizontal_reference_frame = astronomy.sample_ephemeride(body, latitude, longitude, upper_culmination_time, set_time, samples = 100)
ascending_positions_geodetic_reference_frame = astronomy.subpoints_of_positions(ascending_positions_horizontal_reference_frame)
descending_positions_geodetic_reference_frame = astronomy.subpoints_of_positions(descending_positions_horizontal_reference_frame)
return { ... }
The functions are nearly independent of each other. They are simply a collection of astronomy functions.
import calculation1 from astronomy
. Then swapping that out with a new equivalent function can be done simply by changing it toimport calculation1 from foobar
orimport calculationx from foobar as calculation1