抽象能使你的代码变糟
假如你正在做一个游戏项目,你会有许多的物品类 Object
和 玩家类 Player
,它们各自有不同的功能,也有一些共同的功能。此时为了代码复用,减少代码重复,很多架构师都会考虑使用 继承
来复用它们之间重复的代码。
这就是抽象 Abstraction
,而你刚刚创建了一个抽象。
目录
抽象带来的问题
架构师非常擅长识别重复的内容,并将其提取出来。对于代码来说,代码重复是糟糕的,而更多的抽象意味着更高的代码复用,这是公认的结论。
但是还有一个没有被考虑的问题:耦合(COUPLING)。
大多数人从概念上理解了什么是耦合,并且在尝试重构过度耦合的系统的时候,他们也感受到了耦合的存在。但是在设计软件的时候,并不能直接感受到耦合的影响
耦合和抽象的关系
耦合是抽象的一种附加效应,简单来说就是正比关系。
更多的抽象就意味着更多的耦合。
假如你现在有一个类 SaveXML
:
using System.Text.Json;
using System.IO;
class SaveXML
{
privete string fileName;
public SaveXML(string fileName)
{
this.fileName = fileName;
}
public void save(GameState state)
{
XmlWriterOptions option = new XmlWriterOptions();
...
}
}
此时由于架构升级,需要使用 SaveJSON
类。
新建一个类
如果直接新建一个 SaveJSON
类的话,那么可以很轻松的发现这两个类的大部分代码是相同的,而且智能 IDE 也可以检测出来部分重复代码。
using System.Text.Json;
using System.IO;
class SaveJSON
{
privete string fileName;
public SaveJSON(string fileName)
{
this.fileName = fileName;
}
public void save(GameState state)
{
JsonWriterOptions option = new JsonWriterOptions();
...
}
}
显然这是不优雅的。
抽象尝试
我们的直觉就是创建一个名为 FileSaver
的通用类作为上述类的父类。
class FileSaver
{
protected string filename;
public FileSaver(string filename)
{
this.filename = filename;
}
}
FileSaver
的子类可以从这个受保护变量中获取文件名 filename
。
using System.Text.Json;
using System.IO;
class SaveXML : FileSaver
{
public SaveXML(string fileName)
: base(filename)
{ }
public void save(GameState state)
{
XmlWriterOptions option = new XmlWriterOptions();
...
}
}
class SaveJSON : FileSaver
{
public SaveJSON(string fileName)
: base(filename)
{}
public void save(GameState state)
{
JsonWriterOptions option = new JsonWriterOptions();
...
}
}
但这是一个糟糕的做法。
我们现在将两个类获取文件名filename
的途径耦合到了相同的输入源 – 父类 FileSaver
。
而且这种改变并没有任何实质性的作用,对我们没有任何帮助。
再重构
现在我们考虑写一个 save
接口。
class FileSaver
{
void save(GameState state);
}
我们知道,这也会增加两个类之间的耦合。因为现在,这两个类都被限制在了同一个 save
方法中。如果后期需要修改,一旦修改了接口,所有与该接口有关的类都需要进行更改。这种做法没有带来任何好处。
该类的使用
看看这个重构之后的类是如何使用的。
void Save(GameConfig gameConfig)
{
FileSaver saver;
if (gameConfig.saveMode == SaveMode.Xml)
{
saver = new SaveXML("gameSave.xml");
}
else if (gameConfig.saveMode == SaveMode == SaveMode.Json)
{
saver = new SaveJSON("gameSave.json")
}
else
{
throw new ArgumentException("Invalid save option");
}
saver.save(state);
}
可以看到,这种抽象并没有有效的简化程序。
在这个问题上,最好的做法就是:
将它们保留为两个完全没有联系的不同类。
值得抽象的两种情况
有三种以上类型的情况
如果我们有三个以上相似的类,那么它确实值得我们去抽象。
void Save(GameConfig gameConfig)
{
switch(gameConfig.saveMode)
{
case SaveMode.Xml:
SaveXML xml = new SaveXML(gameConfig.saveUrl);
xml.save(state);
break;
case SaveMode.Json:
...
case SaveMode.SqlLite:
...
case SaveMode.AWS:
...
default:
throw new ArgumentException("Invalid save option");
}
}
我们可以考虑将这些代码提取到单独的代码段:
class SaverFactory
{
private GameConfig.config;
public SaverFactory(GameConfig config)
{
this.config = config;
}
public FileSaver create()
{
switch(config.saveMode)
{
case SaveMode.Xml:
return new SaveXML(config.saveUrl);
break
case SaveMode.Json:
return new SaveJSON(config.saveUrl);
break;
case SaveMode.SqlLite:
return new SqlLiteSaver(config.saveUrl);
break;
case SaveMode.AWS:
return new AWSSaver(config.saveUrl);
break;
default:
throw new ArgumentException("Invalid save option");
}
}
}
此时,使用这项功能的代码就可以重构为:
void Save(GameConfig gameConfig)
{
SaverFactory factory = new SaverFactory(gameConfig);
FileSaver saver = factory.create();
saver.save();
}
需要延迟或者重复保存
例如,如果我们希望程序每五分钟自动保存一次。那么此时的定时器不知道这个 saver
对象是谁,也许有很多种类型的对象。那么这时候,抽象确实会使程序变得简洁。
结语
总的来说,最好只在抽象带来的价值超过耦合时才应用抽象。
这意味着,在没有使用抽象的时候,代码可能会有一些重复。
但是我认为,在修改代码时,少量的代码重复要比过度耦合带来的痛苦要小得多。