IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Vous êtes nouveau sur Developpez.com ? Créez votre compte ou connectez-vous afin de pouvoir participer !

Vous devez avoir un compte Developpez.com et être connecté pour pouvoir participer aux discussions.

Vous n'avez pas encore de compte Developpez.com ? Créez-en un en quelques instants, c'est entièrement gratuit !

Si vous disposez déjà d'un compte et qu'il est bien activé, connectez-vous à l'aide du formulaire ci-dessous.

Identifiez-vous
Identifiant
Mot de passe
Mot de passe oublié ?
Créer un compte

L'inscription est gratuite et ne vous prendra que quelques instants !

Je m'inscris !

Les préconditions en C++ - Partie 2,
Un tutoriel de Andrzej Krzemienski, traduit par kurtcpp

Le , par Francis Walter

0PARTAGES

11  0 
Bonjour,

Je vous présente un tutoriel sur les préconditions en C++ de Andrzej Krzemieński : https://akrzemi1.developpez.com/tuto...remiere-partie
Il s'agit de la première partie d'une série de quatre épisodes que nous allons faire l'effort de traduire et publier dans les jours à venir. Cette première partie a été traduite par kurtcpp que l'équipe de la rédaction tient à remercier sans oublier les autres contributeurs qui ont aidé à la correction.

N'hésitez pas à commenter le contenu de ce premier article en répondant à cette discussion.

P.-S. Si vous désirez contribuer aux traductions de ressources C++, vous pouvez me contacter par MP.

Les meilleurs cours et tutoriels pour apprendre la programmation C++
Vous avez lu gratuitement 7 799 articles depuis plus d'un an.
Soutenez le club developpez.com en souscrivant un abonnement pour que nous puissions continuer à vous proposer des publications.

Une erreur dans cette actualité ? Signalez-nous-la !

Avatar de RPGamer
Membre averti https://www.developpez.com
Le 21/12/2015 à 10:56
Excellente idée que cette traduction. Inspiré par un autre article sur le sujet, j'avais rédigé mes propres macros pour la validation de prédicats par le compilateur dans un simple fichier .hpp et c'est plutot efficace.

contract.hpp

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
#ifndef __CONTRACT_HPP
#define __CONTRACT_HPP

#include <Uncopyable.hpp>
#include <type_traits>
#include <cassert>

/*!
 * \macro   require
 * \brief   Vérifie la validité d'une précondition.
 */
#if defined(CONTRACT_NO_PRECONDITION) || defined(CONTRACT_NO_CHECK)
    #define require(contract, text)
#else
    #define require(contract, text) assert(contract && text)
#endif

/*!
 * \macro   ensure
 * \brief   Vérifie la validité d'une postcondition.
 */
#if defined(CONTRACT_NO_POSTCONDITION) || defined(CONTRACT_NO_CHECK)
    #define ensure(contract, text)
#else
    #define ensure(contract, text) assert(contract && text)
#endif

/*!
 * \macro   invariant
 * \brief   Vérifie la validité d'un invariant.
 */
#if defined(CONTRACT_NO_INVARIANT) || defined(CONTRACT_NO_CHECK)
    #define invariant(contract, text)
#else
    #define invariant(contract, text) assert(contract && text)
#endif

/*!
 * \macro   invariants
 * \brief   Débute un bloc d'invariants de classe.
 */
#define invariants(classname) friend class InvariantsChecker<classname>; void _contract_check_invariants() const

/*!
 * \macro   check_invariants
 * \brief   Vérifie la validité des invariants de classe.
 */
#if defined(CONTRACT_NO_INVARIANT) || defined(CONTRACT_NO_CHECK)
    #define check_invariants()
#else
    #define check_invariants() _contract_check_invariants()
#endif

/*!
 * \macro   static_invariants
 * \brief   Débute un bloc d'invariants statiques de classe.
 */
#define static_invariants(classname) static void _contract_check_static_invariants() const

/*!
 * \macro   check_static_invariants
 * \brief   Vérifie la validité des invariants statiques de classe.
 */
#if defined(CONTRACT_NO_INVARIANT) || defined(CONTRACT_NO_CHECK)
    #define check_static_invariants()
