201710 DDD DSL Design War Layout - xiaoxianfaye/Courses GitHub Wiki

1 Copyright Statement

本课程的主体内容均来自于孙鸣、邓辉发布在IBM论坛上的文章《基于Java的界面布局DSL的设计与实现》。
https://www.ibm.com/developerworks/cn/java/j-lo-uidsl/

2 Problem

需求一:请先编写一个迷你计算器的外观程序,如下图所示。注意仅需要编写外观程序,不需要编写控件事件响应和具体计算逻辑。

problem layout 1

需求二:请将所有数字以及操作按钮按照横向和纵向各2%进行留白,如下图所示:

problem layout 2

请具体分析一下,在需求一的基础上,为了满足需求二,修改了哪些代码?有没有减少代码修改量的方法?

3 Showcase & Discuss

请学员展示自己之前的设计思路和实现方式,大家可以互相点评、讨论。以学员为主,讲师为辅。

4 About Layout

  • Do you like UI design? Creative or Boring?
  • Why do you feel creative or boring?
  • What procedures are involved in UI design?
    • Creative Design: UI Layout Style & Interaction with Users
    • Boring Implementation

此时,你头脑中那些清晰、完整的设计概念开始变得琐碎,你不得不和那些低层次的坐标位置打交道。

更糟糕的是,当你好不容易做好了一个界面,但是发现其中某些元素的布局需要做一些调整时,这个你本应认为是一个很简单的改变却造成大量重复的低层次坐标位置更改时,你肯定会认为做界面非常机械和乏味。

  • What is the root cause of the problem?
    • Gap between Creative Design and Implementation Language

问题的根本原因在于:界面设计创意和实现这些创意概念的语言之间存在很大的断层。因为这个断层的存在,在具体实现时,你就必须得把这些清晰、完整的布局样式降级成一些琐碎的、没有什么意义的低层次的坐标值,使得实现语言能够理解。

这个过程不仅乏味,而且最终的实现也非常的脆弱。一个在布局样式层面非常简单的更改,就会造成实现层面的巨大变动。比如,现在要把一组元素同时按比例缩小10%,在座的各位肯定都知道这个更改意味着什么……。

  • What are the existing "solutions"?
    • Visual UI Design Tool
    • Layout Manager
      Do any of them fundamentally solve the problem?

可视化界面设计工具确实避免了不少繁琐的界面元素摆放工作,但是对于专业的界面设计来说,通过拖放设计出来的界面在准确度和规范性上都有待提高。事实上,在座的各位已经很少用可视化界面设计工具了吧?

此外还有更为重要一点,那就是存在于设计者头脑中的布局样式仍然没有被明确地描述出来,而是被降级成一个个摆放在一起的零散的组件,虽然这些组件本身是可视的。这个语义断层的存在同样会使得通过可视化界面设计工具设计出来的界面非常的脆弱。

布局管理器试图通过提供一些常用的布局样式来解决这个问题。但是,这种做法非常的僵化,也就是说你只能使用现有的布局管理器,如果它们无法满足你的要求,你也无法自己定制。此外,这些布局管理器仅仅适合于一些简单的情况。对于一些复杂的布局样式来说,它们的描述能力就显得非常得不足。在座的各位应该都和GridBagLayout斗争过吧?

  • Can we do better?

在这次课程中,我们会带着大家设计出一种更好的解决方案。

我们不去试图把界面设计者头脑中的设计概念和样式逐步降级、分解成所使用的实现语言能够理解的低层概念,也不是提供一些已经完成的、确定的、但难以扩充和更改的布局样式库供界面设计者使用。

我们会开发一种专门用于描述高层界面设计样式的语言。通过这种语言,界面设计者可以直接、明确地描述出他们头脑中的布局设计样式;通过这种语言,界面设计者可以自己方便、灵活地制定自己需要的布局样式。也就是说,本来仅存在于界面设计者头脑中的抽象布局样式现在也变得可描述、可编程了。

5 Analysis

分析:定义清楚问题是什么。

这个问题大家都很熟悉,而且问题本身也是比较清楚的,因此分析阶段就略过了。

6 Design

设计:问题分析清楚以后,提出解决问题的逻辑框架。

6.1 Core Concepts

design core concepts

Component(组件)和Position Relation(组件之间的位置关系),是这个领域的核心概念。

6.2 What does a Component mean?

一个Component是什么意思?什么是一个Component?Component的语义(Semantics)是什么?

Component是一种基本的布局元素,可以对Component进行平移和伸缩,使其和给定容器(Container)中的一个布局空间Rectangle匹配。

比如,对于Button这个Component来讲,它具有传统按钮的外观,但是它在布局上所占的实际空间则是由为其指定的Rectangle决定的。此外,Component最终要在界面上显示出来,就必须有一个物理上的Container。

也就是说,只要给定了一个Rectangle和一个Container,一个Component就可以在界面上指定的布局位置呈现出来。

design rectangle

例如,给定一个Rectangle(0, 0, 200, 60)和一个Container,将一个Button摆放到Rectangle中,描述如下:

button.at(0, 0, 200, 60).in(container)

布局展现如下:
design layout component

显然,仅仅提供Component这样一种基本的布局元素是不够的。

6.3 What does a Position Relation mean?

Component与Component之间还存在Position Relation(相对位置关系)。

6.3.1 What are the Core Position Relations?

有哪几种最基本、最核心的Position Relation呢?
beside和above。

design layout beside

design layout above

6.3.2 What does Beside or Above mean?

那么,beside和above到底是什么意思?什么是beside或above?beside和above的语义(Semantics)是什么?

beside和above用来定义Component之间的Position Relation。它们都由两个Component以及一个Ratio(比例)构造出来,构造出来还是一个Component。只要给定了一个Rectangle和一个Container,beside或above生成的Component就可以在界面上指定的布局位置呈现出来。

在beside中,按照给定的比例把第一个Component摆放在第二个Component的左边。在above中,按照给定的比例把第一个Component摆放在第二个Component的上边。beside和above被称为“组合子Component”(Combination Components: beside, above)。

例如,给定一个Rectangle(0, 0, 300, 60)和一个Container,将一个TextField和一个Button水平顺序摆放到Rectangle中,且TextField占据80%的比例,描述如下:

beside(textField, button, 0.8).at(0, 0, 300, 60).in(container)

布局展现如下:
design layout beside components

同样,给定一个Rectangle(0, 0, 300, 60)和一个Container,将一个TextField和一个Button垂直顺序摆放到Rectangle中,且TextField占据50%的比例,描述如下:

above(textField, button, 0.5).at(0, 0, 300, 60).in(container)

布局展现如下:
design layout above components

6.3.3 The components' world is Closed

Components在beside和above操作下是封闭的。也就是说,两个Component用 “beside”或者“above”组合以后还是一个Component,就可以和其它的Component再次进行“beside”或者“above”组合。这样,我们就可以使用这两个简单的操作生成更加复杂的Component,从而完成复杂的界面布局。

例如,下面这段描述:

above(beside(textField, button1, 0.8), button2, 0.5).at(0, 0, 300, 60).in(container)

布局展现如下:
design layout closed

组合子满足封闭性非常重要,因为只有满足封闭性,组合子才能和其它原子或者组合子再组合……。可以想象,如果一个语言提供的组合手段是能够封闭的,那么它就能够高效地帮助你构建出非常复杂的东西。

6.4 Special Component - Empty

如何描述界面布局中的空白?

我们增加一种特殊的原子元素:Empty。它的作用只是占据一定的布局空间。

例如,给定一个Rectangle(0, 0, 300, 60)和一个Container,将一个Button放在右半边并占据50%的比例,左半边空置,描述如下:

beside(empty, button, 0.5).at(0, 0, 300, 60).in(container)

布局展现如下:
design layout empty

后面会看到,正是这个Empty以及beside和above操作的闭包性质为我们描述任意复杂的布局样式提供了可能。

6.5 Language of Geometric Positions

界面布局位置描述语言:

  • Primitive Components: Empty, Button, TextField ...
  • Combinator Components: beside, above
  • Rectangle:at
  • Container:in

7 Implementation - Language of Geometric Positions

实现:选择实现技术把逻辑框架的软件模型实现出来。

7.1 Overview

7.1.1 OO && Functional

可以用OO或Functional实现。课堂上用OO实现,Functional实现留作作业。

代码包路径:fayelab.ddd.layout.original.oo

7.1.2 Based on Java Swing

我们基于Java Swing来实现界面布局位置描述语言。以后大家可以根据自己的需要在其它的语言和界面开发工具包上去实现该界面布局语言。

7.1.3 About Test

关于测试,一部分代码只能通过肉眼观察布局展现来验证,另一部分代码可以通过TDD测试用例验证。

因此,我们要准备一个用于测试的Test Frame。代码详见TestFrm(Modify Package).java。需要注意的是:

  • 可能需要修改包路径;
  • 因为JPanel自带布局,因此要通过下面的语句,将自带布局去掉。
this.container.setLayout(null);

运行结果如下:
impl layout test framework

7.1.4 Fluent Interface

API的写法采用“Fluent Interface”风格。

“Fluent Interface”是由《Domain Driven Design》的作者Eric Evans提出的概念。

在这种风格中,为了能够将调用形成一个句子,每个调用在结束时都返回了this。另外,在给方法起名时也有不同的考虑,不只是关注该方法的职责和功能,而且更关注该方法名在整个句子的上下文中是否通顺、是否更富表达力。

7.1.5 Steps

实现步骤:

  • Component
  • beside
  • above
  • Empty

7.2 Coding

7.2.1 Component

在前面的小节中,举过下面这个例子,现在我们来实现它。

给定一个Rectangle(0, 0, 200, 60)和一个Container,将一个Button摆放到Rectangle中,描述如下:

button.at(0, 0, 200, 60).in(container)

布局展现如下:
design layout component

TestFrm.java

    private void test_component()
    {
        button("Button").at(0, 0, 200, 60).in(container);
    }

    public static void main(String[] args)
    {
        TestFrm frm = new TestFrm();
        
        //Test Cases
        frm.test_component();
        
        frm.centerShow();
    }

LayoutTool.java

package fayelab.ddd.layout.original.oo;

public class LayoutTool
{
    public static Button button(String text)
    {
        return new Button(text);
    }
}

新增Button类实现Component接口。由于是基于Java Swing来实现界面布局位置描述语言,所以Button类中包括一个JButton的成员变量。为了实现Fluent Interface风格,Button类的所有方法的返回值类型都是Component接口,方法体中返回this。

Component.java

package fayelab.ddd.layout.original.oo.component;

public interface Component
{
    Component at(int left, int top, int width, int height);

    Component in(Container container);
}

Button.java

package fayelab.ddd.layout.original.oo.component;

public class Button implements Component
{
    private JButton btn;
    
    public Button(String text)
    {
        btn = new JButton(text);
    }
    
    @Override
    public Component at(int left, int top, int width, int height)
    {
        btn.setBounds(left, top, width, height);
        return this;
    }

    @Override
    public Component in(Container container)
    {
        container.add(btn);
        return this;
    }
}

运行结果如下:
impl layout component

7.2.2 beside

在前面的小节中,举过下面这个例子,现在我们来实现它。

给定一个Rectangle(0, 0, 300, 60)和一个Container,将一个TextField和一个Button水平顺序摆放到Rectangle中,且TextField占据80%的比例,描述如下:

beside(textField, button, 0.8).at(0, 0, 300, 60).in(container)

布局展现如下:
design layout beside components

TestFrm.java

    private void test_beside()
    {
        beside(textField(), button("Btn"), 0.8f).at(0, 0, 300, 60).in(container);
    }

    public static void main(String[] args)
    {
        TestFrm frm = new TestFrm();
        
        //Test Cases
//        frm.test_component();
        frm.test_beside();
        
        frm.centerShow();
    }

LayoutTool.java

    public static TextField textField()
    {
        return new TextField();
    }

新增TextField类实现Component接口。由于是基于Java Swing来实现界面布局位置描述语言,所以TextField类中包括一个JTextField的成员变量。为了实现Fluent Interface风格,TextField类的所有方法的返回值类型都是Component接口,方法体中返回this。

TextField.java

package fayelab.ddd.layout.original.oo.component;

public class TextField implements Component
{    
    private JTextField textField;

    public TextField()
    {
        this.textField = new JTextField();
    }
    
    @Override
    public Component at(int left, int top, int width, int height)
    {
        textField.setBounds(left, top, width, height);
        return this;
    }

    @Override
    public Component in(Container container)
    {
        container.add(textField);
        return this;
    }
}

再来实现beside。

LayoutTool.java

    public static Component beside(Component leftCmp, Component rightCmp, float ratio)
    {
        return new Beside(leftCmp, rightCmp, ratio);
    }

Beside.java。先保证所有代码编译通过。

package fayelab.ddd.layout.original.oo.position;

public class Beside implements Component
{
    public Beside(Component leftCmp, Component rightCmp, float ratio)
    {
        // TODO Auto-generated constructor stub
    }

    @Override
    public Component at(int left, int top, int width, int height)
    {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public Component in(Container container)
    {
        // TODO Auto-generated method stub
        return null;
    }
}

我们用TDD实现Beside的业务逻辑,即左右两个Component各自对应的布局空间Rectangle是否正确。既然是测试环境,肯定不用真实的控件,用ComponentStub为Component接口打桩。打桩的方法是简单保存一下Component的rectangle参数和container引用,用于验证beside的at()和in()是否以正确的参数被调用过。

BesideTest.java

package fayelab.ddd.layout.original.oo.position;

public class BesideTest extends TestCase
{
    private static final int TOLERANCE = 1;
    
    private ComponentStub leftCmp;
    private ComponentStub rightCmp;
    private Component beside;
    
    @Override
    protected void setUp()
    {
        this.leftCmp = new ComponentStub();
        this.rightCmp = new ComponentStub();
        this.beside = new Beside(leftCmp, rightCmp, 0.8f);
    }
    
    public void test_at()
    {
        beside.at(20, 10, 300, 60);
        
        checkRectangle(new int[]{20, 10, 240, 60}, leftCmp.getRectangle());
        checkRectangle(new int[]{260, 10, 60, 60}, rightCmp.getRectangle());
    }

    public void test_in()
    {
        Container container = new Container();
        
        beside.in(container);
        
        assertSame(container, leftCmp.getContainer());
        assertSame(container, rightCmp.getContainer());
    }
    
    private void checkRectangle(int[] expected, int[] actual)
    {
        assertEquals(expected.length, actual.length);
        
        assertTrue(IntStream.range(0, expected.length)
                            .allMatch(idx -> Math.abs(expected[idx] - actual[idx]) <= TOLERANCE));
    }
}

ComponentStub.java

package fayelab.ddd.layout.original.oo.component;

public class ComponentStub implements Component
{
    private int[] rectangle;
    private Container container;

    @Override
    public Component at(int left, int top, int width, int height)
    {
        this.rectangle = new int[]{left, top, width, height};
        return this;
    }

    @Override
    public Component in(Container container)
    {
        this.container = container;
        return this;
    }

    public int[] getRectangle()
    {
        return rectangle;
    }

    public Container getContainer()
    {
        return container;
    }
}

Beside.java

package fayelab.ddd.layout.original.position;

public class Beside implements Component
{
    private Component leftCmp;
    private Component rightCmp;
    private float ratio;

    public Beside(Component leftCmp, Component rightCmp, float ratio)
    {
        this.leftCmp = leftCmp;
        this.rightCmp = rightCmp;
        this.ratio = ratio;
    }

    @Override
    public Component at(int left, int top, int width, int height)
    {
        leftCmp.at(left, top, (int)(width * ratio), height);
        rightCmp.at(left + (int)(width * ratio), top, (int)(width * (1 - ratio)), height);
        return this;
    }

    @Override
    public Component in(Container container)
    {
        leftCmp.in(container);
        rightCmp.in(container);
        return this;
    }
}

运行结果如下:
impl layout beside

7.2.3 above

在前面的小节中,举过下面这个例子,现在我们来实现它。

给定一个Rectangle(0, 0, 300, 60)和一个Container,将一个TextField和一个Button垂直顺序摆放到Rectangle中,且TextField占据50%的比例,描述如下:

above(textField, button, 0.5).at(0, 0, 300, 60).in(container)

布局展现如下:
design layout above components

TestFrm.java

    private void test_above()
    {
        above(textField(), button("Button"), 0.5f).at(0, 0, 300, 60).in(container);
    }

    public static void main(String[] args)
    {
        TestFrm frm = new TestFrm();
        
        //Test Cases
//        frm.test_component();
//        frm.test_beside();
        frm.test_above();
        
        frm.centerShow();
    }

LayoutTool.java

    public static Component above(Component upCmp, Component downCmp, float ratio)
    {
        return new Above(upCmp, downCmp, ratio);
    }

Above.java。先保证所有代码编译通过。

package fayelab.ddd.layout.original.oo.position;

public class Above implements Component
{
    public Above(Component upCmp, Component downCmp, float ratio)
    {
        // TODO Auto-generated constructor stub
    }

    @Override
    public Component at(int left, int top, int width, int height)
    {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public Component in(Container container)
    {
        // TODO Auto-generated method stub
        return null;
    }
}

我们用TDD实现Above的业务逻辑,即上下两个Component各自对应的布局空间Rectangle是否正确。同beside,使用ComponentStub。

AboveTest.java

package fayelab.ddd.layout.original.oo.position;

public class AboveTest extends TestCase
{
    private static final int TOLERANCE = 1;
    
    private ComponentStub upCmp;
    private ComponentStub downCmp;
    private Component above;
    
    @Override
    protected void setUp()
    {
        this.upCmp = new ComponentStub();
        this.downCmp = new ComponentStub();
        this.above = new Above(upCmp, downCmp, 0.5f);
    }
    
    public void test_at()
    {
        above.at(20, 10, 300, 60);
        
        checkRectangle(new int[]{20, 10, 300, 30}, upCmp.getRectangle());
        checkRectangle(new int[]{20, 40, 300, 30}, downCmp.getRectangle());
    }
    
    public void test_in()
    {
        Container container = new Container();
        
        above.in(container);
        
        assertSame(container, upCmp.getContainer());
        assertSame(container, downCmp.getContainer());
    }

    private void checkRectangle(int[] expected, int[] actual)
    {
        assertEquals(expected.length, actual.length);
        
        assertTrue(IntStream.range(0, expected.length)
                            .allMatch(idx -> Math.abs(expected[idx] - actual[idx]) <= TOLERANCE));
    }
}

Above.java

package fayelab.ddd.layout.original.oo.position;

public class Above implements Component
{
    private Component upCmp;
    private Component downCmp;
    private float ratio;

    public Above(Component upCmp, Component downCmp, float ratio)
    {
        this.upCmp = upCmp;
        this.downCmp = downCmp;
        this.ratio = ratio;
    }

    @Override
    public Component at(int left, int top, int width, int height)
    {
        upCmp.at(left, top, width, (int)(height * ratio));
        downCmp.at(left, top + (int)(height * ratio), width, (int)(height * (1 - ratio)));
        return this;
    }

    @Override
    public Component in(Container container)
    {
        upCmp.in(container);
        downCmp.in(container);
        return this;
    }
}

运行结果如下:
impl layout above

7.2.4 Refactoring

7.2.4.1 Product Code

观察Button和TextField这两个原子组件类,会发现in和at方法有重复代码,因此将重复代码提取到一个父类BaseComponent中。

BaseComponent.java

package fayelab.ddd.layout.original.oo.component;

public class BaseComponent implements Component
{
    protected java.awt.Component cmp;

    @Override
    public Component at(int left, int top, int width, int height)
    {
        cmp.setBounds(left, top, width, height);
        return this;
    }

    @Override
    public Component in(Container container)
    {
        container.add(cmp);
        return this;
    }
}

Button.java

package fayelab.ddd.layout.original.oo.component;

public class Button extends BaseComponent
{    
    public Button(String text)
    {
        cmp = new JButton(text);
    }
}

TextField.java

package fayelab.ddd.layout.original.oo.component;

public class TextField extends BaseComponent
{    
    public TextField()
    {
        cmp = new JTextField();
    }
}

7.2.4.2 Test Code

观察BesideTest和AboveTest这两个测试类,也有一些重复代码,比如TOLERANCE常量定义、checkRectangle方法等,因此将重复代码提取到一个TestUtil工具类中。

TestUtil.java

package fayelab.ddd.layout.original.oo.position;

public class TestUtil
{
    private static final int TOLERANCE = 1;
    
    static void checkRectangle(int[] expected, int[] actual)
    {
        assertEquals(expected.length, actual.length);
        
        assertTrue(IntStream.range(0, expected.length)
                            .allMatch(idx -> Math.abs(expected[idx] - actual[idx]) <= TOLERANCE));
    }
}

BesideTest.java

package fayelab.ddd.layout.original.oo.position;

public class BesideTest extends TestCase
{
    private ComponentStub leftCmp;
    private ComponentStub rightCmp;
    private Component beside;
    
    @Override
    protected void setUp()
    {
        this.leftCmp = new ComponentStub();
        this.rightCmp = new ComponentStub();
        this.beside = new Beside(leftCmp, rightCmp, 0.8f);
    }
    
    public void test_at()
    {
        beside.at(20, 10, 300, 60);
        
        checkRectangle(new int[]{20, 10, 240, 60}, leftCmp.getRectangle());
        checkRectangle(new int[]{260, 10, 60, 60}, rightCmp.getRectangle());
    }

    public void test_in()
    {
        Container container = new Container();
        
        beside.in(container);
        
        assertSame(container, leftCmp.getContainer());
        assertSame(container, rightCmp.getContainer());
    }
}

AboveTest.java

package fayelab.ddd.layout.original.oo.position;

public class AboveTest extends TestCase
{    
    private ComponentStub upCmp;
    private ComponentStub downCmp;
    private Component above;
    
    @Override
    protected void setUp()
    {
        this.upCmp = new ComponentStub();
        this.downCmp = new ComponentStub();
        this.above = new Above(upCmp, downCmp, 0.5f);
    }
    
    public void test_at()
    {
        above.at(20, 10, 300, 60);
        
        checkRectangle(new int[]{20, 10, 300, 30}, upCmp.getRectangle());
        checkRectangle(new int[]{20, 40, 300, 30}, downCmp.getRectangle());
    }
    
    public void test_in()
    {
        Container container = new Container();
        
        above.in(container);
        
        assertSame(container, upCmp.getContainer());
        assertSame(container, downCmp.getContainer());
    }
}

AllTests.java

package fayelab.ddd.layout.original.oo.position;

public class AllTests
{
    public static Test suite()
    {
        TestSuite suite = new TestSuite(AllTests.class.getName());
        //$JUnit-BEGIN$
        suite.addTestSuite(AboveTest.class);
        suite.addTestSuite(BesideTest.class);
        //$JUnit-END$
        return suite;
    }
}

7.2.5 The components' world is Closed

在前面的小节中,我们提到:Components在beside和above操作下是封闭的。也就是说,两个Component用 “beside”或者“above”组合以后还是一个Component,就可以和其它的Component再次进行“beside”或者“above”组合。这样,我们就可以使用这两个简单的操作生成更加复杂的Component,从而完成复杂的界面布局。

在前面的小节中,举过下面这个例子,现在我们来实现它。

above(beside(textField, button1, 0.8), button2, 0.5).at(0, 0, 300, 60).in(container)

布局展现如下:
design layout closed

TestFrm.java

    private void test_beside_above()
    {
        above(beside(textField(), button("Btn1"), 0.8f), 
              button("Btn2"), 0.5f).at(0, 0, 300, 60).in(container);        
    }

    public static void main(String[] args)
    {
        TestFrm frm = new TestFrm();
        
        //Test Cases
//        frm.test_component();
//        frm.test_beside();
//        frm.test_above();
        frm.test_beside_above();
        
        frm.centerShow();
    }

运行结果如下:
impl layout beside above

7.2.6 Empty

在前面的小节中,举过下面这个例子,现在我们来实现它。

给定一个Rectangle(0, 0, 300, 60)和一个Container,将一个Button放在右半边并占据50%的比例,左半边空置,描述如下:

beside(empty, button, 0.5).at(0, 0, 300, 60).in(container)

布局展现如下:
design layout empty

TestFrm.java

    private void test_empty()
    {        
        beside(empty(), button("Button"), 0.5f).at(0, 0, 300, 60).in(container);
    }

    public static void main(String[] args)
    {
        TestFrm frm = new TestFrm();
        
        //Test Cases
//        frm.test_component();
//        frm.test_beside();
//        frm.test_above();
//        frm.test_beside_above();
        frm.test_empty();
        
        frm.centerShow();
    }

LayoutTool.java

    public static Component empty()
    {
        return new Empty();
    }

Empty.java

package fayelab.ddd.layout.original.oo.component;

public class Empty implements Component
{
    @Override
    public Component at(int left, int top, int width, int height)
    {
        return this;
    }

    @Override
    public Component in(Container container)
    {
        return this;
    }
}

运行结果如下:
impl layout empty

8 Implementation - Language of Layout Style

有了前面的那些基础的布局元素和组合手段后,我们就可以通过组合手段来把一些典型的布局样式抽象出来。

8.1 Horizontal Center

给定一个布局空间和一个布局组件,我们期望该组件能够按照指定的留白比例位于该布局空间的横向中心地带。我们可以把该布局样式抽象出来,并命名为hCenter。

design layout style hcenter

我们还可以在更复杂的布局样式中把hCenter当作一个基本语素使用。

TestFrm.java

    private void test_hCenter()
    {
        hCenter(button("Button"), 0.1f).at(0, 0, 545, 325).in(container);
    }

    public static void main(String[] args)
    {
        TestFrm frm = new TestFrm();
        
        //Test Cases
//        frm.test_component();
//        frm.test_beside();
//        frm.test_above();
//        frm.test_beside_above();
//        frm.test_empty();
        frm.test_hCenter();
        
        frm.centerShow();
    }

LayoutTool.java

    public static Component hCenter(Component cmp, float ratio)
    {
        return beside(empty(), beside(cmp, empty(), (1 - 2 * ratio) / (1 - ratio)), ratio);
    }

运行结果如下:
impl layout style hcenter

8.2 Vertical Center

给定一个布局空间和一个布局组件,我们期望该组件能够按照指定的留白比例位于该布局空间的纵向中心地带。我们可以把该布局样式抽象出来,并命名为vCenter。

design layout style vcenter

我们还可以在更复杂的布局样式中把vCenter当作一个基本语素使用。

TestFrm.java

    private void test_vCenter()
    {
        vCenter(button("Button"), 0.1f).at(0, 0, 545, 325).in(container);
    }

    public static void main(String[] args)
    {
        TestFrm frm = new TestFrm();
        
        //Test Cases
//        frm.test_component();
//        frm.test_beside();
//        frm.test_above();
//        frm.test_beside_above();
//        frm.test_empty();
//        frm.test_hCenter();
        frm.test_vCenter();
        
        frm.centerShow();
    }

LayoutTool.java

    public static Component vCenter(Component cmp, float ratio)
    {
        return above(empty(), above(cmp, empty(), (1 - 2 * ratio) / (1 - ratio)), ratio);
    }

运行结果如下:
impl layout style vcenter

可以看到hCenter()和vCenter()之间除了beside、above不一样,其他代码都一样,用函数式编程范式重构去重。

LayoutTool.java

    @FunctionalInterface
    private interface PosLayout
    {
        Component apply(Component cmp1, Component cmp2, float ratio);
    }
    
    public static Component hCenter(Component cmp, float ratio)
    {
        return xCenter(LayoutTool::beside, cmp, ratio);
    }
    
    public static Component vCenter(Component cmp, float ratio)
    {
        return xCenter(LayoutTool::above, cmp, ratio);
    }
    
    private static Component xCenter(PosLayout posLayout, Component cmp, float ratio)
    {
        return posLayout.apply(empty(), posLayout.apply(cmp, empty(), (1 - 2 * ratio) / (1 - ratio)), ratio);
    }

8.3 Center

给定一个布局空间和一个布局组件,我们期望该组件能够按照指定的纵、横留白比例位于该布局空间的中心地带。我们可以把该布局样式抽象出来,并命名为center。

design layout style center

我们可以通过hCenter和vCenter组合实现center。我们也可以在更复杂的布局样式中把center当作一个基本语素使用。

TestFrm.java

    private void test_center()
    {
        center(button("Center"), 0.2f, 0.1f).at(0, 0, 545, 325).in(container);
    }

    public static void main(String[] args)
    {
        TestFrm frm = new TestFrm();
        
        //Test Cases
//        frm.test_component();
//        frm.test_beside();
//        frm.test_above();
//        frm.test_beside_above();
//        frm.test_empty();
//        frm.test_hCenter();
//        frm.test_vCenter();
        frm.test_center();
        
        frm.centerShow();
    }

LayoutTool.java

    public static Component center(Component cmp, float hRatio, float vRatio)
    {
        return vCenter(hCenter(cmp, hRatio), vRatio);
    }

运行结果如下:
impl layout style center

8.4 Horizontal Sequence

给定一个布局空间和一组布局元素,把这组给定的布局元素横向顺序排列。我们可以把该布局样式抽象出来,并命名为hSeq。

design layout style hseq

同理,我们也可以在更复杂的布局样式中把hSeq当作一个基本语素使用。

TestFrm.java

    private void test_hSeq()
    {
        hSeq(asList(button("1"), button("2"), button("3"))).at(0, 0, 300, 60).in(container);
    }

    public static void main(String[] args)
    {
        TestFrm frm = new TestFrm();
        
        //Test Cases
//        frm.test_component();
//        frm.test_beside();
//        frm.test_above();
//        frm.test_beside_above();
//        frm.test_empty();
//        frm.test_hCenter();
//        frm.test_vCenter();
//        frm.test_center();
        frm.test_hSeq();
        
        frm.centerShow();
    }

LayoutTool.java

    public static Component hSeq(List<Component> cmps)
    {
        if(cmps.size() == 1)
        {
            return cmps.get(0);
        }
        
        return beside(cmps.get(0), hSeq(cmps.subList(1, cmps.size())), 1.0f / cmps.size());
    }

运行结果如下:
impl layout style hseq

8.5 Vertical Sequence

我们希望抽象出这样一种布局样式:给定一个布局空间和一组布局元素,把这组给定的布局元素纵向顺序排列。我们可以把该布局样式抽象出来,并命名为vSeq。

design layout style vseq

同理,我们也可以在更复杂的布局样式中把vSeq当作一个基本语素使用。

TestFrm.java

    private void test_vSeq()
    {
        vSeq(asList(button("1"), button("2"), button("3"))).at(0, 0, 150, 200).in(container);
    }

    public static void main(String[] args)
    {
        TestFrm frm = new TestFrm();
        
        //Test Cases
//        frm.test_component();
//        frm.test_beside();
//        frm.test_above();
//        frm.test_beside_above();
//        frm.test_empty();
//        frm.test_hCenter();
//        frm.test_vCenter();
//        frm.test_center();
//        frm.test_hSeq();
        frm.test_vSeq();
        
        frm.centerShow();
    }

LayoutTool.java

    public static Component vSeq(List<Component> cmps)
    {
        if(cmps.size() == 1)
        {
            return cmps.get(0);
        }
        
        return above(cmps.get(0), vSeq(cmps.subList(1, cmps.size())), 1.0f / cmps.size());
    }

运行结果如下:
impl layout style vseq

可以看到hSeq()和vSeq之间除了beside()、above()不一样,其他代码都一样,用函数式编程范式重构去重。

LayoutTool.java

    public static Component hSeq(List<Component> cmps)
    {
        return seq(LayoutTool::beside, cmps);
    }
    
    public static Component vSeq(List<Component> cmps)
    {
        return seq(LayoutTool::above, cmps);
    }
    
    private static Component seq(PosLayout posLayout, List<Component> cmps)
    {
        if(cmps.size() == 1)
        {
            return cmps.get(0);
        }
        
        return posLayout.apply(cmps.get(0), seq(posLayout, cmps.subList(1, cmps.size())), 1.0f / cmps.size());
    }

8.6 Block

在center、hSeq、vSeq这些布局样式的基础上,我们还可以定义出更加高阶的样式。比如,给定一个布局元素序列,我们希望它们在给定的布局空间中按照N行、M列排列。我们可以把该布局样式抽象出来,并命名为block。

design layout style block

TestFrm.java

    private void test_block()
    {
        List<Component> cmps = IntStream.rangeClosed(1, 11)
                                        .mapToObj(i -> button(String.valueOf(i)))
                                        .collect(toList());
                
        block(cmps, 4, 3).at(0, 0, 545, 325).in(container);
    }

    public static void main(String[] args)
    {
        TestFrm frm = new TestFrm();
        
        //Test Cases
//        frm.test_component();
//        frm.test_beside();
//        frm.test_above();
//        frm.test_beside_above();
//        frm.test_empty();
//        frm.test_hCenter();
//        frm.test_vCenter();
//        frm.test_center();
//        frm.test_hSeq();
//        frm.test_vSeq();
        frm.test_block();
        
        frm.centerShow();
    }

LayoutTool.java。normalize()把一维组件列表规格化为二维组件列表,不足的位置用Empty组件补齐(padding)。

    public static Component block(List<Component> cmps, int rowNum, int colNum)
    {
        return vSeq(normalize(cmps, rowNum, colNum).stream().map(rowCmps -> hSeq(rowCmps)).collect(toList()));
    }

    private static Collection<List<Component>> normalize(List<Component> cmps, int rowNum, int colNum)
    {
        List<Component> paddedCmps = padding(cmps, rowNum * colNum - cmps.size());
      
        return IntStream.range(0, paddedCmps.size())
                        .mapToObj(idx -> asList(idx, paddedCmps.get(idx)))
                        .collect(groupingBy(idxAndCmp -> (Integer)idxAndCmp.get(0) / colNum, 
                                            mapping(idxAndCmp -> (Component)idxAndCmp.get(1), toList())))
                        .values();
    }
  
    private static List<Component> padding(List<Component> cmps, int num)
    {
        return Stream.concat(cmps.stream(), Collections.nCopies(num, empty()).stream()).collect(toList());
    }

运行结果如下:
impl layout style block

8.7 Block With Margin

在block布局样式的基础上,希望每个元素都可以指定一些横向和纵向的留白,我们可以把该布局样式抽象出来,并命名为blockm。

design layout style blockm

TestFrm.java

