System Architecture: Generic interface inherit from non-generic interface in Strategy Design pattern

Joyce Hsiao
Hsiao’s Blog
Published in
4 min readJan 11, 2023

#Clean Code #Code Architecture #Design Pattern

The strategy design pattern is a frequently used pattern in our system design. Typically, we design function in interface and have individaul classes implement them. This way increases extensibility and promotes loose coupling.

Recently, I encountered an issue about this design pattern and wrote down my solution here.

Photo by Christin Hume on Unsplash

Imagine we have an automatic drawing system that can create various shapes with the provided attributes, here is system design:

interface Shape<T> {
draw();
clear();
double getArea(object params);
}
class Square : Shape<SquareModel> {
// implementation of Square class
}
class Circle : Shape<CircleModel> {
// implementation of Circle class
}
....other classes shape with different type
class ShapeOperation{
Square squareObj = new Square();
Circle circleObj = new Circle();
Oval ovalObj = new Oval();
Triangle triObj = new Triangle();

public drawShape()
{
squareObj.draw();
circle.draw();
ovalObj.draw();
triObj.draw();
}
public clearShape()
{
squareObj.clear();
circle.clear();
ovalObj.clear();
triObj.clear();
}
}

It is annoying when drawwing all the shapes because we have to call function of each object.

Therefore, using a “List” could be a solution but it doesn’t work right now.

The problem is <T>. T must be a type when creating the list but our goal is adding different type Square type, Circle type, etc.

class ShapeOperation{
Square squareObj = new Square();
Circle circleObj = new Circle();

List<Shape<T>> shapeList = new List<Shape<T>>();
shapeList.add(squareObj)
....

public drawShape()
{
foreach(Shape<T> shape in shapeList){
//if add new object, we don't need to add new line here
shape.draw();
}
}
}

So we need to create another non-generic interface ShapeBase and generic interface Shape<T> inherits from it:

interface ShapeBase{
draw();
clear();
}
interface Shape<T>:ShapeBase {
double getArea(T params);
}
class Square : Shape<SquareModel> {
// implementation of Square class
}
class Circle : Shape<CircleModel> {
// implementation of Circle class
}
class ShapeOperation{
Square squareObj = new Square();
Circle circleObj = new Circle();

List<ShapeBase> shapeList = new List<ShapeBase>();
shapeList.add(squareObj)
....

public drawShape()
{
foreach(ShapeBase shape in shapeList){
shape.draw();
}
}
public clearShape()
{
foreach(ShapeBase shape in shapeList){
shape.clear();
}
}
}

So that we can use ShapeBase as List type and successfully improve the code with “foreach”.

Furthermore, what if we need to get area from each shape?

class ShapeOperation{
Square squareObj = new Square();
Circle circleObj = new Circle();

List<ShapeBase> shapeList = new List<ShapeBase>();
shapeList.add(squareObj)
....

public doSomeThingNeededArea(string shape name, int width){
double result;
switch(shapeName){
case "square":
result=squareObj.getArea( new squareParams(width));break;
case "circle":
result=circleObj .getArea( new circleParams(width));break;

....
}
}
}

For now I only have a workable solution but could not be a good one. We can create the same function with object type parameters and let classes implement this object function and generic function.

interface ShapeBase{
draw();
clear();
double getArea(object params);
}
interface Shape<T>:ShapeBase {
double getArea();
}
class Square : Shape<SquareModel> {
double getArea(SquareModel params){...}

double getArea(object params){
return getArea((SquareModel)params) // Be careful
}
}
class ShapeOperation{
Square squareObj = new Square();
Circle circleObj = new Circle();

Dictionary<string,ShapeBase> shapeDict = new Dictionary<string,ShapeBase>;
shapeList.add("square",squareObj)
....

public doSomeThingNeededArea(string shapeName, int width){
double result;
object params = width;
result = shapeDict[shapeName].getArea(params);
}
}

Now the Square class needs to implement the function getArea(object params) and getArea(SquareModel params).

Need to be noticed is Explicit conversions, we must make sure params is squareModel stucture. Otherwise, it could has an error here.

Is this a good way? It depends on the situation. We need to make a trade-off. In addition, readability must also be taken into account.

--

--