写代码一定要敢于重构

1,052 阅读6分钟
原文链接: zhuanlan.zhihu.com

以前一直不知道为啥Xaml的控件要做成直接把control template赋值进control的属性里,于是我自己在做GacUI的时候,就写了一个IStyleController接口,其实就等与control template对象。

写着写着,我发现有一些控件是得有固定外观的(譬如ScrollView系列,总要有两个滚动条),于是就没办法继续开放ScrollView控件的IStyleController了,只能转而ScrollView提供一个固定的IStyleController实现,然后用template method pattern,做了一个IStyleProvider。所有IStyleProvider的基类跟IStyleController一摸一样,写到这里我就隐约觉得肯定有什么弄错了。

于是两三年过去了,到了2014年,我开始允许GacUI在XML里面写皮肤了,但是XML跟XAML的设计差不多,其实都是在写构造函数,那么用XML去实现接口反而就很别扭,于是我又给每一个控件做了一套Template类,让XML去构造Template,然后用预先写好的代码把相应的Template处理成相应的IStyleController或者IStyleProvider,于是大家就会在GuiControlTemplateStyles.h里面发现一大堆这样的adaptor。

没读过GacUI代码的人可能有些混乱,我就用C#简单的缩写一下:

public class Control
{
    public interface IStyleController
    {
        void SetText(string value);
        void SetFont(Font font);
        // composition 其实就是用来排版的方框
        // 方框里面可以放几何图形
        // 整个窗口其实就是一颗排版树构成的矢量图
        // 而控件就是一个让你方便地控制一颗子树的facade模式的产物
        // 因为你总不想为了改一个按钮的文字去遍历那棵树吧
        Composition BoundsComposition {get;}
    }

    public interface IStyleProvider
    {
        void SetText(string value);
        void SetFont(Font font);
    }

    public Control(IStyleController controller);
}

public class Label : Control
{
    public interface IStyleController : Control.IStyleController
    {
        void SetColor(Color color);
    }

    public Label(IStyleController controller);
}

public class ScrollView : Control
{
    public interface IStyleProvider : Control.IStyleProvider
    {
    }

    private class StyleController : Control.IStyleController
    {
        // 放两个ScrollBar,需要的参数放进IStyleProvider里面
        public StyleController(IStyleProvider provider);
    }

    public ScrollView(IStyleProvider provider)
        :base(new StyleController(provider))
    { ... }
}

class ControlTemplate : Composition
{
    public string Text{get;set;}
    public Font Font{get;set;}
}

class ControlTemplate_StyleController : Control.IStyleController
{
    private ControlTemplate template;

    public ControlTemplate_StyleController(ControlTemplate template)
    {
        this.template = template;
    }

    public void SetText(string value) { controlTemplate.Text = value; }
    public void SetFont(Font font) { controlTemplate.Font = value; }
    public Composition BoundsComposition { get { return controlTemplate; } }
}

然后是控件皮肤的XML:

<Instance ref.Class="MyControlTemplate">
  <ControlTemplate Text=Fuck, Font=Shit>
    <!-- 一些装饰品 -->
  </ControlTemplate>
</Instance>
<!--
上面这段代码生成了class MyControlTemplate : ControlTemplate { ... }
-->

然后用皮肤来创建控件:

<Window>
  <Control ControlTemplate="MyControlTemplate"/>
</Window>
<!--
实际上控件是这么被new出来的:
new Control(new ControlTemplate_StyleController(new MyControlTemplate))
-->

依稀记得当年还跟 @装配脑袋 讨论过这个问题,后来实在没什么办法,先做成这样。但是这种写法的味道其实很差,主要原因如下:

  1. IStyleProvider和IStyleController的功能是一致的。
  2. 由于某些控件对外观的限制,导致IStyleController被占用,只好创造IStyleProvider。其实这就意味着IStyleController的继承很难,不该这么设计。那以后要是某个IStyleProvider又被占用了那咋办?再发明一个新的接口,名字就不够用了,而且给继承的深度来定制名字显然是一件愚蠢的事情。
  3. XML是构造函数,构造函数只能修改类而不能继承接口,于是又要有一堆adaptor。而adaptor其实就是不断地把函数转发给template,把pull模式和push模式互相变一下。
  4. 这样每次添加一个新控件就会超级麻烦。

