我的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类型耦合。

运行起来就能看见正确结果了: