Introduction
Snapper behaviors is an implementation of the Composition Design Pattern. It uses a design principle of preferring composition over inheritance. Instead of an “is a” relationship typically used in inheritance in traditional Object-Oriented Programming, it uses the concept of “has a” relationship. As in, an object has many components providing properties and functionality.
Using snapper behaviors, a snapper object is defined by a collection of behaviors. Each behavior is responsible for a different aspect of the snapper. Depending on how the behaviors are defined, they could be reused by different snappers. This allows for extending an object’s functionality dynamically.
Behaviors should be as orthogonal as possible, meaning that each behavior should only carry out one specific task and that task should overlap as little as possible with all other behaviors. They should be independent of other behaviors.
Note that we don’t have a generic/abstract behavior framework in CM, so the contents of this article describe it in a general concept. For real-world examples, refer to how behaviors are used in the Pallet Racking Framework (MhBehavior) and the K2 Framework (K2Behavior).
Behaviors are not meant as a replacement for inheritance and it is just an additional tool. Inheritance and composition can be combined as behaviors could still have logical inheritance. The goal is to write more modular, reusable, and maintainable code.
Interaction with the System
For behaviors to work, some sort of system is needed to iterate over all objects with a particular behavior to do things. For example, an animation may loop over all candidates and only apply to the candidate if it contains a specific behavior. The behavior will then apply the changes to the object. Various core methods or functions or hooks can be overridden to perform actions based on behaviors.
Examples
A snapper is a complex object with many different functionalities. It is possible to split up the snapper’s functionality to several behavior classes such as:
GfxBehavior |
Provide the graphics for the snapper. |
PartBehavior | Handle calculation/BOM data |
SnapBehavior |
Define what the snapper can snap to and how they should update |
Other logic in a snapper class can be extracted into a behavior class such as an ElevArrowBehavior to indicate that an elevation arrow is able to snap to the snapper and produce and elevation graph. Something extension specific could also be put into a behavior class if it is something that is expected to be shared among other snappers in the extension.
Example with Named Fields
Behaviors can exist as named fields in a snapper class with each behavior having a different responsibility.
public class MySnapper extends Model3DSnapper { public GfxBehavior gfxBehavior; public PartBehavior partsBehavior; public SnapBehavior snapBehavior; public void build3D(FetchEnv3D env) { model = gfxBehavior.?build3D(..); } public void getParts(PartsEnv env, Snapper{} visited) { partsBehavior.?getParts(..); } public bool allowSnap(Connector snap, Connector attach) { return snapBehavior.?allowSnap(..); } }
The concept shown above is similar to using proxy classes as seen in certain parts of our code base (also a type of composition).
Example with Dynamic Sequence
A collection of behaviors stored as a dynamic sequence can be stored in a snapper class. The class would then loop through the behaviors to execute their functionality when necessary.
public class MySnapper extends Model3DSnapper { public Behavior[] behaviors(); public void selected() { super(); for (b in behaviors) b.selected(this); } public void deselected() { super(); for (b in behaviors) b.deselected(this); } public void connectors(ConnectorCollection connectors) { for (b in behaviors) b.connectors(this, connectors); } }
In the connectors method example above, the behaviors would oversee adding new connectors to the snapper so that you don’t have to add a new connector field that would potentially not be used by one of the subclasses.
Purpose Key
A purpose can be used to indicate what a behavior is supposed to do. The system can ask all behaviors with a particular purpose key to carry out a specific behavior if such a behavior exists on the snapper.
public class Behavior extends CoreObject public str purposeKey; …
public class MySnapper extends Model3DSnapper { public Behavior[] behaviors(); final public Behavior behavior(str purposeKey) { if (!purposeKey) return null; for (b in behaviors) if (b.purposeKey == purposeKey) return b; return null; } public box localBound() { if (?BoundBehavior behavior = behavior("localBound")) { if (Box b = behavior.localBound(this)) return b.v; } return super(); } public void build3D(FetchEnv3D env) { if (?GfxBehavior behavior = behavior("gfx")) { model = behavior.?build3D(..); } } }
A combination of props, PropDefs, and XAPI could also be used to make this even more generic.
Reusing Behaviors
One of the main benefits of this design pattern is being able to reuse behavior classes among the different classes in your extension.
MySnapperA snapperA(); snapperA << RectGFXBehavior(); snapperA << AlwaysConnectBehavior(); MySnapperA snapperA2(); snapperA2 << SphereGFXBehavior(); snapperA2 << NeverConnectBehavior(); snapperA2 << UnitIDBehavior(); MySnapperB snapperB(); snapperB << RectGFXBehavior(); snapperB << AlwaysConnectBehavior();
In the above example, despite snapperA and snapperA2 being of the same class, both can behave differently by giving them different behaviors. This will allow for more generic and dynamic objects without the need to subclass. Meanwhile, behaviors could also be reused by a completely different class in the case of snapperB.
Stateless vs stateful behaviors
State
- Data we hold at a moment in time.
Stateless
- Does not retain any historical information and are unaffected by order of operations. As a result, these behaviors are independent of the sequence in which they are executed. This will result in consistent output for a given input without side effects. This feature allows for better reuse across different snappers.
Stateful
- Output may change depending on the order of operations due to changes to the state data.
Instance Reusage
Ideally, we would want instance reusage so that a specific behavior can be reused across different snappers without the need to copy an instance of that behavior each time. This could be done by setting up a behavior as a singleton. This will allow us to save on memory.
In order to implement instance reusage, behaviors would need to be stateless. Since an instance is shared between the snappers, all fields in the behavior will be shared. Therefore, when implementing a stateless behavior, ensure that the number of fields is kept to a minimum and that the fields are not dependent on the user. Because of this, any field that would not work as a copy=reference would not work in a stateless behavior.
Pros and Cons
Pros
- More flexibility.
- Better code reuse/modularity.
- Dynamic, data driven functionalities, that can be changed at runtime.
- More maintainable due to less excessive class inheritance depth/branches
- Easier to model individual behaviors than finding “commonality”.
- Potentially easier to unit test.
- Plug a Behavior into a simpler object to test.
- Potentially better handling of new interfaces/API versions.
- For example, add a new Behavior which developers can opt in/out from.
Cons
- Weaker static typing (object knowledge), which may cause:
- Trickier debugging.
- Harder to understand where actions are executed (from just looking at code).
- Potentially larger memory footprint.
- Performance risks.
- Maybe slower to iterate and ask unrelated objects.
- Risk of creating a dependency mess (if you are not strict enough).
Comments
0 comments
Please sign in to leave a comment.