当然其实我也不是因为犯傻了才这么写的,早期GacUI并没有XML定制控件和皮肤的功能,因此做出来的选择很难把未来发生的事情都完美的考虑进去。这个事情也告诉我们,软件随着功能的变化,以前哪怕是好的设计也可能会变差,一定要敢于重构,才能让软件开发可以持续几十年,不需要推倒重写。

几年前我还曾经想过要写一个《GacUI与设计模式》系列,后来想想这个问题还没有得到妥善解决,因此就先作罢,以后肯定会改的。当然这个设计不行,并不代表这个设计用到的知识是不好的。这里面使用了很多关于Inverse of Control的思想,大家一定要去好好学习一下,而且切记不要使用JavaEE作为学习材料。不知道为什么,那些东西到了JavaEE上都会变味,可能跟他有反射导致大家可以胡乱搞有关系。人一忍不住,就喜欢乱搞。

于是又过了三年,我把很多事情处理得七七八八了,于是重新来想这个问题。后来我发现,Xaml直接让control去跟control template沟通是一件多么明智的选择,完美的避开了上面提高的所有问题。

这6年来写GacUI的时候,遇到过很多设计上的烦恼。凡是我自己想出来的一个后来证实非常完美的做法,后来好奇去看Xaml怎么做的时候,都会发现Xaml也是那么做的。凡是我自己想出来的跟Xaml有不一样的地方,多半是我想的不对。

Xaml的前身Avalon(beta版)是2001年就发布了,而Avalon其实是山寨了Office部门内部的一个Win32的XML控件工具包。不过这个工具包已经很老了,成为了Office开发新功能的重要障碍之一。不过当年开发它的其中一个元老,就是我翻墙的时候最后一面的面试官,都已经去做Distinguished Engineer了。剩下有几个爬上了Partner,还有一堆人留在Principal Engineer就不走了。微软的Principal讲道理不是那么好当的。

当然这只是历史了,Xaml很多出彩的东西其实在Office原本的这个工具包里面是没有的,所以功劳还是应该算在Xaml身上。

所以最近就在着手删除IStyleController和IStyleProvider接口,让control直接跟control template沟通。在几万行代码里面搞这种翻天覆地的重构,一直都不是一件容易的事情。但是能让代码大幅度变好的事情,当然是要去做的。所以大家会在TODO.md里面看到最近添加了三步:

  1. 把所有IStyleProvider换成IStyleController,多出来的要用来继承的实现就转移进相应的ControlTemplate类里面。(今天刚做完)
  2. 把IStyleController删掉,让控件直接跟ControlTemplate沟通,实际上就等于把GuiControlTemplateStyles.h的那一大坨几乎都只有一行的函数直接inline到控件里面去。
  3. 最后把构造函数的ControlTemplate*参数改成一个可以运行时修改的属性。

重构也不能一口吃成个胖子,要一步一步来,安全地进行重构,把代码慢慢改好。

啊,终于变得跟Xaml一样了(逃。下面的C#代码大概就是重构后的样子:

public class ControlTemplate : Composition
{
    public string Text {get;set;} // GacUI的属性可以自带Changed事件
    public string Font {get;set;}
} 

public class Control
{
    public virtual ControlTemplate Template {get;set;}
}

public class LabelTemplate : ControlTemplate
{
    public Color Color {get;set;}
}

public class Label : Control
{
    // 检查传进来的真的是LabelTemplate
    public override ControlTemplate Template {get;set;}
}

干净清爽!Xaml大法好!