Event and UseCase based plugin development for MagicDraw 

írta Sándor Zsolt

The following article describes the lessons learned during the development of plugins for Catia’s MagicDraw. Our company uses MBSE/SysML with MagicDraw to design our distributed, microservice based software solution for digital certificates. During the design of the system we realized very soon that certain functionalities are not provided by the vendor or third parties therefore we decided to develop them on our own. 

Introduction

MagicDraw is a modeling tool developed by NoMagic acquired recently by the french company Dassault Systèmes. MagicDraw allows developers to extend the functionalities of  the product using so called plugins. Plugins might be developed in various languages, but the most common language used is Oracle’s Java. Technologically speaking MagicDraw is a standard desktop application written in Java, using the well known Swing GUI Widget toolkit developed around 2007. This is an advantage and a disadvantage as well, the advantage is that the system is mature, the disadvantage is that nowadays not many people are writing desktop applications in Java therefore the talent pool is limited. What we found however is that a standard backend developer working with Java can pick up the necessary skills rather fast.

Developing an application, the naive way

Since our first plugins were very simple, we followed a very common pattern: add an event listener on the component triggering the execution of some business logic. This business logic might have accessed multiple components either for read or for modification purposes, it also might have read from the file system or created some model elements, even diagrams in the current project. This worked perfectly when we needed to write a very simple plugin. As more and more functions were required to be implemented it was soon realized that the required business use cases and the implementation diverged drastically: in order to check if, where and how a business case was implemented the codebase the answer was the usual: I need some time to dig through the code. As a developer I always thought that this is normal: business cases are implemented in the code, and as more business cases are implemented, the codebase becomes more complex, therefore it needs more time to check and understand. But I started thinking: why is it so? Am I just accepting the status quo, or we, as an industry, are doing something incorrectly? 

It seems I am not the only one having similar thoughts: there is actually a new movement and technique called event modeling trying to provide a solution for the same problem. They had a very nice way of explaining what the problem is, which I felt was true but at the time of reading could not be put into context. Yet. But let’s see how they summarize the problem we talked about previously:

The challenge of automating, especially, non-trivial systems is the rising cost curve for further changes as the system grows in complexity. Most of this extra cost of automation has to do with re-work (shown as red boxes below). Event Modeling minimizes the amount of rework by working off of a blue print that can be created in a very short time compared to existing design and modeling methodologies.

Source: eventmodeling.org

Events, events everywhere…

After reading the concept of event modeling, I wanted to give it a try, but using an iterative, explorational approach. The first step was to introduce events. What is an event? Event is a statement about “something happened”. A button was pressed.  A document is parsed. A table is exported as a CSV file. 

But why do we want to introduce an event? The reason is to decouple the gui code from the business logic: we don’t want to mix the why (someone pressed a button) with the what (load a document from the file system) but keep them separate. Instead of writing the business logic into the action listener of the button, we will create a new event. Example as follows:

loadDocumentButton.addActionListener(e -> {    
  LoadDocumentRequestedEvent event = new LoadDocumentedRequestedEvent(); 
});

The code is rather simple: attach an action listener to a button named loadDocumentButton, and create a new event. This is sadly not enough, we need to let other “components” (whatever we mean by components) know about the creation of this new event. 

For that purpose we need an event bus. An event bus might be implemented as a simple class providing functionality to subscribe a handler to a type of an event, and distribute an event instantiated from an event type to the subscribed handlers. The distribution (publishing) of an event might look like the following example:

loadDocumentButton.addActionListener(e -> {   
  LoadDocumentRequestedEvent event = new LoadDocumentedRequestedEvent();   
  eventBus.publish(event);
}); 

The aforementioned components will be able to subscribe to a certain type of event using the functionalities provided by the event bus and on that event execute some business logic:

eventBus.subscribe(LoadDocumentRequestedEvent.class, this::onLoadDocumentRequested);

private void onLoadDocumentRequested(LoadDocumentedRequestEvent event){
  // load document
}

Details of the Event Bus

So what kind of operations are provided by an event bus? As described before there are two basic functionalities of the event bus.

  • Ability to subscribe an event handler for an event type
  • Ability to publish an event to the bus

A very simple implementation of an event bus might look like this:

import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

public class EventBus {

  private final Map<Class<? extends Event>, Set<EventHandler<? extends Event>>> subscriptions = new ConcurrentHashMap<>();

   public void publish(Event event) {
      for (var entry : subscriptions.entrySet()) {
          if (entry.getKey().equals(event.getClass())) {
              for (EventHandler handler : entry.getValue()) {
                  handler.handleEvent(event);
              }
              return;
          }
      }
  }

