From Naked Objects to Naked Functions | DevsDay.ru

IT-блоги From Naked Objects to Naked Functions

DZone Web Dev 26 ноября 2021 г. Richard Pawson


Functional programming (FP) is, today, roughly where object-oriented programming (OOP) was in the late 1990s. Pure FP languages are gaining popularity, and mainstream languages increasingly support FP idioms. There are some application domains where FP has already become the dominant paradigm – scientific computing, big data, some fin-tech – but there are also substantial application domains where FP has made little impact, one such being transactional enterprise applications built on relational databases. Granted, this is no longer considered to be the ‘hot’ end of systems development, but it still accounts for a huge proportion of commercial programming. Developers working on such systems today might use functional idioms where they can, but it is rare to see one built using FP as the core design ethic.

This situation might be attributed to traditional conservatism in that sector, but I believe there is a bigger issue, which derives from the central conundrum of FP, elegantly articulated by Simon Peyton Jones (lead on the Glasgow Haskell Compiler):

‘Functional programming means building systems out of side-effect free functions [but] the whole point of a system is to produce side-effects.’ 

The resolution to this conundrum is that an FP system comprises pure side-effect free functions, which do the ‘computation’, wrapped in what we might call ‘impure’ functions (functional programmers tend to call them ‘actions’), which perform the Input/Output. Impure functions may call pure functions, but not vice versa – else the latter themselves become impure. Haskell, with its type-safety and ‘IO monad’ can enforce this rule; if you develop in the current versions of C# or Python, say, you must enforce the separation manually.

The problem with transactional enterprise systems is that the ratio of I/O to pure computation is much higher than on those applications where FP has already become established. It is too easy in this situation to end up with nearly 100% impure functions, rendering the practical advantages offered by FP – provability, testability, and scalability through parallelism – hard to realise.

This gives me a strong sense of déjà vu. In the late 1990s, I was doing research into why OOP was making little impact on enterprise systems, despite all its claimed advantages. The term ‘domain driven design’ had yet to be coined but the idea of building systems around a pure OOP model – where all behaviour was encapsulated as methods on domain entities – was already being preached. The problem, I concluded, was that even if you started from a pure OOP domain model, by the time you had added all the layers of code to turn this model into a usable application, the promised benefits – faster development, easier maintenance – were quickly lost.

The solution I proposed was an architectural pattern that I named ‘naked objects’[1], wherein the user interface is delivered 100% automatically by reflecting over the domain model – just as advanced ORMs were beginning to allow the persistence layer to be created automatically. This led in 2002 to the release of the open-source Naked Objects framework, which has been in continuous development ever since (we released version 12 just a few weeks ago).

Three years ago, I started to wonder: could the same idea be applied to FP? Today, I am pleased to announce the release of Naked Functions 1.0.

Writing an Application in Naked Functions

Naked Functions runs on .NET 6.0 and you can write your domain code in either C# or F# – I’ll use the former in the following code examples. The persistence layer is managed via Entity Framework Core, either relying on code conventions or explicit mapping. Naked Functions reflects over your domain code to generate a complete RESTful API – not just to the data but to all the functions too – and this RESTful API may be consumed via a Single Page Application (SPA) client. We provide a generic implementation of such a client, written in Angular. (Incidentally, to reassure any who might be concerned that reflection is an expensive operation: all the reflection takes place during start-up, while building a meta-model of the domain code. Execution of operations at run-time does not typically involve any reflection.)

But where in Naked Objects you write only behaviourally complete domain objects, with Naked Functions you define only immutable domain types and pure side-effect free domain functions. You do not typically need to write any I/O at all, because the Naked Functions framework handles I/O with the client and the database transparently. Critically, your domain functions never make calls into the Naked Functions framework – it is entirely the other way around. This is best understood through examples.

First, we’ll look at a very simple domain type:

C#
 
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;
}

Here’s how an instance of this Department type appears on the generic UI:

Note the following:

  • Domain types are implemented as immutable classes, so properties have {get; init;} accessors to ensure that they are modified only during construction. (Naked Functions will also work with record types, but note the caveats at the end of this article [2]).
  • It is convenient (but not a requirement) to provide each domain type with a  constructor that can copy each property from a supplied cloneFrom instance - we’ll look at the use of that shortly.
  • Any public properties, including collections, are automatically rendered as fields on the screen, unless marked [Hidden].
  • Other attributes such as [MemberOrder] provide hints for the server framework and/or client to interpret.
  • The only method defined on a type is the optional override of the default ToString which defines the presentation title for the instance (as shown in the screenshot).

