3

我目前正在尝试学习 SOLID 设计原则以及行为驱动开发,但我很难理解单一责任原则。我试图为使用测试驱动开发的 c# 找到一个很好的教程,但在这方面找不到任何有价值的东西。无论如何,在花了几天时间阅读之后,我决定最好的学习方式是通过经验,所以我开始尽我所能使用这些原则创建一个小应用程序。

这是一个简单的保龄球得分计算器。我认为最好的方法是从最简单的部分开始,所以我从球(或投掷)层面开始。现在我已经创建了测试以及处理球(投掷)分数的接口和类,以确保它们不是无效的,即。<10>0。在实现这个之后,我意识到球类基本上只是一个可以为空的整数,所以也许我什至不需要它......但现在它就在那里。

继球之后,我决定添加的下一个合乎逻辑的东西是框架接口和类。这就是我卡住的地方。这是我所在的位置:

namespace BowlingCalc.Frames
{
    public interface IFrame : BowlingCalc.Generic.ICheckValid
    {
        void AddThrow(int? score);
        int? GetThrow(int throw_number);
    }
}

namespace BowlingCalc.Frames
{
    public class Frame : IFrame
    {
        private List<Balls.IBall> balls;
        private Balls.IBall ball;
        private int frame_number;

        public Frame(Balls.IBall ball, int frame_number)
        {
            this.ball = ball;
            this.frame_number = frame_number;
            balls = new List<Balls.IBall>();
            check_valid();
        }

        public void AddThrow(int? score)
        {
            var current_ball = ball;
            current_ball.Score = score;
            balls.Add(current_ball);
        }

        public int? GetThrow(int throw_number)
        {
            return balls[throw_number].Score;
        }

        public void check_valid()
        {
            if (frame_number < 0 || frame_number > 10)
            {
                throw (new Exception("InvalidFrameNumberException"));
            }
        }

    }
}

框架使用我之前实现的球类通过依赖注入将球得分添加到框架中。它还实现了一种方法来返回帧中任何给定球的得分。

我想要做的,以及我被困的地方,是我想添加一种方法来确保帧分数是有效的。(目前只是第 1-9 帧的简单情况,其中两个球的总分必须为 10 或更少。稍后我将继续讨论更复杂的第 10 帧情况。)

问题是我不知道如何以可靠的方式实现这一点。我正在考虑将逻辑添加到类中,但这似乎违背了单一责任原则。然后我想将它添加为一个单独的接口/类,然后在其分数更新时在每一帧上调用该类。我还想过创建一个单独的接口IValidator或类似的东西,并在 Frame 类中实现它。不幸的是,我不知道其中哪一种(如果有的话)是最好的/可靠的做事方式。

那么,什么是一个好的、可靠的方法来实现一个验证帧分数的方法呢?


注意:非常欢迎对我的代码提出任何批评。我很高兴学习创建更好的代码,并且很高兴收到任何帮助。

4

2 回答 2

3

当我想到 SRP 时,我倾向于将重点放在责任方面。反过来,类的名称应该理想地描述其职责。对于某些类,这与该类应该是什么有关(如果 Frame 缺少行为并且仅表示状态,则 Frame 可能是一个很好的示例),但是当您有行为责任时,名称是关于该类应该是什么去做'。

计算分数本身是一个相当小的责任,所以让我们考虑一些稍微大一点、更自然可分解的东西。这是保龄球游戏的一种可能的故障,它具有简单的责任和适当的偏执封装(我们都是这里的朋友,但没有人希望任何人误作弊。)

//My job is to keep score; I don't interpret the scores
public interface IScoreKeeper : IScoreReporter
{
    void SetScore(int bowlerIndex, int frameIndex, int throwIndex, int score);
}

//My job is to report scores to those who want to know the score, but shouldn't be allowed to change it
public interface IScoreReporter
{
    int? GetScore(int bowlerIndex, int frameIndex, int throwIndex);        
}

//My job is to play the game when told that it's my turn
public interface IBowler
{
    //I'm given access to the ScoreReporter at some point, so I can use that to strategize
    //(more realisically, to either gloat or despair as applicable)

    //Throw one ball in the lane, however I choose to do so
    void Bowl(IBowlingLane lane);
}

//My job is to keep track of the pins and provide score feedback when they are knocked down
//I can be reset to allow bowling to continue
public interface IBowlingLane
{
    int? GetLastScore();

    void ResetLane();
}

//My job is to coordinate a game of bowling with multiple players
//I tell the Bowlers to Bowl, retrieve scores from the BowlingLane and keep
//the scores with the ScoreKeeper.
public interface IBowlingGameCoordinator
{
    //In reality, I would probably have other service dependencies, like a way to send feedback to a monitor
    //Basically anything that gets too complicated and can be encapsulated, I offload to some other service to deal with it
    //I'm lazy, so all I want to do is tell everybody else what to do.

    void PlayGame(IScoreKeeper gameScore, IEnumerable<IBowler> bowlers, IBowlingLane lane);
}

请注意,如果您想使用此模型来简单地计算分数(不玩真正的游戏),您可以有一个存根 Bowler(什么都不做)和一个 MockBowlingLane,它产生一系列分数值。BowlingGameCoordinator 负责处理当前的投球手、帧数和投球,因此得分会累积。

于 2012-11-07T01:53:15.183 回答
2

接口的目的是ICheckValid什么?你check_valid在别处打电话吗?在我看来,既然frame_number似乎实际上是 a 的只读属性Frame,为什么在没有任何额外接口的情况下在构造函数中验证它的一致性是错误的呢?(构造函数应该产生一致的对象,因此可以随意验证传入的参数。)

但是,与其问如何正确验证该字段,不如问为什么确实需要?frame_number中的属性。Frame看起来这是该项目在某个数组中的索引 - 您可以只使用索引,为什么将其存储在Frame? 您可能希望稍后编写一些 if/else 逻辑,例如:

if (frame_number == 10) {
     // some rules
} else {
     // other rules
}

但是,这不太可能是一种可靠的方法,因为您最终可能会在Frame. 相反,您可以创建一个基类FrameBase,在其中定义大部分逻辑以及一些要在OrdinaryFrame和中实现的抽象方法TenthFrame,您可以在其中定义不同的规则。这将使您frame_number完全避免 - 您只需创建 9OrdinaryFrames和 1 TenthFrame

至于批评:您的代码似乎抽象了球和框架,但由于某种原因忽略了“投掷”或“滚动”。考虑需要添加每个滚动的轨迹信息,您需要更改IFrame界面,添加类似void SetThrowTrajectory(int throwNumber, IThrowTrajectory trajectory). 但是,如果您在 eg 中抽象丢弃IBallRoll,那么与轨迹相关的功能将很容易适合那里(以及一些布尔计算属性,例如IsStrike, IsSpare)。

于 2012-11-06T09:27:30.533 回答