论「面向对象」

写在开头

最近公司在招人,正好我也趁机体验了一把面试官的感觉,说句题外话,面试别人的感觉非常好,学到很多东西,明白自己以前面试中的不足之处。

回到正题,我在面试别人的时候,特效喜欢问这样的一个问题:

“在实际开发过程中,是否有用过面向对象中的封装继承多态?具体是怎么使用过的?”

很可惜,很少有人的回答能达到我的期望目标。

面向对象?!

说到我对面向对象的理解,最开始学编程的时候,也不知道什么是面向对象编程,只知道写代码就是一行接一行,完全没有设计,在到后来,一次机缘巧合下,我在 Java 百度贴吧里面看到了一份教程,这份教程对我的自身以及自身未来在编程这行业的发展都起到了非常至关重要的作用

Java 编写游戏

没错,这个教程就是教别人怎么使用 Java 制作一个游戏,这份教程是一个乐于分享的大神制作的,可以说是非常的高质量的一份教程。

这份教程全部都是视频,至于这份教程的质量,可以说是我看过所有视频中质量最高的,可能视频中用的技术很老(用的还是 Java 6),而且用的还是 Java 中很少见的 Swing 来编写的游戏界面。但是视频作者通过这个游戏的开发所表达的面向对象编程思想在我看来是最最最重要的。可能这也是视频作者的初衷吧!

时至今日,回头看整个游戏的源码,虽然还有些地方不解,不过大部分的代码已经完全理解了。为什么我敢说完全理解?我觉得这就是这个视频牛逼的地方,整个项目的代码并不难,既没有什么高端的框架,更没有什么复查的算法,有的仅仅只是关于软件架构设计的思考

我记得当时我在编写代码的时候,也是看不懂代码,不过我还是硬着头皮写下来了,真的感谢那个时候的自己,如果我没有坚持把那个项目做完,可能现在还是一个"同行"吧。

软件架构

下面这张图就是整个游戏代码的架构设计:

"软件架构"

可以看到,整个设计非常的清晰,非常清晰,非常清晰,重要的事情要说三遍,什么数据访问层,什么界面显示层,什么业务逻辑层在这张图片体现的淋漓尽致。

不要看这是一个游戏的架构图,我们完全可以用这张图的套路来设计我们自己的任何项目

接下来我们就来分析分析这个架构图的套路

代码分析

上面我说道,这个项目的核心作用就是让我对面向对象编程思想有更加深刻的理解,俗话说的好:no code,no bb。那么下面我们就来分析分析源码。

在看代码之前,我们先看看整项目的包结构:

"结构图"

  • config - 配置相关
  • control - 游戏的控制器
  • dao - 游戏的数据访问相关
  • dto - 游戏数据的载体
  • entity - 实体类
  • main - main 函数
  • service - 游戏的逻辑相关
  • ui - 游戏 UI 相关
  • util - 工具类

上面项目包结构的简单介绍。下面是游戏界面的截图:

"游戏界面"

界面有点丑,因为是好几年前的界面了,不过这不是重点!重点在代码上!

下面是整个项目的代码分析。这里我只截取整个项目的一部分代码,具体源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// JPanelGame.java
public class JPanelGame extends JPanel {

//这里的 Config.getLayerConfig() 是伪代码,后面我就介绍它的具体实现。
List<Layer> layers = Config.getLayerConfig()

@override
public void paintComponent(Graphics g) {
super.paintComponent(g);
for(int i = 0; i < layers.size(); i++) layrs.get(i).paint(g);
}

}

首先要说下,在 Java 的 swing 中有一个类似 Android 里面自定义 View 的 onDraw() 方法,在这个方法中可以自定义自己的绘制逻辑。不过在 swing 中这个方法叫 paintComponent()。

上面代码的作用主要是用来绘制游戏界面的,那么问题来了,绘制游戏界面怎么可能和面向对象搭上关系呢?不急,下面来慢慢分析!

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
32
33
34
35
36
37
38
39
40
41
// layer.java
public abstract class Layer {

/**
* 窗体左上角x坐标
*/
protected int x;
/**
* 窗体左上角y坐标
*/
protected int y;
/**
* 窗体宽度
*/
protected int w;
/**
* 窗体高度
*/
protected int h;

protected void createWindow(Graphics g){
//这里有一段公用的逻辑代码,主要是用来绘制边框。
}

/**
*
* @param x 窗口的x坐标
* @param y窗口的y坐标
* @param w 窗口的宽度
* @param h 窗口的高度
*/
public Layer(int x,int y,int w,int h)
{
this.x=x;
this.y=y;
this.h=h;
this.w=w;
rectW=this.w-(PADDING<<1);
}

}

