Class Organisation

Last updated on 29th April 2024

Actions and Sequences

Models can quickly grow to have many actions with splits in a single sequence. Adding the implementation logic for agents in the same place can make the sequence of actions hard to follow.

Actions should be created by functions inside agents. The functions should have clear names indicating the action's purpose. With purposeful naming, the sequence of steps becomes a sequence of clear action names, rather than a sequence of code that is hard to read.

If the function which creates an action is static, that function should return the action it creates.

The easiest way to build an action-creating function is to put all the implementation in a static function, then call that static function inside a model step.

// Static function which creates an action that sends a message.
public static Action<Flat> sendVacancyMessage() {
    return Action.create(
        Flat.class,
        flat -> {
            if (!flat.isOccupied()) {
                flat.getLinks(Links.VacancyLink).send(Messages.Vacancy.class, flat.size)
            }
        };
    );
}

// Agent-based model that uses the static action to send a message.
public class myModel extends AgentBasedModel<GlobalState> {
    @Override
    public void step() {
        run(
            Flat.sendVacancyMessage()
        );
    }
}  

Action-creating functions built in this way can be placed into sequences and splits, while ensuring that overall functionality is still clear.

run(
    Split.create(
        Flat.sendVacancyMessage(),
        Household.bidIfHomeless(),
        Sequence.create(
            Household.giveUpFlat(),
            Flat.notifyBucketOfVacancy()
        ),
        Bucket.processBidsAndVacancies()
        ...
    );
);

When many action-creating functions are built statically though, two different problems can happen. The first problem is that there may be a lot of duplicated code inside the static function, which can cause maintenance problems. The second problem is that creating a lot of static functions may violate encapsulation principles. Static functions are usually best reserved for utility functions, rather than specific functionality in an object. There is really no need to expose all the functionality of the sendVacancyMessage function.

These problems can be solved by implementing a static function inside the agent which creates actions, regardless of what those actions are. A generic function like this keeps all the unique functionality of the sendVacancyMessage function as part of the agent, where it belongs. The sendVacancyMessage function still returns an action. The static action-creating function can then be used throughout the agent, as many times as necessary, without impacting other functions or duplicating code.

public class Flat extends Agent<GlobalState> {

    // Generic static action-creating function.
    public static Action<Flat> action(SerializableConsumer<Flat> action) {
        return Action.create(Flat.class, action);
    }

    // Vacancy message function which calls the action creator.
    public Action<Flat> sendVacancyMessage() {
        return action(
           flat -> {
               if(!flat.isOccupied()) {
                   flat.getLinks(Links.VacancyLink).send(Messages.Vacancy.class, flat.size);
               }
           };
        });
    }
}  

These functions are organized much better, but there is still more refining that can be done. When specific agent functions return actions, as in the previous example, it is always necessary to explicitly call the action creator, and usually an object initializer. These lines inside the agent functions do not actually have anything to do with what the function does; they are just coding glue for the action creator.

To get rid of this coding glue, implement agent functions that do not return anything. This ensures that they contain only the code necessary to perform their jobs, nothing else. These agent functions can then be called using the generic action creator function. Structuring functions and calls in this way completely removes action objects from agent functions, and makes the process of calling a specific action explicitly clear in the code.

// Flat agent class.
public class Flat extends Agent<GlobalState> {

    // Generic static action-creating function.
    public static Action<Flat> action(SerializableConsumer<Flat> action) {
        return Action.create(Flat.class, action);
    }

    // Vacancy message function.
    public void sendVacancyMessage() {
        if(!flat.isOccupied()) {
            flat.getLinks(Links.VacancyLink).send(Messages.Vacancy.class, flat.size);
        }
    }
}  

// Use the static action method explicitly.
run(
    Split.create(
        Flat.action(Flat::sendVacancyMessage),
        ...
    );
);

This strategy makes for explicitly clear, easily maintainable code connecting agents and actions.

Messages

Messages should be defined in a shared message class so they can be used by the entire model. Individual messages should be defined as static classes inside the larger message class. Do not define messages inside agents, inside a model, or as many separate classes. This can lead to code duplication and difficulty in class maintenance.

