Le try/catch est-il, aujourd'hui, aussi performant que des erreurs gérées avec des ifs ?

icon Tags de l'article : ,

Décembre 11, 2019
Ce matin, un collègue m'a montré comment il avait réduit le code d'une méthode en passant par un try/catch (à la place d'un enchainement de if).

Je lui ai expliqué que c'était plutôt à éviter, pour 3 raisons :
  • Moins performant
  • Pas forcément plus clair pour le développeur qui arrivera derrière ("pourquoi il a fait un try/catch ? il y a un cas particulier à gérer ?")
  • Ne permet pas de s'assurer que tous les cas métiers ont été gérés en un seul coup d'oeil

Dans cette situation, les points 2 et 3 ne s'appliquaient pas vraiment, le code étant plutôt simple. Mais le point 1 s'appliquait toujours.

Mon collègue m'a dit qu'après avoir fait des tests de son côté, il s'était rendu compte que l'écart de performances entre un try/catch et un enchainement de ifs était négligeable.

J'ai donc voulu tester ça. #doute

Pour ça, j'ai run 2/3 tests avec le code suivant :

@Component({
    selector: 'app-tab1',
    templateUrl: 'tab1.page.html',
    styleUrls: ['tab1.page.scss']
})
export class Tab1Page {

    nbItems: Number = 100000;
    public resultIf: Number;
    public resultTryCatch: Number;

    constructor() {
        this.doTest();
    }

    doTest() {
        let items = [];
        for(let i = 0; i < this.nbItems ; i++) {
            items.push(this.generateRandomToto())
        }

        // first we try to update the values of the Totos with if
        let startDate = new Date();
        for(let i = 0; i < this.nbItems ; i++) {
            this.testWithIf(items[i]);
        }
        let endDate = new Date();
        this.resultIf = endDate.getTime() - startDate.getTime();

        // then we try with to update the values of the Totos with try/catch
        startDate = new Date();
        for(let i = 0; i < this.nbItems ; i++) {
            this.testWithTryCatch(items[i]);
        }
        endDate = new Date();
        this.resultTryCatch = endDate.getTime() - startDate.getTime();
    }

    generateRandomToto(): Toto {
        if(Math.round(Math.random()) === 0) { // the math.round of a math.random gives 0 or 1
            const result = new Toto();
            result.titi = new Titi();
            result.titi.tutu = 'bonjour';
            return result;
        } else {
            const result = new Toto();
            return result;
        }
    }

    testWithIf(toto: Toto) {
        if(toto && toto.titi && toto.titi.tutu) {
            toto.titi.tutu = 'wesh';
        }
    }

    testWithTryCatch(toto: Toto) {
        try {
            toto.titi.tutu = 'wesh';
        } catch(error) { }
    }
}

class Toto {
    public titi: Titi;
}

class Titi {
    public tutu: string;
}

Maintenant la question : quel écart en fonction du nombre de try/catchs ?

Voici le résultat pour 100 objets : 0ms avec des ifs, 2ms avec des try/catchs.

Woaw ! On a déjà une différence avec seulement 100 try/catchs !

Et si on augmente ?
1 000 objets -> 1ms avec des ifs, 17ms avec des try/catchs.
10 000 objets -> 1ms avec des ifs, 153ms avec des try/catchs.
100 000 objets -> 2ms avec des ifs, 1427ms avec des try/catchs.

Résultat : non, le try/catch n'est pas aussi performant que les ifs. Loin de là.

Même aujourd'hui, à l'aube de 2020, le try/catch ne doit être utilisé que lorsqu'on a pas le choix :
  • pour attraper globalement une erreur
  • lorsqu'on n'a pas le choix (par exemple si l'objet/la couche concernée throw des erreurs)
  • pour gérer un cas particulier

En dehors de ces situations, le try/catch reste à éviter.

Bon dev à tous et toutes !

Créer un template Razor utilisable en JavaScript

icon Tags de l'article : , , ,

Aout 15, 2018
Hello tout le monde,

Aujourd'hui on va voir comment créer un template Razor utilisable en JavaScript.
C'est une problématique assez courante, et la solution que je propose vient d'une implémentation sur un projet pro... et le résultat est pas si mal.

A noter qu'on est à des années lumières d'un vrai moteur de template, et je ne peux que vous recommander d'en implémenter un si c'est un vrai besoin de votre projet.
Cette solution n'est proposée que pour dépanner, ou à utiliser sur un projet où on ne peut/veut implémenter de moteur de template.


Du coup, allons-y !

Tout d'abord, on va créer une vue partielle pour notre template :

@model Projet.ObjetHtmlTemplate

<div class="objet">
    <h1>@Model.Name</h1>
    <p>@Model.Description</p>
    <span class="price">@Model.Price</span>
</div>

Nous allons maintenant créer notre ViewModel, ici ObjetHtmlTemplate.

Deux choses sont cependant à noter sur ce template :
  • Tous ces champs sont des strings
  • Le constructeur va permettre une initialisation des champs avec une chaine en dur contenant des antiquotes et le nom de la variable.

Voyons la classe avant que j'explique le "pourquoi" :

public class ObjetHtmlTemplate
{
    public string Name { get; set; }
	
	public string Description { get; set; }
	
	public string Price { get; set; }
	
	public ObjetHtmlTemplate(bool initForJs = false)
	{
		if(initForJs) 
		{
			Name = "` + Name + `";
			Description = "` + Description + `";
			Price = "` + Price + `";
		}
	}
}

Voilà. Je pense que vous commencez à voir où je veux en venir : nous allons juste créer une méthode en JS qui prendra en paramètres Name, Description et Price, et cela renverra... le HTML généré préalablement par Razor... mais avec nos valeurs JS !

La prochaine étape est donc ce petit morceau de JavaScript dans notre vue :

<script>
	var getObjetHtml = function(Name, Description, Price) {
		return `@Model.Partial("_ObjetHtmlTemplate", new ObjetHtmlTemplate(true))`;
	};
</script>

Magique non ? Maintenant on a une jolie méthode JavaScript "getObjetHtml" qui prend 3 paramètres, et qui va renvoyer le HTML généré à partir de ces 3 paramètres. <3

Et côté ASP.Net, on peut utiliser notre vue partielle comme à notre habitude :

<ul>
@foreach(var item in Model.Items)
{
	<li>@Html.RenderPartial("_ObjetHtmlTemplate", new ObjetHtmlTemplate() { Name = item.Name, Description = item.Description, Price = Math.Round(item.Price, 2) })<li>
}
</ul>

Et... c'est tout ! Simple et efficace, que demander de plus ?!

Allez, bonne journée et bon dev à tous/toutes !

Bug avec bootstrap-datepicker dans sa version v4.17.45

icon Tags de l'article : , , ,

Aout 10, 2018
Hello,

Juste un micro article pour vous prévenir d'un bug dans la version 4.17.45 du bootstrap-datepicker...

Si vous êtes comme moi et que vous passez par des packages nugets, méfiez-vous : la dernière version en Nuget est la 4.17.45 qui contient des bugs.

(Dont un très chiant : si vous faites une sélection particulière en fonction du format, par exemple une sélection par mois uniquement, au 2eme clic le mode de sélection aura disparu).

Les bugs sont corrigés sur la dernière version en ligne : la 4.17.47.

N'hésitez pas à mettre à jour manuellement votre code, étant donné que le package Nuget est à la masse :(

Bon dev à tous/toutes.

KnockoutJS, JavaScript et JQuery : notes et exemples perso

icon Tags de l'article : , ,

Juin 24, 2016
Hello,

Une petite liste de notes perso, de morceaux de code et de comment faire 2/3 choses, pour moi, mais qui peut vous servir.

Enjoy !

<!-- quelques databind par défaut -->

<input type="text" data-bind="value: searchText" />
<td data-bind="text: 1 === Role() ? 'User' : 'Admin'"></td>
<p data-bind="visible: currentlyWorking">

<!-- A noter : si on est dans un cas de visible "si pas ce booléen", il faut mettre les parenthèses : -->
<div data-bind="visible: !currentlyWorking()">
<p data-bind="visible: !showSearchResults() && firstSearchDone()">

<!-- Plusieurs paramètres -->
<input class="btn" type="button" value="Add" data-bind="attr: { 'data-value': Id }, visible: !model.IsMethod(Id)" /> 

<!-- Appeler le modèle parent de l'objet KO courant : $parent -->
<span data-bind="visible: null !== ItemId && $parent.apiConsumerId() !== ApiConsumerId">

  • Dans l'idéal il vaut mieux éviter d'avoir de la logique côté vue, il vaut mieux faire des computed :
self.showSearchResults = ko.computed(function () {
    return self.firstSearchDone() && self.searchedUsers().length > 0;
});

// Attraper la touche entrée sur un champ texte
$('#txtSearch').on('keyup', function (e) {
    if (e.which !== 13) {
        return;
    }

    $('#btSearchUser').click();
});

// Passer des informations depuis la vue ASP.Net MVC vers le fichier JavaScript 
<script type="text/javascript">
    var urls = {
        'promoteUser': '@Url.Action("Promote", "User")',
    };
    var initialData = @MvcHtmlString.Create(Newtonsoft.Json.JsonConvert.SerializeObject(Model.Users));
    var currentItemId = @Model.Item.Id;
</script>

// Demander confirmation pour une action dans certaines conditions ?
// Créer une méthode JS globale "mustConfirm()" qui sera appelée avant chaque action critique comme ça :
if (confirm('Are you sure that you want to DELETE this user account? This action CANNOT BE UNDONE.') && mustConfirm()) {

}

// Un appel en direct dans le HTML :
<input type="submit" value="Purge" class="btn btn-default" onclick="return mustConfirm(); " />

// Et cette méthode dans la Layout CSHTML :
var mustConfirm = function () {
    @{
        if (UserHelper.GetCurrentEnvironment().AskForConfirmation)
        {
            <text>return confirm('Attention: This is a production environment with sensitive personal and customer data. Are you sure?');</text>
        }
        else
        {
            <text>return true;</text>
        }
    }
};

// Recharger la page courante
location.reload();

// Récupérer une collection d'observables à partir d'un JSON
// Pour ça on utilise la lib JS ko.mapping :
self.Users = ko.mapping.fromJS(initialData);

// Envoyer un fichier en POST à un contrôleur ASP.Net MVC
var data = new FormData();
var files = $('#fileUpload').get(0).files;
if (files.length === 1) {
    data.append("UploadedFile", files[0]);
    data.append("CreatorId", $('#creatorId').val());
    $.ajax({
        type: "POST",
        url: urls.import,
        contentType: false,
        processData: false,
        data: data,
        success: function (result) {
            if (0 === result.ErrorMessages.length) {
                setTimeout(function() { document.location = urls.importsList; }, 1000);
            } else {
                model.currentlyWorking(false);
                model.LoadResult(result, false);
            }
        }
    });
} else {
    alert('Error: you must choose a file');
}

// et côté ASP.Net MVC
public ActionResult Import(int? creatorId, int? userId)
{
    var context = this.CreateContext();

    var model = new ImportVM();

    if (Request.Files.Count != 1)
    {
        model.ErrorMessages.Add("You must upload a file to import profiles");
        return Json(model);
    }

    var uploadedFile = Request.Files[0];

    if (!uploadedFile.FileName.EndsWith(".csv", StringComparison.OrdinalIgnoreCase))
    {
        model.ErrorMessages.Add("Invalid extension: only CSV file can be used for import");
        return Json(model);
    }

    _importBusiness.Import(uploadedFile.InputStream, context);

    return Json(model);
}

// Exemple complet côté JS 
var model;

(function () {
    var Users = function () {
        var self = this;

        self.firstSearchDone = ko.observable(false);
        self.searchText = ko.observable("");
        self.searchedUsers = ko.observableArray();

        self.currentlyWorking = ko.observable(false);

        self.showSearchResults = ko.computed(function () {
            return self.firstSearchDone() && self.searchedUsers().length > 0;
        });

        self.cleanSearch = function () {
            self.searchedUsers.removeAll();
            self.searchText('');
            self.firstSearchDone(false);
        }
    }

    $('#txtSearch').on('keyup', function (e) {
        if (e.which !== 13) {
            return;
        }

        $('#btSearchUser').click();
    });

    $('#btSearchUser').on('click', function () {
        $('#txtSearch').val('');
        model.currentlyWorking(true);

        $.post(urls.searchUserByName, { text: model.searchText() }, function (response) {
            if (!response.ok) {
                alert(response.errorMessage);
                model.cleanSearch();
            } else {
                model.firstSearchDone(true);
                model.searchedUsers(response.searchedUsers);
            }
            model.currentlyWorking(false);
        });
    });

    // we initialize the model
    model = new Users();

    ko.applyBindings(model);
})();