Eunice helps improve software architecture by analyzing, measuring, exploring and defining; structure and dependencies.
Eunice is based on the premise that software structure and modularity improve when dependencies are unidirectional. Eunice's approach isn't limited to direct dependencies, but includes indirect dependencies as well.
With Eunice you define an intended structure/architecture, it shows you dependencies that don't match, and you can further refine the definition or update your code to match.
I've based the case study on the 0f3838a commit in the React repository. I've created a fork to make it easier if you want to follow along:
git clone https://github.com/DevSnicket/react.git react-eunice cd react-eunice
Eunice can be downloaded from NPM and run without installing using NPX:
You will be prompted to accept a EULA. Eunice is free to use on open source projects, and its free to use for education and training. Therefore contributions to React and following along with this case study both qualify. There is also an evaluation period to try Eunice out on closed source commercial software.
Running Eunice on React without any parameters will raise the following error:
Error: Analysis of file "packages\create-subscription\index.js" raised the following error. Unexpected token, expected ";" (14:5)
npx eunice \ --babel-parser-plugins=classPrivateProperties \ --babel-parser-plugins=classProperties \ --babel-parser-plugins=dynamicImport \ --babel-parser-plugins=flow \ --babel-parser-plugins=jsx
Running Eunice with the Babel plug-ins specified above will raise a further error:
Error: Analysis of file "scripts\prettier\index.js" raised the following error. 'return' outside of function (32:2)
Eunice parses with a Babel source type of module, but the file specified in the error is a script. The option ignore-path-pattern can be used to ignore the "script" directory. I've also included the default ignores and one for babel.config.js. The option is specified with double quotes as the pipe character is used in the regular expression.
npx eunice \ --ignore-path-pattern="(^(\.|babel.config.js|scripts)|node_modules)" \ --babel-parser-plugins=classPrivateProperties \ --babel-parser-plugins=classProperties \ --babel-parser-plugins=dynamicImport \ --babel-parser-plugins=flow \ --babel-parser-plugins=jsx
Analysis with the options above should complete successfully and output a eunice.html file. Opening the file in a browser should show the following:
On the right is a graphical representation of all the dependencies and the root directories (fixtures and packages) in React. The green and red count arrows represent the dependencies, categorized as follows:
The items in the lowest level are all the module import and CommonJS require calls that Eunice automatically moves below the file content. The other two levels have dependencies that don’t match and so have counts shown in red up arrows. By default Eunice expects code within a file to be ordered high level first with more detail as you move down. The option is-file-content-reversed can reverse this:
npx eunice \ --is-file-content-reversed=true \ --ignore-path-pattern="(^(\.|babel.config.js|scripts)|node_modules)" \ --babel-parser-plugins=classPrivateProperties \ --babel-parser-plugins=classProperties \ --babel-parser-plugins=dynamicImport \ --babel-parser-plugins=flow \ --babel-parser-plugins=jsx
The file will then show with all matching green dependency counts (after reloading the browser):
Looking at the root directory, a lot more of Reacts dependencies are now shown as matching in green:
There are still 1,592 dependencies, shown in a red horizontal arrow, that are between items that don’t have a structure defined yet.
There are often conventions in codebases and in React there are many instances of directories that are always either above or below all the other directories. One example is that the tests directory depends on what it tests. Another example is that the rest of the code depends on the shared directory. This is represented below in YAML, with “existing” being used as placeholder for items not specified:
- [ forks, __tests__ ] - - existing - - src - - shared
The YAML above can be saved into a file (.eunice-stack-modify.yaml) and that file can be specified with the modify-stacks-file option so that Eunice modifies all stacks with the following option:
npx eunice \ --modify-stacks-file=.eunice-stack-modify.yaml \ --is-file-content-reversed=true \ --ignore-path-pattern="(^(\.|babel.config.js|scripts)|node_modules)" \ --babel-parser-plugins=classPrivateProperties \ --babel-parser-plugins=classProperties \ --babel-parser-plugins=dynamicImport \ --babel-parser-plugins=flow \ --babel-parser-plugins=jsx
You can see examples of what effect this has in React by looking inside packages/react-interactions/events and its sub-directory src/dom:
This reduces the count of mismatching dependencies in the red horizontal arrow by over a third, from 1,592 to 903:
YAML stack files of structure like the one above can also be created for individual directories. Eunice automatically does this when files named .eunice-stack.yaml are saved with the code. I’ve worked through React and created 36 stack files. If you want to view the files I’ve created a branch called with-stacks was created and the stack files added in commit 4dffcae.
The stacks have removed all the remaining 903 unstructured dependencies; however, I wasn’t able to define structures that would make all dependencies go in the same direction. These mismatching dependencies are shown in the red up arrow count of 815 below:
You can see more specific dependency count arrows and the structure I chose, by looking inside the packages sub-directory:
You can interact with the result of this case study here.
An example of software that’s close to having only unidirectional dependencies is Eunice itself, you can see this by looking at its dogfooding:
You can interact with Eunice’s dogfooding here.
The structures I’ve chosen for React are what I think are best fit for the current dependencies. This structure might not be ideal for React and its future development. It might be beneficial to structure differently, which would have higher counts of current mismatching dependencies, but would encourage contributors to change the code and its dependencies to better match that intended structure.
I defined these structures for React as an individual; however, by committing stack files to source control and running Eunice (e.g. in continuous integration), teams could collaborate and collectively define and improve the structure.
The case study isn’t intended as a commentary or critique of React. Decisions made during it are constrained by the authors understanding of React. With more insight into React, different choices might be made if Eunice was used by its contributors.
Suggestions to improve the case study are welcome. Hopefully it’s helpful to React contributors or to those who have an interest in React.
By Graham Dyson - creator of Eunice
Find out more at https://devsnicket.com/eunice.