#else
    #define check_static_invariants() _contract_check_static_invariants()
#endif

/*!
 * \class   InvariantsChecker
 * \brief   Vérifie la validité des invariants en début et en fin de scope.
 */
template <typename T>
class InvariantsChecker : private Uncopyable
{
    private:

        T *instance;

    public:

        InvariantsChecker(T *instance) :
            instance(instance)
        {
#if !defined(CONTRACT_NO_INVARIANT) && !defined(CONTRACT_NO_CHECK)
            instance->_contract_check_invariants();
#endif
        }

        ~InvariantsChecker()
        {
#if !defined(CONTRACT_NO_INVARIANT) && !defined(CONTRACT_NO_CHECK)
            instance->_contract_check_invariants();
#endif
        }
};

#endif // __CONTRACT_HPP
On peut alors facilement l'utiliser :

Code : Sélectionner tout
1
2
3
4
5
int divide(int dividend, int divisor)
{
    require(divisor != 0, "Division par zéro indéfinie.");
    return dividend / divisor;
}
Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Square
{
    private:

        size_t width;
        size_t height;

        invariants(Square)
        {
            invariant(width == height, "Un carré a 4 côtés égaux.");
        }

    public:

        Square(size_t width, size_t height) : 
            width(width), height(height)
        {
            check_invariants();
        }
        void resize(size_t width, size_t height)
        {
            InvariantsChecker<Square> check(this);
            this->width = width;
            this->height = height;
        }
        size_t getWidth() const
        {
            InvariantsChecker<Square> check(this);
            return width;
        }
        size_t getHeight() const
        {
            InvariantsChecker<Square> check(this);
            return height;
        }
};
6  0 
Avatar de koala01
Expert éminent sénior https://www.developpez.com
Le 24/03/2016 à 14:23
Salut,
Citation Envoyé par RPGamer Voir le message
A mon sens ça n'a pas lieu d'être. L'héritage multiple est utile pour l'implémentation d'interfaces et les interfaces étant dépourvues de données membres, elles sont dépourvues d'invariants.
Ca, c'est une vision très... java de ce que sont les interfaces! Elle est imposée en java pour éviter les problèmes liés à l'héritage en losange (ou en diamant, comme tu veux) pour l'unique raison que toute classe hérite forcément -- de manière directe ou indirecte -- d'une classe Object.

Mais, si tu y porte un tout petit peu d'attention, tu te rendras compte que la notion d'interface en java (et le mot clé implements qui y est rattaché) réagit exactement de la même manière que la notion de classe ou de structure dans un contexte d'héritage, en respectant scrupuleusement le LSP.

Dés lors, si l'on remet les pendules à l'heure pour ce qui concerne la notion d'interface, et que l'on considère que c'est "un ensemble regroupant un certain nombre de comportement destinés à travailler correctement de concert dans un but clairement déterminé" (et tu avoueras que cette définition correspond sérieusement à la notion d'interface ) on se rend compte que c'est exactement la même chose qu'une classe, surtout en C++, vu qu'il ne dispose pas de la possibilité de faire la distinction.

Dés lors, prenons l'exemple une interface IPositionnable. En java, tu devrais avoir quelque chose ressemblant à
Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
public interface IPositionnable{
    public void moveTo(Position newPos);
    public void move(Type diffX, Type diffY /*, Type diffZ*/);
};
public class MaClasse implements IPositionnable{
/* il faut définir les comportements hérités de IPositionnable dans toutes les classes
 * implémentant cette interface
 */
};
Mais, il n'y a rien qui nous empêche en C++ d'avoir quelque chose ressemblant à
Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class IPositionnable{
public:
    void moveTo(Position const & newPos){
        pos_=newPos;
    }
    void move(Type diffX, Type diffY /*, Type diffZ*/){
        pos_ = Position(pos_.x()+diffX, pos_.y()+diffY/*,pos.z()+diffZ*/);
    }
    Type x() const{
        return pos_.x();
    }
    Type y() const{
        return pos_.y();
    }
    /*
    Type z() const{
        return pos_.z()
    }
    */
private:
    Position pos_;
};
class MaClass : public IPositionnable{
/* on n'a même plus besoin de faire quoi que ce soit pour supporter l'interface IPositionnable
 */
};
voire, pourquoi pas, mieux encore :
Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
template <typename CRTP, typename V, typename F>
class IPositionnable{
public:
    using value_t = V;
    using flag_t = F;
    using pos_t = Position<value_t, flag_t>;
    void moveTo(pos_t const & newPos){
        pos_ = newPos;
    }
    template <typename U = F,
               typename = typename std::enable_if<std::is_same<U, Flag2D>::value>::type>
    void move(value_t diffX, value_t diffY){
        pos_=pos_t{pos_.x()+diffX, pos_.y()+diffY};
    }
    template <typename U = F,
               typename = typename std::enable_if<std::is_same<U, Flag3D>::value>::type>
    void move(value_t diffX, value_t diffY, value_t diffZ){
        pos_=pos_t{pos_.x()+diffX, pos_.y()+diffY, pos_.z()+diffZ};
    }

