Design Analyzer

Design Analyzer (DA) is a tool for analyzing models created using StarUML 2.0.

DA reads a .mdj file created by StarUML, then computes and prints the stability, responsibility, and deviance of every class in the model. DA assumes the model consists of a single class diagram together with the accompanying model elements (classes, associations, generalizations, packages, etc.). Your output should be in table-format in which columns represent metrics and rows classes.

JSON

Assume we use StarUML create a project called HR:

StarUML saves the project in a file called hr.mdj. This file contains a single JSON object that reflects the tree-like structure depicted in the explorer panel to the right of the diagram. You can get a collapsible/expandable look at this huge object by pasting the contents of the file into a JSON formatter/validator.

Here's the basic structure of the JSON object contained in hr.mdj:

{

   "_type": "Project",
   "_id": "AAAAAAFF+h6SjaM2Hec=",
   "name": "HR",
   "ownedElements": [
      {
         "_type": "UMLModel",
         "_id": "AAAAAAFF+qBWK6M3Z8Y=",
         "_parent": {
            "$ref": "AAAAAAFF+h6SjaM2Hec="
         },
         "name": "entities",
         "ownedElements": [...]
      }
   ]
}

We can see that the top-level JSON object, which represents the entire project, has four fields: _type, _id, name ("HR"), and an array of owned elements.

The array contains a single JSON object representing the project's only model: entities. It too has _type, _id, name, and ownedElements fields as well as a _parent field containing a reference to the _id field of its owner, the HR project.

The entities model owns five elements corresponding to main (the class diagram) and the four classes: Company, Employee, Programmer, and Manager.

The class diagram object is long and complicated. For each box, arrow, and label in the diagram it describes in detail things like position, size, color, font, etc. Fortunately, we can skip this part. We're concerned with logic not geometry.

Next come the four class objects. Here's the JSON object representing the Employee class. Notice that it has arrays holding its owned elements (only the association to the Manager class), its attributes (salary, name, and id), and its operations (job and nextID).

{
   "_type": "UMLClass",
   "_id": "AAAAAAFLhnPAnH64Mg8=",
   "_parent": { "$ref": "AAAAAAFF+qBWK6M3Z8Y=" },
   "name": "Employee",
   "ownedElements": [...],
   "visibility": "public",
   "attributes": [...],
   "operations": [...],
   "isAbstract": true,
   "isFinalSpecialization": false,
   "isLeaf": false,
   "isActive": false
}

Our Employer-Manager association doesn't have a name (although it could). Here's what it looks like:

{
   "_type": "UMLAssociation",
   "_id": "AAAAAAFLhn7ApIEXikk=",
   "_parent": { "$ref": "AAAAAAFLhnPAnH64Mg8=" },
   "end1": {...},
   "end2": {...},
   "visibility": "public",
   "isDerived": false
}

Recall that each association has two endpoints. End1 is the side connected to the Employee class, end2 connects to the Manager class. The reference number is the id of the associated class: Here's the end2 object:

{
   "_type": "UMLAssociationEnd",
   "_id": "AAAAAAFLhn7ApIEZWH8=",
   "_parent": { "$ref": "AAAAAAFLhn7ApIEXikk=" },
   "name": "boss",
   "reference": { "$ref": "AAAAAAFLhnRuDX7f/d0=" },
   "visibility": "protected",
   "navigable": true,
   "aggregation": "none",
   "multiplicity": "0..1",
   "isReadOnly": false,
   "isOrdered": false,
   "isUnique": false,
   "isDerived": false,
   "isID": false
}

Here's the salary attribute:

{
   "_type": "UMLAttribute",
   "_id": "AAAAAAFLhnUQgX8HarU=",
   "_parent": { "$ref": "AAAAAAFLhnPAnH64Mg8=" },
   "name": "salary",
   "visibility": "protected",
   "isStatic": false,
   "isLeaf": false,
   "type": "double",
   "isReadOnly": false,
   "isOrdered": false,
   "isUnique": false,
   "isDerived": false,
   "aggregation": "none",
   "isID": false
}

Here's the nextID operation, it contains an array of parameters:

{
   "_type": "UMLOperation",
   "_id": "AAAAAAFLhncCz38rbt0=",
   "_parent": { "$ref": "AAAAAAFLhnPAnH64Mg8=" },
   "name": "nextID",
   "visibility": "private",
   "isStatic": true,
   "isLeaf": false,
   "parameters": [...]
   "concurrency": "sequential",
   "isQuery": false,
   "isAbstract": false
}

