了解SOLID设计的单一责任原则

时间:2012-11-06 08:16:36

标签: c# oop single-responsibility-principle

我目前正在尝试学习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个案例之后转向更复杂的框架。)

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

那么,实现一种验证帧分数的方法会有什么好的SOLID方法呢?


注意:对我的代码的任何批评都是非常受欢迎的。我很高兴学会创造更好的代码,并乐意接受任何帮助。

2 个答案:

答案 0 :(得分:3)

当我认为SRP时,我倾向于强调责任方面。反过来,该类的名称应理想地描述其责任。对于某些类,这是关于类应该“成为”的内容(如果缺少行为并且仅代表状态,框架可能是一个很好的例子),但是当你有行为责任时,名称是关于类的假设'做'。

计算分数本身是一个相当小的责任,所以让我们考虑一些稍微大一些,更自然可分解的东西。以下是保龄球比赛的一个可能细分,其中包含简单的责任和适当的偏执封装(我们都是这里的朋友,但没有人希望任何人有人作弊。)

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

请注意,如果您想使用此模型简单地计算得分(不玩真实游戏),您可以拥有一个存根保龄球(谁什么也不做)和一个产生一系列得分值的MockBowlingLane。 BowlingGameCoordinator负责当前的投球手,投篮和投球,因此积分得以累积。

答案 1 :(得分:2)

ICheckValid界面的目的是什么?你在其他地方打电话给check_valid吗?在我看来,由于frame_number似乎实际上是Frame的只读属性,为什么在构造函数中验证其一致性而没有任何其他接口是错误的呢? (构造函数应该生成一致的对象,因此可以随意验证传入的参数。)

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

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

但是,这不太可能是SOLID方法,因为您可能最终会在Frame的许多部分中编写此if / else语句。相反,您可以创建基类FrameBase,定义大部分逻辑以及在OrdinaryFrameTenthFrame中实现的一些抽象方法,您可以在其中定义不同的规则。这样您就可以完全避免frame_number - 您只需创建九个OrdinaryFrames和一个TenthFrame

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