J'ai deux méthodes simples:
public void proceedWhenError() {
Throwable exception = serviceUp();
if (exception == null) {
// do stuff
} else {
logger.debug("Exception happened, but it's alright.", exception)
// do stuff
}
}
public void doNotProceedWhenError() {
Throwable exception = serviceUp();
if (exception == null) {
// do stuff
} else {
// do stuff
throw new IllegalStateException("Oh, we cannot proceed. The service is not up.", exception)
}
}
La troisième méthode est une méthode d'assistance privée:
private Throwable serviceUp() {
try {
service.connect();
return null;
catch(Exception e) {
return e;
}
}
Nous avons eu une petite conversation avec un de mes collègues sur le modèle utilisé ici:
retourne un objet Exception
(ou Throwable
) à partir de la méthode serviceUp()
.
Le premier avis:
C'est un anti-modèle d'utiliser Exceptions pour contrôler le flux de travail et nous ne devrions renvoyer que des booléens à partir de
serviceUp()
et jamais l'objet Exception lui-même. L'argument est qu'utiliser Exceptions pour contrôler le flux de travail est un anti-modèle.
Le deuxième avis:
C'est bon, car nous devons traiter l'objet ensuite dans le fichier deux premières méthodes différemment et si retourne un objet Exception ou booléen ne change pas le flux de travail du tout
Pensez-vous que 1) ou 2) est correct et en particulier, pourquoi? Notez que la question concerne UNIQUEMENT la méthode serviceUp()
et son type de retour - boolean
vs Exception
object.
Note: Je ne me demande pas s'il faut utiliser des objets Throwable ou Exception.
C'est un anti-modèle d'utiliser des exceptions pour diriger le flux uniquement lorsque l'exception est levée dans une situation non exceptionnelle.*. Par exemple, terminer une boucle en lançant une exception lorsque vous atteignez la fin d'une collection est un anti-motif.
En revanche, le contrôle du flux avec les exceptions réelles constitue une bonne application des exceptions. Si votre méthode rencontre une situation exceptionnelle qu’elle ne peut pas gérer, elle doit lever une exception, ce qui permet de rediriger le flux de l’appelant vers le bloc du gestionnaire des exceptions.
Renvoyer un objet Exception
"nu" à partir d'une méthode, plutôt que de le lancer, est certainement contre-intuitif. Si vous devez communiquer les résultats d'une opération à l'appelant, une meilleure approche consiste à utiliser un objet d'état qui englobe toutes les informations pertinentes, y compris l'exception:
public class CallStatus {
private final Exception serviceException;
private final boolean isSuccess;
public static final CallStatus SUCCESS = new CallStatus(null, true);
private CallStatus(Exception e, boolean s) {
serviceException = e;
isSuccess = s;
}
public boolean isSuccess() { return isSuccess; }
public Exception getServiceException() { return serviceException; }
public static CallStatus error(Exception e) {
return new CallStatus(e, false);
}
}
Désormais, l'appelant recevra CallStatus
de serviceUp
:
CallStatus status = serviceUp();
if (status.isSuccess()) {
... // Do something
} else {
... // Do something else
Exception ex = status.getException();
}
Notez que le constructeur est privé, donc serviceUp
renverrait soit CallStatus.SUCCESS
, soit appeler CallStatus.error(myException)
.
* Ce qui est exceptionnel et ce qui ne l’est pas exceptionnel dépend beaucoup du contexte. Par exemple, les données non numériques provoquent une exception dans la variable Scanner
de nextInt
, car elle considère que ces données ne sont pas valides. Cependant, les mêmes données exactes ne provoquent pas d'exception dans la méthode hasNextInt
, car elles sont parfaitement valides.
Le deuxième avis ("ça va") ne tient pas. Le code n'est pas correct car renvoyer des exceptions au lieu de les lancer n'est pas vraiment idiomatique.
Je n'achète pas non plus le premier avis ("utiliser Exceptions pour contrôler le flux de travail est anti-modèle"). service.connect()
génère une exception et vous devez réagir à cette exception - donc ceci est efficacement le contrôle de flux. Renvoyer un boolean
ou un autre objet d'état et le traiter au lieu de gérer une exception - et penser qu'il ne s'agit pas d'un flux de contrôle basé sur une exception est naïf. Un autre inconvénient est que si vous décidez de redéfinir une exception (encapsulée dans IllegalArgumentException
ou quoi que ce soit d'autre), vous n'aurez plus l'exception originale. Et c'est extrêmement mauvais lorsque vous essayez d'analyser ce qui s'est réellement passé.
Je ferais donc le classique:
serviceUp
.serviceUp
: try-catch
, enregistrez le débogage et avalez l'exception si vous souhaitez continuer l'exception.try-catch
et renvoyez l'exception encapsulée dans une autre exception fournissant plus d'informations sur ce qui s'est passé. Sinon, laissez simplement l'exception originale se propager via throws
si vous ne pouvez rien ajouter de substantiel.Il est très important de ne pas perdre l'exception originale.
Je retournerais un ServiceState
qui peut être, par exemple, RUNNING
, WAITING
, CLOSED
. La méthode serait nommée getServiceState
.
enum ServiceState { RUNNING, WAITING, CLOSED; }
Je n'ai jamais vu les méthodes qui renvoient une exception à la suite de l'exécution. Pour moi, quand une méthode renvoie la valeur, cela signifie que la méthode a terminé son travail sans problème . Je ne veux pas récupérer le résultat et l’analyser avec aucune erreur. Le résultat lui-même signifie que aucun échec n’est survenu - tout s’est déroulé comme prévu.
Par contre, lorsque la méthode lève une exception, je dois analyser un objet special pour comprendre ce qui ne va pas. Je n'analyse pas le résultat, car il n'y a pas de résultat .
Un exemple:
public void proceedWhenError() {
final ServiceState state = getServiceState();
if (state != ServiceState.RUNNING) {
logger.debug("The service is not running, but it's alright.");
}
// do stuff
}
public void doNotProceedWhenError() {
final ServiceState state = getServiceState();
if (state != ServiceState.RUNNING) {
throw new IllegalStateException("The service is not running...");
}
// do stuff
}
private ServiceState getServiceState() {
try {
service.connect();
return ServiceState.RUNNING;
catch(Exception e) {
// determine the state by parsing the exception
// and return it
return getStateFromException(e);
}
}
Si les exceptions levées par le service sont importantes et/ou traitées dans un autre endroit, elles peuvent être enregistrées avec l'état dans un objet ServiceResponse
:
class ServiceResponse {
private final ServiceState state;
private final Exception exception;
public ServiceResponse(ServiceState state, Exception exception) {
this.state = state;
this.exception = exception;
}
public static ServiceResponse of(ServiceState state) {
return new ServiceResponse(state, null);
}
public static ServiceResponse of(Exception exception) {
return new ServiceResponse(null, exception);
}
public ServiceState getState() {
return state;
}
public Exception getException() {
return exception;
}
}
Maintenant, avec ServiceResponse
, ces méthodes pourraient ressembler à ceci:
public void proceedWhenError() {
final ServiceResponse response = getServiceResponse();
final ServiceState state = response.getState();
final Exception exception = response.getException();
if (state != ServiceState.RUNNING) {
logger.debug("The service is not running, but it's alright.", exception);
}
// do stuff
}
public void doNotProceedWhenError() {
final ServiceResponse response = getServiceResponse();
final ServiceState state = response.getState();
final Exception exception = response.getException();
if (state != ServiceState.RUNNING) {
throw new IllegalStateException("The service is not running...", exception);
}
// do stuff
}
private ServiceResponse getServiceResponse() {
try {
service.connect();
return ServiceResponse.of(ServiceState.RUNNING);
catch(Exception e) {
// or -> return ServiceResponse.of(e);
return new ServiceResponse(getStateFromException(e), e);
}
}
La dissonance cognitive évidente est l'anti-modèle ici. Un lecteur vous verra utiliser des exceptions pour le contrôle de flux et un développeur essaiera immédiatement de le recoder s'il ne le fait pas.
Mon instinct suggère une approche comme celle-ci:
// Use an action name instead of a question here because it IS an action.
private void bringServiceUp() throws Exception {
}
// Encapsulate both a success and a failure into the result.
class Result {
final boolean success;
final Exception failure;
private Result(boolean success, Exception failure) {
this.success = success;
this.failure = failure;
}
Result(boolean success) {
this(success, null);
}
Result(Exception exception) {
this(false, exception);
}
public boolean wasSuccessful() {
return success;
}
public Exception getFailure() {
return failure;
}
}
// No more cognitive dissonance.
private Result tryToBringServiceUp() {
try {
bringServiceUp();
} catch (Exception e) {
return new Result(e);
}
return new Result(true);
}
// Now these two are fine.
public void proceedWhenError() {
Result result = tryToBringServiceUp();
if (result.wasSuccessful()) {
// do stuff
} else {
logger.debug("Exception happened, but it's alright.", result.getFailure());
// do stuff
}
}
public void doNotProceedWhenError() throws IllegalStateException {
Result result = tryToBringServiceUp();
if (result.wasSuccessful()) {
// do stuff
} else {
// do stuff
throw new IllegalStateException("Oh, we cannot proceed. The service is not up.", result.getFailure());
}
}
Si les appelants d'une méthode s'attendent à ce qu'une opération réussisse ou échouent et soient prêts à gérer l'un ou l'autre cas, la méthode doit alors indiquer, généralement via une valeur renvoyée, si l'opération a échoué. Si les appelants ne sont pas prêts à gérer efficacement les échecs, la méthode dans laquelle l'incident est survenu doit alors renvoyer l'exception plutôt que de demander à ceux-ci d'ajouter du code.
Le problème, c’est que certaines méthodes auront des appelants prêts à gérer avec élégance les échecs et d’autres non. Je privilégierais que ces méthodes acceptent un rappel facultatif qui est appelé en cas d’échec; si aucun rappel n'est fourni, le comportement par défaut devrait être de lever une exception. Une telle approche réduirait le coût de la création d'une exception dans les cas où un appelant est prêt à gérer une défaillance, tout en minimisant la charge qui en découle pour les appelants. La plus grande difficulté d’une telle conception est de décider des paramètres qu’un tel rappel devrait prendre, étant donné que la modification ultérieure de ces paramètres risque d’être difficile.
renvoyer une exception est en effet un anti-modèle parce que les exceptions devraient être réservées aux erreurs d'exécution, et non à la description de la condition du service.
imaginez s'il y a un bogue dans le code de serviceUp()
qui le fait lancer NullPointerException
. Maintenant, imaginez que le bogue soit dans le service et que la même NullPointerException
soit émise par la connect()
.
Voir mon point?
Une autre raison est l'évolution des exigences.
Actuellement, le service a deux conditions: soit en hausse, soit en baisse.
Actuellement.
Demain, vous aurez trois conditions pour le service: haut, bas. ou fonctionnant avec des avertissements. Le lendemain, vous voudrez également que la méthode retourne des détails sur le service dans json .....
Comme mentionné dans les commentaires précédents "L'exception est un événement", l'objet exception que nous obtenons est juste un détail de l'événement . Une fois qu'une exception est interceptée dans un bloc "catch" et n'est pas relancée, l'événement est terminé .Post que vous avez juste un objet de détail pas une exception, bien que l'objet soit de classe exception/throwable.
Le renvoi de l'objet exception peut être utile en raison des détails qu'il détient, mais cela ajoutera une ambiguïté/confusion, car la méthode d'appel n'est pas "la gestion de l'exception et le contrôle du flux basé sur l'exception". Il s'agit simplement d'utiliser les détails d'un objet retourné pour prendre des décisions.
Donc, à mon avis, un moyen plus logique et moins déroutant sera de renvoyer un boolean/enum basé sur l'exception plutôt que de simplement renvoyer un objet exception.
C'est un anti-motif:
catch(Exception e) {
return e;
}
La seule excuse raisonnable pour renvoyer Throwable
est de fournir des informations détaillées sur les défaillances vers une voie rapide où une défaillance est attendue, sans en payer le prix.1 de la gestion des exceptions. En prime, il est facile de passer à une exception levée si l'appelant ne sait pas comment gérer cette situation particulière.
Mais dans votre scénario, ce coût a déjà été payé. Catching l'exception au nom de votre appelant ne fait aucune faveur.
1Pédantiquement, le coût direct de la capture d'une exception (trouver un gestionnaire correspondant, décompresser la pile) est assez faible ou doit être effectué de toute façon. La plupart du coût de la variable try
/catch
par rapport à une valeur renvoyée correspond à des actions accessoires, telles que la création d'une trace de pile. Par conséquent, le fait de savoir si les objets exceptionnels non exécutés constituent un bon stockage pour des données d'erreur efficaces dépend du fait que ce travail accessoire est effectué au moment de la construction ou au moment du lancement. Par conséquent, si le retour d'un objet d'exception est raisonnable peut différer d'une plate-forme gérée à une autre.
Vous ne pouvez pas dire que 2) l'opinion est fausse, car le workflow s'exécutera comme vous le souhaitez. D'un point de vue logique, cela fonctionnera comme souhaité, donc c'est correct.
Mais c'est une façon très étrange et non recommandée de le faire. D'abord parce que Exception n'est pas conçu pour cela (ce qui signifie que vous faites un anti-motif). C'est un objet spécifique conçu pour être lancé et attrapé. Il est donc étrange de, plutôt de l’utiliser comme prévu, de choisir de le retourner et de l’attraper avec un if. De plus, vous ferez (peut-être que ce sera négligeable) un problème de performances plutôt que d'utiliser un simple booléen comme indicateur, vous instancierez un objet entier.
Enfin, la fonction cause ne devrait pas non plus être retournée si le but de la fonction est d'obtenir quelque chose (ce qui n'est pas votre cas). Vous devriez le reconfigurer pour qu'il soit une fonction qui démarre un service, puis il ne retournera rien car il ne sera pas appelé pour obtenir quelque chose, et une exception se produira si une erreur survient. Et si vous voulez savoir si votre travail de service crée une fonction conçue pour vous donner cette information comme public boolean isServiceStarted()
.