SUMTEC -- There's a thing in my bloglet.

But it's not only one. It's many. It's the same as other things but it exactly likes nothing else...

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::
  263 随笔 :: 19 文章 :: 3009 评论 :: 74万 阅读
< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

    自从上一次发布有点技术含量的东西以来已经3个多月过去了,在这么长的时间里没有更新自己的blog,真是感到非常的难过。还好,一切渐渐开始平复下来,不再忙得每天必需要有25个小时了。当然,现在仍然非常期望一天能够有25个小时。:)

    今天的文章虽然是华容道的系列,但是现在开始是sumtec一个人在独立工作,因此不会有对话内容了。取而代之的是一个思考的过程,会有很多问题和“答案”。好了,废话不多说,让我们开始吧!

    要开始写一个程序,应该怎么开始呢?首先先写一个棋子吗?或者先将算法写出来?不,也许前面我已经说过了,写一个程序不应该从这个方向思考,更不应该从这个方向开始写程序。因为你这样写出来的“棋子”对于最终的“游戏”来说,可能并不是最适应的——可能有些需要的功能有所欠缺,这种情况比较容易挑动大家的痛苦神经。然而还有另一种可能性:那就是你开发了一些其实永远也用不着的东西,这看起来无伤大雅,但是却是暗藏危机。比如说当你开始进行版本升级的时候,这些看似无用的东西你是否也得跟着升级呢?不升级可能意味着新的版本里面已经开始滋生霉菌了,升级的话又有可能在为永远用不上的东西耗费精力。那么应该怎么开始呢?应该从游戏本身开始!

    假设我们开始写一个游戏,那么应该这么写:

   IGame game = InitializeGame();
   PlayTheGame(game);
   PrintRecords(game); 

    让我们先来说说,假如我们现在手上就有一个华容道的玩具,我们是怎么开始玩的呢?首先我们把这个“玩具”找出来摆在桌子上,然后对着说明书或者别的什么开始摆“棋盘”,然后我们就参与其中开始玩起来了。真实的游戏是如此,电脑里的游戏也大概差不多。首先需要有一个“游戏”作为总体大环境的载体,然后我们需要往游戏里面摆棋盘,再下来是指定游戏的参与者,接着就是开始玩游戏,最后呢就是打印了。 

    这就是整个游戏的“全部”了。很奇怪吧?如果认真看过上一次释放的源代码,那么你一定会更加奇怪:“那个代码算是什么东西嘛,都不能够求出最终解的!”如果真的这么认为,那是因为我还没有开始讲解其中的奥妙。

    首先让我们想象一下:Anders Hejlsberg 已经帮你设计好InitializeGame、PlayTheGame以及PrintRecords背后所需要的一切,而且MCS部门带领他们的v-部队将代码全部搞定了,这段程序看起来仍然那么荒谬和可笑吗?不,在我看来一点也不荒谬,本来一切就应当是这样。回到现实当中,我们现在不就是缺少Anders Hejlsberg的架构和设计,以及v-部队所做的编码工作吗?那么接下来我们要做的就是如法炮制IntializeGame等部分内部的内容,直至整个程序写完。

    也许你会说,这个迭代的结果看起来仍然是非常奇怪,因为最终运行的结果仅仅是打印了一个棋盘,完全不是一个游戏。然而我需要说的是,你应该注意到这个程序不管是多么的弱智,也没有完成最终的目标,但是它确实是可以运行的。而你更应该注意的是,这个迭代的代码并非一大堆的txResult.Text += "|S|V|H..."这样的直接输出代码,背后已经有了一个较为完整的框架,思路也已经展开了70%了。所缺乏的只是一些具体发挥真实功能的代码,以及一些相对细节的设计,不是吗?即使是上面那三行的代码,也已经将整个要做的事情简要的说出来了:1、我需要有一个游戏;2、我要玩;3、最后你把结果给我打印出来。 

    写完上面那三行代码,接下来就得往下面一个层次些,细化具体的内容了。活于现实中的人是不能奢望时时刻刻都会有v-部队的支援的。

    首先要写的是InitializeGame,下面是我的思路:

        private IGame InitializeGame()
        
