Question

I have a design question regarding "dynamic information hiding" vs static typing.

I have for the following python code

class RuleInput:
    attr1 = ...
    atrr2 = ...
    ...

def example_rule(input: RuleInput) -> bool:
   # does something and returns bool

I have many of these rule functions, each calculating its output based on the attributes in RuleInput (let's assume it's immutable).

For this code, it is clear what the input should be and tools like mypy can be used to do static type checking. Here also features like auto-completion work.

However, most of the rules only require a small number of attributes from the RuleInput. So developers could make mistakes by accessing the wrong information.

Hence, I had the idea of dynamically reducing the content of the RuleInput specific to each rule (rules just specify what they need beforehand). When doing this I lose the possibility of performing static type checking and features like auto-completion.

Here is the "dynamic information hiding" solution:

RULE_INPUTS = [
    ('attr1', int, field(default=0)),
    ('attr2', int, field(default=0))
]

RuleInputNames = Enum(
    "RuleInputNames",
    " ".join([x[0] for x in RULE_INPUTS])
)

def check(required_props):
    def extract_required_properties(full_input, required_properties):
        all_props = asdict(full_input)
        selected_props = {}
        for key in required_properties:
            selected_props[key] = all_props[key]
        return selected_props

    req_props = [x.name for x in required_props]

    RequiredProperties = make_dataclass(
        "RequiredProperties",
        [x for x in RULE_INPUTS if x[0] in req_props]
    )

    def decorator(func):
        def wrapper(full_rule_input):
            return func(
                RequiredProperties(
                    **extract_required_properties(full_rule_input,
                                                  req_edge_props),
                )
            )
        return wrapper
    return decorator

@rule(required_props=[ParticlePropertyNames.attr1])
def some_rule(rule_input):
    rule_input.attr1 # works
    rule_input.attr2 # wont work

What do you think about this "dynamic information hiding" vs static type checking? Which of these two approaches would you recommend?

Was it helpful?

Solution

The OOP solution is interface segregation. All rule functions you have should accept an interface exposing the methods and attributes they need to function. This allows client code to decide on a given implementation. While beeing very clear on what the rules need to function.

Let's say.

from abc import ABC

def rule_1(input: Rule1Input) -> bool: pass
class Rule1Input(ABC):
    ## define the api the rule function needs
    pass

def rule_2(input: Rule2Input) -> bool: pass
class Rule2Input(ABC):
    ## define the api the rule function needs
    pass

Client code would then be free to use the same object for all rules. Or an object per rule, or an object per every 2 rules.. it will be free on tht regard.

class RuleInput(Rule1Input, Rule2Input):
    pass

An object of RuleInput can them be passed to both rules without them never needing to know it is the same object.

rules = RuleInput()

rule_1_res = rule_1(rules)
rule_2_res = rule_2(rules)

In python, you could just use duck typing. But then you would fallback to the same issue of developers using data they should not.

You can either go all out in static typing or have tests to ensure proper calling of objects by your rule implementations. Nothing non pythonic about neither.

this is one possible solution, the nature and complexity of your problem do matter in the complexity of the code that models it.

Licensed under: CC-BY-SA with attribution
scroll top