控制台RPG开发教程10: 继续代码重构

本次教程的内容:

  1. 类的构造函数
  2. 类属性的可见性
  3. 多个源代码文件
  4. 代码重构

抽象思维,就是从看似不同的具体事物中找出共性。
佛说,离一切相,即一切法。


初建成功的地图工作组成员认为精灵大人对自己感到很满意,毕竟现在主程序的代码已经那么短了。几乎所有具体工作都由地图工作组来完成了,精灵大人只须打几个电话指挥一下就好。
谁知当记者去采访的时候发现,其实精灵大人一点都不满意:后面的工作还有很多等着本精灵去做,我还得打那么多电话去指挥你一个具体工作组的工作,又是初始化,又是显示英雄,又是开始监听。我希望叫到它们的时候就一个电话:干活。

既然如此,让我们来进一步优化一下地图工作组的代码。

我们在上节课看到,当精灵创建了地图工作组后,它首先做的事情就是调用Init()函数对它进行初始化。几乎所有的对象,在工作前都会有一个初始化的需求。于是c++语言为所有的类,设计了一个共同的初始化函数:构造函数。

构造函数的名字和类名相同,没有返回值,在创建对象时自动调用。
我们把初始化的工作放在这里,这样,调用Init()初始化的步骤就可以省略了。

    Map(){
        mapInfo[0]= "■■■■■■■■■■■■";
        mapInfo[1]= "        ■■          ■";
        mapInfo[2]= "■  ■    ■■  ■■  ■";
        mapInfo[3]= "■  ■■        ■      ";
        mapInfo[4]= "■■■■■■■■■■■■";
        x= 0;
        y= 1;
    }


然后我们增加一个work()函数

    void work(){
        showMaop();
        showHero();
        listen();
    }

    
把工作展开的过程也封装起来了。


这样主程序的代码精简到了只有三行,其中与地图工作组直接相关的只有两行。
一行创建对象,一行开始工作。

int main(){
    HideCursor(); 
    Map map1;
    map1.work();
}

这次,精灵大人感觉怎么样呢?记者又一次采访了精灵。

记者:请问您对地图工作组最近的工作还满意么?
精灵:满意?这么小的一张地图翻来覆去走了这么多天,你问我满意不满意?
记者:哦,我相信更多的地图已经在研制中了。请问对它们其他方面的工作情况还有什么意见么?
精灵:其他方面,你去看看它们的listen函数,足足有40行代码,怎么就不能再优化一点?
记者:咳咳,我相信地图工作组的代码优化工作仍在持续地进行中。我听说地图工作组经过努力把外部的调用代码减到了最少?
精灵:我的主程序固然是精简一些了。但是打开文件后,整个代码仍然很长,看起来很乱。你看看控制台工作组做了那么多事情,它在我的代码中只多占了一行的位置include <conio.h>。
记者:我想我弄明白您的意思了。让我们共同期待地图工作组进一步改进它们的工作。

看来想得到精灵大人的肯定还没那么容易。

当我们开始设计一个具有一定复杂度的软件,首先必须掌握的就是化解复杂性的方法。
一个常见的方法就是模块化。
把一个复杂的工作拆分成很多相对比较小的模块化工作。先构建起较小的模块,然后用这些小模块逐渐搭建起更大的模块。直到建设成一个完整的系统。

在整个过程中,我们应当一直对每个模块的复杂性有一个平衡度的把握。如果感觉到一个模块可能过于复杂了,我们就应当建立起一种方法,将它做进一步的拆分。

比如现在的主程序,里面明显包含了两部分内容,一个是主程序,一个是地图工作组。如果能把它们分开,代码会显得更加清晰。

include语句正是用来做这件事的。
我们前面见的include语句,都是后面跟一个用一对尖括号<>包围的名字,这些往往对应着系统所提供的标志库(工作组)。
现在我们自己也开发了一个工作组,也就是建立一个自己的库。这种库也可以用include来引用,一般都用""双引号来标志。
现在我们把现有的代码一分为二,

  • 一个是从main函数开始下面的部分,保存文件名为rpg.cpp。
  • 另一个是main函数以上的所有部分,保存文件名为map.cpp。

然后在rpg.cpp中加一句引用

#include "map.cpp"

至于class Map中的函数listen,确实有点过长了。

