Since actions are the main way of using many app store modules, it is worth investing some time and thought in designing good actions. Here are eleven best practices that I have for anyone who is writing a java(script) action, either for use in a project or as a part of a reusable module.
1. Use descriptive parameter names and suffix them with an underscore
The name of a parameter should be descriptive and not redundant. That is general advice for naming that also holds true here. So avoid names like object, parameter, data. Keep in mind that action parameters are not only used in Mendix when the action is invoked, but also in java(script) code as variable names. This causes a slight problem when the parameter name is a reserved word in the respective language e.g. Object. To deal with this, Mendix would suffix the variable name in code with Parameter1 etc. This has been reworked in newer Mendix versions, which actually makes the problem worse. Now the same action code might work fine on an older Mendix version and fail to compile on a newer Mx version because of the different ways variable names are generated.
By using an underscore suffix all conflicts with reserved words that I am aware of can be eliminated and is actually not even visible when the action is invoked, as seen in the screenshot below. Perfect!
2. Add a new action instead of making signature changes.
We have all been there. Even the most well though-out action will need to change. Some of those changes will inevitably involve changing the signature, for example because a new parameter is added or a return type is changed. This is very disruptive, since all respective action calls throughout the project need to be updated.
The old action should internally call the new one and pass a default value for the new parameters or ignore a return value if one was added. Make sure to give both actions a logical name, perhaps the old action should include the word deprecated somewhere. A supplemental practice to avoid too many signature changes is described next.
3. Bundle flags, enums and integer settings into a single "Options" object.
Many times an action will have several flags or different settings that can be tweaked and therefore need to be exposed somehow. It is best to bundle all of these as a single object parameter, often called Options, Settings or Configuration.
By using an "Options" object multiple problems can be tackled at once: 1) adding a new setting does not change the signature; 2) default values can be defined in the object and do not pollute the constant space; 3) documentation for each flag has a clear place to live without overwhelming the action dialog; 4) the action parameter list can be shorter making it faster and cleaner to invoke the action (remember each parameter has to be explicitly set).
An example of an "Options" object from the parallel execute app store module.
4. Always use a facade instead of calling third-party libraries directly.
This one is for all who use third party dependencies in the form of jar files or javascript modules. I wrote a separate detailed blog post with the best practice for this. By using a facade we avoid hard coupling and make it possible to swap the library without too much hassle. Also, it is best to use a language specific dependency management and build tool such as maven or gradle for java, and npm or webpack for javascript. These tools offer easy management of transitive dependencies, test integration into the build process and packaging for the desired platform.
5. Guide the user in constructing object hierarchies with a builder pattern
In some cases arguments for an action are not a simple number or an object with a few parameters. Instead, an entire hierarchy of objects is needed, and it has to be associated in a specific way that is expected by the action. This is impossible to enforce at design time, so a clever way is required to guide the user in constructing the right hierarchy and setting the right associations.
In the case of medium-sized hierarchies, i.e. one or two associations, I prefer to use a builder pattern to help the user prepare the data. The builder pattern allows complex hierarchies to be built with simple steps, one step at a time. Additionally, each step is constrained using types often only obtainable from other steps. In an ideal world, it would be possible to entirely prevent manual instantiation of some builder objects, thus forcing the user to create them using an action.
Example of builder pattern used in my "Web push notifications" module. In step1 a notification object is constructed using a java action. Then in step2 a NotificationAction object is created and associated to a Notification, but instead of doing this directly again a java action is used which requires a Notification object as a parameter. This makes it impossible to confuse the steps order and call step2 before step1 or to forget setting the association.
6. Use the most specific and constraining types as possible.
This is another general advice, that also applies to actions and is tightly related to the previous best practice. Instead of strings "Yes" or "No" use boolean. Use enums and dates in place of strings where applicable. Avoid overloading nulls or other special values to convey information, instead use specific types or enums for that. For example, if the response is an error message or null (terrible idea btw, see 11) use an object with a boolean "HasError" and an error message instead.
7. Only use code for things that can not be done in Mendix natively.
If something can be done in Mendix then do it in Mendix. Especially for retrieves and/or changing objects. It is much easier and safer to call a flow from the middle of the action to do a retrieve natively in Mendix than to construct the XPath and retrieve with code.
When changing object attributes, remember that renaming the attribute, does not automatically rename existing calls to getter/setter methods on the respective proxy. So again, only use code when there is no alternative to doing the same natively in Mendix.
8. Do not add unnecessary coupling to Mendix runtime and core methods.
Avoid coupling code with Mendix APIs, especially for java actions. Mendix often makes changes to the runtime and core methods, just see this huge list in Mendix 8 version. So to avoid headaches, do not rely on Mendix runtime libraries or core methods unless absolutely necessary. It is clear that some coupling is unavoidable the goal here is to limit this to the absolute minimum.
If you follow the other best practices this one should come naturally. As an added benefit, if at one point you decide to move this part out of Mendix and into a separate server then having fewer dependencies will make it easier to migrate that part of the functionality.
9. Validate the number and types of parameters for microflow parameters.
This only applies when using microflow as parameters. Unfortunately, Mendix still does not allow to restrict the parameter number and types when used in an action. Until that is implemented the next best thing is to check the input and output types at run time. This is not trivial, please check the parallel execute module for a good implementation which also covers inheritance.
10. Use json as intermediary for communicating with actions.
Builder pattern was already mentioned as a way of dealing with medium-sized object hierarchies. But what about large object hierarchies? In this case it is best to resort to json. This gives users complete freedom to map their own entity diagram to/from json using an export/import mapping. Mappings are natively supported in Mendix which means they come with type safety and allow for easy renaming of entities, associations and attributes. This in contrast to code where any such rename would cause a compilation error.
To take it to the next level I suggest using JSON schema to validate the incoming/outgoing json. A free bonus that you get by using json is that it makes actions easier to test. No need to build an object hierarchy for every test case, simply paste a test json.
11. Let exceptions propagate instead of swallowing them.
If an exception occurs inside code, best to let it propagate to Mendix. Then the caller of the action can decide on how to best handle it. There is rarely a good reason to swallow exceptions.
Sometimes it might be useful to re-throw an exception with a simpler error message and log the actual message on a level other than error. For example, "NumberFormatException in line 192*"* is much less informative compared to "Amount is not a valid number".
''I hope you enjoyed reading this post and that it helps you design better Mendix code actions!''