Okay, assuming this is a bi-directional association, how many pilots are associated with a flight? How does a flight refer to its pilots? Are the pilots shared or owned by the flight? What code should be generated? And so many more questions.
Does an instance of the Students class represent a single student or a group of students?
Aggregation (shared and composite) should only be used to emphasize that an association between two classes represents a part-whole relationship:
That said, there are lots of debatable part-whole relationships:
Given that aggregation doesn't make a difference when mapped to reference-based languages like Java, ask yourself if you are creating more confusion or less by using aggregation.
Assume we want to build a model for an online ticket vendor. In this model a ticket has two properties: a price of type Money and an event of type Performance.
We can represent these properties as attributes of as references (endpoints of associations).
The following diagram represents event as an attribute and price as a reference:
Both properties are (correctly) mapped to fields by a code generator:
class Ticket {
Money price;
Performance event;
// etc.
}
However, Money is a very general class that might occur in any commercial model, while Performance is more specific to online ticket sales. It would be better to not clutter the diagram with the Money class. It belongs in some reusable commerce package and can appear as an attribute type. On the other hand, the relationship "T is a ticket for performance P" is a significant relationship in the domain of online ticket sales and therefore should be modeled by an association:
Consider:
Every student is advised by a teacher. Every teacher has a student TA.
Note that advised-by and has-a-TA are two different relationships. One is not the inverse of the other as suggested by this diagram:
(I.e., if Smith is the TA for Prof. Jones, it does not follow that Prof. Jones is Smith's advisor.)
Instead, two associations are needed:
A bloated class has too many responsibilities. A lazy class doesn't have enough.
In the following diagram Ship, Car, and Airplane are empty classes that ae only used for type identification. All of the behaviors are in Vehicle:
Here's what the Vehicle class might look like:
class Vehicle {
void fly() {
if (vehicle.class != Airplane.class)
throw new Exception("Only airplanes can fly");
// etc.
}
void drive() {
if (vehicle.class != Car.class)
throw new Exception("Only cars can drive");
// etc.
} void sail() {
if (vehicle.class != Ship.class)
throw new Exception("Only ships can sail");
// etc.
}
}
Each time a new type of vehicle is added (helicopter, bike, flying saucer) we will need to modify the Vehicle class.
There are two ways an object can make use of another object's methods: delegation and inheritance.
Only use inheritance when the behavior of one object extends the behavior of another.
In this example a stack is a list and therefore inherits all of the operations of a list, even though these operations would allow unwanted access to the stack's data.
In this diagram a stack has a list. The stack operations can delegate to the appropriate list operations without exposing them to clients:
A middleman passes messages between a client and a provider. For example:
If the middleman doesn't add value to the exchange (for example by translating messages from one language/protocol to another), then consider getting rid of it.
Use dependency arrow sparingly in class diagrams.
Class A depends on class B means changes to B might require changes to A. Not much information. Generalization, realization, and association all imply dependency and they give a lot more information.
The world is filled with types. Representing them as strings makes comparing types inefficient and error prone.
In this example gender and status are types. Gender is represented as a string, while status is one of two objects.
Representing gender as a string opens the door for a host of possible errors:
member.gender = "mail";
member.gender = "transfluidstraight";
member.gender = "fEmAlE";
Think of a domain model as a glossary of domain terminology. Therefore, names of classes, operations, attributes, and endpoints should reflect the terminology used by domain experts.
For example, if the domain expert says "Dispatchers dispatch trucks" don't model this as:
Sometimes.
Use interfaces to represent gateways to secondary actors.
In an e-commerce application we may need to communicate with various bank servers (secondary actors). The APIs of these servers could vary, and we may not want to wade into the details during the modeling phase, so we can introduce a gateway interface, then later use the Adapter Pattern to implement it
We might not know how Accounts are represented, so we can use an interface for this, too:
In my opinion visibility is a design-time decision. It's also a topic that would not be understood by other stakeholders who must understand the domain model. On the other hand, if we are practicing domain-driven design, the domain model becomes the domain layer of the design model, at which point visibility gets added to the model. Also, it's easy to hide visibilities.
Example: An organization has a
parent organization and many subsidiaries.
Wrong
Right
Example: A flight has a
departure time and an arrival time.
Wrong
Right
If B is a generalization of A or if A realizes interface B, then the sentence "A is a B" makes sense.
If A is associated with B as if the sentence "A uses-a or has-a B" makes sense.