dimanche 19 septembre 2021

Refractor redundant code through structural design pattern

I have the following snippet of a code that checks the type compatibility between a variable assignment and the expression of a statement that emulates for-in behavior from some ANTLR grammars.

// LoopAnalyzer.java

@Override
public void exitForInLoop(ForInLoopContext ctx) {
    final String rightHandSideType = this.determineExpressionReturnType(ctx.statementExpression());
    this.verifyTypeCompatibility(ctx.start, ctx.solType().getText(), rightHandSideType);
}

This code will read and determine the statement expression return type as denoted by the determineExpressionReturnType method, then verify the type compatibility by comparing the variable declaration type with the array's primitive form on the right-hand side. More specifically, given a snippet of an example for-in statement:

for (string item in items) {
    // do something with the item
}

In that example, the type of item should match with the primitive form of items, and that items itself should be an array. Otherwise, an incompatibility error message is raised, impeding the for-in statement to enumerate over object items. The logic of the type compatibility checking that resembles such behavior is coded at the verifyTypeCompatibility method shown below:

// LoopAnalyzer.java

private void verifyTypeCompatibility(Token token, String leftHandSideType, String rightHandSideType) {
    if (rightHandSideType == null || leftHandSideType == null) {
        return;
    }

    if (!TypeUtils.isArrayType(leftHandSideType, rightHandSideType)) {
        this.addError(token, String.format("Types are not compatible, cannot check containment of %s in %s.", leftHandSideType, rightHandSideType));
    }
}

The code looks good until I realize that the type checking of the for-in statement and the type checking of a variable declaration are very much alike with only small differences. Unlike for-in statements, a variable declaration statement requires only the statement expression type equals the type assigned to the variable name.

For simplicity, the snippet of the code that checks the variable declaration statement is shown as below:

// ExpressionStatementAnalyzer.java

@Override
public void exitVariableDeclarationStatement(VariableDeclarationStatementContext ctx) {
    final String rightHandSideType = this.determineExpressionReturnType(ctx.statementExpression());
    this.verifyTypeCompatibility(ctx.start, ctx.solType().getText(), rightHandSideType);
}

private void verifyTypeCompatibility(Token token, String leftHandSideType, String rightHandSideType) {
    if (rightHandSideType == null || leftHandSideType == null) {
        return;
    }

    if (!TypeUtils.areCompatible(leftHandSideType, rightHandSideType)) {
        this.addError(token, String.format("Cannot assign a %s value to a %s variable.", rightHandSideType, leftHandSideType));
    }
}

With these small differences, how can we extract similar components to be reusable within the two classes?

Currently, I can only think of structural design patterns where we keep the determineExpressionReturnType and verifyTypeCompatibility in the ExpressionStatementAnalyzer class and let the LoopAnalyzer extends the class so that it can override the currently available implementation for both methods. However, this approach is unlikely to be the solution since analyzing loop statements has nothing to do with analyzing the statement expression (i.e., they supposedly do not inherit each other). Thus I asked the question.

Aucun commentaire:

Enregistrer un commentaire