多样式星期名字转换 [Design, C#]

多样式星期名字转换 [Design, C#]

Written by Allen Lee

1. 原来的问题...

Johnsuna 在我的《关于枚举的种种 [C#, IL, BCL]》那里提出了这样一个问题

现在我想做一个多版本的带农历的中国万年历,月历中有星期日、星期一至六,我想使用"星期一","一"或"Monday", "Mon",或"M",但也可能使用其组合,如“星期一Mon”, 于是我定义一个公共Style枚举,里面有ChineseFullName, ChineseShortName, EnglishFullName,EnglishShortName, EnglishSingleLetter等,然后又定义一个公共的ChineseFullName枚举,里面有:星期一,星期二等等,类似的EnglishShortName:Mon,Tue等(类推)。

因为可能要进行组合,比如Style为:ChineseFullName|EnglishShortName以便得到“星期一MON”的结果,所以考虑使用枚举而未使用数组。

我的问题是:

  1. 如果现在选择的是星期五,Style样式为EnglishShortName,如何取得“Fri”的字符串?如果样式Style选择的是EnglishShortName | ChineseShortName,又如何得到“Fri一”呢?
  2. 如果EnglishSingleLetter枚举,则其枚举值为:M,T,W,T,F,S,S,很明显,T(Tuesday)与T(Thursday)重复,S(Saturday)与S(Sunday)重复,这在枚举中是不允许的。那么,如何才能正确实现呢?

2. 正向方案:实施授权。

根据 Johnsuna 的需求,我们得首先有一个 DayStyle 枚举来表示各种可用样式的名字,由于需求明确提出要进行样式的复合表示,所以我们必须在枚举上加上 FlagsAttribute:

// Code#01

[Flags]
enum DayStyle
{
ChineseFullName
=0x0001,
ChineseShortName
=0x0002,
EnglishFullName
=0x0004,
EnglishShortName
=0x0008,
EnglishSingleLetter
=0x0010,
}

补充阅读:

如果你不知道我为什么这样为 DayStyle 的成员设值,你可以看看我的《关于枚举的种种 [C#, IL, BCL]》,我在文章讲述了位枚举的值的设置以及相关注意事项。

下面,我将采用 TDD 的思维一步一步探讨这个问题的解决方案。首先,根据上面的需求,我认为 Johnsuna 会喜欢下面这种做法:

// Code#02

textBox1.Text
= day.ToString(
DayStyle.ChineseFullName
|
DayStyle.ChineseShortName
|
DayStyle.EnglishFullName
|
DayStyle.EnglishShortName
|
DayStyle.EnglishSingleLetter
);

上面这种方法很明显需要我们在 ToString 里面分析客户端把一个什么样的 DayStyle 传递进来了,当然包括单独一个 DayStyle 成员以及通过“|”运算符得到的成员组合这两种情况。

我们知道,当某个位枚举变量和某一位枚举成员的按位与操作结果不为零时,该变量包含了该枚举成员。这样,下面的代码可以放到 ToString 里面用于进行与 ChineseFullName 相关的操作:

// Code#03

if ((style & DayStyle.ChineseFullName) != 0 )
{
//Oh,youwanttheChinesefullname!
}

我将用同样的方法处理其他枚举成员,由于需求中提到希望的到类似“星期一Mon”这样的结果,所以我们不难想象到 ToString 里面将会涉及到字符串相加这一操作。执行这一操作的最佳人员当然就是 StringBuilder 了,于是我们有了下面的设想方案:

// Code#04

if ((style & DayStyle.ChineseFullName) != 0 )
{
m_Content.Append(
"星期一");
}


if ((style & DayStyle.EnglishShortName) != 0 )
{
m_Content.Append(
"Mon");
}

补充阅读:

关于字符串的常见操作以及与之相关的性能问题,我推荐你阅读我所翻译的 Performance considerations for strings in C#,该文比较了直接在 String 对象上执行这些操作和使用 StringBuilder 来执行这些操作在性能上的不同表现,并且从定量的角度给出了一个用于判断使用哪种方法来执行这些常见操作的参考标准。

然而,上面的代码仅能用于示意,它不能真正用到实际中,为什么呢?很明显,如果我们硬编码这些星期的表示,那么 ToString 至少还需要一个参数来判断客户端需要星期几的表示。这个方案除了加重了我们的劳动强度让我们有借口叫老板加工资外,它几乎没有其他好处了。所以,我们必须想办法把这个“星期几”的具体表示分离出来。如何做到呢?很明显,Template Method 模式是一个让我们的繁重工作得到解脱的办法。

现在,我们要做的就是声明一个抽象类 Day 以及一系列的抽象方法,例如 ToChineseFullName,让 Day 的派生类,例如 Monday,实现这一组抽象方法,而 Day.ToString 就通过这一组抽象方法把获取具体的表示的工作“授权”给那些派生类:

// Code#05

protected abstract string ToChineseFullName();

public string ToString(DayStylestyle)
{
if((style&DayStyle.ChineseFullName)!=0)
{
m_Content.Append(ToChineseFullName());
}


//
}

好吧,现在万事俱备,只欠子类了:

// Code#06

class Monday:Day
{
protectedoverridestringToChineseFullName()
{
return"星期一";
}


//
}

好了,剩下的工作就是如法炮制其他派生类,虽然这些工作是劳动密集型的,但你仍得着手完成它。下面是 Monday 的实现:

正向方案 #region正向方案

usingSystem;
usingSystem.Collections.Generic;
usingSystem.Text;

namespaceMyUtils.Day
{
classProgram
{
staticvoidMain(string[]args)
{
Dayday
=newMonday();
Console.WriteLine(day.ToString(
DayStyle.ChineseFullName
|
DayStyle.ChineseShortName
|
DayStyle.EnglishFullName
|
DayStyle.EnglishShortName
|
DayStyle.EnglishSingleLetter
));
}

}


publicabstractclassDay
{
privateStringBuilderm_Content=newStringBuilder();

protectedabstractstringToChineseFullName();

protectedabstractstringToChineseShortName();

protectedabstractstringToEnglishFullName();

protectedabstractstringToEnglishShortName();

protectedabstractstringToEnglishSingleLetter();

publicstringToString(DayStylestyle)
{
if((style&DayStyle.ChineseFullName)!=0)
{
m_Content.Append(ToChineseFullName());
}


if((style&DayStyle.ChineseShortName)!=0)
{
m_Content.Append(ToChineseShortName());
}


if((style&DayStyle.EnglishFullName)!=0)
{
m_Content.Append(ToEnglishFullName());
}


if((style&DayStyle.EnglishShortName)!=0)
{
m_Content.Append(ToEnglishShortName());
}


if((style&DayStyle.EnglishSingleLetter)!=0)
{
m_Content.Append(ToEnglishSingleLetter());
}


returnm_Content.ToString();
}


publicoverridestringToString()
{
returnToString(DayStyle.ChineseFullName);
}

}


publicclassMonday:Day
{
protectedoverridestringToChineseFullName()
{
return"星期一";
}


protectedoverridestringToChineseShortName()
{
return"";
}


protectedoverridestringToEnglishFullName()
{
return"Monday";
}


protectedoverridestringToEnglishShortName()
{
return"Mon";
}


protectedoverridestringToEnglishSingleLetter()
{
return"M";
}

}


[Flags]
publicenumDayStyle
{
ChineseFullName
=0x0001,
ChineseShortName
=0x0002,
EnglishFullName
=0x0004,
EnglishShortName
=0x0010,
EnglishSingleLetter
=0x0020,
}

}


#endregion

值得注意的是,我在 Day 中重载了 Object.ToString 方法,因为我不希望在任何可能隐式调用 Object.ToString 的地方得到一个类型的名字。你可以把这个重载后的版本看作是默认的样式表示转换,套用任何一个你喜欢的转换策略。

3. 后来的问题...

上面那个方案可行,但就是不好,因为它隐藏了一个,一旦这个爆炸,我们不但没借口向老板提出加工资,还要受老板责怪,并且要自己加班收拾残局。你能猜到这个是什么吗?

让我们回顾一下 DayStyle 枚举,这个枚举现在表明我们将可以处理两种语言,并且每种语言至少有两种表达方式,你是否想到了什么呢?如果我们现在要加多一种语言呢?问题就在这里了,这个枚举的成员不够稳定。现在我们已经要处理中文和英文两种语言了,将来难免要处理更多的语言,因为你可能希望你的程序面向国际市场。更糟糕的是,我们并不知道我们将要添加的语种有多少种表达方式,至少现在的情况是英语比汉语多了一个“首字母”表达方式,那么有谁又能知道将要添加的语中会带有哪些稀奇古怪的特殊表达方式呢?

很明显,DayStyle 枚举的不稳定将会扩散到与之相关的每一个角落,而维护这样的代码可能会变成一场噩梦。

如果你读过 Alan ShallowayJames R. TrottDesign Patterns Explained,你应该会记得他们对封装的精辟阐述:

Find what is varying and encapsulate it.

接下来,我将会尝试把变化的根源封装起来...

4. 反向方案:封装变化。

如果你读过我关于枚举的文章,你会知道我主张把枚举看作一种分类手段,并且仅当分类的细节相对稳定的情况下才考虑使用枚举,否则你应该考虑别的出路。

那么,在我们要处理的问题域中,哪些因素容易改变,哪些因素又较为稳定呢?很明显,DayStyle 绝对不会被归入稳定因素的行列,于是剩下的就是 Day 了,那么 Day 稳定吗?当然稳定,因为一个星期有且仅有7天,并且每天的名字也是固定的。那么,把 Day 表示成枚举就合适了:

// Code#07

enum Day
{
Sunday,
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
}

下面,我也将采用 TDD 的思维来一步一步探讨该问题的解决方案。由于我不知道 Johnsuna 会否喜欢上这个方案,我只能假设有一个喜欢这个方案的用户 Foo 了。既然 Foo 喜欢这个方案,他将很有可能使用如下方式来处理转换:

// Code#08

NameConverternc
= new NameConverter();
nc.AddInnerNameConverters(
new ChineseFullNameConverter(),
new ChineseShortNameConverter(),
new EnglishFullNameConverter(),
new EnglishShortNameConverter(),
new EnglishSingleLetterConverter()
);
textBox1.Text
= nc.ToString(Day,Saturday);

从上面的代码可以看出,NameConverter 就是整个转换工程的“总管事”,而那些 XXXConverter 就是负责具体转换工作的“员工”。

“总管事”在这里的工作只是询问客户需要哪些转换,然后安排相关“员工”来完成具体的工作。由于我们将来可能会扩展更多的 XXXConverter 以满足更多的需求,所以 NameConverter 不可能(预先)了解到所有的这些具体的转换细节。为了使得“总管事”能够更好的统一管理整个转换过程,我们需要实施“目标管理”,让“总管事”关注“员工”的工作成果而不是工作过程。

那么,如何实施“目标管理”呢?既然“总管事”关注结果,那么我们可以考虑制定一个“行为规范”,即接口约定:

// Code#09

interface INameConverter
{
stringToString(Dayday);
}

然后让所有准备参与工作的“员工”“学习”这一“行为规范”,例如:

// Code#10

class EnglishFullNameConverter:INameConverter
{
stringINameConverter.ToString(Dayday)
{
returnday.ToString();
}

}

好了,现在我们要改变关注点,接下来,我们来看看“总管事”是如何管理“员工”的。作为“总管事”,它的职责有如下三点:

  1. 获悉客户的需求,即询问客户需要哪些转换服务;
  2. 安排相关的“员工”来处理具体的转换工作;
  3. 把最后的工作成果交到客户的手里。

首先我们来看第一点,为了更好的管理客户的需求,我们得首先把这些需求以后种方式收集起来:

// Code#11

private List < INameConverter > m_NameConverters = new List < INameConverter > ();

然后就是接收客户的需求了:

// Code#12

public void AddInnerNameConverter(INameConvert
分享到:
评论

你可能感兴趣的:(C++,c,工作,TDD,C#)