    private void test_blockm()
    {
        List<Component> cmps = IntStream.rangeClosed(1, 11)
                .mapToObj(i -> button(String.valueOf(i)))
                .collect(toList());
                
        blockm(cmps, 4, 3, 0.1f, 0.1f).at(0, 0, 545, 325).in(container);
    }

    public static void main(String[] args)
    {
        TestFrm frm = new TestFrm();
        
        //Test Cases
//        frm.test_component();
//        frm.test_beside();
//        frm.test_above();
//        frm.test_beside_above();
//        frm.test_empty();
//        frm.test_hCenter();
//        frm.test_vCenter();
//        frm.test_center();
//        frm.test_hSeq();
//        frm.test_vSeq();
//        frm.test_block();
        frm.test_blockm();
        
        frm.centerShow();
    }

LayoutTool.java

    public static Component blockm(List<Component> cmps, int rowNum, int colNum, float hRatio, float vRatio)
    {
        return block(cmps.stream().map(cmp -> center(cmp, hRatio, vRatio)).collect(toList()), rowNum, colNum);
    }

运行结果如下:
impl layout style blockm

9 Implementation - Mini-calculator

我们来看一个稍微复杂一些的例子,我们将使用前面制作的一些布局样式构建一个迷你计算器的外观,如下图所示:

design minicalc

TestFrm.java

    private void test_minicalc()
    {
        List<String> texts = asList("0", "1", "2", "+",
                                    "3", "4", "5", "-",
                                    "6", "7", "8", "*",
                                    "9", "=", "%", "/");
        List<Component> btns = texts.stream().map(text -> button(text)).collect(toList());

        above(above(textField(), beside(button("Backspace"), button("C"), 0.5f), 0.5f), 
              block(btns, 4, 4), 0.3f).at(0, 0, 545, 325).in(container);
    }

    public static void main(String[] args)
    {
        TestFrm frm = new TestFrm();
        
        //Test Cases
//        frm.test_component();
//        frm.test_beside();
//        frm.test_above();
//        frm.test_beside_above();
//        frm.test_empty();
//        frm.test_hCenter();
//        frm.test_vCenter();
//        frm.test_center();
//        frm.test_hSeq();
//        frm.test_vSeq();
//        frm.test_block();
//        frm.test_blockm();
        frm.test_minicalc();
        
        frm.centerShow();
    }

如果我们现在希望将所有数字以及操作按钮按照横向和纵向各2%进行留白,如下图所示:

design minicalc margin

我们所要做的仅仅是一行的改动,就是把block()换成blockm()。

TestFrm类增加测试用例。

    private void test_minicalc_margin()
    {
        List<String> texts = asList("0", "1", "2", "+",
                                    "3", "4", "5", "-",
                                    "6", "7", "8", "*",
                                    "9", "=", "%", "/");
        List<Component> btns = texts.stream().map(text -> button(text)).collect(toList());

        above(above(textField(), beside(button("Backspace"), button("C"), 0.5f), 0.5f), 
              blockm(btns, 4, 4, 0.02f, 0.02f), 0.3f).at(0, 0, 545, 325).in(container);
    }

    public static void main(String[] args)
    {
        TestFrm frm = new TestFrm();
        
        //Test Cases
//        frm.test_component();
//        frm.test_beside();
//        frm.test_above();
//        frm.test_beside_above();
//        frm.test_empty();
//        frm.test_hCenter();
//        frm.test_vCenter();
//        frm.test_center();
//        frm.test_hSeq();
//        frm.test_vSeq();
//        frm.test_block();
//        frm.test_blockm();
//        frm.test_minicalc();
        frm.test_minicalc_margin();
        
        frm.centerShow();
    }

10 Summary

10.1 UI Layout Language

Language-Oriented Programming vs. Object-Oriented Programming
在设计中,我们没有采用对象技术中常用的一些设计手段,我们没有对界面布局本身进行抽象,也不是设计出一些特定的界面布局管理器。相反,我们把对象技术当成一种低层的抽象工具,并基于它来构建更高层次的抽象,创建出更加接近我们所工作的问题领域的界面布局语言,从而获得更高的生产力、表达力以及可重用性(还有什么比语言更加易于重用),这就是Language-Oriented Programming。

APIs for Describing UI Layout Specification vs. Object Interfaces of Java Language
界面布局语言所提供的接口不是Java语言层面上的对象接口,也不是使用基于Java的语法来使用这些接口构建复杂的界面。相反,我们提供了一个面向界面设计规格描述的接口,接口的语义、规则以及命名完全和界面设计中的规则、概念相符,这样就可以直接使用代码来清晰、直接地表达出界面设计中的布局概念。

Layout styles defined by UI layout language can be combined to higher-order layout styles.
使用界面布局语言可以非常方便地定义出一些常见的布局样式,还可以把这些样式组合成更为复杂的一些高阶布局样式,并且这种组合是没有任何限制的。这些布局样式的定义描述方式和界面设计者头脑中的所使用的一些布局词汇和规则是贴近的。

UI designers can write programs to design UI in UI layout language. No boring, enjoy it!
界面设计者可以直接使用界面布局语言进行界面制作,可以直接针对布局进行编程,所写出来的界面代码就是布局规格说明。界面设计者完全可以摆脱那些呆板、机械又难以定制和扩展的布局管理器,可以轻松的把头脑中的布局创意直接描述出来,逐步形成自己的布局样式库,充分享受这种创造性的工作所带来的乐趣。

10.2 A Sequence of Layers of Language

summary layers of language

  • Java Swing界面开发语言:

    • 位于最底层。
    • JButton, JTextField …
    • component.setBounds, container.add
  • 界面布局位置描述语言

    • 基于Java Swing界面开发语言,构建出界面布局位置描述语言。
    • Primitive Components: Empty, Button, TextField …
    • Combinator Components: beside, above
    • Rectangle:at
    • Container:in
  • 界面布局样式描述语言

    • 基于界面布局位置描述语言,构建出用来定义和表达各种布局样式的界面布局样式描述语言。
    • hCenter, vCenter, center, hSeq, vSeq, block, blockm …

在界面布局语言的设计上,我们没有采用定制的面向对象的设计,而是由一组处于不同层次的语言组成,每个层次都是通过对该层的基本原子进行组合构造而来,每个层次所构造出来的实体,则可以作为上一层语言的基本原子使用。这样,我们就在通用的Java语言之上,逐步构建出了一种专用于表达界面布局的语言。比起传统的对象设计,这种方法具有更高的抽象层次和通用性。

  • 程序的健壮性
    界面布局语言是分层的,这种设计非常有助于构建健壮的程序。这里健壮的含意是指:问题领域中的一个小的更改,所导致的程序更改也应当是相应地小的,呈线性是最好的。

比如,我们在构建迷你计算器时,希望所有数字以及运算符按钮都在横向和纵向留一些空白,这个问题领域中的一个小的更改,所对应的程序更改就是把block更改为blockm而已。

  • 程序的隔离性
    此外,由于分层的存在,我们可以自由地修改不同层次的表达细节而对其他层次不会造成任何影响。也就是说,每一层提供了用于表达系统特征的不同词汇以及不同的更改方式和能力。

10.3 Model-View-Presenter

在前面讲述的界面布局语言中仅仅涉及了界面布局元素的显示样式方面的内容,但是一个完整的界面是需要和后端的应用逻辑交互的,因此还需要一个粘合界面显示和应用模型的层次。

之所以在这里没有提这项内容主要是为了避免陷入其实现的琐碎细节中,从而可以集中介绍界面布局语言本身。

为了能够对界面布局元素进行编程控制,我们让每个布局元素都有一个“拥有者”。和布局元素在物理上的包含关系不同,“拥有者”是编程语意上的。也就是说,对布局元素在编程意义上的所有控制操作都在其“拥有者”中完成,这种思路完全隔离了显示和控制,其实就是MVP模式的一种实现。

比如,我们可以这样描述一个Button:

button(“button1”).ownby(btnController);

关于Button的所有事件处理和操控都在btnController中完成。

关于这项内容,就不做具体介绍了,感兴趣的同学可以自己试试。

10.4 Implementation in Others Languages

由于动态语言提供了更高的动态性和元编程能力,因此在动态语言中更容易实现这种设计思路,可以用Python语言基于wxPython界面工具库实现界面布局语言,相比Java,在实现上确实要容易和清晰得多。

支持函数式编程的语言,比如Erlang或Python,实现起来也比Java要容易和清晰得多。

10.5 What is Design?

设计就是把问题变成可计算的。

design computable

10.6 What is Good Design?

设计出的计算模型所提供的语义和问题领域的根本需求是否匹配,匹配就是好的设计,不匹配就是不好的设计。

good design

10.7 How to do Design?

how to do design