    value_t x() const{
        return pos_.x();
    }
    value_t y() const{
        return pos_.y();
    }
    template <typename U = F,
               typename = typename std::enable_if<std::is_same<U, Flag3D>::value>::type>
    value_t z() const{
        return pos_.z()
    }
private:
    pos_t pos_;
};
class MaClasse : public IPositionnable<MaClasse, double, Flag3D>{
    /* il n'y a plus rien à faire en ce qui concerne l'interface IPositionnable */
};
(la version template de IPositionnable est faite à main levée, il y a peut être quelques corrections à apporter )

la classe IPositionnable correspond bel et bien à la notion d'interface (c'est une classe qui regroupe un ensemble de comportements "destinés à travailler ensemble", et pourtant, elle dispose de "tout ce qu'il faut" pour pouvoir définir les comportements en question.

Mieux encore, la version template de cette classe permet, grâce au CRTP, d'éviter que l'interface serve de "glue artificielle" pour regrouper deux hiérarchies de classes entre elles sous prétexte "qu'elles sont toutes les deux positionnables".

En deux mots comme en cent, la restriction imposée par java en ce qui concerne la notion d'interface est spécifique à java et n'est due qu'au fait que ce langage a effectivement forcé la distinction "physique" entre les notions de classes et d'interfaces, alors que conceptuellement parlant, il n'y a absolument rien que pourrait faire une classe qu'une interface ne pourrait faire (à moins que ce soit le contraire )

