On this page

Objects, data and reusable class headers

An ordinary class holds state and operations. A data declaration represents a value-shaped request, response or message. Interfaces describe a contract. A class kind reuses class-header declarations. These tools solve different parts of application design; start with a plain class and add the others when their role becomes clear.

class / constructor: create a valid object

public class CustomerAccount
    public readonly Id as Global.System.Guid
    public Name as String with private set = ""
    public CreditLimit as Decimal with private set = 0

    public constructor(id as Global.System.Guid, name as String, creditLimit as Decimal)
        require arguments.id <> Global.System.Guid.Empty
        require arguments.name is not nothing
        require arguments.name is not whitespace
        require arguments.creditLimit >= 0

        self.Id = arguments.id
        self.Name = arguments.name.Trim()
        self.CreditLimit = arguments.creditLimit
    end constructor

    public action Rename(newName as String)
        require newName is not nothing
        require newName is not whitespace
        Name = newName.Trim()
    end action

    public action ChangeCreditLimit(newLimit as Decimal)
        require newLimit >= 0
        CreditLimit = newLimit
    end action

    public action CanCover(amount as Decimal)
        returns Boolean
        require amount >= 0
        return amount <= CreditLimit
    end action
end class

constructor ... end constructor creates an instance. Its ordinary parameters do not become stored members automatically. The assignments make that storage explicit.

readonly Id must be deliberately initialized exactly once during construction and cannot change later. A private setter has a different meaning: the class may change Name and CreditLimit through its actions after construction, while callers cannot assign those properties directly.

Public stored declarations are properties by default. A private stored declaration is a field by default. Explicit property or field can select the shape where that distinction matters to an external .NET contract.

property / get: compute a value from state

A computed property can present state without storing a second version of it:

public class Contact
    public FirstName as String = ""
    public LastName as String = ""

    public property DisplayName as String
        get
            return (FirstName + " " + LastName).Trim()
        end get
    end property
end class

The getter reads the current names whenever it is called. There is no writable DisplayName property to get out of sync with them. For custom setters, value denotes the incoming value and field denotes that property's backing storage.

action: operate on instance state

Use a receiver scope to group related operations without repeating the object name. For an existing Contact, this action fragment updates the same object:

use contact
    .FirstName = "Ana"
    .LastName = "Petrova"
    Global.System.Console.WriteLine(.DisplayName)
end use

The receiver is evaluated once. Leading dot refers to that captured object. Use is strict: if contact is nothing, the scope fails rather than skipping those statements. It does not create or dispose a Contact.

When absence is expected and skipping is the intended policy, make that choice explicit:

use_if contact
    Global.System.Console.WriteLine(.DisplayName)
end use_if

An absent receiver skips the use_if body. A missing segment in a receiver chain also skips qualification and the body. Exceptions from evaluated getters or body operations still propagate. Ordinary access such as contact.DisplayName remains available; a receiver scope is useful when several operations share one object.

To revise a copy of a present Contact, make copying explicit and then use the new object:

let revised = use copy_of contact
    .FirstName = "Mira"
end use

The source is copied once and revised is the new instance after successful completion. The original Contact keeps its FirstName. Copying is shallow, so other mutable objects referenced by a copy can remain shared. This receiver scope obeys ordinary writability: it cannot bypass a private setter, change a readonly member or rewrite a data construction-only property. A copy does not gain a special with override block.

data: declare a message with value semantics

public data CreditLimitChanged
    CustomerId as Global.System.Guid
    PreviousLimit as Decimal
    NewLimit as Decimal
    Reason as String = "Manual adjustment"
end data

public class CreditLimitMessages
    public static action Create(
        customerId as Global.System.Guid,
        previousLimit as Decimal,
        newLimit as Decimal
    )
        returns CreditLimitChanged
        require customerId <> Global.System.Guid.Empty
        require previousLimit >= 0
        require newLimit >= 0

        return new CreditLimitChanged(customerId, previousLimit, newLimit)
    end action
end class

The three members without initializers must be supplied during construction. Reason has an initializer and is optional. Data members are initialized during construction; callers cannot later assign message.NewLimit = 1000.

Data has record value equality and matching hashes. String members use the language's ordinal case-insensitive comparison. A collection member keeps its ordinary .NET equality contract; two separate Lists with the same elements do not automatically become equal. Construction-only properties also do not freeze the objects or collections they reference.

Use an ordinary class when updates in place are part of the model. Use data when a particular request, event or result should remain a stable description of what happened. Avoid placing passwords or other secrets into an unrestricted record representation that might be logged.

interface / implements: declare and fulfill a contract

public interface IReceiptSender
    action Send(address as String, orderId as Global.System.Guid)
end interface

public class ConsoleReceiptSender
    implements IReceiptSender

    public action Send(address as String, orderId as Global.System.Guid)
        implements IReceiptSender.Send

        require address is not nothing
        require address is not whitespace
        Global.System.Console.WriteLine("Receipt {{orderId}} addressed to {{address}}")
    end action