   public <T extends Event> void subscribe(Class<T> eventClass, EventHandler<T> eventHandler) {
      subscriptions.computeIfAbsent(eventClass, k -> new HashSet<>());
      subscriptions.get(eventClass).add(eventHandler);
  }
}

The subscriptions are stored by a very simple hash map, and if an event is published, each and every subscription handler will be called, one by one, from the same thread.

Please note that this is a very simple, straightforward implementation. It is single threaded, blocking, without any error handling. In production there is definitely a place for improvement, but for the sake of example this is more than enough for us.

The interfaces Event and EventHandler used by the event bus might look like:

public interface Event {}

@FunctionalInterface
public interface EventHandler<T extends Event> {
  void handleEvent(T e);
}

Event is just a simple interface, and EventHandler is just a functional interface with a single method. 

An actual event will implement the Event interface as an immutable class. For keeping the code clean and easy to debug we strongly advise not to use a mutable event. Event is something that happened. Its state shall never change after instantiation.

public class ScriptExecutionRequestedEvent implements Event {

    private final String script;

    public ScriptExecutionRequestedEvent(String script) {
        this.script = script;
    }

    public String getScript() {
        return script;
    }
}

Let’s summarize what we learned. We realized that adding business logic directly to a GUI component’s action handler is not a great idea: we need to decouple the action (pressing the button) and the business logic associated to that action (load a document). This might be done using events published to the event bus and components subscribed to events containing the business logic.

Matching business ideas with code

The next logical step was to define the aforementioned components responsible for subscribing to events and executing some business logic. As most developers, engineers  or analysts, I struggled with naming. What do you call those components? Handler? Listener? Some other technical name? You can call them even FluffyAlpacas technically, but are they really that? I was googling for solutions when I found an article about Clean Architecture by Robert C. Martin. In this architecture pattern he defines a concept called Use Case

The software in this layer contains application specific business rules. It encapsulates and implements all of the use cases of the system. These use cases orchestrate the flow of data to and from the entities, and direct those entities to use their enterprise wide business rules to achieve the goals of the use case.

Source: https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html

We don’t really need his whole architecture pattern, just the Use Case, because this matches what we want. So, can we think about those components as Use Cases, even from the business point of view. Let’s give it a try.

  • When the button is pressed, load the document.
  • When the document is loaded, display the content of the document in the textarea.
  • When the diagram is closed, add the text in the textarea to the history.
  • When the user selects an entry from the history and presses the load history button, replace the text area with the content of the entry represented by the selected item.
  • When the username and password is filled, enable the login button.
  • When the login button is pressed, try to authorize the user.
  • When the user is authorized, display the main screen.
  • When the user authorization fails, display an error message on the login screen.

The structure of a use case

If the previous examples are observed it might come to a realization that their structure is identical and uses the following pattern:

<receive event(s)> <check> <action(s)> <create new event(s)>

There is always one or more events starting a use case. These events are created either by an interaction of the user (button press, close dialog, etc.) or after executing some internal business logic (document loaded, user authorized, user authorization failed, etc.). Nevertheless, from the use case point of view these are always some incoming events. 

The check part ensures that the action is executable. Normally this might be some null pointer checks or in case of stateful use cases a check that a use case is in the desired state.

When the use case is executable, actions are executed. During executing an action the state of the system might be read or changed as well. Typical actions are like: Display some text, add some text to a history file, enable or disable a button, load some document, read a value of a field, do some calculation, etc.  

Finally, new events might be created, which will start other use cases. Document loaded, User authorized, User authorization failed, etc. are typical examples of such events.

So far we described an execution of a use case, but why is it important, what is the advantage of such a concept. We have seen previously that using events decouples the actions and the business logic. Using use cases as a building block helps to match the business requirements with the code. For each and every business use case (e.g. When the button is pressed, load the document) we can have a matching code, preferably a class in case of working with an object oriented language. The class will be named according to the use case. Let’s see an example:

public class DisplayScriptExecutionFailedInOutputTextAreaUseCase implements UseCase {

  private final EventBus eventBus;
  private final JTextArea textArea;

  public DisplayScriptExecutionFailedInOutputTextAreaUseCase(EventBus eventBus, JTextArea textArea) {
      this.eventBus = eventBus;
      this.textArea = textArea;
      eventBus.subscribe(ScriptExecutionFailedEvent.class, this::onScriptExecutionFailed);
  }

  private void onScriptExecutionFailed(ScriptExecutionFailedEvent event) {
      SwingUtilities.invokeLater(() -> textArea.setText(event.getMessage()) );
  }
}

The code is pretty straightforward. A new use case is named according to the business requirement. The use case depends on the event bus for subscription purposes and a textArea, which state will be modified during the action.

