JavaArgs is a java version of Args that is described in a book by Uncle Bob in his book called Clean Code.
This piece of code has been originally forked from the Args program described in: Uncle Bob. I have further tried to implement a clean coding practice on this source code written in Java as part of Assignment given in Software Engineering course taught (in Spring 2020) at International Institute of Information Technology
Use the eclipse IDE to run JavaArgs
Before you Run the code, you might also nees to check for environment setup for Java:
Run:
* sudo add-apt-repository ppa:openjdk-r/ppa
* sudo apt-get update -q
* sudo apt install -y openjdk-11-jdk
Run:
* Run the command given below from the root folder of this repo
* 'java -cp "lib/junit-4.13.jar:lib/hamcrest-core-1.3.jar:build/jar/args.jar" ./test/com/cleancoder/args/ArgsTest.java testCreateWithNoSchemaOrArguments'
Once you have set-up the basic requirements and your environment is ready, you may use the following code to implement the main class ArgsMain.java
package com.cleancoder.args;
import java.util.Map;
public class ArgsMain {
/**
* This method is used to add two integers. This is
* a the simplest form of a class method, just to
* show the usage of various javadoc Tags.
* @param args This is the first paramter to addNum method
*/
public static void main(String[] args) {
try {
Args arg = new Args("l,p#,d*,b##, c[*], m&", args);
boolean logging = arg.getBoolean('l');
int port = arg.getInt('p');
double dob = arg.getDouble('b');
String[] cs = arg.getStringArray('c');
Map<String, String> ma = arg.getMap('m');
String directory = arg.getString('d');
executeApplication(logging, port, directory, dob, cs, ma);
} catch (ArgsException e) {
System.out.printf("Argument error: %s\n", e.errorMessage());
}
}
private static void executeApplication(boolean logging, int port, String directory, double dob, String[] cs, Map<String, String> ma) {
System.out.printf("logging is %s, port:%d, directory:%s, double:%f, stringArray:%s, map:%s",logging, port, directory, dob, cs[0], ma);
}
}
Add ArgsMain.java
file in at the follwing path : root/src/com/cleancoder/args
in your code and use the below schema to operate:
Schema:
- char - Boolean arg.
- char* - String arg.
- char# - Integer arg.
- char## - double arg.
- char[*] - one element of a string array.
Example schema: (f,s*,n#,a##,p[*])
Coresponding command line: "-f -s Bob -n 1 -a 3.2 -p e1 -p e2 -p e3
The goal of this exercise is to implement clean code practises in the code for JavaArgs and that include the following :
- Removing Lints
- Imporving Code-coverage
- Refactor Control Flow
- Refactor Classes and Methods
- End to End Test ..* Add tests for uncovered code ..* Remove duplicate tests ..* Add Test Annotation [Decorators] ..* Refactor if/else/try/catch conditions to improve coverage
- Remove Code Smells
There is a list of comprehensive actions performed under the above-mentioned points and are discussed below in detail.
Or you may find it in eclipse at
src/ArgsClassDiagram.ucls
To understand the code better follow the JavaDocs given for the code.
JavaDocs for tests are not available so as to reduce the on-screen clutter of code and I have tried to make classes/methods/ arg names to be more descriptive in-order to save the clutter
For more discussion on JavaDocs for test check this Stackoverflow 💡 Link.
In this section I am going to discuss about the code cleaning that has been done for the JavaArgs Code.
As stated above there are some major practises that were utilised to clean the code, namely:
1. Removing Code Smells
2. Imporving Code-coverage
3. Refactoring Control Flow
4. Refactoring Classes and Methods
5. End to End Testing
..* Add tests for uncovered code
..* Remove duplicate tests
..* Add Test Annotation [Decorators]
..* Refactoring if/else/try/catch conditions to improve coverage
6. Removing Code Lint
For the above changes some tools were used which needs to be mentioned here :
- 📌 Code Smell : I used Jdeodrant 💚 integration to remove code smells and lints.
- 📌 Code Coverage: I used Junit ❤️
- 📌 Linting : I used CheckStyle ❤️ to remove lint from code
Now lets deep dive into the knitty-gritty of the various aspects of code cleaning:
- 🔘 Code coverage has been imporved from 88.5 % 🔻 to ** 91% ** ✔️ overall.
- 🔘 Code coverage is at a at a whopping 97% ✔️ if tests are not considered.
..* As per the discussion online, tests are generally not covered during the coveraged for more information check this Stackoverflow 💊 link.
Below mentioned are the refactored pieces of code that helped imporve Code Coverage:
❌ Unused lines for methods were removed to imporve coverage as they were affecting significantly.
From ArgsException.java
🔴 --remove
public ArgsException() {}
public ArgsException(String message) {super(message);}
🔂
public void setErrorCode(ErrorCode errorCode) { this.errorCode = errorCode;}
From ArgsExceptionTest.java
➕ Test for OK condition
testOkMessage()
was not available and OK enum was not covered.
Refer below to the piece of code added :
public void testOkMessage() throws Exception {
ArgsException e = new ArgsException(OK, 'x', null);
assertEquals("TILT: Should not get here.", e.errorMessage());
}
From ArgsData.java
🔴 --remove
public ArgsData() {}
❌ Removed wildcards from all import statement in the project. [also from java utils]
There are various ways to add imports in java and one of the two ways is adding imports with wildcards (*)
, I chose to remove all the wildcards and manually add imports taking a reference from the following discussion Stackoverflow 💊 .
➕ Followed some code conventions where I added static
imports before all the util imports and arranged alphabetically.
〽️ In file Argsexception.java
Return
statement shifted from outside of switch case to default case inside switch case.
default:
return ""; // return statement shifted here
}
The vanilla code form base repository had some major code smells in mainly in the follwing files
🔻 Args.java
🔻 ArgsData.java
So inorder to remove code smells I handled the following changes.
Those were in
- God Class
..* for
schema
Extract Class - Long Method ..* Extract Method for variable criteria 'm'
The changes that are stated below are in-coherence with the following manual which clearly states the decomposition methodology. For details refer to Jdeodrant
➕
Name | Refactoring Type | Variable Criteria |
---|---|---|
Long method | Extract method | m |
if (m == null) {
throw new ArgsException(UNEXPECTED_ARGUMENT, argChar, null);
Also,
➕ File ArgsData.java
was created in order to decompose the following class:
Name | Refactoring Type | Variable Criteria |
---|---|---|
God Class | Extract Class | Schema |
❌ Following Code from Args.java
has been decomposed into ArgsData.java
private void parseSchemaElement(String element) throws ArgsException {
char elementId = element.charAt(0);
String elementTail = element.substring(1);
validateSchemaElementId(elementId);
if (elementTail.length() == 0)
marshalers.put(elementId, new BooleanArgumentMarshaler());
else if (elementTail.equals("*"))
marshalers.put(elementId, new StringArgumentMarshaler());
else if (elementTail.equals("#"))
marshalers.put(elementId, new IntegerArgumentMarshaler());
else if (elementTail.equals("##"))
marshalers.put(elementId, new DoubleArgumentMarshaler());
else if (elementTail.equals("[*]"))
marshalers.put(elementId, new StringArrayArgumentMarshaler());
else if (elementTail.equals("&"))
marshalers.put(elementId, new MapArgumentMarshaler());
else
throw new ArgsException(INVALID_ARGUMENT_FORMAT, elementId, elementTail);
}
private void validateSchemaElementId(char elementId) throws ArgsException {
if (!Character.isLetter(elementId))
throw new ArgsException(INVALID_ARGUMENT_NAME, elementId, null);
}
public boolean getBoolean(char arg) {
return BooleanArgumentMarshaler.getValue(marshalers.get(arg));
}
public String getString(char arg) {
return StringArgumentMarshaler.getValue(marshalers.get(arg));
}
public int getInt(char arg) {
return IntegerArgumentMarshaler.getValue(marshalers.get(arg));
}
public double getDouble(char arg) {
return DoubleArgumentMarshaler.getValue(marshalers.get(arg));
}
public String[] getStringArray(char arg) {
return StringArrayArgumentMarshaler.getValue(marshalers.get(arg));
}
public Map<String, String> getMap(char arg) {
return MapArgumentMarshaler.getValue(marshalers.get(arg));
}
}
➕ Following test code has been added in order to cover all the test conditions and remove any potential smells if present at all
public void testOkMessage() throws Exception {
ArgsException e = new ArgsException(OK, 'x', null);
assertEquals("TILT: Should not get here.", e.errorMessage());
}
also to handle concatinated type strings as arguments.
@Test
public void testContinuousFlags() throws Exception {
Args argsData = new Args("x#,y##", new String[]{"-xy", "20", "4.5"});
assertTrue(argsData.has('x'));
assertTrue(argsData.has('y'));
assertEquals(20, argsData.getInt('x'));
assertEquals(4.5, argsData.getDouble('y'),.001);
}
}
➕ MalformedMap
has been modified as well since it was incomplete in the code.
@Test
public void malFormedMapArgument() throws Exception {
try {
new Args("f&", new String[] {"-f", "key1:val1,key2"});
fail();
} catch (ArgsException e) {
assertEquals(MALFORMED_MAP, e.getErrorCode());
assertEquals('f', e.getErrorArgumentId());
}
}
The JavaArgs program is not fully bug free but has a bug in the schema which is described below :
The arguments essentially read only single command line character that is passed and overlooking the characters passed after.
For example
Args args = new Args("x,y", new String[]{"-x", "alpha", "-y", "beta"});
In the above code the arguments taken and tested are primarily alpha and not beta if passed as serial arguments.
➕
Also, in this code if the command line arguments parsed are
-s &
value returned is an address of character and not the output.
In some cases the same has been observed in *
character.
Fix for this would be changing the schema definition handling and parsing in the code