This may be a case where you are overthinking the problem. You've got a number of different approaches to dealing with object construction and initializing data. The simplest approach is to just use an empty argument constructor and put the data in attributes. In fact most forms of serializing and deserializing objects work exactly like this.
Simple Object Use
If your activities are simple data objects, there really isn't any reason to use a factory or anything like that. In this case, you would have a simple no argument constructor and then initialize the values directly. The way that would look is like this:
var activity = new SwimmingActivity {
StartTime = DateTime.Now(),
IsLesson = false
};
student.AddActivity(activity);
If you are using any object serializers like JSON, XML, YAML, etc. then that is what the libraries are doing when they deserialize objects from the input stream.
Prototyping
In a prototyping scenario, you essentially have default set of objects that are already instantiated and then you clone a copy for your use when you assign it to a Student
. In this case, you might have a schedule class that has the available activities to choose from. If a student wants to take on that activity, then they would have to request it from the schedule.
public class Schedule {
private List<BaseActivity> available = new List<BaseActivity>();
// ... skip all the methods to look up the available activities
public void Register(Student student, int byIndex) {
// find the prototype. Do more error checking in real use
var activity = available[byIndex];
// and clone it so the student can't change other people's copy
var userActivity = activity.Clone();
student.AddActivity(userActivity);
}
}
Fluent API
This style of object initialization can work in some cases, but I feel like it might not match your use case. The general idea is that you are able to invoke your activity something like this:
var activity = Activities.Swimming().StartAt(DateTime.Now()).NotLesson().Create();
The implementation would be something like this:
public interface IActivityCreator {
BaseActivity Create();
}
// You really only need an object, but I am summarizing in the example
public interface ISwimmingAcivityCreator : IActivityCreator {
ISwimmingActivityCreator StartAt();
ISwimmingActivityCreator IsLesson();
ISwimmingActivityCreator NotLesson();
}
public static class Activities {
public static ISwimmingActivityCreator Swimming() {
return new SwimmingActivityCreator();
}
}
You'll find some unit testing frameworks that follow this approach.
Bottom Line
Unless you have a real need or design constraint to deviate from simple objects, I would recommend sticking with it. It doesn't break any expectations, and it is easy for others to wrap their head around. Even if you are using Entity Framework (the Object-Relational Mapping framework), it just works.
Another option is to invert the relationship of activities and students. In other words, instead of adding activities to students you are adding students to activities. That opens up the ability of your activity implementations to police the number of attendees if the activity has limited space.
student.addActivity(new SwimmingActivity(...args));
? There is no other way. Of course you can take the above and introduce loads of indirection (and complexity) if you want, but the design of your system mandates the above.