iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 26
0
Software Development

在Kata中尋找Clean Code是否搞錯了什麼系列 第 26

Day 26 封裝集合的方法

在前幾天的文章中提到的DragonBalls的例子中,我們提到可以把List和他的方法抽到一個類別中,讓List和他的相關方法封裝載DragonBalls的類別中,消除Feature Envy的壞味道。

public class DragonBalls
{
    private readonly List<DragonBall> _balls;
    private const int MaxVisibleDistance = 100;

    public DragonBalls(List<DragonBall> balls)
    {
        _balls = balls;
    }

    public IEnumerable<DragonBall> GetVisibleDragonBalls()
    {
        return _balls.Where(ball => ball.Position.GetDistance() < MaxVisibleDistance);
    }

    public DragonBall GetDragonBallAt(int index)
    {
        return _balls[index];
    }
}

public class DragonBallRadar
{
    private readonly DragonBalls _dragonBalls;

    ...
    
    public RadarScreen Visible(int index)
    {
        return new RadarScreen()
        {
            ...
            DragonBalls = new List<DragonBall>() {_dragonBalls.GetDragonBallAt(index)},
            ...
        };
    }
    
    public RadarScreen Visible()
    {
        return new RadarScreen()
        {
            ...
            DragonBalls = _dragonBalls.GetVisibleDragonBalls(),
            ...
        };
    }

    ...
}

但是同時也可以發現相對於List來說,DragonBalls的類別比較難看出他其實是封裝了List,可能是得移進類別方法本身才會知道,這個對於閱讀代碼來說有時候是會有一點不方便。

抽取類別之外,我們還能擴充方法來把List的操作封裝到List中,且保留直觀的List。

public static class DragonBallListExtension
{
    private const int MaxVisibleDistance = 100;
    
    public static IEnumerable<DragonBall> GetVisibleList(this List<DragonBall> dragonBalls)
    {
        return dragonBalls.Where(ball => ball.Position.GetDistance() < MaxVisibleDistance);
    }
}

public class DragonBallRadar
{
    private readonly List<DragonBall> _dragonBalls;

    public DragonBallRadar(List<DragonBall> dragonBalls)
    {
        _dragonBalls = dragonBalls;
    }
    
    public RadarScreen Visible(int index)
    {
        return new RadarScreen()
        {
            DragonBalls = new List<DragonBall>() { _dragonBalls[index] },
            ...
        };
    }
    
    public RadarScreen Visible()
    {
        return new RadarScreen()
        {
            DragonBalls = _dragonBalls.GetVisibleList(),
            ...
        };
    }

    ....
}

相較於上面抽取類別的方法,使用擴充方法來封裝List的操作之後,因為使用的類別還是List,所以也不必特別封裝GetDragonBallAt(int index)的操作了。整體代碼在維持相同的可讀性,但是代碼更簡潔了。

聽起來擴充方法好像是封裝集合操作的首選?其實也不一定,這還是得看實際情況來決定,假設當DragonBalls除了需要增加Add操作之外,還要紀錄最後一個加進List的龍珠。

public class DragonBalls
{
    private readonly List<DragonBall> _balls;
    private const int MaxVisibleDistance = 100;
		private DragonBall _latestDragonBall = null;

    public DragonBalls(List<DragonBall> balls)
    {
        _balls = balls;
    }

		public void AddBall(DragonBall ball)
    {
				_balls.Add(ball);
				_latestDragonBall = dragonBall;
    }

    public IEnumerable<DragonBall> GetVisibleDragonBalls()
    {
        return _balls.Where(ball => ball.Position.GetDistance() < MaxVisibleDistance);
    }

    public DragonBall GetDragonBallAt(int index)
    {
        return _balls[index];
    }
}

當類別除了操作集合之外,還會額外維護一些自己的狀態,此時擴充方法就變得不適合了。因為擴充方法需要static類別,但static類別無法拿來維護狀態。在這種情況下,除了原本的使用DragonBalls組合List之外,我們讓DragonBalls繼承List。

public class DragonBalls : List<DragonBall>
{
    private const int MaxVisibleDistance = 100;
    private DragonBall _latestAddBall = null;

    public void AddBalls(DragonBall dragonBall)
    {
        Add(dragonBall);
        _latestAddBall = dragonBall;
    }
    
    public IEnumerable<DragonBall> GetVisibleList()
    {
        return this.Where(ball => ball.Position.GetDistance() < MaxVisibleDistance);
    }
}

因為繼承了List,所以我們除了能保留List的所有操作之外,還能封裝屬於DragonBalls特有的操作,這個作法的代碼也比原本作法要來得短。

今天聊了兩個封裝集合的方法,加上使用組合的方式,一共有三個方法可以封裝集合。但是這三個作法其實並沒有哪一個比較好,哪一個比較差,更多是依照需求來選擇所需要的作法。

  1. 當需要封裝集合且不需要其他集合的操作,就選擇組合的方式。
  2. 當需要封裝集合且不需要維護其他狀態時,就選擇擴充方法的方式。
  3. 當需要封裝集合且需要其他集合的操作,就可以選擇繼承的方式。

或許還有其他更多的使用使用情境,可以思考一下是不是有更多更好的方式來封裝集合與其相關操作。


上一篇
Day 25 網路上的代碼
下一篇
Day 27 參數過多的問題
系列文
在Kata中尋找Clean Code是否搞錯了什麼30

尚未有邦友留言

立即登入留言