本文通过步骤显示功能的实现讲述了我进行类结构设计的一些心得,通过这些类可以查看三角剖分各个步骤的具体情况,是算法调试可视化的有力工具。
清明小憩之后,我们再续前文。
上篇文章中有一段剖分带洞多边形的录像,在绘制好任意多边形后,点击一些按钮可以查看整个剖分的各个步骤,本篇就详细讲述这个步骤显示功能的实现方式。
每个人在设计类结构时都会有自己的习惯,我的方式是在一片空白时去构想我将要实现功能的各个参与者,它们各自担任的角色以及负责的功能,在这个过程中还不需要考虑接口,更不用考虑代码,只是考虑某个参与者是否有必要并且明确它所承担的责任。我发现将每个参与者拟人化是很有益处的,如果将其拟成某种固有属性与其承担责任非常相近的具体物件则更有帮助,这个过程我称之为形象化,一个著名的例子当属网络爬虫。当你将每个参与者形象化之后,你将发现现在的程序不再是一块块死气沉沉的代码,而变成了一组生机勃勃的团队,他们的目标与分工都是如此明确。
这种方式还可以再进一步,形象化时可以将某些参与者弄的憨态可掬一些,当它上场时它的某些行为很可能就惹得你忍俊不禁,这对提升一个开发者的激情很有好处,对厘清某个复杂的逻辑也颇有裨益,在《第七回:寻找三角形,夺取红旗》中你将看到我的爬虫是怎样在辨别果壳与果核的过程中帮我找到一个个梯形的。
一、设计类结构
在设计本文主题——步骤显示功能的结构时,正是运用了这种形象化的设计方式。首先在一张空白的纸上我画上了一个管理所有显示步骤的集合类ShowStepCollection,该类的角色好似一个看管学校的大爷,如果想找某个显示步骤,不要试图去直接访问,问问大爷就可以找到,当然如果你要添加一个显示步骤也要通过这位大爷,大爷会为他安排好一切。这个类将步骤显示功能封装到了背后,在程序中如果希望访问显示步骤都要到这里来,可以认为该类是一个门面。
下面要考虑的就是这个集合类中要存储的东西,看看上篇文章中的录像你会发现步骤的显示是以每条扫描线进行分组的,所以在此集合类中要有一个显示步骤组ShowStepGroup的集合,而每个步骤组中再包含具体的显示步骤ShowStep,经过这样一系列的包装,显示步骤的存储就算完成了,下面给出了类图。
显示步骤多种多样,比如显示左辅助线、显示扫描线、显示交点、显示三角形等等,而他们各自的处理方式显然不同,这样需要用到面向对象的一个最基本的特性——多态,因而ShowStep是一个抽象类。在上面的设计过程中,体现了针对抽象编程的设计原则。
二、设计接口
类结构搭建好后,下一步就是让他们协调工作起来,而协调的媒介正是接口。
ShowStepCollection需要添加显示步骤组以及添加显示步骤,所以需要addGroup和add两个函数,为了能够访问各个步骤组,需要Count、Item属性以及一个用于遍历的GetEnumerator,该函数实现了IEnumerable的相应接口,还有一个用来撤销所有步骤显示的clear函数。
ShowStepGroup是存储具体显示步骤的地方,所以需要添加显示步骤的add函数,以及与ShowStepCollection功能一致的Count、Item属性和GetEnumerator、clear方法。ShowStepCollection的add函数正是调用ShowStepGroup的add函数来完成添加具体步骤功能的。
ShowStep作为抽象类具有两个抽象函数,show和clear,分别用来显示和撤销显示。
三、代码(略过)
这里的显示全部利用超图组件,因而涉及到显示的部分会传入一个大家很不熟悉的参数: map As AxSuperMapLib.AxSuperMap,这没关系,如果要是画在一个windows窗体上传入的就该是该窗体的Graphics了,当然具体到代码会有些不同。
ShowStepCollection
1Public Class ShowStepCollectionClass ShowStepCollection
2 Implements IEnumerable(Of ShowStepGroup)
3
4 Private myStepGroups As New List(Of ShowStepGroup)
5
6 Public Sub addGourp()Sub addGourp()
7 Me.myStepGroups.Add(New ShowStepGroup)
8 End Sub
9
10 Public Sub add()Sub add(ByVal ss As ShowStep)
11 Me.myStepGroups(Me.myStepGroups.Count - 1).add(ss)
12 End Sub
13
14 Public ReadOnly Property Count()Property Count() As Integer
15 Get
16 Return Me.myStepGroups.Count
17 End Get
18 End Property
19
20 Default Public ReadOnly Property Item()Property Item(ByVal i) As ShowStepGroup
21 Get
22 Return Me.myStepGroups(i)
23 End Get
24 End Property
25
26 Public Sub clear()Sub clear(ByRef map As AxSuperMapLib.AxSuperMap)
27 For Each stepGroups As ShowStepGroup In Me.myStepGroups
28 stepGroups.clear(map)
29 Next
30 End Sub
31
32 Public Function GetEnumerator()Function GetEnumerator() As System.Collections.Generic.IEnumerator(Of ShowStepGroup) Implements System.Collections.Generic.IEnumerable(Of ShowStepGroup).GetEnumerator
33 Return Me.myStepGroups.GetEnumerator
34 End Function
35
36 Public Function GetEnumerator1()Function GetEnumerator1() As System.Collections.IEnumerator Implements System.Collections.IEnumerable.GetEnumerator
37 Return Me.myStepGroups.GetEnumerator
38 End Function
39
40End Class
41
ShowStepGroup
1Public Class ShowStepGroupClass ShowStepGroup
2 Implements IEnumerable(Of ShowStep)
3
4 Private myStepList As New List(Of ShowStep)
5
6 Public Sub add()Sub add(ByVal ss As ShowStep)
7 Me.myStepList.Add(ss)
8 End Sub
9
10 Public ReadOnly Property Count()Property Count() As Integer
11 Get
12 Return Me.myStepList.Count
13 End Get
14 End Property
15
16 Default Public ReadOnly Property Item()Property Item(ByVal i) As ShowStep
17 Get
18 Return Me.myStepList(i)
19 End Get
20 End Property
21
22 Public Sub clear()Sub clear(ByRef map As AxSuperMapLib.AxSuperMap)
23 For Each showstep As ShowStep In Me.myStepList
24 If (TypeOf showstep Is IStableStep) Then Continue For
25 showstep.clear(map)
26 Next
27 End Sub
28
29 Public Function GetEnumerator()Function GetEnumerator() As System.Collections.Generic.IEnumerator(Of ShowStep) Implements System.Collections.Generic.IEnumerable(Of ShowStep).GetEnumerator
30 Return Me.myStepList.GetEnumerator
31 End Function
32
33 Public Function GetEnumerator1()Function GetEnumerator1() As System.Collections.IEnumerator Implements System.Collections.IEnumerable.GetEnumerator
34 Return Me.myStepList.GetEnumerator
35 End Function
36
37End Class
38
ShowStep
1Public MustInherit Class ShowStepClass ShowStep
2
3 Public MustOverride Sub show()Sub show(ByRef map As AxSuperMapLib.AxSuperMap)
4 Public MustOverride Sub clear()Sub clear(ByRef map As AxSuperMapLib.AxSuperMap)
5
6End Class
7
下面给出ShowStep的子类ShowTriangle的代码,其它子类大同小异
ShowTriangle
1Imports SuperMapLib
2
3Public Class ShowTriangleClass ShowTriangle
4 Inherits ShowStep
5
6 Protected myStyle As soStyle
7 Private point1 As Point2
8 Private point2 As Point2
9 Private point3 As Point2
10
11 Public Sub New()Sub New(ByVal point1 As Point2, ByVal point2 As Point2, ByVal point3 As Point2)
12 Me.point1 = point1
13 Me.point2 = point2
14 Me.point3 = point3
15 Me.myStyle = New soStyle
16 myStyle.PenColor = System.Convert.ToUInt32(RGB(255, 0, 0)) '边界的颜色设置为红色
17 myStyle.PenWidth = 8 '宽度为8
18 myStyle.BrushColor = System.Convert.ToUInt32(RGB(255, 0, 0)) '填充区域颜色设置为红色
19 myStyle.BrushOpaqueRate = 30 '透明度设置为30
20 End Sub
21
22 Public Overrides Sub show()Sub show(ByRef map As AxSuperMapLib.AxSuperMap)
23 Dim triangle As New soGeoRegion
24 Dim points As New soPoints
25 points.Add2(Me.point1.X, Me.point1.Y)
26 points.Add2(Me.point2.X, Me.point2.Y)
27 points.Add2(Me.point3.X, Me.point3.Y)
28 points.Add2(Me.point1.X, Me.point1.Y)
29 triangle.AddPart(points)
30 map.TrackingLayer.RemoveEvent("step:triangle" & Me.point1.ToString & Me.point2.ToString & Me.point3.ToString)
31 map.TrackingLayer.AddEvent(triangle, myStyle, "step:triangle" & Me.point1.ToString & Me.point2.ToString & Me.point3.ToString)
32 map.TrackingLayer.Refresh()
33 End Sub
34
35 Public Overrides Sub clear()Sub clear(ByRef map As AxSuperMapLib.AxSuperMap)
36 map.TrackingLayer.RemoveEvent("step:triangle" & Me.point1.ToString & Me.point2.ToString & Me.point3.ToString)
37 map.TrackingLayer.Refresh()
38 End Sub
39
40End Class
41
四、操纵显示步骤
经过上面的三个过程,显示步骤的存储、绘制等功能已经完成,也就是说静态的结构已经搭建完毕,下面就要让这些步骤以一种合适的方式动起来。这时你有一些选择的余地,一种方式是将步骤切换、步骤组切换等逻辑分别写到ShowStepGroup以及ShowStepCollection中去,这样做之所以会被想到正是受到封装思想的影响,封装的思想是什么,自己的数据自己消费,由于各个步骤组是步骤集合ShowStepCollection的数据,所以很自然的就会想到在该类内部添加一些方法,来操纵这些组,同样的想法也会出现在步骤组中。这样的想法不好吗,这种对数据的封装不对吗,我要说的是这不涉及对错,只是在这里不太适合,有更好的方式让问题更简单。那为什么不适合呢?
为了显示各个步骤,步骤集合ShowStepCollection需要做一些事情,ShowStepGroup也需要做一些事情,但从整体来看二者做的是同一件事情的不同部分,一个负责组切换、一个负责步骤切换,但两者之间并非泾渭分明,会有相互的影响,最明显的是当组切换时,步骤得回到新组的第一个。因而这样设计会增加二者的耦合度,并且违反了最小知识、单一职能等设计原则,扩展性极差,实现难度较高。
我的做法是设计一个专门的类ShowStepScanner来负责控制步骤的显示,无论是组切换还是步骤切换都在这一个类里完成,将分散在ShowStepCollection和ShowStepGroup中的控制逻辑抽取出来,合在一起放到这个类的相关方法中。下面是该类的类图以及代码。
ShowStepScanner
1Public Class ShowStepScannerClass ShowStepScanner
2
3 Private stepGroups As ShowStepCollection
4
5 Private currentGroupIndex As Integer
6 Private currentStepIndex As Integer
7
8 Private WithEvents timer As Windows.Forms.Timer
9
10 Public Sub New()Sub New(ByVal ssc As ShowStepCollection)
11 Me.stepGroups = ssc
12 currentGroupIndex = 0
13 currentStepIndex = -1
14 End Sub
15
16 Public Sub nextStep()Sub nextStep(ByRef map As AxSuperMapLib.AxSuperMap)
17 If (currentStepIndex = stepGroups(Me.currentGroupIndex).Count - 1) Then '如果达到某组末尾
18 For Each showstep As ShowStep In Me.stepGroups(currentGroupIndex) '将本组内的所有步骤清掉
19 If (TypeOf showstep Is IStableStep) Then Continue For '如果是稳定步骤不予处理
20 showstep.clear(map)
21 Next
22 If (currentGroupIndex = stepGroups.Count - 1) Then '如果达到组集合的末尾
23 currentGroupIndex = 0 '返回组集合头
24 Dim i As Integer = 1
25 Do While i <> map.TrackingLayer.EventCount + 1
26 If (map.TrackingLayer.Event(i).Tag.StartsWith("step:")) Then
27 map.TrackingLayer.RemoveEvent(i)
28 Else
29 i += 1
30 End If
31 Loop
32 Else '否则
33 currentGroupIndex += 1 '进入下一组
34 End If
35 currentStepIndex = 0 '回到下一组头
36 Else '如果没有达到组尾
37 currentStepIndex += 1 '前进到下一步
38 End If
39 If (Me.currentStepIndex > 0) Then '如果不是第一个步骤
40 Dim lastStep As ShowStep = Me.stepGroups(Me.currentGroupIndex)(Me.currentStepIndex - 1) '得到上一个步骤
41 If (TypeOf lastStep Is ITempStep) Then '如果上一步是临时步骤
42 lastStep.clear(map) '清掉上一步骤
43 End If
44 End If
45 Dim currentStep As ShowStep = Me.stepGroups(Me.currentGroupIndex)(Me.currentStepIndex) '得到当前步骤
46 currentStep.show(map) '显示当前步骤
47 End Sub
48
49 Public Sub lastStep()Sub lastStep(ByRef map As AxSuperMapLib.AxSuperMap)
50 If (currentStepIndex <= 0) Then '如果达到某组头(0)或第一次扫描步骤(-1)
51 If (currentGroupIndex = 0) Then '如果达到组集合头
52 currentStepIndex = -1 '返回组集合头
53 Do While Not (currentGroupIndex = stepGroups.Count - 1 AndAlso currentStepIndex = stepGroups(stepGroups.Count - 1).Count - 1)
54 Me.nextStep(map)
55 Loop
56 currentGroupIndex = stepGroups.Count - 1 '返回组集合尾
57 currentStepIndex = 0 '返回组集合头
58 Else '否则
59 For Each showstep As ShowStep In Me.stepGroups(currentGroupIndex) '将本组内的所有步骤清掉
60 showstep.clear(map)
61 Next
62 currentGroupIndex -= 1 '进入前一组
63 currentStepIndex = 0 '回到下一组头
64 Do While currentStepIndex <> Me.stepGroups(currentGroupIndex).Count - 1
65 Me.nextStep(map)
66 Loop
67 currentStepIndex = Me.stepGroups(Me.currentGroupIndex).Count - 1 '回到下一组尾
68 End If
69 Else '如果没有达到组尾
70 currentStepIndex -= 1 '前进到下一步
71 End If
72 If (Me.currentStepIndex < Me.stepGroups(Me.currentGroupIndex).Count - 1) Then '如果不是最后一个步骤
73 Dim nextStep As ShowStep = Me.stepGroups(Me.currentGroupIndex)(Me.currentStepIndex + 1) '得到下一个步骤
74 If Not (TypeOf nextStep Is IStableStep) Then '如果下一步是临时步骤
75 nextStep.clear(map) '清掉下一步骤
76 End If
77 End If
78 Dim currentStep As ShowStep = Me.stepGroups(Me.currentGroupIndex)(Me.currentStepIndex) '得到当前步骤
79 currentStep.show(map) '显示当前步骤
80 End Sub
81
82 Private supermap As AxSuperMapLib.AxSuperMap
83
84 Public Sub animation()Sub animation(ByRef map As AxSuperMapLib.AxSuperMap)
85 If (timer Is Nothing) Then
86 timer = New Timer()
87 End If
88 If (timer.Enabled) Then
89 timer.Stop()
90 Else
91 Me.supermap = map
92 timer.Interval = 500
93 timer.Start()
94 End If
95 End Sub
96
97 Private Sub timer_Tick()Sub timer_Tick(ByVal sender As Object, ByVal e As System.EventArgs) Handles timer.Tick
98 Me.nextStep(supermap)
99 End Sub
100
101 Public Sub Dispose()Sub Dispose()
102 If (timer IsNot Nothing AndAlso timer.Enabled) Then
103 timer.Stop()
104 End If
105 End Sub
106
107End Class
108
从类图和代码中可以看到,nextStep负责前进一步,通过控制组索引和步骤索引来实现下一步骤的显示,lastStep则是回退一步,animation则通过一个定时器实现了自动显示步骤的功能,如果没有将控制显示的逻辑从那两个类中提取出来,这些功能的实现是很困难的。
一个完整的步骤显示功能实现过程展现于此,在这其中有一些我自己开发中经常用到的技巧:为有待实现功能的各参与者进行功能划分;根据参与者的特点将其形象化,在可能的情况下添加一些趣味在里面;如果某项工作的逻辑较为分散将其抽取出来组织成一个新的实体,这其实就是添加了一个参与者。经过上述过程的反复迭代,最后会形成一个结构清晰、逻辑缜密、组织良好的类结构,为后续的编码甚至维护提供有力的保障。