我们使用面向对象思想,把整个界面想象成成不同的区域(界面参考上面架构途中的 JPanel那一项),这些区域有相同点,也有不同点,于是我们就把相同点写在父类,将不同的定义为抽象或者接口。于是就构成了上面的 Layer.java 和 xxxLayer.java。

在 Layer.java 这个父类中,通过编写 createWindow() 这个方法来封装上面说到的相同点,那么问题来了,这个游戏的界面中的那些不同的区域有哪些共同点呢?很简单,每个区域的背景都是一样的,而不同点就是背景上的内容,比如左边的显示的横着的进度条,而右边有的区域是用来显示分数,有的区域是显示下一个方块的等等(参考上面的游戏界面的截图)。

通过分析这些不同点个相同点,就可以很快的抽象出父类的结构,也就是上面的 Layer 类,但是光有父类没有子类也是白搭,所以下面让我们来看看起子类,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// LayerNext.java 用来绘制下一个方块
public class LayerNext extends Layer {

//这个构造函数很重要,用来设置这块区域显示在窗口的坐标
public LayerNext(int x, int y, int w, int h) {
super(x, y, w, h);
}

public void paint(Graphics g) {
//通过调用父类的共同点,来实现通用逻辑。
this.createWindow(g);
//实现自己的逻辑
if(this.dto.isStart()){
//画下一个方块图片
this.drawImageAtCenter(Img.NEXT_ACT[this.dto.getNext()], g);
}
}
}

LayerNext 这个类通过调用父类的 createWindow() 来绘制背景,然后在在 paint() 方法中实现自己的绘制方法,drawImageAtCenter() 这个方法也是封装父类的一个方法,可以把一个图片画在一个区域的中心位置。

我们在看看还有没有共同点,很显然左边的数据库和历史记录的两个区域其实大部分内容都是一样的,唯一不一样的就是数据源不一样,所以我们可以把共同的逻辑封装在父类中,但是我们其他的区域并不需要这些逻辑,如果封装在 Layer 类中明显显得比较不妥,那怎么办,很简单,添加一个中间层,让这个中间层去实现数据库和历史记录这两个区域的公共逻辑,这样就既不会破坏 Layer 类的代码,也能不用编写重复的代码。源码如下:

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
32
33
34
35
36
37
38
39
40
// LayerData.java
public abstract class LayerData extends Layer {

public void showData(Image imgTitle,List<Player> players,Graphics g){
//实现通用的绘制逻辑
}

}

// LayerDataBase.java
public class LayerDataBase extends LayerData {

public LayerDataBase(int x, int y, int w, int h) {
super(x, y, w, h);
}
public void paint(Graphics g)
{
//绘制背景
this.createWindow(g);
//调用公共逻辑
this.showData(Img.DB,this.dto.getDbRecode(), g);
}

}

// LayerDisk.java
public class LayerDisk extends LayerData {

public LayerDataBase(int x, int y, int w, int h) {
super(x, y, w, h);
}
public void paint(Graphics g)
{
//绘制背景
this.createWindow(g);
//调用公共逻辑
this.showData(Img.DB,this.dto.getDiskRecode(), g);
}

}

LayerDataBase 是显示从数据库中获取的信息,而 LayerDisk 用来显示从本地获取的信息。
分析到这,整个项目的界面部分差不多都分析完了,你以为这样就完了?no no no,好戏还在后面呢。

界面与逻辑分离

首先说下什么是界面什么又是逻辑

界面泛指界面逻辑,比如 Android 中的 xml,前端的 HTML,等等。。。
逻辑泛指业务逻辑,这个就更具不同的项目编写不同的逻辑了。

打个通俗一点的比方,界面有点像我们人的外表,而逻辑就像我们人的内在,外表是别人只关可以看到的(用户看到),而内在除非通过沟通了解,不然很难有人能直接看出一个人的内在(只有程序员才看的懂代码)。

为什么要把界面与逻辑进行分离?这是一个很大的话题,有很多手段实现,我在这里不去探讨这些手段,我们是说说为什么要分离,想想看如果界面和逻辑不分离会发生?比如在 Android 中,假设我们没有布局文件这种概念,而是直接使用代码去编写界面,比如下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

//伪代码
public class MainActivity extends AppCompatActivity{
//如果没有了布局文件,那么 setContentView() 方法就只能传入一个 View 对象
public void onCreate(){
super.onCreate()
setContentView(buildContentView())
}

//这里我们简化了创建 View 的过程
private View buildContentView(){
ViewGroup root = new ViewGroup();
root.add(new ImageView())
root.add(new TextView())
// ...
return root;
}
}

上面的代码也能实现功能,但是呢有几个的问题:

  1. 我们无法实时的预览我们编写的界面,这样会让我们在编写界面的时候会很缓慢,因为我们不知道我们编写的代码所显示出来的界面是不是和设计图保持一致。当我们的界面非常复杂的时候,构建 UI 的代码会非常的多而且重复。。
  2. 加入我们把网络请求,数据验证等等逻辑都写在 Activity 中,整个 Activity 就会变得非常的臃肿,有一句话是这么说的「引起一个类修改的原因最好只能有一个!」,如果我们把很多不同种类的逻辑都写在 Activity 里面,就会导致引起 Actiivty 修改的原因会有很多个。

可以看到如果 Android 使用代码去构建 UI,会比较麻烦,倒不是说这样做不好,因为用 Kotlin 编写的名为 Anko 的库就是使用代码去构建 UI,是 xml 构建 UI 的一种替换方案,不过由于 AndroidStudio 不支持代码构建 UI 的实时预览,所以就导致这个库在构建复杂界面的时候会很繁琐。

所以用 xml 构建界面既可以让我们在编写界面的时候既可以实时预览,又可以把 UI 的构建逻辑从 Activity 中抽离出来。近几年各种 MVX(MVC,MVP,MVVM…) 架构被提起,归根结底这些架构解决的问题就是将界面逻辑进行分离,而 Android 中的布局文件,就是起到了将 UI 的构建逻辑抽离出来,但是这还不够,因为我们还需要把数据填充到 UI 中,所以 MVX 架构就是帮我们使用更好的姿势去填充数据。

xml

在说如何分离之前,我们先了解一下 xml 这个东西。身为一个 Android 开发者,对 xml 一定不会陌生,平时我们写布局文件的时候都是用 xml 编写。xml 全称为 Extensible Markup Language 翻译过来为可扩展标记性语言。

说到 xml,它真是一种神奇的语言,早期的时候,WebService 就可以采用 xml 作为数据交换的格式,后来慢慢的被 json 给取代了,原因也很简单,json 比 xml 更加简洁。

虽然在数据交换这一块被 json 给取代了,但这并不代表 xml 无用武之地了,看看我们的 Google 大大,就是用 xml 作为 Android 开发中的布局文件使用的语言,上面说了虽然 xml 比 json 更复杂,当时当数据多到一定程度,json 的可读性极差,但是 xml 可读性却不会因为数据过多而下降,这个时候 xml 的臃肿反倒变成它的优势(仅仅是个人的看法)。

如何分离

下面就来谈谈怎么来实现分离,前面我们讲到用封装,继承,多态来把代码写的更健壮,这些也算是分离,但是仅仅只是对逻辑的内部进行了分离,我们还需要宏观的对整个项目的架构进行设计,这样可以让我们代码的变得更好。

在上面的分析中,我们只提到了将界面划分成不同的 Layer,每个 Layer 负责绘制一块区域的内容,这样起到的作用就是,各负责各的绘制,互相不打扰。但是没个 Layer 的创建需要一些参数,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//JPanelGame.java
private List<Layer> layers = null;

private void initLayer(GameDto dto) {
layers = new ArrayList<Layer>();
layers.add(new LayerAbout(x,y,w,h));
layers.add(new LayerData(x,y,w,h));
layers.add(new LayerBackground(xy,w,h));
// ...
}

@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
for (int i = 0; i < layers.size(); i++) layers.get(i).paint(g);
}