  • 对问题领域进行深入分析,发现问题领域的核心需求;
  • 通过核心需求驱动出计算模型和语义;
  • 再围绕这个计算模型提供一套语言,给外面的人使用这个计算模型提供一个接口,这个接口可以是API、可以是数据表达、也可以是语言;
  • 最终要实现这个计算模型,实现的方法有解释器和编译器两种。

这就是DDD!这才是DDD!
这就是DSL!这才是DSL!

10.8 What is Programming?

“编程”不过是在某个计算模型上用某种语言去表达计算。 用DSL编程不过是在问题领域的计算模型上用DSL来表达计算。
计算模型是相应领域中的“通用机器”。 问题领域的计算模型是问题领域的通用机器。
编程语言不过是描述计算机器的一种方法。 DSL描述的是DSL语言的计算机器。
程序是对特定机器的描述,这个特定机器可以被通用机器仿真。 用DSL程序实现了问题领域中的某个功能,这个DSL程序就是对这个功能(特定机器)的描述。

10.9 Design & Programming

design and programming

  • 从问题领域导出核心需求,得到领域的计算模型(通用计算机器),在上面可以包装一个DSL语言或者数据或者API,基于这些开发程序和应用。
  • 领域的计算模型和通用语言的计算模型之间存在鸿沟,可以用解释器或者编译器来填补。
  • 解释器和编译器听起来很复杂,其实思想很简单,而且我们没有必要实现一个工业级别的、非常全面的解释器和编译器,只要借鉴这个思想实现我们的计算模型就够了。

Common Languages(Java/C) UML对应Java通用语言。
Common Languages(Java/C) UM对应Java通用语言提供的计算模型(通用计算机器)。

10.10 How to improve Design Capability?

提升设计能力的根本在于提升计算模型构造和语义定义能力。

Thinking, thinking, thinking …
Practice, practice and practice …
No shortcuts.

11 Homework

  1. 【必选】实现“全局参数配置”功能的外观,如下图所示:

impl global param

  1. 【可选】用函数式编程方式实现Layout。

12 My Homework

12.1 Global Parameters

代码包路径:fayelab.ddd.layout.globalparam.oo

TestFrm.java

    private void test_globalparam()
    {
        Component params = vSeq(asList(
                param("Parameter 1", textField()),
                param("Parameter 2", textField()),
                param("Parameter 3", textField())));

        Component btns = beside(empty(), 
                                center(beside(button("Set"), 
                                              beside(empty(), button("Close"), 0.1f), 0.5f), 0.06f, 0.2f), 0.2f);

        above(params, btns, 0.8f).at(0, 0, 545, 320).in(container);
    }
    
    private Component param(String labelText, Component cmp)
    {
        return center(beside(label(labelText), beside(empty(), cmp, 0.1f), 0.3f), 0.05f, 0.3f);
    }
    
    public static void main(String[] args)
    {
        TestFrm frm = new TestFrm();
        
        //Test Cases
//        frm.test_component();
//        frm.test_beside();
//        frm.test_above();
//        frm.test_beside_above();
//        frm.test_empty();
//        frm.test_hCenter();
//        frm.test_vCenter();
//        frm.test_center();
//        frm.test_hSeq();
//        frm.test_vSeq();
//        frm.test_block();
//        frm.test_blockm();
//        frm.test_minicalc();
//        frm.test_minicalc_margin();
        frm.test_globalparam();
        
        frm.centerShow();
    }

LayoutTool.java

    public static Label label(String text)
    {
        return new Label(text);
    }

Label.java

package fayelab.ddd.layout.globalparam.oo.component;

public class Label extends BaseComponent
{
    public Label(String text)
    {
        cmp = new JLabel(text);
    }
}

运行结果如下:
impl global param

12.2 Functional

12.2.1 About Test

用于测试的Test Frame同OO实现。

12.2.2 Language of Geometric Positions

12.2.2.1 Component

TestFrm.java

    private void test_component()
    {
        button("Button").apply(rectangle(0, 0, 200, 60)).accept(container);
    }

    public static void main(String[] args)
    {
        TestFrm frm = new TestFrm();
        
        //Test Cases
        frm.test_component();
        
        frm.centerShow();
    }

Layout.java

public class Layout
{
    public static Rectangle rectangle(int left, int top, int width, int height)
    {
        return new Rectangle(left, top, width, height);
    }
    
    public static Function<Rectangle, Consumer<Container>> button(String text)
    {
        JButton btn = new JButton(text);
        return rectangle -> {
            btn.setBounds(rectangle);
            return container -> container.add(btn);
        };
    }
}

12.2.2.2 beside

先实现TextField。

TestFrm.java

    private void test_beside()
    {
        beside(textField(), button("Btn"), 0.8f).apply(rectangle(0, 0, 300, 60)).accept(container);
    }

    public static void main(String[] args)
    {
        TestFrm frm = new TestFrm();
        
        //Test Cases
//        frm.test_component();
        frm.test_beside();

        frm.centerShow();
    }

Layout.java

    public static Function<Rectangle, Consumer<Container>> textField()
    {
        JTextField textField = new JTextField();
        return rectangle -> {
            textField.setBounds(rectangle);
            return container -> container.add(textField);
        };
    }

再来实现beside。

我们用TDD实现beside的业务逻辑,即左右两个Component各自对应的布局空间Rectangle是否正确。既然是测试环境,肯定不用真实的控件,componentStub函数用来打桩,简单保存两个Component的rectangle参数和container引用。另外,为了更好地验证逻辑,rectangle左上角的坐标没有从(0, 0)开始。

LayoutTest.java

package fayelab.ddd.layout.original.functional;

public class LayoutTest extends TestCase
{
    private static final int TOLERANCE = 1;
    
    private ArrayList<Rectangle> rectangles;
    private ArrayList<Container> containers;
    private Container container;

    protected void setUp()
    {
        this.rectangles = new ArrayList<>();
        this.containers = new ArrayList<>();
        this.container = new Container();
    }
    
    public void test_beside()
    {
        Function<Rectangle, Consumer<Container>> leftCmp = componentStub();
        Function<Rectangle, Consumer<Container>> rightCmp = componentStub();
        
        beside(leftCmp, rightCmp, 0.8f).apply(rectangle(20, 10, 300, 60)).accept(container);
        
        checkRectangle(rectangle(20, 10, 240, 60), rectangles.get(0));
        checkRectangle(rectangle(260, 10, 60, 60), rectangles.get(1));
        
        assertSame(container, containers.get(0));
        assertSame(container, containers.get(1));
    }

    private Function<Rectangle, Consumer<Container>> componentStub()
    {
        return rectangle -> {
            rectangles.add(rectangle);
            return container -> containers.add(container);
        };
    }
    
    private void checkRectangle(Rectangle expected, Rectangle actual)
    {
        assertEquals((int)expected.getX(), (int)actual.getX(), TOLERANCE);
        assertEquals((int)expected.getY(), (int)actual.getY(), TOLERANCE);
        assertEquals((int)expected.getWidth(), (int)actual.getWidth(), TOLERANCE);
        assertEquals((int)expected.getHeight(), (int)actual.getHeight(), TOLERANCE);
    }
}

Layout.java

    public static Function<Rectangle, Consumer<Container>> beside(Function<Rectangle, Consumer<Container>> leftCmp, 
            Function<Rectangle, Consumer<Container>> rightCmp, float ratio)
    {
        return outerRectangle -> container -> {
            leftCmp.apply(rectangle((int)outerRectangle.getX(), 
                                    (int)outerRectangle.getY(),
                                    (int)(outerRectangle.getWidth() * ratio), 
                                    (int)outerRectangle.getHeight())).accept(container);;
            rightCmp.apply(rectangle((int)(outerRectangle.getX() + outerRectangle.getWidth() * ratio), 
                                     (int)outerRectangle.getY(),
                                     (int)(outerRectangle.getWidth() * (1 - ratio)), 
                                     (int)outerRectangle.getHeight())).accept(container);
        };
    }

button()和textField()这两个函数存在重复代码,将重复代码抽取到函数中。

Layout.java

    public static Function<Rectangle, Consumer<Container>> button(String text)
    {
        return atIn(new JButton(text));
    }
    
    public static Function<Rectangle, Consumer<Container>> textField()
    {
        return atIn(new JTextField());
    }
    
