IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    我在《上海轨道交通》中是如何解决Windows Phone拼音分组bug的

    汪宇杰发表于 2014-07-19 00:48:07
    love 0

    我的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(sGroup)
                          .For(s => s.StationName == "莘庄", "S", "X")
                          .For(s => s.StationName == "长江南路", "Z", "C")
                          .Resolve()
                          .ToObservableCollection();

    其中,sGroup就是用MSDN伟大光荣正确的分组类分出的List

    var sGroup = AlphaKeyGroup.CreateGroups(
    AllStations,
    new CultureInfo("zh-CN"),
    s => s.StationName.Substring(0, 1),
    true);

    少年们肯定要问了,为什么我不在AlphaKeyGroup里面就做判断,直接把莘庄、长江南路撸正确呢?为什么还要多此一举写PinYinGroupResolver呢?我是不是傻逼啊?

    其实。。。这就是菜鸟和高手的区别。菜鸟不会考虑代码重用,碰到任何问题都喜欢用高耦合的方法去改,而这样就影响了一个function应该保持的原则:只做一件事。如果在AlphaKeyGroup里面写死了莘、长等站点名称首字母的判断,那首先你这个AlphaKeyGroup就被束缚在中文的应用里了,并且你也只能把本来泛型T改成Station类了,并且只针对这一个应用了,再也不能愉快的重用到别的地方了。我们需要的是“切入”的做法,不能修改AlphaKeyGroup,要把灵活性留出来。所以一定要另外写个类,专门查找和修复拼音首字母分组中错误的项。

    首先,因为要处理多个分组错误,我们需要一个数据模型,描述错误的分组和他们应该在哪个组。

    public class PinYinGroupResolverItem
    {
        public Predicate MatchPredicate { get; set; }
    
        public string SourceKeyName { get; set; }
    
        public string TargetKeyName { get; set; }
    }

    注意看本高手的设计思路,始终要记住高内聚、低耦合!所以这是个泛型类,并且把对象赋值的工作“外包”给了一个Predicate,这种类型就允许你用装逼Lambda表达式来赋值:s => s.StationName == "莘庄"

    下面开始创建非常高大上的PinYinGroupResolver。我们需要两个属性:

    TargetGroup:用于存放被AlphaKeyGroup分组后的,包含错误项的原始分组数据。

    PinYinGroupResolverItems:用于存放需要被修正的那些分组项的信息。

    public class PinYinGroupResolver
    {
        public PinYinGroupResolver(List> fuckedGroupedItems)
        {
            TargetGroup = fuckedGroupedItems;
            PinYinGroupResolverItems = new List>();
        }
    
        public List> TargetGroup { get; private set; }
    
        public List> PinYinGroupResolverItems { get; set; }
    
    //... more
    }

    直接用带参构造给TargetGroup赋值,这样就强制调用者做正确的事情(如果你在设计API给别人用就得有这样的思想),他们就知道这个PinYinGroupResolver的TargetGroup对象是一定要赋值的。

    于是调用者就有了

    GroupedStations = new PinYinGroupResolver(sGroup)

    下面就要增加这样的装逼风格的实现:

    .For(s => s.StationName == "莘庄", "S", "X")
    .For(s => s.StationName == "长江南路", "Z", "C")

    这种风格这几年很火,原本是JS的写法,后来被发扬光大到处都有了。想了解可以看http://en.wikipedia.org/wiki/Promise_(programming)

    在PinYinGroupResolver中加入方法:

    public PinYinGroupResolver For(Predicate fuckedItem, string inKey, string toKey)
    {
        PinYinGroupResolverItems.Add(new PinYinGroupResolverItem
        {
            MatchPredicate = fuckedItem,
            SourceKeyName = inKey,
            TargetKeyName = toKey
        });
    
        return this;
    }

    这个方法的作用只是为PinYinGroupResolverItems增加对象,以备稍后的Resolve()方法使用。

    注意神笔:return this。这就是为什么可以一直.For().For()...下去。因为这个方法return的就是对象本身,对象本身当然包含.For()方法,.For()又返回了对象本身……

    现在调用者就可以:

    GroupedStations = new PinYinGroupResolver(sGroup)
                          .For(s => s.StationName == "莘庄", "S", "X")
                          .For(s => s.StationName == "长江南路", "Z", "C")

    之后我们就要写最关键的Resolve()方法了。思路是:

    针对每个修正的对象

    1. 找到它所在的错误的分组

    2. 找到它应该在的正确的分组

    3. 把对象加到正确分组

    4. 从错误分组中删除对象

    写成代码就是:

    public List> 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类型耦合。

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



沪ICP备19023445号-2号
友情链接