1

My intention is to describe the architecture of a tennis court management software with a class diagram, keeping the open-closed principle.

Initially, the software should only provide the following functions:

  1. CourtOfAWeek(week:int):void. This function outputs all tennis courts in the console.
  2. CourtOfADay(day:int):void. This function outputs all tennis courts of a day in the console.

It should now be possible to add new functionality to the software at any time without having to change the existing software.

My solution concept is to apply the strategy pattern. Each functionality inherits from the abstract class AbstractFunction. I would also include all functions as a list in the Tennis_Center class. (see the class diagram and the code).

enter image description here

AbstractFunction:

 abstract class AbstractFunction{
    
 }

GetCourtByDayClass:

class GetCourtByDayClass:AbstractFunction{

    public void GetCourtByDay(int day){
        Console.WriteLine("Court 1, Court 2 and Court 3 are free.");
    }

}

GetCourtByWeekClass:

class GetCourtByWeekClass:AbstractFunction{

    public void GetCourtByWeek(int week){
        Console.WriteLine($"On Week {week} Court 5 and 6 are available");
    }

}

Tennis_Center:

List<AbstractFunction> functionList=new List<AbstractFunction>();

functionList.Add(new GetCourtByDayClass());
functionList.Add(new GetCourtByWeekClass());

functionList[1].GetCourtByWeek(1);

Console.ReadKey();

Now to my problem: I think I misunderstood or incorrectly implemented the strategy pattern. Now when I call the functions in the Tennis_Center class, I get the error message:

"AbstractFunction" does not contain a definition for "GetCourtByWeek", and no accessible GetCourtByWeek extension method could be found that accepts a first argument of type "AbstractFunction" (possibly a using directive or assembly reference is missing). [Tenniscenter]csharp(CS1061)

Can you give me some advice on which pattern is best for implementing the above situation.

Z.J
  • 301
  • 1
  • 7
  • 1
    `FunctionList` stores only `AbstractFunction` instances. You have defined `AbstractFunction` as a class that has no methods, hence the error. – ChrisBD Aug 16 '22 at 12:09

3 Answers3

4
  1. Define an abstract method in AbstractFunction class.
abstract class AbstractFunction
{
    public abstract void GetCourt(int @value);
}
  1. For the derived classes, override the abstract method from base AbstractFunction.
class GetCourtByDayClass : AbstractFunction
{
    public override void GetCourt(int day)
    {
        Console.WriteLine("Court 1, Court 2 and Court 3 are free.");
    }
}

class GetCourtByWeekClass : AbstractFunction
{
    public override void GetCourt(int week)
    {
        Console.WriteLine($"On Week {week} Court 5 and 6 are available");
    }
}
  1. For the caller function:
functionList[1].GetCourt(1);

AbstractFunction func = new GetCourtByWeekClass();
func.GetCourt(1);

Demo @ .NET Fiddle

