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