函数复杂性的一个标准,就是代码的行数。这里并没有硬性的标准,有个一般的建议是一个函数的长度最好不超过一个屏幕。这样当我们编辑和修改这个函数时,可以不用到滚动条。
怎样缩短一个函数呢?
一个简单的方法就是通过把一些功能直接剥离出来形成新的函数,比如main函数的缩短。
另一种高级一些的方法是找到相似的结构,把它们提炼成函数,避免重复。比如listen函数就是这样。
处理四个方向的移动都有一个共同的逻辑结构:

  1. 明确新的目标位置
  2. 判断目标位置是否允许移动
  3. 如果可以
  4. 隐藏英雄
  5. 把英雄的当前位置改为新的目标位置
  6. 显示英雄

现在的代码中,这套逻辑重复了4遍,如果我们把它提炼为一个函数,可以更加精炼。

完整源代码如下:

map.cpp

# include <iostream>
# include <conio.h>
# include <windows.h>

using namespace std;

void HideCursor(){
    CONSOLE_CURSOR_INFO cursor_info = {1, 0};
    SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cursor_info);
}

void gotoxy(unsigned char x,unsigned char y){
    COORD cor;
    HANDLE hout;
    cor.X = x;
    cor.Y = y;
    hout = GetStdHandle(STD_OUTPUT_HANDLE);
    SetConsoleCursorPosition(hout, cor);
}

class Map{
    string mapInfo[5];
    int x, y;    
    void showMap(){
        for (int i=0; i< 5; i++){
            cout << mapInfo[i]<< endl;
        }
    }
    void showHero(){
        gotoxy(x,y);
        cout << "♀";
    }
    void hideHero(){
        gotoxy(x,y);
        cout << "  ";
    }
    void tryMove(int ax, int ay){
        hideHero();
        if (mapInfo[ay][ax]== ' ') {
           x= ax;
           y= ay;
        }
        showHero();
    }
    void listen(){
        int a;
        while(1){
            a= getch();
            if (a==72) {
               // 向上 
               tryMove(x, y-1); 
            }
            if (a==80) {
               // 向下 
               tryMove(x, y+1); 
            }
            if (a==75) {
               // 向左 
               tryMove(x- 2, y); 
            }
            if (a==77) {
               // 向右 
               tryMove(x+ 2, y); 
            }
        }
    }
  public:
    Map(){
        mapInfo[0]= "■■■■■■■■■■■■";
        mapInfo[1]= "        ■■          ■";
        mapInfo[2]= "■  ■    ■■  ■■  ■";
        mapInfo[3]= "■  ■■        ■      ";
        mapInfo[4]= "■■■■■■■■■■■■";
        x= 0;
        y= 1;
    }
    void work(){
        showMap();
        showHero();
        listen();
    }
}; 

我们看一下,增加了一个tryMove函数,不但使得listen函数更加简短,而且让它的代码含义也更加清晰了。另外,充分利用了已经定义的显示英雄函数showHero,以及模仿showHero创造了hideHero函数,都有助于让代码的含义更加清晰。
后面我们会看到,还有手段让代码的含义更加清晰。
对于一个复杂项目来说,在代码可读性上的努力终究会物有所值的。

最后把变量定义和所有函数都放到了public: 标记的前面。保证只有构造函数Map和work函数能够被外界看到。

rpg.cpp

//# include <iostream>
//# include <conio.h>
//# include <windows.h>
# include "map.cpp"

using namespace std;

int main(){
    HideCursor(); 
    Map map1;
    map1.work();
}

主程序极大地精炼到了。由于现在几乎所有的工作都来自map.cpp中的定义,甚至连原有的几个include都可以取消了。但是考虑到我们将来或许还会用到,所以暂时把它们注释起来,以备后用。这也是注释这一功能的常见用法之一。

后来记者又一次采访了精灵。
记者:您看现在的代码怎样?
精灵:非常好!这么简短的源代码,真是让人干劲十足啊!
记者:听说地图工作组还优化了listen函数的实现方法。
精灵:什么listen函数?我不清楚,也不关心,那是它们自己的事情。
记者:嗯,听说它们还在准备引入更多、更大、更好看的地图!
精灵:想法很好,不过设计地图不适合它们来做,我已经另有人选了。它们当前的重点工作是业务逻辑还差得太远。
记者:哦哦,您对地图组还有什么建议么?
精灵:每次修改,保存文件后,到主程序这边来运行。
记者:明白了,无论代码有多少,主程序的地位仍不能取代。感谢您接受采访。


课程小结:

本节课讲了类的构造函数和类函数的可见性,介绍了一些代码优化的方法,以及多源文件协作的方法。

发布了24 篇原创文章 · 获赞 0 · 访问量 4572

猜你喜欢

转载自blog.csdn.net/xiaorang/article/details/104872920