Music Player

出处

Solution

抽象

抽象的意思是能够根据设计要求,得出程序运行的逻辑框架以及定义需要的功能。简单来说,就是构想自己是该程序的使用者, 列举实现程序某个功能需要哪些步骤。比如对于我们所说的例子,播放一首歌的运行流程可能包括:添加歌曲到音乐库,添加歌曲到播放列表,播放,删除等。对于某些系统,系统可以处于多个不同状态,每个状态的功能不同,这就需要构造有限状态机。关于有限状态机的一些参考资料请见“工具箱”。在抽象的过程中,往往需要和面试官进行沟通,确认需要实现什么功能。在抽象结束后,所得到的实体就应该成为一个个对象,而功能就是对象的函数接口。比如,在本例中,涉及的实体有播放器,专辑,歌曲,播放列表。函数包括添加歌曲,删除歌曲,播放,停止,暂停等等。之后,我们就要根据抽象的结果,进行对象设计,并且把所需的功能以函数接口的形式分配给不同的对象。

设计对象

通常而言,对于抽象出的每个实体,我们都应该构造一个类去描述它。对象之间的关系可能是继承,或者包含,具体分析请见“ 继承/组合/参数化类型”。对于这里所需要实现的音乐播放器,我们就可以构造播放器,专辑,歌曲,播放列表这几个类。

设计接口

接口用于与用户进行交互,以及对象之间的交互。设计接口的核心在于明确每个对象需要实现什么功能,当上层对象调用下层对象的接口时,只需要提供相应的参数,就能够期待获得对应的结果。这样,上层对象不需要知道对方如何获得结果,下层对象也不需要知道对方拿到结果会进行什么样的操作。如此,通过设计恰当的接口我们就实现了decoupling:让程序具有逻辑上的层次,每一层的对象实现特定的功能,对象可以独立地更改实现功能的方法,而不会影响上层和下层。

在设计接口的时候我们可能需要添加一些不那么明显的辅助类,使得程序更具有层次。比如说,考虑添加歌曲这个功能,用户会通过调用播放器的添加歌曲接口,传入一首歌。考虑到当音乐库变得很复杂的时候,我们需要判断诸如该歌曲是否已经存在,应当用怎样的数据库存储数据等等一系列问题。如果都由播放器对象来处理这些问题,会使得播放器这一层变得过为臃肿,减少代码的可读性和可维护性。所以我们可以引入另一个类,歌曲管理器,用来处理数据相关的操作。这样,用户通过播放器接口添加歌曲,播放器调用歌曲管理器的添加接口将新歌写到数据库,歌曲管理器负责打开数据库,判断数据库中是否存在重复的歌曲,写入数据等等。当之后出于某些原因需要更换储存方式的时候,只要更改歌曲管理器的代码即可。同时,如果发现播放器中有重复的歌曲,也只需要查看歌曲管理器的实现过程有什么漏洞,而不是漫无目的的找bug,这样也提升了程序的可维护性。

同时,在确定接口功能的时候,尽量把功能的具体实现向下层“推” 。比如,播放一首歌曲可以包括打开文件,访问文件等。当用户调用播放器接口要求播放一首歌时,我们可以让播放器打开文件,也可以让播放器调用歌曲对象的播放接口,由歌曲对象负责打开文件等操作。这样做的好处在于,用户可能在任何时候播放一首新的歌曲,播放器只需要有一个指针指向当前播放的歌曲对象,如果用户需要播放新的歌曲,播放器只需要:调用当前歌曲的停止接口,设置新的当前播放,调用新歌曲的播放接口即可,而不需要在播放器层反复地进行文件操作。这样,播放器层负责切换歌曲的逻辑,而歌曲层负责具体的播放操作,两者相互独立。这样的好处在于,当以后用户需要更复杂的切歌方式,只需改变播放器的实现即可;当存在更好的音乐编解码,只需要改变歌曲的播放操作即可。我们成功地decouple了切换和播放。

通常,接口的函数定义可以遵从如下模式:函数参数包括输入参数和输出参数,返回值为错误代码,或者是标示操作是否成功的布尔变量。函数需要对参数的有效性进行验证。输出参数通常传入地址,函数内可以将结果直接写到地址中。比如说,创建一个播放列表可以如下定义:

ErrorResult createPlaylist(Vector<Song *> inSongsArray, Playlist *outPlaylist)

当ErrorResult为0时表示创建成功,outPlaylist指向该播放列表。

经过上述例子,我们作出如下总结:

  • 可以依照抽象,设计对象,设计接口的流程进行思考
  • 针对接口而不是实现来进行编程:不同对象之间保持接口
  • 一致,调用接口时不需要基于接口内的实现方式。

Complexity

设计题

Code

设计题