The functionality of the application appears as actions on the user interface, either ‘main menu’ actions, for example:

Or on the menu of Actions associated with an instance of a specific type, for example:

Both forms of action are generated reflectively from public static functions in the domain code, and in both cases the corresponding dialog box (if applicable) is generated reflectively from the function’s parameters. Looking at the implementation of the main menu function …

C#
 
public static IQueryable<Product> FindProductByName(
  string match, IContext context) =>
     context.Instances<Product>().Where(x =>
       x.Name.ToUpper().Contains(match.ToUpper())).OrderBy(x => x.Name);

we can see that this function also defines a parameter of type IContext– which is not rendered in the dialog. When the user invokes the action via the dialog box, the function will be called, and the framework will automatically supply an implementation of IContext. Amongst other things this provides access to an IQueryable<T>for any persisted domain type via the Instances method, which eliminates any need to reference the DbContextdirectly. Because this function’s return type is an IEnumerablethe results will be rendered as a list of references (with the automatic option to switch to a table view); in this case the result type is specifically IQueryable<T>which the UI will automatically render as a paged list:

As you would expect, clicking on any row takes you to a view of that instance, or right-clicking will open it in a pane alongside as shown here:

Any associations between instances (single or collection) may also be navigated by clicking.

This example of a main-menu function makes no changes to the database – which means, incidentally, that on the auto-generated RESTful API, the resource corresponding to the function will automatically specify the Http GET method.

Our other example action is ‘contributed’ to an Employee instance, because the function follows ‘extension method’ syntax – the first parameter defining the type that it is to be contributed to at the UI:

C#
 
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);
}

The other significant difference to our earlier ‘query’ action, is that this function needs to make changes in the database. Note the following in the implementation:

  • As well as taking an IContext as a parameter, it returns an IContext that contains a list of any updated and/or newly-created instances to be saved .
  • The Employee passed in as the first parameter (being the instance from which the user has invoked the action) is never mutated. Instead, a new instance is created, for example by using the constructor that takes in Employee cloneFrom with specified differences in the braces following. (The benefit of this pattern is more obvious in domain types with more properties).
  • The IContextis also immutable. Calling WithUpdated returns a copy of the input context, with the updated instance (and corresponding original instance – to save hitting the database twice) added to the list. New instances to be inserted into the database are similarly registered via the WithNewmethod.
  • The function itself makes no changes to the database (nor to the screen). After the function exits, the framework (which called the function in response to an incoming request from the client) renders the modified version of the original object on screen and determines from the returned IContext what needs to be saved to the database – all within the scope of a transaction, initiated automatically by the framework.
  • If the function needed to make some persistent changes and display a different instance on the screen, it can return a tuple e.g. (EmployeeHistory, IContext).

If the function is deemed only to be updating properties then it may be marked up with an [Edit] attribute:

C#
 
[Edit]
public static IContext UpdateNationalIDNumber(
   this Employee e, [MaxLength(15)] string nationalIdNumber, IContext context) =>
      context.WithUpdated(e, new(e)
         {NationalIDNumber = nationalIdNumber, ModifiedDate = context.Now() });

The result is that instead of appearing in the Actions menu on an instance, a pencil icon will be rendered next to the property on the UI, and when clicked the property will give the appearance of being editable:

