我的App《上海轨道交通》有个很坑的bug,按拼音首字母分组的站点列表会出现分组错误的情况,比如“莘庄”应该在X下,而WP的SortedLocaleGrouping居然把它分在了S下。这是因为“莘庄”的“莘”是多音字。同理,应该在C下的“长江南路”也到了“Z”下。类似的还有:
分组的代码用的是MSDN的范例改的:http://msdn.microsoft.com/library/windows/apps/jj244365(v=vs.105).aspx 这个范例对于英文来说是没问题的,中文就会出现多音字的问题。
为了解决这个坑爹问题,我写了个PinYinGroupResolver,用的时候可以写出很装逼的代码,比如这样:
GroupedStations = new PinYinGroupResolver<Station>(sGroup) .For(s => s.StationName == "莘庄", "S", "X") .For(s => s.StationName == "长江南路", "Z", "C") .Resolve() .ToObservableCollection();
其中,sGroup就是用MSDN伟大光荣正确的分组类分出的List
var sGroup = AlphaKeyGroup<Station>.CreateGroups( AllStations, new CultureInfo("zh-CN"), s => s.StationName.Substring(0, 1), true);
少年们肯定要问了,为什么我不在AlphaKeyGroup里面就做判断,直接把莘庄、长江南路撸正确呢?为什么还要多此一举写PinYinGroupResolver呢?我是不是傻逼啊?
其实。。。这就是菜鸟和高手的区别。菜鸟不会考虑代码重用,碰到任何问题都喜欢用高耦合的方法去改,而这样就影响了一个function应该保持的原则:只做一件事。如果在AlphaKeyGroup里面写死了莘、长等站点名称首字母的判断,那首先你这个AlphaKeyGroup就被束缚在中文的应用里了,并且你也只能把本来泛型T改成Station类了,并且只针对这一个应用了,再也不能愉快的重用到别的地方了。我们需要的是“切入”的做法,不能修改AlphaKeyGroup,要把灵活性留出来。所以一定要另外写个类,专门查找和修复拼音首字母分组中错误的项。
首先,因为要处理多个分组错误,我们需要一个数据模型,描述错误的分组和他们应该在哪个组。
public class PinYinGroupResolverItem<T> { public Predicate<T> MatchPredicate { get; set; } public string SourceKeyName { get; set; } public string TargetKeyName { get; set; } }
注意看本高手的设计思路,始终要记住高内聚、低耦合!所以这是个泛型类,并且把对象赋值的工作“外包”给了一个Predicate<T>,这种类型就允许你用装逼Lambda表达式来赋值:s => s.StationName == "莘庄"
下面开始创建非常高大上的PinYinGroupResolver。我们需要两个属性:
TargetGroup:用于存放被AlphaKeyGroup分组后的,包含错误项的原始分组数据。
PinYinGroupResolverItems:用于存放需要被修正的那些分组项的信息。
public class PinYinGroupResolver<T> { public PinYinGroupResolver(List<AlphaKeyGroup<T>> fuckedGroupedItems) { TargetGroup = fuckedGroupedItems; PinYinGroupResolverItems = new List<PinYinGroupResolverItem<T>>(); } public List<AlphaKeyGroup<T>> TargetGroup { get; private set; } public List<PinYinGroupResolverItem<T>> PinYinGroupResolverItems { get; set; } //... more }
直接用带参构造给TargetGroup赋值,这样就强制调用者做正确的事情(如果你在设计API给别人用就得有这样的思想),他们就知道这个PinYinGroupResolver的TargetGroup对象是一定要赋值的。
于是调用者就有了
GroupedStations = new PinYinGroupResolver<Station>(sGroup)
下面就要增加这样的装逼风格的实现:
.For(s => s.StationName == "莘庄", "S", "X") .For(s => s.StationName == "长江南路", "Z", "C")
这种风格这几年很火,原本是JS的写法,后来被发扬光大到处都有了。想了解可以看http://en.wikipedia.org/wiki/Promise_(programming)
在PinYinGroupResolver中加入方法:
public PinYinGroupResolver<T> For(Predicate<T> fuckedItem, string inKey, string toKey) { PinYinGroupResolverItems.Add(new PinYinGroupResolverItem<T> { MatchPredicate = fuckedItem, SourceKeyName = inKey, TargetKeyName = toKey }); return this; }
这个方法的作用只是为PinYinGroupResolverItems增加对象,以备稍后的Resolve()方法使用。
注意神笔:return this。这就是为什么可以一直.For().For()...下去。因为这个方法return的就是对象本身,对象本身当然包含.For()方法,.For()又返回了对象本身……
现在调用者就可以:
GroupedStations = new PinYinGroupResolver<Station>(sGroup) .For(s => s.StationName == "莘庄", "S", "X") .For(s => s.StationName == "长江南路", "Z", "C")
之后我们就要写最关键的Resolve()方法了。思路是:
针对每个修正的对象
1. 找到它所在的错误的分组
2. 找到它应该在的正确的分组
3. 把对象加到正确分组
4. 从错误分组中删除对象
写成代码就是:
public List<AlphaKeyGroup<T>> Resolve() { // Windows Phone Bug Workround // e.g. "莘庄" is under [S], it should be under [X] foreach (var pinYinGroupResolverItem in PinYinGroupResolverItems) { var sk = pinYinGroupResolverItem.SourceKeyName; var tk = pinYinGroupResolverItem.TargetKeyName; // source group to remove from var sg = TargetGroup.FirstOrDefault(g => string.Compare(g.Key, sk, StringComparison.InvariantCultureIgnoreCase) == 0); // target group to add into var tg = TargetGroup.FirstOrDefault(g => string.Compare(g.Key, tk, StringComparison.InvariantCultureIgnoreCase) == 0); if (null != sg && null != tg) { // find match item var ob = sg.Find(pinYinGroupResolverItem.MatchPredicate); if (!tg.Contains(ob)) { Debug.WriteLine("Adding {0} into {1}", (ob as Station).StationName, tg.Key); tg.Add(ob); } if (sg.Contains(ob)) { Debug.WriteLine("Removing {0} from {1}", (ob as Station).StationName, sg.Key); sg.Remove(ob); } } } return TargetGroup; }
其中Debug.WriteLine的两行是可以去掉的,只是我自己调试时候用的。所以代码并不和Station类型耦合。
运行起来就能看见正确结果了: