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?
-
How many attributes are in a RuleInput instance?ddcastrodd– ddcastrodd2020年06月28日 01:18:44 +00:00Commented Jun 28, 2020 at 1:18
-
@ddcastrodd around 15 attributessteve– steve2020年06月28日 07:27:35 +00:00Commented Jun 28, 2020 at 7:27
1 Answer 1
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.
-
-
@amon I'm not very familiar with mypy. may I ask what would it do in practice? could one simply use duck typing instead of defining interfaces left and right?Pedro Rodrigues– Pedro Rodrigues2020年07月05日 19:52:40 +00:00Commented Jul 5, 2020 at 19:52
-
1Duck typing is the classic "Pythonic" way but then you can't use type annotations. ABCs as in your answer is a common approach to make this type-checkable with mypy, and I have used exactly that strategy in the past. But since this in nominative typing, it is necessary to inherit from those ABCs. Protocols make it possible to do structural typing, i.e. something like static duck typing as in Typescript or Go. It's like defining an ABC, except that you don't have to inherit from it and only describe a type signature.amon– amon2020年07月05日 21:40:15 +00:00Commented Jul 5, 2020 at 21:40
-
Hi @PedroRodrigues, thanks for your post! Since all my rules are completely different, I would define an separate interface for them, which kind of loses the meaning right? Also I don't really know how I can make the code which calls the rules feed the correct arguments (I can only think of if else statements)steve– steve2020年07月06日 07:23:20 +00:00Commented Jul 6, 2020 at 7:23
-
1My solution is exactly equal to what you are doing right now, the only difference is explicity (is that a word. In other words, I'm beeing explicit by declaring an interface, you are being implicit by using duck typing. Does interface segration result in more code than duck typing? Yes. Is that boilerplate code? maybe, in your case I would argue it is not. Python is not statically typing, so to fix the issue of devs using the wrong attributes from the parameters, you would need tests. Are tests boilerplate? I don't think so, so neither are my interfaces since they serve the exact same porpuse.Pedro Rodrigues– Pedro Rodrigues2020年07月06日 10:37:04 +00:00Commented Jul 6, 2020 at 10:37