# 组件式设计

在代码复用和组织数据方面，面向对象可能是大家第一反应。面向对象三大特性继承，封装，多态，在一定程度上能解决不少代码复用，数据复用的问题。不过面向对象不是万能的，它也有极大的缺陷：  

## 1. 数据结构耦合性极强
一旦父类中增加或删除某个字段，可能要影响到所有子类，影响到所有子类相关的逻辑。这显得非常不灵活，在一套复杂的继承体系中，往父类中改变字段会变得越来越麻烦，比方说ABC是D的子类，某天发现需要增加一个AB都有的数据，但是C没有，那么这个数据肯定不好放到父类中，只能将AB抽象出来一个父类E，E继承于D，AB共有的字段加到E中，一旦继承结构发生了变化，可能接口也要改变，比方说之前有个接口传入参数类型是E，当AB不再需要共用的那个字段，那么需要调整继承关系，让AB重新继承D，那么这个接口的传入参数类型需要改成D，其中的逻辑代码很可能也要发生调整。更可怕的是游戏逻辑变化非常复杂，非常频繁，可能今天加了个字段，明天又删掉了，假如每次都要去调整继承结构，这简直就是噩梦。继承结构面对频繁的数据结构调整感觉很无力。    
## 2. 难以热插拔
继承结构无法运行时增加删除字段，比如玩家Player平常是走路，使用坐骑后就骑马。问题是坐骑的相关信息就需要一直挂在Player对象上面。这就显得很不灵活，我不骑马的时候内存中为啥要有马的数据？接口也有同样的问题，一个类实现了一个接口，那么这个接口就永远粘在了这个类身上，你想甩掉她都不行，还是以骑马为例，玩家Player可以进行骑行，那么可能继承一个骑行的接口，问题是，当我这个Player从坐骑上下来时，玩家Player身上还是有骑行的接口，根本没法动态删掉这个接口！可能例子举得不是很对，但是道理表述的应该很清楚了。  

使用面向对象可能导致灾难性后果，游戏开发中有新人有老人，有技术好的，有技术差的。人都是喜欢偷懒的，当你发现调整继承关系麻烦的时候，有可能AB中增加一个字段为了省事直接就放到父类D中去了。导致C莫名奇妙的多了一个无用的字段。关键还没法发现，最后导致父类D越来越大，到最后有可能干脆就不用ABC了，直接让所有对象都变成D，方便嘛！是的，很多游戏就是这么干的，开发到最后根本就不管继承关系了，因为想管也管不了了。  

## 3. 方法与数据耦合
传统面向对象都是class中带有方法，并且特别提倡虚函数多态。方法跟数据放在一起带来了特别多耦合的问题。为了解决这些耦合，大家想出了大量的设计模式，比如依赖接口，依赖转置。说实话，这就是脱裤子放屁，为了解耦合，把类做成接口，然后继承接口，难道这就不叫依赖了？这些做法导致，代码中到处是接口，代码阅读极其困难。写起代码来也没有个标准，高手跟菜鸡写出来的代码完全是两回事。大部分码农都是逻辑仔，谁有时间天天想这个类要怎么设计啊？随着逻辑越来越复杂类里面的方法将越来越庞大，可怕的是，这是这个类的方法，极其难以重构，很多项目中能看到类里面存在上万行代码的虚函数。天哪！


面向对象在面对复杂的游戏逻辑时很无力，所以很多游戏开发者又倒退了回去，使用面向过程进行开发游戏，面向过程，简单粗暴，不考虑复杂的继承，不考虑抽象，不考虑多态，是开发届的freestyle，挽起袖子就开撸，但同时，代码逻辑的复用性，数据的复用性也大大降低。面向过程也不是一种好的游戏开发模式。  


组件模式很好的解决了面向对象以及面向过程的种种缺陷，在游戏客户端中使用非常广泛，Unity3d，虚幻4，等等都使用了组件模式。组件模式的特点：
1.高度模块化，一个组件就是一份数据加一段逻辑  
2.组件可热插拔，需要就加上，不需要就删除  
3.类型之间依赖极少，任何类型增加或删除组件不会影响到其它类型。  