{
            IGame game 
= new HrdGame();
            ILayout startLayout 
= HrdLayoutFactory.Create("HRD1");
            IPlayer computer 
= HrdAlgorithmFactory.Create("Sumtec");

            game.InitializeLayout(startLayout);
            game.AddPlayer(computer);

            printLayout(startLayout);

            
return game;
        }



    上面的这一段代码属于游戏的初始化部分,理所当然地,应该包括“构造游戏”、“设定棋盘”以及“指定游戏参与者”。当然,这一份代码并不是非常完美,因为这里多了一个“输出棋盘”的调用。这一个代码是调试过程中“插入的”代码,不过这也无伤“大雅”,大家在后面的迭代中会看到这一有伤“小雅”的尾巴会被割掉。

    看到这里,大家会发现用了工厂模式,也许在后面陆续还会看到一些其它的模式。然而事实上我却不想在这里讨论太多有关模式的事情,我希望在这里讨论的更多的是,为什么棋盘的定制为什么不是Game对象内部的问题。这一个问题不知道大家有没有注意,我当时是注意到这个问题,并花了一点时间仔细思考了一下,最后做出了这个设计。也许大家会觉得上面的代码这么写会比较好:

game.AddLayout("HRD1");

    可是这样写会有一些比较麻烦的问题,比如说日后需要调整添加棋盘逻辑的时候,是不是需要对game进行一项手术呢?再比如说,如果这么进行设计,那么是否拥护的添加逻辑也应该封装到game内部呢?以后可能还会有更多的元素(比如说游戏规则,裁判……),这些元素的添加逻辑是否也应该封装到里面去呢?实际上在考虑这一类问题的时候,我们要记住一个“单一指责原则”——任何一个类最好只负责一件事情。稍微跑题远一点,假如说一个前端控制器在负责“扳道岔”的功能之外,还负责了往页面填充数据的功能,你一定会非常不爽。打个比方,假如一列火车在遇到道岔的时候必须同时在那里“加油”,并且也只能够在那里加油,那么问题就会很严重——如果火车想加油,那就必须满世界找岔道口;如果方圆一百里没有岔道口还就歇菜了。
    其实这里的game它只有一个作用,就是作为游戏本身的载体。你给他一个棋盘布局,然后找两个人过来就可以开始玩了。至于说棋盘布局是如何得到的,人是哪儿找来的,应该与游戏完全无关,这些工作应该有相应的类来完成,在这个示例里面就是由一个HrdLayoutFactory来完成的。

    接下来我们开始写PlayTheGame,这也很简单,没有什么值得说明的:

        private void PlayTheGame(IGame game)
        
{
            game.Play();
        }


    而PrintRecords也很简单:

        private void PrintRecords(IGame game)
        
{
            ICollection records;
            records 
= game.Records;

            HrdRecordTextPrinter.Print(records);
        }


    但是这里就有点不一样了,前面几个步骤都是以game为中心的,而这里虽然用到了game,但是实际上的工作是HrdRecordTextPrinter来完成。为什么这里会采取与吕震宇不一样的工作方式呢?理由跟前面的一样,也是从职责的角度来考虑的。打印结果本身并非游戏的一部分,假如打印游戏结果被添加到游戏当中那会遇到什么问题呢?比如我们某天不想用这种比较弱智的文本方式来显示,那就需要对game本身作出改动,这显然是不合理的。而如果某天我们希望去除打印的功能,也许还会遇到拔除代码的过程,这也显然是不合理的。 

    写到这里,我们又描述完一个层次的问题了。接下来就要开始写game里面的东西了,因为只有我们将game描述清楚了,才有可能知道Layout应该长什么样,除了Layout还有什么东西等等问题。现在急于写Layout实际上并不明智。

    然而今天已经没有时间了,哎,好好的一个星期天,才开始写了不到一半,公司就有事情要做了,于是只好留着半截在肚子里面了。(顺便说一下,这个系列注定会很长,也注定不会写了一半写不下去的。因为整个系列的代码都已经写好了,一共经历了5次迭代,并且这5次迭代的代码都分别保留。而目前只放出了第一个迭代的代码,可见后面还有多少东西可以写。)
posted on   Sumtec  阅读(3009)  评论(3编辑  收藏  举报
编辑推荐:
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
阅读排行:
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· .NET周刊【3月第1期 2025-03-02】
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
点击右上角即可分享
微信分享提示