    private static Function<Rectangle, Consumer<Container>> atIn(Component cmp)
    {
        return rectangle -> {
            cmp.setBounds(rectangle);
            return container -> container.add(cmp);
        };
    }

12.2.2.3 above

我们用TDD实现above的业务逻辑,即上下两个Component各自对应的布局空间Rectangle是否正确。既然是测试环境,肯定不用真实的控件,componentStub函数用来打桩,简单保存两个Component的rectangle参数和container引用。另外,为了更好地验证逻辑,rectangle左上角的坐标没有从(0, 0)开始。

LayoutTest.java

    public void test_above()
    {
        Function<Rectangle, Consumer<Container>> upCmp = componentStub();
        Function<Rectangle, Consumer<Container>> downCmp = componentStub();
        
        above(upCmp, downCmp, 0.5f).apply(rectangle(20, 10, 300, 60)).accept(container);
        
        checkRectangle(rectangle(20, 10, 300, 30), rectangles.get(0));
        checkRectangle(rectangle(20, 40, 300, 30), rectangles.get(1));
        
        assertSame(container, containers.get(0));
        assertSame(container, containers.get(1));
    }

Layout.java

    public static Function<Rectangle, Consumer<Container>> above(Function<Rectangle, Consumer<Container>> upCmp, 
            Function<Rectangle, Consumer<Container>> downCmp, float ratio)
    {
        return outerRectangle -> container -> {
            upCmp.apply(rectangle((int)outerRectangle.getX(), 
                                  (int)outerRectangle.getY(),
                                  (int)outerRectangle.getWidth(), 
                                  (int)(outerRectangle.getHeight() * ratio))).accept(container);
            downCmp.apply(rectangle((int)outerRectangle.getX(), 
                                    (int)(outerRectangle.getY() + outerRectangle.getHeight() * ratio),
                                    (int)outerRectangle.getWidth(), 
                                    (int)(outerRectangle.getHeight() * (1 - ratio)))).accept(container);
        };
    }

TestFrm.java

    private void test_above()
    {
        above(textField(), button("Button"), 0.5f).apply(rectangle(0, 0, 300, 60)).accept(container);
    }

    public static void main(String[] args)
    {
        TestFrm frm = new TestFrm();
        
        //Test Cases
//        frm.test_component();
//        frm.test_beside();
        frm.test_above();
        
        frm.centerShow();
    }

12.2.2.4 The components' world is Closed

TestFrm.java

    private void test_beside_above()
    {
        above(beside(textField(), button("Btn1"), 0.8f), 
              button("Btn2"), 0.5f).apply(rectangle(0, 0, 300, 60)).accept(container);
    }

    public static void main(String[] args)
    {
        TestFrm frm = new TestFrm();
        
        //Test Cases
//        frm.test_component();
//        frm.test_beside();
//        frm.test_above();
        frm.test_beside_above();
        
        frm.centerShow();
    }

12.2.2.5 Empty

TestFrm.java

    private void test_empty()
    {        
        beside(empty(), button("Button"), 0.5f).apply(rectangle(0, 0, 300, 60)).accept(container);
    }

    public static void main(String[] args)
    {
        TestFrm frm = new TestFrm();
        
        //Test Cases
//        frm.test_component();
//        frm.test_beside();
//        frm.test_above();
//        frm.test_beside_above();
        frm.test_empty();
        
        frm.centerShow();
    }

Layout.java

    public static Function<Rectangle, Consumer<Container>> empty()
    {
        return outerRectangle -> container -> {};
    }

12.2.3 Language of Layout Style

12.2.3.1 Horizontal Center

TestFrm.java

    private void test_hCenter()
    {
        hCenter(button("Button"), 0.1f).apply(rectangle(0, 0, 545, 325)).accept(container);
    }

    public static void main(String[] args)
    {
        TestFrm frm = new TestFrm();
        
        //Test Cases
//        frm.test_component();
//        frm.test_beside();
//        frm.test_above();
//        frm.test_beside_above();
//        frm.test_empty();
        frm.test_hCenter();
        
        frm.centerShow();
    }

Layout.java

    public static Function<Rectangle, Consumer<Container>> hCenter(
            Function<Rectangle, Consumer<Container>> cmp, float ratio)
    {
        return beside(empty(), beside(cmp, empty(), (1 - 2 * ratio) / (1 - ratio)), ratio); 
    }

12.2.3.2 Vertical Center

TestFrm.java

    private void test_vCenter()
    {
        vCenter(button("Button"), 0.1f).apply(rectangle(0, 0, 545, 325)).accept(container);
    }

    public static void main(String[] args)
    {
        TestFrm frm = new TestFrm();
        
        //Test Cases
//        frm.test_component();
//        frm.test_beside();
//        frm.test_above();
//        frm.test_beside_above();
//        frm.test_empty();
//        frm.test_hCenter();
        frm.test_vCenter();
        
        frm.centerShow();
    }

Layout.java

    public static Function<Rectangle, Consumer<Container>> vCenter(
            Function<Rectangle, Consumer<Container>> cmp, float ratio)
    {
        return above(empty(), above(cmp, empty(), (1 - 2 * ratio) / (1 - ratio)), ratio); 
    }

可以看到hCenter()和vCenter()之间除了beside、above不一样,其他代码都一样,用函数式编程范式重构去重。

Layout.java

    @FunctionalInterface
    private interface PosLayout
    {
        Function<Rectangle, Consumer<Container>> apply(Function<Rectangle, Consumer<Container>> cmp1,
                Function<Rectangle, Consumer<Container>> cmp2, float ratio);
    }
    
    public static Function<Rectangle, Consumer<Container>> hCenter(
            Function<Rectangle, Consumer<Container>> cmp, float ratio)
    {
        return xCenter(Layout::beside, cmp, ratio); 
    }
    
    public static Function<Rectangle, Consumer<Container>> vCenter(
            Function<Rectangle, Consumer<Container>> cmp, float ratio)
    {
        return xCenter(Layout::above, cmp, ratio); 
    }
    
    private static Function<Rectangle, Consumer<Container>> xCenter(PosLayout posLayout,
            Function<Rectangle, Consumer<Container>> cmp, float ratio)
    {
        return posLayout.apply(empty(), posLayout.apply(cmp, empty(), (1 - 2 * ratio) / (1 - ratio)), ratio);
    }

12.2.3.3 Center

TestFrm.java

    private void test_center()
    {
        center(button("Center"), 0.2f, 0.1f).apply(rectangle(0, 0, 545, 325)).accept(container);
    }

    public static void main(String[] args)
    {
        TestFrm frm = new TestFrm();
        
        //Test Cases
//        frm.test_component();
//        frm.test_beside();
//        frm.test_above();
//        frm.test_beside_above();
//        frm.test_empty();
//        frm.test_hCenter();
//        frm.test_vCenter();
        frm.test_center();
        
        frm.centerShow();
    }

Layout.java

    public static Function<Rectangle, Consumer<Container>> center(
            Function<Rectangle, Consumer<Container>> cmp, float hRatio, float vRatio)
    {
        return vCenter(hCenter(cmp, hRatio), vRatio);
    }

12.2.3.4 Horizontal Sequence

TestFrm.java

    private void test_hSeq()
    {
        hSeq(asList(button("1"), button("2"), button("3"))).apply(rectangle(0, 0, 300, 60)).accept(container);
    }

    public static void main(String[] args)
    {
        TestFrm frm = new TestFrm();
        
        //Test Cases
//        frm.test_component();
//        frm.test_beside();
//        frm.test_above();
//        frm.test_beside_above();
//        frm.test_empty();
//        frm.test_hCenter();
//        frm.test_vCenter();
//        frm.test_center();
        frm.test_hSeq();
        
        frm.centerShow();
    }

Layout.java

    public static Function<Rectangle, Consumer<Container>> hSeq(List<Function<Rectangle, Consumer<Container>>> cmps)
    {
        if(cmps.size() == 1)
        {
            return cmps.get(0);
        }
        
        return beside(cmps.get(0), hSeq(cmps.subList(1, cmps.size())), 1.0f / cmps.size());
    }

12.2.3.5 Vertical Sequence

TestFrm.java

    private void test_vSeq()
    {
        vSeq(asList(button("1"), button("2"), button("3"))).apply(rectangle(0, 0, 150, 200)).accept(container);
    }

    public static void main(String[] args)
    {
        TestFrm frm = new TestFrm();
        
        //Test Cases
//        frm.test_component();
//        frm.test_beside();
//        frm.test_above();
//        frm.test_beside_above();
//        frm.test_empty();
//        frm.test_hCenter();
//        frm.test_vCenter();
//        frm.test_center();
//        frm.test_hSeq();
        frm.test_vSeq();

        frm.centerShow();
    }

Layout.java

