I try to implement multiple concrete classes, which share the same API. The base functionalities between these classes are the same, but they support different types of configuration (among shared ones). I would like to keep the implementation between these concrete classes as separate as possible.
Therefore I came up with an (abstract) base class which defines the API, but I am not sure how to define the function arguments.
-
Common config containing all possibilities
-
(+) Same API for all implementations.
-
(-) API suggests certain unsupported configurations. (It might be in the future though).
Perhaps I would need to address this in the doc and/or even raise an exception in case of inproper use.
-
(-) In case of a new config attribute gets added, the other already existing concrete classes don’t address this new attribute.
@dataclass class CommonCfg: cfg_1: Optional[int] = None cfg_2: Optional[int] = None cfg_3: Optional[int] = None class AbstractA: @abstractmethod def func_z(self, cfg: CommonCfg): class B(AbstractA): def func_z(self, cfg: CommonCfg): # Does not use `cfg_3`. class C(AbstractA): def func_z(self, cfg: CommonCfg): # Does not use `cfg_2`.
-
-
Generic signature of base class
class AbstractA: def func_z(self, *args, **kwargs): # Or expect `CommonCfg` which only contains common config parameters (e.g. `cfg_1`)
For
func_z
,ClassB
&ClassC
would expect their specificCfgB
andCfgC
, respectively.
In a perfect scenario I could just exchange objects of different concrete implementations (at least as long as only their common configs are relevant).
E.g.
...
b = ClassB()
c = ClassC()
cfg = CommonCfg()
b.func_z(cfg)
# I would like to have the least amount of hassle replacing it, e.g. with:
# c.func_z(cfg)
One possible solution might be to delegate the base functionalities to the base class. This way the base class method would not have to be updated if you add additional parameters to the CommonCfg
object. The code could look something like this:
class AbstractA:
def func_z(self, cfg: CommonCfg):
# Do some work using cfg
class B(AbstractA):
def func_z(self, cfg: CommonCfg):
super().func_z(cfg)
# Do some additional work
class C(AbstractA):
def func_z(self, cfg: CommonCfg):
super().func_z(cfg)
# Do some additional work
This would also allow you to check if the cfg
object is supported by the
called method before calling the base method.
As for unsupported configurations, you already mentioned a solution which should give you the most flexibility. Just adding documentation for each method detailing which configurations are not supported should give any
decent user enough information to correctly use the API. Throwing an exception would also help the user detect any errors in their code.
1