In Model Driven Development: The Basics (Part I) we have set up an environment for modeling a system with Enterprise Architect and for generating C++ code to be compilable in Visual Studio.
In this second part of the post we are going to build on that and see how some fundamental entitites in UML are translated into C++. This should give the readers enough experience to play with the basics of MDD and be able to find more advanced features themselves. For brevity I am not going to describe in detail how to access features which have already been described in Part I.
1. Add an Attribute to the Logger Class
The objective of this activity is to put in the Logger class an element which records the current required level of tracing. As usual, we want to use UML while keeping in mind that we want the tools to generate valid C++ source code from that.
- Let’s open again our diagram in the model.
- Context Menu on the Logger class and select Features & Properties -> Attributes.
- A window opens. Let’s enter requestedLevel for the Name of the new attribute, Private for the Scope and LOG_TRACE for its Initial Value. As far as the Type is concerned, we want the attribute to have LogLevel. We can find it in the listbox in the specific column, by selecting “Select Type“. After this a window similar to the Project Browser opens and we can easily find our way to the type previously defined in the model.
- Something like “This attribute keeps track of the tracing level required by the client.” should be entered in the Notes box as an explanation of the meaning of the attribute.
- Then we have to take care of how the clients could access the attribute. We are going to grant read-write access. In order to do that, with the attribute highlighted, we just consider the box at the bottom left of the window where the characteristics of the attribute are visible. Just click on the button with dots in the cell close to Property and a window describing the possibilities is shown. Language should be predefined as C++, then we can see the name for the Getter and the Setter (please see also next point) and their scope. For what we want to do everything should be fine. Click OK. If everything has gone according to plan, you should end up with the following window.
- There is some discretion about how the names for the accessors are going to be generated (i.e. get/set with initial capital letters or not, first letter of the name of the attribute capitalized or not). Option can be seen in Start > Workspace -> Preferences -> Source Code Engineering (and specific instructions for C++)
- The diagram should now appear as in the following picture.
- Time for a new test on Visual Studio. Code is generated again as described in Part I para 7.4 point 1.
- Open the MDDTest solution in Visual Studio and build it. If everything has gone correctly, the compilation should fail! Intellisense in Visual Studio gives us a clue as to why this is happening. We are just using the LogLevel type of which the compiler knows nothing about. Of course, the problem is easy to spot, we miss the “include” for the enumeration type.
- Considerations. The solution to fix the problem, however, might not be as straightforward as it seems, because it is related to the processes in the business involved. In a real MDD environment, the developer has to go back to the UML model and introduce what can generate the code correctly. I know that there are different opinions on this and different processes lead to different solutions, I am only reporting what worked better for me compared to other experiences.
- Back to the model then. The <<use>> stereotype of the dependency gives us what we need. Therefore, select the “Class” option in the Toolbox and open the Class Relationships tree.
- Select the Usage relationship, then click on the Logger class box in the diagram and drag the segment that appears to the box representing the Loglevel enumeration. If we select the dependency arrow in the diagram and select Properties we can define several parameters for the dependency, but in our case the type of dependency is very simple and the default values look just fine.
- If we peek at the generated code in Logger.h we see that the include directive is now there.
- Time repeat step 8. described above. This time the compilation fails, but for a different reason. The “getter” for the attribute has no implementation and the return value is missing.
- We can see that in the implementation file (Logger.cpp) the C++ generator has introduced some markings in the implementation of the getter and setter. The markings indicate an area of the code that won’t be touched by successive regenerations of the code. This indicates to us the first possibility to solve the problem, that is, writing the behavioral code ourselves in the implementation file. The alternative is described at point 17. It involves writing the behavior of the function in the model and generate the implementation file. For Model Driven Engineering the second option would be the only one, but I have worked using both approaches in very successful projects.
- Let just write the line of code return requestedLevel; in the getter and, since we are at it, requestedLevel = newVal; in the setter.
- Alternative to point 16. Go to Context Menu on Logger Class -> Features & Properties -> Operations. Select getRequestedLevel (however it is spelled in your case according to enabled settings) -> Select the Behavior tab in the block at the bottom center of the window -> Click on the Initial Code button -> and enter return requestedLevel; -> Click OK. Now select setRequestedLevel, follow the same procedure as before but this time enter requestedLevel = newVal; -> Click OK -> Click Close. Generate th code and you can see that the code is at the right place, just where we would put it manually.
- Compile the solution in Visual Studio and we should have a successful build.
- Fig. 3 shows the current status of our model.
2. Add a method to the Logger Class
Now we want to add a method that logs the messages on the screen. It will take two parameters, the LogLevel for which the message has to be shown and the message itself. It returns nothing.
As previuosly noted, we are using C++ but not its standard library, only the much more basic C language standard library, because we pretend to be working in an embedded environment and our tools are geared towards it. Also, function security and anything related to potential vulnerabilities are out of scope here.
EDIT 2018/01/08 : A reply to a specific question to LieberLieber Software, the developer of Embedded Engineer, has confirmed that from version 2.1 of the plug-in it is also possible to generate include directives for the C++ Standard Library. I have tested the feature and I can confirm that (please se also below).
- In UML we want to introduce the method
log(LogLevel, std::string):void Go for Context Menu on the Logger class in the diagram -> Features & Properties -> Operations
- A window appears
- Let’s enter log in the Name column, select void from the list available in the Return Type column and leave the predefined Public as Scope.
- In the block of the window dedicated to parameters we want to enter the two we need our function to accept.
- Let’s enter level as a name for the first one and select LogLevel as the type from the list. As it is not probably included already in the list, we need to go for the “Select Type…” option and find the type as we did before for the requestedLevel attribute.
- Now for the second one. Let’s enter msg as its name. As far as the type we are going to use the old C language char *.
EDIT 2018/01/08 : It is also now possible to use the C++ Standard Library, therefore we could also use
std::stringas the type for the msg parameter. This would introduce a dependency on the
stringlibrary of the C++ Standard Library.
- Switch to the Notes tab in the same block and enter something like “This operation prints a log message on the console depending on the level set in the object“
- Click Close. The diagram should look now as in Fig. 5.
- We can generate the code at this moment and it should compile in Visual Studio. However, we haven’t implemented the behavior of the function yet. As it makes no difference for what we are going to discuss here, we model the behaviour directly in the model by entering the Initial Code as we did before for the accessors to the attribute.
- Let’s open the properties for the operations and select the log operation.
- Behaviour tab -> Initial Code -> type
if (requestedLevel >= level)EDIT 2018/01/08 : If we had modeled
printf("LOG level: %02d Msg: %s\n", level, msg);
std::string, we would probably want to write here instead
if (requestedLevel >= level)
std::cout << "LOG level: " << level << " Msg: " << msg << std::endl;
This would also introduce a dependency on the iostream library of the C++ Standard Library.
Click OK -> Close
- Recompiling the code after generation leads to a failure. The problem is very easy to spot because, as in previous cases, we miss an include directive, in this case the classic (for C Language)
#include <stdio.h>EDIT 2018/01/08: When using the C++ Standard Library we would be missing
- We need to put in the model a dependency on an external library. For that first we create a second package in the MDDTest view. Source code is normally generated for full packages, but, of course, we don’t want to generate the code for the C standard library, therefore we are going to put interfaces to the standard library in a package for which we won’t generate the code.
Context Menu on the MDDTest view in the Project Browser -> Add Package… -> enter External as name -> select Create Diagram. A window for a new diagram opens -> check that UML Structural and Class are selected -> click OK. A new diagram External has been created under the new package
- Open the new diagram -> select the LieberLieber Embedded Engineer tool from the Toolbox
- Select the Interface icon from the Toolbox and drag it into the diagram. A new window appears. Just change the name to stdio and check that LL Embedded C++ is the chosen language. Other values are appropriate. Click OK.
EDIT 2018/01/08: Steps from 13 to 15 are also valid for dependencies on the C++ Standard Library. However, at step 15, after checking the language also modify the stereotype. Click ot the button with the three dots close to it -> A new window appears -> Select LieberLieber Embedded Engineer as Profile -> tick the box close to “standard lib” -> Click OK. This way the code generator would create the correct “C++” include directives.
- Now let’s go back to the MDDTest diagram. From Project Browser (External package) -> select the stdio interface and drag it to the diagram. Accept the default values (show it as a Link)
- Select Class in the Toolbox -> expand the tree Class Relationships -> select Usage -> create a dependency from Logger to External::stdio.
- We can now generate the MDDTest package and we should be able to compile it successfully in Visual Studio. Fig. 6 shows the final version of the model.
As we have seen, it takes very little time to master the basics and use Model Driven Development. What we have shown here constitutes the bulk of the work in modeling. I believe that we can’t overhestimate the importance of concentrating on designing a system.
Next post will be on how to deal with legacy code.
Notice on copyright holders
Visual Studio is a registered mark of Microsoft Corp., USA. Enterprise Architect is a registered mark of SparxSystems Ltd., Australia. Embedded Engineer is a registered mark of LieberLieber Software GmbH, Austria
This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.