Cours Génie logiciel


Gestionnaire de processus

Questions

Question 1 : Il s’avère que d’autres types de processus peuvent exister (par exemple des processus avec différents niveaux de priorités). Selon cette constatation précise, quel principe SOLID a été violé et pourquoi ?

La classe ProcessManager dépend de la classe concrète Process et donc si on souhaite changer de classe pour les processus, on est obligé de changer la classe ProcessManager. Selon le principe DIP, il faut dépendre d’abstractions, pas d’implémentations.

Question 2 : Par ailleurs, le gestionnaire de processus pourrait avoir une autre façon de sélectionner les prochains processus à exécuter et une autre façon d’exécuter un processus pendant un quantum de temps. Selon cette constatation précise, quel principe SOLID a été violé et pourquoi ?

Selon cette remarque, la classe ProcessManager a deux raisons de changer correspondant à l’implémentation des méthodes selectNext et exec qui peuvent changer de manière indépendante (pas de lien entre les changements). La classe ProcessManager a donc deux responsabilités et ne respecte pas SRP.

Question 3 : Si on veut proposer de nouveaux états pour les processus, il faut changer le contenu la classe Process. Selon cette constatation précise, quel principe SOLID a été violé et pourquoi ?

Selon cette remarque, on est obligé de changer le code interne de la classe Process sans possibilité de passer par une extension, car la gestion des états est complétement interne à la classe Process sans passer par d’autres classes. On viole donc OCP.

Question 4 : On souhaite rajouter des processus qui ne sont jamais censés s’arrêter (les processus “démons”). Or, la classe Process possède une méthode int getTimeLeft(). Selon cette constatation précise, quel principe SOLID a été violé et pourquoi ?

Si on regarde l’utilisation de la méthode getTimeLeft dans les classes autres que Process, on s’aperçoit qu’elle n’est utilisé dans ProcessManager que pour savoir si le processus est fini ou pas. Par conséquent, on aurait pu définir à la place une méthode boolean isOver() qui indique si le processus est terminé. Dans ce cas, il n’y aura pas de problème pour définir des processus “démons” qui implémenteraient cette nouvelle méthode. On peut donc dire qu’on viole ISP dans le sens où la classe client ProcessManager dépend de getTimeLeft alors qu’elle n’a pas vraiment besoin de cette fonctionnalité, mais d’une fonctionnalité plus restreinte.

Question 5 : Pour rajouter les processus démons de la question précédente, on vous demande de rajouter des classes AbstractProcess, Daemon et FiniteProcess de telle sorte à ce que Daemon et FiniteProcess étendent AbstractProcess. L’objectif est que FiniteProcess remplace la classe Process et que Daemon permette de créer des processus démons. Pour ceci AbstractProcess contiendra une méthode abstraite boolean isOver() qui indique si le processus est terminé. La classe AbstractProcess ne devra pas avoir de méthode getTimeLeft.

Les classes AbstractProcess, Daemon et FiniteProcess ont le code suivant :

public abstract class AbstractProcess {
    enum State { READY, RUNNING, BLOCKED }
    private final String name;

    private State state = State.READY;
    private int lastExecDate = 0;

    public AbstractProcess(String name) {
        this.name = name;
    }

    public void execQuantum(int date) {
        System.out.println(date + ":" + name);
        lastExecDate = date;
    }

    public void handleSignal(Signal signal) {
        switch (signal) {
            case BLOCK -> {
                if (state == State.RUNNING) {
                    System.out.println(name + " blocked");
                    state = State.BLOCKED;
                }
            }
            case UNBLOCK -> {
                if (state == State.BLOCKED) {
                    System.out.println(name + " unblocked");
                    state = State.READY;
                }
            }
            case SLEEP -> {
                if (state == State.RUNNING)
                    state = State.READY;
            }
            case AWAKE -> {
                if (state == State.READY)
                    state = State.RUNNING;
            }
        }
    }
    public boolean isReady() {
        return state == State.READY;
    }
    public boolean isRunning() {
        return state == State.RUNNING;
    }
    public int getLastExecDate() {
        return lastExecDate;
    }
    public String getName() {
        return name;
    }
    public abstract boolean isOver();
}
public class Daemon extends AbstractProcess {
    public Daemon(String name) {
        super(name);
    }
    @Override
    public boolean isOver() {
        return false;
    }
}
public class FiniteProcess extends AbstractProcess {
    private int timeLeft;