在上面的 initLayer() 方法中,通过创建不同类型的 Layer 对象,让后在将对象添加到 layers 这个集合中,然后在 paintComponent() 方法中循环调用 paint() 方法。

emmmmm,看上去设计很优美,不过有一个很大的弊端,如果我们游戏打包成可执行文件之后,用户觉得界面不好看,想手动设置每个 Layer 的长宽和位置,甚至用户觉得我们整个 UI 都很丑,想自己设计,类似一些大型单机游戏的 MOD,这个时候该怎么办呢?

配置文件

毕竟用户是爸(er)爸(zi),我们身为程序开发者,一定要考虑这种需求,所有,配置文件就诞生了,我们平时在玩别人开发的游戏的时候,因为每个人的操作习惯不一样,所以基本上所有的游戏在首页都会有一个设置选项,我们可以在里面修改一些基本的设置,这些配置都是保存在本地的文件中,当游戏启动的时候回去读取这些文件,并把里面的内容映射到程序中。

我们这个游戏虽然简单,但是也是”五脏俱全”的,具体代码如下:

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
// GameConfig.java
public class GameConfig {
//界面配置对象
private static FrameConfig FRAMECONFIG = null;
//系统配置对象
private static SystemConfig SYSTEMCONFIG=null;
//数据库配置对象
private static DataConfig DATACONFIG=null;

static{
try {
//创建xml 读取器
SAXReader reader=new SAXReader();
//获取路径
Document doc = reader.read("data/cfg.xml");
//获取父节点
Element game=doc.getRootElement();
//创建界面配置对象
FRAMECONFIG=new FrameConfig(game.element("frame"));
//创建系统配置对象
SYSTEMCONFIG=new SystemConfig(game.element("system"));
//创建数据库配置对象
DATACONFIG=new DataConfig(game.element("data"));
} catch (Exception e) {
e.printStackTrace();
}
}
}

GameConfig 顾名思义就是游戏配置类,在这个勒种我们读取 cfg.xml 这个 xml,在 这个 xml 中定义了很多配置信息,如果游戏窗口的大小,窗口的剧中偏移量等等,具体内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- cfg.xml 中的 Frame -->
<frame title="Java俄罗斯方块" windowUp="20" width="1168" height="680"
padding="16" border="7" sizeRol="5">
<button w="105" h="40">
<start x="820" y="74" />
<userConfig x="965" y="74" />
</button>
<layer className="ui.LayerBackgroud" x="0" y="0" w="0" h="0" />
<layer className="ui.LayerDisk" x="40" y="32" w="334" h="588" />
<layer className="ui.LayerGame" x="414" y="32" w="334" h="590" />
<layer className="ui.LayerButton" x="788" y="32" w="334" h="127" />
<layer className="ui.LayerNext" x="788" y="188" w="176" h="116" />
<layer className="ui.LayerLevel" x="964" y="188" w="158" h="116" />
<layer className="ui.LayerPoint" x="788" y="336" w="334" h="282" />
</frame>

可以看到在 layer 元素中定义了很多属性,其中 className 对应的就是我们系统中的那些类的相对路径,x,y,w,h 这些就是在构建 layer 对象的时候会传入构造函数的参数。

这下好了,有了配置文件,再也不怕用户爸(er)爸(zi)哔哔,如果觉得游戏窗口大了,直接修改 frame 中的 width 和 height 然后重新启动游戏就能看到效果了!

最后我们再来看看怎么在 Java 代码中使用这些配置文件,其实在 Java 中经常和配置文件一起出现的”兄弟”就是反射,这”兄弟”和配置文件真的是绝配!下面就来看看是怎么运用反射来使用配置文件的,具体代码如下:

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
//JPanelGame.java
private void initLayer(GameDto dto) {
try {
// 获取游戏配置对象
FrameConfig frameConfig=GameConfig.getFameConfig();
// 获取游戏窗口配置对象
List<LayerConfig> layersCfg = frameConfig.getLayersConfig();
// 实例化layers数组 长度是窗口配置对象的个数
layers = new ArrayList<Layer>(layersCfg.size());

for (LayerConfig layerCfg : layersCfg) {
// 获得类对象
Class<?> cls = Class.forName(layerCfg.getClassName());
// 获得构造函数
Constructor<?> ctr = cls.getConstructor(int.class, int.class,
int.class, int.class);
// 调用构造函数创建对象
Layer layer = (Layer) ctr.newInstance(layerCfg.getX(), layerCfg
.getY(), layerCfg.getW(), layerCfg.getH());
//设置数据源对象
layer.setDto(dto);
// 把创建的对象放到集合中
layers.add(layer);
}
} catch (Exception e) {
e.printStackTrace();
}
}