The Simudyne SDK provides built-in classes for messages which contain a single primitive type (integer, long, double, float, boolean) or an empty body. Modellers need only extend these built-in classes to get all the functionality they need.

public class Messages {

    // Custom message classes that extend the built-in message type for doubles
    public static class Bid extends Message.Double {}
    public static class Vacancy extends Message.Double {}
}

If a message needs to pass multiple different primitive types, but no complex objects or behaviour, extend the Simudyne SDK Message object.

// Message class for price quotes, which passes only primitive types.
public class Messages {
    public static class PriceQuoteMessage extends Message {
        double price;
        double reducedPrice;
    }
}

// Class demonstrating usage of price quote messages.
public class Seller extends Agent<GlobalState> {
    public static Action<Seller> sendPriceQuote() {
        return Action.create(
            Seller.class,
            seller -> {
                seller
                    .getLinks(Links.BankLink.class)
                    .send(
                        Messages.PriceQuoteMessage.class,
                        message -> {
                            message.price = 3;
                            message.reducedPrice = 2.3;
                        };
                    );
            };
        );
    }
}

To send a complex data type, extend the Simudyne SDK Message.Object<T> generic object. Provide the name of the complex data type object in place of the "T" variable. When an agent receives a message containing a complex data type object, that agent has access to the complex data type's functions.

// Message class for price quotes which passes a complex data object.
public class Messages {
    public static class PriceQuoteMessage extends Message.Object<PriceQuote> {}
}

// Class demonstrating usage of complex price quote messages.
public class Seller extends Agent<GlobalState> {
    public static Action<Seller> sendPriceQuote() {
        return Action.create(
            Seller.class,
            seller -> {
            seller
                .getLinks(Links.BankLink.class)
                .send(Messages.PriceQuoteMessage.class, new PriceQuoteMessage(...))
            };
        );
    }
}

Agents And Inheritance

When multiple agent types need to share some common functionality, modellers can reduce code duplication and build better defined agents using inheritance. Extend the Simudyne SDK Agent class with the base class, and then the derived classes can be implemented with their own unique functionalities. In the following example, an Accomodation agent is the base for two derived agent types, Hotel and Flat. All of these agent types can be used just like other agents which don't implement inheritance.

// Accomodation base class.
public class Accomodation extends Agent<GlobalState> {
    public int size;
    public Action<Accomodation> sendVacancyMessage() {
        return Action.create(
            Accomodation.class,
            accomodation -> {
                // Send message if vacant.
            };
        );
    }
}

// Flat derived class.
public class Flat extends Accomodation {
    public Action<Flat> registerNewHousehold() {
        return Action.create(
            Flat.class,
            flat -> {
                // Register a new flat.
            };
        );
    }
}  

// Hotel derived class.
public class Hotel extends Accomodation {
    public Action<Hotel> registerBooking() {
        return Action.create(
            Hotel.class,
            hotel -> {
                // Make a new booking.
            };
        );
    }
}

// Model containing actions for Accomodation, Hotel, and Flat.
public class myModel extends AgentBasedModel<GlobalState> {
    @Override
    public void step() {
        run(
            Accomodation.sendVacancyMessage(),
            Household.requestHome(),
            Split.create(
                Flat.registerNewHousehold(),
                Hotel.registerBooking()
            );  
        );
    }
}

External Classes

External classes should be used for functionality that is not specific to agents. This functionality may be needed by multiple agents or by the model, but should not be unique to a specific agent type. A data loader or distribution builder could both be reasonable uses for these external classes. Methods in these classes should be static so they can be accessed from anywhere in a model.

// External distribution class.
public class Distribution {
    public static EmpiricalDistribution loadDistribution() {
        // Load a distribution.
    }
}

// Flat agent class which uses the distribution class.
public class Flat extends Agent<GlobalState> {
    public Action<Flat> registerNewHousehold() {
        return Action.create(
            Flat.class,
            flat -> {
                EmpiricalDistribution distribution = Distribution.loadDistribution();
            };
        );
    }
}