Yong Shun
  • 35,286
  • 4
  • 24
  • 46
  • Thank you for your explanation. From your solution I can see how to apply the strategy pattern to the case I described. Thank you for your effort. I'm thinking about whether your solution also fits when adding a functionality like booking a tennis court (Booking) or canceling a tennis court (Canceling). – Z.J Aug 16 '22 at 14:20
  • You're welcome. For booking a tennis court (Booking) or canceling a tennis court (Canceling), I doubt that it may not suitable to apply strategy pattern. For my perspective, strategy is used when you are receiving the same parameter(s), process and result with different outcomes. (Same method signature). Think of calculator when perform addition, subtraction, multiply... – Yong Shun Aug 17 '22 at 01:24
  • You may revise what is the expected parameters and return type of new methods. And also think of what if the new methods are not matched the method signature, when you gonna change the `AbstractFunction`, the change will also impact the derived classes. Hence this is not suitable as it violated the Open Closed principle. Maybe this article: [Strategy Pattern | RefactoringGuru](https://refactoring.guru/design-patterns/strategy) brings you the idea whether it suitable or not. – Yong Shun Aug 17 '22 at 01:30
2

I think you are misusing the pattern.

The patterns are used to solve common problems. If you do not have a problem then do not complicate everything using patterns. In your case you don't need to use classes and maybe you need to learn about delegates Action<T> or Func<T>.

Now I'll show an example of when to use the Open-Close Principle:

public class Circle { }
 
public class Square { }
 
public static class Drawer {
   public static void DrawShapes(IEnumerable<object> shapes) {
      foreach (object shape in shapes) {
         if (shape is Circle) {
            DrawCircle(shape as Circle);
         } else if (shape is Square) {
            DrawSquare(shape as Square);
         }
      }
   }
   private static void DrawCircle(Circle circle) { /*Draw circle*/ }
 
   private static void DrawSquare(Square square) { /*Draw Square*/ }
}

As you can see DrawShapes is a function that uses shapes. Inside of it there is switch that checks the type and uses the corresponding functionality. The problem is that you can have hundreds of functions that use the same Shape and all of them use switch. Imagine you are adding a new Shape, then you need to change all hundreds of those functions adding new switch case to them and that's a huge problem, because you just wanted to add a new Shape but now you need to change 100 functions. It violates the Open-Close principle.

Let's look at how the principle solves this problem.

public interface IShape { void Draw(); }
 
public class Circle : IShape { public void Draw() { /*Draw circle*/ }}
 
public class Square : IShape { public void Draw() { /*Draw Square*/ } }
 
public static class Drawer {
   public static void DrawShapes(IEnumerable<IShape> shapes) {
      foreach (IShape shape in shapes) {
         shape.Draw();
      }
   }
}

The function DrawShapes or other 99 functions just use IShape interface (it could be just a base class with default implementation) and call it, meaning the function does not know which Shape was provided. Now if you want to add a new Shape then you only need to create a new class that implements the interface. The code is open for extending but closed for modifying (because you do not need to change the code in other places).

  1. Keep the code as simple and use the patterns when needed.
  2. My personal rule: use switch only for checking the type and only in one place. Otherwise it's a code smell.
  • Thank you for your good example. It showed me once again that the strategy pattern is suitable when there are different implementations for a given method. Young Shun has shown in his article that there can be different implementations for the method getCourt and that the strategy pattern could be used. What recommended way would you suggest to add completely different functionalities (such as booking, canceling, deleting, etc.) to a software without changing it. As you have correctly shown, the strategy pattern is rather unsuitable. – Z.J Aug 16 '22 at 14:28
0

yeah, I know that I am a little bit late to the party, but give me a try.

You are right that Strategy pattern can be used to select an algorithm at runtime. However, it is necessary to store these algorithms somewhere. I think, this is a place where Factory pattern can be used.

So let me show an example. I've little bit refactored your code as AbstractFunction is not good idea to call class.

So let's create an abstract class that will define common behaviour for all courts:

public abstract class Court
{ 
    public abstract void Get(int value);
}

And its concrete implementations CourtByDayClass and CourtByWeekClass :

public class CourtByDayClass : Court
{
    public override void Get(int day)
    {
        Console.WriteLine("Court 1, Court 2 and Court 3 are free.");
    }
}

public class CourtByWeekClass : Court
{
    public override void Get(int week)
    {
        Console.WriteLine($"On Week {week} Court 5 and 6 are available");
    }
}

Then we need a place to store these strategies and take it when it is necessary. This is a place where Factory pattern can be used:

public enum CourtType 
{
    Day, Week
}

public class CourtFactory
{
    private Dictionary<CourtType, Court> _courtByType = 
        new Dictionary<CourtType, Court>()
    {
        { CourtType.Day, new  CourtByDayClass() },
        { CourtType.Week, new CourtByWeekClass() },
    };

    public Court GetInstanceByType(CourtType courtType) => _courtByType[courtType];
}

And then you can call the above code like this:

TennisCenter tennisCenter = new TennisCenter();
CourtFactory courtFactory = new CourtFactory();
Court courtByDay = courtFactory.GetInstanceByType(CourtType.Day);
courtByDay.Get(1); // OUTPUT will be "Court 1, Court 2 and Court 3 are free."

So, here we've applied open closed principle. I mean you will add new functionality by adding new classes that will be derived from Court class.

StepUp
  • 36,391
  • 15
  • 88
  • 148