    public FiniteProcess(String name, int duration) {
        super(name);
        this.timeLeft = duration;
    }

    @Override
    public boolean isOver() {
        return timeLeft <= 0;
    }
    @Override
    public void execQuantum(int date){
        super.execQuantum(date);
        timeLeft--;
    }
}

Question 6 : Réécrire la classe ProcessManager pour qu’elle délègue la sélection de processus à une instance d’une classe de l’interface ProcessSelector contenant l’unique méthode Process selectNext(List<Process> processes) (cf. le diagramme ci-dessous).

public class ProcessManager {
    private List<AbstractProcess> processes = new ArrayList<>();
    private int date = 0;
    private AbstractProcess running = null;
    private ProcessSelector selector;


    public ProcessManager(ProcessSelector selector) {
        this.selector = selector;
    }

    public void add(AbstractProcess process) {
        processes.add(process);
        System.out.println(process.getName() + " new");
    }


    public void exec(int nbQuantum) {
        for(int i = 0; i < nbQuantum ; i++) {
            AbstractProcess candidate = selector.select(processes);
            switchRunningProcess(candidate);
            execRunningProcessQuantum();
            date++;
        }
    }

    private void switchRunningProcess(AbstractProcess candidate) {
        if (candidate == null) return;
        if (running != null) running.handleSignal(Signal.SLEEP);
        candidate.handleSignal(Signal.AWAKE);
        running = candidate;

    }

    private boolean canExecRunningProcess() {
        return running != null && running.isRunning();
    }

    private void execRunningProcessQuantum () {
        if (!canExecRunningProcess())
            return;
        running.execQuantum(date);
        if (running.isOver()) {
            processes.remove(running);
            running = null;
        }
    }
}
public interface ProcessSelector {
    AbstractProcess select(List<AbstractProcess> processes);
}
public class RoundRobinSelector implements ProcessSelector{
    @Override
    public AbstractProcess select(List<AbstractProcess> processes) {
        AbstractProcess candidate = null;
        int lastDate = Integer.MAX_VALUE;
        for(AbstractProcess process : processes) {
            int lastExecDate = process.getLastExecDate();
            if(process.isReady() && lastExecDate < lastDate) {
                candidate = process;
                lastDate = lastExecDate;
            }
        }
        return candidate;
    }
}

Question 7 : Pour le moment, les processus définis ne peuvent être que dans trois états : ready, running ou blocked. Comme un processus peut habituellement être dans plus d’états (il existe neuf états de processus sous Unix), il faut prévoir l’ajout (ou le retrait) d’autres états dans des versions ultérieures du programme. Vous allez utiliser le patron de conception State pour définir les états possibles d’un processus ainsi que ses transitions. Dessinez le diagramme de classe correspondant à cette modification.

Question 8 : Écrivez l’interface ProcessState ainsi que les classes qui l’implémentent et décrivez les modifications à faire sur la classe AbstractProcess de manière que le comportement du programme soit le même qu’auparavant.

L’interface ProcessState est la suivante :

public interface ProcessState {
    void handleSignal(Signal signal, AbstractProcess process);
    boolean isRunning();
    boolean isReady();
}

La classe AbstractProcess doit être modifiée comme suivant :

public abstract class AbstractProcess {
    private ProcessState state;

    public AbstractProcess(String name) {
        this.name = name;
        state = new ReadyState();
    }
    public void handleSignal(Signal signal) {
        state.handleSignal(signal, this);
    }

    public boolean isReady() {
        return state.isReady();
    }
    public boolean isRunning() {
        return state.isRunning();
    }
  // code précédent
}

Les classes pour les états sont les suivantes :

public class BlockedState implements ProcessState{


    @Override
    public void handleSignal(Signal signal, AbstractProcess process) {
        if (signal == Signal.UNBLOCK) {
            System.out.println(process.getName() + " unblocked");
            process.setState(new ReadyState());
        }
    }

    @Override
    public boolean isRunning() {
        return false;
    }

    @Override
    public boolean isReady() {
        return false;
    }
}
public class ReadyState implements ProcessState {

    @Override
    public void handleSignal(Signal signal, AbstractProcess process) {
        if(signal == Signal.AWAKE)
            process.setState(new RunningState());
    }

    @Override
    public boolean isRunning() {
        return false;
    }

    @Override
    public boolean isReady() {
        return true;
    }
}
public class RunningState implements ProcessState{

