1. FeiWongReed
  2. agatsuma

Wiki

Clone wiki

agatsuma / Dependency system

In nutshell

This is good practice to split big system into small parts. But what if one part cannot be used w/o other one? What if our lego-robot cant work w/o body, legs, hands and head? What if we'll run a robot w/o this part? In the real life we'll get simply nothing, in the development... so, nothing, exceptions, errors, problems, unused parts(in the case if robot - part of big robots system). Solution pretty simple - create a dependency system, define a dependency meaning. And that creates one more way of development - development through implementation of dependencies by its requirements.

Its only in mind! It can be rewrited in hundred times!

Ideology of dependency system in the Agatsuma

Each Spell can use other spells by its dependency-class implementation.

Writing of dependency-class implementation like a writing a adapter for other spells. In the nutshell, it is.

If spell needs other spell for some work, it means need to create a dependency interface:

...
class IFooDependency(BasicDependencyInterface): #see, parent isn't interface
        #its just a class for some works like a development sugar, it's from Agastuma
    @abstractmethod
    @returns(int)
    def create_bar(arg1, arg2, arg3):
        pass

    @abstractmethod
    @returns(dict, {'arg1':str, 'arg2':str, 'arg3':str})
    def get_bar(barId):
        pass

If other spell wants to create a functionality for other spells(to be a provider), it needs to implement this interface. We have two ways to do that: one for small implementations, that easy to fit in the small amount of code AND one for big implementations, that needs a one individual class, file.

Big dependency functionality

In that case, we need to implement a dependency with a lot of methods or amount of code. We need to create new file in the spell directory.

.
└── project
    ├── spell.py
    └── spells
        ├── bar
        │   ├── ifoo_dep.py #dependency-class interface
        │   └── spell.py
        └── foo
            ├── foo_imp.py #created file, dependency-class implementation
            └── spell.py

After that, we need to implement our class.

from bar import IFooDependency

class FooImplementation(IFooDependency):
    def create_bar(arg1, arg2, arg3):
        #we can do anything. for example, create a new model instance via mongokit
        #after that, we need to return an uid of record(bar)
        return uid

    #remember that:
    #@returns(dict, {'arg1':str, 'arg2':str, 'arg3':str})
    def get_bar(barId):
        #now we can execute a query for selecting bar by id(uid)
        return bar.to_python() #remember, it must to create a dict with our args!

Ok, almost done. Now we need just to say in the spell about that implementation.

...
from foo_imp import FooImplementation
class ProjectSpell(...spells...):
    name = Atom.TestAppSpell
    provides = [
        FooImplementation,
        ]
    config = {}
...

As we see, we said what implementations we have. Now, Agatsuma must to get this list, find spells, that wants this implementation by its dependency, create instance of implementation for each of its and run application.

Small dependency functionality

If we've just a small problem like a key-value backend dependency, we can implement it's functionality in the spell class.

.
└── project
    ├── spell.py
    └── spells
        ├── bar
        │   ├── ifoo_dep.py #dependency-class interface
        │   └── spell.py
        └── foo
            └── spell.py #spell with dependency-class implementation

And implementation:

...
from bar import IFooDependency
class ProjectSpell(...spells..., IFooDependency):
    name = Atom.TestAppSpell
    provides = [
        self,
        ]
    config = {}

    def create_bar(arg1, arg2, arg3):
        #we can do anything. for example, create a new model instance via mongokit
        #after that, we need to return an uid of record(bar)
        return uid

    #remember that:
    #@returns(dict, {'arg1':str, 'arg2':str, 'arg3':str})
    def get_bar(barId):
        #now we can execute a query for selecting bar by id(uid)
        return bar.to_python() #remember, it must to create a dict with our args!
...

Is is hard? No. But if we'll get a big amount of dependency functions with big implementation, it'll creates just a one big heap of code, that pretty hard to read and customize.

Fuck this shit!

So what i must to do: create an absract spell or dep interface?

This is really interesting question. This is question of ideology. We need to merge an dependency and spells ideology.

Dont forget about spell configuration! Some implementations needs to get a some settings like a db connection information!

Fei Wong's point of view on required dependency system updates

Functionality dependencies

Simple case

Functionality client

...

class AbstractSettingsBackend(object)
    @abstractmethod
    @returns(bool)
    def set_option(name, value):
        pass

    @abstractmethod
    def get_option(name):
        pass

class SettingsSpell(AbstractSpell, InternalSpell):
    description = 'Agatsuma Settings Spell'
    dependencies = ()
    requirements = (AbstractSettingsBackend, # Backend is usual spell, instantiated by Agatsuma
                   )
    name = Atom.agatsuma_settings

Functionality provider

...
from agatsuma.spells.core_settings import AbstractSettingsBackend

class MongoSettingsBackend(AbstractSpell, InternalSpell, AbstractSettingsBackend):
    description = 'Agatsuma MongoDB settings backend'
    dependencies = (Atom.agatsuma_mongo_driver, ) # see "Direct dependencies" section
    name = Atom.agatsuma_settings_backend_mongo

    def set_option(name, value):
        success = False
        # write into storage
        return success

    def get_option(name):
        value = None
        # read from storage
        return value

Complex case

When you want to implement some spell with huge interface you should think twice before writing any code. Complex interfaces are bad, they are signs of bad design. Try to decomposite complex abstraction. If it's not possible, then you should use PIMPL pattern or something similar. We insist that such tasks are not require changes in Agatsuma's dependency system and must not be exposed to spells' interaction level.

Functionality client

...

class ComplexSpellClient(AbstractSpell, InternalSpell):
    description = 'Agatsuma Settings Spell'
    dependencies = ()
    requirements = (IComplexSpell, # IComplexSpell declares 10-20-100 methods
                   )
    name = Atom.agatsuma_mind_blow

Functionality provider

Metaprovider
...

class VeryComplexSpell(AbstractSpell, InternalSpell, IComplexSpell):
    description = 'Agatsuma's guaranteed mind blow'
    name = Atom.agatsuma_mind_blow

    def __init__(self):
        self.foo_impl = FooImpl()
        self.bar_impl = BarImpl()

    def do_something(self, *args):
        self.foo_impl.do_something(*args, parent_spell)
Real provider(s)
...
class FooImpl(object): # not a spell!
    def do_something(self, *args):
        pass

Direct dependencies

Functionality provider:

...

class SettingsSpell(AbstractSpell, InternalSpell):
    description = 'Agatsuma MongoDB driver'
    name = Atom.agatsuma_mongo_driver

Functionality client:

...
from agatsuma.spells.core_settings import AbstractSettingsBackend

class MongoSettingsBackend(AbstractSpell, InternalSpell, AbstractSettingsBackend):
    description = 'Agatsuma MongoDB settings backend'
    dependencies = (Atom.agatsuma_mongo_driver, ) 
    name = Atom.agatsuma_settings_backend_mongo

    def set_option(name, value):
        success = False
        storage_spell = Spell(Atom.agatsuma_mongo_driver)
        storage_spell.write(arg1, arg2)
        # write into storage
        return success

    def get_option(name):
        value = None
        # read from storage
        return value

Updated