Virtual Disk 项目总结

来源:互联网 发布:淘宝店怎么发布宝贝 编辑:程序博客网 时间:2024/05/23 15:35

虚拟磁盘项目总结


前言

一个迭代,两个星期,三回重构;
从泛型 N 叉树到 N 叉树到无树;
从数据抽象到问题抽象到全局抽象;
代码量从长到短到长,从大作业到项目的转变。

PS:最底下有福利哦


第一次设计

第一次设计,老大只给了我一天的时间去分析和设计,所以没有非常细致的去考虑。而且之前从未接触过类似的需求,所以很多设计都是无用功。

老大总共给了我一周的时间去实现需求。

第一次设计概述

第一次设计奠定了后来所有设计的思路基础,即设计的大体方向不变,只是后来的设计更加细致和全面。

第一次设计,程序的流程如下:

打开程序
启动 VDos
在第一次使用 DiskSystem 时初始化
等待输入
解析输入
得到操作指令句柄
执行操作
等待输入
释放资源

其中,一个文件一定对应一个 N 叉树的结点,不管它是什么文件还是文件夹。

第一次设计中的不足

  • 指令解析直接通过流式读取(线性处理方式),没有从全局角度考虑(段式处理)
    所谓流式读取,就是一个字符一个字符处理,如果你能明白,就不用看了
// 为了缩减文章行数,代码中有部分省略或格式上的处理void StringParser::parseString(const string& str) {    auto it = str.begin(); // 噩梦的开始    // fault tolerence    while(*it == ' ') { it++; }    // command    auto itStart = it;    do { it++; } while(*it != ' ' && it != str.end());    m_command = string(itStart, it);    // fault tolerence: no arguments no targets    if(it == str.end()) { return; }    // fault tolerence: space between command and arguments    while(*it == ' ') { it++; }    // arguments    while(*it == '/') { // argument is same like: /d /s /r /...        itStart = it;        do { it++; } while(*it != ' ' && it != str.end());        m_arguments.push_back(string(itStart, it));        // fault tolerence: no arguments no targets        if(it == str.end()) { return; }        // fault tolerence: space between arguments        while(*it == ' ') { it++; }    }    // fault tolerence: no arguments no targets    if(it == str.end()) { return; }    // fault tolerence: space between arguments and targets    while(*it == ' ') { it++; }    // targets    itStart = it;    do {        it++;        if(*it == ' ') {            m_targetList.push_back(string(itStart, it));            // fault tolerence: no arguments no targets            if(it == str.end()) { return; }            // fault tolerence: space between targets            while(*it == ' ') { it++; }            itStart = it;        }    } while(it != str.end());    m_targetList.push_back(string(itStart, it));    // parsePath();}
  • 指令执行带入的必要参数仍然是字符串集合,意味着在每一个指令的执行过程中仍然需要再一次解析这些字符串。
void command(vector<string> arguments, vector<string> targets);
  • 将当前位置绑定到文件系统上。原本应该是一个 dos 界面一个当前位置,我现在整个绑定到 DiskSystem 上了
// 代码有省略class DiskSystem {public:    void setCurrentNode(NTree<File>* node); // forward call    NTree<File>* getCurrentNode(); // forward call    string getCurrentPath();private:    DiskPartition* m_currentPartition = nullptr;    string m_currentPath;};
  • 分区抽象概念与 NTree 的功能重复了。