The return type of an operation is considered to be a parameter with direction = "return":

{
   "_type": "UMLParameter",
   "_id": "AAAAAAFLhnclH38yalQ=",
   "_parent": { "$ref": "AAAAAAFLhncCz38rbt0=" },
   "visibility": "public",
   "isStatic": false,
   "isLeaf": false,
   "type": "int",
   "isReadOnly": false,
   "isOrdered": false,
   "isUnique": false,
   "direction": "return"
}

NodeJS

JSON stands for "Java Script Object Notation". It is the preferred way to serialize and de-serialize objects in JavaScript. It makes sense, then that processing the JSON objects created by StarUML might be easier in JavaScript rather than a strongly typed language like Java where many awkward casts will need to be performed.

Normally, JavaScript programs are executed by browsers and run in an environment containing a DOM representation of a web page. However, JavaScript is becoming popular as a server-side replacement for PHP. (There are several articles purporting to explain why someone would want to use JavaScript instead of PHP.)

One implementation of a server-size JavaScript interpreter, the one we will be using, is NodeJS.

Sample Programs

Example 1

We want to be able to create a multi-module application. In NodeJS modules are simply files. For example, here's a module called math.js:

// my math functions
exports.square = function(x) { return x * x; }
exports.cube = function(x) { return x * exports.square(x); }

Notice that each function begins with the prefix "exports.". Without this, the function could only ever be visible within the module. It could not be exported to other modules.

Here's the contents of another file called testMath.js:

var mathFuns = require('./math.js');
console.log("cube(4) = " + mathFuns.cube(4));

The require function effectively imports the exported functions from math.js into testMath.js.

If properly installed, we can invoke the NodeJS interpreter from a command console:

c:\demos> node ./testMath
cube(4) = 64

Example 2

I created a useful library of NodeJS functions called tools.js. (Warning: I am not a JavaScript programmer, but my son is an he gave me lots of useful tips.)

The following function loads a file containing a JSON object into a variable:

exports.getModel = function (fname) {
   var fs = require('fs');
   return JSON.parse(fs.readFileSync(fname, 'utf8'));
}

The visit function recursively performs a depth-first traversal of a JSON object applying a call back function to each node:

exports.visit = function(obj, callBack) {
   var result = callBack(obj);
   if (!result && obj != null && (typeof obj == "object" || typeof obj == "array")) {
      for(var i in obj) {
         result = exports.visit(obj[i], callBack);
         if (result) break;
      }
   }
   return result;
}

In case of a search for a specific element, the call back function can return true to indicate to the visitor that the element was found and to suspend the traversal. (I don't use this feature below.)

The collectElements function uses the visit function to extract all sub-elements x of a JSON object such that x.key == val into an array:

exports.collectElements = function(obj, key, val) {
   var result = [];
   var filter = function(elem) {
      if (elem != null && typeof elem[key] != null && elem[key] == val) {
         result.push(elem);
      }
   }
   exports.visit(obj, filter);
   return result;
}

There are a few other useful functions in the file.

My main.js file contains two functions. The first generates a report listing all of the classes and their methods:

var makeReport = function(projectFile) {
   var tools = require('./tools.js');
   var project = tools.getModel(projectFile);
   var classes = tools.collectElements(project, "_type", "UMLClass");
   console.log("project classes:");
   for(var i in classes) {
      console.log("  " + classes[i].name);
      var ops = tools.collectElements(classes[i], "_type", "UMLOperation");
      console.log("    operations:");
      for(var k in ops) {
        console.log("      " + ops[k].name);
      }
   }
};

The main function prompts the user for a file name, then passes it to makeReport:

var main = function() {
   var readline = require('readline');
   var rl = readline.createInterface({
      input: process.stdin,
      output: process.stdout
   });

   rl.question("Enter file name: ", function(answer) {
      makeReport(answer);
      rl.close();
    });
};

At the end of the file main is called:

main(); // call main

Here's a sample run:

c:\demos>node ./main
Enter file name: hr.mdj
project classes:
  Company
    operations:
      payroll
  Programmer
    operations:
      code
  Employee
    operations:
      jobDescription
      nextID
  Manager
    operations:
      fire