之前我们的 Layer 对象是通过手动 new 出来的,现在有了配置文件,我们可以通过类的完全限定名加反射来动态的创建这些对象,并且我们还能把配置文件中定义的一些值设置在反射创建出来的对象上。

最后要说下,没有真正意义上的完全分离,只能使用一些手段,将修改代码的成本降到最低。当然,这要建立在优秀的代码架构之上。

题外话

看到上面使用 xml + 反射的方式来分离界面的构建,我第一时间就想到了 Spring 这个框架,在早期的时候,Spring 就是使用这种方式来实现 IOC 的,当然现在都是使用注解来实现了。无论是使用注解还是使用 xml 其实差别都不大,二者所扮演的都是一个"中间人"的角色,起到了一个映射的作用,将我们自己的代码映射到 Spring 中,从而让 Spring 帮我们实现 IOC。

Controller

ok,上面算是把整个游戏的 UI 逻辑给理了一遍,但是这仅仅只是 UI 逻辑,游戏的逻辑要比 UI 逻辑复杂得多,在本文的开头有一张整个游戏的架构图,在图中有标出一个名为控制器的东西,很显然这个才是整个游戏的核心,下面就来看看,这个控制器为什么叫”控制器”吧!

首先来看一个所有 Java 程序都有的东西,不对是所有程序都有的东西,那就是 main 方法,代码如下:

1
2
3
4
5
6
7
8
public class Main {
public static void main(String[] args) {
/**
* 初始化 游戏控制器
*/
new GameControl();
}
}

很简洁,只有一行代码,完美的将逻辑封装在了 GameControl 这个类中,不像某些程序员写的游戏,在 main 方法中又是初始化这个又是初始化那个的,基本上所有类的初始化都是放在了 main 方法。导致 main 方法非常臃肿。扯远了,下面来看看 GameControl() 这个类干了些什么!代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public GameControl() {
// 创建游戏数据源
this.dto = new GameDto();
// 创建游戏逻辑块
this.gameService = new GameTetris(dto);
// 创建本地接口
this.dataB = craeteDataObject(GameConfig.getDataConfig().getDataB());
// 把通过接口读取到的数据 保存到dto中
this.dto.setDiskRecode(this.dataB.loadData());
// 创建游戏面板
this.panelGame = new JPanelGame(dto, this);
// 读取用户配置
this.setControlConfig();
// 初始化设置窗口对象
this.frameConfig = new JFrameConfig(this);
//创建保存分数窗口对象
this.frameSavePoint=new JFrameSavePoint(this);
// 创建游戏窗口 安装游戏面板
new JFrameGame(this.panelGame);
}

在 GameControl 的狗构造函数中,首先是初始化了 GameDto 这个对象很重要,它是用来存储游戏运行的时候所要保存的数据,是一个全局的,有点像 Android 中的 ApplicationContext,第二个被创建的是 IGameService 对象,看名字就知道是个接口,实现类是 GameTetris,接下来会去创建用于获取数据的数据接口,类型为 Data,并通过数据获取接口读取数据保存到 GameDto 中,最后就是现实 JFrameGame 这个窗口即游戏窗口。

Interface

首先我们来看看 IGameService 和 Data 这两个接口,接口定义如下:

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
32
33
34
35
36
37
38
39
40
41
42
43
// IGameService.java
public interface IGameService {
/**
* 上
*/
public boolean keyUp();
/**
* 下
*/
public boolean keyDown();
/**
* 左
*/
public boolean keyRight();
/**
* 右
*/
public boolean keyLeft();
/**
* 作弊键
*/
public boolean keyLevelUp();
/**
* 开关 阴影
*/
public boolean keyIsShadow();
/**
*直接下落
*/
public boolean keyDirectDown();
/**
* 暂停游戏
*/
public boolean keyStopGame();
/**
* 开始游戏
*/
public void startGame();
/**
* 游戏主要行为
*/
public void mainAction();
}

