0

I have this use case (I've simplified it):

UML Use case

Each student can apply for one or multiple activities. As of now we only have Swimming activity onboarded on the system, but we expect to have more. Base activity is not abstract because it's the base activity if the student doesn't like any of the activities on the list. I was thinking that Abstract Factory design pattern will be the best fit for this use case to instantiate each activity. But I struggle to understand how to instantiate each activity as, potentially, each of them could have different constructor params.

I've thought on applying an interface to hold each activity params. Something like this:

public interface IArgs
{
    //Can be empty
}

public class SwimmingActivityArgs : IArgs
{
    public int a;
    public string b;
}

Then use those args for the construction of each of the Activities.

I'm looking for someone who will challenge my thoughts and learn throughout the process, a new way of solving this use case.

3
  • 1
    Since SwimmingActivity is not really a BaseActivity it should not inherit from it. Instead you should probaby introduce an interface IActivity with both classes implementing it.
    – Helena
    Apr 17, 2022 at 7:23
  • It's more of a strategic inheritance, as SwimmingActivity shares some of the properties that BaseActivity has. I didn't want to duplicate properties
    – Juanma
    Apr 17, 2022 at 22:23
  • 1
    Uhh.... 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. Apr 19, 2022 at 14:50

2 Answers 2

2

In short

The problem is not what you think it is. You need to take a step back, look at the whole problem, reframe it, and opt for a different approach.

Some more thoughts

How to solve your current issue?

Indeed, every specialization of BasicActivity could require a different set of parameters. Constructing SwimmingActivity does not require the same arguments as MartialArtActivity or OutdoorActivity.

You could solve this by using a parameterized factory method: the factory would take some parameters, for example an activity code and an IArg or even a JSON string, and return an activity that matches the arguments.

What's the real issue?

This factory would work well to solve your specific problem in practice. But it has two serious issues:

  • First, the factory (or cascading factories) itself might become a very complex method with a lot of switch...case and would not comply with the open-closed principle.
  • Second, how would the context calling the factory know what parameters are necessary for which activity ?

This second issue is in reality the cornerstone of your problem. A stone you might stumble upon, whatever you chose as solution.

How to reframe the problem

It's difficult to guess, as we know very little about your full design. Think abstract and polymorphic. Some hints (ideas among others):

  • Outsource the construction of the activity to where the choice of the activity is made. If your app has an activity choice list, you would have anyway to design the user interface to map the right fields to the the right activity.
  • Add a state to the activities and create the activities without any argument (state ToBeConfigured). Add them to the list. Configure them in a second step (you could then even use double dispatch to profile any parameter that can be deduced from the activity and the student).
1
  • 1
    I like your inputs, I think I tend to overthink to every problem I face. The key thing is, like you said, take a step back. I like the idea of having states on the activities, creation at first place and configuration at a second step. I'll give it a shot and will tell you how it went.
    – Juanma
    Apr 18, 2022 at 17:36
1

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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.