Validation des entrées avec des automates finis en Java

Validation des entrées avec des automates finis en Java

1. Vue d'ensemble

Si vous avez étudié CS, vous avez sans aucun doute suivi un cours sur les compilateurs ou quelque chose de similaire; dans ces classes, le concept d'automate fini (également connu sous le nom de machine à états finis) est enseigné. C'est une façon de formaliser les règles de grammaire des langues.

Vous pouvez en savoir plus sur le sujethere ethere.

Alors, comment ce concept oublié peut-il nous être utile, programmeurs de haut niveau, qui n’avons pas à nous soucier de la création d’un nouveau compilateur?

Eh bien, il s’avère que le concept peut simplifier un grand nombre de scénarios commerciaux et nous permettre de raisonner sur une logique complexe.

Comme exemple rapide, nous pouvons également valider une entrée sans une bibliothèque tierce externe.

2. L'algorithme

En un mot, une telle machine déclare les états et les moyens de passer d’un état à un autre. Si vous faites passer un flux par celui-ci, vous pouvez valider son format avec l'algorithme suivant (pseudocode):

for (char c in input) {
    if (automaton.accepts(c)) {
        automaton.switchState(c);
        input.pop(c);
    } else {
        break;
    }
}
if (automaton.canStop() && input.isEmpty()) {
    print("Valid");
} else {
    print("Invalid");
}

Nous disons que l'automate "accepte" le caractère donné s'il y a une flèche partant de l'état actuel, qui contient le caractère. La commutation d'états signifie qu'un pointeur est suivi et que l'état actuel est remplacé par l'état indiqué par la flèche.

Enfin, lorsque la boucle est terminée, nous vérifions si l’automate «peut s’arrêter» (l’état actuel est à double cercle) et que l’entrée a été épuisée.

3. Un exemple

Écrivons un validateur simple pour un objet JSON, pour voir l'algorithme en action. Voici l'automate qui accepte un objet:

image

Notez que la valeur peut être l'une des valeurs suivantes: chaîne, entier, booléen, null ou un autre objet JSON. Par souci de concision, dans notre exemple, nous ne considérerons que des chaînes.

3.1. Le code

L'implémentation d'une machine à états finis est assez simple. Nous avons les éléments suivants:

public interface FiniteStateMachine {
    FiniteStateMachine switchState(CharSequence c);
    boolean canStop();
}

interface State {
    State with(Transition tr);
    State transit(CharSequence c);
    boolean isFinal();
}

interface Transition {
    boolean isPossible(CharSequence c);
    State state();
}

Les relations entre eux sont:

  • La machine à états a unState courant et nous dit si elle peut s'arrêter ou non (si l'état est final ou non)

  • UnState a une liste de transitions qui pourraient être suivies (flèches sortantes)

  • UnTransition nous dit si le caractère est accepté et nous donne lesState suivants

publi class RtFiniteStateMachine implements FiniteStateMachine {

    private State current;

    public RtFiniteStateMachine(State initial) {
        this.current = initial;
    }

    public FiniteStateMachine switchState(CharSequence c) {
        return new RtFiniteStateMachine(this.current.transit(c));
    }

    public boolean canStop() {
        return this.current.isFinal();
    }
}

Notez que lesFiniteStateMachine implementation is immutable. C’est principalement pour qu’une seule instance puisse être utilisée plusieurs fois.

Ensuite, nous avons l'implémentationRtState. La méthodewith(Transition) retourne l'instance après l'ajout de la transition, pour la fluidité. UnState nous dit également s'il est définitif (double encerclé) ou non.

public class RtState implements State {

    private List transitions;
    private boolean isFinal;

    public RtState() {
        this(false);
    }

    public RtState(boolean isFinal) {
        this.transitions = new ArrayList<>();
        this.isFinal = isFinal;
    }

    public State transit(CharSequence c) {
        return transitions
          .stream()
          .filter(t -> t.isPossible(c))
          .map(Transition::state)
          .findAny()
          .orElseThrow(() -> new IllegalArgumentException("Input not accepted: " + c));
    }

    public boolean isFinal() {
        return this.isFinal;
    }

    @Override
    public State with(Transition tr) {
        this.transitions.add(tr);
        return this;
    }
}

Et enfin,RtTransition qui vérifie la règle de transition et peut donner lesState suivants:

public class RtTransition implements Transition {

    private String rule;
    private State next;
    public State state() {
        return this.next;
    }

    public boolean isPossible(CharSequence c) {
        return this.rule.equalsIgnoreCase(String.valueOf(c));
    }

    // standard constructors
}

Le code ci-dessus esthere. Avec cette implémentation, vous devriez pouvoir construire n’importe quelle machine à états. L'algorithme décrit au début est aussi simple que:

String json = "{\"key\":\"value\"}";
FiniteStateMachine machine = this.buildJsonStateMachine();
for (int i = 0; i < json.length(); i++) {
    machine = machine.switchState(String.valueOf(json.charAt(i)));
}

assertTrue(machine.canStop());

Vérifiez la classe de testRtFiniteStateMachineTest pour voir la méthodebuildJsonStateMachine(). Notez qu'il ajoute quelques états de plus que l'image ci-dessus, afin de saisir également les guillemets qui entourent correctement les chaînes.

4. Conclusion

Les automates finis sont d'excellents outils que vous pouvez utiliser pour valider des données structurées.

Cependant, ils ne sont pas très connus car ils peuvent se compliquer lorsqu'il s'agit de saisie complexe (car une transition ne peut être utilisée que pour un seul caractère). Néanmoins, ils sont parfaits pour vérifier un ensemble de règles simples.

Enfin, si vous voulez faire un travail plus compliqué en utilisant des machines à états finis,StatefulJ etsquirrel sont deux bibliothèques qui méritent d'être examinées.

Vous pouvez vérifier les échantillons de codeon GitHub.