以前一直不知道为啥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))
-->
依稀记得当年还跟 @装配脑袋 讨论过这个问题,后来实在没什么办法,先做成这样。但是这种写法的味道其实很差,主要原因如下:
- IStyleProvider和IStyleController的功能是一致的。
- 由于某些控件对外观的限制,导致IStyleController被占用,只好创造IStyleProvider。其实这就意味着IStyleController的继承很难,不该这么设计。那以后要是某个IStyleProvider又被占用了那咋办?再发明一个新的接口,名字就不够用了,而且给继承的深度来定制名字显然是一件愚蠢的事情。
- XML是构造函数,构造函数只能修改类而不能继承接口,于是又要有一堆adaptor。而adaptor其实就是不断地把函数转发给template,把pull模式和push模式互相变一下。
- 这样每次添加一个新控件就会超级麻烦。
当然其实我也不是因为犯傻了才这么写的,早期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里面看到最近添加了三步:
- 把所有IStyleProvider换成IStyleController,多出来的要用来继承的实现就转移进相应的ControlTemplate类里面。(今天刚做完)
- 把IStyleController删掉,让控件直接跟ControlTemplate沟通,实际上就等于把GuiControlTemplateStyles.h的那一大坨几乎都只有一行的函数直接inline到控件里面去。
- 最后把构造函数的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大法好!