    public static Function<Rectangle, Consumer<Container>> vSeq(List<Function<Rectangle, Consumer<Container>>> cmps)
    {
        if(cmps.size() == 1)
        {
            return cmps.get(0);
        }
        
        return above(cmps.get(0), vSeq(cmps.subList(1, cmps.size())), 1.0f / cmps.size());
    }

可以看到hSeq()和vSeq()之间除了beside、above不一样,其他代码都一样,用函数式编程范式重构去重。

Layout.java

    public static Function<Rectangle, Consumer<Container>> hSeq(List<Function<Rectangle, Consumer<Container>>> cmps)
    {
        return seq(Layout::beside, cmps);
    }
    
    public static Function<Rectangle, Consumer<Container>> vSeq(List<Function<Rectangle, Consumer<Container>>> cmps)
    {
        return seq(Layout::above, cmps);
    }
    
    private static Function<Rectangle, Consumer<Container>> seq(PosLayout posLayout, 
            List<Function<Rectangle, Consumer<Container>>> cmps)
    {
        if(cmps.size() == 1)
        {
            return cmps.get(0);
        }
        
        return posLayout.apply(cmps.get(0), seq(posLayout, cmps.subList(1, cmps.size())), 1.0f / cmps.size());
    }

12.2.3.6 Block

TestFrm.java

    private void test_block()
    {
        List<Function<Rectangle, Consumer<Container>>> cmps = 
                IntStream.rangeClosed(1, 11)
                         .mapToObj(i -> button(String.valueOf(i)))
                         .collect(toList());
                
        block(cmps, 4, 3).apply(rectangle(0, 0, 545, 325)).accept(container);
    }

    public static void main(String[] args)
    {
        TestFrm frm = new TestFrm();
        
        //Test Cases
//        frm.test_component();
//        frm.test_beside();
//        frm.test_above();
//        frm.test_beside_above();
//        frm.test_empty();
//        frm.test_hCenter();
//        frm.test_vCenter();
//        frm.test_center();
//        frm.test_hSeq();
//        frm.test_vSeq();
        frm.test_block();
        
        frm.centerShow();
    }

Layout.java

    public static Function<Rectangle, Consumer<Container>> block(
            List<Function<Rectangle, Consumer<Container>>> cmps, int rowNum, int colNum)
    {
        return vSeq(normalize(cmps, rowNum, colNum).stream().map(rowCmps -> hSeq(rowCmps)).collect(toList()));
    }
    
    @SuppressWarnings("unchecked")
    private static Collection<List<Function<Rectangle, Consumer<Container>>>> normalize(
            List<Function<Rectangle, Consumer<Container>>> cmps, int rowNum, int colNum)
    {
        List<Function<Rectangle, Consumer<Container>>> paddedCmps = padding(cmps, rowNum * colNum - cmps.size());
      
        return IntStream.range(0, paddedCmps.size())
                        .mapToObj(idx -> asList(idx, paddedCmps.get(idx)))
                        .collect(groupingBy(idxAndCmp -> (Integer)idxAndCmp.get(0) / colNum, 
                                            mapping(idxAndCmp -> (Function<Rectangle, Consumer<Container>>)idxAndCmp.get(1), toList())))
                        .values();
    }

    private static List<Function<Rectangle, Consumer<Container>>> padding(
            List<Function<Rectangle, Consumer<Container>>> cmps, int num)
    {
        return Stream.concat(cmps.stream(), Collections.nCopies(num, empty()).stream()).collect(toList());
    }

12.2.3.7 Block With Margin

TestFrm.java

    private void test_blockm()
    {
        List<Function<Rectangle, Consumer<Container>>> cmps = 
                IntStream.rangeClosed(1, 11)
                         .mapToObj(i -> button(String.valueOf(i)))
                         .collect(toList());
                
        blockm(cmps, 4, 3, 0.1f, 0.1f).apply(rectangle(0, 0, 545, 325)).accept(container);
    }

    public static void main(String[] args)
    {
        TestFrm frm = new TestFrm();
        
        //Test Cases
//        frm.test_component();
//        frm.test_beside();
//        frm.test_above();
//        frm.test_beside_above();
//        frm.test_empty();
//        frm.test_hCenter();
//        frm.test_vCenter();
//        frm.test_center();
//        frm.test_hSeq();
//        frm.test_vSeq();
//        frm.test_block();
        frm.test_blockm();
        
        frm.centerShow();
    }

Layout.java

    public static Function<Rectangle, Consumer<Container>> blockm(
            List<Function<Rectangle, Consumer<Container>>> cmps, int rowNum, int colNum, float hRatio, float vRatio)
    {
        return block(cmps.stream().map(cmp -> center(cmp, hRatio, vRatio)).collect(toList()), rowNum, colNum);
    }

12.2.4 Mini-calculator

TestFrm.java

    private void test_minicalc()
    {
        List<String> texts = asList("0", "1", "2", "+",
                                    "3", "4", "5", "-",
                                    "6", "7", "8", "*",
                                    "9", "=", "%", "/");
        List<Function<Rectangle, Consumer<Container>>> btns = texts.stream().map(text -> button(text)).collect(toList());
        
        above(above(textField(), beside(button("Backspace"), button("C"), 0.5f), 0.5f), 
              block(btns, 4, 4), 0.3f).apply(rectangle(0, 0, 545, 325)).accept(container);
    }

    public static void main(String[] args)
    {
        TestFrm frm = new TestFrm();
        
        //Test Cases
//        frm.test_component();
//        frm.test_beside();
//        frm.test_above();
//        frm.test_beside_above();
//        frm.test_empty();
//        frm.test_hCenter();
//        frm.test_vCenter();
//        frm.test_center();
//        frm.test_hSeq();
//        frm.test_vSeq();
//        frm.test_block();
//        frm.test_blockm();
        frm.test_minicalc();
        
        frm.centerShow();
    }

如果我们现在希望将所有数字以及操作按钮按照横向和纵向各2%进行留白,我们所要做的仅仅是一行的改动,就是把block()换成blockm()。

TestFrm.java

    private void test_minicalc_margin()
    {
        List<String> texts = asList("0", "1", "2", "+",
                                    "3", "4", "5", "-",
                                    "6", "7", "8", "*",
                                    "9", "=", "%", "/");
        List<Function<Rectangle, Consumer<Container>>> btns = texts.stream().map(text -> button(text)).collect(toList());
        
        above(above(textField(), beside(button("Backspace"), button("C"), 0.5f), 0.5f), 
              blockm(btns, 4, 4, 0.02f, 0.02f), 0.3f).apply(rectangle(0, 0, 545, 325)).accept(container);
    }

    public static void main(String[] args)
    {
        TestFrm frm = new TestFrm();
        
        //Test Cases
//        frm.test_component();
//        frm.test_beside();
//        frm.test_above();
//        frm.test_beside_above();
//        frm.test_empty();
//        frm.test_hCenter();
//        frm.test_vCenter();
//        frm.test_center();
//        frm.test_hSeq();
//        frm.test_vSeq();
//        frm.test_block();
//        frm.test_blockm();
//        frm.test_minicalc();
        frm.test_minicalc_margin();
        
        frm.centerShow();
    }

12.2.5 Global Parameters

代码路径:globalparam.functional

TestFrm.java

    private void test_globalparam()
    {
        Function<Rectangle, Consumer<Container>> params = vSeq(asList(
                param("Parameter 1", textField()),
                param("Parameter 2", textField()),
                param("Parameter 3", textField())));

        Function<Rectangle, Consumer<Container>> btns = 
                beside(empty(), 
                       center(beside(button("Set"), 
                                     beside(empty(), button("Close"), 0.1f), 0.5f), 0.06f, 0.2f), 0.2f);

        above(params, btns, 0.8f).apply(rectangle(0, 0, 545, 325)).accept(container);
    }
    
    private Function<Rectangle, Consumer<Container>> param(
            String labelText, Function<Rectangle, Consumer<Container>> cmp)
    {
        return center(beside(label(labelText), beside(empty(), cmp, 0.1f), 0.3f), 0.05f, 0.3f);
    }

    public static void main(String[] args)
    {
        TestFrm frm = new TestFrm();
        
        //Test Cases
//        frm.test_component();
//        frm.test_beside();
//        frm.test_above();
//        frm.test_beside_above();
//        frm.test_empty();
//        frm.test_hCenter();
//        frm.test_vCenter();
//        frm.test_center();
//        frm.test_hSeq();
//        frm.test_vSeq();
//        frm.test_block();
//        frm.test_blockm();
//        frm.test_minicalc();
//        frm.test_minicalc_margin();
        frm.test_globalparam();
        
        frm.centerShow();
    }

Layout.java

    public static Function<Rectangle, Consumer<Container>> label(String text)
    {
        return atIn(new JLabel(text));
    }
⚠️ **GitHub.com Fallback** ⚠️