    @Override
    public void handleSignal(Signal signal, AbstractProcess process) {
        if(signal == Signal.BLOCK) {
            System.out.println(process.getName() + " blocked");
            process.setState(new BlockedState());
        }
        else if(signal == Signal.SLEEP)
            process.setState(new ReadyState());
    }

    @Override
    public boolean isRunning() {
        return true;
    }

    @Override
    public boolean isReady() {
        return false;
    }
}

Question 9 : On souhaite rajouter de nouveaux processus avec différents niveaux de priorités allant de 1 à 10 et d’utiliser les priorités pour la sélection du processus à exécuter (par exemple en ne sélectionnant le prochain processus à exécuter uniquement parmi ceux de priorité maximale qui peuvent s’exécuter). Décrivez à l’aide d’un diagramme de classe la manière de réorganiser le code pour prendre compte cette modification.

Pour faire ce changement, le plus simple est d’utiliser le patron de conception stratégie pour la gestion de la méthode isOver puis de créer une classe PriorityProcess étendant Process.

Ensuite on peut paramétrer la classe ProcessManager avec le type des processus :

public class ProcessManager<P extends Process> {
    private final List<P> processes = new ArrayList<>();
    private int date = 0;
    private P running = null;
    ProcessSelector<P> processSelector;

    public ProcessManager(ProcessSelector<P> processSelector) {
        this.processSelector = processSelector;
    }

    public void add(P process) {
        processes.add(process);
        System.out.println(process.getName() + " new");
    }

    private P selectNext() {
        return processSelector.selectNext(processes);
    }

    public void execQuantum(int nbQuantum) {
        for(int i = 0; i < nbQuantum ; i++) {
            P candidate = selectNext();
            switchRunningProcess(candidate);
            execRunningProcessQuantum();
            date++;
        }
    }

    private void switchRunningProcess(P candidate) {
        if (candidate == null) return;
        if (running != null) running.handleSignal(Signal.SLEEP);
        candidate.handleSignal(Signal.AWAKE);
        running = candidate;

    }

    private boolean canExecRunningProcess() {
        return running != null && running.isRunning();
    }

    private void execRunningProcessQuantum () {
        if (!canExecRunningProcess())
            return;
        running.execQuantum(date);
        if (running.isOver()) {
            processes.remove(running);
            running = null;
        }
    }
}

On paramètre l’interface ProcessSelector avec un type P correspondant à un type qui est une extension de Process.

public interface ProcessSelector<P extends Process> {
  P selectNext(List<? extends P> processes);
}

Ensuite, on peut créer des sélecteurs spécifiques qui fonctionne sur les Process ou les PriorityProcess.

public class RoundRobinSelector implements ProcessSelector<Process> {
    @Override
    public Process selectNext(List<? extends Process> processes) {
        Process candidate = null;
        int lastDate = Integer.MAX_VALUE;
        for(Process process : processes)
            if(process.isReady() && process.getLastExecDate() < lastDate) {
                candidate = process;
                lastDate = process.getLastExecDate();
            }
        return candidate;
    }
}
public class PrioritySelector implements ProcessSelector<PriorityProcess>{
    public PriorityProcess selectNext(List<? extends PriorityProcess> processes) {
        PriorityProcess candidate = null;
        int maxPriority = -1;
        for(PriorityProcess process : processes) {
            int priority = process.getPriority();
            if(process.isReady() && priority > maxPriority) {
                candidate = process;
                maxPriority = priority;
            }
        }
        return candidate;
    }
}

Finalement, il faut adapter le code de TestProcessManager.

public class TestProcessManager {
    public static void main(String[] args) {
        PriorityProcess p1 = new PriorityProcess("P1", 0, new DaemonTimeManager());
        PriorityProcess p2 = new PriorityProcess("P2", 5, new DaemonTimeManager());
        PriorityProcess p3 = new PriorityProcess("P3", 10, new DaemonTimeManager());
        ProcessManager<PriorityProcess> processManager = new ProcessManager<>(new PrioritySelector());
        processManager.add(p1);
        processManager.add(p2);
        processManager.execQuantum(10);
        processManager.add(p2);
        processManager.execQuantum(2); 
        processManager.add(p3); 
        p1.handleSignal(Signal.BLOCK);
        processManager.execQuantum(3);
        p1.handleSignal(Signal.UNBLOCK);
        processManager.execQuantum(10);
    }
}