可以看到在 IGameService 接口定义了整个游戏中所有的”动作”。

1
2
3
4
5
6
7
8
9
10
11
12
public interface Data {
/**
* 读取数据
* @return 要获取的数据集合
*/
public List<Player> loadData();
/**
* 存储数据
* @param players 要存储的数据集合
*/
public void saveData(Player players);
}

Data 接口定了我们这个游戏获取数据的 动作

上面为什么说是 “动作”,因为在我看来,接口中定义的函数不关心具体的”动作”的实现,仅仅只是定义了我们需要哪些动作。(动作这个词语可能不准确)

至于上面两个接口具体的实现我们就不在这里讨论了,感兴趣的同学可以自行查看源码。回到 GameControl 这个类,这个类在整个项目中扮演的是一个控制器的角色(正如它的名字一样)。所谓的控制器其实相当于一个总负责人,用户的操作首先是进入它,然后由它来决定是交给谁,比如用户按下开始按钮,这个时候程序会先通知 GameControl,然后由 GameControl 启动游戏线程来使方块下落。

在 GameControl 中有一段比较有意思的逻辑,那就是在监听用户的按键并调用相应的方法的时候,一般情况下应该会这么写:

1
2
3
4
5
6
7
8
9
10
11
//伪代码
public void onKeyDown(int keyCode) {
switch(keyCode):
case KEY_UP:
keyUp();
break
case KEY_DOWN:
keyDown();
break;
// ...
}

很显然,这种写法没有丝毫的可扩展性,下面是本项目中的写法。代码如下:

1
2
3
4
5
6
7
8
//伪代码
HashMap<Integer,Method> actionList = new HashMap();
actionList.put(KEY_UP,getClass().getMethod("keyUp"));
actionList.put(KEY_DOWN,getClass().getMethod("keyDown"));
// ...
public void onKeyDown(int keyCode) {
actionList.get(keyCode).invoke(this);
}

使用反射加 HashMap 可以替代几乎所有 switch 代码块,可能使用反射性能有点损耗,但是是在可接受的范围之内的。上面代码的好处在于,我们可以把这个 HashMap 对象通过对象留直接写到硬盘上,在在另设置界面中读取出来,这样用户就可以随意更改游戏的键位设置了。

小结

分析到这里,其实整个项目的代码都被分析了一遍,回到开始的那个问题,界面逻辑分离了么?其实根本就没有完完全全的分离,因为只要类与类之间进行了调用,那么就是无法分离的,我们能做的就是将来在修改代码的时候,能把修改成本尽可能的降到最低,比如我们修改这个项目的界面我们只需要改改 xml 就能实现,而如果我们想修改游戏的逻辑,我们只需要修改一下 GameTetris 这个类中的代码就行了,这样看起来是不是轻松了许多呢?

end

到这里,这篇文章就结束了,其实我写这篇文章的初衷主要有如下几个:

  1. 在编写 「Java 俄罗斯方块」这个项目的时候,因为实力不济,无法体会到项目在架构设计上的精妙之处,虽然还有很多不足的地方,但是这个项目本身其实是一个很好的教材。通过这个项目可以学到很多面向对象编程的思想,从而做一个真正面向对象编程的程序猿。:)
  2. 无论是前端还是后端,无论是 Android 还是 IOS,技术的本质都是不会变得。可以看到本篇文章所说的只是一个很简单的的小游戏,但是透过游戏本身,观察其内部设计思想,能看到很多熟悉的影子,学习这些思想比学习怎么调用 API 要有用的多。
  3. 好的代码架构,能带来很多好处,但是一味的追求架构,而不去注重具体的业务逻辑是不对的,我们应该在实现了业务逻辑的前提条件只要再来考虑代码的架构,当然,也可在完成整个项目之后在对项目的整体架构进行重构。真正好的架构绝对是经过千锤百炼的。所以不要觉得重写相同的代码没有意义,关键是要在重写的时候多思考原本的设计有哪些不足的地方,能通过什么方法去改掉了不合理的设计,最终的收获一定会让你大吃一惊的。