end class

The class header declares its interface relationship. The action states which interface member it implements, so that relationship stays visible when reading the action by itself. The implementation must have a compatible signature.

For related interface members, a named implementation block can supply that intent:

public class ConsoleReceiptSender
    implements IReceiptSender
        public action Send(address as String, orderId as Global.System.Guid)
            require address is not nothing
            require address is not whitespace
            Global.System.Console.WriteLine("Receipt {{orderId}} addressed to {{address}}")
        end action
    end implements
end class

This replaces the preceding class declaration and uses the same interface. Send matches the interface member by name and compatible signature within the block, so it needs no individual implements clause. Renamed members still need an explicit mapping, and unrelated supporting helpers do not satisfy missing requirements. Members outside the block use explicit member mappings.

Inheritance supplies an existing implementation. A base action intended for replacement uses overridable; the derived action says overrides base.Member after its header. Constructors are not inherited. If the base requires arguments, the derived constructor forwards them with calls base(...) before its ordinary body.

overridable / overrides: replace inherited behavior

public class DeliveryService
    public readonly ProviderName as String

    public constructor(providerName as String)
        require arguments.providerName is not nothing
        require arguments.providerName is not whitespace
        self.ProviderName = arguments.providerName
    end constructor

    public overridable action EstimateDays(destination as String)
        returns Integer
        require destination is not nothing
        return 5
    end action
end class

public class ExpressDeliveryService
    inherits DeliveryService

    public constructor()
        calls base("Express carrier")
    end constructor

    public action EstimateDays(destination as String)
        returns Integer
        overrides base.EstimateDays

        require destination is not nothing
        if destination = "MK" then
            return 1
        end if
        return 2
    end action
end class

The base constructor establishes a required provider name. The derived constructor forwards it before its own body. Calling EstimateDays through a DeliveryService reference still selects the derived implementation for an ExpressDeliveryService instance.

The explicit overrides line distinguishes replacement from an unrelated same-named operation. Omitting it for a matching overridable base member is an error. Calling the base implementation is a separate choice; use call base.EstimateDays(destination) when the derived policy needs that behavior.

Inheritance fits a genuine specialization. Prefer composition when two independent policies need to vary, such as a shipping calculator and a notification sender. That avoids a class hierarchy for every possible combination.

of / where: define generic classes

A generic class can hold the same application structure for several element types without giving up their types:

import Global.System.Linq.*

public class ProcessingBatch <of T>
    private readonly items as Global.System.Collections.Generic.List <of T>

    public constructor(source as Global.System.Collections.Generic.IEnumerable <of T>)
        require source is not nothing
        items = source.ToList()
    end constructor

    public property Count as Integer
        get
            return items.Count
        end get
    end property

    public action Get(index as Integer)
        returns T
        require index >= 0 and index < items.Count
        return items[index]
    end action
end class

ProcessingBatch <of Customer> returns a Customer from Get; it does not return an Object that callers must cast. ToList takes a snapshot of the sequence's membership during construction. It is a shallow snapshot: if a T is a mutable object, the batch still refers to that same object.

Some generic code needs more than storage. A factory may need a common base property, an interface operation and a public parameterless constructor. Declare those requirements with where restrictions rather than casting Object or guessing that an arbitrary T has the right members. The import-draft factory shows an ordinary class using all three requirements and returning its actual draft type.

class kind: reuse a class header

FluentValidation validators often inherit a generic base and expose the same generic interface. A kind can name that pattern:

import Global.FluentValidation.*

public class kind Validator <of T>
    exposes Global.FluentValidation.IValidator <of T>
    inherits Global.FluentValidation.AbstractValidator <of T>
    default scope internal
end class kind

public class Customer
    public Name as String = ""
    public Email as String = ""
end class

public class CustomerValidator is Validator <of Customer>
    public constructor()
        RuleFor(.Name).NotEmpty().Length(2, 100)
        RuleFor(.Email).NotEmpty().EmailAddress()
    end constructor
end class

This example requires the FluentValidation package. Its namespace import brings the extension methods into scope; the import may instead live in the project's import declarations. RuleFor, NotEmpty, Length and EmailAddress are library calls. No new validation keywords are involved.

The kind expands the class header at compile time. It does not create another base type between CustomerValidator and AbstractValidator <of Customer>. The class still owns its constructor and behavior. exposes lets the kind stand for the stated interface in a type position; a kind without an exposed interface cannot be used as a parameter or result type.

default scope internal applies when a class using this kind omits its own visibility. The explicit public above takes precedence. The kind's visibility and its class visibility policy are separate choices.

The application chapter builds ordinary validation into a workflow before introducing this optional shortcut. See classes and interface binding for precise rules.

Practice: classes, interfaces and kinds

Change the Customer validation boundary, then explain why using a class kind does not change the rule or activate the customer.

Previous: Actions and callables · Next: Collections and text