Mais, du coup, on se rend donc aussi compte que les invariants destinés à garantir le bon fonctionnement de l'interface se trouvent... au niveau de l'interface, et non de la classe qui l'implémente
3  0 
Avatar de koala01
Expert éminent sénior https://www.developpez.com
Le 24/03/2016 à 19:45
Citation Envoyé par RPGamer Voir le message
A mon sens il n'y a pas de vision "Java" de l'interface. Java a simplement une philo plus assistante. L'interface "impose" l'implémentation d'une interface, c-à-d d'une série de fonctions membres ou méthodes ici (implicitement publics) qui forment tout ou partie du service de la classe implémentante. Par exemple une interface "Positionnable" imposerait à toute classe l'implémentant de définir des fonctions membres permettant à toutes ses instances d'être positionnées (dans un système polaire ou cartésien par exemple).
Non, la seule chose qu'impose la notion d'interface (et que j'ai par malheur oublié de citer dans mon intervention précédente) est effectivement d'être abstraite.

Mais il y a d'autres moyen d'assurer cette restriction que l'utilisation de fonction virtuelles pures à la manière java! : si tu places le destructeur de l'interface (et tant qu'à faire, le constructeur, juste pour respecter le principe du miroir), dans l'accessibilité protégée, tu obtiens bel et bien une classe qui n'est pas instanciable en tant que telle, mais qui l'est au travers des classes qui en dérivent.

Ainsi, si on modifie un tout petit peu les codes que j'ai présentés plus haut pour leur donner la forme de
Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class IPositionnable{
public:
    void moveTo(Position const & newPos){
        pos_=newPos;
    }
    void move(Type diffX, Type diffY /*, Type diffZ*/){
        pos_ = Position(pos_.x()+diffX, pos_.y()+diffY/*,pos.z()+diffZ*/);
    }
    Type x() const{
        return pos_.x();
    }
    Type y() const{
        return pos_.y();
    }
    /*
    Type z() const{
        return pos_.z()
    }
    */
protected:
    /* il n'y a de toutes façons pas de sens à permettre la destruction d'une instance
     * dérivée alors qu'on la connait comme étant du type de l'interface, 
     * un destructeur protégé et non virtuel fait donc pleinement l'affaire
     */
    ~IPositionnable() = default;
   /* et tant qu'à faire, pour respecter le principe du miroir */
   IPositionnable(Position const & p=Position{}):pos_(p){
   }
private:
    Position pos_;
};
ou sous la forme template proche de
Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
template <typename CRTP, typename V, typename F>
class IPositionnable{
public:
    using value_t = V;
    using flag_t = F;
    using pos_t = Position<value_t, flag_t>;
    void moveTo(pos_t const & newPos){
        pos_ = newPos;
    }
    template <typename U = F,
               typename = typename std::enable_if<std::is_same<U, Flag2D>::value>::type>
    void move(value_t diffX, value_t diffY){
        pos_=pos_t{pos_.x()+diffX, pos_.y()+diffY};
    }
    template <typename U = F,
               typename = typename std::enable_if<std::is_same<U, Flag3D>::value>::type>
    void move(value_t diffX, value_t diffY, value_t diffZ){
        pos_=pos_t{pos_.x()+diffX, pos_.y()+diffY, pos_.z()+diffZ};
    }
 
    value_t x() const{
        return pos_.x();
    }
    value_t y() const{
        return pos_.y();
    }
    template <typename U = F,
               typename = typename std::enable_if<std::is_same<U, Flag3D>::value>::type>
    value_t z() const{
        return pos_.z()
    }

protected:
    /* il n'y a de toutes façons pas de sens à permettre la destruction d'une instance
     * dérivée alors qu'on la connait comme étant du type de l'interface, 
     * un destructeur protégé et non virtuel fait donc pleinement l'affaire
     */
    ~IPositionnable() = default;
   /* et tant qu'à faire, pour respecter le principe du miroir */
   IPositionnable(pos_t const & p=pos_t{}):pos_(p){
   }
private:
    pos_t pos_;
};
tu obtiens bel et bien quelque chose qui ne peut être instancié qu'au travers des classes qui en dérivent, et le fait que les comportements exposés soient effectivement définis au niveau de la classe de base n'en font pas moins des classes présentant toutes les notions indispensables à la notion d'interface

Après, que tu ailles plus loin pour pouvoir mettre en place des politiques de positionnement en système polaire ou cartésien ne correspond qu'à un "détail d'implémentation" (qui pourrait, le cas échéant, être lui aussi défini à l'aide d'un flag particulier dans la version template)
A partir de là, une interface avec des données membres, à fortiori des données membres privées qui plus est, n'a a mon avis pas grand sens. En tout cas ça n'est plus interface, c'est un mix entre une interface et une classe, une sorte de classe abstraite?
uniquement parce que la vision que tu as d'une interface est biaisée par les restrictions imposées par un langage (qui, pour notre malheur, est sans doute le langage qui a réellement donné naissance à la notion d'interface).

Mais à partir du moment où tu considère la notion d'interface comme une notion de conception pure, les règles imposées par un langage particulier n'ont forcément plus cours, vu qu'une notion conceptuelle est forcément indépendante de tout langage.

Et quand tu t'affranchis des règles imposées par java, tu te rend compte que la seule qualité imposée par une interface est de ne pouvoir être instanciée qu'au travers d'une classe qui implémente l'interface en question.

Bien sur, les fonctions virtuelles pures sont l'une des solutions dont on dispose pour y arriver, mais je viens de démontrer qu'il en existe d'autres et, à partir de ce moment là, on n'en a plus rien à foutre si l'interface dispose des détails d'implémentations permettant de définir précisément les comportements auxquels nous sommes en droit de nous attendre de la part de l'interface.
2  0 
Avatar de Luc Hermitte
Expert éminent sénior https://www.developpez.com
Le 23/03/2016 à 15:46
Cela s'appelle le pattern NVI, pour Non-Virtual-Interface.
Par contre, cela se corse en cas d'héritage de multiple "interface/contrats" pour traiter les invariants.

-> http://luchermitte.github.io/blog/20...e-c-plus-plus/
1  0 
Avatar de RPGamer
Membre averti https://www.developpez.com
Le 23/03/2016 à 16:13
Citation Envoyé par Luc Hermitte Voir le message
Par contre, cela se corse en cas d'héritage de multiple "interface/contrats" pour traiter les invariants.
Avec l'approche que je donne, chaque implémentation (doFunc()) possède une fonction d'appel public (func()) vérifiant les invariants. Il n'y a donc aucune confusion sur le contrat. En revanche, un développeur imprudent pourrait alléger le contrat en redéfinissant la fonction d'appel public de façon erronée mais ça entre aussi dans le cadre du changement de portée permis par l'héritage C++ par exemple. En principe on ne le fait pas, à moins d'avoir une très bonne raison.
1  0 
Avatar de Luc Hermitte
Expert éminent sénior https://www.developpez.com
Le 23/03/2016 à 17:00
Ta vérification d'invariant n'est pas factorisée : à chaque fonction, tu dois vérifier les invariants courants et enfants, plus les préconditions, et à la sortie: les invariants (courant et enfants) plus les post-conditions. Ca c'est sur un seul contrat.

Arrive le second contrat, l'invariant porte maintenant sur les deux contrats et sur la classe concrète. Cela veut dire que les fonctions du contrat 1 doivent vérifier les contrats venant du contrat 2 et de la classe concrète qu'il ne connait pas.

Ce n'est pas si simple quand on doit tout faire à la main. Ce qui me fait penser que j'avais été déçu par P0287R0, il ne respecte pas le LSP, voire cela fait tout le contraire. J'espère qu'ils corrigeront le tir.

Code : Sélectionner tout
1
2
3
4
5
6
7
// OK, we can not relax preconditions as we have already seen
double g(double);
double (*pg)(double x) [[expects: x != 0]] = &g; // ERROR.

 // but we can strengthen them ...
double f(double x) [[expects: x >= 0]];
double (*pf)(double) = &f; // OK.
1  0 
Avatar de Luc Hermitte
Expert éminent sénior https://www.developpez.com
Le 24/03/2016 à 11:23
Tout l'intérêt des interfaces et d'isoler les responsabilités pour les avoir les plus atomiques possibles -> c'est l'ISP.
Les objets concret peuvent alors implémenter plusieurs interfaces en toute quiétude dans la mesure où elles seront orthogonales. C'est le cas d'héritage multiple qui marche sans trop de soucis. C'est bien bien pour ça qu'il est celui gardé en Java.
Je ne dis pas que chaque utilisation d'héritage multiple est pertinente, etc. Juste que parmi les pertinentes, on a justement des interfaces orthogonales, et que là où ça devient intéressant, c'est quand ces interfaces sont en fait des contrats.

Pour le lien, je parlai du billet de blog, exemple avec l'héritage multiple de contrats.
1  0 
Avatar de Luc Hermitte
Expert éminent sénior https://www.developpez.com
Le 24/03/2016 à 14:48
NB: j'ai répondu trop vite tout à l'heure.
Effectivement, une interface pure à la Java n'a pas exactement d'état. Est-ce pour autant qu'elle ne peut pas avoir d'invariant ? Je n'en suis pas sûr. On peut toujours exprimer l'invariant avec les informations publiques mises à disposition par la classe. p.ex:
Code : Sélectionner tout
1
2
3
4
[[inv: capacity() <= size()]] struct IBufferedContainer;
[[inv: is_sorted(begin(), end())]] struct ISortedContainer;

struct sorted vector : IBufferedContainer, ISortedContainer;
1  0 
Avatar de koala01
Expert éminent sénior https://www.developpez.com
Le 25/03/2016 à 17:58
Citation Envoyé par RPGamer Voir le message
C'est dommage qu'on s'éloigne du sujet de base mais je fais pleinement la différence entre une classe abstraite et une interface (qui est aussi abstraite par la force des choses). A mon avis ls interfaces ne sont pas plus Javaesques que la PpC est Eiffeilesque (petit retour au sujet )

En ajoutant une implémentation dans ton interface, tu prives l'utilisateur de définir sa propre implémentation car la force de l'interface est justement d'imposer l'interface et non l'implémentation
D'abord, on se rend compte, pour beaucoup d'interface, il est tout à fait possible de définir des comportements "par défaut" qui s'avèrent "suffisants et corrects" dans la plupart des situations, et, de plus, je ne vois absolument pas ce qui pourrait empêcher l'utilisateur de l'interface de (re)définir un comportement particulier pour les quelques cas où le comportement "par défaut" ne conviendrait pas.

Dans la version non template que je donne, il n'y a en effet absolument rien qui interdise de déclarer les fonctions membres virtuelles, au même titre qu'il n'y a absolument rien qui nous oblige à les déclarer comme virtuelles pures

Et, dans la version template, il n'y a absolument rien qui nous interdise de prévoir une spécialisation (partielle ou totale) en cas de besoin
On va ainsi être capable de définir définir ce qu'est un véhicule volant par exemple, sans pour autant préciser la façon dont il vole, attérri, etc. L'implémentation est l'affaire des classes. On peut alors par exemple facilement les regrouper dans un container, faire décoller tous les véhicules volants au même moment ou je ne sais quoi. Dans cet exemple pour qu'un véhicule puisse être considéré comme tel, il doit forcément pouvoir faire le plein puisqu'il est équipé d'un moteur (peu importe lequel). Il faut donc définir le remplissage du réservoir à carburant et surtout le niveau de carburant restant. On pourrait retrouver un contrat au niveau de la classe Vehicle pour imposer un carburant à base de pétrol par exemple puis durcir le contrat sur les classes en fonction du type précis de carburant nécessaire.
Mais qu'est ce qui nous empêche de définir un comportement par défaut si l'on est capable d'en trouver un qui puisse s'avérer "correct et suffisant" dans la majorité des cas

En dehors de l'approche de la notion d'interface telle que présentée par java, la réponse est simple : absolument rien. Et c'est encore plus vrai dans le sens où C++ nous fournit énormément de possibilités qui n'existent pas en java pour obtenir un résultat équivalent!
1  0 
Avatar de koala01
Expert éminent sénior https://www.developpez.com
Le 28/03/2016 à 20:04
Citation Envoyé par RPGamer Voir le message
C'est ce que je fais avec la classe abstraite Vehicle, c'est l'objectif de la classe abstraite (implémenter ce comportement commun qui consiste à faire le plein), qui n'est pas exactement le même objectif que les interfaces, comme on peut le constater dans cet exemple. C'est pour ça que je le trouve particulièrement bon pour mettre en avant cette différence. En outre, les interfaces offrent de l'évolutivité car il est directement admis qu'on ne connait pas tous les modes de propulsion possible. On permet donc tous types d'implémentations et on laisse cet responsabilité aux classes filles.
Encore une fois, il semble que l'on soit plus ou moins d'accord sur le principal, bien qu'il reste quelques points de désaccord sur les détails

Pour toi, un classe fournissant des comportements "par défaut" sera considérée comme une classe abstraite et ne sera pas une interface, alors que, de mon coté, je ne vois absolument pas pourquoi nous ne pourrions désigner ce genre de classe sous le terme d'interface

Mais pour ma part, je fais surtout la distinction entre le fait qu'une classe soit spécifiquement destinée à servir de classe de base (par exemple : une classe Vehicle, dont hériteraient aussi bien des classes Plane, Submarine ou Car), qui ont de fortes chances d'être abstraites "par nature", et toutes les classes qui ne font qu'exposer des comportements susceptibles d'être ajoutés à certaines classes (plus ou moins) concrètes.

Pour moi, tout ce qui entre dans cette deuxième catégorie mérite d'être désigné sous le terme générique d'interface, que les services exposés nous proposent une implémentation par défaut ou non.

Et, attention! je ne dis pas qu'il est systématiquement possible de fournir un comportement par défaut pour les services exposés par une interface; loin de là Je dis juste que c'est juste une possibilité qui peut être envisagée si le langage utilisé le permet, et donc que la restriction du "aucun comportement n'est implémenté dans une interface" n'a de raison d'être que... dans le cadre d'un langage qui impose ce genre de restriction (comme java).
1  0