Skip to content

Warehouse Tutorial 3 ‐ Operations

Introduction

In this tutorial, we introduce the core concepts of DMLA related to operations and annotations, including validation, contracts, and dynamically changing the structure of the model. This tutorial builds upon the structure tutorial. The Operation Language Reference briefly summarizes the statements we can use in DMLA's Operation Language.

Step 1: Defining the GetName Operation

Continuing from where we left off, domain entities can now store data in their slots. The next step is to define their behavior. When listing the products, it would be beneficial to display meaningful information about each product.

Behavior can be defined by adding operations to the entities. The definition of operations closely resembles method definitions in most programming languages. The GetName operation can be defined as follows:

entity Product: Classifier {
    //...

    operation String GetName() {
        return "${GetID(this)}";
    }
}

entity Book: Product {
    //...

    override operation String GetName() {
        return "Book [${this.Get(Book.Author)}: ${this.Get(Book.Title)}";
    }
}
The Product entity returns a basic placeholder text by retrieving the built-in unique identifier of the entity using the operation GetID. Note that this ID is different from the ProductID slot discussed in the previous tutorial. In the entity Book, we can enhance the GetName operation (by refining the Product.GetName operation) to display more meaningful information, such as the author's name and the book's title. To retrieve the value of a slot, we use the Get operation, specifying which slot's value we wish to access.

Step 2: Displaying Warehouse Stock

The next step is to support listing of items in the warehouse:

override operation void ListAll() {
    PrintLn("Warehouse: ${GetID(this)}");
    foreach (Product product in this.GetAll(Warehouse.Stock)) {
        PrintLn("Product: ${product.GetName()}");
    }
}
The unique ID of the warehouse is displayed, which helps identify the warehouse being listed. Next, we utilize the GetAll operation to iterate through the collection of products. While the Get and GetAll operations are quite similar, Get is suitable for retrieving a single value from a slot, whereas GetAll returns an array of values. This distinction is crucial in the context of the warehouse stock, which may contain multiple products; thus, in this case, Get would not function as intended (it would return the first item in the stock only).

Step 3: Adding Validation Logic

Operations serve not only to model behavior but also to validate entities. Every entity includes a Validate operation that allows for the definition of custom validation logic. If an entity implements custom validation through Validate, the logic will be invoked along the refinement chain during the validation process.

For instance, consider the Size slot of the Wheel entity. We can impose a requirement that the size of all NCycle wheels must fall within a specific range. This ensures that any wheel associated with an NCycle adheres to predefined size criteria, enhancing the integrity of our model.

SF: a Basics-ben jó lenne írni a validáció elvéről, mit jelent az, hogy valid egy modell

entity Wheel: Product {
    @Type = {TypeName = Number;}
    new slot Size;
}

entity Ncycle: Vehicle {
    //...

    override operation void Validate(Object subject, Base? context = null) {
        foreach (Wheel wheel in subject.GetAll(NCycle.Wheels)) {
            if (wheel.Get(Wheel.Size) < 8 || wheel.Get(Wheel.Size) > 24) {
                throw new ValidationException {
                    ValidationSource = this;
                    Meta = GetMeta(this);
                    Instances = new Base[subject];
                    Category = "Webshop";
                    ErrorCode = "Webshop_NCycle_0";
                    Message = "The size (${wheel.Get(Wheel.Size)}) of wheel '${GetID(wheel)}' is invalid in ${GetID(subject)}.";
                };  
            }
        }
    }
}
The Validate operation has two parameters: subject is the entity we are validating, while context is the container entity (if any). The context parameter is used only if we validate a slot or annotation (for example if we validate the Size slot, context will be the Wheel entity containing it). In the Product.Validate operation, we assess the size of all wheels and throw a ValidationException if the size does not meet the specified restrictions. Utilizing ValidationException is the standard approach for managing validation errors. This mechanism promotes data integrity by enforcing rules that all Wheel entities must adhere to, thereby preventing invalid states from occurring in the model.

If we define a custom validation logic, all of refinements of the host entity will inherit the validation. For example, UniCycles will check the size of their wheels automatically. It is important to note that the logic described will also apply to Bicycle entities, even though they do not have an explicit Wheels slot. This is because we previously divided the Wheels slot into FrontWheel and RearWheel. The classifier slot NCycle.Wheel can still be utilized to access both wheels. This demonstrates the flexibility of the model in managing relationships and ensuring that validation logic remains consistent across different entities, even when their structure is refined.

Step 4: Defining Contracts

When examining the Car and MobilePhone entities, we may observe that both possess the Model and Manufacturer slots. It would be advantageous to handle them uniformly from this perspective, but creating a common classifier for both entities is not feasible since only one classifier can be assigned to an entity. To overcome this issue and be able to reference slots across different entities, DMLA has Contracts.

A contract serves as an annotation that defines a partial structure definition, akin to structural typing in languages like TypeScript. When an entity adheres to a contract, it ensures that the slots defined in the contract are available on that entity, and all annotations associated with those contract slots are also applied to the entity's slots. (The only exception to this is the ContractSlot annotation, which must be explicitly included on all slots of the contract.)

Let's proceed to create a contract that encompasses the Model and Manufacturer slots and apply it to both the Car and MobilePhone entities:

entity ManufacturerAndModel: Contract {
    @Type = {TypeName = String;}
    @ContractSlot
    new slot Manufacturer;

    @Type = {TypeName = String;}
    @ContractSlot
    new slot Model;
}

@ManufacturerAndModel
entity Car: Vehicle { ... }

@ManufacturerAndModel
entity MobilePhone: Product { ... }
By employing the @ManufacturerAndModel annotation, we guarantee that both the Car and MobilePhone entities fulfill the contract. Adding the contract to an entity does not mean that the contract slots are added implicitly, we have to take care of this. The framework automatically verifies whether the contract is fulfilled during model validation. This mechanism allows for consistent and reliable behavior across entities that share common attributes, enhancing the integrity and maintainability of the model.

Since the framework ensures that entities fulfill their contracts, we can obtain the value of the slots using the contract definition as well:

foreach (Product product in GetAll(Warehouse.Stock)) {
    if (product.HasAnnotation(ManufacturerAndModel)) {
        PrintLn("Product - direct:   ${product.Get(Car.Manufacturer)}");
        PrintLn("Product - contract: ${product.Get(ManufacturerAndModel.Manufacturer)}");                    
    }
}
In the above code, both Get operations will return the correct result.

Step 5: Global operations

Although most of the operations are to be interpreted within the context of an entity, there are cases when global operations are useful. When a global operation has no parameters, it can be executed directly from the editor using the option Run or by invoking the dmla launcher in a terminal at the workspace root with the entrypoint argument set to the fully qualified name of the operation e.g.

dmla run --entrypoint Webshop.ListEverything

The next step is to create a ListEverything operation that lists the stock of all warehouses.

operation void ListEverything(){
   foreach(Warehouse warehouse in GetInstances(Warehouse, true))
   {
      warehouse.ListAll();
   }    
}
Here, we use the built-in global operation GetInstances, which returns all refinements of a given entity. The second parameter of the operation call shows whether we want to retrieve all entities along the refinement chain, or the direct refinements only.

Step 6: Creating and testing

By using global operations, we have a convenient way to create test operations (similar to unit tests) for our domains. When creating such a test case, it is worth to dynamically create new entities. Let us define a test that creates a new warehouse, adds a few items to it and lists the stock of the warehouse.

operation void Test(){
        Warehouse myWareHouse = Create(Warehouse);
        Book myBook = Create(Book)
                        .Set(Book.Title, "Hamlet")
                        .Set(Book.Author, "William Shakespeare");
        myWareHouse.AddValue(Warehouse.Stock, myBook);        
        myWareHouse.AddValue(Warehouse.Stock, Create(ToyotaCar));
        myWareHouse.AddValue(Warehouse.Stock, Create(Bicycle));
        myWareHouse.ListAll();
}
The built-in Create operation creates a new refinement of a given entity similarly as if we would instantiate a class in an object-oriented language. The Set operation is used to set the value of a slot in an entity. It can be used in a fluent API-style thus chaining the method calls, since Setreturns the entity modified. Since the stock of the warehouse is a list, not a single item, we cannot use Set when adding new items to the stock, but the operation AddValue can used instead.

Although DMLA does not support constructors, we can add custom operations to entities that initialize the slots of the entity. For example, we can create an operation that creates a bicycle with wheels of specified size.

entity Bicycle {
...
       operation Bicycle BuildNewBike(Number size) {
            Bicycle bike = Create(Bicycle)
                            .Set(Bicycle.FrontWheel, Create(Wheel).Set(Wheel.Size, size))
                            .Set(Bicycle.RearWheel, Create(Wheel).Set(Wheel.Size, size));

            return bike;
        }
}