La programmation fonctionnelle (FP) est, aujourd’hui, à peu près là où se trouvait la programmation orientée objet (POO) à la fin des années 1990. Les langages FP purs gagnent en popularité et les langages courants prennent de plus en plus en charge les idiomes FP. Il existe certains domaines d’application où FP est déjà devenu le paradigme dominant – calcul scientifique, mégadonnées, certaines technologies financières – mais il existe également des domaines d’application substantiels où FP a eu peu d’impact, l’un d’entre eux étant les applications d’entreprise transactionnelles construites sur des bases de données relationnelles. Certes, ce n’est plus considéré comme la fin « chaude » du développement de systèmes, mais cela représente toujours une proportion énorme de la programmation commerciale. Les développeurs travaillant sur de tels systèmes aujourd’hui peuvent utiliser des idiomes fonctionnels là où ils le peuvent, mais il est rare d’en voir un construit en utilisant FP comme éthique de conception de base.
Cette situation peut être attribuée au conservatisme traditionnel dans ce secteur, mais je pense qu’il y a un problème plus important, qui découle de l’énigme centrale de FP, élégamment articulée par Simon Peyton Jones (responsable du Glasgow Haskell Compiler) :
« La programmation fonctionnelle signifie la construction de systèmes à partir de fonctions sans effets secondaires [but] tout l’intérêt d’un système est de produire des effets secondaires.
La résolution de cette énigme est qu’un système FP comprend de pures fonctions sans effet secondaire, qui effectuent le « calcul », enveloppées dans ce que nous pourrions appeler des fonctions « impures » (les programmeurs fonctionnels ont tendance à les appeler « actions »), qui effectuent le Entrée sortie. Les fonctions impures peuvent appeler des fonctions pures, mais pas vice versa – sinon ces derniers deviennent eux-mêmes impurs. Haskell, avec sa sécurité de type et sa « monade IO » peut faire respecter cette règle ; si vous développez dans les versions actuelles de C# ou Python, par exemple, vous devez appliquer la séparation manuellement.
Le problème avec les systèmes d’entreprise transactionnels est que le rapport entre les E/S et le calcul pur est beaucoup plus élevé que sur les applications où FP est déjà établi. Il est trop facile dans cette situation de se retrouver avec près de 100 % de fonctions impures, ce qui rend les avantages pratiques offerts par FP – provabilité, testabilité et évolutivité grâce au parallélisme – difficiles à réaliser.
Cela me donne un fort sentiment de déjà vu. À la fin des années 90, je faisais des recherches pour savoir pourquoi la POO avait peu d’impact sur les systèmes d’entreprise, malgré tous ses avantages revendiqués. Le terme « conception axée sur le domaine » n’avait pas encore été inventé, mais l’idée de construire des systèmes autour d’un modèle POO pur – où tous les comportements étaient encapsulés sous forme de méthodes sur des entités de domaine – était déjà prêchée. Le problème, j’en ai conclu, était que même si vous partiez d’un modèle de domaine purement POO, au moment où vous aviez ajouté toutes les couches de code pour transformer ce modèle en une application utilisable, les avantages promis – développement plus rapide, maintenance plus facile – étaient vite perdu.
La solution que j’ai proposée était un modèle architectural que j’ai nommé « objets nus »[1], dans lequel l’interface utilisateur est livrée 100% automatiquement en réfléchissant sur le modèle de domaine – tout comme les ORM avancés commençaient à permettre la création automatique de la couche de persistance. Cela a conduit en 2002 à la sortie du framework open source Naked Objects, qui est en développement continu depuis (nous avons publié la version 12 il y a quelques semaines à peine).
Il y a trois ans, j’ai commencé à me demander : la même idée pourrait-elle s’appliquer à la PF ? Aujourd’hui, j’ai le plaisir d’annoncer la sortie de Naked Functions 1.0.
Écrire une application dans des fonctions nues
Naked Functions s’exécute sur .NET 6.0 et vous pouvez écrire votre code de domaine en C# ou F# – j’utiliserai le premier dans les exemples de code suivants. La couche de persistance est gérée via Entity Framework Core, en s’appuyant soit sur des conventions de code, soit sur un mappage explicite. Naked Functions réfléchit sur votre code de domaine pour générer un Achevée API RESTful – non seulement pour les données mais aussi pour toutes les fonctions – et cette API RESTful peut être consommée via un client d’application à page unique (SPA). Nous fournissons une implémentation générique d’un tel client, écrite en Angular. (Par ailleurs, pour rassurer ceux qui pourraient être concernés que la réflexion est une opération coûteuse : toute la réflexion a lieu lors du démarrage, tout en construisant un méta-modèle du code du domaine. L’exécution des opérations au moment de l’exécution n’implique généralement aucun réflexion.)
Mais là où dans Naked Objects vous écrivez uniquement des objets de domaine complets sur le plan comportemental, avec Naked Functions, vous ne définissez que des types de domaine immuables et des fonctions de domaine sans effets secondaires purs. Vous n’avez généralement pas besoin d’écrire tout E/S du tout, car le framework Naked Functions gère les E/S avec le client et la base de données de manière transparente. Surtout, vos fonctions de domaine ne font jamais d’appels dans le framework Naked Functions – c’est tout à fait l’inverse. Ceci est mieux compris à travers des exemples.
Tout d’abord, nous allons examiner un type de domaine très simple :
public class Department
{
public Department() {}
public Department(Department cloneFrom)
DepartmentID = cloneFrom.DepartmentID;
Name = cloneFrom.Name;
ModifiedDate = cloneFrom. ModifiedDate;
}
[Hidden]
public short DepartmentID { get; init; }
[MemberOrder(1)]
public string Name { get; init; } = "";
[MemberOrder(2)]
public string GroupName { get; init; } = "";
[MemberOrder(99), Versioned]
public DateTime ModifiedDate { get; init; }
public override string ToString() => Name;
}
Voici comment une instance de ce type de service apparaît sur l’interface utilisateur générique :
Notez ce qui suit :
- Les types de domaine sont implémentés en tant que classes immuables, donc les propriétés ont
{get; init;}
accesseurs pour s’assurer qu’ils ne sont modifiés que pendant la construction. (Naked Functions fonctionnera également avec les types d’enregistrement, mais notez les mises en garde à la fin de cet article [2]). - Il est pratique (mais pas obligatoire) de fournir à chaque type de domaine un constructeur qui peut copier chaque propriété à partir d’un
cloneFrom
exemple – nous verrons l’utilisation de cela sous peu. - Toutes les propriétés publiques, y compris les collections, sont automatiquement affichées sous forme de champs à l’écran, sauf si elles sont marquées
[Hidden]
. - D’autres attributs tels que
[MemberOrder]
fournir des conseils pour le cadre du serveur et/ou le client à interpréter. - La seule méthode définie sur un type est le remplacement facultatif de la valeur par défaut
ToString
qui définit le titre de présentation de l’instance (comme indiqué dans la capture d’écran).
La fonctionnalité de l’application apparaît sous forme d’actions sur l’interface utilisateur, soit d’actions « menu principal », par exemple :
Ou au menu de Actions associé à une instance d’un type spécifique, par exemple :
Les deux formes d’action sont générées de manière réflexive à partir de fonctions statiques publiques dans le code de domaine, et dans les deux cas, la boîte de dialogue correspondante (le cas échéant) est générée de manière réflexive à partir des paramètres de la fonction. En regardant la mise en œuvre de la fonction de menu principal…
public static IQueryable<Product> FindProductByName(
string match, IContext context) =>
context.Instances<Product>().Where(x =>
x.Name.ToUpper().Contains(match.ToUpper())).OrderBy(x => x.Name);
on voit que cette fonction définit aussi un paramètre de type IContext
– qui n’est pas rendu dans la boîte de dialogue. Lorsque l’utilisateur invoque l’action via la boîte de dialogue, la fonction sera appelée et le framework fournira automatiquement une implémentation de IContext
. Cela donne entre autres accès à un IQueryable<T>
pour tout type de domaine persistant via le Instances
méthode, qui élimine le besoin de référencer le DbContext
directement. Parce que le type de retour de cette fonction est un IEnumerable
les résultats seront rendus sous forme de liste de références (avec l’option automatique de basculer vers une vue tabulaire) ; dans ce cas, le type de résultat est spécifiquement IQueryable<T>
que l’interface utilisateur affichera automatiquement en tant que paginé liste:
Comme vous vous en doutez, cliquer sur n’importe quelle ligne vous amène à une vue de cette instance, ou un clic droit l’ouvrira dans un volet à côté, comme indiqué ici :
Toutes les associations entre les instances (simple ou collection) peuvent également être parcourues en cliquant.
Cet exemple de fonction de menu principal n’apporte aucune modification à la base de données – ce qui signifie, incidemment, que sur l’API RESTful générée automatiquement, la ressource correspondant à la fonction spécifiera automatiquement le Http GET
méthode.
Notre autre exemple d’action est « contribué » à un Employee
instance, car la fonction suit la syntaxe « méthode d’extension » – le premier paramètre définissant le type auquel elle doit être contribuée au niveau de l’interface utilisateur :
public static IContext ChangeDepartmentOrShift(
this Employee e, Department department, Shift shift, IContext context) {
var currentAssignment = CurrentAssignment(e);
var updatedCA = new(currentAssignment) {
EndDate = context.Now(),
ModifiedDate = context.Now()
};
var newAssignment = new EmployeeDepartmentHistory {
EmployeeID = e.BusinessEntityID,
DepartmentID = department.DepartmentID,
ShiftID = shift.ShiftID,
StartDate = context.Today(),
ModifiedDate = context.Now()
};
return context.WithUpdated(currentAssignment, updatedCA).WithNew(newAssignment);
}
L’autre différence significative par rapport à notre action « requête » précédente est que cette fonction doit apporter des modifications à la base de données. Notez les points suivants dans la mise en œuvre :
- En plus de prendre une
IContext
en paramètre, il renvoie unIContext
qui contient une liste de toutes les instances mises à jour et/ou nouvellement créées à enregistrer . - Les
Employee
transmis comme premier paramètre (étant l’instance à partir de laquelle l’utilisateur a invoqué l’action) n’est jamais muté. Au lieu de cela, une nouvelle instance est créée, par exemple en utilisant le constructeur qui prend en chargeEmployee cloneFrom
avec des différences spécifiées dans les accolades suivantes. (L’avantage de ce modèle est plus évident dans les types de domaine avec plus de propriétés). - Les
IContext
est aussi immuable. AppelWithUpdated
renvoie un copie du contexte d’entrée, avec l’instance mise à jour (et l’instance d’origine correspondante – pour éviter de frapper la base de données deux fois) ajoutée à la liste. Les nouvelles instances à insérer dans la base de données sont également enregistrées via leWithNew
méthode. - La fonction elle-même fait aucun changement dans la base de données (ni à l’écran). Une fois la fonction terminée, le framework (qui a appelé la fonction en réponse à une demande entrante du client) affiche la version modifiée de l’objet d’origine à l’écran et détermine à partir de la
IContext
ce qui doit être enregistré dans la base de données – le tout dans le cadre d’une transaction, initiée automatiquement par le framework. - Si la fonction avait besoin d’apporter des modifications persistantes et d’afficher une instance différente à l’écran, elle peut renvoyer un tuple, par exemple (
EmployeeHistory
,IContext
).
Si la fonction est considérée uniquement comme la mise à jour des propriétés, elle peut être balisée avec un [Edit]
attribut:
[Edit]
public static IContext UpdateNationalIDNumber(
this Employee e, [MaxLength(15)] string nationalIdNumber, IContext context) =>
context.WithUpdated(e, new(e)
{NationalIDNumber = nationalIdNumber, ModifiedDate = context.Now() });
Le résultat est qu’au lieu d’apparaître dans le Actions menu sur une instance, une icône de crayon sera affichée à côté de la propriété sur l’interface utilisateur, et lorsqu’elle est cliquée, la propriété donnera l’impression d’être…