System Architecture: 技術筆記03 泛型與非泛型介面在Strategy pattern之應用

Joyce Hsiao
Hsiao’s Blog
Published in
9 min readJan 12, 2023

#Clean Code #Code Architecture #Design Pattern

在眾多的design pattern當中strategy pattern是應用最廣泛,使用卻不自知的一種設計模式,簡單來說就是使用interface去定義各種action並讓各自class去實作,以達到程式的可擴充性(extensibility)和低耦合(loose coupling)。

網路上有很多相關的介紹,而這邊我想記錄下實作時在generic interface所遇到的問題。

Photo by Christin Hume on Unsplash

假設我們有一個自動繪圖的系統,這個系統能夠藉由不同的參數畫出各式各樣的形狀,簡單描述系統架構如下:

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();
}
}

當創建的形狀class越來越多後,會很惱人的發現每增加一個就要在ShapeOperation中的各個地方去增加方法呼叫。好的程式架構是在做新的需求時可以做到最少的更動,因此這邊我嘗試去改寫它,最快想到的方法就是List,卻因為泛型<T>讓這個改寫變得困難很多。

因為在實例化時必須要給它一個type,但是我們目標是List有各種type circle, square之類的,只是它們都實作了Shape<T>,並想要利用shape<T>所提供的function去簡化我們的程式碼。

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

List<Shape<T>> shapeList = new List<Shape<T>>(); //doesn't work
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();
}
}
}

所以必須要建立另一個non-generic interface 讓原本的generice interface去繼承它:

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();
}
}
}

如此一來,我們在List的type就可以避開Shape<T>而是直接使用ShapeBase,並且成功在ShapeOperation中用foreach的方式做改善。

但是這樣的方式只能用在non-generic function像是draw(), clear(),如果我們想要使用getArea怎麼辦?

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;

....
}
}
}

目前我只有"可行"的解決方案,但是我認為應該會有更好的做法。這個可行作法是在ShapeBase新增一樣的方法<T>改成object type,並讓class去實作所有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);
}
}

此時的Square class必須實作getArea(object params)和 getArea(SquareModel params)。

在ShapeOperation當中呼叫的是ShapeBase中的getArea(object params),因此呼叫getArea時會先進到class中被實作的getArea(object params),在那裏把object type的params轉成各class需要的typer進到真正邏輯實作的getArea<T params>。

但這裡在型別轉化(Explicit conversion)時就需要特別注意,因為在coding時可能不會出錯但在執行時也許會出現一些非預期錯誤。

因此這方法就取決於使用情境,去衡量要逐一撰寫或者改成這樣的寫法,另外也要考慮程式可讀性,寫得太崎嶇也可能不是個好方法。

Reference:

--

--