防止幻数和字符串的设计
Design to prevent magic numbers and strings
我正在构建一个从第三方检索并提交给第三方的服务。第三方提供了一个复杂的数据模型,其中包括三种不同的状态类型,它们都是带有模糊数字系列的整数。下面提供了一个示例(键:值):
HandlerStatus CustomerStatus ArchiveStatus
--- --- ---
activeUnallocated:40 draftOpen:0 openOpen:0
activeAllocated:26 submitted:2 closed:1
activePromoted:102 approved:100
Declined:2 declined:1
... ...
组合数量有限,每个组合都有期限(提交、委托给其他用户、等待确认等)。例如:
Combination called "Submitted":
HandlerStatus has to be "activeUnallocated" (40)
CustomerStatus has to be "submitted" (2)
ArchiveStatus has to be "openOpen" (0)
我们预计会添加其他值,用户友好的名称可能会随着时间的推移而改变。
此外,有些操作(如提交和委派给其他用户)不需要用户选择新值,这意味着服务需要知道在这些操作发生时必须设置的一组组合。
- 我必须能够为每个状态类型中的每个状态值定义用户友好的显示名称
- 我必须能够从用户那里接收新的状态值并映射到正确的状态类型
- 我必须能够查找已定义组合的值
- 我必须能够根据一组状态值查找定义的组合
我想到了三种不同的解决方案,各有利弊。
1。为每个状态类型定义 en enum
优点:
- 强类型
- 维护仅限一个文件
缺点:
- 枚举
- UI 演示文稿的字符串格式
2。在外部 JSON 文件中定义 key/value 对
优点:
- 添加无需更改代码
- 可以在运行时更新
缺点:
- 需要魔法 strings/numbers 用于具有关联组合的操作(无需用户输入的操作)
经常从文件系统读取
3。为不同的可用状态组合创建代理 类
优点:
- 强类型
- 灵活
缺点:
- 第三方系统发生变更时需要代码维护
我已经尝试研究最佳实践和模式,以查看哪种解决方案最适合可维护性和良好的代码设计,但我没有取得任何进展(与此同时,我在开发过程中严重依赖魔术字符串和数字).
您可以在 json、xml、resx、纯文本、sql 或任何您想要的格式中定义键值对。将三个状态字段合并为字符串键:
key value
A2|B3| Submitted
A2|B3|X Submitted with error
A1|B1| Started
etc...
然后在应用程序启动时,读取此文件并将其存储在字典中。
Dictionary<string, string> StatusTypes = new Dictionary<string, string>();
那就只有优点了:
- 在单独的文件中定义灵活
- 新状态无需更改代码
- 快速,在内存中,因此无需持续访问磁盘
首先也是最重要的:你在避免幻数和字符串方面是正确的。
一个经验法则是它是一个不会(经常)改变的整数,那么枚举是好的。如果它是一个不经常更改的字符串,那么其中包含字符串的静态(共享)class 是好的。在这些情况下,您的目标是集中定义。这为您提供了几个优势,例如避免拼写错误(在字符串中),使代码更具可读性(使用枚举名称而不是数字)并且您可以跟踪使用(查找用法)。请注意,您可能不想使用 const 关键字,除非您的值是固定不变的并且永远不会改变。
对于经常变化的值,您需要考虑程序逻辑来处理这些值。没有人硬编码客户姓名和地址(希望如此),因此如果您的状态经常更改,您希望像处理任何其他数据一样处理它们。如果它们与程序逻辑相关联,那么您需要定义需要什么程序逻辑,并以某种方式 link 提高状态。
对于更改频率较低的值,您可以选择硬编码 (enum/string) 和从配置文件读取。硬编码显然需要重新编译,然后您应该考虑长期成本。今天需要你 5 分钟才能完成的事情,将来可能需要几天时间。 2 年后,分配更新其中一个值的任务的另一位开发人员可能会花费几天时间来设置正确的开发环境、编译和测试。看到从文件中读取值是多么容易,我的建议通常是使用配置文件。
对于您的特定问题,您似乎需要的是翻译 table。您的应用程序有特定的任务要执行。答案似乎使应用程序通用并将翻译放在文件中。您希望在文件(或 sql-table)中保留对状态更新(什么会将什么状态转换成什么)和友好显示(对于用户)的约束。简而言之,使应用程序无法了解状态字段经历的转换。
不用担心文件IO。很容易实现缓存。
private static TranslationObject _translationObject = null;
public static TranslationObject GetTranslationObject() {
if (_translationObject == null)
lock (_translationObject)
_translationObject = JsonConvert.DeserializeObject<TranslationObject>(File.ReadAllTextt("TranslationTable.json"));
return _translationObject;
}
即使在网络应用程序中,应用程序也会存活一段时间,因此这会在请求之间缓存文件。无论如何,OS 应该有足够的内存来缓存几千字节数据的磁盘 IO。
为了说明多种实现方式中的一种,我添加了一个示例,其中将结构与字典结合使用,以便在友好状态和其他状态组合之间进行快速双向查找。我不确定这是否正是您所要求的,但希望它能对您有所帮助。
void Main()
{
// Get translation
var translationObject = GetTranslationObject();
// Find friendly status based on combo
var friendly1 = translationObject.ComboStatusToFriendlyStatus[new StatusCombo(0, 30, 5)];
// Find combo based on friendly status
var combo1 = translationObject.FriendlyStatusToComboStatus[0];
}
public struct StatusCombo
{
// Please note that fields are readonly for immutability.
// This is particularly important since the GetHashCode() value is used in dictionaries.
// Note that status fields can also be strings (because we use .GetHashCode() in GetHashCode()).
public readonly int Status1;
public readonly int Status2;
public readonly int Status3;
[JsonConstructor]
public StatusCombo(int status1, int status2, int status3)
{
Status1 = status1;
Status2 = status2;
Status3 = status3;
}
public override int GetHashCode()
{
unchecked
{
int hashCode = Status1.GetHashCode();
hashCode = (hashCode * 397) ^ Status2.GetHashCode();
hashCode = (hashCode * 397) ^ Status3.GetHashCode();
// ... Repeat for every extra statuscode you add
return hashCode;
}
}
}
public class TranslationObject
{
public Dictionary<int, string> Status1Mapping;
public Dictionary<int, string> Status2Mapping;
public Dictionary<int, string> Status3Mapping;
public Dictionary<int, string> FriendlyStatus;
public Dictionary<int, StatusCombo> FriendlyStatusToComboStatus;
[JsonIgnore]
public Dictionary<StatusCombo, int> ComboStatusToFriendlyStatus;
}
private static TranslationObject _translationObject = null;
public static TranslationObject GetTranslationObject()
{
if (_translationObject == null)
lock ("Reading _translationObject")
{
_translationObject = JsonConvert.DeserializeObject<TranslationObject>(File.ReadAllText(@"TranslationTables.json"));
// Populate reverse lookup
_translationObject.ComboStatusToFriendlyStatus=new Dictionary<UserQuery.StatusCombo, int>();
foreach (var t in _translationObject.FriendlyStatusToComboStatus)
_translationObject.ComboStatusToFriendlyStatus.Add(t.Value, t.Key);
}
return _translationObject;
}
示例 JSON 文件:
{
"Status1Mapping": {
"0": "Status1_0",
"10": "Status1_1"
},
"Status2Mapping": {
"30": "Status2_0",
"55": "Status2_1"
},
"Status3Mapping": {
"5": "Status3_0",
"2": "Status3_1"
},
"FriendlyStatus": {
"0": "Submitted",
"1": "Received"
},
"FriendlyStatusToComboStatus": {
"0": {
"Status1": 10,
"Status2": 55,
"Status3": 2
},
"1": {
"Status1": 0,
"Status2": 30,
"Status3": 5
}
}
}
以及我用来填充示例的代码 JSON:
var tro = new TranslationObject();
tro.Status1Mapping = new Dictionary<int, string>();
tro.Status2Mapping = new Dictionary<int, string>();
tro.Status3Mapping = new Dictionary<int, string>();
tro.Status1Mapping.Add(0, "Status1_0");
tro.Status1Mapping.Add(10, "Status1_1");
tro.Status2Mapping.Add(30, "Status2_0");
tro.Status2Mapping.Add(55, "Status2_1");
tro.Status3Mapping.Add(5, "Status3_0");
tro.Status3Mapping.Add(2, "Status3_1");
tro.FriendlyStatus = new Dictionary<int, string>();
tro.FriendlyStatus.Add(0, "Submitted");
tro.FriendlyStatus.Add(1, "Received");
tro.FriendlyStatusToComboStatus = new Dictionary<int, UserQuery.StatusCombo>();
tro.FriendlyStatusToComboStatus.Add(0, new StatusCombo(10, 55, 2));
tro.FriendlyStatusToComboStatus.Add(1, new StatusCombo(0, 30, 5));
File.WriteAllText(@"TranslationTables.json", JsonConvert.SerializeObject(tro));
我正在构建一个从第三方检索并提交给第三方的服务。第三方提供了一个复杂的数据模型,其中包括三种不同的状态类型,它们都是带有模糊数字系列的整数。下面提供了一个示例(键:值):
HandlerStatus CustomerStatus ArchiveStatus --- --- --- activeUnallocated:40 draftOpen:0 openOpen:0 activeAllocated:26 submitted:2 closed:1 activePromoted:102 approved:100 Declined:2 declined:1 ... ...
组合数量有限,每个组合都有期限(提交、委托给其他用户、等待确认等)。例如:
Combination called "Submitted": HandlerStatus has to be "activeUnallocated" (40) CustomerStatus has to be "submitted" (2) ArchiveStatus has to be "openOpen" (0)
我们预计会添加其他值,用户友好的名称可能会随着时间的推移而改变。
此外,有些操作(如提交和委派给其他用户)不需要用户选择新值,这意味着服务需要知道在这些操作发生时必须设置的一组组合。
- 我必须能够为每个状态类型中的每个状态值定义用户友好的显示名称
- 我必须能够从用户那里接收新的状态值并映射到正确的状态类型
- 我必须能够查找已定义组合的值
- 我必须能够根据一组状态值查找定义的组合
我想到了三种不同的解决方案,各有利弊。
1。为每个状态类型定义 en enum
优点:
- 强类型
- 维护仅限一个文件
缺点:
- 枚举
- UI 演示文稿的字符串格式
2。在外部 JSON 文件中定义 key/value 对
优点:
- 添加无需更改代码
- 可以在运行时更新
缺点:
- 需要魔法 strings/numbers 用于具有关联组合的操作(无需用户输入的操作)
经常从文件系统读取
3。为不同的可用状态组合创建代理 类
优点:
- 强类型
- 灵活
缺点:
- 第三方系统发生变更时需要代码维护
我已经尝试研究最佳实践和模式,以查看哪种解决方案最适合可维护性和良好的代码设计,但我没有取得任何进展(与此同时,我在开发过程中严重依赖魔术字符串和数字).
您可以在 json、xml、resx、纯文本、sql 或任何您想要的格式中定义键值对。将三个状态字段合并为字符串键:
key value
A2|B3| Submitted
A2|B3|X Submitted with error
A1|B1| Started
etc...
然后在应用程序启动时,读取此文件并将其存储在字典中。
Dictionary<string, string> StatusTypes = new Dictionary<string, string>();
那就只有优点了:
- 在单独的文件中定义灵活
- 新状态无需更改代码
- 快速,在内存中,因此无需持续访问磁盘
首先也是最重要的:你在避免幻数和字符串方面是正确的。
一个经验法则是它是一个不会(经常)改变的整数,那么枚举是好的。如果它是一个不经常更改的字符串,那么其中包含字符串的静态(共享)class 是好的。在这些情况下,您的目标是集中定义。这为您提供了几个优势,例如避免拼写错误(在字符串中),使代码更具可读性(使用枚举名称而不是数字)并且您可以跟踪使用(查找用法)。请注意,您可能不想使用 const 关键字,除非您的值是固定不变的并且永远不会改变。
对于经常变化的值,您需要考虑程序逻辑来处理这些值。没有人硬编码客户姓名和地址(希望如此),因此如果您的状态经常更改,您希望像处理任何其他数据一样处理它们。如果它们与程序逻辑相关联,那么您需要定义需要什么程序逻辑,并以某种方式 link 提高状态。
对于更改频率较低的值,您可以选择硬编码 (enum/string) 和从配置文件读取。硬编码显然需要重新编译,然后您应该考虑长期成本。今天需要你 5 分钟才能完成的事情,将来可能需要几天时间。 2 年后,分配更新其中一个值的任务的另一位开发人员可能会花费几天时间来设置正确的开发环境、编译和测试。看到从文件中读取值是多么容易,我的建议通常是使用配置文件。
对于您的特定问题,您似乎需要的是翻译 table。您的应用程序有特定的任务要执行。答案似乎使应用程序通用并将翻译放在文件中。您希望在文件(或 sql-table)中保留对状态更新(什么会将什么状态转换成什么)和友好显示(对于用户)的约束。简而言之,使应用程序无法了解状态字段经历的转换。
不用担心文件IO。很容易实现缓存。
private static TranslationObject _translationObject = null;
public static TranslationObject GetTranslationObject() {
if (_translationObject == null)
lock (_translationObject)
_translationObject = JsonConvert.DeserializeObject<TranslationObject>(File.ReadAllTextt("TranslationTable.json"));
return _translationObject;
}
即使在网络应用程序中,应用程序也会存活一段时间,因此这会在请求之间缓存文件。无论如何,OS 应该有足够的内存来缓存几千字节数据的磁盘 IO。
为了说明多种实现方式中的一种,我添加了一个示例,其中将结构与字典结合使用,以便在友好状态和其他状态组合之间进行快速双向查找。我不确定这是否正是您所要求的,但希望它能对您有所帮助。
void Main()
{
// Get translation
var translationObject = GetTranslationObject();
// Find friendly status based on combo
var friendly1 = translationObject.ComboStatusToFriendlyStatus[new StatusCombo(0, 30, 5)];
// Find combo based on friendly status
var combo1 = translationObject.FriendlyStatusToComboStatus[0];
}
public struct StatusCombo
{
// Please note that fields are readonly for immutability.
// This is particularly important since the GetHashCode() value is used in dictionaries.
// Note that status fields can also be strings (because we use .GetHashCode() in GetHashCode()).
public readonly int Status1;
public readonly int Status2;
public readonly int Status3;
[JsonConstructor]
public StatusCombo(int status1, int status2, int status3)
{
Status1 = status1;
Status2 = status2;
Status3 = status3;
}
public override int GetHashCode()
{
unchecked
{
int hashCode = Status1.GetHashCode();
hashCode = (hashCode * 397) ^ Status2.GetHashCode();
hashCode = (hashCode * 397) ^ Status3.GetHashCode();
// ... Repeat for every extra statuscode you add
return hashCode;
}
}
}
public class TranslationObject
{
public Dictionary<int, string> Status1Mapping;
public Dictionary<int, string> Status2Mapping;
public Dictionary<int, string> Status3Mapping;
public Dictionary<int, string> FriendlyStatus;
public Dictionary<int, StatusCombo> FriendlyStatusToComboStatus;
[JsonIgnore]
public Dictionary<StatusCombo, int> ComboStatusToFriendlyStatus;
}
private static TranslationObject _translationObject = null;
public static TranslationObject GetTranslationObject()
{
if (_translationObject == null)
lock ("Reading _translationObject")
{
_translationObject = JsonConvert.DeserializeObject<TranslationObject>(File.ReadAllText(@"TranslationTables.json"));
// Populate reverse lookup
_translationObject.ComboStatusToFriendlyStatus=new Dictionary<UserQuery.StatusCombo, int>();
foreach (var t in _translationObject.FriendlyStatusToComboStatus)
_translationObject.ComboStatusToFriendlyStatus.Add(t.Value, t.Key);
}
return _translationObject;
}
示例 JSON 文件:
{ "Status1Mapping": { "0": "Status1_0", "10": "Status1_1" }, "Status2Mapping": { "30": "Status2_0", "55": "Status2_1" }, "Status3Mapping": { "5": "Status3_0", "2": "Status3_1" }, "FriendlyStatus": { "0": "Submitted", "1": "Received" }, "FriendlyStatusToComboStatus": { "0": { "Status1": 10, "Status2": 55, "Status3": 2 }, "1": { "Status1": 0, "Status2": 30, "Status3": 5 } } }
以及我用来填充示例的代码 JSON:
var tro = new TranslationObject();
tro.Status1Mapping = new Dictionary<int, string>();
tro.Status2Mapping = new Dictionary<int, string>();
tro.Status3Mapping = new Dictionary<int, string>();
tro.Status1Mapping.Add(0, "Status1_0");
tro.Status1Mapping.Add(10, "Status1_1");
tro.Status2Mapping.Add(30, "Status2_0");
tro.Status2Mapping.Add(55, "Status2_1");
tro.Status3Mapping.Add(5, "Status3_0");
tro.Status3Mapping.Add(2, "Status3_1");
tro.FriendlyStatus = new Dictionary<int, string>();
tro.FriendlyStatus.Add(0, "Submitted");
tro.FriendlyStatus.Add(1, "Received");
tro.FriendlyStatusToComboStatus = new Dictionary<int, UserQuery.StatusCombo>();
tro.FriendlyStatusToComboStatus.Add(0, new StatusCombo(10, 55, 2));
tro.FriendlyStatusToComboStatus.Add(1, new StatusCombo(0, 30, 5));
File.WriteAllText(@"TranslationTables.json", JsonConvert.SerializeObject(tro));