但是目前只有极少有服务端使用了组件的设计，守望先锋服务端应该是使用了组件的设计，守望先锋的开发人员称之为ECS架构，其实就是组件模式的一个变种，E就是Entity，C就是Component，S是System，其实就是将组件Component的逻辑与数据剥离，逻辑部分叫System，话题扯远了，还是回到ET框架来把。  

ET框架使用了组件的设计。一切都是Entity和Component，任何类继承于Entity都可以挂载组件，例如玩家类：

```C#
public sealed class Player : Entity
{
    public string Account { get; private set; }
    public long UnitId { get; set; }
	
    public void Awake(string account)
    {
        this.Account = account;
    }
	
    public override void Dispose()
    {
        if (this.Id == 0)
        {
            return;
        }
        base.Dispose();
    }
}
```
给玩家对象挂载个移动组件MoveComponent，这样玩家就可以移动了，给玩家挂上一个背包组件，玩家就可以管理物品了，给玩家挂上技能组件，那么玩家就可以施放技能了，加上Buff组件就可以管理buff了。  

```C#
player.AddComponent<MoveComponent>();
player.AddComponent<ItemsComponent>();
player.AddComponent<SpellComponent>();
player.AddComponent<BuffComponent>();
```

组件是高度可以复用的，比如一个NPC，他也可以移动，给NPC也挂上MoveComponent就行了，有的NPC也可以施放技能，那么给它挂上SpellComponent，NPC不需要背包，那么就不用挂ItemsComponent了  

ET框架模块全部做成了组件的形式，一个进程也是由不同的组件拼接而成。比方说Loginserver需要对外连接也需要与服务器内部进行连接，那么login server挂上  

```C#
// 内网网络组件NetInnerComponent，处理对内网连接  
Game.Scene.AddComponent<NetInnerComponent, string, int>(innerConfig.Host, innerConfig.Port);
// 外网网络组件NetOuterComponent，处理与客户端连接
Game.Scene.AddComponent<NetOuterComponent, string, int>(outerConfig.Host, outerConfig.Port);
```

比如battle server就不需要对外网连接（外网消息由gateserver转发），那么很自然的只需要挂载一个内网组件即可。
类似Unity3d的组件，ET框架也提供了组件事件，例如Awake，Start，Update等。要给一个Component或者Entity加上这些事件，必须写一个辅助类。比如NetInnerComponent组件需要Awake跟Update方法，那么添加一个这样的类即可：  

```C#
[ObjectEvent]
public class NetInnerComponentEvent : ObjectEvent<NetInnerComponent>, IAwake, IUpdate
{
    public void Awake()
    {
        this.Get().Awake();
    }

    public void Update()
    {
        this.Get().Update();
    }
}
```

这样，NetInnerComponent在AddComponent之后会调用其Awake方法，并且每帧调用Update方法。
ET没有像Unity使用反射去实现这种功能，因为反射性能比较差，而且这样实现的好处是这个类可以放到热更dll中，这样组件的Awake Start，Update方法以及其它方法都可以放到热更层中。将Entity和Component做成没有方法的类，方法都放到热更层，方便热更修复逻辑bug。  

组件式开发最大的好处就是不管菜鸟还是高手，开发一个功能都能很快的知道怎么组织数据怎么组织逻辑。可以完全放弃面向对象。使用面向对象开发最头疼的就是我该继承哪个类呢？之前做过最恐怖的就是虚幻三，虚幻三的继承结构非常多层，完全不知道自己需要从哪里开始继承。最后可能导致一个非常小的功能，继承了一个及其巨大的类，这在虚幻三开发中屡见不鲜。所以虚幻4改用了组件模式。组件模式的模块隔离性非常好，技术菜鸟某个组件写得非常差，也不会影响到其它模块，大不了重写这个组件就好了。 

ET的组件设计有所创新，方法跟数据分离，完全解除耦合，不用绞尽脑汁去想怎么解除耦合，随意写静态方法即可，根本不存在耦合，即使是菜鸟写的代码也很容易重构。

正是因为ET使用了可拆卸的组件模式，ET可以将所有服务器组件都装到同一个进程上，那么这一个进程就可以当作一组分布式服务器使用。从此用vs调试分布式服务器成为了可能。正因为这样，平常开发只使用一个进程，发布的时候发布成多个进程就行了。说实在的，不是吹牛，这是一个伟大的发明，这一发明解决了分布式游戏服务器开发中的大大大难题，极大的提高了开发效率。    


