Is it actually a good practice to declare a type for every single variable and method? It is easier to write code, but it seems quite forced. I also have some problems with object-oriented programming. Even in this simple example, there are too many layers of abstraction. I wanted the Container to be an independent class, but when it turned out to have functionality that was only remotely similar to the Module's, I added a new one. It seems like it would be difficult to manage in more serious projects. I understand that code doesn’t need to be perfect, but this one was created solely to practice good practices. Its only purpose is to be as good as possible, so I would be grateful if anyone more skilled than I could do a quick review.
from abc import ABC
from typing import cast
class Protocol(ABC):
def __init__(self) -> None:
self._modules: dict[MType, Module] = {}
def get_module[T: Module](self, type: type[T]) -> T:
module = self._modules.get(type)
if module is None:
raise KeyError('Module not found!')
return cast(T, module)
def add_context(self, module: Module) -> None:
self._modules[type(module)] = module
class Module(Protocol):
def __init__(self, *required_context: MType) -> None:
super().__init__()
self._required_context = set(required_context)
@property
def required_context(self) -> set[MType]:
return self._required_context
def call(self) -> str: ...
def check_missing_modules(self, modules: set[MType]) -> None:
missing_modules = self._required_context - modules
if missing_modules:
message = ', '.join([m.__name__ for m in missing_modules])
raise KeyError(f"Missing needed context: {message}")
MType = type[Module]
class ModuleB(Module):
def __init__(self, text: str) -> None:
super().__init__()
self._text = text
def call(self) -> str:
return f'B {self._text}'
class ModuleC(Module):
def __init__(self) -> None:
super().__init__(ModuleB)
def call(self) -> str:
b_module = self.get_module(ModuleB)
return 'C' + b_module.call()
class Container(Protocol):
def _check_module_compatibility(self, module: Module) -> None:
all_modules = set(self._modules.keys())
module.check_missing_modules(all_modules)
def _add_module_context(self, module: Module) -> None:
for m_type in module.required_context:
context = self.get_module(m_type)
module.add_context(context)
def start(self) -> None:
for module in self._modules.values():
self._check_module_compatibility(module)
self._add_module_context(module)
c = Container()
c.add_context(ModuleC())
c.add_context(ModuleB('idk'))
c.start()
mb = c.get_module(ModuleB)
mc = c.get_module(ModuleC)
print(mc.call(), mb.call())