工厂模式是为了客户端简化对象的创建过程,使创建与表示分离。
代码案例:
在ARPG的技能系统中,因为技能的攻击范围不同,一般会有不同的技能选择器。我们以此为案例实现一个创建技能选择器的工厂模式。
public interface IAttackSelector
{
GameObject[] FindTargets ();
}
public class SectorAttackSelector:IAttackSelector
{
public GameObject[] FindTargets ()
{
return new GameObject[]{ new GameObject("Sector1"),new GameObject("Sector2")};
}
}
public class CircleAttackSelector:IAttackSelector
{
public GameObject[] FindTargets ()
{
return new GameObject[]{ new GameObject("circle1"),new GameObject("circle2")};
}
}
不使用工厂模式创建Selector对象:
public class Test:MonoBehaviour
{
{
GameObject[] arr1 = CreateAttackSelector ("circle").FindTargets ();
foreach (var item in arr1)
Debug.Log (item.name);
GameObject[] arr2 = CreateAttackSelector ("sector").FindTargets ();
foreach (var item in arr2)
Debug.Log (item.name);
}
private IAttackSelector CreateAttackSelector (string selector)
{
switch (selector)
{
case "circle":
return new CircleAttackSelector ();
case "sector":
return new SectorAttackSelector ();
default :
return null;
}
}
}
如果有多个类创建Selector对象就会导致多次重写CreateAttackSelector方法,这样就导致了大量的重复代码,维护性就变得极差。
我们做得就是将CreateAttackSelector方法封装到单独一个类中。
public class Factory
{
public static IAttackSelector CreateAttackSelector (string selector)
{
switch (selector)
{
case "circle":
return new CircleAttackSelector ();
case "sector":
return new SectorAttackSelector ();
default :
return null;
}
}
这是我们只要在需要创建Selector的地方通过Factory的CreateAttackSelector方法就可以了。
不需要在客户端多次重写CreateAttackSelector。
public class FactoryTest : MonoBehaviour
{
private void Start ()
{
GameObject[] arr1 = Factory.CreateAttackSelector ("circle").FindTargets ();
foreach (var item in arr1)
Debug.Log (item.name);
GameObject[] arr2 = Factory.CreateAttackSelector ("sector").FindTargets ();
foreach (var item in arr2)
Debug.Log (item.name);
}
}
}
这样似乎实现了代码的复用性。但是如果再次出现矩形、菱形等其他类型的选择器时,就不得不再次修改CreateAttackSelector方法,如果选择器有几十种,就会使得switch语句极其庞大。
如何避免出现switch惊悚语句,答案就是使用反射。
public class Factory
{
public static IAttackSelector CreateAttackSelector (string selectorName)
{
Type objType = Type.GetType(selectorName + "AttackSelector");
return Activator.CreateInstance(objType) as IAttackSelector;
}
}
工厂模式中就是应对多选一的场景的是简单工厂,也称为静态工厂;还有一种多选多(一个系列)的场景,则使用抽象工厂。
例如:
游戏中的角色数据、技能数据、任务数据都是动态获取的。那么获取数据的方式就会有多中,可能从XML中获取,也可能从数据库中获取,还有可能从普通文件中获取。因此我们就需要根据不同的数据类型创建不同的工厂来从不同的数据类型文件中获取数据。
public interface ISkillService
{
string GetSkillData();
}
public class XMLSkillService:ISkillService
{
public string GetSkillData ()
{
return "Skill";
}
}
public class DBSkillService:ISkillService
{
public string GetSkillData ()
{
return "Skill";
}
}
public interface ICharacterService
{
string GetCharacterData();
}
public class XMLCharacterService: ICharacterService
{
public string GetCharacterData ()
{
return "Character";
}
}
public class DBCharacterService: ICharacterService
{
public string GetCharacterData ()
{
return "Character";
}
}
public interface IFactory
{
ICharacterService GetCharacterService ();
ISkillService GetSkillService ();
}
public class XMLFactory:IFactory
{
public ICharacterService GetCharacterService ()
{
return new XMLCharacterService ();
}
public ISkillService GetSkillService ()
{
return new XMLSkillService ();
}
}
public class DBFactory:IFactory
{
public ICharacterService GetCharacterService ()
{
return new DBCharacterService ();
}
public ISkillService GetSkillService ()
{
return new DBSkillService ();
}
}
public class Test:MonoBehaviour
{
private void Start()
{
ISkillService skillService = new DBFactory ().GetSkillService ();
skillService.GetSkillData ();
ICharacterService characterService = new DBFactory ().GetCharacterService ();
characterService.GetCharacterData ();
}
}
当出现从File普通文件中获取数据时,只需要创建FileFactory工厂类和FileCharacterService和FileSkillService就可以,不需要修改以前的代码,符合开闭原则。
1.但是这样会带来平行继承的问题,如果此时增加从服务器获取数据就需要再次增加工厂类,使得客户端调用者需记住使用哪个类型的工厂,因为我们希望数据来源来自一个地方,技能和角色数据统一来自XML或者来自数据库,这样如果中间修改数据来源,就需要客户端调用地方全部替换为一个系列,这样的修改对于客户端而言是比较痛苦的。
2.如果此时增加一个获取任务数据的方式,就会造成更麻烦的事情,我们必须要修改IFactory接口,增加GetTaskService()的方法。这样一来所用工厂类都需要修改。
注:抽象工厂类适合解决纵向变化(比如增加File获取数据方式,不需要修改以前的代码),而不适合解决横向变化(增加获取任务的方式,需要修改接口,改动较大)。虽然会带来平行继承的问题,但实际开发中可能并没有那么多平行类,因此抽象工厂还是比较有用的。
有没好的方式解决平行继承,切换数据获取方式不需要修改客户端代码:
那还得使用简单工厂,但是需要做一下简单修改:利用传参的方式,通过switch…case 判断来创建不同的Service,但是因为switch语句带有会使代码膨胀的风险,所以改用反射,这是简单工厂的实现方式。
public class FactoryNormal
{
public static ISkillService CreateSkillService (string type)
{
Type objType = Type.GetType (type + "SkillService");
return Activator.CreateInstance (objType) as ISkillService;
}
public static ICharacterService CreateCharacterService (string type)
{
Type objType = Type.GetType (type + "CharacterService");
return Activator.CreateInstance (objType) as ICharacterService;
}
}
public class Test:MonoBehaviour
{
private void Start()
{
ISkillService skillService = FactoryNormal.CreateSkillService ("XML");
print(skillService.GetSkillData ());
ICharacterService characterService = FactoryNormal.CreateCharacterService ("XML");
print(characterService.GetCharacterData ());
}
}
解决了平行继承的问题,但是客户端调用比较麻烦,因为需要记住获取数据的方式。这里就不能使用传统的简单工厂,因为是多选多(一个系列),因此我们应该将type封装在工厂类中。
public class FactoryNormal
{
private static string type;
static FactoryNormal()
{
type = Resources.Load<TextAsset> ("Version").text;
}
public static ISkillService CreateSkillService ()
{
return CreateService<ISkillService>("Skill");
}
public static ICharacterService CreateCharacterService ()
{
return CreateService<ICharacterService>("Character");
}
private static T CreateService<T>(string name) where T:class
{
Type objType = Type.GetType (type + name +"Service");
return Activator.CreateInstance (objType) as T;
}
}
修改Resources文件夹下Version文件中内容就可以切换数据获取方式(“XML”换成“DB”就可以将从XML文件中获取数据的方式切为从DB中);如果增加文件数据获取方式,不需要修改Factory类,若果增加任务数据,只需要在Factory类中增加一个CreateTaskService的方法即可,改动相对也比较小。
关于抽象工厂和简单工厂的对比:(工厂方法模式不再举例)
静态(简单)工厂模式(Switch…case变庞大) 改进:(通过反射解决Switch惊悚现身)
抽象工厂模式(多选多 成系列创建)(平行继承体系) 改进:(简单工厂方式实现抽象抽象工场思维,利用反射) 配置注入 设置注入 构造注入
工厂方法 模式(多选一)(工厂实现类增多,平行继承体系)(废弃)