The use case starts on the ScriptExecutionFailed event. There is no check required. The action is implemented in the onScriptExecutionFailed method: it sets the text of the text area based on the message stored in an event. 

Based on our experience the advantages are tremendous. The biggest advantage is the ability to think in business requirements. Each and every business requirement will have a well defined place in the code, non intermixed with others. This helped us to add new increments without touching the existing code base. We also found that when an existing use case code had to be changed it also meant that a business requirement also changed, starting a discussion with the business side. This kind of transparency enabled better communication and better understanding of requirements and implementation. Testing and debugging became easier because using use cases gave us the ability to think in business terms. What is the use case I am currently executing? What is the event it is started by? What other use cases will run? The ability to think in use cases allowed us to filter out all the parts of the code which will not run during a specific problem, allowing us to put a spotlight on the possible location of the problem. 

Categorizing use cases

Use cases may be categorized the following ways:

  • Chainable or terminating use case
  • Stateless or stateful use case

A chainable use case creates events after its state change step. This means that other use cases might subscribe to these events and use cases representing an atomic business logic block can be chained, combining multiple use cases into a bigger, complex sentence: a story. By pressing the button a document is loaded. When the document is loaded the contents of the document is displayed in the textarea. When the textarea is modified the previous data is added to the undo list. etc 

A terminating use case does not create new events, stopping the story line or branch permanently. 

A stateless use case does not store any kind of state, therefore it’s easy to test and maintain. There might be cases when a use case is waiting for multiple event types to complete, for example an export to a CSV file might happen only after a target file is selected and an export button is pressed. This requires the use case to subscribe to two different types of events: file selected event and export requested event. The use case action shall run only if both arrive. This functionality might be implemented using a state machine, or in simple cases a variable.

public class CsvExportUseCase implements UseCase {

    private final EventBus eventBus;
    private TabularResult result;
    
    public CsvExportUseCase(EventBus eventBus) {
        this.eventBus = eventBus;
        eventBus.subscribe(TabularResultCreatedEvent.class, this::onSmartGridResultCreated);
        eventBus.subscribe(ExportFileSelectedEvent.class, this::onExportFileSelect);
    }

    private void onSmartGridResultCreated(TabularResultCreatedEvent event) {
        this.result = event.getTabularResult();
    }

    private void onExportFileSelect(ExportFileSelectedEvent event) {
        if (result != null){
            // work with result and data from event
        };
    }

}

Life cycle management

 Lifecycle management is a crucial part of every object oriented solution. In case the plugin only displays a single dialog the creation of the dialog also creates the associated use cases as well. When the dialog is destroyed, so will the use cases as well. This is why in the simple case, the use cases are instantiated in the constructor of the dialog, or the content panel of the dialog:

public class ScriptEditorPanel extends BaseScriptEditorPanel {

    private final EventBus eventBus;
    private final UseCase[] useCases;

    public ScriptEditorPanel(EventBus eventBus) {
        super();
        this.eventBus = eventBus;

        useCases = new UseCase[]{
                new AppendScriptToHistoryOnCloseUseCase(eventBus, this.scriptTextArea),
                new ClearFilterTableUseCase(eventBus, this.outputTable)        
        };
    }

//...

}

In case of multiple dialogs, the situation is a bit more complex and requires the concept of modules, which will be discussed in a further article.

A complete example

Although our internally developed plugins are not available for the public, I wrote an open source script editor plugin for Visual Paradigm providing scripting functionality supporting multiple languages (groovy, python), syntax highlight, error highlight, CSV export, etc.

Although I have not had much time to work on the project lately, and Visual Paradigm is a little bit different from MagicDraw, still, it might serve as a generic example for fellow plugin developers. The source code is available at:

https://github.com/sz332/visual_paradigm_scripting_plugin_oss

Conceptual model

The diagram above describes a possible conceptual model for the aforementioned solution. A plugin has a reference to an event bus, and contains modules. In case of a single dialog there is a single module. A module contains a screen (a generalized concept of a dialog) consisting of a certain number of graphical widgets. A widget can be a button, an input field, a text area, a table, etc. The use cases register to the event bus, and consume events. They might also publish events. We have two types of events: ones are the inside module events sent in the context of a single module, or between module events, which are sent across modules. 

Summary

In this article a new architecture for plugin development was presented. Using an event based solution the user interface and the business logic can be decoupled, while by using use cases the business requirements might be traced to a well defined, self-contained part of the codebase. The event-use case based system allows the programmer to reason about the execution and state change of the system, providing easier extensibility, maintainability and a common language framework among the stakeholders and the developers.  

Related Posts