自从上一次发布有点技术含量的东西以来已经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,下面是我的思路:














上面的这一段代码属于游戏的初始化部分,理所当然地,应该包括“构造游戏”、“设定棋盘”以及“指定游戏参与者”。当然,这一份代码并不是非常完美,因为这里多了一个“输出棋盘”的调用。这一个代码是调试过程中“插入的”代码,不过这也无伤“大雅”,大家在后面的迭代中会看到这一有伤“小雅”的尾巴会被割掉。
看到这里,大家会发现用了工厂模式,也许在后面陆续还会看到一些其它的模式。然而事实上我却不想在这里讨论太多有关模式的事情,我希望在这里讨论的更多的是,为什么棋盘的定制为什么不是Game对象内部的问题。这一个问题不知道大家有没有注意,我当时是注意到这个问题,并花了一点时间仔细思考了一下,最后做出了这个设计。也许大家会觉得上面的代码这么写会比较好:
game.AddLayout("HRD1");
可是这样写会有一些比较麻烦的问题,比如说日后需要调整添加棋盘逻辑的时候,是不是需要对game进行一项手术呢?再比如说,如果这么进行设计,那么是否拥护的添加逻辑也应该封装到game内部呢?以后可能还会有更多的元素(比如说游戏规则,裁判……),这些元素的添加逻辑是否也应该封装到里面去呢?实际上在考虑这一类问题的时候,我们要记住一个“单一指责原则”——任何一个类最好只负责一件事情。稍微跑题远一点,假如说一个前端控制器在负责“扳道岔”的功能之外,还负责了往页面填充数据的功能,你一定会非常不爽。打个比方,假如一列火车在遇到道岔的时候必须同时在那里“加油”,并且也只能够在那里加油,那么问题就会很严重——如果火车想加油,那就必须满世界找岔道口;如果方圆一百里没有岔道口还就歇菜了。
其实这里的game它只有一个作用,就是作为游戏本身的载体。你给他一个棋盘布局,然后找两个人过来就可以开始玩了。至于说棋盘布局是如何得到的,人是哪儿找来的,应该与游戏完全无关,这些工作应该有相应的类来完成,在这个示例里面就是由一个HrdLayoutFactory来完成的。
接下来我们开始写PlayTheGame,这也很简单,没有什么值得说明的:





而PrintRecords也很简单:








但是这里就有点不一样了,前面几个步骤都是以game为中心的,而这里虽然用到了game,但是实际上的工作是HrdRecordTextPrinter来完成。为什么这里会采取与吕震宇不一样的工作方式呢?理由跟前面的一样,也是从职责的角度来考虑的。打印结果本身并非游戏的一部分,假如打印游戏结果被添加到游戏当中那会遇到什么问题呢?比如我们某天不想用这种比较弱智的文本方式来显示,那就需要对game本身作出改动,这显然是不合理的。而如果某天我们希望去除打印的功能,也许还会遇到拔除代码的过程,这也显然是不合理的。
写到这里,我们又描述完一个层次的问题了。接下来就要开始写game里面的东西了,因为只有我们将game描述清楚了,才有可能知道Layout应该长什么样,除了Layout还有什么东西等等问题。现在急于写Layout实际上并不明智。
然而今天已经没有时间了,哎,好好的一个星期天,才开始写了不到一半,公司就有事情要做了,于是只好留着半截在肚子里面了。(顺便说一下,这个系列注定会很长,也注定不会写了一半写不下去的。因为整个系列的代码都已经写好了,一共经历了5次迭代,并且这5次迭代的代码都分别保留。而目前只放出了第一个迭代的代码,可见后面还有多少东西可以写。)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 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——大语言模型本地部署的极速利器