Notice that:

  • In the code the instance is not being mutated – a copy is made and returned attached to an IContext instance as before.
  • The context provides a Now() method to access the current date time. (In the previous example a similar Today() method is called. This is to avoid having to call DateTime.Now or .Today, which would violate FP principles (because the function would then depend upon the something, not passed in as a parameter).

In a similar vein the IContext offers several other methods that provide FP-pure solutions to small but tricky challenges (all documented in the  Naked Functions – Developer Manual):

  • How can you get hold of the current user (IPrincipal), or create a new Guid, needed in some database schemas, without creating a dependency on System?
  • How can a function use random numbers without a dependency on System, and without creating side effects? Just calling Random.Next()would generate side-effects because it mutates the state of Random. (Random numbers are not a common requirement in enterprise systems, but it’s one that we’ve encountered – for example in quality control processes).

The IContext also provides a mechanism for the less common cases where the system needs to create side-effects other than writing to the screen or modifying the database. Here’s one example – sending a confirmation by email:

C#
 
public static IContext SendConfirmation(
  this Person p, string message, IContext context) =>
      context.WithDeferred(
         c => c.GetService<IEmailSender>().SendMessage(p.EmailAddr, message));

Note:

  • The SendConfirmation function does not send an email. Instead, it defines a function – in this case a lambda so that it can capture locally-scoped details – and registers it as a ‘deferred function’ on the returned IContext. Naked Functions automatically invokes this deferred function, but only after the SendConfirmation function has exited, thus keeping the latter side-effect free.
  • The IContext.GetService method used above, can access any service registered in the system configuration.

The IContext interface is defined in the lightweight NakedFunctions.ProgrammingModel NuGet package, together with a few attributes that may be used to provide hints to the server framework, or be passed on for interpretation by the client. This is the only package that an application project needs to depend upon, and it contains no executable code. The framework executables are installed into a separate server project, which references your domain model project(s), not vice versa. The server project depends upon the NakedFunctions.Server package and allows system configuration using standard .NET patterns.

Like Naked Objects, Naked Functions also supports ‘complementary functions’, which enrich the behaviour of an action. These are recognised by naming conventions and coupled to the corresponding action at run time by the framework, either within the server, or at the client as appropriate. For example:

C#
 
public static IContext UpdateDateOfBirth(
   this Employee e, DateTime? dateOfBirth, IContext context) => ...

public static string? ValidateUpdateDateOfBirth(
  this Employee e, DateTime? dateOfBirth, IContext context) =>
    dateOfBirth > context.Today().AddYears(-16) ||
       dateOfBirth < context.Today().AddYears(-100) ? "Invalid Date Of Birth" : null;

ensures that the action UpdateDateOfBirth can only be invoked with a date that falls within a defined range, and

C#
 
public static IContext RemoveDetail(
  this SalesOrderHeader soh, SalesOrderDetail detailToRemove, IContext context) => ...

public static IEnumerable<SalesOrderDetail> Choices1RemoveDetail(
  this SalesOrderHeader soh) => soh.Details;

offers the user the list of existing SalesOrderDetail objects as a drop-down list for parameter number 1 – detailToRemove. Other ‘complementary function’ conventions include: AutoComplete, Disable, Hide, and Default.

I’ve referred to several similarities between Naked Objects and Naked Functions. In fact, 91% of the server code is common – now factored out into common assemblies using the name Naked Framework. The generic SPA client is completely common to both. The generated RESTful API adopts the same Restful Objects specification. Only the programming model is different: pure OOP in one case; pure FP in the other. Apart from reducing our own maintenance overhead as framework authors, another huge advantage of the commonality is that the core reflection (which builds a comprehensive meta-model at start-up) and core run-time execution is all proven code, developed and refined over 20 years, and deployed at very large scale.

The generic client may be customised using standard Angular coding patterns. The generic client comprises 5 NPM packages representing 5 separate layers, so you can re-use as few or as many layers as you wish. Nonetheless, we caution newcomers to Naked Objects, and now to Naked Functions, not to focus initially on how to customise the client. There are huge advantages to staying with the generic client as long as possible: it helps both application developers and business users to focus on the functional requirements of the application, rather than getting bogged down early on with the UI detail; it keeps the UI completely consistent across all aspects of the application; and it dramatically improves the business agility of the system. Many of our users have started with the intention of writing a custom client, but in the end have opted to stick with the generic client, just with some generic restyling via CSS. If you do need a fully bespoke client, then the fully generic RESTful API means that the client may be developed independently from the domain model.

If you’d like to try using Naked Functions for yourself, start by downloading the Naked Functions – Developer Manual which will explain how to start from the Template project. There is no need to build the source code as the server and client templates work from the published packages (NuGet and NPM respectively).

References

[1] R Pawson, Naked Objects (Ph.D. thesis), June 2004, .pdf version

[2] When running under  .NET 6,  Microsoft's Entity Framework Core has a serious bug (see https://github.com/dotnet/efcore/issues/26602 ) that manifests if you try to use records with inheritance, and are also using EF Core's lazy loading (via proxies) capabilities, which Naked Functions requires. Until Microsoft fixes that bug, if you need inheritance between domain types we advise that you make those types immutable classes rather than records. Also, see notes on the use of records in the  Naked Functions – Developer Manual.

Источник: DZone Web Dev

Наш сайт является информационным посредником. Сообщить о нарушении авторских прав.

.net tutorial c# functional programming user interface enterprise architecture entity framework core