一直以来我都在思考网络游戏开发的OO模式。如何将领域建模运用到网络游戏这种异步项目中。在我的网络游戏架构中,为了避免多线程引发的诸多问题,逻辑服务模块采用单线程方式,考虑到单线程下阻塞模式会严重影响服务器的性能,因此采用了异步方式进行逻辑的通讯。我想大多的网络游戏开发应该都是采用异步方式。但异步方式,很容易将一个原本完整的逻辑,折分成一个一个request、response等网络命令,服务器端将一些逻辑代码写在收到指令的地方,另一些写在业务类中,同样在收到指令的地方调用。没有一种清晰的思路来区分哪些逻辑该和异步的收发混在一起,哪些逻辑该放在领域层。
昨天重新看了一些DDD(Domain Driven-Desgin)的文章,复习了一个Entity、Value、Service的概念,下面以玩家在商店购买一个道具为例,整理了一下思路。(由于只是快速草稿,因此很多语法细节是错误的)
――――――――――应用层――――――――――――
// 客户端的购买道具逻辑代理
class ShoppingAgent : public ServiceAgent
{
public:
// 发出购买某道具的
void requestBuyItem(int itemId, int qty)
{
BuyItemRequest request(itemId, qty);
send(request);
}
void onBuyItemResponse(BuyItemResponse response)
{
if(response.errorId == NO_ERROR)
m_listener->onBuyItemOk();
else
m_listener->onBuyItemFail();
}
protected:
ShoppingListener m_listener;
}
// 服务端购买道具的逻辑服务
class ShoppingService : public Service
{
public:
// 响应购买道具的请求
void onBuyItemRequest(BuyItemRequest r)
{
BuyItemResponse response;
Shopping shopping;
try
{
shopping.buyItem(r.playerId, r.shopId, r.itemId, r.qty);
}
catch(ShoppingException e)
{
// 购买失败的错误ID会跟随response发回给客户端
response.errorId = e.errorId;
}
send(response);
}
}
应用层,在这里主要负责对网络的异步通讯封装。ShoppingService作为服务在部署在服务器上,而ShoppingAgent作为访问服务的一个接口部署在客户端。对于客户端的应用上层,不需要调用到任何通讯指令,全部委托给ShoppingAgent。异步的结果,皆通过观察者模式,通知给上层。
――――――――――领域层――――――――――――
// 购买道具的服务
//(对应Erice Evans 在Domain 中的service)
class Shopping
{
public:
// 购买道具
void buyItem(int playerId, int shopId, int itemId, int qty)
{
Shop shop = m_respository.getShop(shopId);
Player player = m_respository.getPlayer(playerId);
// 验证商店是否有要购买的道具
validateShopHasItem(shopId, itemId, qty);
// 取出道具价格
int amount = shop.getPrice(itemId, qty);
// 验证玩家是否有足够的钱
validatePlayerHasMoney(playerId, amount);
// 验证玩家背包是否有足够空间
validatePlayerHasSpace(playerId, itemId, qty);
// 玩家收到一定数量的道具
player.receiveItem(itemId, qty);
// 玩家付款
player.pay(amount);
}
protected:
void validateShopHasItem(shopId, itemId, qty)
{
Shop &shop = m_respository.getShop(shopId);
if(shop.hasItem(itemId, qty))
throw new ShopHasNoItemException();
}
void validatePlayerHasSpace(int playerId, int itemId, int qty)
{
Player &player = m_respository.getPlayer(player);
Item &item = m_itemRespository.getItem(itemId);
if(player.getBag().getSpaceAvailabe() < item.getCapability() * qty)
{
throw new NoEnoughSpaceException();
}
}
void validatePlayerHasMoney(int playerId, int price)
{
Player &player = m_respository.getPlayer(player);
if(player.hasMoney(price))
{
throw new NoEnoughMoneyException();
}
}
}
// 商店,提供道具的购买,对道具定价(不同商店卖的东西不同)
//(对应Erice Evans 在Domain 中的entity)
class Shop
{
public:
int getPrice(int itemId, int qty)
{
// …查找商品价格并乘上数量
}
bool hasItem(int itemId, int qty)
{
// …查找商品
}
}
// 玩家
//(对应Erice Evans 在Domain 中的entity)
class Player
{
public:
void receiveItem(int itemId, int qty)
{
getBag()->pack(itemId, qty);
}
void pay(int money)
{
m_money -= money;
}
}
// 资源仓库,由于是例子,所以先写到一块来
class Respoistory
{
public:
Shop& getShop(int id)
{
}
Player& getPlayer(int id)
{
}
Item& getItem(int id)
{
}
}
Shopping 同Shop的分离,是我重新看DDD文章后的重大改变。不然之前一直困惑于buyItem()中会对于玩家的一些逻辑操作,似乎不应该是shop这个领域Entity的职责。
Shopping同ShoppingService是分离的,在我的概念里,领域层应该足够的单纯,应该不用知道异步通讯这回事。对于这两者的职责我是这样分配的:Shopping负责真正的购买逻辑,协调玩家、商店、道具等对象。而ShoppingService主要负责处理异步收到请求,将处理结果返回给客户端。如果需要,分布式的事务也会在这层消化掉。至于逻辑,只是简单的调用领域服务Shopping::buyItem()。
经过这次整理,感觉总体的思路清晰了很多。但仍还有一些困惑。如果player.receiveItem(),player.pay()不是在同一台服务器上处理怎么办,也就是任何逻辑服务器如果需要影响player的数据,都必须统一通过专门的一台服务器。这时候问题就来了,只要需要访问到别的服务器,那么就有异步操作。也就是要在Shopping类中用到异步通讯,这又违背了一开始我希望领域层不关心异步通讯的期望。如果上提到ShoppingService,似乎是可以的,但这一来又犯糊涂了。到底什么时候要放在ShoppingService,什么时候要放在Shoping?既然想让领域层不关心异步,那么领域层就应该按原本的思路去设计,自然而然会将player.pay()和player.receiveItem()放在shop.buyItem()中。
如何是好呢???于是我问了一个没有研究过DDD的朋友,关于领域建模的概念,他也只是从我这里听到一些概念。他说,领域建模只是一种分析模型,和最终的设计模型之间本就是不同的,也就是说不管异步不异步,领域建模是不会变的。但设计模型肯定要关心异步还是同步。他这样说,似乎也有道理。但是Eric Evans指出的的Domain层难道不是最终的设计模型中的一部分吗?难道异步还是同步,一定要影响到所有逻辑的编写,而没办法在某一层消化掉吗?