class DiskPartition {public:    DiskPartition();    DiskPartition(NTree<File>* node);    ~DiskPartition();    void createFile(File file, NTree<File>* ntree);    void removeFile(File file, NTree<File>* ntree);    void setPartitionName(string name);    void setPartitionNode(NTree<File>* ntree);    void setCurrentNode(NTree<File>* node);    void changeFileName(NTree<File>* node, string name);    string getPartitionName() const;    NTree<File>* getPartitionNode();    NTree<File>* getCurrentNode();    // search from specific node    NTree<File>* searchFile(File file, NTree<File>* ntree);    void traversalPartition();private:    string m_partitionName;    NTree<File>* m_partitionNode = nullptr;    NTree<File>* m_currentNode = nullptr;};
  • Printer 类为了使用策略模式,完全没有必要。
class Printer {public:    virtual void printResult() = 0;};class CommandDIRPrinter : public Printer {public:    CommandDIRPrinter(vector<File> elements) : m_elements(elements) {}    virtual void printResult();private:    vector<File> m_elements;};
  • 没有将文件划分为更加细致的类,而只是做了一个单层的抽象 File,保存所有的数据。
class File {public:    File();    File(string name, FileType type);    File(const File& file);    File(File&& file);    ~File();    string getPath() const;    string getFileName() const;    string getFileDate() const;    string getFileTime() const;    string getFileInfo() const;    FileType getFileType() const;    size_t getFileSize() const;    void setFileName(string name);    void setFileType(FileType type);    File& operator= (const File& file);    friend bool operator== (const File& lFile, const File& rFile);    // NOTE: for STL map, cannot declear as member function    friend bool operator< (const File& lFile, const File& rFile);    friend ostream& operator<< (ostream& os, const File& file);private:    string m_path;    string m_name;    string m_date;    string m_time;    string m_info;    FileType m_type;    size_t m_size = 0;    void* m_dataPtr = nullptr;};
  • 对于一个文件系统中,一定要有一个 n 叉树的数据结构,所以就抽象出了一个 TNTree 作为底层的数据结构,并且考虑到将来代码的复用性,将 TNTree 设计成泛型的形式。(你没看错,这就是不足)
template <typename T>class NTree {public:    NTree();    NTree(const T& value); // lvalue    NTree(T&& value); // rvalue    ~NTree();    void addNTreeChild(NTree<T>* ntree);    void removeNTreeChild(NTree<T>* ntree);    void setNTreeValue(T value);    void setNTreeParent(NTree<T>* ntree);    T& getNTreeValue();    NTree<T>* getNTreeParent();    typename map<T, NTree<T>*>::iterator searchNTree(T value);    map<T, NTree<T>*>& getNTreeChildren();private:    T m_value;    NTree<T>* m_parent;    map<T, NTree<T>*> m_children;};
  • 内存池设计,什么鬼东西?
class MemoryPool {public:    static MemoryPool* getInstance();    static void destoryInstance();private:    MemoryPool();    ~MemoryPool();    static MemoryPool* m_instance;    allocator<File> m_allocator;    void* m_startPtr = nullptr;};
  • 应用类型擦除技术保存文件数据,又是什么鬼东西?

第一次设计吐槽

很快,一周的时间过去了,做完大部分需求之后,拿去给老大看,然后老大指派了一位师傅去 review 我的代码。(重要的不是准时,而是完成需求,你可以 delay,但是你得完成需求,准时完成是锦上添花

师傅让我给他讲了讲设计,然后代码稍微翻了翻,就说出一堆设计中的不足(师傅说他从没做过这个东西)。

师傅的师傅(听说是某知乎大神)也在旁边凑热闹,看到某一处代码,他捋了捋杂乱的胡子,露出邪恶的笑容(以上纯属想象),直接运行程序,敲了某几个字符后程序崩溃……

当时不以为然,觉得那块没做好。后来细想……难道祖师爷是随便看了一眼就找出代码中的问题,然后直奔那个问题输入一个可以让程序崩溃的内容。卧槽,这得有多深厚的功力……

然后被师傅和祖师爷各种吐槽我代码上的错误(不是逻辑错误),比如这个地方没用 const 修饰,那个地方没用引用返回,这函数的输入值还能当输出……

最后硬是被师傅认为我的语法不过关,说我既然使用 C11,那就要熟练地使用 C11,你现在连 C11 都不熟练,搞得两头都不好(我之前给师傅说我熟悉 C11,不熟悉也不喜欢 C99)

说真的,这个“语法不过关”的评语,师傅啊,扎心了。我自认为学 C++ 两年多,上到MP,下到OOP,随手一发FP,时刻不离GP,自信在 C++ 四大范式中只要不是特别偏门的东西我都会一些……


第二次设计

第二次设计,我以为没有了一周的时间限制,所以慢慢悠悠重新去做,吸取第一次的教训,听从师傅的教诲。(其实是没忘师傅的吐槽:))

第二次设计概述

程序的大体方向不变,在细节上改进了部分设计内容。第二次设计基本决定了大部分设计思路。

改进的地方

  • 字符串采用全局处理方式,用正则表达式提取输入字符串中的有效信息
// 代码有省略void StringParser::parse(const string& inputString, NTree* currentNode){    vector<CommandPath> commands;    string s = inputString;    regex regex_str("\\S+");    smatch sm_str;}
  • 在第一次设计中,对每一个命令需要计算命令对象的路径,将路径算法封装提取出来做成全局算法,供命令调用(降低了单个命令与字符串处理功能的耦合度)
NTree* calculateNode(NTree* currentNode, string path, function<NTree*(NTree*, string)> func, function<NTree*(NTree*, string)> targetFunc);
  • 使用工厂模式根据解析出来的内容创建命令对象
// 代码有省略class CommandFactory{public:    function<bool(const CommandPath& commandPath, NTree*& currentNode)>& createCommand(string command);private:    unordered_map<string, function<bool(const CommandPath& commandPath, NTree*& currentNode)>> m_commandTable = {        {"dir", &DIRCommand}, // 省略    };    function<bool(const CommandPath& commandPath, NTree*& currentNode)> error = &ERRORCommand;};
  • 将命令的传入参数包装,而不是单纯的字符串容器的形式
struct CommandPath {    string command;    string argument;    string path;    NTree* node;};bool command(const CommandPath& commandPath, NTree*& currentNode);
  • 文件体系抽象
class File{public:    File();    File(const string& name);    File(File&& file);    virtual ~File() = 0;    string getDate() const;    string getTime() const;    size_t getSize() const;    string getName() const;    string getInfo() const;    FileType getType() const;    void setName(string name);protected:    string m_date;    string m_time;    size_t m_size = 0;    string m_name;    string m_info;    FileType m_type;};class GeneralFile : public File// 省略class DirectoryFile : public File// 省略class SymbolGeneralFile : public GeneralFile// 省略class SymbolDirectoryFile : public DirectoryFile// 省略
  • TNTree 退化成 NTree,不再使用模板,不再使用智能指针和裸指针混合,统一成裸指针操作
class NTree{public:    NTree();    NTree(File* filePtr);    ~NTree();    void addChild(NTree* node);    void removeChild(string key);    void setFile(File* filePtr);    void setParent(NTree* node);    File* getFile();    NTree* getParent();    typename map<string, NTree*>::iterator search(string name);    std::map<string, NTree*>& getChildren();private:    File* m_filePtr = nullptr;    NTree* m_parent = nullptr;    map<string, NTree*> m_children = { {".", this}, {"..", m_parent} }; // FIXME: GeneralFile should not have these};
  • 去掉了 DiskPartition 这一层

第二次设计中的不足

  • 命令的执行流程和路径解析流程仍然没有区分开
// 以 CD 命令为例bool CDCommand(const CommandPath& commandPath, NTree*& currentNode){    cout << "cmd:" << commandPath.command << endl;    cout << "arg:" << commandPath.argument << endl;    cout << "tar:" << commandPath.path << endl;    auto wrongPathFunc = [](NTree* currentNode, string findPath) {        return nullptr;    };    auto gotoNodeFunc = [](NTree* currentNode, string findPath) {        auto it = currentNode->search(findPath);        if(it != currentNode->getChildren().end())        {            // NOTE: generalFile            auto type = it->second->getFile()->getType();            switch (type)            {                case FileType::symlink:                case FileType::generalFile:                {                    cout << "The directory name is invalid." << endl;                    currentNode = nullptr;                    break;                }                case FileType::directoryFile:                {                    currentNode = it->second;                    break;                }                case FileType::symlinkd:                {                    // 未实现                    break;                }            }        }        else        {            currentNode = nullptr;        }        return currentNode;    };    // TODO: deal commandPath.path in CD command, change folder\ to folder    auto path = commandPath.path;    if(path.find_last_of("\\") != string::npos)    {        if(path.find_last_of("\\") == path.size()-1)        {            path = path.substr(0, path.size()-1);        }    }    auto afterCalculationNode = calculateNode(currentNode, path, wrongPathFunc, gotoNodeFunc);    if(afterCalculationNode != nullptr)    {        // NOTE: change currentNode        currentNode = afterCalculationNode;    }    else    {        // NOTE: output error message        cout << "The system cannot find the path specified." << endl;        return false;    }    return true;}
  • 路径计算算法中包含了命令执行的功能
NTree* calculateNode(NTree* currentNode, string path, function<NTree*(NTree*, string)> func, function<NTree*(NTree*, string)> targetFunc);// func 和 targetFunc 可能含有命令的执行功能
  • 通配符的处理仍然写在命令的实现中

  • 一个文件对应一个 NTree 结点,那么必然有一个一般文件(非文件夹文件)含有 addChild 功能

第二次设计感想

第二次设计,相当于一次重构,用了四天的时间重构所有的代码,但是感觉在实现命令的时候仍然非常不顺畅,一旦出现附加功能就得附加更多的代码或者结构去弥补这些功能和错误什么的。

而且感觉职责仍然很分散,或者重复。


第三次设计

第三次设计可以说是一次飞跃式的突破,因为不再使用 NTree 了,所以说 NTree 是一个糟糕的设计,完全没必要,而且局限思维。因为我所有的实现最终都要考虑回到 NTree,而中间还有得有一层文件,甚至出现了 GeneralFile 可以 addChild 的奇葩情况。

第三次设计概述

大体流程不变,将 NTree 与 File 类族整合,对 DirectoryFile 类族才提供 addChild 功能。

改进的地方

  • 整合 Ntree 与 File 类族
class DirectoryFile : public File{public:    DirectoryFile();    DirectoryFile(const string& name);    virtual ~DirectoryFile();    DirectoryFile* getParent() const;    void addChild(File* file);    void removeChild(const string& name);    map<string, File*>& getChildren();    typename map<string, File*>::iterator search(const string& name);private:    map<string, File*> m_children = { {".", this}, {"..", nullptr} };};

第三次设计中的不足

  • 与第二次一样,除了 NTree 和 File 部分

第三次设计感想

第三次设计,第二次重构,突破繁琐的调用接口的限制(NTree),用了两天的时间完成。

但是在命令实现部分仍然跟第二次一样,混乱。不过不再出现类职责分散或者重复的感觉了。


第四次设计

在前几次设计过程中,不停地在跟师傅讨论设计中的缺陷和不足,师傅总是说这么一句话:“我觉得这样可以,至于要不要这么做,看你自己”。当时没懂他为什么要这么强调,直到……

第四次设计概述

第四次设计又是一次飞跃,应用了 MVC 将软件划分模块。

Model 处理核心数据,即 File 类族和一些其他的数据抽象。

View 用作交互处理,即显示信息处理,主要是 VDos 和固定的错误信息。

Controller 是处理数据部分以及软件的骨架。

改进的地方

  • 在 Model 中更加细致化了待处理的实体,重新思考了命令的操作形式,提出了以下几个操作实体和新的命令格式
Msg command(queue<Object> objects);// 操作的交互信息class Msg{public:    Msg(bool successful = false, string msg = "", DirectoryFile* directoryFile = nullptr);    ~Msg();    bool m_successful;    string m_msg;    DirectoryFile* m_directoryFile = nullptr;};// 操作的操作对象class Object{public:    Object(const Path& path, vector<string> arguments, DirectoryFile* directory);    Path m_path;    vector<string> m_arguments;    File* m_file = nullptr;    File* m_fileParent = nullptr;    static bool createMode;};// 操作对象的路径class Path{public:    Path(const string& pathStr); // pathStr seems like: folder\folder\folder    queue<string> m_pathQueue;};
  • 参数化错误信息,不再分散在各个命令中
const string errorCmdMessage = "Command is not recognized as an internal or external command, operable program or batch file.";const string errorSyntaxMessage = "The syntax of the command is incorrect.";const string errorDirMessage = "System can not find specify directory.";const string errorFileMessage = "System can not find specify file.";const string requestNameMessage = "Command request file name.";const string errorExistMessage = "File is already exists.";const string errorNonEmptyMessage = "The directory is not empty.";const string errorNonFileMessage = "File Not Found.";const string errorInvalidSwitch = "Invalid Switch.";
  • 更加细致的路径解析方式,定义 StringParserCmd 类族,应用策略模式,将命令划分为不同的种类,根据命令的不同应用不同种类的解析方式。这里讲命令划分为0,1,2,N对象命令。
class StringParser{public:    StringParser();    ~StringParser();    void parse(const string & inputString, DirectoryFile * currentDirectory);    string getCommand() const;    queue<Object> getObjects() const;    void clear();private:    string m_command;    queue<Object> m_objects;    DirectoryFile* m_currentDirectory = nullptr;    StringParserCmd* m_stringParserCmd = nullptr;    const vector<string> cmd0 = {"cls"};    const vector<string> cmd1 = {"cd", "save", "load"};    const vector<string> cmd2 = {"ren", "copy", "move", "mklink"};    const vector<string> cmdN = {"dir", "md", "rd", "del", "touch"};};class StringParserCmd{public:    StringParserCmd(const queue<string>& strings, DirectoryFile* currentDirectory);    virtual ~StringParserCmd();    virtual queue<Object> getObjects() = 0;protected:    queue<string> m_strings;    DirectoryFile* m_currentDirectory;};class StringParserCmd0 :    public StringParserCmd{public:    StringParserCmd0(const queue<string>& strings, DirectoryFile * currentDirectory);    ~StringParserCmd0();    virtual queue<Object> getObjects() override;};class StringParserCmd1 :    public StringParserCmd{public:    StringParserCmd1(const queue<string>& strings, DirectoryFile* currentDirectory);    ~StringParserCmd1();    virtual queue<Object> getObjects() override;};class StringParserCmd2 :    public StringParserCmd{public:    StringParserCmd2(const queue<string>& strings, DirectoryFile* currentDirectory);    ~StringParserCmd2();    virtual queue<Object> getObjects() override;};class StringParserCmdN :    public StringParserCmd{public:    StringParserCmdN(const queue<string>& strings, DirectoryFile* currentDirectory);    ~StringParserCmdN();    virtual queue<Object> getObjects() override;};
  • 命令执行对象的创建更加细致化;应用一个对象生成器,可以在创建对象的时候附加额外的操作或者条件(软件工程的基本原则:任何问题都可以通过引入一个额外的间接层解决)
class ObjectGenerator{public:    ObjectGenerator(const string& objectStr, DirectoryFile* currentDirectory);    ~ObjectGenerator();    vector<string> getObjects();private:    vector<string> m_objects;};

第四次设计感想

注意时间问题,本来老大给我这个项目的时间是一周。我那天误以为老大指派师傅后这个时间概念就没了,也就不在意时间问题,以为让师傅给我指导错误这就结束了。

然而实际上是,老大给我指派师傅,只是让师傅给我指出错误和指导一些未实现功能的实现。

老大不关心设计,老大只关心你有没有完成这个项目,在期限内。

于是,第一次设计七天,第二次设计四天,第三次设计两天,在第四次设计还正在酝酿和尝试的过程中,老大开始检查我的项目。

在老大的眼里,这个项目我没有交付,所以现在就是 delay 100% 的状态。

没错,老大生气了。老大训示如下:
1、时间估不准是正常的,可以调整,但别在最后一天调整。
2、bug 可以出,但别老出同样的问题。
3、工作要有责任心,不管是编码也好测试也好装机器也好,都需要有责任心。
4、有问题最好尽快沟通,有自己的想法可以在前期提出来。没出问题前把问题提出来是立功,出了问题再说性质就不同了。

此时此刻,我才明白师傅一直说的那句话:“我觉得这样可以,至于要不要这么做,看你自己”。

是的,你的设计可以很烂,你的代码也可以很烂,但是你得实现需求。

没有办法,又只能跟老大要了两天的时间(双休日)。

两天时间,没日没夜地编码,调试,实现。这一次没有之前那么快,是因为命令的接口重做了,几个关键信息的抽象导致以前的代码完全不能用了。但是好处就是,很彻底,很纯粹。

命令的功能实现只考虑传入的对象是否合适(可被操作),而传入的对象是什么不在命令的考虑范围内。

这样子,每一个命令的实现就非常的直接,不再掺杂其他的内容。

我有时候在想,这些模块我可以单独拿出来使用。(我一直向往组件式的编程,所有的问题通过子问题自称,程序通过子程序组成,子程序可以继续复用)


项目收获

通过本次项目,有了以下几点收获:

  • 重要性:实现需求 > 没有 bug > 良好设计 > 通俗易懂 > 奇淫巧技

  • 你最初的设计有多么具体,就会为你最终的实现省去多少力气,因为你可以在设计的过程中发现问题。(此时我才明白软件工程中的详细设计为什么要写的那么详细,甚至觉得冗余。师傅在询问我的设计的时候就能提前预料到我将来编码中会出现的问题,这就是水平的差距。)

  • 对问题中的实体,做最直观的抽象,不要做元抽象(NTree 到 DirectoryFile 的转变)

  • 不要轻易为实现而改动设计

  • 那些你看起来很 low 的做法恰恰就是你解决问题的最快方法

  • 建立问题原型,或者场景复现的时候可以用数据结构

  • 程序崩了?甭管问题是什么,先 try throw catch 保证程序不会崩掉再说

  • 灵活使用 try throw cath 提高程序健壮性,但是前期编码不使用,后期在处理一些需要大改程序设计的问题上使用 try throw catch 有奇效


结语

今天把代码提交了,通知了老大一声,虽然老大没有理我,但是可能他会花时间看我的代码吧。

现在这个项目还在 debug 阶段,基本已经没有崩溃问题了。

今天 debug 了一天……

唉,心累……


后记

debug 了两天,除了一个磁盘序列化再反序列化回来会导致链接文件全都失效的问题,其他的应该没啥问题了。

源码戳这,不定期更新,直到彻底没问题。