100% found this document useful (3 votes)
1K views484 pages

Delphi Legacy Projects - Strategies and Survival Guide

Uploaded by

Andry
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
100% found this document useful (3 votes)
1K views484 pages

Delphi Legacy Projects - Strategies and Survival Guide

Uploaded by

Andry
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 484

Delphi Legacy Projects

Strategies and Survival Guide

William Meyer

June 24, 2022

Meyer Design
Disclaimer
All product names, logos, and brands are property of their respective owners. All
company, product and service names used in this website are for identification
purposes only. Use of these names, logos, and brands does not imply endorsement.

Copyright © 2021-2022 by William Meyer


All rights reserved. No part of this publication may be reproduced, distributed,
or transmitted in any form or by any means, including photocopying, record-
ing, or other electronic or mechanical methods, without the prior written
permission of the publisher, except in the case of brief quotations embodied in
critical reviews and certain other noncommercial uses permitted by copyright law.

Colophon
This document was typeset in LATEX with the help of KOMA-Script and LATEX
using the kaobook class.

The LATEX source code of this book package is available at:


https://github.com/fmarotta/kaobook

Publisher
First printed June 24, 2022 by Meyer Design

ISBN: 979-8833375693
Dedicated to those who labor in the fields of legacy
code, and to my wife Lin, whose patience with this
project was essential to its completion.
Contents
Contents v

Preface 1

1 Frame of Reference 3
1.1 Your Current Practice . . . . . . . . . . . . . . . . . . . . . . . . 3
1.2 More Thoughtful Coding . . . . . . . . . . . . . . . . . . . . . . 3

2 Introducing the Problem 7


2.1 Purpose and Perspective . . . . . . . . . . . . . . . . . . . . . . 9
2.2 Areas of Difficulty . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.2.1 Evolution, not Revolution . . . . . . . . . . . . . . . . . . 10
2.3 We May Be the Enemy . . . . . . . . . . . . . . . . . . . . . . . 12
2.3.1 Too Many Components? . . . . . . . . . . . . . . . . . . 12
2.3.2 Abusing the Search Path . . . . . . . . . . . . . . . . . . 13
2.3.3 Coding Too Soon? . . . . . . . . . . . . . . . . . . . . . . 14
2.3.4 Inheritance . . . . . . . . . . . . . . . . . . . . . . . . . . 15
2.3.5 Excessive Coupling . . . . . . . . . . . . . . . . . . . . . 17
2.3.6 Unit Dependency Cycles . . . . . . . . . . . . . . . . . . 17
2.4 The IDE, Again . . . . . . . . . . . . . . . . . . . . . . . . . . . 19

3 Strategies 21
3.1 Philosophy and Reality . . . . . . . . . . . . . . . . . . . . . . . 21
3.2 Setting Goals . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
3.2.1 Patience . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
3.3 Select Your Components . . . . . . . . . . . . . . . . . . . . . . 23
3.4 Pre-convert, if Necessary . . . . . . . . . . . . . . . . . . . . . . 23
3.5 Isolate Your Code . . . . . . . . . . . . . . . . . . . . . . . . . . 24
3.6 Achieving a Build . . . . . . . . . . . . . . . . . . . . . . . . . . 26
3.7 Module Inclusion . . . . . . . . . . . . . . . . . . . . . . . . . . 28
3.8 Separating Projects - How and Why . . . . . . . . . . . . . . . . 28
3.8.1 Multiple Projects in One Delphi Version . . . . . . . . . . 29
3.8.2 Multiple Projects in Multiple Delphi Versions . . . . . . . 30
3.9 Start Small . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
3.10 Incremental Change . . . . . . . . . . . . . . . . . . . . . . . . . 32
3.11 Strip Deadwood . . . . . . . . . . . . . . . . . . . . . . . . . . . 32

4 Beginning 35
4.1 The IDE . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
4.2 The Build Process . . . . . . . . . . . . . . . . . . . . . . . . . . 36
4.2.1 Build Management . . . . . . . . . . . . . . . . . . . . . 36
4.2.2 Accept Compiler Advice . . . . . . . . . . . . . . . . . . 36
4.3 Data and Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
4.3.1 Scope of Data . . . . . . . . . . . . . . . . . . . . . . . . 41
4.3.2 Writeable “Constants” . . . . . . . . . . . . . . . . . . . 43
4.4 Scope in General . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
4.4.1 Forms vs. Units . . . . . . . . . . . . . . . . . . . . . . . 46
4.4.2 Proper to Forms . . . . . . . . . . . . . . . . . . . . . . . 46
4.4.3 Proper to Class Units . . . . . . . . . . . . . . . . . . . . 47
4.4.4 Proper to Data Modules . . . . . . . . . . . . . . . . . . 48
4.4.5 Everything Private! . . . . . . . . . . . . . . . . . . . . . 48
4.4.6 Global not Always Bad . . . . . . . . . . . . . . . . . . . 49
4.4.7 Partitioning . . . . . . . . . . . . . . . . . . . . . . . . . 49
4.5 Exception Handling . . . . . . . . . . . . . . . . . . . . . . . . . 51
4.5.1 A Simple Example . . . . . . . . . . . . . . . . . . . . . . 51
4.5.2 Exceptional Failures . . . . . . . . . . . . . . . . . . . . . 52
4.5.3 Exceptional Confusion . . . . . . . . . . . . . . . . . . . 55
4.5.4 Scope of Exception Handlers . . . . . . . . . . . . . . . . 57
4.5.5 Default Exception Handlers . . . . . . . . . . . . . . . . 58
4.6 Code Formatting . . . . . . . . . . . . . . . . . . . . . . . . . . 58
4.6.1 Formatting Matters . . . . . . . . . . . . . . . . . . . . . 60
4.6.2 Manageable Formatting . . . . . . . . . . . . . . . . . . . 63
4.6.3 Minor Formatting . . . . . . . . . . . . . . . . . . . . . . 64
Digging Into the Challenges 65
5 Comments 67
5.1 Useless Comments . . . . . . . . . . . . . . . . . . . . . . . . . 67
5.2 Annoying Comments . . . . . . . . . . . . . . . . . . . . . . . . 67
5.3 Thoughtless Comments . . . . . . . . . . . . . . . . . . . . . . . 68
5.4 Essential Comments . . . . . . . . . . . . . . . . . . . . . . . . . 69

6 Cleaning Uses Clauses 71


6.1 Initialization . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
6.1.1 Independent Factors . . . . . . . . . . . . . . . . . . . . . 73
6.1.2 Initialization of Units . . . . . . . . . . . . . . . . . . . . 73
6.2 Removing Dead and Duplicate Units . . . . . . . . . . . . . . . 80
6.2.1 ProjectFiles Tool . . . . . . . . . . . . . . . . . . . . . . . 80
6.2.2 Other Useful Tools . . . . . . . . . . . . . . . . . . . . . 83
6.3 Removing and Demoting Unit References . . . . . . . . . . . . 84
6.4 Using the Tools . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
6.5 Unit Dependency Cycles . . . . . . . . . . . . . . . . . . . . . . 87
6.6 Collecting the UDCs . . . . . . . . . . . . . . . . . . . . . . . . 87

7 Simple Things 89
7.1 Structural Changes in Delphi . . . . . . . . . . . . . . . . . . . . 89
7.2 Hints and Warnings . . . . . . . . . . . . . . . . . . . . . . . . . 90
7.3 Types and Constants . . . . . . . . . . . . . . . . . . . . . . . . 91
7.3.1 No Magic Numbers . . . . . . . . . . . . . . . . . . . . . 91
7.3.2 Unicode? . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
7.3.3 Numeric Type Aliases . . . . . . . . . . . . . . . . . . . . 93
7.3.4 Global can be OK . . . . . . . . . . . . . . . . . . . . . . 94
7.3.5 Assignment Compatibility . . . . . . . . . . . . . . . . . 95
7.3.6 Use Enumerations . . . . . . . . . . . . . . . . . . . . . . 96
7.3.7 Use Sets . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
7.3.8 Use Defined Ranges . . . . . . . . . . . . . . . . . . . . . 99
7.4 Variables and Scope . . . . . . . . . . . . . . . . . . . . . . . . . 100
7.4.1 Avoid Global Variables . . . . . . . . . . . . . . . . . . . 101
7.4.2 Using AOwner . . . . . . . . . . . . . . . . . . . . . . . . 101
7.5 Forms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
7.5.1 Form Variables . . . . . . . . . . . . . . . . . . . . . . . . 104
7.5.2 Form File Format . . . . . . . . . . . . . . . . . . . . . . 104
7.6 Delphi Versions . . . . . . . . . . . . . . . . . . . . . . . . . . . 105

8 Not so Simple Things 111


8.1 Persistence in Settings . . . . . . . . . . . . . . . . . . . . . . . . 111
8.1.1 Persistence with Enumerations . . . . . . . . . . . . . . . 111
8.2 Dangers in Use of ClassName . . . . . . . . . . . . . . . . . . . 115
8.2.1 Decoupling with a Dictionary . . . . . . . . . . . . . . . 116
8.3 Reviewing Code . . . . . . . . . . . . . . . . . . . . . . . . . . . 123

9 Cleaning Legacy Code 125


9.1 Local Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
9.1.1 I/O Checking . . . . . . . . . . . . . . . . . . . . . . . . 126
9.1.2 Overflow Checking . . . . . . . . . . . . . . . . . . . . . 127
9.1.3 Range Checking . . . . . . . . . . . . . . . . . . . . . . . 129
9.1.4 Rename Local Variables . . . . . . . . . . . . . . . . . . . 129
9.1.5 Remove Local Variables . . . . . . . . . . . . . . . . . . . 131
9.2 Remove Wrappers . . . . . . . . . . . . . . . . . . . . . . . . . . 133
9.3 Coding Style . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133
9.4 Form State . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
9.4.1 Form State Components . . . . . . . . . . . . . . . . . . 136
9.5 Form vs. Code Dependency . . . . . . . . . . . . . . . . . . . . 137
9.6 Types & Consts Again . . . . . . . . . . . . . . . . . . . . . . . . 140
9.6.1 Doppelgangers . . . . . . . . . . . . . . . . . . . . . . . . 142
9.7 Misplaced Routines . . . . . . . . . . . . . . . . . . . . . . . . . 142

10 Local Components 145


10.1 Component Basics . . . . . . . . . . . . . . . . . . . . . . . . . . 145
10.2 Managing Components . . . . . . . . . . . . . . . . . . . . . . . 146
10.3 Component Pitfalls . . . . . . . . . . . . . . . . . . . . . . . . . 147
10.3.1 Components Doing Too Much . . . . . . . . . . . . . . . 147
10.3.2 Bad Assumptions . . . . . . . . . . . . . . . . . . . . . . 148
10.3.3 Failing to Handle Exceptions . . . . . . . . . . . . . . . . 148
10.3.4 Mixing Component and Application Code . . . . . . . . 149
10.4 Cleaning House . . . . . . . . . . . . . . . . . . . . . . . . . . . 150
10.4.1 Use Library Modules . . . . . . . . . . . . . . . . . . . . 152
10.4.2 Keep the interface uses Clause Small . . . . . . . . . . . . 152
10.4.3 Refactor Utility Units . . . . . . . . . . . . . . . . . . . . 153

11 Refactoring 155
11.1 A Workable Approach to Small Change . . . . . . . . . . . . . . 157
11.2 The Value of a Data Module . . . . . . . . . . . . . . . . . . . . 158
11.3 Testing in a Data Module . . . . . . . . . . . . . . . . . . . . . . 159
11.3.1 Information Hiding . . . . . . . . . . . . . . . . . . . . . 159
11.3.2 Minimizing Points of Contact . . . . . . . . . . . . . . . 160
11.4 The Challenge of Testing . . . . . . . . . . . . . . . . . . . . . . 161
11.4.1 Massive Routines . . . . . . . . . . . . . . . . . . . . . . 162
11.4.2 Massive Coupling . . . . . . . . . . . . . . . . . . . . . . 162
11.4.3 Risk is Unavoidable . . . . . . . . . . . . . . . . . . . . . 163
11.5 Code for the Maintainer . . . . . . . . . . . . . . . . . . . . . . 164
11.5.1 Resources . . . . . . . . . . . . . . . . . . . . . . . . . . . 165
11.6 Prefer Library Code . . . . . . . . . . . . . . . . . . . . . . . . . 165
11.7 Use Nested Routines . . . . . . . . . . . . . . . . . . . . . . . . 166
11.8 Extract Classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167
11.9 Prefer Composition . . . . . . . . . . . . . . . . . . . . . . . . . 167
11.9.1 The Public Interface . . . . . . . . . . . . . . . . . . . . . 168
11.9.2 The Private Behaviors . . . . . . . . . . . . . . . . . . . . 172
11.9.3 Notes on Implementation . . . . . . . . . . . . . . . . . . 175
12 Removing Code from Forms 177
12.1 Extract Utility Routines . . . . . . . . . . . . . . . . . . . . . . . 177
12.1.1 Refactoring Praxis . . . . . . . . . . . . . . . . . . . . . . 178
12.2 Building Utility Units . . . . . . . . . . . . . . . . . . . . . . . . 179
12.3 Shared Classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180
12.4 Use Data Modules . . . . . . . . . . . . . . . . . . . . . . . . . . 181
12.4.1 Common Operations . . . . . . . . . . . . . . . . . . . . 181
12.5 Separate Business Logic . . . . . . . . . . . . . . . . . . . . . . 183
12.5.1 What is Business Logic? . . . . . . . . . . . . . . . . . . . 183
12.5.2 Organization . . . . . . . . . . . . . . . . . . . . . . . . . 185
12.5.3 Separate Data Handling . . . . . . . . . . . . . . . . . . 186

13 Fixing Erroneous Coding 187


13.1 Errors of Function . . . . . . . . . . . . . . . . . . . . . . . . . . 187
13.2 Reduce Noise . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188
13.3 Reduce Logic . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188
13.3.1 Using Partial Products . . . . . . . . . . . . . . . . . . . 189
13.3.2 Prefer Positive Logic . . . . . . . . . . . . . . . . . . . . . 189
13.4 Loop Forms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 190
13.5 Minimize Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . 193
13.6 Use Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 196
13.6.1 Static Arrays . . . . . . . . . . . . . . . . . . . . . . . . . 196
13.6.2 Dynamic Arrays . . . . . . . . . . . . . . . . . . . . . . . 197
13.6.3 Open Arrays . . . . . . . . . . . . . . . . . . . . . . . . . 197
13.6.4 Generic Arrays . . . . . . . . . . . . . . . . . . . . . . . . 198
13.6.5 Constant Arrays . . . . . . . . . . . . . . . . . . . . . . . 199
13.6.6 Arrays vs. Cases . . . . . . . . . . . . . . . . . . . . . . . 199
13.7 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 200

14 Class and Record Helpers 201


14.1 Alternative Approaches . . . . . . . . . . . . . . . . . . . . . . 204
14.2 TStringHelper . . . . . . . . . . . . . . . . . . . . . . . . . . . . 204
14.3 Legacy Cleanup . . . . . . . . . . . . . . . . . . . . . . . . . . . 208
14.4 More Insights . . . . . . . . . . . . . . . . . . . . . . . . . . . . 209

15 Using Datasets Well 211


15.1 TField Properties . . . . . . . . . . . . . . . . . . . . . . . . . . 211
15.2 Component Differences . . . . . . . . . . . . . . . . . . . . . . . 212
15.3 Normalize Data . . . . . . . . . . . . . . . . . . . . . . . . . . . 212
15.4 Less Specificity . . . . . . . . . . . . . . . . . . . . . . . . . . . 213
15.5 Think Before Coding . . . . . . . . . . . . . . . . . . . . . . . . 214

16 Pragmatism 215
16.1 Assessing the Costs . . . . . . . . . . . . . . . . . . . . . . . . . 216
16.2 Measuring Value . . . . . . . . . . . . . . . . . . . . . . . . . . 216
16.2.1 Use EurekaLog . . . . . . . . . . . . . . . . . . . . . . . . 217
16.2.2 Use Analytics . . . . . . . . . . . . . . . . . . . . . . . . 217
16.3 Missing Source Code . . . . . . . . . . . . . . . . . . . . . . . . 218
16.4 Analytical Modules . . . . . . . . . . . . . . . . . . . . . . . . . 219
16.5 Little-Used Modules . . . . . . . . . . . . . . . . . . . . . . . . 219

17 Interfaces 221
17.1 History . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 221
17.2 Reducing Scope . . . . . . . . . . . . . . . . . . . . . . . . . . . 222
17.2.1 Constants and Types . . . . . . . . . . . . . . . . . . . . 222
17.2.2 Properties . . . . . . . . . . . . . . . . . . . . . . . . . . 223
17.3 Managing Memory . . . . . . . . . . . . . . . . . . . . . . . . . 227
17.4 Examples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 227
17.5 Going Deeper . . . . . . . . . . . . . . . . . . . . . . . . . . . . 230
17.5.1 Variations . . . . . . . . . . . . . . . . . . . . . . . . . . 235

18 Testability 237
18.1 Testing Legacy Code . . . . . . . . . . . . . . . . . . . . . . . . 237
18.2 Layers of Difficulty . . . . . . . . . . . . . . . . . . . . . . . . . 237
18.3 Design Specifications . . . . . . . . . . . . . . . . . . . . . . . . 238
18.4 Interdependency Issues . . . . . . . . . . . . . . . . . . . . . . . 239
18.5 Coupling, Partitioning . . . . . . . . . . . . . . . . . . . . . . . 239
18.6 Code on Forms . . . . . . . . . . . . . . . . . . . . . . . . . . . 239
18.7 Testability Defined . . . . . . . . . . . . . . . . . . . . . . . . . 240
18.8 Ensure Testability . . . . . . . . . . . . . . . . . . . . . . . . . . 241
18.9 Unit Test Frameworks . . . . . . . . . . . . . . . . . . . . . . . . 241
18.10 Testing Practices . . . . . . . . . . . . . . . . . . . . . . . . . . . 242

19 Performance 245
19.1 Realities . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 245
19.1.1 Profiling . . . . . . . . . . . . . . . . . . . . . . . . . . . 245
19.1.2 Profiling Legacy Code . . . . . . . . . . . . . . . . . . . 246
19.1.3 Approaching Profiling . . . . . . . . . . . . . . . . . . . 247
19.2 Painful Realities . . . . . . . . . . . . . . . . . . . . . . . . . . . 248
19.3 You Need Tools . . . . . . . . . . . . . . . . . . . . . . . . . . . 248

20 Disruptive Forces 249


20.1 Code Details . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 249
20.1.1 Hints and Warnings . . . . . . . . . . . . . . . . . . . . . 249
20.1.2 Static Analysis . . . . . . . . . . . . . . . . . . . . . . . . 250
20.2 Component Issues . . . . . . . . . . . . . . . . . . . . . . . . . . 251
20.2.1 Orphaned Components . . . . . . . . . . . . . . . . . . . 252
20.2.2 Local Components . . . . . . . . . . . . . . . . . . . . . . 252
20.3 Dependency Cycles . . . . . . . . . . . . . . . . . . . . . . . . . 255
20.3.1 The Gordian Knot . . . . . . . . . . . . . . . . . . . . . . 256
20.3.2 Small Steps . . . . . . . . . . . . . . . . . . . . . . . . . . 256
20.3.3 Larger Steps . . . . . . . . . . . . . . . . . . . . . . . . . 257
20.3.4 Cycles in Components . . . . . . . . . . . . . . . . . . . 258
20.4 Compiler Versions . . . . . . . . . . . . . . . . . . . . . . . . . . 259
20.5 Issues You must Find . . . . . . . . . . . . . . . . . . . . . . . . 260
Best Practices 263
21 Some Simple Principles 265
21.1 Principle of Least Astonishment . . . . . . . . . . . . . . . . . . . 265
21.2 DRY: Don’t Repeat Yourself . . . . . . . . . . . . . . . . . . . . . 266
21.2.1 The Rule of Three . . . . . . . . . . . . . . . . . . . . . . 266
21.2.2 Causes of Repetition . . . . . . . . . . . . . . . . . . . . 267
21.2.3 Using the Libraries . . . . . . . . . . . . . . . . . . . . . 267
21.2.4 String Utilities . . . . . . . . . . . . . . . . . . . . . . . . 268
21.2.5 Date and Time Utilities . . . . . . . . . . . . . . . . . . . 268
21.3 YAGNI: You Ain’t Gonna Need It . . . . . . . . . . . . . . . . . 269
21.4 SOC: Separation of Concerns . . . . . . . . . . . . . . . . . . . . . 270

22 SOLID 273
22.1 What is SOLID? . . . . . . . . . . . . . . . . . . . . . . . . . . . 273
22.1.1 Single Responsibility . . . . . . . . . . . . . . . . . . . . 274
22.1.2 Open/Closed . . . . . . . . . . . . . . . . . . . . . . . . 275
22.1.3 Liskov Substitution . . . . . . . . . . . . . . . . . . . . . 276
22.1.4 Interface Segregation Principle . . . . . . . . . . . . . . . . 277
22.1.5 Dependency Inversion . . . . . . . . . . . . . . . . . . . 278

23 Inheritance, Aggregation, Composition 285


23.1 Inheritance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 285
23.2 Composition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 286
23.3 Aggregation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 288

24 Design Patterns 289


24.1 Anti-patterns . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 289
24.2 Delphi Idioms . . . . . . . . . . . . . . . . . . . . . . . . . . . . 290
24.2.1 Create/Destroy . . . . . . . . . . . . . . . . . . . . . . . 290
24.2.2 If/Else . . . . . . . . . . . . . . . . . . . . . . . . . . . . 293
24.2.3 Helpers . . . . . . . . . . . . . . . . . . . . . . . . . . . . 294
24.2.4 Smart Pointers . . . . . . . . . . . . . . . . . . . . . . . . 295
24.3 Recommended Practices . . . . . . . . . . . . . . . . . . . . . . 298
24.4 Patterns of Interest . . . . . . . . . . . . . . . . . . . . . . . . . 298
24.4.1 Adapter Pattern . . . . . . . . . . . . . . . . . . . . . . . 299
24.4.2 Facade Pattern . . . . . . . . . . . . . . . . . . . . . . . . 299
24.4.3 Dependency Injection . . . . . . . . . . . . . . . . . . . . . 300
24.5 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 301

25 Dependency Injection 303


25.1 Starting Small . . . . . . . . . . . . . . . . . . . . . . . . . . . . 303
25.2 Types of Injection . . . . . . . . . . . . . . . . . . . . . . . . . . 306
25.2.1 Constructor Injection . . . . . . . . . . . . . . . . . . . . 307
25.2.2 Property Injection . . . . . . . . . . . . . . . . . . . . . . 307
25.2.3 Method Injection . . . . . . . . . . . . . . . . . . . . . . 308
25.3 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 310

26 Unit Testing 313


26.1 DUnit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 315
26.2 DUnit2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 315
26.3 DUnitX . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 316
26.4 TestInsight . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 317
26.5 Delphi Mocks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 317

Appendix: Tools 319


27 Tools Overview 321
27.1 IDE Plug-ins . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 321
27.2 Standalone . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 322
27.3 Some Disclaimers . . . . . . . . . . . . . . . . . . . . . . . . . . 322

28 CnPack 325
28.1 Structural Highlighting . . . . . . . . . . . . . . . . . . . . . . . 326
28.2 Tab Order . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 327
28.3 Uses Cleaner . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 328
28.4 CnPack Summary . . . . . . . . . . . . . . . . . . . . . . . . . . 330

29 CodeSite 331
29.1 Details in Your Hands . . . . . . . . . . . . . . . . . . . . . . . . 331
29.2 Data from the Field . . . . . . . . . . . . . . . . . . . . . . . . . 333
29.3 Simple Example . . . . . . . . . . . . . . . . . . . . . . . . . . . 333

30 Delphi Mocks 335


30.1 Delphi Mocks in Use . . . . . . . . . . . . . . . . . . . . . . . . . 336
30.2 Why Mock? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 338
30.3 When to Mock . . . . . . . . . . . . . . . . . . . . . . . . . . . . 339
30.4 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 340

31 Documentation Insight 341

32 Delphi Unit Dependency Scanner 343

33 DUNIT 347
33.1 DUnit in Recent IDEs . . . . . . . . . . . . . . . . . . . . . . . . 348
33.2 Developing Test Cases . . . . . . . . . . . . . . . . . . . . . . . 352
33.3 Start with the Familiar . . . . . . . . . . . . . . . . . . . . . . . 355
33.4 Coverage in Legacy Projects . . . . . . . . . . . . . . . . . . . . 356
33.5 Test Driven Development . . . . . . . . . . . . . . . . . . . . . . 357
33.6 Unit Test Lifetime . . . . . . . . . . . . . . . . . . . . . . . . . . 358
33.7 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 359

34 DUnit2 361

35 DUnitX 365
35.1 Looking at Some Code . . . . . . . . . . . . . . . . . . . . . . . 367
35.1.1 DUnit vs. DUnitX Features . . . . . . . . . . . . . . . . . 375
35.2 DUnit vs. DUnitX . . . . . . . . . . . . . . . . . . . . . . . . . . 375
35.3 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 377
36 FixInsight 379
36.1 Real World Data . . . . . . . . . . . . . . . . . . . . . . . . . . . 381

37 GExperts 383
37.1 Configuration . . . . . . . . . . . . . . . . . . . . . . . . . . . . 384
37.2 Editor Experts . . . . . . . . . . . . . . . . . . . . . . . . . . . . 386
37.3 Replace Components . . . . . . . . . . . . . . . . . . . . . . . . 387
37.4 GREP Search . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 388
37.5 GExperts Summary . . . . . . . . . . . . . . . . . . . . . . . . . 389

38 Homebrew Tools 391


38.1 Cycles Analyzer . . . . . . . . . . . . . . . . . . . . . . . . . . . 391
38.1.1 Cycles Analyzer Code . . . . . . . . . . . . . . . . . . . . 395
38.1.2 Dependency Cycles in Components . . . . . . . . . . . . 405
38.1.3 Unit Dependencies: A Closer View . . . . . . . . . . . . 406
38.2 Map Explorer . . . . . . . . . . . . . . . . . . . . . . . . . . . . 407
38.3 Component Collector . . . . . . . . . . . . . . . . . . . . . . . . 409
38.3.1 Collecting Installed Components . . . . . . . . . . . . . 411
38.3.2 Collecting the DFM Files List . . . . . . . . . . . . . . . . 415
38.3.3 Collect Component Instances . . . . . . . . . . . . . . . 416
38.3.4 Producing the Aggregate Component Data . . . . . . . . 421
38.3.5 Putting Together the Pieces . . . . . . . . . . . . . . . . . 423
38.4 Separating Wheat and Chaff . . . . . . . . . . . . . . . . . . . . 423

39 MapFileStats 425

40 MMX 427
40.1 Add Class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 429
40.2 Add Field . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 429
40.3 Add Method . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 430
40.4 Sort Class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 431
40.5 Synchronize Signatures . . . . . . . . . . . . . . . . . . . . . . . 432
40.6 Swap Scopes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 432
40.7 Unit Dependency Analyzer . . . . . . . . . . . . . . . . . . . . . 433
40.8 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 434

41 Peganza Products 435


41.1 Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 435
41.2 Pascal Analyzer . . . . . . . . . . . . . . . . . . . . . . . . . . . 436
41.3 Pascal Expert . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 438
41.4 Pascal Browser . . . . . . . . . . . . . . . . . . . . . . . . . . . . 439

42 ProDelphi 443
42.1 Profiling Some Demo Code . . . . . . . . . . . . . . . . . . . . . 443
42.1.1 Considering ExportDataSetToCSV . . . . . . . . . . . . . . 446
42.2 Profiling, in General . . . . . . . . . . . . . . . . . . . . . . . . . 447
42.2.1 Why Profile, Why Optimize? . . . . . . . . . . . . . . . . 447

43 TestInsight 449

Bibliography 455

Alphabetical Index 457


List of Figures

4.1 Enabling the separate build process . . . . . . . . . . . . . . . . . . 36


4.2 Graph of units in a small project . . . . . . . . . . . . . . . . . . . . 50
4.3 Graph subsection in legacy code . . . . . . . . . . . . . . . . . . . 50
4.4 Graph of legacy project . . . . . . . . . . . . . . . . . . . . . . . . . 51

6.1 ProjectFiles Edit Boxes . . . . . . . . . . . . . . . . . . . . . . . . . 81


6.2 ProjectFiles Edit Boxes - Order . . . . . . . . . . . . . . . . . . . . . 81
6.3 ProjectFiles Member Files . . . . . . . . . . . . . . . . . . . . . . . 81
6.4 ProjectFiles Mapped Files . . . . . . . . . . . . . . . . . . . . . . . 82
6.5 ProjectFiles DCU Files . . . . . . . . . . . . . . . . . . . . . . . . . 82
6.6 ProjectFiles Dead Files . . . . . . . . . . . . . . . . . . . . . . . . . 82
6.7 ProjectFiles Duplicate Files . . . . . . . . . . . . . . . . . . . . . . . 82
6.8 ProjectFiles Dead Files Removal . . . . . . . . . . . . . . . . . . . . 83
6.9 ProjectFiles Duplicate Files Removal . . . . . . . . . . . . . . . . . 83
6.10 MMX Unit Dependencies project dialog . . . . . . . . . . . . . . . 87

8.1 Testing the ClassDict . . . . . . . . . . . . . . . . . . . . . . . . . . 122

28.1 CnPack Main menu . . . . . . . . . . . . . . . . . . . . . . . . . . . 326


28.2 CnPack Structural Highlighting . . . . . . . . . . . . . . . . . . . . 327
28.3 CnPack Tab Order . . . . . . . . . . . . . . . . . . . . . . . . . . . . 327
28.4 CnPack Uses Cleaner . . . . . . . . . . . . . . . . . . . . . . . . . . 329

29.1 CodeSite Properties . . . . . . . . . . . . . . . . . . . . . . . . . . . 331


29.2 CodeSite DataSets . . . . . . . . . . . . . . . . . . . . . . . . . . . . 332
29.3 CodeSite XML Data . . . . . . . . . . . . . . . . . . . . . . . . . . . 332
29.4 CodeSite Messages . . . . . . . . . . . . . . . . . . . . . . . . . . . . 334

31.1 DocumentationInsight Explorer: IDE . . . . . . . . . . . . . . . . . . 341


31.2 DocumentationInsight: Inspector . . . . . . . . . . . . . . . . . . . . 341
31.3 DocumentationInsight: Sample XML . . . . . . . . . . . . . . . . . . 342

32.1 Delphi Unit Dependency Scanner . . . . . . . . . . . . . . . . . . . . 343

33.1 DUnit GUI Runner . . . . . . . . . . . . . . . . . . . . . . . . . . . 347


33.2 DUnit Delphi Wizard . . . . . . . . . . . . . . . . . . . . . . . . . . 348
33.3 DUnit Delphi Test Project Wizard . . . . . . . . . . . . . . . . . . . 349
33.4 DUnit Delphi Test Project Wizard . . . . . . . . . . . . . . . . . . . 349
33.5 DUnit Delphi Test Project Wizard . . . . . . . . . . . . . . . . . . . 350
33.6 DUnit Delphi Test Project Wizard . . . . . . . . . . . . . . . . . . . 351
33.7 DUnit Delphi Test Project Wizard . . . . . . . . . . . . . . . . . . . 355

34.1 DUnit2 GUI Runner . . . . . . . . . . . . . . . . . . . . . . . . . . . 362


34.2 DUnit2 Excluding Tests . . . . . . . . . . . . . . . . . . . . . . . . . 363

37.1 GExperts Main menu . . . . . . . . . . . . . . . . . . . . . . . . . . 383


37.2 GExperts Configuration Dialog page 1 . . . . . . . . . . . . . . . . . 384
37.3 GExperts Configuration Dialog General . . . . . . . . . . . . . . . . 385
37.4 GExperts Configuration Dialog Editor Experts . . . . . . . . . . . . 385
37.5 GExperts Configuration Dialog Experts List . . . . . . . . . . . . . 386
37.6 GExperts Editor Experts . . . . . . . . . . . . . . . . . . . . . . . . 387
37.7 GExperts– Replace Components wizard . . . . . . . . . . . . . . . 388
37.8 GExperts– Replace Components wizard . . . . . . . . . . . . . . . 388
37.9 GExperts– Replace Components wizard . . . . . . . . . . . . . . . 389

38.1 Cycles Analyzer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 392


38.2 Cycles Analyzer - Controls . . . . . . . . . . . . . . . . . . . . . . . 393
38.3 Cycles Analyzer - New Files . . . . . . . . . . . . . . . . . . . . . . 394
38.4 Cycles Report Analyzer . . . . . . . . . . . . . . . . . . . . . . . . 406
38.5 Cycles Report Analyzer - Top band . . . . . . . . . . . . . . . . . . 407
38.6 Cycles Report Analyzer - Search . . . . . . . . . . . . . . . . . . . . 407
38.7 Cycles Report Analyzer - Data . . . . . . . . . . . . . . . . . . . . . 407
38.8 Map File - Segments . . . . . . . . . . . . . . . . . . . . . . . . . . 408
38.9 Map File - Publics . . . . . . . . . . . . . . . . . . . . . . . . . . . . 408
38.10 Map Explorer - Modules . . . . . . . . . . . . . . . . . . . . . . . . 409
38.11 Map Explorer - Publics . . . . . . . . . . . . . . . . . . . . . . . . . 409

39.1 MapFileStats Tree . . . . . . . . . . . . . . . . . . . . . . . . . . . . 425


39.2 MapFileStats StrUtils . . . . . . . . . . . . . . . . . . . . . . . . . . 425
39.3 MapFileStats fMain . . . . . . . . . . . . . . . . . . . . . . . . . . . 426

40.1 MMX Main Menu . . . . . . . . . . . . . . . . . . . . . . . . . . . . 427


40.2 MMX Code Explorer . . . . . . . . . . . . . . . . . . . . . . . . . . 428
40.3 MMX Add Class wizard . . . . . . . . . . . . . . . . . . . . . . . . 429
40.4 MMX Add Field Wizard . . . . . . . . . . . . . . . . . . . . . . . . 430
40.5 MMX Add Method Wizard . . . . . . . . . . . . . . . . . . . . . . 431
40.6 MMX Sorting Options . . . . . . . . . . . . . . . . . . . . . . . . . 432
40.7 MMX Unit Dependency Analyzer . . . . . . . . . . . . . . . . . . . 433

41.1 Pascal Analyzer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 436


41.2 Pascal Expert Options, General . . . . . . . . . . . . . . . . . . . . . 439
41.3 Pascal Expert Options, Alerts . . . . . . . . . . . . . . . . . . . . . . 439

42.1 ProDelphi Viewer . . . . . . . . . . . . . . . . . . . . . . . . . . . . 443


42.2 ProDelphi ZipCodes . . . . . . . . . . . . . . . . . . . . . . . . . . . 444

43.1 TestInsight Explorer . . . . . . . . . . . . . . . . . . . . . . . . . . . 450


43.2 TestInsight Explorer . . . . . . . . . . . . . . . . . . . . . . . . . . . 453
43.3 TestInsight Explorer . . . . . . . . . . . . . . . . . . . . . . . . . . . 453
List of Tables

14.1 TStringHelper Methods . . . . . . . . . . . . . . . . . . . . . . . . . 205

22.1 Legacy Code vs. Single Responsibility. . . . . . . . . . . . . . . . . . 274

35.1 DUnit vs. DUnitX . . . . . . . . . . . . . . . . . . . . . . . . . . . . 376


Preface

Although I have always enjoyed writing, I did not seriously begin


working on a book until I realized one day how many years I have
spent working on legacy projects. Moreover, despite the great
differences in the applications, the array of problems I observed
was very consistent, from one project to another.
When you are in the middle of such a project, it is hard to imagine
that you can ever overcome the legacy issues, yet it is probably
the single greatest desire you have in your work. Keeping up with
maintenance is a challenge, and there may be times you doubt
you will ever have time for anything beyond defect repair. But
without making time for some serious rework, the complexity of
the code—and of the maintenance—will only increase.
Technical debt is a term we hear often. But as with credit card debt,
paying it off is not fun. You must sacrifice, and commit hours and
hours with no outwardly apparent progress. No new features, no
shiny new forms. Management buy-in is problematic.
As I began work on this book, I wanted to share approaches which
I have found useful, and to suggest that however large the project,
there are strategies you can use to achieve gradual improvement. It
is even practical to interleave this work with normal maintenance,
though that will increase the routine load on QA.
There are tools you can use to aid the process. But you may find
the need to write some of your own, as I did, since there are
few available which will facilitate action at the project level, or
from outside the project. In this book, I present some coverage
of commercial tools, as well as some tools I built for myself. The
code for examples in the book, and for these custom built tools is
available online at:
https://github.com/wmeyer48/DL-code
I hope that you will find this a useful volume, and wish you
success in surviving your legacy projects!
Acknowledgments I am deeply thankful to those who assisted
me in this effort:
2 Preface

Eric Schreiber, a colleague who proof-read the earliest drafts of


the first several chapters, and contributed thoughtful and useful
suggestions.
Dalija Prasnikar, an Embarcadero MVP who repeatedly read
and commented on all the content, and in particular, offered
suggestions on sample code included in the text.
Neven Prasnikar, Sr., whose revision to the book cover made it
striking, rather than the mundane form I had created.
All errors, of course, are mine alone.

William Meyer
June 24, 2022
Frame of Reference 1
Before beginning to present my approach to legacy projects, I 1.1 Your Current Practice . . 3
think it may be helpful to consider the frame of reference which 1.2 More Thoughtful Coding 3
is needed for success in such projects. Delphi is the toolset, but
you may find in legacy projects evidence of coding which began
in Turbo Pascal. No doubt you have picked up this book in the
hope of finding a better way of attacking the problems you face.
Hopefully, much of what I offer here will be useful.

1.1 Your Current Practice

Since we are discussing legacy projects, it is safe to assume that


you have a customer base which expects defects to be repaired, and
updates to be issued in a timely fashion. Those activities are time
consuming, and often seem to be a barrier to the very real progress
which you must make to ensure the product has a future.
In code maintenance you will be focused on defect repair. From
time to time, you will also have to add new features. These activities
not only rob time from the rework you know is needed, but may
exacerbate the existing problems, as you continue to extend the
existing code. Often, developers become impatient to make the
repairs as quickly as possible, and move on to code rework.
The mindset you have developed during years of business as usual
is in need of change. Maintenance and the adding of features are
essential to the revenue stream without which there is no point to
the work at all.

1.2 More Thoughtful Coding

The single most important principle I would drive home is that You will see that I use these side
you must always leave a code module better than you found it. notes to emphasize, often through
repetition. Might as well start early!
You will need to settle for incremental change; evolution, not
revolution. Every little improvement makes a difference, and over
time, those improvements accumulate to reshape your code. In
4 1 Frame of Reference

this volume, I will present strategies for such change, and propose
specific ways to alter things. But none of that will be of much use
unless you are able to adapt your perspective.

▶ Think patiently about each action before coding.


▶ Be willing to rewrite, again and again.
▶ Be ever watchful for tangled code, and work to reduce the
tangles.
▶ Always consider whether a routine or variable might be
better named than it is.
▶ Strive for short routines. Make use of nested routines along
the way.
▶ Be willing to test routines in separate projects; you probably
cannot unit test your application, so isolation is a reasonable
tool in the near term.

You will need patience. Be thoughtful, be philosophical, perform


rework with less hurry than may affect your defect repair work.
Accept that progress will be slow. The complexity you are attempt-
ing to reduce was not created overnight, neither will you simplify
it all overnight.

Victories
Savor the victories, however small they may be. This is a
marathon, not a sprint.

And don’t think patience is easy. I recently had occasion to


write:

Developing vs. Coding

To be a developer means working at:


▶ good naming
▶ thoughtful coding
▶ meaningful comments
▶ written docs, as needed
Else we’re not actually developing, just coding. Not at all the
same.

The programmer’s primary weapon in the never-


ending battle against slow system is to change the
1.2 More Thoughtful Coding 5

intramodular structure. Our first response should be


to reorganize the modules’ data structures. – Frederick
Brooks

It is worth a reminder, as we work in a Pascal-based language, that


Nicklaus Wirth authored the very important Algorithms + Data [16]: Wirth (1976), Algorithms + Data
Structures = Programs[16]. We should be planning the organization Structures = Programs

of the data we will use before we begin coding. Well organized data
not only simplifies and often reduces the code needed, but may
also help with the Separation of Concerns. (See chapter 21.4).
Introducing the Problem 2
When Delphi was introduced, the IDE was astonishing in its 2.1 Purpose and Perspec-
capabilities. Visual Basic came on the scene in 1991 and the IDE tive . . . . . . . . . . . . 9
was a rough first attempt at a new paradigm. 2.2 Areas of Difficulty . . 9
2.2.1 Evolution, not Revolu-
Delphi 1, in 1995, got it right, particularly in the implementation of
tion . . . . . . . . . . . . 10
two-way tools. Over the years, however, there have been missteps,
including buggy releases, and there have been defects in the IDE 2.3 We May Be the Enemy 12
2.3.1 Too Many Compo-
which remained for far too long. In spite of these issues, Delphi
nents? . . . . . . . . . . 12
grew stronger and more capable, with versions Delphi 3, Delphi 5,
2.3.2 Abusing the Search
and Delphi 7, each being remarkable products. Path . . . . . . . . . . . 13
2.3.3 Coding Too Soon? . . . 14
Some History 2.3.4 Inheritance . . . . . . . 15
2.3.5 Excessive Coupling . . 17
Delphi, so different from prior Pascal compilers, was a long 2.3.6 Unit Dependency
time coming, both in dreams and evolution. We owe a great Cycles . . . . . . . . . . 17
debt to Anders Hejlsberg and his team. 2.4 The IDE, Again . . . . 19
For those who came late to the
adventure, see this article:
The rumors of the death of Delphi continue to be greatly exag- https://en.wikipedia.org/wiki/
gerated. The language has remained in development, and new Anders_Hejlsberg
releases have added such features as generics, attributes, anony-
mous methods, and inline variables.
As with any software language, the quality of the result depends
on the knowledge and skill of the practitioners. We must keep in
mind that computers, operating systems, and language tools are
perpetually changing. It is easy to forget, at times, that forty years
ago, most software for small computers was written in assembly
language, and that most of what we think we understand of
programming came later. The C language was around, but early
versions running on small computers—they were not yet PCs—
were fairly painful to use. Turbo Pascal arrived in 1983, and put
the lie to then-conventional wisdom about compiled languages on
small target machines. Turbo Pascal for Windows didn’t come until
1991, but then, Windows 1.0 was 1985, and Windows 3.0 (the first
which didn’t frustrate endlessly) in 1990.
While all of this was going on, the pundits offered endless fads
about how to develop software:
8 2 Introducing the Problem

▶ Waterfall
▶ Top-down
▶ Bottom-up
▶ Spiral
▶ Structured programming
▶ Nassi-Shneiderman diagrams
▶ Warnier-Orr diagrams
▶ Unified Modeling Language (UML)
And there have been others, too numerous to count. There will
be disagreements over calling these fads, but at one time, each of
these was the definitive answer to the problem, and now it is not.
Perhaps fashions would be a less contentious description.

Waterfall Persists
To be fair, waterfall was not and is not a fad. Despite all
arguments over other approaches, there are huge projects which
are always managed in waterfall, and probably always will be.
Examples include the systems on major airplanes, especially
military models, where everything is and must be planned and
specified before work begins. And more significantly, where
the agile approach of leaving out unfinished features will never
fly. (Pun intended.)

It is common these days to hear people present their methods


as obvious and axiomatic, but software design and development
are still less science than art. And though we now take one pass
compilers for granted, David Gries, in the early seventies, described
[8]: Gries (1971), Compiler Construction a 63 pass compiler on which he had worked.[8]
for Digital Computers
Delphi has long provided support for IDE plug-ins, several of
which should be considered essential, and will be discussed in
this book. Those most likely to affect your productivity and coding
style on a daily basis include:
▶ GExperts
▶ CnPack
▶ MMX (originally ModelMaker Code Explorer)
▶ TestInsight
There are also commercial plug-ins which are very useful:
▶ DocumentationInsight
▶ FixInsight
2.1 Purpose and Perspective 9

See the Appendix beginning at Chapter 27 for more information


on these and other tools.

2.1 Purpose and Perspective

This book was inspired by many years of working with legacy


projects, and a desire to share techniques which have been useful
in that context. Some basics are covered, but in the context of
achieving better code. There are topics which are addressed more
than once, as the book proceeds into different aspects of the good
and the bad of legacy code.

What the Book is Not


This is not a Delphi language manual, nor is it a conventional
introduction to programming. There are many good books
which meet those needs. It is not an introduction to general
methodologies, except as relates very specifically to Delphi
legacy projects.

2.2 Areas of Difficulty

Delphi accomplishes a great deal of what originally set it apart by


having live components in the IDE, which is how it is possible,
for example, to see live data in a grid in design mode. But having
the live components in the IDE creates a vulnerability: a poorly
designed component can interfere with the correct behaviors of
the IDE. When this happens, developers are inclined to fault the
IDE, though it may have been the victim, not the cause.
As Delphi has matured, features have been added making the IDE
more useful, and developers more productive. Code Completion,
Code Insight, and Error Insight are examples. Most recently,
Language Server Protocol (LSP) has been added, which helps
to restore the responsiveness of the IDE, responsiveness which
had degraded at least partly in support of the features named
above.
10 2 Introducing the Problem

2.2.1 Evolution, not Revolution

Improving legacy code is a process, and a long one, at that. It can


be tempting to simply start over, but unless the project is fairly
small, that is unlikely to go well. On a large project, estimating
development time is always difficult, and when you must maintain
legacy code even as you design and write new, your workload will
be more than doubled.

Why not Rewrite? Requirements for a fresh start would in-


clude:
▶ Product design documentation
▶ Analysis of existing features
▶ Analysis of features to be dropped
▶ Specification of features to be added
▶ Architectural design
We could go on, but as you are unlikely to have those additional
items, why belabor the point?
Odds are that the existing application started small, and just grew.
Features get tacked on, rather than integrated. There is no point
in starting over if you will simply repeat the old processes.

Convoluted but Practical In any non-trivial legacy code, there


will be some very dark corners, and a widespread haziness. There
is a lack of documentation, and there are almost certainly no unit
tests. Further, some or all of the original developers may no longer
be available. How to specify for a redesign?
Refactoring offers a path to cleaning and restructuring your code. It
is an iterative process, and although modern advice demands that
you have unit tests in place before you begin, that is impractical.
You have been maintaining the code without those unit tests, so
insisting on seat belts now would be a bit silly. Worse, it is likely
that the existing code structure makes unit testing difficult if not
impossible.
Recognize that refactoring involves varying levels of risk. Facing
a multi-hundred line method of badly structured code can be
daunting, but extracting 40 or 50 lines of inline assignment state-
ments into a well-named subroutine is low risk and high benefit.
2.2 Areas of Difficulty 11

To understand properly what the method does, you must first


reduce the complexity you perceive, and extracting a number of
subroutines is a great first step. In Delphi, it is usually best to make
these nested routines, as you avoid introducing complexities of
scope which are better addressed later.
Separating code into behavioral groups will be essential. In most
projects, you are likely to find lots of code on forms. This makes
unit testing impossible, and produces a snarl of overlapping
responsibilities. In broad strokes, you need to:
▶ Minimize code on forms
▶ Create testable classes to support activity on the forms
▶ Relocate database operations to data modules

Achieving the goals of those points will by no means be easy or


quick. But they are essential goals. Successful unit testing requires
minimization of external dependencies, and reaching these goals
also will reduce maintenance costs in future.
Forms which contain only trivial code are easily tested by hand.
Unit tests for subroutines and classes will protect future work.
Database testing imposes different concerns than other program
logic, so total separation into data modules is a great help.

The Problem of Buy-In Sales people and project managers


are usually motivated by adding features. The value of paying
technical debt requires some measure of technical understanding.
Whether you want to refactor or to redesign, substantial developer
resources are needed, and deliver only modest perceived return.
Return on investment is often seen as only a positive number, but
customer retention is a real value, and when customers begin to
seek greener pastures because of product defects, your ROI (return
on investment) declines and can go negative. New features rarely
overcome the user dissatisfaction with a defect which may have
persisted for years.
The benefits of refactoring include:
▶ Reduced defects
▶ Reduced and simpler maintenance
▶ Increased program performance
▶ Easier process to add features
12 2 Introducing the Problem

2.3 We May Be the Enemy

Delphi 1 provided a slim but dense manual on writing components:


[3]: (1995), Delphi Component Writer’s The Delphi Component Writer’s Guide.[3]
Guide
Many of us ventured into the realm of writing components, with
varying degrees of success. Creating components in Delphi is rela-
Writing components is quite different tively easy; creating well crafted components, not so much. Some
to writing application code. The latter early components had to be uninstalled, as they brought instability
routinely interacts with the visual
side of Delphi, while the former has
to the environment. Installing poorly crafted components is one
more in common with writing library way we can damage the IDE, but there are others.
code.

Commercial vs. Home-grown

It would be nice if all commercial components were of high


quality. But experience shows that they are not. There have
been some very poorly designed component packages in the
life of Delphi, and not all of those are extinct. In the realm of
visual components it is fair to mention that some are now so
complex that thorough testing of the full range of usage they
offer is simply not possible. The best resource will be vendors
who have shown themselves to be responsible and responsive
when defects become apparent.

2.3.1 Too Many Components?

Delphi is a powerful tool because of the easy use of components.


But in some cases, the developed application looks as though
Having plentiful choices among com- the developers never saw a component they didn’t like. Since
ponents is a Good ThingTM ; your ap- many component vendors offer their own improved versions of
plications will be better for making
sparing use of them. Variety easily
components which ship with Delphi, as component libraries are
leads to irregular user interfaces. added, there is an ever growing number of labels, panels, and
Often the motivation for adding com- grids, from which to choose. But these all consume memory, and
ponents to an application was simply though we’ve all become pretty relaxed since leaving behind the
from the excitement of a new feature
which seemed just perfect for a cur-
non-NT version of Windows, and the serious limitations of 16-bit
rent requirement. Delphi 1, memory remains a limited resource.
Recognize that Delphi remains a 32-
Since the release of Delphi 2 the IDE has remained a 32-bit appli-
bit application, and therefore your
256GB desktop monster cannot re- cation, and in large projects, it is not rare to see an Out of Memory
duce the limitations which apply to error before a build completes. An excess of component varieties
Delphi. contributes to memory loading and also becomes a maintenance
2.3 We May Be the Enemy 13

issue. Keeping a consistent and clean user interface becomes more


difficult as the variety of used components increases.

Components not playing well together

Sometimes there may be a collision between component behav-


iors. A simple example is when you use the TMS TAdvOffi-
cePager which is visually very attractive, and has a gradient
background by default. If you drop a DevExpress TcxCheckBox
on it, you will find that it looks out of place, because it does
not handle the gradient background well. Setting ParentColor
to True, or Transparent to True does not resolve this. Instead,
the solution is to use the TMS TAdvOfficeCheckBox, which
looks like a natural part of the page.

Slimming down the component collection can be a challenge. There have been components in use
Panels and labels will usually be pretty easy, but the more complex which presented unusual capabili-
ties, and when those have been fully
the component, the more likely that some design rework will exploited, replacing the component
be needed. You may find it helpful to extract from the map file may be a major challenge. The incen-
the identities of components actually used in your executable. tive may also be great, as the vendor
Replacing one type with another, however, even for panels and may have disappeared, leaving the
component an orphan.
labels, will require visiting each of the forms in question. The
Replace Component function in GExperts is a help for such work.
Form inheritance can make the otherwise simple task of replacing
components more difficult.

2.3.2 Abusing the Search Path

Nothing in Delphi was added arbitrarily, and that includes the use
of the Search Path. However, it may be used, or it may be abused.
When the DPR file in a large project is kept rather small, leaving
the IDE to find many of the project’s modules from the Search
Path, then the responsiveness of the IDE is adversely affected. The
Code Insight features become sluggish, or even fail. Ctrl-click on a
method name may not take you to the unit in question.
Your life will be simpler if you do
Some of the issues can be remedied through use of plug-ins such not fight against the way the tools
were designed to be used. Inclusion
as GExperts, or CnPack, but a more direct solution is to add to the of your modules in the DPR file is
project explicitly each of the units which has been created for the a case in point. And though there
project. After all, Delphi did that when they were created; someone are reasons to edit the DPR file by
elected to remove them and use the Search Path instead. hand, adding and removing project
modules is not one of them.
14 2 Introducing the Problem

It is worth emphasizing here that the correct method of adding


modules to a project is through the IDE, not by manually editing
There are reasons to edit the DPR, the DPR file.
but this must always be done with
care, and there are times when it will Note that the inclusion of all projects and form units in the project
be much more easily accomplished is a useful approach in Windows, but that the situation becomes
in a separate editor than in the IDE.
more complex in applications which target multiple devices. That
said, the focus of this book is on legacy projects, which will mostly
be on Windows. There do not yet appear to be many multi-target
applications which:
▶ Qualify as “legacy” applications, or
▶ Make use of thousands of modules

So in this book, you may assume that the strategies are based
on Windows projects. And after all, the horrible things we see in
legacy applications belong to the distant past, right? Who among
us would write such code on Windows, much less for mobile
targets?

2.3.3 Coding Too Soon?

Delphi offers nearly instant gratification: Create a VCL Forms


project, drop a button, double-click on the button, write code. It
is seductive, and in the beginning, was pretty exciting, especially
if you had experience with the tedious manual process so well
[14]: Petzold (1998), Programming presented by Charles Petzold[14].
Windows®, Fifth Edition (Developer
Reference) For small projects, you can get away with this, but as complexity
increases, this approach is a liability. There is much to be gained
Keep in mind, however, that even Del- from a study of Delphi library source code. But even at a relatively
phi library code was written by mere superficial level, you will see that the organization of library
mortals—there are good lessons and
bad to be found there.
routines is carefully categorized. The care in this design is what
makes it possible for each of those modules to function with a
minimum of interface to other units.

Write Less Code!


It is inevitable that legacy code will contain small routines
which can now be replaced by calls into the Delphi libraries.
Moreover, you may gain unexpected benefits in retiring the
now redundant routines.
2.3 We May Be the Enemy 15

Consider System.DateUtils, for example. There have been ex-


tensive revisions in support of ISO-8601 standard date and time
handling. As we all must be mindful of international issues,
and few of us are fluent in the details of many ISO standards,
this is no small thing.
Consider System.StrUtils, in which much has also been up-
dated. Part of this is for Unicode support, but there have been
other improvements, as well. System.SysUtils.TStringHelper
has added many benefits with its numerous small utilities and
support for fluent coding.
There are riches in the libraries which will free us to focus on
our own problem domains.
Simply put, if we do not make full use of library routines, we
are working too hard.

For decades, we’ve read one book after another which harped on
first creating a design, then coding. Delphi does not make that less
essential. Despite the attention given to design, we are all subject
to some impatience to see results. Although it is certainly possible
to begin coding right away, you must be willing—even eager—to
refactor that code repeatedly. And such refactoring must always
consider that the routine you are working on may be in the wrong
unit or class. Ignoring those realities will lead to maintenance
issues, such as:
▶ Duplicated code
▶ Too-wide scope (an issue I will raise repeatedly)
▶ Unit dependency cycles (see below)

Though I won’t say that code can always be improved, refactoring


is an iterative process, and there is no magic number of times
through any given block which can be said to be sufficient. As
should be apparent, this is another reason to keep your code blocks
small and well focused.

2.3.4 Inheritance

For those of us who spent years writing procedural code before we


gained access to OOP, inheritance was pretty heady stuff. Those
old Delphi hierarchy diagrams were incredible. But those diagrams,
impressive as they were, should have served as a warning. The
16 2 Introducing the Problem

larger the hierarchy, the greater the complexity, and the greater
the constraints. Inheritance is not a Bad ThingTM , but it should not
be—as it became for a time—the most used tool in our kit.
Early on we learned that although you can easily make public an
inherited method, you cannot reduce the scope of a method. So if
you decide you want to inherit from TStringList, every one of its
myriad methods and properties will become a part of your public
interface. Messy.
Composition is a more flexible option than inheritance. Inheritance
is described as an is a relation; composition as a has a relation.
It is more common now to refer to aggregation or composition,
and some will argue over the specifics of each. When you rely
on inheritance, you get the full family tree—all the ancestors are
contained in each descendant. When you choose aggregation or
The specific term is of less conse- composition, you need not expose anything but what you choose
quence than the notion it represents. I to apply. You may use a class instance within your own class, and
am more likely to refer to composition,
which clearly implies intentionality,
not make public any of its properties or methods. You get the
than to aggregation, which seems to benefit of what the class can do, but limit the interaction with
me less thoughtful. consumers to the scope of operations you designed your class to
support.
More recently, the trend has been to favor composition, an approach
in which your class may contain instances of other classes, while
not inheriting from them. What is hidden in your class may be
complex, but the complexity of contained classes is not public
unless you make it so. One consequence of this is that designing
a class using composition is a good deal easier than when you
Name collisions will not be an issue, use inheritance. There are no name collisions to worry over, so
but conceptual collisions may arise, your method and property names can be exactly what you choose.
nonetheless. Exposing a name used
in other classes and giving it a differ-
Moreover, there is no real limit to the number of classes you may
ent behavior will win you no friends. choose to make members of your own, unlike the limit of the
single inheritance rule.
You may also find it useful to create a new class that encapsulates
for the sake of constraint a much larger class, such as the ubiquitous
TStringList. That has become a minor idiom for me, and besides
making the focus very clear, it presents an opportunity to create
an interfaced class, thereby removing the requirement for manual
memory management.
2.3 We May Be the Enemy 17

2.3.5 Excessive Coupling

Coupling is the degree to which modules in a software system


are dependent on one another. Tight coupling leads to brittle code
and is to be avoided. It also makes unit testing more difficult to
implement. In legacy systems, it is common to find classes which
are too large and unfocused, which inevitably requires either
increased coupling with other modules, or code repetition.
There are multiple approaches to reducing coupling, but perhaps
the most essential point is to keep classes small and focused.
The Single Responsibility Principle (section 22.1.1) principle is key
here. A well designed class should not resemble a pocket multi-
tool. Smaller classes with fewer dependencies will always yield
improvements in build times. They will also increase readability
and maintainability of code.

2.3.6 Unit Dependency Cycles

In Delphi, if Unit One needs access to code in Unit Two, then that
access is gained through adding a uses clause:

unit UnitOne ;
interface
uses
UnitTwo ;
implementation

end .

All well and good, and if the modules are well designed, no ill
effects will result. However, consider what happens when Unit
One uses Unit Two, and Unit Two, in turn needs a method from
Unit One. This can be resolved with a second uses clause:

unit UnitTwo ;
interface

implementation
uses
UnitOne ;

end .
18 2 Introducing the Problem

Now these units might be diagrammed as:

Unit Two Unit One Unit Two

Although a better representation might be this:

Unit Two Unit One

The MMX Unit Dependency tool (see Appendix) would report the
unit cycles thus:
Cycles for UnitOne (1)
UnitOne,UnitTwo,UnitOne
Cycles for UnitTwo (1)
UnitTwo,UnitOne,UnitTwo

This is not the end of the world by any means, but as the project
complexity increases, so does the depth of such cycles. When you
are working with 1,000 or more units, and the majority of them are
participating in dozens (or more) of such cycles, and the length
of the cycles is dozens (or more) units, then you have a real snarl.
And worse, you will find that the days of building in seconds are
long gone, and that building the application now takes minutes.
In the main, Unit Dependency Cycles are the product of poor design.
Returning to the simple case given above, a solution might be to
create a Unit Three which contains the method needed by both
Unit One and Unit Two. Then the picture in Unit One would
reduce to this:

unit UnitOne ;
interface
uses
UnitTwo , UnitThree ;
implementation

end .

unit UnitTwo ;
interface
uses
2.4 The IDE, Again 19

UnitThree ;
implementation

end .

UnitThree provides what is needed by UnitOne and UnitTwo,


and thus, no dependency cycles. The good news is that in many Always keep scope as narrow as pos-
cases, a correction may be just that simple; the bad news is that sible. Begin with private, and only
widen the scope for good reasons.
making such changes is labor intensive, both in determining how
to redesign, and in updating the other dependent modules in the
system.

2.4 The IDE, Again

Unit Dependency Cycles also affect the IDE. As soon as you type a
character in a code module, the prior state of the code as the IDE
understood it is now broken. The IDE then begins the reconstruction As I write this, Delphi 10.4 Sydney
of parse trees, and you will see a busy cursor indicating the IDE is has just been released, and as it im-
plements the Language Server Pro-
not ready for input. What we do not see is all that is happening tocol (LSP), the visual behavior will
behind the scenes. The interactive behaviors of the IDE depend be different, and reportedly will not
for their performance on the state of the parse trees, and hence the present the busy cursor.
delay when they must be rebuilt.
Rebuilding parse trees means that for each unit, the uses clauses
must be visited, leading to other units which must again be parsed.
Units which are included in the project are found using the paths
included in the declarations in the DPR file; the rest must be
discovered through the Search Path.
Delphi library modules need not be searched for, as they are known
to Delphi through registry entries. Similarly, components installed
using the installation tools provided with them will have registry
entries in place, and are in the collection of known packages.
What is essential is that the units we create in our own applications
should be added explicitly to the project. When we create a new
unit, form, or data module, Delphi adds it to the project, making
the entry in the DPR which includes path information.
If we make use of a unit of our own which is not declared a member In our code, entropy increases. We
of the project, then the Search Path provides the means of finding make changes, and in so doing, we
introduce defects. This is especially
the files. None of this need concern us much in small projects, but a concern when we lack unit tests. In
in large projects, these issues are important. And in extreme cases, legacy projects, a lack of unit tests
is the rule, not the exception, and
you can’t just create a collection of
them when much of your code is
untestable.
20 2 Introducing the Problem

consider that sometime in history, someone created a unit called


SpecialFormats.pas, and placed it in the main project folder. Later,
as the project grew, it was placed into a new sub-folder called
Formatting, but the original file remained where it had been. Over
Duplicate units become problematic, time, the SpecialFormats unit is modified and expanded. Things
and are best dealt with immediately. keep working because it continues to be found in the Search Path.
But in legacy projects they are not
uncommon.
But now, someone else alters the ordering in the search path,
which then changes which unit is located in search to fill the need
for SpecialFormats. Perhaps the correct version and the incorrect
version have identical public interfaces, but in the correct version
defects have been repaired. This reintroduces problems which are
unexpected, and can be challenging to discover.

Entropy is Reality

Entropy is unavoidable. Code rot happens. ”But I am better


than that!” Perhaps. And are you the sole developer in your
code? And are you always intensely focused on whatever
you are coding? Every minute? And you never, ever, make a
mistake? That’s great. It’s also unlikely, at least for most of us.

It is easy to dismiss the scenario above, as it might have been


avoided, had the unit been moved, not copied. There would then
be only one unit to be found. But the point here, and in much of
this book, is that in legacy projects it is axiomatic that:
▶ Best practices have not always been followed.
▶ Some units were created by departed team members.
▶ Naming practices were poor and inconsistent.
▶ Commenting was poor and inconsistent.
▶ Scope is broad, even global.
▶ Multiple projects may exist in a single file tree.
▶ No design documents exist.
Again, the perspective here in this volume is not optimistic, but
realistic. The issues presented have been seen and addressed in
production code. You will do well to thoughtfully consider these
issues in any large project you are called to maintain. Certainly
there is a cost in refactoring and reworking code into a coherent
design, but it can be done incrementally, and will always bring
value.
Strategies 3
3.1 Philosophy and Reality21
3.1 Philosophy and Reality
3.2 Setting Goals . . . . . 22
3.2.1 Patience . . . . . . . . . 22
Decades of software development have led to a recognition that de-
3.3 Select Your Compo-
sign should precede coding. However, a great many legacy projects
nents . . . . . . . . . . . 23
offer evidence that design has been largely an afterthought.
3.4 Pre-convert, if Neces-
Philosophy posits ideals, and is very attractive, but in the commer- sary . . . . . . . . . . . 23
cial world, at least, software products very often spring from hazy 3.5 Isolate Your Code . . . 24
goals, and a lack of design. When you first confront a requirement 3.6 Achieving a Build . . . 26
for a program, it may be difficult to define the scope of behaviors 3.7 Module Inclusion . . . 28
it should implement. And without that, any attempt at design will 3.8 Separating Projects -
be sorely limited. How and Why . . . . . 28
We will not waste much time on the top-down philosophy here. 3.8.1 Multiple Projects in
One Delphi Version . . 29
This book is about legacy projects, and the safest assumption is
3.8.2 Multiple Projects in
that no design exists. All is not lost; rather, we will seek to impose
Multiple Delphi Ver-
a design on this code, as we recognize what the design should be. sions . . . . . . . . . . . 30
The existence of a software product does not often imply that we
3.9 Start Small . . . . . . . 32
are ready to retire to a room filled with white-boards and render a
design. Much of the time we will learn that there are complexities
3.10 Incremental Change . 32

which are not apparent from the high-level view. As we discover 3.11 Strip Deadwood . . . . 32
them, we will need to rethink and be flexible. Bringing order out
of chaos will require patience, persistence, and perseverance. We
will meet challenges and obstacles.
There are useful strategies for attacking the problem of a large
legacy project, and this chapter will be a whirlwind tour. The
details comprise the rest of the book.
▶ Set your goals
▶ Select components with care
▶ Isolate from unnecessary code
▶ Pragmatically achieve a build
▶ Include all project modules in the project
▶ Start small
▶ Plan on incremental change
▶ Strip away the deadwood
22 3 Strategies

You may think the order is odd, but it is based on minimizing


wasted motion. I’ll explain in the sections which follow.

3.2 Setting Goals

Usually, one of the primary goals you will have is to move to a


newer release of Delphi. That change alone will force some of your
approaches. There may be other important goals, and you should
definitely get them documented, and share with your manager
and team members.
A close second on the goals list may be the reduction of compo-
nent variety. Do you really need to use grids from five different
component libraries? Reducing component variety will:
▶ Reduce license costs
▶ Make forms appearance more consistent
▶ Make coding more consistent

Other benefits may be apparent, but these three will be high on


the list.

3.2.1 Patience

Legacy projects develop over a period of years, with changing


staff, changing goals, and perhaps changing ownership. All of
these factors contributed to the code you now hope to clean up.
As the project took years to reach its current state, do not expect
radical change to take only days.
Goals in such work are best considered as milestones. Managers
will want dates assigned to these, and as understandable as that
Demanding a date may be fruitless, may be, the dates will increase stress, and may cause those same
and lead to false expectations. In mi- managers to consider abandoning the project. In the earliest stages
grating to a newer version of Delphi
you may find that it takes quite a
of rework, you will do well to avoid calendar concerns.
while to complete a build. Stronger
Even percentages of completion may be impractical. Consider
typing, issues with Unicode, stream-
ing, compression, all may contribute. some unknowns:
Predicting when you will achieve
that build is simply guesswork—but ▶ How many modules may now be unused?
it may get you a black mark if you ▶ Will there be delays in corporate renewal of component
predict badly. licenses?
▶ Have some component publishers disappeared?
3.3 Select Your Components 23

▶ Have some component architectures changed?

Each of these factors introduces delays which can be impossible


to predict.
An issue here, to borrow a phrase, is
As a developer, you will need an abundance of patience. Stay with unknown unknowns. We can-
focused on what you hope to achieve, and do not be discouraged; not estimate that which we do not
each day that brings improvements is a day closer to completion. perceive. We can’t even offer a useful
placeholder.
Understand, too, that your manager likely answers to a manager
above, and needs to be kept apprised of progress in more detail
than might be needed on other work. This is especially true
when in early stages you are unable to achieve a build, much less
demonstrate anything.

3.3 Select Your Components

Imagine you currently have about 30+ component vendors’ prod-


ucts in use, and are stuck with the problem of what to kill. Here
are some suggestions:
▶ Discuss with team members. They will usually be able to
list quickly the small number of essential vendors.
▶ Consider the quality of the products. Which components
give greatest ease of use? Best appearance? Fewest coding
hassles?
▶ Identify unique features. There may actually be reasons why
you need grids from more than one component vendor,
but know why it is so, and be certain. There are real costs
associated with mix and match, and not merely the license
fees.
▶ Use a tool to discover components in use. The MAP file is a
starting point. But there are commercial tools as well which
can help.

3.4 Pre-convert, if Necessary

You may now have identified components which are in use and
which you will not install in the newer Delphi. If that is so, you will
want to pre-stage to a branch in your existing repository where
you can replace these components with others which will be in
the newer Delphi.
24 3 Strategies

If you have not managed such a mi- It is much easier to pre-stage, since you will be replacing one
gration before, this may be less than component with another. GExperts can help. Otherwise, when you
obvious. Changing out components
you do not want for those you do
move forms to the new Delphi where components are missing, the
is much easier in the environment forms rework will be more difficult. In some cases, architectural
in which you are able to build and changes will prevent GExperts form being useful. For example,
test. Forms moved across with com- some years ago, DevExpress made substantial changes in the
ponents which are not present in the
new Delphi will force you into a re-
architecture of some of their components. The net benefit to
design operation. customers was large, but came at a cost. They do provide a tool
to assist in converting from the old grids to new, but changes in
coding remain essential.

Hints and Warnings

In preparing your project to migrate to a new compiler, I


strongly urge you to handle all Hints and Warnings. Despite
being free from such messages, your code almost certainly will
provoke Hints and Warnings from the new compiler. If you
did the cleanup prior to migration, then you may be certain
that these issues are not from the code as written, but reflect
changing concerns in the Delphi language, Object Pascal.

Since the production code contained instances of both varieties of


the grid, it was the right place to convert the remaining instances of
the old to the new. This allowed us to use the DevExpress conversion
wizard, and also made possible the retest of code which needed
rework.

3.5 Isolate Your Code

For a variety of reasons, create a new repository in your source


control and copy your existing production code there. This will
become your pre-stage branch, as discussed above. After you have
completed the conversion of components, the code in this new
repository should not require visual components which are not
present in the new Delphi. In migrating from pre-stage to another
repository for the new Delphi, consider whether the project source
tree is organized as well as it might be. This may be the time to
reorganize. But in considering that, keep in mind that changing
the arrangement of the folders may affect the validity of the DPR
and DPROJ files.
3.5 Isolate Your Code 25

Plan to make this new repository support just one project. Your
old code base may have multiple projects in the same tree, and More detail will follow, but consider
that adds complexity, slowing your progress. Keep the one you that often multiple projects inhabit
a single folder tree. Although this
will be focused on and toss the rest. Once you have tamed the may seem logical in the beginning,
beast, you can revisit the issue of other projects to be brought in, it becomes very difficult to manage,
but it will be in the context of a clean house. and leads to issues with coupling and
UDCs.
If you make your new repository for a single project, you will be
able to remove unneeded modules as you achieve a build. Over
time, you may need to migrate one or more other projects, and
should follow the same approach. Eventually, you will extract
modules which are common to multiple projects into a shared
sub-tree, where they are easily recognized as affecting multiple
projects. This will be a boon to maintenance later.

Defect Merging Later


If you are a member of a product
Given that you plan to do significant refactoring and that the team, then the issue of merging de-
fect repairs has a relatively simple
project rework will continue over a period of months, the solution. Make the team members
management of merging defect repairs becomes an issue. As responsible to merge repairs to the
your migrated project evolves, the resemblance between old migration in the same way as they
code and new will continue to diminish. Unless you merge would to any other active branch.
This is a solution which will need
repairs as quickly as possible, your best strategy probably will management buy-in. However, if the
be to compare current release code to the pre-stage code to entire team works on the migration,
identify what must be merged to the new project. So keep that then this solution will be very natu-
ral.
pre-stage repository on hand!
Also bear in mind that the merging of defect repairs will be a
recurring task, unless your migration is very rapid!

With this new repository, you are now free to take a few essential
steps:
▶ Remove unneeded DPR and DPROJ files. (You need just one
of each.)
▶ Ensure all the component sets you selected for the new
environment are in place.
▶ Prepare a plan for documenting what you do, and what will
need to be done later.
26 3 Strategies

3.6 Achieving a Build

Now begins the tedious process of making the project build. Oh,
and in Project Options, disable writeable constants. Although in
formal terms, these are called typed constants, I prefer to call them
writeable, as that is their essential feature—they are not actually
constants.

About Writeable (Typed) Constants

The use of writeable constants—typed constants—is interesting.


Most developers, I think, would now suggest their use is a bad
practice, and liken them to global variables. They exist to hold
stateful content, and as such, are dangerous. The wider the
scope of these writeable constants, the greater the risk of their
misuse.
Some would argue to clean these out later. Disabling them will
likely introduce problems, and you will then need to fix those.
Ultimately, when to deal with these is a judgment call. You
may even decide to leave them, though I would suggest you
will regret that decision. As with many aspects in the process
of updating legacy code, the sequence is largely up to you.

This is a voyage of discovery which I can’t document, other than


to say that there will be issues to resolve, such as:
▶ Files not found. Did you change the tree structure? Or
the search path? See the next section for more on project
modules.
▶ Issues with types. It’s a brave new world, and Delphi is
less forgiving of types than before. The Integer you used to
pass for the help context is now a THelpContext. The DWORD
parameter to a Windows call is now a NativeUInt. You’ll soon
get a feel for it.
▶ The previously writeable constants are now variables which
must be initialized. You will need to determine with care
when and how to accomplish the initialization. You will
have errors which you can resolve by applying the compiler
directives in your code (recommended), or by creating proper
variables now, and leaving the constants as they are. Your
call, and the issue is discussed in some detail in Writeable
“Constants” on page 43.
3.6 Achieving a Build 27

▶ Deal with other issues as they arise. Don’t take the easiest
path, as you will need to revisit these later. At worst, if you
must comment something out, then place a comment with
your name and the date, as well as an explanation. And add
a TODO item.
How long will it take to reach buildable code? You will be asked. Are you unnecessarily rebuilding
The honest response is: ”I don’t know.” You can guess, or estimate, components when you build your ap-
plication? Check in the folder where
but depending on how you are doing the migration, you may be you direct your DCUs. If you are,
wildly wrong. You know the line count in the old version, so that then the compiler line count will be
should help, right? Maybe. But it is only going to be close if you different than you expect, and the
have replicated all of the dozens of factors which affect how that build time will be longer.

line count is achieved. Once you have passed the old line count
with no end in sight, what will your answer be?
What you may find at least moderately helpful is to save the
messages from a build in the old compiler, and compare with the
messages from the new one. But you are likely to find the new
compiler emitting hints and warnings relating to the increasingly
strict typing. So even this will be of little help, unless you filter out
all but the messages which identify the modules. Then you would
get some sense of progress.

How to Avoid Rebuilding Components

Most commercial component packages will manage their file


placement to spare you from rebuilding their components.
Some will not. And open-source packages often will not. For
those which you find rebuilding each time, you need to do
these things:
▶ Create a folder where you can place the compiled files,
or plan to put them into one of:
• $(BDSLIB)
• $(BDSCatalogRepository)
• $(BDSCatalogRepositoryAllUsers)
Look in your existing Library Path where you will
likely find examples.
▶ Copy to the target folder all the files from the component
set which are .DFM, .DCR, .DCU, or .RES
▶ If you created your own folder, add that path into the
entries in the Library Path
28 3 Strategies

▶ Delete from your project compiled files all .DCU files


▶ Rebuild project
▶ Verify that none of those component .DCU files you
moved are present in the project compiled files

3.7 Module Inclusion

The Search Path (See Section 2.3.2) is often used as a means of


finding project modules, but I recommend in favor of explicitly
including your modules in the project. The benefits include:
▶ Increased responsiveness in the IDE on Code Insight
▶ Ability to use the CnPack Uses Cleaner (after the project
builds)
▶ Greater utility in the project tree of the IDE

This explicit inclusion seems to reduce instability in the IDE, and


certainly makes Code Insight more responsive.

3.8 Separating Projects - How and Why

As mentioned above, you will do well to begin with a single


project, and to put that project in its own tree. There are many
scenarios you may be facing, and I shall try to cover some common
possibilities:
▶ Multiple projects, all in one Delphi version.
▶ Multiple projects, in two or more Delphi versions.

A necessary first step is to identify the target projects, regardless


of Delphi versions. Similarly, you will need to identify the required
component packages—presumably you will be using a single new
version of Delphi—and ensure that you have current licenses. De-
termine the order in which you prefer to work on these migrations,
and bear in mind that you really will want to do just one at a
time.
3.8 Separating Projects - How and Why 29

3.8.1 Multiple Projects in One Delphi Version

This may be the most common scenario, and can be present


complex challenges. Difficulties include:
▶ Can be difficult to determine which projects are dead and
can be removed.
▶ Very difficult to determine which modules are dead, so
clutter accumulates.
▶ Difficult to untangle the cross connections which have de-
veloped.
The specific folder structure will be for you to determine, but if you
have three projects to migrate, then I would suggest something
like this:
▶ Project A
▶ Project B
▶ Project C
▶ Shared Modules
Project A, your first, will get copies of all subfolders and files
needed to build that project. That usually means you will begin
with a copy of the entire tree and its files, needed or not, as you
can’t easily identify the unneeded files at this point.
You will then proceed with the work needed to achieve a build
of Project A. Once that is completed, you can also identify the
unneeded files, and remove them. MMX can produce a report of
the necessary modules, but you may need to cross-check them
against the list of modules in the map file, or the collection of DCU
files produced, to catch any false positives.
Project B will get your attention next, and you will again copy
in the full set of subfolders and files, as you still have no better
approach available. You could, as an alternative, begin with a small
set of source modules and your DPR and DPROJ files, and then
add files as the compiler reports them needed. This becomes very
tedious, and can only be recommended if the project is relatively
small.
As you did with Project A, you will work to achieve a build of
Project B. Identify the unneeded modules, and remove them from
this tree, cross-checking against the map file, or the collection of
DCU files produced, as before.
30 3 Strategies

Project C now becomes your target, and the process repeats, as


for A and B.
Shared Modules is a project of a different sort. If you have removed
all dead modules from projects A, B, and C, you should be able
easily to collect the lists of files used by each project. You can
build some simple tools of your own, or make use of existing tools,
including a spreadsheet.
Assuming a spreadsheet program is at hand, then put the list of
files for each project into it, one project per worksheet. You can
then sort them alphabetically and look for files common to each.
You may wish to use the VLOOKUP function to find matches. Any file
which is used in two or more projects should really be relocated
to the Shared Modules folder tree. Keep in mind that for each file
to be relocated, you must:
▶ Verify that the multiples are identical.
▶ Copy one to the Shared Modules.
▶ Remove that file from each project folder.
▶ Update each affected project to see reference the affected
file from its new location (preferred approach), or to make
available the Shared Modules folder in its Search Path.
Whether you accomplish this work comparing project pairs, or all
migrated projects at once, is your choice. The end goal is that each
shared module will have only a single existence in the new folder
structure. A checklist of steps can be useful.

3.8.2 Multiple Projects in Multiple Delphi Versions

The scenario above still applies, but with adjustments. Begin with
Project A, as before.
Project B may be the same, if it is presently in the same Delphi
version. However, if it is in a different Delphi environment, then
things are somewhat different. Copy the full set of folders needed
for the project into your new Project B folder.
Project B has a different DPR and DPROJ, and will use a different
search path. None of this is troublesome. However, there are two
areas in need of special understanding:
3.8 Separating Projects - How and Why 31

▶ Components needed. Some will likely be as in Project A,


but there may be others needed, as well. Your Delphi IDE
has one set of components, so all the sets needed for all your
projects must be in that IDE.
▶ Locating the correct source code when file names match.
Perhaps you use uStringUtils in both environments, but the
content is not identical. If Delphi tries to use the one from A
when building B, there will be problems.
Component needs are simply met. Install any missing components
into the IDE. All will be there, and all will need to be found in
your Library Path.
Building Project B will be an iterative process. So how can we
isolate it from the files of Project A? Simply by renaming the Project
A folder to something like xxProject A. When your files in Project
B are not found, you will be adding them—explicitly—from the
Project B folder tree.
Once Project B builds without error, you can proceed to Project C,
and may then want to rename Project B to xxProject B.
Finally, after all projects are building, restore the folder names,
removing the leading double underscore, and rebuild fully each
project.
This is not terribly complicated, but it is easy to make mistakes,
so you may have some cleanup to do. And at last, the Shared
Modules process comes up again, in which you will look for files
which can be shared among projects.

Limits on Shared Modules


You may have perfectly good reasons not to share some mod-
ules. The most obvious would be that, despite the shared name,
the shared functionality is not very great. On the other hand,
it may then be worth considering whether one or both might
be due to be renamed, so that the names may better reflect the
behaviors provided by them.

All this work in managing the migration of multiple projects is


another work of refactoring. There is no immediate pressure to
extract modules that can be shared, though it will reduce mainte-
nance in the future. Be logical and eliberate in your approach—this
work is an investment of high value.
32 3 Strategies

3.9 Start Small

Once you have achieved a build, you probably will have amassed
a list of things you want to change. That’s great, and a normal
part of the process. But don’t go crazy, work with incremental
changes.
A likely area for rework is in forms appearance. You may have
forms which are inconsistent in color, dialogs on which OK and
Cancel buttons are not consistently placed, a mish-mash of different
brands of checkboxes and radiobuttons.

3.10 Incremental Change

Rule: If you edit a module, leave it better than you found it. No
exceptions.
As your project is almost certainly not ready for unit testing,
you must focus on making small changes. A rewrite is high risk,
but small tweaks here and there will be relatively safe. This is
particularly true when you are factoring out nested routines from
long and ungainly old code, as the nested routines will tend to be
very focused, and easy to verify visually. Moreover, if you name
them well, the calls which replace the old inline code will serve to
make the entire routine more comprehensible. See A Workable
Approach to Small Change on page 157 for more detail.

3.11 Strip Deadwood

Now for some serious housecleaning. This had to wait for the
project to build. Start by having a look at ProjectFiles Tool. The
Project Files tool will help you to find and remove files with
duplicate names, and unused source modules. In a large legacy
project the number of dead files can be surprisingly large. And all
the modules you remove are modules you need not maintain.
If you are using MMX (and if you are not, you should be) then
you can use the MMX Linked Module Analyzer to produce a list of
modules linked into your project. Compare this list to the modules
present in your project tree and remove those not needed. Be
3.11 Strip Deadwood 33

mindful that in migration, you may find it expedient to comment


out blocks of code for a time, and this may alter the list of linked
modules. Therefore consider when it is going to be best to do this
removal. Also expect to be an iterative task as you improve your
migrated project.
Beginning 4
We will begin with a summary view of various challenges in the 4.1 The IDE . . . . . . . . . 35
legacy project environment. There is no shortage of concerns to 4.2 The Build Process . . . 36
address, and later we will investigate them in depth, but it is good 4.2.1 Build Management . . 36
to review the basics. 4.2.2 Accept Compiler Ad-
vice . . . . . . . . . . . . 36
There are many aspects to the process of improving legacy code,
and many names for the aspects and principles involved. Arguably, 4.3 Data and Code . . . . . 39
4.3.1 Scope of Data . . . . . . 41
the essential summary is what is termed technical debt. And as with
4.3.2 Writeable “Constants” 43
any other debt, the technical sort must eventually be paid. Keep
4.4 Scope in General . . . 44
in mind the reality of technical debt, and particularly when it is
4.4.1 Forms vs. Units . . . . 46
asserted that a set of tasks has little or no ROI. An often overlooked
4.4.2 Proper to Forms . . . . 46
reality is that ROI is not always a positive number, and this applies 4.4.3 Proper to Class Units . 47
especially to ignoring technical debt. 4.4.4 Proper to Data Modules 48
4.4.5 Everything Private! . . 48
4.4.6 Global not Always Bad 49
4.1 The IDE 4.4.7 Partitioning . . . . . . . 49
4.5 Exception Handling . 51
4.5.1 A Simple Example . . . 51
The IDE has always been one of Delphi’s essential features, and
4.5.2 Exceptional Failures . . 52
from the first release, provided code editing, forms design, and 4.5.3 Exceptional Confusion 55
debugging. Over time, the layout of the IDE has changed from 4.5.4 Scope of Exception
the floating multiple windows of the earlier releases to the single Handlers . . . . . . . . 57
window with multiple floating or docked children. 4.5.5 Default Exception
Handlers . . . . . . . . 58
The dominant feature of the IDE was initially the Component
4.6 Code Formatting . . . 58
Palette, but now that seems just one of many essential elements.
4.6.1 Formatting Matters . . 60
A core capability is the addition of plug-ins to the IDE which 4.6.2 Manageable Formatting 63
allow third-party developers to create productivity enhancements. 4.6.3 Minor Formatting . . . 64
Among the most familiar are probably these:
▶ CnPack
▶ DocumentationInsight (commercial)
▶ FixInsight (commercial)
▶ GExperts
▶ MMX (formerly ModelMaker Code Explorer)
▶ TestInsight
These plug-ins provide some very useful features, and you will
do well to explore them.
36 4 Beginning

4.2 The Build Process

When Delphi was introduced, the build speed was amazing. For
small applications, that remains the case. But in large legacy
applications, there are numerous factors which affect the build
time, and you can certainly expect it to degrade over time. Entropy
increases, unless we take active steps to reverse it, not once, but
every time we touch a module.

4.2.1 Build Management

In recent versions of Delphi, MSBuild is used, and you can elect in


Project Options to build in a separate process:

Figure 4.1: Enabling the separate


build process

In older versions of Delphi, this option is not available.


Many of us will prefer not to use the separate build process. It
feels disconnected, as there is no feedback, no progress bar update,
no line count. However, as a separate process, it runs in its own
memory space, so it can provide relief from the Out of Memory
error.

4.2.2 Accept Compiler Advice

This should be obvious, but bears repeating: Eliminate all reported


Hints and Warnings. Once that is done, your code will be cleaner, and
there will be fewer potential defects. What is the point of helpful
advice from the tool if you choose to ignore it?
4.2 The Build Process 37

It would also be good to check the entire project with FixInsight


and deal with the issues reported there. See Chapter 36 for more
on FixInsight.
In Project Options you can elect to have the compiler add code
for:
▶ I/O Checking
▶ Overflow Checking
▶ Range Checking

These will be discussed later in some depth, but you really should
consider turning them all on in your debug configuration.

Owner Disposal Never manage component lifetimes manually,


unless forced to do so. This relates not so much to the compo-
nents you drop on your forms, as to the larger components you
create dynamically and which many people manage explicitly
in code. Perhaps people forget that a form is a component, and
thereby overlook an opportunity. Whatever the case, Delphi has
always provided a clean and simple solution for managing the
disposal of forms and other components through the use of AOwner.
Consider:

var
MyForm : TMyUsefulForm ;
begin
MyForm := TMyUsefulForm . Create ( nil ); // AOwner
try
// the important operations are here ...
finally
MyForm . Free ;
end ;

That is a valid way of managing a form, though you will have


written more code than necessary. When the dynamically created
form is used only inside a single method, it doesn’t seem so bad.
But in many instances, you may create the form in one method,
and destroy in another. It is not uncommon to create such forms
in the constructor of the calling form, and to free them in the
destructor. A better way, however, would be this:

var
MyForm : TMyUsefulForm ;
begin
38 4 Beginning

MyForm := TMyUsefulForm . Create ( Self );


// the important operations are here ...

This seems as good a place as any Passing Self to the constructor assigns the current form as the
to mention that although names owner of the dynamically created form. The owner has the respon-
like MyForm and TMyForm have long
been used in books and articles about
sibility to dispose of such objects when it is freed. This is as safe a
Delphi, they really ought never to be mechanism as the use of try/finally, and adds no clutter to code.
names you would use in your appli- It is possible that the dynamic form may be called more than once
cation. A pet peeve, but as names are in the life of the calling form, and you will want this pattern in
important, do consider it.
such instances:

var
MyForm : TMyUsefulForm ;
begin
if not Assigned ( MyForm ) then
MyForm := TMyUsefulForm . Create ( Self );
// the important operations are here ...

You must also design that dynamic form to be free of any persistent
state which could cause different behaviors on subsequent calls.
The options above impose some degree of performance overhead,
but should really be used, at least in your debug configuration.
Note that range and overflow checking are off by default, probably
because they incur performance overhead. It is a very good idea,
however, to enable them in your debug configuration at least, as
their use can help you to discover and correct defects before your
users do.
Perhaps you never fail to check such issues in your code, but in
legacy code, you can bet someone has failed to ensure that an array
bound is respected, or an index is calculated without checking
that the resulting value is in range. Delphi can help, and although
in new code you will want to be proactive, you must still survive
the horrors of code you have not yet even read.

Summary

Whether you are in the most recent Delphi version, or an older one,
the build time is something which you can observe as an indicator
of project health. If it takes minutes to build your project, you need
to do some work!
4.3 Data and Code 39

4.3 Data and Code

In 1976, Niklaus Wirth wrote Algorithms + Data Structures = Pro-


grams1 . Anders Hejlsberg’s initial work on Turbo Pascal reportedly 1: Wirth, Niklaus (1976). Algorithms
was inspired by Wirth’s Tiny Pascal compiler in that book, so we + Data Structures = Programs. Prentice
Hall.
may consider that volume to be the progenitor of Turbo Pascal.
Despite this heritage, it is common in legacy code that data
structures are rarely used to simplify code. In this trivial example,
the initial coding was thoughtless, at best.

function TwmMain . GetCaption ( WantFrench : Boolean ;


CaptionIdx : Integer ): string ;
begin
if WantFrench then
begin
case CaptionIdx of
1: Result := ’Un ’;
2: Result := ’ Deux ’;
3: Result := ’ Trois ’;
end ;
end
else
begin
case CaptionIdx of
1: Result := ’ One ’;
2: Result := ’ Two ’;
3: Result := ’ Three ’;
end ;
end ;
end ;

This is mind-numbing, and pointless. The strings are constants,


and the selection is based on two variables. Rather than use such
a verbose construct, we should favor at least an array of string
constants, accessed by those variables. A better approach would
have been:

function TwmMain . GetCaption2 (


const WantFrench : Boolean ;
const CaptionIdx : Integer ): string ;
const
arrCaptions : array [ False .. True , 1..3] of string =
( ( ’ One ’, ’ Two ’, ’ Three ’) ,
( ’ Un ’ , ’ Deux ’, ’ Trois ’) );
begin
40 4 Beginning

Result := arrCaptions [ WantFrench , CaptionIdx ];


end ;

Of course, there is more which could—and should—be done.


This example looks contrived, yet is based on actual code from
an application. The second solution is hardly an example of
desirable coding practice, but that is at least an approach which
was implemented after a bit of thought.
Note that the second version has a weakness not found in the first.
The case statement used originally will only respond to indices in
the range 1..3, but in the second version, you really should guard
against an out of range value:

function TwmMain . GetCaption2 (


const WantFrench : Boolean ;
const CaptionIdx : Integer ): string ;
const
arrCaptions : array [ False .. True , 1..3] of string =
( ( ’ One ’, ’ Two ’, ’ Three ’) ,
( ’Un ’, ’ Deux ’, ’ Trois ’) );
begin
if CaptionIndex in [1..3] then
Result := arrCaptions [ WantFrench , CaptionIdx ];
end ;

Many people would assume from the start that this section referred
to data as found in a database, but as the example shows, data
can be very simple. Keep in mind that data can often simplify
Magic numbers refers to the use in coding, and be watchful for such opportunities. Moreover, the
code of numeric literals which com- removal of strings from code is usually an improvement, though
municate no obvious meaning. For
example, although the fourth column
some may find it less obviously one than the elimination of magic
in your data may be ‘Description’, numbers.
coding a reference to a numeric index
is far less useful than a reference to a And though I refer to magic numbers the principle can be applied as
constant named colDescription. well to the use of hard-coded strings, especially where such strings
are used repeatedly. It would seem obvious that these should be
declared as named constants.
In this discussion, data should then be taken as including:
▶ named constants
▶ variable references
▶ fields in classes
▶ enumerated constants
▶ arrays
4.3 Data and Code 41

▶ records

Anything which can be resolved at compile time offers the oppor-


tunity of using the compiler to reduce the likelihood of errors.
Using enumerated constants certainly falls into this category, but
consider that you could use an array of records indexed by an
enumeration. That may sound contrived, but I have done exactly
that in managing the reporting of summaries where there are
different summary types (the enumeration), each with a record
which contains the display name and a set of visible columns
(from another enumeration).
Carefully considering what you can reduce to data may not only
reduce the complexity of code, but may also make it possible to
code with no reference to the specific data content or meaning.
This is another example of the Separation of Concerns principle
(section 21.4): it is not essential—or desirable—for presentation
code to embody other than presentation matters such as visibility,
format, and so on.

4.3.1 Scope of Data

One of the problems with hard-coded strings and numbers is that


they are local, although it will often be better to consider that they
may used to advantage in a wider scope. An obvious example For example, in dealing with day
is the names of days, which Delphi conveniently manages for us. names, Delphi applies index 1 to Mon-
day, and 7 to Sunday. To do otherwise
There is no reason to define our own set of names of weekdays in your own would simply provide
if the form we want is already available in the Delphi libraries. an opportunity for error.
And if you do need to define names of your own, let them be
consistent in form with those in the Delphi libraries unless you
have a compelling reason to do otherwise.

Conventions and Standards


In my side note, I mentioned index 1 is Monday, and this is
consistent with the specification in ISO-8601. However, I should
mention here that you must take care to recognize that Delphi
offers two views of days of the week. From the Delphi help:
Call DayOfTheWeek to obtain the day of the week
represented by a specified TDateTime value. Day-
OfTheWeek returns a value from 1 through 7, where
42 4 Beginning

1 indicates Monday and 7 indicates Sunday.


Note: DayOfTheWeek is ISO 8601 compliant (in
which Monday is considered the first day of the
week). To obtain the day of the week, where Sunday
is considered the first day of the week, use the
DayOfWeek function instead.
System.SysUtils.DayOfWeek and System.TDateTime.DayOfWeek re-
turn 1 for Sunday. This is from help in Delphi 10.4 Sydney. You
will do well to exercise care in this area; check the behavior of
whichever routine you will use, and be sure of the result.

Given the need for an array of constants, or an enumerated constant,


or any defined type of constant, we will always do well to consider
the scope in which it will have value. Unlike variables, global
Always prefer the narrowest prac- constants and types exact no penalty and will cause no problems.
tical scope. This is less a concern Indeed, there are times when making such things global is a
with types and enumerations, but
still worth pursuing.
particular advantage. There are dangers, as well, in having multiply
defined types and constants. Obviously, you will create problems
if you define two enumerations of the same name in different
contexts and in which either the members are different, or their
ordering varies. The behavior of parts of your code will depend
on the particular enumeration which is discovered first by the
compiler.
Equally, you will cause confusion by defining your own type
under the same name as a well known Delphi library type, such as
TIntArray. Possible issues include implementation differences and
assignment compatibility. Name collisions with standard Delphi
libraries become more likely as the libraries expand, but may be
avoided very easily if we always use a naming convention not used
by Delphi nor in any of the component libraries we use. In place of
TIntArray, I might declare TwmIntArray. If it has not yet been done,
you should establish a company standard for the notation used in
your code.
On the other hand, there are benefits to reducing scope, even in the
use of constants and types. One such would be the categorization
of our types and constants. This may be a value in bringing new
developers into the environment, as it becomes pretty obvious
where things should be found if we have units such as:
▶ uGlobalConsts
▶ uGlobalTypes
4.3 Data and Code 43

▶ uUIConsts
▶ uUITypes
▶ uDBConsts
▶ uDBTypes

4.3.2 Writeable “Constants”

Delphi continues to provide the writeable typed constant, though


it should rightly be seen as a global variable. You can argue against
this view, but a compelling example is this:

const
IniFileFolder : string = ’’;

A nominal constant which is initially an empty string is manifestly


not a constant, as it is utterly useless until a value is assigned at
runtime. There was in history a case to be made for the typed
constant as a means of persisting a local value across multiple
calls. Not a strong case, but a case. But in Delphi, which has always
had objects, we have numerous alternatives, the most common
being the use of a field variable.
In some cases its applications can be astonishing, and certainly
dangerous. The issue, of course, is the effect on program state, and
the potential—especially with wide, even global scope—for the
state of a nominal constant to be altered and not restored. Such
practices should be avoided in new code, and should be coded
out of existence in legacy code as soon as practical.
One relatively painless approach to clean-up is to disable writable
constants globally in your project. In Project Options you can
turn off Assignable typed constants and then rebuild. Any code
which makes use of the feature will throw an error. You may then
either recode, or as is likely in a large project, locally re-enable
the option and enter a TODO item you can easily search on later.
Even without a TODO, however, such entries are easily found by
searching on the conditional.

Writeable Constants and Evolution


The writeable constant came to us from Turbo Pascal, if memory
serves. I no longer possess all the manuals, and am unable to cite
44 4 Beginning

a version. In the past, they were writeable by default. In recent


releases, they are disabled by default. I strongly recommend
they be disabled, and that you deal with them pragmatically
to reach a buildable state. You may find it necessary to enable
them locally in some cases, because of the effects on code
which is too tangled to be dealt with immediately. Do so, and
observe that those local declarations make it a trivial task to
locate the troublesome elements later. In the end, you should
consider writeable constants to be no more welcome than
global variables.

A workable strategy for short-term improvement may be to create


a local variable and initialize it from the global constant. This will
avoid persistent state change, and will narrow the scope of the
problem.

4.4 Scope in General

Scope is one of the details most abused in legacy code. The use of
global variables is by now well understood to be a Bad ThingTM , but
what is often overlooked is that making public things which need
not be is equally bad. Make things private as a first determination.
Increase scope only as necessary, and keep the public interface of
each module as thin as you can.
Within each unit, you have multiple scope declarations available:
▶ private
▶ protected
▶ public
▶ published
In more recent versions, you will also see:
▶ strict private
▶ strict protected

There is also default scope, which is the scope of components


on the form and of event handlers. Default is the same scope as
public, though not declared in that way.
Class helpers have some unusual characteristics, in that they act to
extend an existing class, but cannot add field variables. In a way,
they access the existing class(es) as though they were contained
4.4 Scope in General 45

Class helpers can access private and


in the same code unit, which provides access to class members in strict private members of the class
they help, but not in Delphi 10.1 Berlin
restricted scope, and that makes it possible to add functionality or later, where this access was con-
to even components, without altering the component code. In sidered a compiler defect. This “de-
particular, a class helper is entirely different than a partial class. fect” view is at odds with a com-
Class helpers: ment from Allen Bauer (then Chief
Scientist at Embarcadero) in 2009
▶ Can add functionality. on https://stackoverflow.com/
questions/1516493/difference-
▶ Give the appearance of being part of the class.
between- strict- private- and-
▶ A class helper can introduce class fields, but no instance protected-access-modifiers-in-
fields. delphi Stack Overflow, in which he
▶ Operator overloading is not supported. asserted that “the access rules imple-
ment implicit ‘friendship’ at the unit
▶ A class can have only one helper. In later versions of Delphi, level without adding explicit syntax
this becomes very important, as there are now class and to do so.”
record helpers in the Delphi libraries, such as for the string
type.
You can cheat, with care. Only one
In a form, the controls are declared in the default scope, which is helper for a class is in scope at
any time, so it is possible to play
public, but not the same as the public scope declaration. Published games with this, but it can’t be rec-
is also public, but in the context of a VCL component, published ommended.
properties appear in the Object Inspector , while public properties
do not. In addition, published properties which are altered from
their default state are saved in the form.
Private members are not accessible from outside the unit, but
the private members of a class are accessible from another class
declared in the same unit. This is roughly the same as the “friend”
relationship in C++. Strict private members are not accessible
outside the class, even from another class in the unit.
You are also able to declare constants inside a class specifier. Al- The attraction to declaring constants
though this may seem to make some sense in certain circumstances, in a class is obviously to limit their
scope. Declaring them instead in
the effect of a constant declaration inside the class specification is the implementation section of the
no different to a constant declared in the implementation section of module will be equally effective in
the unit. If you have more than one class in the module, then you many cases.
may see a greater value to using constants inside the classes, but as
noted, some versions of Delphi, have a defect which confuses code
navigation in a class. In Delphi 2007, for example, if you declare one
or more private constants in a class, then below that declaration
the Ctrl-Shift-Up/Dn hotkeys for moving between declaration and
implementation will not function. Better to declare the constants
in the implementation section of the unit.
Protected members are not directly accessible outside the unit, but
are visible to descendants of the class, even in other units. Strict
46 4 Beginning

protected blocks this “friend” access, but is otherwise the same as


protected.

Public members, as the name implies, are visible inside and outside
the unit.
In recent versions of Delphi a class declaration can also contain
embedded class declarations. Whether this is a better approach
than declaring separately the class that will be a member is a
decision for the developer.

4.4.1 Forms vs. Units

Every component on a form is declared in the form class in default


scope. Any unit which accesses a form can access any default or
public member of the class. Forms in legacy code often contain
business logic, and should not. Forms are not easily tested, and
such testing as is done on them is hit or miss.
There are commercial tools which facilitate automated testing
through the UI of an application. They are expensive, they are
slow, and maintenance of test scripts may require a larger staff
than you employ for development.
[2]: Bucknall (2005), ‘Creating Easily Years ago, in The Delphi Magazine, Julian Bucknall[2] presented an
Testable User Interfaces’ approach to making forms testable. The technique is complicated,
and would make form design much more tedious than it is now,
though the benefit is testability of a form in DUnit. An additional
benefit is that this technique really encourages the developer to
minimize code in event handlers. Moreover, as it is necessary
to create interfaces for the components, it will also encourage a
reduction in your component inventory.

4.4.2 Proper to Forms

Forms must obviously contain all of the visual components needed


to implement their role. They should, however, contain as little
The issue is testability. Forms testing code as possible. The event handler on a button should be as brief
requires scripting, and coverage is as possible: One line is a nice goal, more than six is an indication
problematic. Code in separate units
can be designed to be testable, reduc-
to try harder.
ing maintenance issues.
You may have forms in which the state of some controls is inter-
locked with the state of others, as often happens with checkbox
4.4 Scope in General 47

options. The logic for those behaviors is specific to the form, and
should be implemented there. However, if that logic requires
complex functions, those should be separated, not written in the
form.

Separation of Concerns in Forms

An ideal design would be one in which business logic and


data manipulation occur in separate modules which have been
designed to be unit testable. The logic attached to form elements
then would be as thin as possible. Consequently, in testing the
user interface, the focus reduces to ensuring that the controls
trigger the desired actions, not whether the data have been
correctly processed. Such manipulations have already been
unit tested. UI testing then will mean finding cases where
perhaps a control has triggered the wrong action altogether.

An example of separation, however, would be the OnGetText event


handler of a grid, where it may be desirable to convert display
values, based on value. Although the event handler clearly will be
on the form, the coding for the conversion—which may well recur
on other grids and forms—should not be there. Rather, it ought
to exist in a testable module. The value of this separation is not
simply based on the amount of code in the event handler, but on
the likelihood that the routine will be needed in multiple forms.
Always prefer separation of logic over repetition of code.
Business logic https://en.wikipedia.org/wiki/Business
_logic is generally understood to encompass the code which is
interactive with data manipulation, major application functionality,
and the like. Such logic should be placed in a unit apart from the
form, so that it can be unit-tested. Business logic is also not to be
confused with data management. You may find that your forms
call upon one or more units of business logic, and one or more
data modules. This division of labor will increase testability and
improve maintainability.

4.4.3 Proper to Class Units

Class units should contain all of the business logic in an application.


They should be designed to be testable, ideally without reference
to many other units. That last can be a challenge, but it is one
48 4 Beginning

which comes with a high reward, as maintenance is much simpler


This is not intended to suggest that all when the code is as clean and simple as possible.
code should be in classes; procedural
code still has value. In structuring your business logic, you will do well to make the
greatest possible use of Delphi libraries, in preference to coding
your own functions. Benefits of libraries include:
▶ Less coding labor.
▶ Library routines are generally well tested and stable.
▶ Your focus and labor remain on your application.

You should also be thinking always in terms of refactoring, and


extracting common subroutines and classes which may be used
to advantage elsewhere in the application. Any time you need to
rework a routine, you should again consider refactoring. Good
code degrades over time, not on its own, but through careless
rework. The only possible tactic for avoidance of code decay is
constant vigilance, and a commitment to cleanup when poor code
is observed.

4.4.4 Proper to Data Modules

Data Modules are more than simply class units, and less than forms.
They stand between those levels in providing non-visual services,
yet being containers for statically committed components. They
may reasonably contain business logic, though it will normally
be present because it is related directly to the datasets which are
contained in the module.
Data modules can be tested, if the datasets are loaded, usually
from CSV or XML test data. This is highly advantageous, as actual
databases in a development shop tend to be corrupted often and
badly.

4.4.5 Everything Private!

Yes, the point was made above, but it cannot be sufficiently stressed.
Narrow scope is a contributor to good design, and wide scope is an
invitation to defects, as well as to maintenance challenges.
It is not possible for literally everything to be private, but nothing
should be exposed by any module unless there is a clear reason to
do so. When you add a method or a field variable, your default
4.4 Scope in General 49

assumption should be to keep it private. Certainly some elements


must be public, but it is also worth considering, when the public
interface of a class is large, whether that may be an indication of
the need for refactoring.
A class should have a single duty to perform. When the class
interface grows large, you probably have included operations
which should be separated into other classes or procedures. The
motivations for refactoring are then to reduce the complexity of
the class, and to support code reuse. The narrower the focus of any
class, the more likely you are to be able to reuse it effectively.

A classic example of things which


4.4.6 Global not Always Bad ought to be globally defined would
be:

Global variables are best avoided, because they put the program ▶ CR = #13;
▶ LF = #10;
state at risk. Better by far to use Field members in classes, which
▶ CRLF = #13#10;
are distinct between class instances.
In particular, any constant which is
Global types and constants, on the other hand, raise no such issues. referenced in multiple units should
In particular, it will be better to declare a global type—and use it— be declared in only one.

than to naively declare a local type which is then not assignment


compatible with another structurally identical variable.
The larger the application, the more you may wish to consider
creating categorized units of types and of constants. New members
of your team will find it easier to locate what they need than in a
single file of types and constants.

4.4.7 Partitioning

You may have modules of your own in support of operations on


strings, dates, times, and datasets. At times, there will be a need
to create some new routine which reasonably could be put in
either of two such modules. When such ambiguities arise, you will
need to make the determination on the basis of minimizing unit
interdependency. In other words, if it could be in data utilities or
string utilities, but placing it in string utilities necessitates adding
a data utilities module to the uses clause, and putting it in data
utilities does not, then prefer the latter. Failing to consider such
a seemingly minor consideration is a step on the road to unit
dependency cycles.
50 4 Beginning

If you find yourself dealing with large uses clauses, and if you have
long build times, then you should be considering that you have par-
Large uses clauses and an excessive titioning issues. These go hand in hand. Routines wrongly placed
number of Unit Dependency Cycles are lead to dependencies which ought not to exist. Getting things into
bad because they increase coupling.
Left unchecked, at some point you
the right classes and modules will lead to simplification.
will find that everything seems cou-
Also, you should note that the Unit Dependency Cycles problem
pled to everything. That puts you in
a strait-jacket. tends to increase exponentially. The exponent may be small, but
the ever increasing build times, like compound interest, will kill
you. It is not only the degraded build times, but the complexity
of code which Unit Dependency Cycles create. Such cycles are
manifestations of tight coupling, and they interfere with your
ability to separate things, as well. You will find yourself looking
for the end of the thread to pull, and it may be very difficult to
recognize. Moreover, there are likely to be many such threads to be
found. It is a tedious but essential task which is best accomplished
at the earliest opportunity. And repeatedly.
Delphi Unit Dependency Scanner can emit a CSV file of information
on the unit relations suitable for use by the open source Gephi
(https://gephi.org) tool. A graph of one of my small projects is
here:

Figure 4.2: Graph of units in a small


project

If that were typical, there would be no need for this book. However,
here is a small fragment of edges (connections) from a large legacy
project:

Figure 4.3: Graph subsection in


legacy code

That is far closer to typical, and it’s what we are fighting to


overcome. The complete graph, which does little to help us, looks
like this:
4.5 Exception Handling 51

Figure 4.4: Graph of legacy project

4.5 Exception Handling

Delphi has provided exception handling ever since the first release.
And yet, it is all too common to find in legacy code a variety of
offenses against exception handling logic. Consider these general
priorities for the benefits of exception handling:
▶ Prevent application crashes.
▶ Provide users with useful, comprehensible error messages.
▶ Provide the developers with useful information for defect
repair.
None of these can be fulfilled unless the exception handling of
Delphi is properly understood and used. Let me emphasize here
the value of useful, comprehensible error messages. All too often, the
messages put in front of users are of almost no value.

4.5.1 A Simple Example

Before looking at the misuse of exception handling, let’s look at a


very simple example. As it is quite natural to consider try/except
and try/finally in the same discussion, this example will include
both.

procedure TfrmMain . Button1Click ( Sender : TObject );


var
sl : TStringList ;
begin
try
try
sl . CommaText := ’s ’;
ShowMessage ( ’ This won ’’t show ! ’);
except on E: Exception do
52 4 Beginning

end ;
finally
ShowMessage ( ’In finally ’);
end ;
end ;

When you run this tiny application and click the button, the
assignment to sl.CommaText will throw an exception, as we have
not yet instantiated the object sl. Click Continue on the exception
dialog, and you will then see the message presented by the finally
clause. Execution of the first call to ShowMessage is blocked by the
exception handler; any code following an exception up to the
except clause itself, will not execute.

This tiny example illustrates two important behaviors in Delphi


coding:
▶ Exception handlers safely capture operations which are
dangerous to your application. They provide you the op-
portunity to do something to overcome the problem — in
the best case – or at least to communicate to the user in
comprehensible terms — in the worst case.
▶ When an exception is trapped, the code flow is altered, and
the code following the exception does not execute. With an
exception handler in place, however, you limit the effects
of the problem, and as in this example, the containing
try/finally is still operative.

Both of these behaviors are critical to good coding. Blocking an


action which cannot succeed is obviously a benefit, but the execu-
tion of the finally clause allows for orderly recovery, including
the freeing of objects which could otherwise contribute memory
leaks.

4.5.2 Exceptional Failures

If the worst case is in failing to guard against potential exceptions,


then this is a close second:

procedure TForm1 . FixAll ( const AString : string );


var
s: string ;
sl : TStringList ;
begin
4.5 Exception Handling 53

sl := TStringList . Create ;
try
sl . Text := AString ;
for s in sl do
begin
// ... do work here
end ;
Note that the empty finally clause
finally
is a coding failure unto itself. In this
end ; instance, because an object was not
end ; freed. But an empty finally clause
is always a code smell.
You may object that this is obviously incorrect, and so it is, but
such errors have been seen in production code, and they may lie
in your path, as well. It is easily corrected:

procedure TForm1 . FixAll ( const AString : string );


var
s : string ;
sl : TStringList ;
begin
sl := TStringList . Create ;
try
sl . Text := AString ;
for s in sl do
begin
// ... do work here
end ;
finally
sl . Free ;
end ;
end ;

Another similar problem—a twin to the empty finally clause— As Nick Hodges points out in his
occurs when a call is made to some system which is assumed to first book: ”Very often, the code in
the try block raises an error that isn’t
have been initialized, but may not have been. The right action easily found, and rather than do the
would be to analyze the program flow enough to make certain it hard work of actually tracking down
is always initialized before use. The code smell here is the empty the error, the programmer will take
except clause[10]: the easy way out and just eat the ex-
ception.” I quote Nick here because
procedure TForm1 . SendIt ( EMailObject : TEmailObject ); I agree completely with his assess-
ment.
begin
[10]: Hodges (2014), Coding in Delphi
try
MyMailer . Send ( EMailObject );
except
end ;
end ;
54 4 Beginning

Not only is an empty exception han- This code simply hides the exception when it occurs. The user
dler a coding evil, but an exception is not notified, even though the expected result will not occur.
handler ought not to be coded at such
a high level—the place to catch an
This is lazy, and is often seen in connection with code where the
exception is as near as possible to its developer may not properly understand the possible errors.
source.
This is better, but not hugely so:

procedure TForm1 . SendIt ( EMailObject : TEmailObject );


begin
try
MyMailer . Send ( EMailObject );
except
on E: Exception do
ELogMailError (E. ClassName , E. Message );
end ;
end ;

The problem here is that this code collects and reports any excep-
tion thrown in this scope, and in doing so, swallows the exception.
A much more useful approach is to trap the exception(s) expected
in the context of the operation:

procedure TForm1 . SendIt ( EMailObject : TEmailObject );


begin
try
MyMailer . Send ( EMailObject );
except
on E: EDivByZero do
ELogMailError ( ’ Div by zero error in : ’ +
E. ClassName , E. Message );
end ;
end ;

Now if your exception handler did not find the exception you
specified, you need to handle that case, as you may have other
issues to resolve. The first thing to do is to modify your exception
handler to re-raise the exception:

procedure TForm1 . SendIt ( EMailObject : TEmailObject );


begin
try
MyMailer . Send ( EMailObject );
except
on E: EDivByZero do
ELogMailError ( ’ Div by zero error in : ’ +
E. ClassName , E. Message );
4.5 Exception Handling 55

raise ;
end ;
// If no exception was trapped , then any code
// here will execute .
end ;

If an unexpected exception occurs, the user will be faced with the


very unfriendly default message Delphi provides, but that is far
better than silently eating the exception. Ultimately, you should
protect your users from all default exception messages, but if there
is a possibility you have overlooked, this approach does not hide
the problem.

4.5.3 Exceptional Confusion

Another common misunderstanding is the mechanics of possible


exceptions with multiple object instances. The basic try/finally
pattern is clear enough, but with multiple objects, this is a common
error:

procedure TForm1 . MultiError ;


begin
slOne := TStringList . Create ;
slTwo := TStringList . Create ;
slThree := TStringList . Create ;
try
// working code snipped for clarity
finally
slThree . Free ;
slTwo . Free ;
slOne . Free ;
end ;
end ;

Should there have been an error in creating slTwo or slThree,


an exception would be thrown, and the entire try/finally block
would be skipped. The canonical solution is to use a try/finally
block for each object:

procedure TForm1 . MultiError ;


begin
slOne := TStringList . Create ;
try
slTwo := TStringList . Create ;
56 4 Beginning

try
slThree := TStringList . Create ;
try
// working code here
finally
slThree . Free ;
end ;
finally
slTwo . Free ;
end ;
finally
slOne . Free ;
end ;
end ;

The more objects you must create, of course, the worse this gets.
An alternative form is possible, as shown here:

procedure TForm1 . MultiError ;


begin
slOne := nil ;
slTwo := nil ;
slThree := nil ;
try
slOne := TStringList . Create ;
slTwo := TStringList . Create ;
slThree := TStringList . Create ;
// working code here
finally
slThree . Free ;
slTwo . Free ;
slOne . Free ;
end ;
end ;

That looks odd to me, and I prefer this form:

procedure TForm1 . MultiError ;


begin
slThree := nil ;
slTwo := nil ;
slOne := TStringList . Create ;
try
slTwo := TStringList . Create ;
slThree := TStringList . Create ;
// working code here
4.5 Exception Handling 57

finally
slThree . Free ;
slTwo . Free ;
slOne . Free ;
end ;
end ;

However, in using this form, it is essential to recognize that if


slThree or slTwo throws an exception when destroyed, the rest of
the finally clause will not execute, resulting in memory leakage.
It is a cardinal rule in Delphi that a destructor is not to throw an
exception as that will always result in memory leakage. You can
rely on it, when using objects from the Delphi classes, but in your
own code, you must be especially vigilant about your destructors,
if you wish to use this form.
Even small leaks lead to memory fragmentation and other issues,
so you must strive to avoid them.

Spring4D Offers an Alternative

Spring4D is an excellent library https://bitbucket.org/sgli


enke/spring4d/src/master/ worth your attention. It requires
you be on Delphi 2010 or later. Consider:
var
sl1 : IShared < TStringList >;
sl2 : IShared < TStringList >;
sl3 : IShared < TStringList >;
begin
sl1 := Shared . New ( TStringList . Create );
sl2 := Shared . New ( TStringList . Create );
sl3 := Shared . New ( TStringList . Create );
sl1 . CommaText := ’A ,B ,C ,D ’;
// DoSomethingWith ( sl1 , sl2 , sl3 );
end ;

No need for try/finally, and yet the objects are disposed when
they go out of scope.

4.5.4 Scope of Exception Handlers

A critical aspect of good exception handling is to keep the scope Hopefully you will notice I keep re-
as narrow as possible. In practice this means putting the exception peating this advice.
58 4 Beginning

handler as close as possible to the source of the exception thrown.


Often when we find an empty except clause, it is there in part
because the code within is not directly involved in the problem.
This occurs when a developer doesn’t bother to find the real
issue, but simply applies an empty exception handler to hide the
problem.
While you are in the area, also consider whether there could be
other possible exceptions thrown. If so, add clauses to handle
those cases as well.

4.5.5 Default Exception Handlers

If you do not deal with the exceptions possible in your application,


Delphi will. But Delphi will assault your user with a technical
message which the user will not understand. This reflects badly
on you, as the developer, so you should try never to let a default
exception handler be visible to your users.
In practice, this means that you must be diligent in discovering
not only the cause of any given exception, but the full range of
possible exceptions an action might trigger. This can be hard work,
but your users will be much happier not being bombarded by
indecipherable jargon.
If you do not already, consider adding to your project one of the
excellent tools for collecting information from exceptions, such
as EurekaLog (https://www.eurekalog.com/) or MadExcept
(http://www.madshi.net).

4.6 Code Formatting

In many legacy projects, there will have been a long trail of different
developers, each of them leaving footprints in the code. One of
the more obvious results is an incoherent collection of formatting
styles. Arguments are offered for leaving it as it is:
▶ Reformatting makes a mess of source control.
▶ Formatting is only cosmetic, not functional.
▶ Avoids endless debates over what format is the “right” one.
▶ Reformatting offers no return on investment.
4.6 Code Formatting 59

Reformatting makes a mess of source control. Reformatting does


nothing to source control. What it does is to introduce some
challenges in later work where manual merges may be important.
One way to reduce such complexity is to copy the old module(s)
to a new folder and reformat them there, to be used subsequently
in merge operations. When the work is complete, those temporary
files can be discarded. It is more work than some alternatives, but
our goal of clean code will require real work.
Keep in mind that it will be very difficult to undertake major
refactoring in a production branch of code. That being true, you
may as well create a new repository, copy the code, and spend less
time worrying over the source control impact. After all, you are
taking on this work to make major improvements, so why accept
arbitrary limitations? Once you have separated code from forms,
the comparison to old code is simply not going to work as it does
across branches in your existing repository.

Two Stage Merge Strategy

Given that your existing project is in production and is being


maintained during your new project work, and if you follow
my suggestions, you will have these repositories:
▶ Production
▶ Pre-stage
▶ New version

The simplest approach to merging defects will then be to use


your favorite Diff tool to identify what has changes in pro-
duction compared to pre-stage. Then you will have a modest
collection of repairs to make in your new code. There will be
modules where you may be able to diff production against
your new code, but even then, you must be mindful to avoid in-
advertently changing any difference which should be retained.

Formatting is only cosmetic, not functional. This is true only to the


extent that it makes no difference to the compiler. It is utterly false
when considered with regard to code readability, and inconsistency
in formatting also tends to mask coding errors which would
otherwise be readily apparent to skilled developers.
Avoids endless debates over what format is the “right” one. Only true if
there is no leader, no ultimate arbiter of project policy. Discussion
60 4 Beginning

on the way to a decision is worthwhile, and will also help to


gain buy-in from the development team, but there comes a time
when the discussion is complete. Unanimity is unlikely, but if the
discussions were well managed and resulted in some degree of
adjustment, the results should still be well received.
Reformatting offers no return on investment. A short-sighted view
which overlooks the practical issue of developer productivity.
Odd formatting casts a veil over the code, making it less readily
comprehensible.

4.6.1 Formatting Matters

Long routines tend to hide or ob- Well crafted code presents patterns throughout. The human mind
fuscate patterns. The clutter in such is probably the ultimate pattern recognition tool, and is much more
routines is white noise to the mind.
effective when the patterns are consistently used. At some point,
the consistency achieved becomes transparent to the developer
reading the code, and though a particular developer may be new
to that code, it will be more comprehensible for being written
consistent with the practice in other modules.
Some will argue in favor of permitting this form:

if SomeCondition then DoThis else DoThat ;

But it is a short step from that rationale to this:

for i := 0 to 9 do ActionRoutine (i);

While the former may not seem objectionable, the latter has a
horrible stench. But even the former small example should be
disallowed, as it makes debugging difficult. You can break on
the test, but to break on DoThis or DoThat will require placing
breakpoints inside those routines, and if they are in other modules,
that is fairly disruptive to the debug session. This is objectively
better:

if SomeCondition then
DoThis
else
DoThat ;
for i := 0 to 9 do
ActionRoutine (i);

You may disagree, but consider:


4.6 Code Formatting 61

Any fool can write code that a computer can under-


stand. Good programmers write code that humans
can understand. – Martin Fowler
An excellent starting point is the guide written long ago by Charles
Calvert, which can be found here: Object Pascal Style Guide (http: I find it silly to break lines now at
//www.sourceformat.com/coding-standard-delphi-borland. 80 characters. It made sense on 80
column screens, but we’re well past
htm). You may find you need to update the document, as newer that. And moreover, with the increas-
language elements are absent, and our screens are now larger, but ing use of qualified references, many
this will still be a solid foundation. lines are normally longer than 20 or
even 10 years ago.
Also keep in mind that the Delphi libraries are (mostly) formatted
based on the Calvert document:

// StrLenLimit : Scan Src


// for a null terminator up to MaxLen bytes
function StrLenLimit ( Src : PWideChar ;
MaxLen : Cardinal ): Cardinal ; overload ;
begin
if Src = nil then
begin
Result := 0;
Exit ;
end ;
Result := MaxLen ;
while ( Src ^ <> #0) and ( Result > 0) do
begin
Inc ( Src );
Dec ( Result );
end ;
Result := MaxLen - Result ;
end ;

There are those who will argue for a slightly different approach:

// StrLenLimit : Scan Src


// for a null terminator up to MaxLen bytes
function StrLenLimit ( Src : PWideChar ;
MaxLen : Cardinal ): Cardinal ; overload ;
begin
if Src = nil then begin
Result := 0;
Exit ;
end ;
Result := MaxLen ;
while ( Src ^ <> #0) and ( Result > 0) do begin
Inc ( Src );
62 4 Beginning

Dec ( Result );
end ;
Result := MaxLen - Result ;
end ;

I am not a fan of this style, both because I find it less readable, and
because the effect on the structural highlighting of either CnPack
or Castalia is ugly. An argument often advanced in support of this
Long routines are a code smell! They style is that it takes fewer lines. Although that is true, it is a minor
should be on your priority list for gain, and if your code suffers from large routines, the benefit is
refactoring.
very small; better to refactor.
The sample is taken from Delphi XE, and might now be written
thus:

// StrLenLimit : Scan Src


// for a null terminator up to MaxLen bytes
function StrLenLimit ( Src : PWideChar ;
MaxLen : Cardinal ): Cardinal ; overload ;
begin
if Src = nil then
Exit (0) ;
Result := MaxLen ;
while ( Src ^ <> #0) and ( Result > 0) do
begin
Inc ( Src );
Dec ( Result );
end ;
Result := MaxLen - Result ;
end ;

Some may also point out that the Delphi libraries do not always
follow the layouts I recommend, nor those documented by Charlie
Calvert:

function TrimLeft ( const S: string ): string ;


var
I , L: Integer ;
begin
L := Length (S);
I := 1;
while (I <= L) and (S[I] <= ’ ’) do Inc (I);
if I = 1 then Exit (S);
Result := Copy (S , I , Maxint );
end ;
4.6 Code Formatting 63

function TrimRight ( const S: string ): string ;


var
I : Integer ;
begin
I := Length (S);
if ( I > 0) and (S[I] > ’ ’) then Exit (S);
while ( I > 0) and (S[I] <= ’ ’) do Dec (I);
Result := Copy (S , 1, I);
end ;

It’s a fair point which still does not diminish the arguments
I advance in support of more consistent formatting. It is also
reasonable to imagine that work has been done in the libraries by
developers whose skills were rather less than those tasked with
compiler or IDE design.
Be kind to those who follow in your footsteps:
Code is read much more often than it is written, so
plan accordingly. – Raymond Chen

4.6.2 Manageable Formatting

Delphi contains a code formatter, as do CnPack and GExperts; MMX


is not a formatting tool, as such, but does offer some formatting
tools. All are competent, and will bring a degree of consistency to
code presentation. Neither CnPack nor GExperts them alters code
structure outside of a routine; MMX can reorder members of a
class.
MMX formatting offers many options. It also offers class sorting,
which can be applied to achieve a consistent ordering of code in
the modules. Here again, there are options which can be applied
to the sorting, so you need not accept the default alphabetical
sorting. This capability will again cause some to raise concerns
over source control, but if these are valid arguments against the
practice, they are equally valid against refactoring or any other
significant changes to code.
Whatever tool is used, you will need to achieve a “standard” set of
options. When this is done, it is also easily possible for a developer
who strongly prefers a different formatting to reformat before
doing his work, then to reformat before check-in, to return to
64 4 Beginning

standard form. It requires more effort, and the simplest way to


avoid that is to accept the local standard.

4.6.3 Minor Formatting

There are some very simple formatting operations which can be


beneficial. One of these is to sort your classes. MMX makes this easy,
and there are options available, though the simple alphabetical
sort seems very effective, and is the default the Delphi IDE imposes,
until you interfere with it.

Formatting by Default

When you create a unit, Delphi structures things in an orderly


fashion. By default, routines are kept in alphabetical order, but
if you relocate one, then new routines are simply appended
to the end of the module, and eventually you have a mess. At
least it seems to me to be a mess.

Another is to use the GExperts Ctrl-Alt-Z alignment tool. This


makes it very easy to align on the punctuation, such as :=, to
effectively create columns in your code. It reduces the sense of
clutter, and presents a more readable code, albeit at the expense of
making searches somewhat more difficult unless you use grep.
Digging Into the Challenges
Comments 5
Searching online regarding comments, you will inevitably find 5.1 Useless Comments . . . 67
rationales for why good code needs no comments. Sorry, no sale. 5.2 Annoying Comments . 67
5.3 Thoughtless Comments 68
5.4 Essential Comments . . 69
5.1 Useless Comments

Working in legacy projects, you will often see useless comments. Never comment the obvious. And
These are the sort of entries which lead people to rationalize that when in doubt, try first to make the
code obvious. If that fails, then use
comments are a waste of space and effort. comments.
// Save the settings to a file
procedure SaveSettingsToFile ( AFileName : string );
begin
if not AFileName . IsEmpty then
begin
if FileExists ( AFileName ) then
DeleteFile ( AFileName );
// and so on ...
end ;
end ;

Clearly, the comment above the routine tells us nothing we would


not learn from the procedure name. To be charitable, some people
had been taught to comment each method, at the least, so lacking
any meaningful information to offer, they indulged in this sort of
nonsense.

5.2 Annoying Comments

A comment becomes annoying when it goes on at great length, yet


provides little or no real value. The annoyance is still greater when
(having skipped ahead in disgust) we later discover that deep
in the mass of verbosity, there was hidden one valuable datum.
Such discoveries often come after we have puzzled out the actual
operation of the too often spaghetti-coded routine.
68 5 Comments

The author of that mess, almost certainly now gone from the scene,
should have been taught to keep in mind that later developers
may well be psychopathic and vengeful.
Another source of annoyance comes from explanatory comments
which have been written not by the author of the code, but by
a successor who wrongly thought he had properly understood
the operation and need of the code. Having thus planted seeds
of confusion, the guilty party has probably been rewarded by
someone who observed the output without comprehending its
future cost.

5.3 Thoughtless Comments

There are so many examples of thoughtless comments which


spring to mind that it is hard to know where to begin. Rather than
bore you with trivial examples, I shall offer instead one of the most
thoughtless varieties: code blocks commented out.
The issue is not that a block of code has been removed from service,
but that so often the comment lacks some critical content:
▶ Who? The identity of the person who decided to comment
out the block. There’s no one to explain when no initials are
given.
▶ When? The date when the change was made. Or reference
to a specific repair case. Anything that might offer a context
in which to understand why the change was made.
▶ Why? Was the functionality obsolete? Was it not to have
been in this branch? Was it to be enabled later?
▶ Why not removed? The obvious remedy for unnecessary or
obsolete code is deletion. You are using source control, aren’t
you?
The point is that this kind of change wastes the time of those who
come later to the code. Time is rarely a surplus commodity.
Almost as thoughtless is a block commented out and dated, but
with no name, initials, or reason. And when the date is years in
the past, it beggars the imagination.
5.4 Essential Comments 69

Why Keep Dead Code?

We are all using source control systems—we are, aren’t we? So


why then would we keep massive chunks of code commented
out and carry then along, even for weeks, much less for years.
Yet it is not an uncommon practice.

5.4 Essential Comments

In spite of the categories above, there are many reasons to comment


your code:
▶ At the module level, there may be a need to explain the
motivation in terms of domain-specific knowledge. Many
applications serve specialized needs in domains which re-
quire particular knowledge, such as: medicine, atomic power,
or radio and television operations. Developers new to the
project will be well served by thoughtfully written orienta-
tion of this sort.
▶ At the method level, particular routines may also exist for
domain-specific reasons. Likewise, there may be details
of implementation which are dictated by the particular
requirements of the domain.
▶ Within a method, and in spite of well named routines, there
may still be reasons to explain why particular steps are
needed.
Comments should be written because they save the developer and
maintainer from having to research why things were done. Or how
to ensure that correct results will be delivered. In an application
with millions of lines of code, assuming that motivation for any
particular operation is obvious will simply lead to increased
maintenance issues in the future.
Cleaning Uses Clauses 6
Large legacy projects are likely to contain very large uses clauses, 6.1 Initialization . . . . . . 72
and though developers are quick to add to them as needed, 6.1.1 Independent Factors . 73
cleaning them is a different matter, that being a tedious job. In one 6.1.2 Initialization of Units . 73
project, however, cleaning the uses clauses produced a build time 6.2 Removing Dead and
reduction of about 8:1. When you consider that, the tedium of the Duplicate Units . . . . 80
effort for a few days seems minor. Also, this is a task you can begin 6.2.1 ProjectFiles Tool . . . . 80
without having to refactor code, create new modules, or anything 6.2.2 Other Useful Tools . . 83
else, except managing the initialization sequence. 6.3 Removing and Demot-
ing Unit References . 84

Unit Dependency Cycles and Build Time 6.4 Using the Tools . . . . 85
6.5 Unit Dependency
Regarding build time, remember: Cycles . . . . . . . . . . 87
▶ Unit Dependency Cycles do affect build time 6.6 Collecting the UDCs . 87
Note that unit dependency cycles are
▶ Your results may vary
a major factor in that project.
▶ To obtain a marked benefit, such as I did, would depend
on your project having a very large number of Unit
Although you can begin without
Dependency Cycles
refactoring, you will find as you pro-
ceed that some repairs cannot be ac-
The impact of UDCs on build time appears to be exponential,
complished without refactoring or
so although it is initially unnoticeable, once it reaches a certain rewriting. Code which was added
point, the degradation advances very noticeably. This makes into the wrong class or unit must
sense if you consider that as you add modules, and they sooner or later be transplanted.
reference others which cause UDCs, the effect is multiplicative.

If your project makes use of initialization and finalization The alternative would be to carefully
clauses in any modules, then altering uses clauses is likely to analyze all initialization actions in
the application, and establish how
trigger initialization sequence problems. With that in mind, a safe best to group them. And then deter-
sequence of actions would be: mine in what order the groups would
need to be applied. Worth doing, but
a big project.
72 6 Cleaning Uses Clauses

Log the current ini-


Log
tialization sequence.

Make initialization se-


Init
quence deterministic.

Remove Remove dead files.

Clean unneeded refer-


Clean
ences from uses clauses.

Reduce Reduce visibility in uses clauses.

6.1 Initialization

Initialization in legacy code is often a Pandora’s Box of disorgani-


zation. Usually, it will have evolved, independent of any sort of
design. As a starting point, you may wish to identify two groups:
Things which are independent of user identity, and those which
depend entirely on user identity (login.) The independent factors
can be addressed before the user enters ID and password.
If your project makes no use of global variables, be happy. In
most cases, though, you will have to deal with globals, and often
their initialization is chaotic. One possible approach is to create
a singleton class instance to contain the globals you are for now
unable to eliminate. Containing them in a class, you can also access
them through methods, hiding the field variables, and this will
facilitate debugging.
6.1 Initialization 73

6.1.1 Independent Factors

There is no way to enumerate all the things which might exist in


your application as global variables, but some may be:
▶ Windows directory
▶ System directory
▶ User Documents directory
▶ Registry keys
▶ INI file path
Certainly some of these could be handled with constants, but
consider the debugging issue. It is not unusual for a project to
contain multiple assignments to a global variable—after all, that’s
one of the evils of using them—so making access only through a
method is a way of discovering such accesses and tracing them.

6.1.2 Initialization of Units

If you have units which contain initialization and possibly finaliza-


tion sections, then you have another chore to complete. In recent
years, many have come to see those sections as a code smell, and
the conventional advice is to get rid of them. Why should these
sections be considered a smell? Consider:
▶ How can you know in what order they execute?
▶ If there are sequence dependencies among the units, then you
must know—and be able to control—the order of execution.
▶ If you need to debug, where will you put a breakpoint?
▶ If there are hundreds of modules with initialization sections,
how do you even begin?
▶ If your project(s) contain Unit Dependency Cycles, the order
of execution in such sections may change as you rework uses
clauses.
A workable strategy is to document the initialization sequence, You will break the sequence because
then take explicit control of it. Designing the problems out of you will be removing references from
large uses clauses, and that will al-
existence is a less urgent issue – the first order must be stabilization. ter the sequencing provided by the
Why? Because when you begin cleaning up unit dependencies in compiler. Far better to begin by log-
a large app, you will break execution. ging to document the sequence that now
works, and then ensure that sequence
In my own situation, I was working to reduce Unit Dependency is maintained.
Cycles in a large project. The work was proceeding well, and the
results more than justified the effort—until I did some testing, and
74 6 Cleaning Uses Clauses

found that I had broken the application. First, it was puzzling, as I


had not altered code. My changes were:
▶ Removed unnecessary unit names from unit clauses.
▶ Demoted used units to the implementation section, where
they were not needed for the interface.
I had altered no routines, but the changes to the uses clauses clearly
had changed something. A colleague was also testing the new build,
and found that there was a class being accessed which had not
been instantiated. This led to the discovery that the initialization
sequence had changed.
When you rely on the initialization and finalization sections to
manage things, you are leaving it to the compiler to determine the
sequence. That may be altogether reliable in most situations, but
an added twist was that in some initialization sections, there
was code which relied on objects from other units having been
initialized.
Pascal Analyzer offers a report of ini-
tialization order, but that has proved The first task was to learn the actual initialization section in
in my experience to be unreliable.
This may be a consequence of some
the most recently functional version of the code. That led me to
modules in their initialization blocks logging.
forcing the initialization of other
modules, an ad hoc “solution” which I added logging into each initialization section, to get a list of the
may have been added in the distant sequence, using conditional directives, it could be easily turned off.
past. Logging must be simple enough to function before the application
is initialized. Here is a very simple logger class:

Dalija Prasnikar kindly reviewed my unit u_Logger ;


original work on this logger, and
brought clarity and simplicity to it interface
that makes it a much more useful
demo. Her thoughtful feedback is uses
appreciated.
System . Classes , Winapi . Windows ;

type
TLogger = class
private
FLog : TStringList ;
FOutputFile : string ;
public
constructor Create ;
destructor Destroy ; override ;
procedure AddItem ( const AItem : string );
end ;
6.1 Initialization 75

var
Logger : TLogger ;

implementation

uses
Winapi . ShlObj , System . SysUtils , System . DateUtils ;

function GetSpecialFolderPath (
CSIDLFolder : Integer ): string ;
var
FilePath : array [0 .. MAX_PATH ] of char ;
begin
SHGetFolderPath (0 , CSIDLFolder , 0, 0, FilePath );
Result := FilePath ;
end ;

constructor TLogger . Create ;


var
sPath : string ;
begin
inherited ;
sPath := GetSpecialFolderPath ( CSIDL_PERSONAL );
FLog := TStringList . Create ;
FOutputFile :=
IncludeTrailingPathDelimiter ( sPath )+ ’ Log . txt ’;
end ;

destructor TLogger . Destroy ;


begin
if FileExists ( FOutputFile ) then
DeleteFile ( FOutputFile );
FLog . SaveToFile ( FOutputFile );
FLog . Free ;
inherited ;
end ;

procedure TLogger . AddItem ( const AItem : string );


var
lTime : string ;
begin
lTime := DateTimeToStr ( Now );
FLog . Add ( lTime + ’ ’ + AItem );
end ;

// For simplicity of the demo , the logger


76 6 Cleaning Uses Clauses

// is created and destroyed implicitly ,


// using the initialization and finalization .
initialization
Logger := TLogger . Create ;

finalization
Logger . Free ;

end .

This module is used in a simple logging demo application. First,


the DPR file:

program DemoLogger ;

uses
Vcl . Forms ,
u_Logger in ’ u_Logger . pas ’,
fMain in ’ fMain . pas ’ { frmMain } ,
u_Files in ’ u_Files . pas ’;

{ $R *. res }

begin
Application . Initialize ;
Application . MainFormOnTaskbar := True ;
Application . CreateForm ( TfrmMain , frmMain );
Application . Run ;
end .

The demo is artificial, of course, but should serve to illustrate the


utility.

unit fMain ;

interface

uses
Winapi . Windows , Winapi . Messages , System . SysUtils ,
System . Variants , System . Classes , Vcl . Graphics ,
Vcl . Controls , Vcl . Forms , Vcl . Dialogs ;

type
TfrmMain = class ( TForm )
public
end ;
6.1 Initialization 77

var
frmMain : TfrmMain ;

implementation

{ $R *. dfm }

uses
u_Logger ;

initialization
Logger . AddItem ( ’ frmMain initialization ’);

finalization
Logger . AddItem ( ’ frmMain finalization ’);

end .

A final unit continues the pattern:

unit u_Files ;

interface

implementation

uses
u_Logger ;

initialization
Logger . AddItem ( ’ u_Files initialization ’);

finalization
Logger . AddItem ( ’ u_Files finalization ’);

end .

If you must add logging to many units, then you may wish to use
a conditional:

initialization
{ $IFDEF LOG_INIT }
Logger . AddItem ( ’ frmMain Initialization ’);
{ $ENDIF }
// initialization code here
78 6 Cleaning Uses Clauses

finalization
{ $IFDEF LOG_INIT }
Logger . AddItem ( ’ frmMain Finalization ’) ;
{ $ENDIF }
// finalization code here

end .

When you run the demo, it will produce a log file with content
similar to this:

2020-10-13 17:00:40 frmMain initialization


2020-10-13 17:00:40 u_Files initialization
2020-10-13 17:01:11 u_Files finalization
2020-10-13 17:01:11 frmMain finalization

Nothing too exciting, but if you are working on a project with over
100 units which contain initialization sections, this can quickly give
you a reliable knowledge of the sequence in which the initialization
occurs.
In the next round, edit each of your project units, putting the code
The names are up to you, but these from initialization into a procedure called InitializeUnit, and
seem a reasonable choice. if there was a finalization section, put that code into a procedure
called FinalizeUnit. Comment out the original initialization and
finalization sections, or remove them.
Finally add a unit to the project—InitMgr, perhaps—with two
public routines: Initialize and Finalize. In the Initialize routine,
put calls to the InitializeUnit procedures, ordered according to
the sequence you acquired by logging. In the Finalize procedure,
put the calls to FinalizeUnit, in reverse order to their sequence in
Initialize. In each case, the calls are qualified by the unit names.
All of the units involved are added to the implementation uses
clause. An example of the Initialize code is:

procedure Initialize ;
begin
MainData . InitializeUnit ;
COMInterface . InitializeUnit ;
// and so on
end ;

procedure Finalize ;
begin
6.1 Initialization 79

COMInterface . FinalizeUnit ;
MainData . FinalizeUnit ;
end ;

In the DPR, add calls to InitMgr.Initialize and InitMgr.Finalize.


At this point, you have achieved a deterministic initialization
sequence, and your coming changes to the uses clauses will not
cause initialization problems.
The ordering in Finalize may not be critical, but in a large project, I could have applied the finalization
you can’t assume that. I chose to reverse the initialization order order from the log, but if it differed
from a reversal of initialization or-
in the finalization section, and hope for the best. Had any errors der, then it would be necessary to
occurred—they did not—then it would have been necessary to determine why. Instead, the reversal
debug the problems. becomes a policy after it proves ef-
fective, and obviates future logging
Note that you will probably want to eliminate some of these requirements.
initialization sections, as the code in them may be trivial; you may
elect to code all those little actions into a single routine in InitMgr,
and thereby reduce the code smell. If your project is large, you
may prefer to keep to a narrow set of actions in each file, which
though tedious, will be more rapidly concluded than if you are
making judgments in each module.
As I said at the outset, the logger here is very simple; its creation
was originally motivated by the need to document the initialization
order in a large project which had become quite fragile. I needed
only to obtain the sequence, and to minimally disturb the already
fragile code. Given its simplicity, it will not find wide use, but you
may find reasons to embellish it a little.
There are numerous tools available for more comprehensive log-
ging requirements. I can strongly recommend CodeSite based on
my own experience, but there are also open source logging tools
available. At the time I was resolving these problems, the state
of the project prevented CodeSite from running. So as always,
necessity was the mother of invention.
In the long term you will likely want to begin restructuring
how initialization is managed. At the very least, it will be good
to establish where various actions should be coded. Some will
be independent of user login, for example, while others must
wait until login or after. Categorizing the actions and collecting
them logically can be done gradually, and will help simplify
maintenance.
80 6 Cleaning Uses Clauses

6.2 Removing Dead and Duplicate Units

It is likely that your project will contain modules which are no


longer contributors to the application executable. Or worse, you
may also find that you have a small collection of files which share
the same name, and are in different folders. Duplicated names are
especially troublesome, as you have the challenge of discovering
which one is actually in use.
An excellent first step to cleaning uses clauses is to remove the
dead files which are likely otherwise to become distractions to
the cleaning process. The tools below, for example, will happily
collect information for you on the various things you should do to
clean those dead files. Use the grep search in GExperts to collect
all the .PAS files in your project tree. Then compare the number
of modules found to the number of .DCU files produced in your
build. That will give you a count of the dead files.
If your project files are in a tree shared with other projects, life
will be complicated. Making changes to the uses clauses may
lead to breakage in some of the other projects. Unless you are
willing to clone a new directory tree to contain your work, you will
have to deal with collateral damage as it happens. My approach
would be to create a new tree, and remove from it all the units not
needed for the one project it is intended to support. When you have
accomplished that, create another tree for the next project. As you
discover the modules which are common to both, relocate those to
a tree which will contain only those shared modules. This will not
be accomplished overnight, but then, neither was the complexity
produced rapidly which you must now untangle.
Also note that MMX can produce a report of used units which
includes their explicit paths. This removes any doubt about which
in a set of duplicates may actually be in use.

6.2.1 ProjectFiles Tool

In the demo code for this book is a project called ProjectFiles. It


was built to make some of the cleanup work simpler than it might
otherwise be, and serves also to illustrate some useful approaches
in code.
6.2 Removing Dead and Duplicate Units 81

Build and run the tool, or run the pre-compiled executable, and
when the form opens, you will see four edit boxes at the top:

Figure 6.1: ProjectFiles Edit Boxes

Proceed as follows:
1. Click on the dialog button for the Project Folders and navi-
gate to the root folder of your project code, then click Select
Folder.
2. Click on the dialog button for the Map File and navigate to
the map file produced in your build. Select the file, and click
Open.
3. Click on the dialog button for the DCU Folder and navigate
to the folder containing your DCU files, then click Select
Folder.
4. Click on the dialog button for the Unit Report file, navigate
The Unit Report is produced by an
to it and click Open. MMX operation. If you are unfa-
miliar with it, see the discussion of
The order of use of the buttons is seen here: MMX, below.

Figure 6.2: ProjectFiles Edit Boxes -


Order

Operational sequence is important, as some of the later actions


depend on earlier results. Once the folders and files have been
selected, you will see something like this:

Figure 6.3: ProjectFiles Member Files

This is a simple listing which shows the modules present in the


specified path. Somewhat more useful is the list of files extracted
from the project map file, as these are actually involved in the
82 6 Cleaning Uses Clauses

application. However, the list includes all system and component


files which contribute:

Figure 6.4: ProjectFiles Mapped Files

Next is the list of DCU files produced in compiling the application:

Figure 6.5: ProjectFiles DCU Files

The next collection is of immediate value, as it presents the files


known to be unused, or dead:

Figure 6.6: ProjectFiles Dead Files

And finally, the list of duplicate files, if any. These would be high
on my priority list for cleanup.

Figure 6.7: ProjectFiles Duplicate


Files

This example is artificial, as I added a folder with a copy of a file


to illustrate it. Notice, however, that the Used column contains
6.2 Removing Dead and Duplicate Units 83

checkboxes, and the one which is checked is active, while the other
can be removed.
Although this looks simple in a small project, imagine a few
thousand modules in a large project, and the value of this simple
tool is more apparent. Further, the tool aids in removal of the dead
modules:

Figure 6.8: ProjectFiles Dead Files


Removal

And also in the removal of the duplicates:

Figure 6.9: ProjectFiles Duplicate


Files Removal

As you can see, files may be tagged and removed. But in addition,
for the sake of a large project, you are also able to export the lists
to CSV files, for import to MS Excel .

6.2.2 Other Useful Tools

CnPack provides the CnPack Uses Cleaner, which is quite good for
removing unneeded references. It is not perfect, and does suggest
removals from the interface uses clause which are not valid. This
causes no difficulty, as the units are added in again by the IDE on
saving the file.
Pascal Analyzer, however, is perhaps the only available tool which
will tell you which units can be moved from the interface uses
clause to the implementation uses clause. And MMX makes the
work of demoting to implementation level painless. There does not
84 6 Cleaning Uses Clauses

appear to be any tool on the market which will automate the move-
ment of unit references from the interface to the implementation
section. This is not a simple issue, and Pascal Analyzer routinely
produces false positives. In particular:
▶ Class helpers are flagged as not needed.
▶ Math and StrUtils units will be flagged as unneeded if they
are referenced only for the IfThen() functions.
▶ Unreliable in determining whether some Delphi libraries can
be removed or demoted to implementation.
▶ Unreliable in reporting unneeded component modules.

In spite of these issues—and you will soon recognize the false


positives in most cases—the CnPack Uses Cleaner is definitely worth
using.
MMX also provides a tool to reformat the uses clauses. The most
recent version supports only Delphi 10.x, and strips embedded
comments from the uses clauses. Older releases do not handle
a uses clause which contains comments or compiler directives,
so you must strip them manually before reformatting. In my
experience, comments embedded in uses clauses seem to have
little value, but incur a significant burden when you want to clean
things up.

6.3 Removing and Demoting Unit


References

At this point, you should be able to actually rework your uses


clauses without damage to your project. You will need to use the
suggested tools—or your own—as Delphi does not provide what
we need. Neither does any single tool discussed here, but each has
its value.
Removing references from uses clauses is an obvious benefit,
MMX helps greatly with demoting and will probably remove some of your dependency cycle issues.
references, as it has a keyboard short- Demoting references—moving them from the interface to the
cut for the purpose which moves the
reference without moving your cur-
implementation section—is less obviously a benefit, but is at least
sor. equally important.
In passing, you may get lucky, and find reason to make some
small refactoring which will significantly reduce Unit Dependency
Cycles . In one instance, I relocated a routine to another unit
6.4 Using the Tools 85

where it should have been, and reduced by nearly 800 cycles the
contribution of the unit it came from to the unit dependency cycle
total, but you will read more of refactoring later. Most gains will
be less spectacular, but you only need one such large improvement
to strengthen your determination.

6.4 Using the Tools

Run the Peganza Pascal Analyzer on your project. If you do not


have a license, and can’t quickly get one through your corporate
purchasing, then use the free Pascal Analyzer Lite version. Some
advice, however, is in order. If your project is large, you will want
the 64-bit version of the analyzer. And you will need plenty of
RAM—gigabytes—for it to do its work; if you work in a virtual
machine, then be sure to allocate sufficient RAM. On a large project,
I have seen Pascal Analyzer Lite consume more than 5GB of RAM.
At the beginning, my most recent project took a few hours to
analyze with the default selections. Before running the analysis,
use the settings to exclude your components and Delphi source
files. Have patience; the report from Pascal Analyzer will give you
plenty to do, so you are unlikely to run it even once a day.
Once the analysis is complete, you will need the Pascal Analyzer
uses report, from the Uses.txt file. Open it in your favorite editor,
and search on the > character. Units in the uses clauses which can
be removed will have the ==> sequence to their left. Units which
can be demoted to the implementation section will be prefixed by
-->. When there are many references to remove, you may prefer
to use the CnPack Uses Cleaner . For unit references which can be I found that in RDP operations, the
demoted, in Delphi, position the cursor in the unit name, then use default hotkeys for these operations
did not work, so I reassigned them
the MMX Ctrl-Alt-Shift-Up/Dn hot key. That avoids many steps. in MMX .
Save often, and compile often. Some references will be reinserted
when you save. Those were false positives. Some will also be In working through these improve-
reinserted in the interface uses clause after you demoted them. ments, you may find that Delphi be-
comes unstable, and needs to be
You will then need to remove the ones from the implementation killed and restarted.
uses clause. It sounds worse than it is, and after a few units, you
will become accustomed to which are false reports, and have less
cleanup to do.
An example of a uses report for a single module is here:

Program EnumConst uses:


86 6 Cleaning Uses Clauses

Used units:

System source not found


Forms source not found
fMain in implementation
==> FormLogic unnecessary
--> uInterfaced in implementation

As explained above, the FormLogic module can be removed, and


the uInterfaced module reference can move to the implementation
uses clause. As mentioned, the Pascal Analyzer report is not always
correct. In particular, it seems to think class helpers are not needed,
and often suggests the removal of some units from certain vendors
which the IDE then reinserts when you save. Still, after a bit of use,
you will recognize these false positives, and simply leave them
alone.
The CnPack Uses Cleaner is best applied to one unit at a time, or to
all units opened for editing. And you will need to add the current
units to the project, in order to clean them. But that is a Good
ThingTM , as you probably will find that reducing dependence on
the Search Path will help with IDE stability and performance.
Again, save often and compile often. These activities will stress the
Delphi IDE, and you may need to kill the process and run again.
The good news is that as you proceed, the instabilities will reduce.
You may also find that with a large number of Unit Dependency
Cycles, the time it takes to build your application increases with
Reduction of Unit Dependency Cycles each build action. Killing the IDE will “reset” your build time
will not only reduce the build time, because it will remove cached data, and in some cases it will be
but will reduce the tendency of the
build time to increase in any session.
faster to kill and reload Delphi than to wait for the build to finish.
This flattening of the curve is another
When you think you have completed this work, run the Pascal
measure of the improvements you
make. Analyzer again. You are likely to find more work to do in removing
units from uses clauses, and in demoting to implementation level.
Every action has consequences, and in this case, the first round
work will have made some other references in the uses clauses
unnecessary. Legacy cleanup is always an iterative process.
6.5 Unit Dependency Cycles 87

6.5 Unit Dependency Cycles

Rather than repeat the phrase endlessly, I shall refer to UDC when
discussing unit dependency cycles.
Here is the sequence of actions I recommend:
▶ Scan for and remove duplicate units. The MMX Uses Report
makes it easy to check which source files are actually in use,
which makes duplicate removal simpler.
▶ Run the MMX Unit Dependency Analyzer, and then save the
Cycles Report to a file. You may want to name it as a baseline
report, against which you can check your progress later. You
may wish to also use my simple Uses Analyzer, which will
parse the report and present cycle counts per module.
▶ Run the Peganza Pascal Analyzer tool against your project.
Save the reports to a folder for that purpose. Initially you
will use only the Uses.txt file from Pascal Analyzer, but you
will need to use it for some considerable time, depending
on the size of your project.

6.6 Collecting the UDCs

Open your project in the Delphi IDE. Build the project, and then
from the MMX menu, go to Tools–>Unit Dependencies.... You will
see this:

Figure 6.10: MMX Unit Dependen-


cies project dialog

Processing a large project will take some time. Minutes, not seconds.
Be patient.
88 6 Cleaning Uses Clauses

In a really large application, the reports produced inside MMX


inside the IDE may not give much insight. The problem is in
determining which modules actually create the troublesome cycles,
and which are simply along for the ride. Cleaning the uses clauses
seems to me to be the logical first step, but you may wish to
remove dead units first. Any units which are not contributing to the
executable are a noisy distraction, and will consume development
resources unless you are careful to verify they are dead. The
obvious approach is by checking for .DCU files. Scan your source
tree for all the .PAS files, and record the count. Then check the
number of .DCU files in your compiled units folder. The difference
in those numbers is a measure of the unused modules. If you have
not done so, do try the DupeUnits tool I presented above.
After processing the Unit Dependencies, you will see something
like the adjacent image.
This is from the MMX documentation, and represents a small
project. Consider the results where you may have thousands of
UDCs. The Cartesian map will not be very useful with such a
project. MMX can produce from this a unit cycles report, which
may be megabytes of plain text, and is unlikely to give you any
insights. However, processing that file with the simple analyzer I
built will yield a grid populated with files and cycle counts, which
is somewhat more useful. Moreover, as the analyzer allows you to
load an old report and a new one, you can also readily see what
progress you have made.
Simple Things 7
Now that we have looked briefly at some of the issues we can 7.1 Structural Changes in
expect in our legacy projects, it is time to begin looking closely at Delphi . . . . . . . . . 89
these issues in detail. We will review some of the fundamentals, as 7.2 Hints and Warnings 90
millions of lines of legacy code suggest that it will not be a waste. 7.3 Types and Constants 91
If you are confident of your approach in these areas, you can skip 7.3.1 No Magic Numbers . 91
to more fertile areas. 7.3.2 Unicode? . . . . . . . 92
7.3.3 Numeric Type Aliases 93
In this section, we will explore in more depth the various aspects
7.3.4 Global can be OK . . 94
of code, especially as they relate to refactoring legacy projects.
7.3.5 Assignment Compati-
Presumably you are reading this book because you are an active, bility . . . . . . . . . . 95
experienced, Delphi developer, so we will not offer detailed expla- 7.3.6 Use Enumerations . . 96
nation of the basics of the language, but will focus on the ways we 7.3.7 Use Sets . . . . . . . . 98
use those basics to our benefit. 7.3.8 Use Defined Ranges . 99
7.4 Variables and Scope 100
7.4.1 Avoid Global Variables 101
7.1 Structural Changes in Delphi 7.4.2 Using AOwner . . . . 101
7.5 Forms . . . . . . . . . 103
7.5.1 Form Variables . . . . 104
The language in Delphi is not static; it evolves. Although much of
7.5.2 Form File Format . . 104
the evolution comprises added features, new features can mean
7.6 Delphi Versions . . . 105
changes which break code, and others which warn of (likely)
future breakage.
An example of the latter is a call to FileExists, which will provoke
a warning that you should call the version of it in SysUtils. Since
the routine is in that module, this suggests that in future, it may
be necessary to qualify the reference, though is it not yet.
An example of a breaking change is one which affects a number of
things which had been global system variables in earlier versions.
One such is the DecimalSeparator, which must now be found in
the global FormatSettings variable: s := DecimalSeparator; Will
provoke this error: [dcc32 Error] MyFile.pas(101): E2003 Unde-
clared identifier: ’DateSeparator’ You will now need to revise in
this fashion: s := FormatSettings.DecimalSeparator; Do spend time
with the help topic for TFormatSettings, as it is likely to be an issue
in your code. There are quite a few members in TFormatSettings,
and it will be helpful to keep these in mind.
90 7 Simple Things

As Delphi is large, do not imagine that I am presenting an exhaustive


list of concerns, here or elsewhere in the book.

7.2 Hints and Warnings

Your project should generate no Hints and Warnings. If you have not
Yes, seriously, zero (0) Hints and achieved that, do so now. Hints and Warnings are the most basic
Warnings. level of error protection the compiler can provide. These protect us
from nasty oversights and coding errors which could be difficult
to trace in defect repair. It is surprising how many experienced
Delphi developers pay little attention to these messages.
Static code analysis with FixInsight will also save you time in the
long run. There are many trivial artifacts which should be removed
right at the outset.

if MyCondition = False then // wasted test


DoSomething ;
if MyCondition = True then // wasted test
DoSomething ;
if MyCondition then
else // horrible malpractice !
DoSomething ;

These should all be recognized as maintenance issues. The compiler


can digest any correct syntax; the reader, not so much! As with
many aspects of legacy code rework, the only way to fix these
things is to chip away at them.

if not MyCondition then


DoSomething ;
if MyCondition then
DoSomething ;
// third example , properly coded , duplicates first
if not MyCondition then
DoSomething ;

Yes, bad code. It’s not a judgment call, More important, your attention will be drawn by FixInsight to
these are signs of trouble coming. empty except clauses, empty finally clauses, and other such bad
code.
7.3 Types and Constants 91

7.3 Types and Constants

Types and constants are essential elements in our coding, and


when used well, will contribute to clarity and stability in the code,
and will reduce potential errors.
Types should be declared with as much care as you give to naming
your routines—or more. The purpose of these declarations is to
communicate clearly, and with few assumptions about what the
reader understands. Even if your successor is skilled in Delphi,
there are likely to be areas of knowledge in the application which
are so specialized to the problem domain that extra care in naming
and explaining them will add value.
In particular, it is an excellent practice to use a local prefix in
your naming. I might declare TwmIntRange rather than TIntRange.
Benefits include:
▶ Clearly indicates a locally declared entity.
▶ Facilitates unambiguous searching.
▶ Avoids risk of hiding Delphi types.

At times, you may need to declare something in an unusual way.


Anything out of the ordinary should always be prefaced by an
explanatory comment so that your successors understand why
things were done as they were. We can all read what was done,
but often the reasons behind the code are less than obvious.

7.3.1 No Magic Numbers

Named constants are infinitely better than hard-coded values.


However, their value is not fully realized unless you gather them Obviously, gathering them into units
into appropriately named units, and ensure that each has only one applies to those which are needed by
more than a single unit.
definition in your project code.

Database in Particular
Curiously, it is very common in legacy projects that field widths
in Delphi and in SQL are not centralized. This is madness. Sooner
or later you need to expand a field width—inevitably a field
which is widely accessed—and that simple change becomes a
huge task.
92 7 Simple Things

Note that if you wish to completely centralize these definitions,


then you will need to take a different approach to datasets in
data modules. These are commonly placed in their modules,
and the field definitions are added, statically. But at some point
in the future, when you need to change the width of a field, like
StreetAddress, you will have to make the change in each module
in which datasets contain that field. An alternative is to popu-
late the field definitions dynamically, but in turn, that means
you will no longer be able to use references to field objects like
dMainStreetAddress, but will have to rely on FieldByName. Or
perhaps you will wish to create an enumeration for your fields
and use that to direct the field creation from an array of records.
The ordinal values of the enumeration members would then be
used for field indexing: dMain.Fields[Ord(dmStreetAddress)].
But all of that adds considerable complexity.
Centralize your field declarations. Let there be a single defini-
tion for each field type. You can thank me later.

7.3.2 Unicode?

Depending on the version of Delphi you are using, and the age of
your legacy code, you may be facing issues with Unicode. The
topic has been addressed ably by others: Delphi and Unicode by
Marco Cantù:
https://www.embarcadero.com/images/old/pdf/Delphi-
Unicode181213.pdf
Delphi Unicode Migration for Mere Mortals by Cary Jensen:
https://www.embarcadero.com/images/dm/technical-
papers/delphi-unicode-migration.pdf
Delphi in a Unicode World by Nick Hodges:
https://www.embarcadero.com/images/dm/technical-
papers/delphi-in-a-unicode-world-updated.pdf
The Absolute Minimum Every Software Developer Absolutely, Positively
Must Know About Unicode and Character Sets (No Excuses!) by Joel
Spolsky: https://www.joelonsoftware.com/2003/10/08/the-
absolute-minimum-every-software-developer-absolutely-
positively-must-know-about-unicode-and-character-sets-
no-excuses/
7.3 Types and Constants 93

In any recent version of Delphi, it will be best always to use the


default value types. So you should use Integer, string, and so on,
not ShortInt, AnsiString, WideString, and whatnot, unless you have
a real need for those versions. Ordinarily, the only time you will need
to consider specialized value types is at the boundaries between
your application and Windows, or your application and others Yes, there are exceptions, but they
with which it must communicate. should be recognized as such, and
ordinary practice should prefer the
Delphi simplifies Unicode issues intelligently, and a simple single use of default types.
principle is worth keeping in mind: Most string variables should
simply be declared as type string. The need for specialized string
types is usually found only where your application connects with
others. Even in the Windows API, it is unusual for you to need to
use PAnsiChar, for example. Within your application, stay with
the normal string type, and let Delphi handle the details.

Streaming and Blobs

Help is not always helpful. If you have in your database blob


fields which predate Delphi adding support for Unicode, then
there could be extra issues to resolve. Specifically, in older
versions, the declaration of the blob type as text or binary
would make little difference. However, if the blob type is
text and the blob contains a stream, and you use a recent
Delphi version to read the stream, you will have problems. The
streaming code will attempt to help you by handling the text
as Unicode. But it is not, so the result is not pretty. A solution
(there may be others) is to run scripts to convert your blobs to
binary, which Delphi will not alter.
In the project at hand, the existing Delphi 2007 application had
no difficulty reading the blobs as before.

7.3.3 Numeric Type Aliases

Deep in the history of Pascal we were given some less than obvious
numeric type declarations. Happily, Delphi has long provided
aliases for them, and these are highly recommended, being in the
nature of self-documenting:

type
Int8 = ShortInt ;
Int16 = SmallInt ;
94 7 Simple Things

Int32 = Integer ;
IntPtr = NativeInt ;
UInt8 = Byte ;
UInt16 = Word ;
UInt32 = Cardinal ;
UIntPtr = NativeUInt ;

Some types of this form are not aliases, and are declared exter-
nally:

type
Int64 = __int64 ;
UInt64 = unsigned __int64 ;

This does not alter my recommendation in favor of using default


types such as Integer, but where you have a real need for specialized
types, making use of these aliases may simplify maintenance. Note
that the ever increasing specialization of type declarations means
that these aliases may not always be interchangeable with the
types they represent.

7.3.4 Global can be OK

Although global variables are always to be avoided, types and


constants can be defined globally without risk. It is, however,
essential that they not be redefined, so all developers on the
project must be vigilant in that regard.
You may find it beneficial to create multiple units for types and for
constants, depending on your project complexity. It could help to
have all your database types in one unit, and business logic types
in another, and so on.
Where you have a real need for global variables, consider some
strategies you might apply:
▶ Qualify your references to globals with the unit name: uGlob-
Possibly out of place, but searching als.NowPrinting, for example. At the very least, it warns
in code in a large project can be very others of the nature of the variable, and also facilitates
tedious. Qualifying references with
class or unit names makes things
productive searching.
much easier. And for the same reason, ▶ Package related globals into a class which exists for the
I suggest you never refer to a prop- purpose. Expose them through properties, and make use of
erty in the unit in which is declared, accessors in which you may wish to insert conditional code
but use the field name or accessor
routine.
in support of logging, for example. In particular, a benefit
7.3 Types and Constants 95

of this approach is in being able to debug accesses to these


variables, or to make some of them read-only.
Nonetheless, you should be working to reduce to the barest mini-
mum your use of global variables.

7.3.5 Assignment Compatibility

As Delphi is strongly typed, we should prefer to define the types we


need, and not make use of ad hoc variables. It is better to declare:

type
TIntArray10 = array [0..9] of integer ;
var
arrA : TIntArray10 ;
arrB : TIntArray10 ;
begin
arrA := arrB ; // works fine
end ;

Rather than:

var
arrA : array [0..9] of integer ;
arrB : array [0..9] of integer ;
begin
arrA := arrB ; // E2008 Incompatible types
end ;

The reason is simple: In the first example, variable arrA10 can be


assigned to variable arrB10, but in the second, it cannot be, as in
the second, arrA10 and arrB10, though of identical structure are
not assignment compatible, but are distinct types.
When you declare your types, keep in mind that Delphi declares
many types, and try to avoid using names already declared in
Delphi. For example:

type
TIntArray = array [0..0] of Integer ; // bad choice
// Delphi XE has , in SConnect :
type TIntArray = array [0..0] of Integer ;
96 7 Simple Things

It would be better to either use the already defined type, or if


you wish to declare your own, then distinguish it by a consistent
form:

type
TwmIntArray = array [0..0] of Integer ;
TwmStrArray = array [0..0] of string ;

Using your initials, or company initials, or some similarly mean-


ingful set of letters avoids creating confusion.

Stronger Typing is a Good ThingTM

In moving forward from an older version of Delphi you are


Internal to your app, the type spe- likely to run afoul of type changes. PChar is not the same as
cializations can be largely ignored; PAnsiChar and the string which was always 8-bit characters is
using default types will be best in
most cases. At seams, such as calls
now by default 16-bit characters. In most cases, these changes
into Windows, you may need to con- are minor annoyances, and are easily resolved.
cern yourself with specifics.
Less expected is the differentiation among integer types. You
will find types which are functionally Cardinal but will not ac-
cept a Cardinal, requiring instead the use of a NativeUInt. These
are instances of assignment incompatibility having been intro-
duced to keep us out of trouble. In resolving these problems
(we hope) you will be making your code more robust.

7.3.6 Use Enumerations

Delphi provides enumerated constants as a first class type. The


ways we can use these to our advantage is limited only by our
imagination. Consider an enumeration we have all seen:

type
TAlignment =
( taLeftJustify , taRightJustify , taCenter );

Besides enhancing readability, these values are strongly typed, so


they can be checked at compile time, preventing embarrassing
errors. If you explore the Delphi library source code, you will find
that enumerations are extensively used. In addition, the values
may be used to define ranges, which may simplify coding in other
areas:
7.3 Types and Constants 97

type
TLeftRight =
TAlignment . taLeftJustify ..
TAlignment . taRightJustify ;

The members need not be qualified as in this example, but using


the qualifier is always an option, and may help make plain to those
who follow you just what you intended. Moreover, if someone
uses those member names in another enumeration, the qualifier
avoids an unpleasant source of error.
By default, the ordinal values in an enumeration start from zero,
but values may also be assigned:

type
TWeekdays =
( wdMon =1 , wdTue , wdWed , wdThu , wdFri ,
wdSat , wdSun );
TSuits =
( Spades = 1, Diamonds = 3, Hearts = 5,
Clubs = 7) ;

Although there is no requirement to use a prefix in naming


members of an enumeration, you may find it useful to do so, as did
the designers of Delphi. Enumerations have ordinal values, and by
default, the first member has the value zero. In the example above,
wdMon=1 sets the ordinal value of the first entry, and subsequent
entries follow in sequence. In the TSuits enumeration, however,
all of the values are explicitly assigned.

Ordinal Values of Enumeration Members


As shown, you can assign a value to a member in an enumer-
ation. In fact, you can assign a value up to MaxInt. However,
when assigning ordinal values, keep in mind that when large
ordinal values are involved, the enumeration cannot be used
in a set, where the maximum would be 255.

Scoped Enumerations Recent versions of Delphi offer the pos-


sibility of using scoped enumerations. The compiler directive
$SCOPEDENUMS determines whether this mode is active for
any particular enumerations. That may sound confusing, but
the scope of the feature is local, meaning that you are free to
98 7 Simple Things

turn it on and off in multiple locations. An enumeration which


has been declared with $SCOPEDENUMS ON will require you
to reference it with the type qualifier. If you do not, then you
will get a compiler error. On the other hand, enumerations not
declared with $SCOPEDENUMS ON may still be referenced with
their type qualifier, or without. An example from Delphi help:
http://docwiki.embarcadero.com/RADStudio/Sydney/en/Scop
ed_Enums_(Delphi)
type
TFoo = (A , B , Foo );
{ $SCOPEDENUMS ON }
TBar = (A , B , Bar );
{ $SCOPEDENUMS OFF }

begin
WriteLn ( Integer ( Foo ));
WriteLn ( Integer (A)); // TFoo .A
WriteLn ( Integer ( TBar .B));
WriteLn ( Integer ( TBar . Bar ));
WriteLn ( Integer ( Bar )); // Error
end ;

In large applications, there can be some difficulty in keeping


straight the names of enumeration members, and the use of a qual-
ifier will often make code more readable. The $SCOPEDENUMS
ON directive simply provides enforcement. Because it is local
in scope, you cannot simply set it at project level, but you may
certainly update declarations, incrementally, and add the required
qualifiers. Over time, this will likely help you to reduce odd
defects.
You may qualify enums without setting the directive; applying
the $SCOPEDENUMS ON directive tells the compiler to enforce
that you are then required to use the qualifier on types so declared.
This is another way of getting help from the compiler to ensure
your code does not introduce defects which can be hard to track
down.

7.3.7 Use Sets

Delphi also contains sets as first class members, unlike many other
languages. We can test for membership, as well as conjunction,
7.3 Types and Constants 99

disjunction, and other set properties. The use of sets often allows us
to simplify our coding without any sacrifice in logical completeness.
Consider:

type
TSuits =
( Spades = 1, Diamonds = 3, Hearts = 5,
Clubs = 7) ;

function CardIsBlack ( ASuit : TSuits ): Boolean ;


begin
Result :=
ASuit in [ Spades , Clubs ]; // True if in set
end ;

This is a compact and convenient test made at runtime. But what


if we wish to pass in a set?

type
TSuits =
( Spades = 1, Diamonds = 3, Hearts = 5,
Clubs = 7) ;
TSuitSet = set of TSuits ;

function CardIsBlack ( ASuitSet : TSuitSet ): Boolean ;


begin
Result := ( ASuitSet * [ Spades , Clubs ]) <> [];
end ;

7.3.8 Use Defined Ranges

When you declare a subrange, you create a type which is fully


supported by the language. So, for example, if you write:

type
TIntTen = 1..10;
var
idx : TIntTen ;
begin
idx := 10; // compiles properly
idx := 11; // compile time error here
end ;

Ranges can be very useful when you are dealing with calculated
indices, for example. If the possible values are limited, then declar-
100 7 Simple Things

ing a subrange to check validity can save you some hard to find
errors. The range checking, however, is a compiler feature, so if
you have a variable of your subrange type, and you enable range
checking, then at run time an error will be thrown if the value is
incompatible with the range.

7.4 Variables and Scope

Variables should be as narrowly visible as possible, especially


where they interact with program state and business logic.
Prefer variables local to routines, wherever possible. Having no
Conventions matter. Field variable persistence, they cannot cause much trouble. A for argument is
names should always begin with F, now a deprecated prefix. There is a chart of the normal prefixes.
type names with T, and interfaces
with I. The naming of enums needs
If a variable must persist between calls to various routines, then
more discussion, as the old conven- you might use a variable declared in the implementation section
tion of two prefix characters which of the unit. That is global to the unit itself, but not visible outside.
would indicate something of their na- However, if the variable contains class state, and there may be
ture is unnecessary of you use scoped
enums. It is up to you whether to
multiple instances of the class, then the right storage will likely be
stay with the older approach, but the a private field variable in the class.
scoped enums have much to recom-
mend them.
Full Set of Prefix Conventions
Long ago, there was a longer list of prefixes, probably initiated
by Charlie Calvert, who was an early documenter at Borland.
These included:
▶ A for argument
▶ C for constant
▶ E for exception
▶ F for field
▶ I for interface
▶ L for local
▶ P for pointer
▶ S for resourcestring
▶ T for type
We could quibble over specifics, such as S for resourcestring,
since Charlie had probably left Borland before that was added.
There is clear value in some of these, and a bit less in others.
But always, always think in terms of those who must later read
and maintain your code. Use these where they help.
7.4 Variables and Scope 101

For example, you may well hesitate to begin refactoring a 1,000


line routine, but a good beginning would be to give prefixes to
the local variables, as that will be helpful a couple of hundred
lines below the declarations.

When you need a variable to retain its last value between calls,
there are better ways. Inside a class you may wish to add a field
variable, for example. If the value is to be known to multiple
instances of the class, then declare a class field variable.

7.4.1 Avoid Global Variables

We (should) all know by now how dangerous global variables are,


but the topic of this book is legacy code, and we must expect to find
global variables. As these are likely to be the source of defects, and Yes, I’ve mentioned this already, and
certainly will cause confusion, we should do our best to reduce more than once. It’s important!
their scope as much as possible. Where you do require global
scope, you may be better served by embedding such variables in a
class and accessing through properties or accessors, which will
allow you to introduce logging on changes, or breakpoints, either
of which can assist in tracking defects.
The use of global variables is easy, and requires no thought—it is,
all too often, a thoughtless act.
The complete elimination of global variables can be a challenge.
Don’t expect to reach that goal in a single sitting, or even in several
days of intense effort. The work it takes to eliminate them should
be a clear indication of why they can be a source of problems.

7.4.2 Using AOwner

All object instances are owned, at least in the sense of having


been created by some particular module. The same module which
creates them should be responsible for destroying them. The one
exception to this is an interfaced object which frees itself. (See
Interfaces, below.) When a class constructor contains the AOwner
parameter, then if it is created by a form, you would do well to
pass Self as that parameter. The form which creates it is then
responsible to free it.
102 7 Simple Things

AOwner? What’s Up with that Name?

As mentioned above, there has long been a convention that


argument names begin with A. So AItem is not illiterate usage;
it is simply an argument which is an Item, hence AItem.
In recent years, the A for Argument convention has been falling
out of use, but the others remain.
One reason the prefix conventions are important can be seen
in the use of properties which expose field variables.
type
TSomeClass = TObject
private
FCount : Integer ;
public
property Count : Integer
read FCount write FCount ;
end ;

If you access FCount inside the class, it is clearly a local reference.


If you access Count, the name gives the maintainer no clue. It
could be a routine, in or out of the current module. It could be
a global variable. As I argue elsewhere in the book, you should
always access the field directly—or use the accessors—from
inside the module, and use the property access only from
outside.

In legacy code, it is very common to see a class instantiated with


nil as the AOwner value. That usually means that the author of that
code did not properly understand the benefit of using AOwner. An
instance with nil in place of an owner must then be managed in
code. If the lifetime of the instance is greater than that of a single
routine, then your best alternative may be to instantiate it in the
module constructor, and free it in the module destructor.
If the module is a form or a data module, for example, then you
could simply have passed Self and let Delphi do the housekeeping.
Keep in mind, however, that there are times when you will want
to directly manage object lifetime. In the context of legacy projects,
you will likely create data modules in parallel to forms, so that
you can separate the data handling from the form itself. If there
may be multiple instances of the form open, then you will likely
treat the data module as its direct property, and both will have
the same lifetime. There may be other cases, however, where you
7.5 Forms 103

have multiple forms or instances of a single form sharing a data


module.

Safety First!

When you must access an object you do not own, even if it


is supposed to be global and therefore always made present,
always check for nil. It is not uncommon to see code such as:
begin
if ExternalObject . Count > 0 then
begin
// action code here
end ;

That’s fine, until someone makes the mistake elsewhere of


freeing the object. Then you have a problem, and will have to
discover the source of the AV. Better instead to write:
begin
if Assigned ( ExternalObject ) then
begin
if ExternalObject . Count > 0 then
begin
// action code here
end ;
end
else
raise Exception . Create (
’ ExternalObject is nil ! ’);

You may also wish to include the name of the routine where
the exception is raised. That will be less important if you use a
tool such as EurekaLog.

7.5 Forms

Delphi can autocreate forms for you when you run your application.
This feature is enabled by default in new installations. You will
probably want to disable it.
Manually instantiating a form is not unlike the notion of lazy
initialization: You don’t create a resource until you need it. In small
applications, the difference is negligible, but in a large application
with dozens of forms, autocreate is a bit silly:
104 7 Simple Things

▶ Wastes memory
▶ Slows down your application start
▶ Instills bad habits

It is the nature of Delphi to require us to instantiate resources


manually. You may argue that in Dependency Injection the container
manages that, but it’s still being done in code, simply not your
code. And you must still register those classes. That’s one reason
autocreate is a bad habit. Another is that it is a project option, so
it’s easy to forget you enabled it.
Autocreate your main form, but take charge of the rest in code.

7.5.1 Form Variables

When you create a new form, Delphi puts in the code a variable to
contain an instance of that form. This may be a good thing, but
if you need to instantiate more than one copy of the form object,
then you will need other variables to contain them. In that case,
you may find it better to remove the variable from the form unit,
as the variables used to contain independent instances should
logically be declared where they will be managed.
I wrote managed—not used—to draw attention to the fact that the
life of these instances must be managed. The mechanics of that
are up to you, but the task must be handled.

7.5.2 Form File Format

In the beginning, Delphi stored forms in binary format. But memory


is no longer in such short supply, and we really should store forms
as text. Many of us can probably recall when we discovered a
binary form file was corrupted, and we had to rebuild it, as we
were not yet using source control.
I suggest that in the course of migration, you convert any binary
forms to text.
A form in text format can also be corrupted, but you have the
ability to edit it in a text editor, if you are desperate. But you use
source control, so it is not really a problem, right?
7.6 Delphi Versions 105

7.6 Delphi Versions

We’ve all probably seen many compiler version include files, most
of which resemble this one from ZipForge:

{ $IFDEF WIN32 }
{ $DEFINE D2_OR_HIGHER }
{ $ENDIF }

{ $IFNDEF VER80 }
{ $IFNDEF VER90 }
{ $IFNDEF VER93 }
{ $IFNDEF VER100 }
{ $IFNDEF VER110 }
{ $IFNDEF VER120 }
{ $IFNDEF VER125 }
{ $IFNDEF VER130 }
{ $DEFINE Unknown_Version }
{ $DEFINE BD5 }
{ $ELSE }
{ $DEFINE BD5 }
{ $DEFINE DELPHI_COMPILER }
{ $ENDIF }
{ $DEFINE BCB4_OR_HIGHER }
{ $ELSE }
{ $DEFINE BCB4 }
{ $DEFINE BCB_COMPILER }
{ $ENDIF }
{ $DEFINE D4_OR_HIGHER }
{ $ELSE }
{ $DEFINE BD4 }
{ $DEFINE DELPHI_COMPILER }
{ $ENDIF }
{ $DEFINE BCB3_OR_HIGHER }
{ $ELSE }
{ $DEFINE BCB3 }
{ $DEFINE BCB_COMPILER }
{ $ENDIF }
{ $DEFINE D3_OR_HIGHER }
{ $ELSE }
{ $DEFINE BD3 }
{ $DEFINE DELPHI_COMPILER }
{ $ENDIF }
{ $DEFINE BCB1_OR_HIGHER }
{ $ELSE }
{ $DEFINE BCB1 }
106 7 Simple Things

{ $DEFINE BCB_COMPILER }
{ $ENDIF }
{ $DEFINE D2_OR_HIGHER }
{ $ELSE }
{ $DEFINE BD2 }
{ $DEFINE DELPHI_COMPILER }
{ $ENDIF }
{ $DEFINE D1_OR_HIGHER }
{ $ELSE }
{ $DEFINE BD1 }
{ $DEFINE DELPHI_COMPILER }
{ $ENDIF }

Actually, that one is old and pretty clean. Small, too. Most are
worse. But it does share with most others the checking for specific
compiler versions. One difficulty with this is the need for a decoder
ring—unless you are inclined to memorize VER185 and similar, you
will surely need one. The official one is: http://docwiki.embarc
adero.com/RADStudio/Sydney/en/Compiler_Versions
Just this morning, I learned of another approach written by
Fr0sT-Brutal: https://github.com/Fr0sT-Brutal. As the au-
thor says:
With these files you can write code that is compatible
with older Delphi compilers or even other Pascal com-
pilers (FreePascal) by simply enclosing modern/spe-
cific constructions with appropriate define. Moreover
you don’t have to check compiler version where a
required feature first appeared, you just check the
availability of this feature.
Some examples are given, and you may find this as appealing as I
do.

// DON ’T:
var
s: { IFDEF UNICODE } UnicodeString
{ $ELSE } WideString { $IFEND };

// DO :
var
s: { IF DECLARED ( UnicodeString )}
UnicodeString { $ELSE } WideString { $IFEND };
7.6 Delphi Versions 107

This approach makes use of three separate include files: Com-


pilerOpts.inc, CompilersDecl.inc, and CompilersDef.inc. In Com-
pilersDef.inc we find some interesting declarations:

// Declare capability constants to enable


// { $IF Cap_ **} construction

Cap_Region = { $IFDEF CAPS_REGION } True


{ $ELSE } False { $ENDIF };

Cap_ClassProps = { $IFDEF CAPS_CLASSPROPS } True


{ $ELSE } False { $ENDIF };

Cap_Inline = { $IFDEF CAPS_INLINE } True


{ $ELSE } False { $ENDIF };

Cap_ClassNested = { $IFDEF CAPS_CLASSNESTED } True


{ $ELSE } False { $ENDIF };

Cap_Strict = { $IFDEF CAPS_STRICT } True


{ $ELSE } False { $ENDIF };

Cap_ForIn = { $IFDEF CAPS_FORIN } True


{ $ELSE } False { $ENDIF };

Cap_ClassMarks = { $IFDEF CAPS_CLASSMARKS } True


{ $ELSE } False { $ENDIF };

Cap_MethMarks = { $IFDEF CAPS_METHMARKS } True


{ $ELSE } False { $ENDIF };

Cap_ClassFields = { $IFDEF CAPS_CLASSFIELDS } True


{ $ELSE } False { $ENDIF };

Cap_OpOverload = { $IFDEF CAPS_OPOVERLOAD } True


{ $ELSE } False { $ENDIF };

Cap_ClassHelpers = { $IFDEF CAPS_CLASSHELPERS } True


{ $ELSE } False { $ENDIF };

Cap_EnhancedRecs = { $IFDEF CAPS_ENHANCEDRECS } True


{ $ELSE } False { $ENDIF };

Cap_Generics = { $IFDEF CAPS_GENERICS } True


{ $ELSE } False { $ENDIF };
108 7 Simple Things

There still is no free lunch; the need for the familiar pattern
remains:

{ $IF CompilerVersion >= 17 }


{ $DEFINE RAD_2005_UP }
{ $IFEND }
{ $IF CompilerVersion >= 18 }
{ $DEFINE RAD_2006_UP }
{ $IFEND }
{ $IF CompilerVersion >= 19 }
{ $DEFINE RAD_2007_UP }
{ $IFEND }

// Numerous lines removed here to reduce boredom .

{ $IF CompilerVersion >= 30 }


{ $DEFINE RAD_10_UP }
{ $DEFINE RAD_SEATTLE_UP }
{ $IFEND }
{ $IF CompilerVersion >= 31 }
{ $DEFINE RAD_10_1_UP }
{ $DEFINE RAD_BERLIN_UP }
{ $IFEND }
{ $IF CompilerVersion >= 32 }
{ $DEFINE RAD_10_2_UP }
{ $DEFINE RAD_TOKYO_UP }
{ $IFEND }
{ $IF CompilerVersion >= 33 }
{ $DEFINE RAD_10_3_UP }
{ $DEFINE RAD_RIO_UP }
{ $IFEND }

The capabilities list is by no means complete here, but it is sufficient


to give a hint of the value in this approach. This is another exercise
in naming, and is no less important than any other application of
good naming. We recognize that hard coded integer values make
terrible field references into a dataset, so why is it not equally
obvious that VER220 is relatively useless to the maintainer? How
productive can you be if you must go look up the relevance of a
value every few lines of code, in order to understand the function
of that code?
Consider:

// would you not rather code :


{ $IFDEF Cap_Generics }
7.6 Delphi Versions 109

// than
{ $IFDEF CompilerVersion >= 22 }

And for those who must maintain the code, this will be a great
value, as well.
Not so Simple Things 8
There are many issues which can arise in legacy code which are 8.1 Persistence in Settings 111
not so simple to resolve, and where possible improvements may 8.1.1 Persistence with Enu-
aggravate other issues of concern. In approaching these, you may merations . . . . . . . 111
find you need to make some notes and then let them sit. Or perhaps 8.2 Dangers in Use of
sketch out multiple approaches, and then look for their strengths ClassName . . . . . . 115
and weaknesses. 8.2.1 Decoupling with a
Dictionary . . . . . . . 116
8.3 Reviewing Code . . . 123
8.1 Persistence in Settings

Years ago, we used INI files to store settings. Then we learned


that using the registry was somehow better. But in modern ap-
plication design, neither of these is very attractive, as both use
values persisted in the workstation. When a user needs to log into
an application in a meeting room, it will be most useful if the
settings are keyed to the user login, and therefore are persisted
in a database. Where the database resides is not critical to the
discussion, but freedom from using a particular workstation is the
essential feature.
Developing this solution will require a table in which the settings
can be stored. Although your particular needs may vary, a useful
starting point is to expect to create columns at least for UserID,
Key, and Value. In my experience, UserID has been an integer, and
Key and Value have been strings. The implementation needs to
be robust, and string values generally offer greater resilience than
other types.

8.1.1 Persistence with Enumerations

Having asserted the superiority of strings in saving settings, we


now consider how to pack the content of those strings, and one
very useful tool in handling user selections is through the use
of enumerations. But obviously, these are not string values. One
approach would be to use the ordinal values as positions in a
bitmap, and then to translate between integer and string. There
are several problems with this approach:
112 8 Not so Simple Things

▶ There are a number of manipulations needed in the conver-


sions.
▶ Inserting a member into an enumeration breaks the decoding
of existing bitmaps.
▶ Reordering members of an enumeration also breaks decod-
ing of existing data.
▶ Bitmaps in a table have no obvious meaning on inspection.

A better approach is available, and makes good use of library


functionality in Delphi, as well. Forget the bitmaps, and instead
use the enumeration members by name. Obvious benefits:
▶ Meaning is independent of ordering of members.
▶ Adding and removing members has no impact on existing
data.
▶ Saved options are comprehensible in raw data.

So how does this work, in practice? First, we must implement a


means of converting between enumerated constants and strings,
and it must be symmetrical. Moreover, it should impose minimal
burden in the client code. Here is an implementation:

unit EnumStrings ;

interface

uses
System . TypInfo ;

type
TEnumRec <T: record > = record
private
class function ToInteger (
aValue : T): integer ; overload ; static ;
function ToString : string ; overload ;
public
class operator Implicit (
aValue : integer ): TEnumRec <T >;
class operator Implicit (
aValue : TEnumRec <T >) : integer ;
class operator Implicit (
aValue : TEnumRec <T >) : string ;
class operator Implicit (
aValue : T): TEnumRec <T >;
class operator Implicit (
const aValue : string ): TEnumRec <T >;
8.1 Persistence in Settings 113

public
case integer of
0: ( Value : T);
1: ( IntValue : Int8 );
end ;

implementation

uses
System . Rtti ;

class function TEnumRec <T >. ToInteger (


aValue : T): integer ;
begin
case Sizeof (T) of
1: Result := pByte ( @aValue ) ^;
2: Result := pWord ( @aValue ) ^;
4: Result := pCardinal ( @aValue ) ^;
end ;
end ;

class operator TEnumRec <T >. Implicit (


aValue : integer ): TEnumRec <T >;
var s : string ;
begin
Result . IntValue := aValue ;
end ;

class operator TEnumRec <T >. Implicit (


aValue : TEnumRec <T >) : integer ;
begin
Result := aValue . IntValue ;
end ;

class operator TEnumRec <T >. Implicit (


aValue : T): TEnumRec <T >;
var
v : TValue ;
begin
Result . Value := aValue ;
end ;

class operator TEnumRec <T >. Implicit (


const aValue : string ): TEnumRec <T >;
begin
Result := GetEnumValue ( TypeInfo (T) , aValue );
114 8 Not so Simple Things

if Result . IntValue = -1 then


Result := 0;
end ;

class operator TEnumRec <T >. Implicit (


aValue : TEnumRec <T >) : string ;
begin
Result := GetEnumName ( TypeInfo (T) ,
aValue . IntValue );
end ;

function TEnumRec <T >. ToString : string ;


begin
Result := GetEnumName ( TypeInfo (T) , IntValue );
end ;

end .

Keep in mind:
▶ This is a pretty wrapper around existing functionality from
Delphi libraries. Nothing here is either exotic, though it may
not be familiar to you.
▶ This code is dependent on generics and on records with
methods. It should work on Delphi 2010 or later.
As to usage, it is pretty simple:

type
TestEnum = { eOne , eTwo , eThree };

procedure TestConv ;
var
EnumRec : TEnumRec ;
s: string ;
e: TestEnum ;
n: Integer ;
begin
EnumRec := eOne ;
s := EnumRec ;
e := EnumRec ;
n := EnumRec ;
end ;

You may think that the usage makes this resemble a variant, and
it does, and you may not find this as clear as you would like. But
8.2 Dangers in Use of ClassName 115

the record has been reduced as much as possible without losing


functionality. You may certainly add functions, if you prefer to see
code like this:

EnumRec := eTwo ;
s := StrFromEnum ( EnumRec );
n := IntFromEnum ( EnumRec );
// or perhaps
s := StrFromEnum ( eThree );

Readability is a judgment call.

8.2 Dangers in Use of ClassName

I routinely use TMS FixInsight to find issues which need attention,


and I recommend it highly. One of the issues it reports is when a
ClassName is compared to a string. Such coding may seem very
useful, but it presents liabilities, especially for maintenance. As
the advice in FixInsight states:

The ClassName property is compared with a string. It


is better to use the ClassType property instead. In the
example below ’SomeClass.ClassType = TSomeClass’
comparison would be safer.

var
SomeClass : TSomeClass ;
begin
...
if SomeClass . ClassName = ’ TSomeClass ’ then
...
end ;

The reason for the warning is very simple, and obvious if you
consider it for a time. First, bear in mind that in practice, the class
declaration is not staring you in the face, as it is in the example. If
you have something like this:

procedure UpdateLabels ;
begin
if Self . Owner . ClassName = ’ TShinyForm ’ then
chkOption . Caption := ’Do This ’
else if Self . Owner . ClassName = ’ TDullForm ’ then
chkOption . Caption := ’Do That ’;
116 8 Not so Simple Things

end ;

Now a year or two later, and for some very good reason, TDullForm
is renamed to TSatinForm. The UpdateLabels routine now fails
to update the label on chkOption. You will need to search your
thousands of modules to see where that property is written, and
apply a fix. Not difficult, merely annoying.
The point is that there is a liability here which exists because re-
naming the form does not provoke a warning from the compiler.
A better approach would be:

procedure UpdateLabels ;
begin
if Self . Owner is TShinyForm then
chkOption . Caption := ’Do This ’
else if Self . Owner is TDullForm then
chkOption . Caption := ’Do That ’;
end ;

In this case, renaming TDullform to TSatinForm will cause the com-


piler to complain. But this code introduces another complication.
In order to reference TShinyForm and TDullForm, this calling module
must add the containing modules for those classes to its uses
clause, which may lead to adding UDCs.
And that’s why the topic is in this chapter rather than the prior
one.

8.2.1 Decoupling with a Dictionary

The concerns in handling these references are two: First, that


the use of constant strings for checking against ClassNames is a
maintenance issue, and second, that in the better approach using
the is and actual class, you must increase scope in a way which
may introduce UDCs. Surely there must be a better way.
Let’s look at a different way, and one which I think is better, and
is certainly more maintainable. At the core, a TDictionary is used
to let us verify that our target class is a match. Of course, the
dictionary will have to be initialized, and some thought and care
will be required in how you do things, but a great benefit is the
low level of coupling.
8.2 Dangers in Use of ClassName 117

To begin, we will look at the dictionary. If you have not used one, it
is a generic collection which contains keys and values. In this case,
the key will be either the class name or a GUID. There is no need to
support this dual approach, but as we offer this as a replacement
for the class name testing, showing the two approaches may help
to illustrate the mechanism. Here is the class declaration:

type
TClassDict = class ( TObject )
private
FDictionary : TDictionary < string , TClass >;
public
constructor Create ;
destructor Destroy ; override ;
procedure AddItem ( const AKey : string ;
AItem : TClass );
function FoundItem (
const AKey : string ): Boolean ;
function GetClass ( const AKey : string ): TClass ;
function GetClassName (
const AKey : string ): string ;
end ;

Again, the dictionary is a generic collection, and the form of its


content is specified in the declaration:
FDictionary: TDictionary<string, TClass>;

It is deceptively simple, and very flexible. All we have declared is


that the key is a string. Remember that in this example, the key
may be a class name or a GUID. In a TDictionary, keys must be
unique, but no such constraint is imposed on values, so we will
have two entries for each class, one keyed on the name, and the
other on a GUID.
Now it will be good to look at one of our classes, and how it will
initialize its own entries into the dictionary.

unit u_ClassOne ;

interface

uses
u_GuidAttr ;

type
[ TGuidAttribute (
118 8 Not so Simple Things

’ {942 D85C8 -9475 -4 DC8 - BEF0 - D4DEA73D0C73 } ’)]


TClassOne = class ( TObject )
end ;

procedure InitializeUnit ;

implementation

uses
u_ClassDict ;

procedure InitializeUnit ;
begin
ClassDict . AddItem ( ’ tclassone ’, TClassOne );
ClassDict . AddItem ( GetGuidAttribute ( TClassOne ) ,
TClassOne );
end ;

end .

The class itself is empty, as what it contains is irrelevant to the


operation of the approach under discussion. The GUID is defined
with an attribute, at which we will look more closely later. The
InitializeUnit procedure will be called to register this class in the
dictionary. Note that although the class name is presented here as
a string constant:
ClassDict.AddItem(’tclassone’, TClassOne);

A better approach ordinarily would be to extract the string:


ClassDict.AddItem(TClassOne.ClassName.ToLower, TClassOne);

Typically, we are not dealing with persistence layer issues here, so


it will not usually be a major concern that the name has changed.
This sort of interface is more likely to be used in two contexts:
▶ Decoupling of modules within a large application. This deals
with the issue of coupling through the uses clauses, allowing
us to gain separation from some of the cross-couplings that
would otherwise be needed. In legacy code, this may be a
helpful first step, and easier to accomplish than would be a
major redesign to reduce the need for such coupling.
▶ Linkage to external systems. Calling into services, or COM
servers, there must be a means for the two programs to
8.2 Dangers in Use of ClassName 119

define a shared interface. This is a case where the use of


GUIDs is particularly encouraged.
Now let’s look at the handling of the GUID.

unit u_GuidAttr ;

interface

uses
System . TypInfo ;

type
TGuidAttribute = class ( TCustomAttribute )
public
FValue : String ;

constructor Create ( const AValue : String );


end ;

function GetGuidAttribute ( AType : TClass ): string ;

implementation

uses
System . Rtti ;

constructor TGuidAttribute . Create (


const AValue : String );
begin
inherited Create ;
FValue := AValue ;
end ;

function GetGuidAttribute ( AType : TClass ): string ;


var
LContext : TRttiContext ;
LType : TRttiType ;
LAttr : TCustomAttribute ;
begin
{ Create a new Rtti context }
LContext := TRttiContext . Create ;

{ Extract type information for TSomeType type }


LType := LContext . GetType ( AType );

{ Search for the custom attribute and do some


120 8 Not so Simple Things

custom processing }
for LAttr in LType . GetAttributes () do
if LAttr is TGuidAttribute then
Result := TGuidAttribute ( LAttr ). FValue ;

{ Destroy the context }


LContext . Free ;
end ;

end .

An attribute is a class which inherits The implementation and use of attributes will not be presented
from TCustomAttribute and must here, and there are numerous references you can consult to ac-
be implemented as any other class.
quaint yourself with them if necessary. See the Embarcadero
DocWiki help: https://docwiki.embarcadero.com/RADStu
dio/Sydney/en/Attributes_(RTTI) You will also want to
read the article on Extracting Attributes at Run Time: https:
//docwiki.embarcadero.com/RADStudio/Tokyo/en/Extractin
g_Attributes_at_Run_Time.
For now, accept that this small class works, and let us again look
at how it is used.

type
[ TGuidAttribute (
’ {942 D85C8 -9475 -4 DC8 - BEF0 - D4DEA73D0C73 } ’)]
TClassOne = class ( TObject )
end ;

This may be the most confusing aspect of attributes. The attribute


is placed immediately before the class here, to which it applies.
This attribute is now associated with the class. And although it is
not necessary to this discussion, note that multiple attributes may
be placed on a class. So now, looking again at InitializeUnit, we
can see that both the class name and the GUID are added to the
dictionary as keys for the class itself.

procedure InitializeUnit ;
begin
ClassDict . AddItem ( TClassOne . ClassName . ToLower ,
TClassOne );
ClassDict . AddItem ( GetGuidAttribute ( TClassOne ) ,
TClassOne );
end ;

Finally, here is how the initialization is accomplished:


8.2 Dangers in Use of ClassName 121

procedure TForm1 . FormCreate ( Sender : TObject );


begin
u_ClassDict . InitializeUnit ;
u_ClassOne . InitializeUnit ;
u_ClassTwo . InitializeUnit ;
u_ClassThree . InitializeUnit ;
end ;

Not very illuminating, but the point is that we must first initialize
the dictionary class, then call InitializeUnit in each of the client
classes to perform the initialization. Where you choose to do this
is up to you, but for this simple demo, I put it in the application
FormCreate.

And now, the point of all this is seen in the usage:

procedure TForm1 . btnTestClick ( Sender : TObject );


begin
lbLog . Clear ;
if ClassDict . FoundItem ( ’ tclassone ’) then
lbLog . Items . Add ( ’ Found TClassOne by Name ’);
if ClassDict . FoundItem (
’ {942 D85C8 -9475 -4 DC8 - BEF0 - D4DEA73D0C73 } ’)
then
lbLog . Items . Add ( ’ Found TClassOne by Guid ’);
if ClassDict . FoundItem ( ’ tclasstwo ’) then
lbLog . Items . Add ( ’ Found ’ +
ClassDict . GetClassName ( ’ tclasstwo ’) +
’ by Name ’);
if ClassDict . FoundItem (
’{ D93D97C7 -27 D4 -4 BE0 -83 E4 -4 D3741F298BF } ’)
then
lbLog . Items . Add ( ’ Found ’ +
ClassDict . GetClassName (
’{ D93D97C7 -27 D4 -4 BE0 -83 E4 -4 D3741F298BF } ’) +
’ by Guid ’);
if ClassDict . FoundItem ( ’ tclassthree ’) then
lbLog . Items . Add ( ’ Found ’ +
ClassDict . GetClassName ( ’ tclassthree ’) +
’ by Name ’);
if ClassDict . FoundItem (
’{ B651C821 - E847 -45 D3 - B495 -24 FCE166BC40 } ’)
then
lbLog . Items . Add ( ’ Found ’ +
ClassDict . GetClassName (
’{ B651C821 - E847 -45 D3 - B495 -24 FCE166BC40 } ’) +
122 8 Not so Simple Things

’ by Guid ’);
end ;

This looks a bit cluttered as we are using both forms of key


for lookup, which you would not do in practice. Running the
application, here is the result:

Figure 8.1: Testing the ClassDict

You may be wondering how using a GUID is any better than the
string constant for the class name. After all, both are just strings.
That’s true, but the string constant for the class name is not likely
to be seen as anything special by most developers. Worse, it is off
in some corner of the application, used on one or two modules,
and gets little attention. Worse yet, the class name itself could be
changed. We are talking legacy projects, after all, and we have
certainly seen poorly named classes. Improving such things is an
ordinary part of code cleanup. Moreover, we know that when we
alter the class name, the compiler will tell us of references we
break in the process. But string constants enjoy no such scrutiny
by the compiler.
A GUID likewise is not checked by the compiler against any
standard. However, no developer with any experience is going to
alter a GUID, as it is recognized as a constant which is critical to
some aspect of the application. So although a GUID is just a string,
it is treated with respect, and as it has no meaning obvious to a
developer, will not be altered for readability.
8.3 Reviewing Code 123

Earlier, I mentioned Unit Dependency Cycles, which can certainly


be an unpleasant feature of legacy code. The loose coupling
demonstrated in this demo project produces no such cycles, which
is the standard to which we should aspire. Admittedly, they are
rarely a problem in small applications like this one, but they
become an issue when and because we cease concerning ourselves with
their creation.

8.3 Reviewing Code

Code reviews, code walk-throughs, code read-throughs, all are


beneficial. Pairing on a code review delivers better value than
a review by a single developer. But as helpful as these may be,
things will be missed.
The ClassDict project, small is it is, has been checked using TMS
FixInsight, which reported nothing in need of attention. It has also
been through the MMX Unit Dependency Analyzer, and no cycles
were found. Those are checks which took only seconds, and were
not affected by my lack of sleep, distraction with meetings, or any
other influence. Clean bills of health from those tools means that
in any sort of discussion and review with colleagues, we can focus
on design issues and functional quality, rather than whether the
code is clean and logically correct.
Granted, FixInsight blurs the lines a bit, as some of its checks
address elements of design, but not with the depth a person brings
to the task.
Routinely using the available analytical tools is a way to ensure
that the small things remain small things.
Cleaning Legacy Code 9
Cleaning is such a simple, innocent term, yet in this connection, it 9.1 Local Variables . . . 125
can refer to almost anything we consider less than ideal. Common 9.1.1 I/O Checking . . . . . 126
cleaning operations include: 9.1.2 Overflow Checking . 127
9.1.3 Range Checking . . . 129
▶ Remove or rewrite unhelpful comments. 9.1.4 Rename Local Vari-
▶ Replace hard coded literals with named constants. ables . . . . . . . . . . 129
▶ Remove unused local variables. 9.1.5 Remove Local Vari-
▶ Remove with clauses. ables . . . . . . . . . . 131
▶ Factor out repeated code blocks. 9.2 Remove Wrappers . . 133
▶ Remove unnecessary Boolean tests. 9.3 Coding Style . . . . . 133
▶ Ensure that except and finally clauses are not empty.
9.4 Form State . . . . . . 136
▶ Ensure that object creation is guarded by a try/finally 9.4.1 Form State Compo-
clause for disposal. nents . . . . . . . . . . 136
We should define the term code smell, as it is commonly used in 9.5 Form vs. Code Depen-
discussions of refactoring, and is the motivator for any rework. dency . . . . . . . . . . 137
From Wikipedia: https://en.wikipedia.org/wiki/Code_sme 9.6 Types & Consts Again 140
ll 9.6.1 Doppelgangers . . . . 142
9.7 Misplaced Routines 142
In computer programming, a code smell is any charac-
teristic in the source code of a program that possibly
indicates a deeper problem. Determining what is and
is not a code smell is subjective, and varies by language,
developer, and development methodology.

The term is useful, though subjective, and the presence of smell is


not necessarily determined by the use of a particular feature, but
perhaps by its misuse or overuse.

9.1 Local Variables

When there are unused local variables, they should be removed,


to reduce the possible confusion. FixInsight is a great help with
that.
Local variables need initialization. When they are not initialized,
they can lead to errors which can be hard to trace. FixInsight can
help with that, as well.
126 9 Cleaning Legacy Code

When a routine contains more than a few local variables, the


routine is probably doing too much work. Refactor. Keep the
routine focused as narrowly as possible.

9.1.1 I/O Checking

I/O checking is managed by the {$I-} and {$I+} directives. For


some operations, you may need to disable I/O checking, as when
you need to explicitly manage I/O error handling.

var
error : Integer ;
begin
// Turn off I/O error checking
{$I -}
MkDir ( ’ MyDir ’);

// Was it created ?
error := IOResult ;
if error = 0 then
ShowMessage ( ’ Created OK ’)
else
ShowMessageFmt ( ’ Creation failed : error %d ’,
[ error ]) ;
// Delete the directory to tidy up
RmDir ( ’ MyDir ’);
// Turn I/O checking on
{ $I +}
end ;

Although this example is not very friendly, it is sufficient to show


the mechanics and purpose of such actions. This might more
thoughtfully have been coded a bit differently, eliminating the
need for trivial comments:

var
error : Integer ;
begin
{ $IOCHECKS OFF }
CreateDir ( ’ MyDir ’);

error := IOResult ;
if error = 0 then
ShowMessage ( ’ Created OK ’)
else
9.1 Local Variables 127

ShowMessageFmt ( ’ Creation failed : error \% d ’,


[ error ]) ;

RemoveDir ( ’ MyDir ’);


{ $IOCHECKS ON }
end ;

It’s nice that the original coder considered that we might forget
what the short form stands for, but would have been nicer still had
the long—self documenting—form been used. We should always
seek to make code explain itself, and add comments only when
there is a pressing need. In general, line comments are much less
useful than block comments, as the latter should be used to explain
why a routine exists, and why and how it does what it does.

9.1.2 Overflow Checking

Overflow checking is managed by the {$Q-} and {$Q+} directives.


This warns when the result of a math operation exceeds the limits
of the variable type to which it assigned. There are penalties to
its use, in both performance and code size, but it is well worth
using in your debug configuration. From Delphi help: https:
//docwiki.embarcadero.com/RADStudio/Sydney/en/Overflow
_checking_(Delphi)
The $Q directive controls the generation of overflow
checking code. In the {$Q+} state, certain integer arith-
metic operations (+, -, *, Abs, Sqr, Succ, Pred, Inc,
and Dec) are checked for overflow. The code for each
of these integer arithmetic operations is followed by
additional code that verifies that the result is within
the supported range. If an overflow check fails, an
EIntOverflow exception is raised (or the program is
terminated if exception handling is not enabled).
The $Q switch is usually used in conjunction with the
$R switch, which enables and disables the generation
of range-checking code. Enabling overflow checking
slows down your program and makes it somewhat
larger, so use {$Q+} only for debugging.
128 9 Cleaning Legacy Code

These options can be applied dynamically. You may turn them on


and off in code areas that you choose. If you do, then you should
probably use something of this sort:

{ $IFOPT Q+}
{ $DEFINE OVERFLOW_ON }
{$Q -}
{ $ELSE }
{ $UNDEF OVERFLOW_ON }
{ $ENDIF }
//
// Your code here
//
{ $IFDEF OVERFLOW_ON }
{ $Q +}
{ $UNDEF OVERFLOW_ON }
{ $ENDIF }

Consider, too, that you can implement overflow checking in your


own code. If you choose to do so, then coverage is up to you; if
you use Delphi’s implementation, then it is applied according to
rules.
In the case at hand, it is obvious by implication what Q means,
but certainly it would be difficult to argue that Q is intuitively
obvious as a symbol for OVERFLOW. A long form symbol—
OVERFLOWCHECKS—is available, though not ideal here:

{ $IFOPT Q+}
{ $DEFINE OVERFLOW_ON }
{ $OVERFLOWCHECKS OFF }
{ $ELSE }
{ $UNDEF OVERFLOW_ON }
{ $ENDIF }
//
// Your code here
//
{ $IFDEF OVERFLOW_ON }
{ $OVERFLOWCHECKS ON }
{ $UNDEF OVERFLOW_ON }
{ $ENDIF }

The problem being that IFOPT is designed to work with the short
form symbols and a + or - symbol.
Despite the advice about performance effects of overflow checking,
9.1 Local Variables 129

there are many people who now prefer to leave it enabled in


production code. Check the real impact on performance. Weigh
the benefits and decide for your own needs.

9.1.3 Range Checking

Range checking is managed by the {$R-} and {$R+} directives. When


you make access to an array, and especially when you calculate
the index to be used, range checking is helpful to avoid accessing
nonexistent array elements. This may often be an issue when
you work with dynamic arrays. As with Overflow Checking, the
compiler will add code, increasing size, and reducing performance,
but it is very useful in debugging. From Delphi help: https:
//docwiki.embarcadero.com/RADStudio/Sydney/en/Range_ch
ecking
The $R directive enables or disables the generation
of range-checking code. In the {$R+} state, all array
and string-indexing expressions are verified as being
within the defined bounds, and all assignments to
scalar and subrange variables are checked to be within
range. When a range check fails, an ERangeError excep-
tion is raised (or the program is terminated if exception
handling is not enabled).
Enabling range checking slows down your program
and makes it somewhat larger.
At the risk of repeating myself in spirit, also note that that Range
Checking has a long form symbol: RANGECHECKS which again is self
documenting in your code.
And again, many prefer to leave this enabled in production code.
Check the real impact on performance. Weigh the benefits and
decide what meets your needs.

9.1.4 Rename Local Variables

Why worry over the names of local variables? When your routine
is 1,000 lines of ugly code, “local” has no meaning. When you
have 70+ local variables, each one needs to communicate clearly
why it is used. When you begin refactoring, remember to move
130 9 Cleaning Legacy Code

local variables into the nested routines, wherever possible, but


still name them clearly.
Multi-level loops are common in legacy code, and the loop indices
need names which help you understand what each level is doing.
Also keep in mind whether the loop is of the right type. There is a
tendency for developers to use one loop type by default, often the
simple for loop. That’s fine if you need to loop through an entire
collection, but if you expect to terminate the loop, then perhaps
the while or repeat will be a better choice. In recent versions, there
is also the for var in collection do loop construct.
A for loop in which the loop index is If you use FixInsight, it will report on any for loop in which the
not referenced in the contained code index is not referenced inside the loop, which is a good indication
will be reported by FixInsight. If you
use a while loop, FixInsight will not
that a different loop form may be a better fit. But back to the
note that. naming of indices. If you are looping through year, month, and
day, then:

var
yearIdx , monthIdx , dayIdx : Integer ;

will be more useful than:

var
i , j , k: Integer ;

There is a place for short local variable names, certainly. But use
them with care, and for any variable—even local—which cannot
be overwritten on a whim, avoid them. It is perfectly reasonable
to use s as a string variable name, if:
▶ The variable is used as a short-term temporary.
▶ S need not remain unchanged over more than a very few
lines of code.
▶ The variable name does not affect readability.

So, for example:

function IsFloat ( const AString : string ): Boolean ;


var
s: string ;
d: double ;
begin
s := Trim ( AString );
Result := TryStrToFloat (s , d);
end ;
9.1 Local Variables 131

In the code above, s is used simply to contain the trimmed copy


of the input string. Similarly, d is a meaningless variable name
used for the variable which is required by TryStrToFloat. Cer-
tainly this routine could be reduced to a single line of code, but
there will be times when splitting such operations will facilitate
debugging.

TryStrToFloat and Company

In case you missed their introduction, Delphi offers a collection


of conversion functions such as TryStrToFloat. The benefits of
these routines are:
▶ Fail quietly, without throwing exceptions
▶ Return a boolean result which reports success or failure
▶ Operate on a variable which is set to zero if the conversion
fails
Member routines include:
▶ TryEncodeDate, TryEncodeTime
▶ TryFloatToCurr, TryFloatToDateTime
▶ TryStrToBool
▶ TryStrToCurr
▶ TryStrToDate, TryStrToDateTime
▶ TryStrToInt, TryStrToInt64
▶ TryStrToTime
▶ TryStrToUInt, TryStrToUInt64
▶ TrySystemTimeToDateTime

If you have a plethora of local variables, and hundreds of lines of


code, then you really need to refactor. Even if you take the easy
path of extracting code to nested routines, a benefit is that the
local variables for that block move to the nested routine. This is a
net reduction in noise, and helps increase comprehensibility. And
since you will have used excellent names for your nested routines,
the containing routine will begin to be readable as an explanation
of its function.

9.1.5 Remove Local Variables

In a large routine, it is likely that some variables may easily be


removed, and for a variety of reasons:
132 9 Cleaning Legacy Code

▶ Variable is not used.


▶ Variable is used only once.
▶ Variable is written to, but not read from.

In lengthy routines, it is not uncommon that a local variable may


have been declared that is then actually read from only once. Unless
this is done because the source of the value is a drill-down into
some object hierarchy, or is passed into a routine with numerous
arguments, this is just another bit of noise:

var
s: string ;
begin
// noisy approach
s := Trim ( AString );
UpdateWithStr (s);
// direct , and better
UpdateWithStr ( Trim ( AString ));
// ... more code here , but no further use of s
end ;

In this example, there is still only one reference, but the use of the
local variable contributes to readability:

var
s: string ;
begin
s :=
BigTable . FieldByName ( ’ LastName ’). AsString + ’,’
+ BigTable . FieldByName ( ’ FirstName ’). AsString ;
LocalTable . Edit ;
LocalTable . FieldByName ( ’ FullName ’). AsString := s;
LocalTable . Post ;
// ... more code here
end ;

Viewed in isolation, these may seem to be inconsequential dif-


ferences, but when you are lost in spaghetti code, this sort of
difference helps to reduce confusion. When you use such a vari-
able, keep the assignment to it, and its use, in consecutive lines for
clarity. If this is in a lengthy routine, and there is no other reference
to the variable, then you may wish to add a comment explaining
that. In the course of later refactoring, when the comment is no
longer needed, it may be removed, but in the meantime will serve
to assist those who follow you in maintenance.
9.2 Remove Wrappers 133

9.2 Remove Wrappers

Often you may find that someone with an aversion to typing has
created a routine like this:

function S2I ( AString : string ): Integer ;


begin
Result := StrToInt ( AString );
end ;

This may seem relatively innocent, but it is a source of distraction


and sometimes confusion when new developers join the team.
Although you may not wish to make this a high priority item,
replacing such little gems will bring lasting value to the project.
On the other hand, such trivial wrappers have their place on
a temporary basis. You may have found a home-grown routine
which can be replaced by a Delphi library routine. You might
choose to keep the routine and simply call the Delphi routine from
it, removing the old code. The call overhead is not likely to be
horrible, and the old routine as wrapper can be flagged deprecated
as a note to deal with it later.

9.3 Coding Style

In any team of developers, there is usually resistance to following


a consistent style. While the niceties of consistent formatting may
not be a high priority, it is generally best if all follow the same
approach to structuring flow control code, as it will make them all
more productive. There should also be a drive toward using the
simplest logic forms, and a preference for positive logic.

if SomeCondition = True then // seriously ?


DoSomeAction ;
if SomeCondition <> True then // seriously ?
DoSomeAction ;
if SomeCondition <> False then // seriously ?
DoSomeAction ;
// should be :
if SomeCondition then
DoSomeAction ;
if not SomeCondition then
DoSomeAction ;
134 9 Cleaning Legacy Code

if SomeCondition then
DoSomeAction ;
// And , watch for this :
if SomeCondition then
Result := True ;
// which should have been :
Result := SomeCondition ;

All sorts of silly variations will pop up in legacy code. Some


variations may seem unimportant, but consistent style is an aid
when different team members must work on a collection of units.
The consistency of format means that all team members will
recognize with equal ease the logic flow presented. Consider:

begin
// one possibility
if ConditionA then
begin
end
else if ConditionB then
begin
end
else
begin
end ;
// and another
if ConditionA then begin
end else if ConditionB then begin
end else begin
end ;
// and a third
if ConditionA then begin
end
else if ConditionB then begin
end
else begin
end ;
end ;

All three layouts represent the same logic, but they are not equally
readable, in my view. Pattern recognition is an essential part of
development work, and the more consistently the patterns are
employed, the more value they confer on the project. Some patterns
are idiomatic:

// here we know what to expect


9.3 Coding Style 135

sl := TStringList . Create ;
try
// do work here
finally
sl . Free ;
end ;
// here there is some confusion
sl := TStringList . Create ;
try
// do work here
finally
sl . Free ;
sl2 . Free ; // should not happen here
// now we search to find where sl2 was created
end ;

The reference to sl2 in the second finally clause is not merely


confusing, but wrong. Objects should be freed by their creators,
their owners, not by interlopers.
In passing, let’s also consider a form which is often found in
code where it is a repeating pattern within a block, usually of
assignment statements. It is a form to be avoided, and for a few
reasons:

// a bad choice here


if Cond then
s := ’ Something ’
else s := ’ Nothing ’;
// better alternative
s := ’ Nothing ’;
if Cond then
s := ’ Something ’
// still better
s := IfThen ( Cond , ’ Something ’, ’ Nothing ’);

These forms are functionally equivalent, though if you use IfThen


you must keep in mind that the arguments must all be evaluable at
run time. The same is not required of the it/then/else approach,
which is subject to short-circuit evaluation. If the IfThen is new to
you, you will find the string form in StrUtils, and the numeric
versions in Math.
136 9 Cleaning Legacy Code

9.4 Form State

Forms may have options, settings, features which put the form into
some state, and which should be persisted. Users now take such
things for granted, and consider them essential behaviors, rather
than features. As features are added, and an application evolves,
the options and other stateful data tend to increase. Sometimes a
developer may add separate routines in support of persistence, as
well as restoration of state on loading.
It’s good not to let a single routine grow endlessly, but it is even
better to make sure that:
▶ Consistent naming of such routines is practiced, so that
developers recognize that identifiers like:
• UpdateSomeState
• PersistSomeState and
• RestoreSomeState
are all related and involved in such operations.
▶ A single routine should normally be used on form loading,
where it can be called form the form’s OnCreate event handler.
Good practices will make it easier to be sure that such routines
are not overlooked, and that all members of the team recognize
the idiom such routines represent.

9.4.1 Form State Components

In dealing with form state, keep in mind that there are components
available from several vendors which simplify the persistence
of form state. By default, they all, I think, persist for size and
position, but generally also have events which may be used to
add functionality you may require. Also, most will have been
designed to use either the registry or an INI file for persistence,
but in modern applications, you may find that you need to persist
to a table in a database, so that the persisted items follow the user
login, not the physical machine.
9.5 Form vs. Code Dependency 137

9.5 Form vs. Code Dependency

It can be difficult to keep the relationship between form content


and related code in full agreement. One such issue is found in
tying code to a RadioGroup:

case Options . ItemIndex of


0: HandleDefault ;
1: HandleVerbose ;
2: HandleAbbreviated ;
end ;

This may seem perfectly reasonable, and indeed, is not terrible,


but what about a RadioGroup with a dozen or more buttons?
There are maintenance issues which could be reduced with a more
thoughtful approach. In the particular case of a RadioGroup which
is fully defined on the form, any approach may seem less than
ideal. But what happens when the order of items is altered? Or an
item is added, or removed? This is, again, a matter of thoughtful
design.
Assuming that the RadioGroup is to remain on the form, there are
still strategies available to reduce risk.
1. Declare an enumeration for the items.
2. Declare the option captions in a constant.
3. Assert that the items count on the form is the same as in
code.
Now, lets see what that looks like, using the same options as
above:

type
TSomeOptions = ( soDefault , soVerbose , soAbbrev );
const
SOptions = ’ Default , Verbose , Abbreviated ’;
var
oldCount : Integer ;
begin
oldCount := Options . Items . Count ;
Options . Items . CommaText := SOptions ;
Assert ( oldCount = Options . Items . Count ,
’ Options different than on form ! ’);
end ;

// then in the OnClick event


138 9 Cleaning Legacy Code

case TSomeOptions ( Options . ItemIndex ) of


soDefault : HandleDefault ;
soVerbose : HandleVerbose ;
soAbbrev : HandleAbbreviated ;
end ;

Again, with only three items, this may seem a lot of work, but
with a larger collection, it will more than repay your efforts. The
assignment to Options.Items will be done in the form creation.
The declaration of the enumeration, the Items strings, and the
initialization can all be in your new business logic unit:

unit SomeFormBusinessLogic ;
interface
type
TSomeOptions = ( soDefault , soVerbose , soAbbrev );

TFormLogic = class
public
procedure InitOptions ( AOptions : TRadioGroup );
end ;

implementation

const
SOptions = ’ Default , Verbose , Abbreviated ’;

procedure TFormLogic . InitOptions (


AOptions : TRadioGroup );
var
oldCount : Integer ;
begin
oldCount := AOptions . Items . Count ;
AOptions . Items . CommaText := SOptions ;
Assert ( oldCount = AOptions . Items . Count ,
’ Options different than on form ! ’);
AOptions . ItemIndex := 0;
end ;

end .

This approach allows the convenience of keeping the RadioGroup


on the form, but ensures the code and form are in agreement.
And further, if the options count is altered in code, and not on the
form, a warning is given. Likewise if the item count on the form
9.5 Form vs. Code Dependency 139

is changed without updating the supporting code. It may not be


perfect, but it is much better than the too simple approach in the
original example. A further benefit of this approach is that there
may be user actions which require that a particular Option be
asserted. Such actions can now be made very low maintenance:

Options . ItemIndex := Ord ( soVerbose );

This assignment is now impervious to changes in the order of the


options, depending only on the continued existence of the enumer-
ation member. Further, if the enumeration is altered to remove that
member, the compiler will complain when you build, whereas a
hard-coded integer index could lead either to an incorrect selection,
or to an out of range index.
Calculated item indices can be a source of maintenance headaches.
In legacy projects, the List Out of Bounds error often crops up.
This may often happen because a list was populated in the form
design, and code was written to select an item based on its index,
but the list length later changed:

SomeComboBox . ItemIndex := 8; // error !

One little line, yet so much is wrong! It is never smart to hard-


code an index; it’s fragile and finding the problem can be tedious.
Second, populating a ComboBox list on the form is risky, at best.
There are few benefits, and the risks are many. If, for some reason,
you are not ready to rewrite, then at least practice some defensive
coding:

if SomeComboBox . Items . Count > 8 then


SomeComboBox . ItemIndex := 8; // this is ok

But that is simply a bandage, and a leaky one, at that; the root
problems remain. A better approach would be similar to that for
the RadioGroup, above. Again, hard-coded values will become
problems in the future. There is a tendency for ComboBox lists to
change, with items added, removed, or reordered. Assigning an
index as though it would always be correct is terrible practice.

unit SomeFormBusinessLogic ;
interface
type
TListIndices = ( soDefault , soVerbose , soAbbrev );
140 9 Cleaning Legacy Code

TFormLogic = class
public
procedure InitList ( AOptions : TComboBox );
end ;

implementation

const
SOptions = ’ Default , Verbose , Abbreviated ’;

procedure TFormLogic . InitList ( AOptions : TComboBox );


var
oldCount : Integer ;
begin
oldCount := AOptions . Items . Count ;
AOptions . Items . CommaText := SOptions ;
Assert ( oldCount = AOptions . Items . Count ,
’ Options different than on form ! ’);
AOptions . ItemIndex := 0;
end ;

end .

Note that you could make broader use of the TListIndices enu-
meration:

Options . ItemIndex := Ord ( soVerbose );

9.6 Types & Consts Again

Now that you have seen some examples of using enumerations


and constants to reduce maintenance issues, we can look at some
other possibilities. First, a few examples, with no preamble:

type
TDataIndices = ( soItem , soDate , soName , soStreet ,
soCity , soState , soZip );
const
DataFields : array [ TDataIndices ] of string = (
’ RecID ’, ’ LastEdit ’, ’ FullName ’, ’ Street ’,
’ City ’, ’ State ’, ’ Zip ’);

procedure ProcessFields ;
begin
9.6 Types & Consts Again 141

for idx := Ord ( soItem ) to Ord ( soZip ) do This code uses FieldByName in pref-
erence to Fields[idx]. Unless you
begin
have a severe performance issue to
s := SomeData
resolve, the use of FieldByName is
. FieldByName ( DataFields [ idx ]) . AsString ; often desirable because it frees you
ApplyTheStr ( idx , s); from the concern of column ordering.
end ;
end ;

Legacy code often indulges in hard coding of field names in


repeated calls to FieldByName(), but clearly it can be done better.
Here is another possibility, though there are better ways do deal
with datasets, as will be discussed later.

type
TDataIndices = ( soItem , soDate , soName , soStreet ,
soCity , soState , soZip );
TFldInfoRec = record
fld : string ;
cap : string ;
end ;
const
DataFlds : array [ TDataIndices ] of TFldInfoRec = (
( fld : ’ RecID ’; cap : ’ Item No . ’) ,
( fld : ’ LastEdit ’; cap : ’ Updated ’) ,
( fld : ’ FullName ’; cap : ’ Name ’) ,
( fld : ’ Street ’; cap : ’ Street ’) ,
( fld : ’ City ’; cap : ’ City ’) ,
( fld : ’ State ’; cap : ’ State ’) ,
( fld : ’ Zip ’; cap : ’ Zip Code ’) );

procedure ProcessFields ;
var
fldName : string ;
begin
for idx := Ord ( soItem ) to Ord ( soZip ) do
begin
fldName := DataFlds [ idx ]. fld ;
s := SomeData . FieldByName ( fldName ). AsString ;
ApplyTheStr ( idx , s , DataFlds [ idx ]. cap );
end ;
end ;

The point is that a great deal can be done with types and constants
to resolve things which legacy code often addresses through hard-
coded approaches. This is certainly true for many things which
can be fully resolved at compile time.
142 9 Cleaning Legacy Code

9.6.1 Doppelgangers

It is inevitable in a large application that routines will be written


(in different units) which have identical names, and even identical
parameter lists. When the application reaches a certain size, it is
also likely that both units may be referenced by another, which
calls the named routine. When that occurs, the order of references
in the uses clause of the calling unit determines which routine is
called.
The danger lies in the two routines behaving differently. There
are strategies for resolving such problems, and you will need to
determine which to follow as each case arises.
▶ Remove one routine from the code base. This works if the
routines deliver identical behavior.
▶ Qualify the calls with the name of the unit or class. This
works when the two are coded differently.
But what if the two routines are coded differently? Should they
be? Or is one correct, and the other not? In legacy code, and with
no unit tests, these are unpleasant questions, and the investigation
adds burden, but this sort of discovery should be seen as an
opportunity to resolve an issue which may not yet have been
reported.
Add to these considerations that the routine might duplicate the
behavior of a Delphi library routine. In that case, you may wish to
retire both versions in your project, and use the library routine.
Whatever change you anticipate, there will be a good deal of work
to be done, as you may need to edit calls in dozens or hundreds of
units.

9.7 Misplaced Routines

A misplaced routine is one which should really be in a different


class or unit. This becomes important when we look at the issue
of Unit Dependency Cycles . You may have a data utilities module
which contains a routine to return a string, and uses a string
utilities module which in turn uses the data utilities module.
The resulting unit dependency cycle should be eliminated, and
the likely solution will be to move that routine into the data
9.7 Misplaced Routines 143

utilities and remove the dependency of that module on the string


utilities.
If that explanation is less than clear, it is because the issue here The term connascence was invented by
is really coupling, and that becomes complex. In simplest terms, Meilir Page-Jones as a description for
coupling in software programs, and
you want to narrow the scope of all your routines as much as he defined several categories under
possible. Given units A and B, if a routine in B creates the need for a that term. For an introduction to the
circular reference between A and B, then you need to relocate that subject, see Wikipedia: https://en
routine. If moving it to A removes the dependency issue which .wikipedia.org/wiki/Connascenc
e
was resolved by a circular reference, fine. If not, then perhaps one
or more routines need to be in a new unit C. When you first begin
to untangle these problems, it will seem difficult, but as you gain
experience with it, the refactoring becomes easier.
To repeat: A routine should be placed in whatever module allows it
to be coded with as little complexity as possible. And complexity
includes the not always obvious matter of unit dependencies. Do
not be surprised, in the course of refactoring, to find that you may
relocate some routines because they now fit more comfortably into
a different module.
Local Components 10
Delphi without components would simply be Turbo Pascal. 10.1 Component Basics . 145
Sooner or later we all write components. This is not always a Good 10.2 Managing Compo-
ThingTM . nents . . . . . . . . . 146
10.3 Component Pitfalls 147
Delphi 1 shipped with the Component Writer’s Guide in the box. 10.3.1 Components Doing
It is evident that many did not study the guide, and even fewer Too Much . . . . . . 147
understood well what it tried to convey. To be fair, the learning 10.3.2 Bad Assumptions . . 148
curve was steep, and adding the Component Writer’s Guide to the 10.3.3 Failing to Handle
mix actually seemed to multiply the effort needed. This was no Exceptions . . . . . . 148
fault of the Guide, rather it was a predictable result of our efforts 10.3.4 Mixing Component
to fully appreciate a massive paradigm shift. and Application Code 149
10.4 Cleaning House . . 150
A man has to know his limitations. – Harry Callahan 10.4.1 Use Library Modules 152
10.4.2 Keep the interface uses
Clause Small . . . . 152
10.1 Component Basics 10.4.3 Refactor Utility Units 153

The first edition of the Delphi Component Writer’s Guide tried hard to
drive home the principle that a component must be ”contract free”,
a phrase which I do not see in the version of Component Writer’s
Guide which shipped with Delphi 7. Perhaps it was considered
insufficiently clear. In simple terms, that means that you drop
it on a form and can immediately interact with the published
properties, to determine details of the component’s behaviors. For
example, place a TLabel on your form and you can then assign the
Caption, and the Font properties, such as size and color.
The notion of ”contract free” becomes more complicated when
components are linked together, but one thing is certain: when
linked, interactions with the Object Inspector should not cause
any exception to be thrown.
If you have not written any components, I urge you to read the Do not overlook the old but still ex-
Component Writer’s Guide first, and start with simple things. I cellent Visual Developer Developing
Custom Delphi 3 Components, by Ray
remember struggling with the first edition, and the one from Konopka. He goes more deeply into
Delphi 7 is much more comprehensible. But then, my reading the practice of component design,
it now is influenced by 25 years of work in Delphi, so it may and the book should be required
simply be a change in my perception. At any event, the Component reading for anyone who seriously in-
tends more than dabbling in compo-
nent design.
146 10 Local Components

Writer’s Guide is the definitive document for learning how to write


components, and you ignore its advice at your peril. Although,
I can say from sad experience that I have struggled with some
components which were very badly written.

10.2 Managing Components

Although one might expect commercial component packages to


follow with care the wisdom of the Delphi Component Writer’s
Guide, there is a surprising variety of approaches in common use.
Add to that the innovations of some long gone predecessor, and
you have a recipe for chaos.
Properly installed, components ought not to be rebuilt when the
project is built. Such gratuitous rebuilds add to the build time, and
deliver no value for their overhead. In some component packages,
you may have observed a folder called Lib, or something similar.
That name is better than most, as it suggests, at least, that it may
have to do with the component library.
The Library Path is the correct place to inform Delphi of where
to find the component packages. In that path, the references you
should add will be to the Lib folder—or equivalent—mentioned
above. If you are writing your own components, you will want to
create such a folder, but what should you put there?
▶ DCU files
▶ DFM files
▶ RES files
▶ ICO files
In short, any file needed in the construction of the components
which is not a PAS file, DPROJ or GROUPPROJ file or INC file.
Some may elect to create their own scheme for managing compo-
nent installation, as none of us enjoy the package dance. However,
before doing such a thing, you must consider those who come after
you. Anything you create which is out of the ordinary, whether
with batch files or custom applications must be documented and
maintained, lest your successors come after you with pitchforks.
So is it worth the initial and continuing costs? Or is it better to
stick with the more widely understood approaches?
10.3 Component Pitfalls 147

10.3 Component Pitfalls

Common pitfalls in designing components include:


▶ Making the component do too much.
▶ Making unwarranted assumptions about the environment.
▶ Failing to prevent exceptions from being thrown.
▶ Intermingling component code with application code.

10.3.1 Components Doing Too Much

Keep in mind that a component is a class. And a well designed class


should have a single responsibility. The functionality of a button
is extremely simple, as it should be. One of the most complex
aspects of a button is when a group of them is interlocked, such
that selecting one releases any other that had been selected. This
is more commonly handled now with radio buttons, but can still
be implemented with a group of TSpeedButton components.
A grid is obviously much more complex than a button, yet it Some grids do much more than
still implements only the functions needed to present rows and others, of course, and that doesn’t
mean that they violate the principles
columns of data. More complex behaviors are usually implemented of good component design. It may
in other components. mean, however, that converting the
displayed data for a report or an ex-
Consider a TDataset and a TDataSource. Some might have thought port may be more complex than with
there was no reason to separate the behaviors into two components, simpler components.
but because they are separate, the simple TDataSource is used with
a TTable, TQuery, TClientDataset, or a TFDMemData. Design patterns should by now have
become commonplace in usage. Cer-
In some cases, a first design may be overly complex, and you tainly Delphi has made use of them
may find that it is a candidate for division into two or more from the beginning, as loose coupling
was essential to the implementation
components, which may work together at run time. Alternately,
of the tool. In particular, the Observer
you may simply need to factor out some behaviors into library pattern is in wide use, including in
modules. Regardless of the approach, the goal must remain the the data components.
same: keeping the component tightly focused.

Changing Perspective

In dealing with a large legacy codebase, it may often seem


that these new rules impose too much of a burden. Changing
the culture takes time, but it is worth being brutal about it
in new code. For one thing, you will be reducing the future
148 10 Local Components

maintenance load, and for another, this model will help in


bringing about the needed cultural change.

10.3.2 Bad Assumptions

Legacy code is always complex. It is all too easy to overlook details


which later become problems. A full rewrite is rarely a realistic
option, because of the lack of a solid specification, and the time
needed for the redesign.
▶ Code defensively.
▶ Expect tests in code to be incomplete.
▶ Take this opportunity to make each refactored routine as
solid as it can be.
Code defensively means expect the worst, and code for it. Things
which can go wrong eventually will. Don’t let your user suffer
because you didn’t make the necessary checks. Make sure the
index is in range before applying it to an array or list. If some data
manipulation is valid only for positive numbers, then make sure
you check for negatives.
Incomplete tests in code allow for errors to sneak through. Ex-
amine the tests to be sure that the coverage is sufficient. Guard
against bad input to your routines.
Refactoring should not only achieve cleaner code, but more reliable
code, as well. One reason for keeping routines small is that when
a routine is too complex, it becomes almost impossible to test
adequately. Let your routines do as little as possible, and do it as
well as possible.

10.3.3 Failing to Handle Exceptions

Here is an example from the Component Writer’s Guide. The fol-


lowing code comes from the TDatabase component. After loading,
the database tries to reestablish any connections that were open at
the time it was stored, and specifies how to handle any exceptions
that occur while connecting.

procedure TDatabase . Loaded ;


begin
// call the inherited method first
10.3 Component Pitfalls 149

inherited Loaded ;
try
if FStreamedConnected then
Open { reestablish connections }
else
CheckSessionName ( False );
except
if csDesigning in ComponentState then
{ at design time ... }
{ let Delphi handle the exception }
Application . HandleException ( Self )
else
raise ; { otherwise , reraise }
end ;
end ;

Correctly handling an exception depends on recognizing the dif- If you are unable correctly to handle
ference between design time and run time. At run time, you will an exception in your try/except cod-
ing, do not swallow it. Raise it again,
ensure that an exception is raised. The component user must un- so it is not lost.
derstand that a component can throw an exception, and therefore
provide suitable coverage in the application code.

Exception Handlers are not Business Logic!

The name should be at least a clue: Exceptions are things which


could not reasonable have been expected to occur. They should not
substitute for ordinary logic. For example, we can reasonably
anticipate that a file we are about to write may need to overwrite
an existing file of the same name. Therefore, we should check
for that, and advise the user before overwriting, offering the
opportunity to modify the name of the file to be saved.
The misuse of exception handlers as logic is especially egre-
gious inside loops, as handling an exception incurs significant
overhead.

10.3.4 Mixing Component and Application Code

A key property of any component is that you can depend on


it consistently doing what it was designed to do. That alone is
reason enough to keep it entirely separated from application code
modules which might be modified for reasons not related to the
component. In a perfect world, unit testing would still protect you,
150 10 Local Components

but we’re considering legacy applications, and code which likely


lacks comprehensive unit testing.

Independence of Components
Third-party components do not A well designed component should be able to provide its
make use of your application code. functionality in any project. If you borrow application code in
Neither should your own components do
so.
building a component, you make that component less indepen-
dent, and less reliable. A developer should never have to fear
that a component will cease working because of some repair
made in application code.
Sometimes it may be difficult to square this with the notion
of not duplicating code. Ask yourself why the component is
so specific to the application. Could you perhaps split the
component into more than one, and what would have been
duplicated code becomes instead a component that can provide
the needed behavior in your application?
Design should always precede coding.

Another excellent reason to separate application modules from


component code is the problem of Unit Dependency Cycles . Com-
ponent code should always be tight, clean, and focused. Too often,
application modules fit none of those descriptions. Remember
too that components are loaded live in the IDE at design time, so
It bears repeating: Components are that we can see our forms. This is another reason to keep things
loaded live in the IDE at design time. as lean as possible. And keep in mind as well that badly written
components not only jeopardize your application, but can render
the IDE unstable.
Few will write a component they do not expect to use widely, nor
should they. Component design and development is more critical
than ordinary coding in your application. Components inevitably
have a larger impact; ripples on the pond can become tidal waves.
The good news is that when you fix a defective component, those
effects are equally wide.

10.4 Cleaning House

As always, in a legacy application, we must play the hand we are


dealt. When you find a component which is tangled up with any
10.4 Cleaning House 151

number of application modules, refactoring is needed, but not as


you might normally imagine.
When your component is caught up in a chain of UDCs , start by
reworking the main module. Remove references to application
modules and try to build the component(s). When you learn what
doesn’t build, then let the compiler guide your repair work. As
you do so, you must choose which way to resolve the lack:
▶ Consider the Delphi libraries, which may often provide sim-
ple replacements for routines which were written years
ago when the libraries were much thinner. This is particu-
larly true of DateTime operations, which have been greatly
increased over the years.
▶ Implement a local function. In rare cases, you might elect to
write a local method to implement what you need. Usually
not; if the need were purely local, the link to another unit
would not have been needed.
▶ Create a new unit to contain needed function(s). This will
often prove to be the best choice, as you are refactoring
existing methods to a new resource. In doing so, you need
to keep the new module(s) as clean and focused as possible.
▶ In some cases, you may find that the coupling to application
code is essential to the behavior of the component. It would
be best then to consider removing the component, and
arranging instead to use units, frames, forms, and ordinary
application coding praxis.
You may prefer to untangle the unit dependencies. Good luck with This is not to suggest that you
that. Those dependencies came into existence because routines should not untangle the dependen-
cies. Far from it—you must. How-
were put into the wrong modules, which in turn necessitated ever, a frontal attack will be expen-
reference to other modules which should not have been needed. It sive, and an incremental approach
is certainly possible to tackle the refactoring head-on, rather than more likely to be manageable. More-
incrementally. The challenge is in determining where to apply your over, you must consider not only the
difficulty of such an attack, but its
efforts to greatest effect. I am not aware of any tool which helps consequences in collateral damage.
much with that, so instead would recommend simply picking a
unit which is involved, and doing the best you can. In general,
though, you will find the task much simpler if you begin with
modules which are procedural code rather than classes. You can
also do a search to find in how many modules each of these is
referenced. The hope must be that the more widely it is used, the
greater will be the benefit from refactoring.
Refer again to the items above. It is common to find in application
152 10 Local Components

code that functions have been written which could now be replaced
Using library functions does not by—or at least simplified by—using library functions. You may
mean you will never encounter er- find the remaining code is a modest percentage of what was there
rors; Delphi has defects, even as any
other non-trivial software applica-
before, and library functions have been well tested before you
tion. received them.
After culling the functions which were obviated by library code,
the remaining functions should be separated out to new modules.
Keeping things clean means keeping the scope in these modules
as narrow as possible.
▶ Freely use Delphi library modules, as needed.
▶ Keep the interface uses clause as small as possible; prefer
putting references into the implementation uses section.
▶ Reject making use of existing application modules.

10.4.1 Use Library Modules

Fair warning: Delphi library modules are generally very solid, but
they are not bug-free. Using them does not exempt you from the
need for unit testing, but they do reduce the likelihood of defects.
Consider the example of DateUtils. In recent versions, Delphi
DateUtils code has been reworked to fully support the ISO-8601
standard. You reap the benefits without doing the research or the
coding. Even if you need specialized routines for handling dates,
you will do well to implement those on top of the Delphi library.

10.4.2 Keep the interface uses Clause Small

Earlier, I advised keeping scope as narrow as possible. Units cited


in the interface uses clause can contribute to UDCs; references in
the implementation uses clause typically do not.

Interface vs. Implementation

Keep in mind the differences between declarations in the


interface and those in the implementation.

In the interface uses clause must be any units which declare


types used as parameters in methods of the class, as well as
any needed for components on the form.
10.4 Cleaning House 153

In the implementation uses clause belong any units which are


referenced in code, but not essential to the form or method
signatures.

The other thing about keeping uses clauses small is that the need
for many references suggests that the class in the module is doing
too many things. Remember the Single Responsibility Principle.
Keep your classes well focused, and you will not need huge uses
clauses.

10.4.3 Refactor Utility Units

In legacy applications, there is a high likelihood that your various


utility modules help create UDCs. At best, they likely contain some
routines which ought to be in different modules. The shortest path
to cleanup in your code is to reduce the use of old modules in
favor of new. You can factor out the old routines, and place them—
appropriately—into new modules which are as narrowly focused
as the old should have been. Sooner or later, you will need to retire
the old routines, but that task can be an incremental one, rather
than an all at once monster.

Sage Advice

You can’t reduce the old problems by repeating the old prac-
tices.

When you have created new modules, you will also want to
remove the old functions from the modules in which they had
been written. The application code can make use of the new
modules, instead. But how is this better than before? And is this
not again intermingling?
In creating new modules, you need to consider how to make them
as clean as a Delphi library module. If you are able to do that,
then locate them in a new folder which makes clear that they are,
indeed, libraries. You and your colleagues will need to keep in
mind stricter rules for these new modules:
▶ Ruthlessly avoid the creation of UDCs.
▶ Build comprehensive unit tests for the new modules, and
diligently maintain them.
154 10 Local Components

▶ Ensure that all team members understand the importance


of these rules.
▶ Use MMX or other tools to ensure these new modules do
not get involved in UDCs.
▶ Consider having the team do code reviews on these modules
at any change.
Again, as rewriting everything is not an option, you need to be
quite rigid in your approach If you are uncertain where to relocate
a routine, you will do better to leave it where it was, and recognize
the repairs are unfinished, than to put it in the wrong place,
creating a new problem in need of repair. Relaxing your approach
will only bring more pain in the long run.
Refactoring 11
Refactoring, by Martin Fowler, is an essential book for developers 11.1 A Workable Ap-
which was published twenty years ago. If you have not read it, do proach to Small
so, now. Change . . . . . . . . 157

Refactoring is also the practice of code restructuring, generally


11.2 The Value of a Data
Module . . . . . . . . 158
undertaken in preference to redesign, especially when the target
code is complex and there is significant risk for damage. As it says 11.3 Testing in a Data
Module . . . . . . . . 159
on Wikipedia:
11.3.1 Information Hiding 159
Code refactoring is the process of restructuring exist- 11.3.2 Minimizing Points of
Contact . . . . . . . . 160
ing computer code—changing the factoring—without
changing its external behavior. Refactoring is intended 11.4 The Challenge of
to improve nonfunctional attributes of the software. Testing . . . . . . . . 161
11.4.1 Massive Routines . . 162
Advantages include improved code readability and
11.4.2 Massive Coupling . 162
reduced complexity; these can improve source-code
11.4.3 Risk is Unavoidable 163
maintainability and create a more expressive internal
11.5 Code for the Main-
architecture or object model to improve extensibility.
tainer . . . . . . . . . 164
Typically, refactoring applies a series of standardised 11.5.1 Resources . . . . . . 165
basic micro-refactorings, each of which is (usually) 11.6 Prefer Library Code 165
a tiny change in a computer program’s source code 11.7 Use Nested Routines 166
that either preserves the behaviour of the software, 11.8 Extract Classes . . . 167
or at least does not modify its conformance to func- 11.9 Prefer Composition 167
tional requirements. Many development environments 11.9.1 The Public Interface 168
provide automated support for performing the me- 11.9.2 The Private Behaviors 172
chanical aspects of these basic refactorings. If done 11.9.3 Notes on Implementa-
well, code refactoring may help software developers tion . . . . . . . . . . 175
discover and fix hidden or dormant bugs or vulnerabil-
ities in the system by simplifying the underlying logic
and eliminating unnecessary levels of complexity. If
done poorly it may fail the requirement that external
functionality not be changed, introduce new bugs, or
both.

As Fowler wrote an entire book on the subject[5], I will not attempt [5]: Fowler (1999), Refactoring
to offer comparable coverage here. There are several key points,
however, which should be kept in mind:
▶ Always leave a unit better than you found it.
156 11 Refactoring

▶ Factor out inline operations, for readability. Assignment


statements, for example, are excellent candidates for capture
in a nested routine. Such things when inline simply increase
the noise level.
A reasonable limit on length is to ▶ Any routine which cannot be seen entirely on your screen
keep routines to under 50 lines of needs work. It is not merely the distraction of scrolling; there
code. And not by using multiple state-
ments per line!
is a real limit to what we may keep focused in our thoughts
at any time. Short, simple, well named routines bring clarity
and ease of maintenance.
▶ Always minimize scope.
▶ Separate business logic from user interface.
▶ Strive for testability.
▶ Prefer library functions to new coding. For one thing, you
are unlikely to be reworking the Delphi source, or even that
from third parties. And if you don’t change it, you won’t
break it.
Much of your work in refactoring will depend on judgment calls,
but there certainly are indicators for evaluating routines which
can be quantified. If a routine is flagged for one or more of the
simple indicators, it is ripe for refactoring. You may wish to add
to it, but this list is a useful foundation:
▶ Lines of code. A good threshold is 50 lines, and the more the
count exceeds that figure, the stronger the need to refactor.
▶ Lines of code on a form. Code in a UI module should be
really small. Testing through the user interface is tedious,
and should be minimized. Therefore, prefer always to put
business logic elsewhere.
▶ Number of parameters passed. A limit ought really to be
about 10, and more than that argues heavily for a rewrite.
However, sometimes you can gather related parameters
into a record, and pass that. Passing a TPoint, is better than
passing X and Y; passing a TRect is better than passing top,
left, height, and width.
▶ Number of local variables. Again, 10 seems a good limit. If
you need more, you probably are doing too much in the
routine. Whether to resolve that with nested routines, or a
small class, or private methods of the class you are in, you
must decide for yourself.
▶ Number of local constants. Probably fewer than 6. And
never, never define locally what should be a global constant.
An excellent example would be CRLF = #13#10; which is so
11.1 A Workable Approach to Small Change 157

pervasive in its utility it should always be a global definition.


▶ Number of public methods. Keep your classes focused, and
keep private as much as possible. TStringList is a very useful
class, but don’t use it as a model—it really does too much,
and not all of it well.
▶ Number of units in your uses clauses. It’s not practical to
assign a numeric limit on this one, but anything over 30
is certainly suggestive of issues which need to be resolved.
There can be special cases where a large number of units
may be referenced, but life will be better if, on average, your
references to other modules are very limited.

Measuring the Need for Refactoring

TMS FixInsight is a static analysis tool which can be used to


advantage in targeting your work. You can run it against a
single file, or against your whole project, and it will give you a
comprehensive list of issues. You can set threshold values for
lines of code, number of parameters, number of local variables,
and so on, and can also enable or disable per issue type whether
or not to report it. So for example, it is very easy to produce a
report on nothing but empty except clauses.

How to strategize your work is a question only you can answer.


“No battle plan survives first contact with the enemy.”
– Helmuth van Moltke
My suggestion is that whatever strategy you plan to use, you must
be prepared to alter the approach as you proceed. In some cases,
it may be practical to focus on major rework to a single module
until it is complete. The more likely scenario is that you set off in
one direction, and must then adapt as other issues become larger
concerns, possibly even blockers to your original plan.

11.1 A Workable Approach to Small Change

We often find in legacy code that routines are overlarge, unfocused,


and all but indecipherable. One method of attack which is useful
is to factor out simple blocks into nested routines, with descriptive
names. For example, there may be forty lines of code initializing
158 11 Refactoring

internal states of the routine. These are ripe for refactoring, and
with very low risk. A modest bit of editing leaves you with a well
named call in place of the forty or more lines which have been
Remember that your nested routines factored out. This amounts to an iterative analysis of functionality,
should also be kept as short as possi- from the inside out. As the outer routine is reduced—and lines
ble.
and lines of code are replaced by well-named calls—your ability
to comprehend what it really is doing is increased.
The nested routines may, in some cases, be better placed in a class.
If they are not useful other than in the present class, then define
the class in the implementation section of the current unit. As
work proceeds, it may become apparent that one or more other
modules contain similar code, and at that point, the class should
be moved to a separate unit.
Always, in such refactoring, keep in mind the long term goal of
testable code.
▶ Nested routines are better than inline code.
▶ A private class is better than nested routines.
▶ A (testable) class unit is better than a private class.
▶ Smaller routines are easier to write, test, understand, and
use.

11.2 The Value of a Data Module

When a form contains multiple datasets, it is a candidate to be


coupled with a data module. Data manipulation and other business
logic have no place in a form—they need to be testable. See Using
Datasets Well (Chapter 15).
Create a new data module and let it be used by the form unit from
which its code was taken. Strive to keep it in the implementation
uses clause. Move the datasets from the form to the data module,
one at a time, and after each move, compile and revise, as needed.
A little extra effort at this point can save much pain later. Once
this is complete, you can begin to examine the form code for
opportunities to relocate data manipulation code out of the form
and into the data module.
Keep in mind that a data module is the place to keep data ma-
nipulation. That includes data-specific business logic. It does not
include utility routines for strings, dates, times, or anything else
11.3 Testing in a Data Module 159

that can reasonably find use in more than one module. Stay fo- A class should do one thing really
cused. Keep your code focused. You may elect to use one of the well. It should not yield to the creeping
features syndrome. See SOLID (Chap-
well-known approaches to Separation of Concerns (section 21.4), ter 22.)
whether MVC, MVP, or other framework, or you may simply main-
tain minimal code in your forms, business logic in testable units,
and data handling in data modules. Whatever your strategy:
▶ Keep scope narrow.
▶ Keep forms as dumb as possible.
▶ Put business logic into well-designed classes in their own
units.
▶ Put data handling into data modules.

11.3 Testing in a Data Module

Although there is certainly cause for testing against a live database,


many basic tests can be run with canned data loaded from XML,
or other files. The advantages to this sort of testing include:
▶ ”Gold” dataset operational testing.
▶ Include corner case data, and anticipated errors.
▶ Well suited to DUnit or other unit test engine.
▶ Comprehensive testing possible.
▶ Faster than against a live database.

11.3.1 Information Hiding

A data module offers a perfect package for encapsulating routines


which are not used elsewhere. It presents another opportunity
to reduce scope, though in some cases, the real opportunity may
be code reuse. Remember that these routines should also be as
narrow as possible in scope. You need to take every opportunity
to reduce scope, and in doing so, to reduce coupling.
From Wikipedia: https://en.wikipedia.org/wiki/Information_-
hiding

The term encapsulation is often used interchangeably


with information hiding. Not all agree on the dis-
tinctions between the two though; one may think of
information hiding as being the principle and encap-
sulation being the technique. A software module hides
160 11 Refactoring

information by encapsulating the information into a


module or other construct which presents an interface.
A common use of information hiding is to hide the
physical storage layout for data so that if it is changed,
the change is restricted to a small subset of the total
program. For example, if a three-dimensional point
(x,y,z) is represented in a program with three floating
point scalar variables and later, the representation
is changed to a single array variable of size three, a
module designed with information hiding in mind
would protect the remainder of the program from
such a change.
In object-oriented programming, information hiding
(by way of nesting of types) reduces software devel-
opment risk by shifting the code’s dependency on
an uncertain implementation (design decision) onto a
well-defined interface. Clients of the interface perform
operations purely through it so if the implementation
changes, the clients do not have to change.

Information hiding is a mechanism which supports keeping scope


narrow. The larger your application, the more essential is narrow
scope.

11.3.2 Minimizing Points of Contact

Ideally, you will want separation between your form and its
business logic. The data module should not be the repository
for all business logic, but only for that portion which is directly
involved with data manipulation. The public interface between
the data module and the form(s) it serves should be as small as
possible.
The business logic which is not tightly linked to database opera-
tions should be in one or more separate units. For a major form,
begin with the form, a data module, and a unit containing business
logic. The form and the business logic unit should have similar
names, so their relationship is obvious. Naming the data module(s)
may be less simple, as there may well be multiple forms served by
a data module. This separation of concerns (section 21.4) should
serve to guide your approach in new work.
11.4 The Challenge of Testing 161

Steps to Attacking Big Forms

Refactoring a large complex form is a daunting task. This


sequence has worked well for me.
▶ Copy the existing module (.PAS and .DFM) to a newly
named module.
▶ Create similarly named data module and business logic
module.
▶ Transfer datasets to data module.
▶ Transfer data manipulation code to data module.
▶ Transfer business logic code to business logic module.
▶ Ensure that these new modules build. Repair as needed.
▶ Alter the code that calls into the old module to call the
new. Ensure that the functionality has not been altered.
Repair, if needed.
▶ Only after these steps should you begin refactoring heav-
ily.
All advice is subject to adaptation. You may find it necessary to
so some minor refactoring in the course of moving code off the
form. Just keep in mind that you want to minimize the work
needed to get the separated modules functioning.

In approaching legacy code, keep in mind the same sort of separa-


tion; begin by creating a data module, and do what is necessary
to move the data operations off the form and into that module.
The more complex the form, the greater the challenge in achieving
separation, but refactoring is always an incremental and iterative task.
Big forms did not spring into existence overnight, and will take
considerable effort to untangle. During this activity, you must also
keep in mind these goals:
▶ Simple routines
▶ Clear, clean coding
▶ Separation of concerns
▶ Testability

11.4 The Challenge of Testing

Refactoring code always brings the risk of introducing defects. It is


best, therefore, if you are able to create tests for the code which can
162 11 Refactoring

be verified on the unmodified code, and again after modification.


The shortest path to achieving a mea- In legacy code, this can be problematic, or even impossible. Early
sure of unit testing is to refactor code in the refactoring, you may have no better choice than to arrange a
into business modules and data mod-
ules, removing it from forms. By it-
set of representative operations which you can verify are correct
self, the refactoring is insufficient, but before and after, in the context of the user interface. As you make
it remains an essential precursor to progress, of course, you should be building a larger body of
workable unit testing. testable code, and reducing that which you cannot unit test.
There will be those who say this is bad practice, but if you are
working on a large legacy code base, you must play the hand you
are dealt. We are not starting over, but must rework code which
already delivers the value we sell. We may have defects, but one
of our goals is to reduce defects. Another is to reduce the cost of
maintenance. In legacy projects, pragmatism decides many issues.
In the beginning, risk may be high, but it is no higher than when
you repair defects with no unit testing capability. Reality is a stern
mistress.

11.4.1 Massive Routines

There are many ways to judge the size of a routine, and one
good one is that if you can’t read all of it without scrolling, it’s
too big. Often I have worked on routines which are hundreds of
lines long. If everything were well named, they would still be
incomprehensible, but it is axiomatic that these monsters also are
peppered with meaningless names. My approach varies somewhat,
based on what I find, and what brought me there. But there are
common actions:
▶ Factor out blocks of assignments.
▶ On multi-level loops, begin at the inside, factor out nested
Keep in mind that most people find it
much easier to comprehend the effect routines with good names.
of positive logic than of negative. If ▶ Apply logic reduction. Simplify, always simplify.
you must use negation, be creative in ▶ Where actions are dependent on state, group as possible.
making it comprehensible. Adding
▶ Create private classes, where appropriate.
partial terms may help.
You cannot fully understand huge routines.

11.4.2 Massive Coupling

Ideally, units will be tested in isolation. Think of the Delphi StrUtils


unit, and consider how to test it. Although it is a very large unit,
11.4 The Challenge of Testing 163

and many tests will be needed, it is a clean module, with minimal


dependencies, and no UDCs. You can write your tests without
having to consider cross connections to other units.
Now look at your legacy code, and consider how to achieve
comparable isolation. Refactoring will certainly be necessary. You
may need to create multiple units, to logically isolate functionality,
and to group related logic. It is also possible that you will find
routines which have dependencies you can’t yet separate, and
you may elect to put those in a module together, gathering the
problems into a smaller area, and keeping the rest of the logic
clean and closer to being ready for testing.
The problem here is coupling, the interlinking of modules and
routines, one to another, until what you see resembles a huge
maze.
You cannot test what you cannot isolate.

11.4.3 Risk is Unavoidable

Conventional wisdom is that you cannot refactor without testing.


That is not true, and you will have to do some refactoring before
you can approach meaningful unit testing. It doesn’t mean that
overnight you will be using DUnit to defend against errors. You
won’t. You can’t. The ideal process for refactoring, we are told, is
this:
1. Run unit tests before refactoring.
2. Rework code.
3. Repeat unit tests.
4. Repair code, as needed.
5. Repeat unit tests... success!
But you are working in legacy code, not in an ideal world, and
your forms are filled with business logic. So you can’t execute
the first step of that plan. What to do? The same thing you do
now, when you must repair defects, or add features: proceed with
caution, one small step at a time. Do your best in testing, following
your normal process.
You cannot reach the goal of testability without taking some risk.
As you proceed, remove business logic from forms, and place
it into testable units. Initially, for fMyOldForm, create uMyOldForm,
164 11 Refactoring

and let that hold the business logic. If appropriate, dMyOldForm


can be the data module in this set of modules. When you move
the code, try also to refactor it well, renaming routines to be self-
documenting, and factoring 800 line monsters into maintainable
code. Eventually, you will find yourself recognizing in the business
logic small routines which are found in other modules, as well.
Then you will need to create another testable unit to contain
these soon to be shared routines. This is progress. Evolution, not
revolution, but it is substantial improvement.
Reducing risk is a goal of refactoring.

11.5 Code for the Maintainer

Sage advice from Coding Horror: https://blog.codinghorro


r.com/coding-for-violent-psychopaths/ Always code as if the
person who ends up maintaining your code is a violent psychopath who
knows where you live.
Or in less paranoid terms: Alternatively, always code and comment in
such a way that if someone a few notches junior picks up the code, they
will take pleasure in reading and learning from it.
Also from the Coding Horror site: https://blog.codingh
orror.com/the-noble-art-of-maintenance-programming/,
observations from Software Conflict 2.0: The Art and Science of
Software Engineering:
Software maintenance is...
▶ Intellectually complex—it requires innovation while placing
severe constraints on the innovator.
▶ Technically difficult—the maintainer must be able to work
with a concept and a design and its code all at the same time.
▶ Unfair—the maintainer never gets all the things the main-
tainer needs, such as documentation.
▶ No-win—the maintainer only sees people who have prob-
lems.
▶ Dirty work—the maintainer must work at the grubby level
of detailed coding.
▶ Living in the past—the code was probably written by some-
one else before they got good at it.
11.6 Prefer Library Code 165

▶ Conservative—the going motto for maintenance is “if it ain’t


broke, don’t fix it”.
It may be very satisfying—for a time—to be the indispensable
coder who maintains a particular area of your application, but
over time, you will begin to feel chained, and indeed will be held
back from other areas of interest because no one else understands
your area. That is a Bad ThingTM , not a good one.

11.5.1 Resources

Others have written better than I could about the process.[4, 5] [4]: Feathers (2004), Working Effec-
The seminal works are in the side margin of this page. They will tively with Legacy Code
[5]: Fowler (1999), Refactoring
richly reward the time you invest. They are worth repeated study,
and will change the way you code.

11.6 Prefer Library Code

Before you write any new code, you should consider whether the
needed functions are already available in library code sources, in
this order of preference.
1. Delphi libraries
2. Trusted third-party libraries or components
3. In-house unit tested libraries
4. In-house libraries, not unit tested
There are costs to any coding activity, and not merely for de-
You may have fundamentally criti-
velopment, but for documentation, testing, and maintenance. It cal routines which depend on library
will always be cheaper to make use of code you do not have to routines, and are concerned about
document or test. the risk of library correctness. You
can certainly create unit tests for li-
The Delphi libraries have evolved over a quarter century and have brary code, but will probably wish
been heavily tested. Some of that code had also come from Turbo to limit the testing to the specific rou-
tines you find critical. Should any er-
Pascal, so has an even longer history. Though they are generally rors be reported, you will then need
reliable, they have occasionally been found to contain defects. Still, to dig more deeply, but the unit tests
they are probably the most widely used Delphi code available, and will have raised a proper warning.
That said, you will need to ensure the
you should make good use of them wherever possible.
absolute correctness of your tests.
Trusted third-party libraries are as beneficial as the Delphi libraries,
but there is that qualifier: trusted. Among the third-party com-
ponents and libraries are products which range from excellent to
166 11 Refactoring

very poor. As the saying goes, trust, but verify. If you have cause
to doubt the reliability of any third-party code, you will need
either to replace it, or heavily test it. Unit testing, if the coverage
of test cases is thorough, can raise such modules to trusted status.
Remember:
▶ Commercial software is not always great.
▶ Free and open source code may be better than commercial.
Or not. Make no assumptions. Trust but verify.
▶ Tests always trump trust.

In-house libraries must be unit tested. If not now, then as soon as


possible. Libraries are the foundation of our projects, and if the
foundation is not rock-solid, the house cannot be.
In the course of refactoring legacy code, be mindful of the oppor-
tunities to replace in-line code, or even subroutine calls, with code
from a higher order in that preference list.

11.7 Use Nested Routines

Legacy projects usually contain lengthy and poorly structured


routines. These need to be simplified, but the goal is hard to reach
when you have only a hazy idea of what the routine really does.
It’s well and good to say you will simply analyze the behavior of
the routine, but if it is 1,000 lines or more, that is risky and time
consuming.
When you work on huge routines, try to work from the inside out,
analyzing small blocks of code, and then moving them into nested
routines with names which clearly state their purpose. These small
routines are easier to understand, and the more of them can be
factored out the clearer the containing routine becomes.
What you will do once you have reached the end of this factoring,
you must decide. You may want to put those nested routines into
a private class, instead. Or you may wish to keep them as simple
nested routines. Your first purpose will have been to render the
containing routine understandable, and once that is done, it may
be time to move to other tasks, for now.
11.8 Extract Classes 167

11.8 Extract Classes

Extracting classes from within large and complex routines may be


a logical next step after factoring out nested routines. Should the
new class be useful in only the current module, you may wish to
declare it in the implementation section, rather than expose it to
external use. Later, you may have cause to use it elsewhere, and at
that point, the likely solution will be to put it in a unit of its own,
and let that be used by any client modules which need it.
Whichever path you follow, the collection into well named nested
routines will have provided a clearer view of the patterns involved.
And when you find yourself factoring out similar nested routines
in another module, hopefully, you will recognize the familiar code
blocks, and move toward consolidation.
Remember that in the nested routines, you are simply moving
things around, and will not affect the testability of the containing
routine. Creating a new class for those behaviors should lead to
something which can be unit tested, however, so that may affect
how you code the class.

11.9 Prefer Composition

As mentioned earlier, inheritance is a limiting means of securing


reuse of code. A good inheritance hierarchy becomes harder to
manage the deeper the inheritance goes. Scope may also become
an issue, as you may wish that in a descendant you were able to
hide some of the ancestor’s methods or properties. If we are to
fulfill the Single Responsibility Principle, then our classes must be
lean and focused, features which argue against inheritance.
Composition and aggregation are similar, but whereas composition
assumes the class owns the member objects, aggregation assumes
that the class holds only references to the aggregated members.
Consider a TStrings object, for example. In composition, the class
must hold an actual instance of a TStringList, with a local copy of
the data, while in aggregation, it need only contain a pointer to
the TStringList which exists elsewhere. So there are performance
considerations, but there may also be concerns over whether the
class receiving the list can alter content. In composition, it is a
local copy, while in aggregation, it is shared.
168 11 Refactoring

Composition and aggregation are examples of information hiding,


a fundamental element of object-oriented programming. In com-
position, the class instance owns its members, and is responsible
to dispose of them. In aggregation, it would hold only pointers, so
must never dispose of its aggregated members.

11.9.1 The Public Interface

In a class which contains a private member of type TStringList,


the class need not expose the content of that list, nor the access
to its members. This is equally true in aggregation, when only
a pointer is held to the actual list. This class, a consumer with
respect to the original list, will do only what it is designed to do,
and no consumer of this class will be able to do more to the data.
The TStringList has a very large public interface, but as a member
of another class, it may be invisible to the consumer of that class.
I have found it useful to have an interfaced version of TStringList
in my own work, and will present a version of it here.

interface

uses
System . Classes ;

type
IDelimitedText = interface ( IInterface )
[ ’{ FB67C93C -8779 -4 F51 - A257 -37 BC89C4A813 } ’]
procedure Add ( const S: string );
procedure Clear ;
procedure Delete ( const Index : Integer );
function Find ( const S: string ;
var Index : Integer ): Boolean ;
function GetCommaText : string ;
function GetCount : Integer ;
function GetDelimitedText : string ;
function GetDelimiter : Char ;
function GetSorted : Boolean ;
function GetStrictDelimiter : Boolean ;
function GetStrings ( Index : Integer ): string ;
function GetText : string ;
procedure IgnoreDupes ( const State : Boolean );
function IndexOf ( const S: string ): Integer ;
procedure PutStrings ( Index : Integer ;
11.9 Prefer Composition 169

const Value : string );


procedure SetDelimitedText (
const Value : string );
procedure SetDelimiter ( const Value : Char );
procedure SetSorted ( const Value : Boolean );
procedure SetStrictDelimiter (
const Value : Boolean );
procedure SetText ( const Value : string );

property CommaText : string read GetCommaText ;


property Count : Integer read GetCount ;
property DelimitedText : string
read GetDelimitedText
write SetDelimitedText ;
property Delimiter : Char
read GetDelimiter
write SetDelimiter ;
property Sorted : Boolean
read GetSorted
write SetSorted ;
property StrictDelimiter : Boolean
read GetStrictDelimiter
write SetStrictDelimiter ;
property Strings [ Index : Integer ]: string
read GetStrings
write PutStrings ; default ;
property Text : string
read GetText
write SetText ;
end ;

function GetIDelimitedText : IDelimitedText ;

Some of the members are not strictly needed, but were added to
make the class more widely useful. That is a slippery slope, and if
you are too quick to add features, you end up with something as
complex as the TStringList with which the work started. Note in
particular the function at the end of the listing. There are many
ways in which you might design an interfaced class, but as my
purpose here was specifically to restrict the public interface, I
elected to define the interface in public, and the implementation
privately in the implementation section of the same unit. The
function is needed then to instantiate the class. You may prefer to
define the interface in a separate module, and perhaps the module
will contain a collection of interface declarations. This is not a
170 11 Refactoring

matter of right and wrong, but of the goals of the design.


The class is declared in the implementation:

implementation

uses
System . StrUtils ;

type
TDelimitedText = class ( TInterfacedObject ,
IDelimitedText )
private
FStringList : TStringList ;
function GetCommaText : string ;
function GetCount : Integer ;
function GetDelimitedText : string ;
function GetDelimiter : Char ;
function GetSorted : Boolean ;
function GetStrictDelimiter : Boolean ;
function GetStrings ( Index : Integer ): string ;
function GetText : string ;
procedure PutStrings ( Index : Integer ;
const Value : string );
procedure SetDelimitedText (
const Value : string );
procedure SetDelimiter ( const Value : Char );
procedure SetSorted ( const Value : Boolean );
procedure SetStrictDelimiter (
const Value : Boolean );
procedure SetText ( const Value : string );
public
constructor Create ;
destructor Destroy ; override ;
procedure Add ( const S: string );
procedure Clear ;
procedure Delete ( const Index : Integer );
function Find ( const S: string ;
var Index : Integer ): Boolean ;
procedure IgnoreDupes ( const State : Boolean );
function IndexOf ( const S: string ): Integer ;
property CommaText : string read GetCommaText ;
property Count : Integer read GetCount ;
property DelimitedText : string
read GetDelimitedText
write SetDelimitedText ;
property Delimiter : Char
11.9 Prefer Composition 171

read GetDelimiter
write SetDelimiter ;
property Sorted : Boolean
read GetSorted
write SetSorted ;
property StrictDelimiter : Boolean
read GetStrictDelimiter
write SetStrictDelimiter ;
property Strings [ Index : Integer ]: string
read GetStrings
write PutStrings ; default ;
property Text : string
read GetText
write SetText ;
end ;

function GetIDelimitedText : IDelimitedText ;


begin
Result := TDelimitedText . Create ;
end ;

The class implements the contract presented by the interface, as


it must. As the class is declared and implemented entirely in
the implementation section, it is necessary to provide a function
to instantiate the class, hence GetIDelimitedText. One purpose of
using interfaces is to limit the public view of a class. The class may
implement additional methods, but they will not be visible to the
consumer of the interface. There is nothing to prevent someone
changing your implementation, or extending the interface, but in
using this approach, you certainly make plain the intention that
the interface is to be the sole definition of the object.
Currently, Delphi help warns that:
All members of an interface are public. Visibility spec-
ifiers and storage specifiers are not allowed. (But an
array property can be declared as default.)

Interface History

The implementation of interfaces in Delphi was created in Delphi


3 specifically to add support for COM. Sadly, that saddles it
with limitations which are not important when used apart
from COM. Particularly, the issue of visibility. In the sample
172 11 Refactoring

code above, the private specifier in the class specification will


not hide the members in that section, as they are made public
by the interface declaration. In practice, that means that the
getter and setter for a property will be visible, like it or not.

In the current implementation, the interface seems a bit noisy, and


so it will remain until it is redesigned. In many cases, however,
I have found it beneficial to follow this model, where only the
interface itself is visible in public. One benefit is that it eliminates
the potential for participation of this code in UDCs.

11.9.2 The Private Behaviors

With an interfaced class defined, it must then be implemented. In


this case, as below:

constructor TDelimitedText . Create ;


begin
inherited ;
FStringList := TStringList . Create ;
FStringList . Delimiter := ’,’;
FStringList . StrictDelimiter := True ;
FStringList . Duplicates := dupIgnore ;
// for IndexOf () :
FStringList . CaseSensitive := False ;
end ;

destructor TDelimitedText . Destroy ;


begin
FStringList . Free ;
inherited ;
end ;

procedure TDelimitedText . Add ( const S: string );


begin
FStringList . Add (S);
end ;

procedure TDelimitedText . Clear ;


begin
FStringList . Clear ;
end ;

procedure TDelimitedText . Delete (


11.9 Prefer Composition 173

const Index : Integer );


begin
if Index < FStringList . Count then
FStringList . Delete ( Index );
end ;

function TDelimitedText . Find ( const S: string ;


var Index : Integer ): Boolean ;
begin
Result := FStringList . Find (S , Index );
if not Result then
Index := -1;
end ;

function TDelimitedText . GetCommaText : string ;


begin
Result := FStringList . CommaText ;
end ;

function TDelimitedText . GetCount : Integer ;


begin
Result := FStringList . Count ;
end ;

function TDelimitedText . GetDelimitedText : string ;


begin
Result := FStringList . DelimitedText ;
end ;

function TDelimitedText . GetDelimiter : Char ;


begin
Result := FStringList . Delimiter ;
end ;

function TDelimitedText . GetSorted : Boolean ;


begin
Result := FStringList . Sorted ;
end ;

function TDelimitedText
. GetStrictDelimiter : Boolean ;
begin
Result := FStringList . StrictDelimiter ;
end ;

function TDelimitedText . GetStrings (


174 11 Refactoring

Index : Integer ): string ;


begin
Result := FStringList [ Index ];
end ;

function TDelimitedText . GetText : string ;


begin
Result := FStringList . Text ;
end ;

procedure TDelimitedText . IgnoreDupes (


const State : Boolean );
const
arr : array [ False .. True ] of TDuplicates =
( dupAccept , dupIgnore );
begin
FStringList . Duplicates := arr [ State ];
end ;

function TDelimitedText . IndexOf (


const S: string ): Integer ;
begin
Result := FStringList . IndexOf (S);
end ;

procedure TDelimitedText . PutStrings ( Index : Integer ;


const Value : string );
begin
FStringList [ Index ] := Value ;
end ;

procedure TDelimitedText . SetDelimitedText (


const Value : string );
begin
FStringList . DelimitedText := Value ;
end ;

procedure TDelimitedText . SetDelimiter (


const Value : Char );
begin
FStringList . Delimiter := Value ;
end ;

procedure TDelimitedText . SetSorted (


const Value : Boolean );
begin
11.9 Prefer Composition 175

FStringList . Sorted := Value ;


end ;

procedure TDelimitedText . SetStrictDelimiter (


const Value : Boolean );
begin
FStringList . StrictDelimiter := Value ;
end ;

procedure TDelimitedText . SetText (


const Value : string );
begin
FStringList . Text := Value ;
end ;

11.9.3 Notes on Implementation

There were several motivations which led to this code:


▶ Implicit memory management—interfaces are reference
counted and disposal is automatic; no need for a try/finally
wrapper.
▶ Narrowly focused functionality. The TStringList is a pocket
multi-tool, and I wanted something to cleanly and simply
handle the comma delimited lists in the application with
minimum bother.
▶ Composition as an aid to keeping focus narrow.

In this case, the delimited lists were to be passed across a COM in-
terface, so it was imperative that the conversion between string and The isolation afforded by properties
list be reliable and unchanging. Making a TStringList a member leaves open the possibility of a com-
pletely new approach to implemen-
of the class hid the plethora of members of that class, and allowed tation in the future. I might decide to
me to be selective in what would be exposed. Moreover, in moving eliminate the use of TStringList in
the code to the latest Delphi compiler, the internal implementation favor of lighter-weight alternatives.
may change radically, and eliminate the TStringList altogether, in
favor of more efficient code from the newer Delphi libraries.
One of the benefits of properties in Delphi is that they hide from
the consumer the implementation, so it is not apparent in the call
whether it is a simple field access or a method call. Similarly, using
composition allows the implementation to be hidden from view,
and the public interface may remain constant, even as the private
implementation is altered. These are among the key principles
which make OOP an attractive methodology.
Removing Code from Forms 12
Code on forms is not easily unit tested. It is possible, however, 12.1 Extract Utility Rou-
to remove so much from the form that the data handling and tines . . . . . . . . . 177
business logic may be fully tested, leaving the form testing as a 12.1.1 Refactoring Praxis . 178
much simpler exercise which ensures no more than the proper 12.2 Building Utility
logical interaction of the controls. When business logic and data Units . . . . . . . . . 179
handling cannot be unit tested, then GUI testing is generally used 12.3 Shared Classes . . . 180
as a fallback, but there are issues: 12.4 Use Data Modules . 181
▶ Access to business logic is indirect; it is difficult to be sure 12.4.1 Common Operations 181
all behaviors have been tested. 12.5 Separate Business
▶ Data handling is relatively slow, and if there are many Logic . . . . . . . . . 183
12.5.1 What is Business
behaviors, then automation testing is necessary.
Logic? . . . . . . . . 183
▶ Test coverage is always a concern, and hard to prove.
12.5.2 Organization . . . . 185
12.5.3 Separate Data Han-
dling . . . . . . . . . 186
12.1 Extract Utility Routines

The distinction between business logic and utility routines is not


always clear. In a large application, it is likely that there will be low-
level utility routines which have some connection to business logic,
so you may need your own specializations of StrUtils and others.
These will not be replacements, but supplements or extensions.
It will always be preferable to make use of Delphi library routines Keep in mind that there is a vast dif-
which meet your needs. These have been through years of use ference between library routines and
the coding of visual components. Li-
and testing, and provide their functionality without placing on braries can be made entirely testable;
the user a burden of low-level testing. Whether to treat third- visual components are as problem-
party libraries in the same way will be something you must judge. atic in testing as are your own forms,
Not all vendors are equally thorough and reliable. Still, if you and the more complex the function-
ality of these components, the more
have confidence in them, they represent a further reduction in difficult it is to ensure they are free
local coding and testing. Where you may have uncertainty, there from defects.
remains the possibility of assembling unit tests for any member
routines you would like to use.
A larger task is to recognize when a code pattern in your application The Don’t Repeat Yourself (section
should be extracted to a utility module. This is a continuing task 21.2) principle suggests that if a code
block occurs more than twice, it
and will yield only to the diligence of developers and their ability should be made a subroutine. Ex-
to recognize familiar snippets. This is a worthy exercise, but not tend this thinking to the number of
modules in which a block of code
repeats.
178 12 Removing Code from Forms

an urgent one. If such little routines are moved into business


logic units while still inside larger routines, they will eventually
be recognized and refactored. The first order of business is to
recognize routines which may be larger, yet useful in the context
of multiple modules.

Dependency Cycles: Be on Guard

It is not a primary goal to reduce Unit Dependency Cycles while


separating logic from forms, but you certainly do not want
to make things worse. You will do well to use the MMX Unit
Dependencies tool often. See section 40.7.

12.1.1 Refactoring Praxis

It is well and good to speak of refactoring, but it may not be


obvious how to proceed. The short answer is: simplify!
▶ Group similar actions. If there are many assignment state-
ments within a routine, try to group them in clusters, being
mindful of those which may depend upon program logic
within the same routine. Those which can be clustered are
then candidates to be removed to a nested routine which
you will, of course, name to make clear what it does. Often
you may find initialization statements which can be grouped
together, but be mindful of sequence dependencies.
▶ Separate data manipulation. It will generally be best to place
all data manipulations into a data module; they certainly do
not belong on a form.
▶ Separate business logic. A separate and testable unit should
contain all the business logic needed for the form. Begin
simply, creating functions and procedures. At some point,
you will recognize that some of these ought to be members
of a class, and then it makes sense to refactor. Others may
be candidates for inclusion in a utilities module which is
shared among multiple modules in your application.
▶ Remove all code that is not directly interactive with the form.
A form is home to controls, and the logic which interlinks
them should remain on the form. Use your judgment here. If
complex logic controls the state of controls on the form, then
perhaps that logic should be in the business logic module,
12.2 Building Utility Units 179

and deliver results to alter control state. Similarly, if control


state depends on data manipulation, then the logic should
be in the business logic module, and that should call into
the data module. In making these determinations, keep in
mind testability.
▶ Arrange shared variables into a class. In the process of
untangling existing code on a form, you will find variables
which must be known to the form, the business logic, and
the data module. As the form must use the business logic
and the data module, you cannot have them also use the
form. Well, you can, but you will simply create a new mess.
Instead, put those variables into a class in another unit. It
can then be used by the form, the data module, and the
business logic, and no dependency cycles result.
Refactoring is an iterative process—it never ends.

12.2 Building Utility Units

Delphi has a large collection of library routines we all use, such


as IntToStr(), DateTimeToStr(), and so on. Your application will
similarly have a collection of utility routines—or should have—
which are specific to the special needs of your application. These
should be collected into modules in the same way as Delphi library
routines. In other words:
▶ Any particular routine should be coded only once. Less to
debug, less to maintain. See Don’t Repeat Yourself , section
21.2, below.
▶ Utility modules will be widely used, so should never intro-
duce UDCs. Note that in Delphi libraries, you will not find
UDCs.
▶ Exercise care in placing these routines into modules. Keep
related routines in a single module. It may sometimes be
difficult to recognize into which module a given routine
should be placed. Avoiding UDCs is a useful metric.
▶ If a utility module needed reference to more than a few other
modules, reconsider your design.
▶ If a utility routine appears to need knowledge of an appli-
cation module, consider passing in parameters to eliminate
that need.
180 12 Removing Code from Forms

Scope Qualifiers

Qualifying a property or method call with the name of the


class in which it is defined is a practice which leads to some
arguments. It is worth noting that such qualifiers seem to be
gaining acceptance, and in very large applications, they may
add significantly to understanding. In some cases, they may be
essential, as a module may reference two classes which have
some identically named members.
As you refactor your code, there is some likelihood that dupli-
cate method names will be found, and that some will require
qualifiers. The mere existence of these duplicates, however,
should trigger an analysis:
▶ Are both methods names as clearly as possible? If not,
then rename as needed.
▶ Are the methods actually functionally identical? If so,
consider relocating both to a shared module.

12.3 Shared Classes

As mentioned above, you may find it useful to create a class which


contains variables needed by a group of modules. If any of these
reflect the state of an instance, then the class will need to be an
instance variable, perhaps of your form. Although I specify the
collecting of variables, there may be instances in which you will
want to add some methods which are specific to the variables in
the class.
In some cases, you will find it sufficient to pass in parameters, but
as you proceed, if the parameter lists become long, then another
approach is needed. It will be very helpful to use a tool like
FixInsight in this process, to warn you when things are getting out
of hand.
You may find that such shared classes can later be designed out.
But in the beginning, your challenge is to reduce the noise level in
the code sufficient to thoroughly understand the operation of the
code.
12.4 Use Data Modules 181

12.4 Use Data Modules

Data modules will make your work easier, by allowing you to


remove from the form all TDatasets, TQueries, TDataSources, and
other data components, and the code which operates on them.
Portions of this work will be very simple, while others will lead
you into refactoring and redesign. Because the existing form code
is likely to be quite tangled, it would be best initially to do the least
work necessary to relocate these items and make them work as
before. This work can be done incrementally, and it is best if you
try it first on a relatively simple form, as you will more quickly
see the sense of it.
Components can be moved first, and the code left unchanged, apart
from adding the data module to the uses clause. You may even
leave the components on the form, for now, though the duplicate
components are likely to cause confusion. Get the components into
the data module, repair code as necessary, and retest functionality.
You will find it is less daunting than it seems. Once the components
and code are in the data module, and the old components and
code gone from the form, you are ready to look seriously at the
transfer of business logic.

12.4.1 Common Operations

Often you will need to transfer data from a dataset used for the
user interface to a somewhat different dataset which will be passed
to a report or export module. There are often many reasons why
the fields list of one is quite different to that of the other. Sometimes
the reasons are excellent, sometimes merely that two different
developers were involved. This give rise to some very predictable
coding:

// Field to field assignments


cdsReport . Append ;
cdsReportRecId . AsInteger := cdsUIRecID . AsInteger ;
cdsReportName . AsString := cdsUIName . AsString ;
cdsReportDate . AsDateTime := cdsUIName . AsDateTime ;
// and so on ...
cdsReport . Post ;
182 12 Removing Code from Forms

This kind of thing is pretty dull to code, and certainly deserves


to be hidden away in a separate module. But let’s consider an
alternative:

type
TXferRec = record
srcField : string ;
dstField : string ;
end ;
const
XferFields : array [] of TXferRec = (
( srcField : ’ RecID ’; dstField : ’ RecID ’) ,
( srcField : ’ UserName ’; dstField : ’ Name ’) ,
( srcField : ’ LastDate ’; dstField : ’ Date ’) ,
( srcField : ’ Items ’; dstField : ’ Qty ’) ,
( srcField : ’ Cost ’; dstField : ’ Price ’)
);
var
idx : Integer ;
srcFld , dstField : TField ;
begin
cdsUI . First ;
while not cdsUI . Eof do
begin
cdsReport . Append ;
for idx := 0 to Length ( XferFields ) - 1 do
begin
srcFld :=
cdsUI . FieldByName (
XferFields [ idx ]. srcField );
dstFld :=
cdsReport . FieldByName (
XferFields [ idx ]. dstField );
if srcFld is TIntegerField then
dstFld . AsInteger := srcFld . AsInteger
else if srcFld is TStringField then
dstFld . AsString := srcFld . AsString
else if srcFld is TDateField then
dstFld . AsDate := srcFld . AsDate ;
end ;
cdsReport . Post ;
end ;
end ;

This small example is simplistic, but imagine a case where you


have some thirty or more fields to copy, and you can see the benefit.
12.5 Separate Business Logic 183

Moreover, the if/then/else chain could be avoided by creating


a number of overloaded procedures to handle the various field
types.
It will be more effective to use the DisplayFormat property of each
column than to make your destination fields into strings and
format them during the copy. Considering Separation of Concerns
(section 21.4) quickly makes obvious that data and presentation
are distinct concerns.

12.5 Separate Business Logic

You will need to create new business logic units which will be the
(testable) home of the large routines which are now on your form.
Similar to the process of moving things to the data module, the
initial move will be little more than a transplant. Along the way,
you will find opportunities for testing and for refactoring. The nice
thing about refactoring is that with each round of activity, the code
becomes less murky, more comprehensible. And to the degree
that you are able to factor out subroutines, especially common
subroutines, you are reducing the maintenance burden of the
future.
The immediate benefit is that you are reducing chaos, and enhanc-
ing your ability to conduct current maintenance with less risk and
more certainty. The growing benefit is that you will keep moving
in the direction of fully testable business logic.

12.5.1 What is Business Logic?

Business logic is a broad category which refers to the logic which


delivers the primary operations of your product. In the first section
of this chapter, we considered utility modules, which you should
be able to identify based on similarity to Delphi library modules.
Business logic, on the other hand, is less general than utility
routines, and less specific than data manipulation. So what does
that include?
▶ State management. User interaction with a form affects what
the user is permitted to do at any given point. It’s reasonable
to put the trivial state issues—interaction and interlock
184 12 Removing Code from Forms

among controls on the form—in event handlers on the form.


But more complex state management affecting the operation
of business routines or data module behaviors belongs not
on the form, but in a business unit, where it can be tested.
▶ Implementation of business rules.
▶ Task ordering. Follows from state management. Some tasks
cannot be executed until others have been completed.
User preferences—settings—are business logic, as the affect the
behaviors of your primary logic. One example would be where the
user has a setting for the numeric precision to be used in particular
contexts. There will be a settings form, and there will be business
logic related to that form, to handle the persistence of such settings.
But other business logic in any number of modules will consult the
settings and pass them to routines whose operations are affected
by these settings.
▶ Form business. There will likely be a business unit associated
with each business form.
▶ General business. There may be shared modules which
encapsulate business rules and logic which apply to the
application in general, or to a category of operations within
the application.
▶ Separation of Concerns (section 21.4) requires us to analyze
the scope of business rules and business logic, and to place
them logically within a collection of modules. Failure in this
process makes maintenance more difficult, and may result
in duplicated code.
Realistically, the arrangement of business logic and business rules
in the code base must be under continuous review. A routine in
a business unit relating to a single form may prove to be useful
with other forms. At that point, refactoring is needed.
▶ Relocate code from a narrow scope to a broader one, rather
than replicating it in other modules.
▶ Relocate code from a more general scope to a narrower one
when you find it is now used only in a single form. This may
be more difficult to manage than widening scope, especially
when the code base has not yet been fully refactored.
▶ Refactoring includes recognizing that existing code may
need to be extracted to a utility module. This may happen
when existing routines are large and unruly, and code has
been replicated in multiple routines.
12.5 Separate Business Logic 185

Legacy projects are often so cluttered that you may find it very If a module lacks focus, perhaps it is
difficult to know where to put some pieces of code. Don’t let trying to serve several different areas
of work. If that is the case, then you
that be a barrier to refactoring. Simplification of the existing will do well to separate out multiple
code is essential, and early decisions are subject to change as the modules, and refactor to give each a
appropriate structuring becomes evident. You must recognize that clear focus.
long routines obfuscate structure, and as you chip away at the
clutter, your understanding of the purpose of any piece of code
will increase.
Code development must embrace change. Hitting a moving target is
more challenging, but our work is made far more difficult if we
insist on a fully detailed design before coding.

12.5.2 Organization

Like any other code, business logic must be organized and placed
in suitable modules. Some code will comprise general routines
which can be assembled into business libraries. Other code will
be specific to a particular form. Later in the book I will offer rules
for best practices, but for now, accept that one of them is not to
duplicate code.
If your application is a large one, then consider how best to
organize these modules. One approach would be through file
names, such as these:
▶ f_MainForm—The form module
▶ bl_MainForm—The business logic for the form
▶ d_MainForm—The data module for the form

Another approach would be to use folder names:


▶ Forms
▶ BLogic
▶ Data

Which way to manage it is largely a matter of preference. However,


you will generally want the module names of modules in a 1:1
relationship to be similar, so I tend to favor just using file names.
186 12 Removing Code from Forms

12.5.3 Separate Data Handling

Data manipulation is no more appropriate in a form than is


business logic. How to separate the data into modules is something
you will have to work out for yourself. There will be data modules
which have content useful to multiple functional modules, making
them similar to library modules, but specific to data. There may
also be modules in which the data organization and handling is
unique to the needs of a particular form.
As with business logic, avoid duplication of functionality. As
mentioned above, you will do well to use the DisplayFormat of
each field to handle presentation issues. Keep in mind that the
DisplayFormat needed in the UI may be different to that needed in
a report or for Excel export.
Fixing Erroneous Coding 13
What do we mean by erroneous coding? There are many kinds of 13.1 Errors of Function . 187
error which creep into your code. 13.2 Reduce Noise . . . . 188
▶ Errors of function (defects) 13.3 Reduce Logic . . . . 188
▶ Errors of form—badly designed code, badly written code 13.3.1 Using Partial Prod-
▶ Errors against testability ucts . . . . . . . . . . 189
13.3.2 Prefer Positive Logic 189
▶ Repetition of code
13.4 Loop Forms . . . . . 190
13.5 Minimize Tests . . . 193

13.1 Errors of Function 13.6 Use Arrays . . . . . 196


13.6.1 Static Arrays . . . . . 196
13.6.2 Dynamic Arrays . . 197
Under errors of function, consider: 13.6.3 Open Arrays . . . . . 197
13.6.4 Generic Arrays . . . 198
▶ Empty except clauses
13.6.5 Constant Arrays . . 199
▶ Empty finally clauses
13.6.6 Arrays vs. Cases . . 199
▶ Unnecessary boolean tests
13.7 Summary . . . . . . 200
▶ Out of place object instantiation
▶ Hard-coded constants
▶ Multiply defined constants
▶ Uninitialized variables
▶ Object freed by other than owner
▶ Indices not range-checked
▶ Arithmetic overflow
More could be added, but the idea should be clear.
One of Delphi’s greatest features since the first release has been
structured exception handling. And yet, many developers, 25 years
on, still fail to use exceptions properly.
Constants are both misused and underused. Misuse includes hard-
coded literals in code which should have been defined constants.
It is common that there is very limited use of structured constants,
such as constant arrays of records, which often can be used to
simplify run-time logic.
Object disposal is another area of abuse. Objects should always Battles have raged in online discus-
be freed by the class which created them. And FreeAndNil is sion groups over whether to use
Free or FreeAndNil. Nothing will
not a universal solution. The use of interfaces or smart pointers be gained by revisiting the argu-
to facilitate automatic disposal may be an attractive alternative. ments here, but you may find them
easily online.
188 13 Fixing Erroneous Coding

Spring4D also implements a very attractive Shared<T> record for


this purpose.

13.2 Reduce Noise

Sources of noise in code include:


▶ Tests repeated without need
▶ Blocks of assignment statements inside business logic
▶ Thoughtless manipulation of strings for captions and re-
peated string constants
▶ Failure to create subroutines where appropriate
▶ Repeated (duplicate) tests or assignments

Noise obscures code meaning, and distracts from comprehensi-


bility. Some of these issues are easily and quickly resolved with
nested functions. Others will require a thoughtful redesign of
constants.
Pattern recognition is a very human skill, and an essential one
for developers. We don’t have an arsenal of weapons which will
help us root out repetition, and must depend on developers to
recognize it.
Some issues will yield to assessment with FixInsight. We need
good tools, but our sharpest tool is our own mind, and we need to
use it, constantly. Refactoring is not simply a code rework action,
but a mindset. Constant vigilance.

13.3 Reduce Logic

De Morgan’s Laws (https://en.wikipedia.org/wiki/De_Morga


n’s_laws) should be second nature to us all. Keeping it as simple
as possible, we have:
▶ not (A or B) = not A and not B; and
▶ not (A and B) = not A or not B

Logic reduction is an essential aspect of coding, and should be


done routinely. When more than a couple of terms are involved,
and no effort is made to reduce the logic, it is very difficult to
recognize the effect of the logic as you read the code.
13.3 Reduce Logic 189

13.3.1 Using Partial Products

Sometimes you will need to deal with a large number of factors


to reach a logical result. Some of the factors may be members of
members of objects. Putting all of that into a single expression is
probably not a good approach.
▶ There is a limit to the number of variables we can keep in our
minds at one time. Some research concluded that the limit
was seven; more recent work suggests a lower limit when
devices of repetition cannot be applied. Simpler is better.
▶ Mixed negative and positive logic increases complexity.
▶ Repeated use of terms suggests the value of nested logic.
▶ Use of partial products usually simplifies debugging.

Given that you are looking at a situation in which all terms must Sometimes you must deal with eval-
be evaluated at once—no hierarchical logic—then partial products uation routines which have side-
effects. In other words, the evalua-
can certainly help. tion itself alters one or more of the
input conditions to the evaluation.
▶ Group together all the positive terms, and separately all
This is a risky practice, and certainly
the negative terms. Alternately, you might wish to create complicates later coding, especially
variables into which to place the complement of the negative as it is not an expected behavior.
terms, so the entire expression can be evaluated in positive
terms.
▶ Create variables for the partial products.
▶ Finally evaluate the simplified partial products.

Refactoring advice commonly refers to explaining variables, which


are just variables which allow the terms to be simplified. Careful
naming will help to make clear what the evaluation really does.
None of this matters to the computer, but a good refactoring of a
multi-term monster is extremely beneficial to maintenance.

13.3.2 Prefer Positive Logic

Most people recognize logical results more easily when the terms In an extreme case I saw a massive
are expressed in positive logic. Occasionally I see code so snarled abuse of the C ternary operator in
which each of the three elements was
up that multiple lines, multiple levels of parentheses, and multiple so long that it required horizontal
inversions are needed to get to the result. It is rare that such a mess scrolling to read the line. And deep
can’t be significantly reduced, and made comprehensible. in that mess was an error in the use
of parentheses. Visual C++ compiled
Sometimes these things evolve because a developer sits down to it; Borland C++ Builder did not. But
add a term, and rather than untangle what was there, just looks what was the effect on the intended
logic?
for the easiest way to add his term and run for cover. Once you
190 13 Fixing Erroneous Coding

accept the need for refactoring, it is simply unacceptable to do that.


Always leave the code better than you found it!
Consider this all too common pattern:

if ( not CheckBox1 . Checked and


not CheckBox2 . Checked and
not CheckBox3 . Checked ) then
begin
// Active code here .
end ;

Which should really be expressed as here:

if not ( CheckBox1 . Checked or


CheckBox2 . Checked or
CheckBox3 . Checked ) then
begin
// Active code here .
end ;

The benefit becomes obvious in more complex expressions, but


in general, most people find it easier to consider the result of the
positive logic with a final inversion than the previous form, with
each term inverted. Another common bad practice is:

if ( CheckBox1 . Checked or
CheckBox2 . Checked or
CheckBox3 . Checked ) then
else
begin
// Active code here .
end ;

13.4 Loop Forms

The most commonly used loop in Delphi seems to be:

var
idx : Integer ;
begin
for idx := 0 to Count - 1 do
begin
// ... code here
end ;
13.4 Loop Forms 191

Nothing wrong, if you really need to visit every item in the


collection. But if you are looking for a terminating condition, it
may be better to use:

function IsFound ( const AValue : string ): Boolean ;


var
idx : Integer ;
begin
idx := 0;
Result := False ;
while ( idx < List . Count ) and not Result do
begin
// ... code here
Result := ATestFunc ;
Inc ( idx );
end ;
end ;

Or you may recognize that the loop will always execute at least
once, so you might use:

function IsFound ( const AValue : string ): Boolean ;


var
idx : Integer ;
found : Boolean ;
begin
idx := 0;
Result := False ;
repeat
// ... code here
until Result or idx = List . Count ;
end ;

The point is that there are choices, and although the for loop is very
often used, it may not be the most natural choice for the work to
be done. This has more to do with clarity of code than correctness.
The for loop is usually applied when we need to examine each
member of a collection, and when used for a different purpose,
will require us to use Break or Exit for termination. The other loop
forms are inherently conditional, with a test included at either the
top or bottom of the loop. This is a clear signal to anyone reading
the code that the expectation is that the loop will not be expected
to iterate over all members.
192 13 Fixing Erroneous Coding

Personal Preferences
There may be those who prefer the for loop to other varieties,
and as already mentioned, this is not an issue of correctness.
There is value to using the idioms you find easiest to understand,
though in a team situation, others may find your preferences
less than transparent. Keeping things in perspective, there are
many other issues in legacy projects which will be of greater
importance than your choice of loop style.
I do think that it is always beneficial for us to review our own
practices, and sometimes to change them for the sake of clearer
expression of function.

A while loop is very natural when working with a dataset, for


example:

function IsFound ( const AValue : string ;


ADataset : TDataset ): Boolean ;
begin
idx := 0;
Result := False ;
ADataset . First ;
while ( not ADataset . Eof ) and not Result do
begin
// ... code here
ADataset . Next ;
end ;
end ;

If you use FixInsight, you may also have seen that it will complain
about a for loop in which the index is not referenced. As mentioned
above, it’s not intrinsically incorrect, but less natural and obvious
than using a loop form which is terminated by a condition in data
being met.
In newer versions of Delphi, you also have the option of another
loop form:

function IsFound ( const AValue : string ;


Although this loop form is simple
and clean, be aware that the order of AStrings : TStrings ): Boolean ;
iteration is not specified. At present, var
it proceeds from 0..n, but there is no s: string ;
guarantee that will not change. The begin
processing order is considered to be idx := 0;
a detail of implementation.
Result := False ;
13.5 Minimize Tests 193

for s in AStrings do
begin
// ... code here
end ;
end ;

Using the most natural loop for the task is a matter of clarity, as
are so many things.

Objections to Break and Continue

At the risk of triggering flames, there are those who object to Some consider these to be as ob-
the use of Break and Continue, almost as strenuously as to the jectionable as goto, but I disagree.
Break and Continue are structured
use of goto. The common objection is to having multiple exits mechanisms which cannot arbitrarily
from a routine, and some will argue that these are merely goto cross block boundaries.
in disguise. Although there is some validity to the view, any
high level language is an abstraction, and a look into the CPU
window will soon show you that jmp label is commonplace,
and jmp is clearly a goto.
I find the arguments against Break and Continue to be rather
weak. They are structured flow management, and I see no
rational objection to their use. That said, it is not unusual that
code can be restructured to make the flow simpler, and that is
always a benefit.

13.5 Minimize Tests

There is a sense that if/then/else is a code smell. It is no more


practical to write software without branching than without assign-
ments, so how can this be a smell? Tests are necessary elements in
code, but they can be overdone, or badly done.
Everything should be made as simple as possible, but
no simpler. –Albert Einstein
If we find a lengthy if/then/else tree in our code, it is almost
certainly a code smell; there are better ways (See 1). Sometimes
an array can help, other times, you may find a case statement
attractive. In other cases, analyzing the conditions tested may lead
to you recognize an opportunity for logic reduction, which in turn
may suggest factoring out one or more nested routines.
194 13 Fixing Erroneous Coding

Deciding which strategy to follow will depend on what you need


to achieve. If this is early in the refactoring process, then likely
you cannot use unit tests, and should prefer a solution which
minimizes the risk of altering functional logic. If you are working
on code which is supported by unit tests, you can take greater
risks, as you are able to validate the end result.
One thing you should always consider is the factoring out of
nested routines for the various branch operations. In other words,
in this code:

if ConditionA then
begin
// block A
end
else if ConditionB then
begin
// block B
end
else if ConditionC then
begin
// block A
end
else
begin
// block D
end ;

Each of the code blocks is a candidate to become a nested routine.


Moreover, as you consider these blocks, you may find redundant
content. You may even find identical functions, perhaps with
different variables.
Many of these large decision trees began small, and people simply
added on, as the application complexity grew. Sometimes you
will even find the opportunity to combine sections, as one of your
predecessors failed to recognize duplicated code:

if ConditionA or ConditionC then


begin
// block A
end
else if ConditionB then
begin
// block B
end
13.5 Minimize Tests 195

else
begin
// block D
end ;

It is also common, especially when testing multiple conditions for


each case, that you can reduce logic by nesting blocks.

if ConditionA then
begin
// block A
end
else if ConditionA and ConditionB then
begin
// block E
end
else if ConditionB then
begin
// block B
end
else if ConditionC then
begin
// block C
end
else
begin
// block D
end ;

Such a tree can become this:

if ConditionA then
begin
// block A
if ConditionB then
begin
// block E
end
end
else if ConditionB then
begin
// block B
end
else if ConditionC then
begin
// block C
196 13 Fixing Erroneous Coding

end
else
begin
// block D
end ;

The possibilities are huge, and the larger the tree, the more possi-
bilities you must consider. When you analyze the logic flow, do
not be surprised if you find an error or two, particularly where
multiple qualifying conditions are used to select a particular exe-
cution block. That sort of complexity is a major reason to avoid
such constructs wherever possible.

13.6 Use Arrays

Delphi offers a number of array types:


▶ Static
▶ Dynamic
▶ Open
▶ Generic
▶ Constant

13.6.1 Static Arrays

A static array is a variable with dimension(s) defined at compile


time. The array may have multiple dimensions, but each dimension
is constant. A typical declaration looks like this:

var
SmallIntArray : array [0..15] of Integer ;
// indices need not be zero - based
RangeArray : array [3..28] of Word ;
AnotherArray : array [ Byte , 1..10] of Word ;
// be careful !!
LargeArray : array [ Integer , 0..3] of Word ;

In the third and fourth declarations, we see one dimension of each


is defined as a number type. This means that the dimension will
be the full range of the type used. In the fourth declaration, using
Integer means that the first dimension will be from -2,147,483,648
to 2,147,483,647, and with a small dimension of 4 and a type
13.6 Use Arrays 197

Word, the total size will be 4,294,967,295 * 8 bytes! So in using


declarations of this form, be careful.
It can also be useful to define an array dimension using a custom
type, such as an enumeration:

type
TFeatures = ( feDoors , feWindows , feRegisters ,
feReceptacles );
var
Room : array [ TFeatures ] of Integer ;

13.6.2 Dynamic Arrays

A dynamic array is a variable declared without a known length:

var
idx : Integer ;
lengths : array of double ; // dynamic
begin
SetLength ( lengths , 12) ;
// now you can assign values :
for idx := 0 to Length ( lengths ) - 1 do
lengths [ idx ] := idx + 1;
end ;

This example obviously does nothing useful, but should suffice to


illustrate.

13.6.3 Open Arrays

Delphi supports passing open array parameters to methods. It is


not uncommon to have need of a routine which will operate on
all the members of an array where the number of members is not
known at compile time. Here is an example:
function Mean(Data: array of Double): Double;
A more familiar construct is found in the Format function: Yes, weakness. The Format function
is lifted from the C routine of the
function Format ( const Format : string ; same name. C is known for shoot
Args : array of const ): string ; from the hip possibilities, but not so
much for a safe coding environment.
The weakness of the coding in Format is that the array members The point of this book is to suggest
must be equal in number to the format specifiers in the format ways to make code more robust. This
seems apt.
198 13 Fixing Erroneous Coding

string, and the responsibility for that falls to the programmer.


In your own code, you may prefer what seems a more robust
approach:

type
TmdFormattable = record
Format : string ;
Value : string ;
end ;

function Fmt (
const Content : array of TmdFormattable ): string ;
var
idx : Integer ;
formats , values : array of string ;
begin
SetLength ( formats , Length ( Content ));
SetLength ( values , Length ( Content ));
for idx := 0 to Length ( Content ) - 1 do
begin
formats [ idx ] := Content [ idx ]. Format ;
values [ idx ] := Content [ idx ]. Value ;
end ;
ApplyFormats ( formats , values );
end ;

The end result is the same, but where the caller creates the Content
array, it will be clear that format/value pairs must be filled.

13.6.4 Generic Arrays

Support for generics was added in Delphi 2009, and is well worth
looking at if you have not done so. There are many benefits to their
use, including the potential to implement dynamic arrays which
require less checking by the consumer. You can, for example, code
them to allow you to add members without regard to the size of
the array before the assignment. This can be convenient in many
situations, and is helpful in reducing maintenance issues, since
the array itself is designed to avoid range errors.
13.6 Use Arrays 199

13.6.5 Constant Arrays

The constant array is declared like a static array but the declaration
includes initialization.

const
Months : array [1..12] of string = ( ’ January ’,
’ February ’, ’ March ’, ’ April ’, ’ May ’,
’ June ’ , ’ July ’, ’ August ’, ’ September ’,
’ October ’, ’ November ’, ’ December ’);

It is as simple as it looks, but you can do some very useful things


with constant arrays of records. Such constructs can help eliminate
a good deal of conditional logic in your code.

13.6.6 Arrays vs. Cases

Another common pattern is the use of a case statement to imple-


ment what could more clearly be accomplished with an array.

case colIdx of
0: Caption := ’ One ’;
1: Caption := ’ Two ’;
2: Caption := ’ Three ’;
end ;

This would be clearer:

const
colCaps : array [0..2] of string = (
’ One ’, ’ Two ’, ’ Three ’);

// then :
Caption := colCaps [ colIdx ];

This code comes with a risk, however, and might better be coded
with this range check:

const
colCaps : array [0..2] of string = (
’ One ’, ’ Two ’, ’ Three ’);

// then :
Caption := ’’;
if colIdx in [0..2]
200 13 Fixing Erroneous Coding

Caption := colCaps [ colIdx ];

The case statement includes an intrinsic range check which you


must code directly with the lookup table. But the case statement
also failed to handle the out of range case, which could have been
written:

case colIdx of
0: Caption := ’ One ’;
1: Caption := ’ Two ’;
2: Caption := ’ Three ’;
else
Caption := ’’;
end ;

The array remains the better approach, I think.

13.7 Summary

This chapter has attempted to present in relatively simple terms


a collection of problem areas which may be much smaller than
what you see in your own project. Legacy projects in Delphi tend
to have many common features. However, the problem domain
always differentiates projects. Even when the projects share an
area of business, the different application focus areas will draw
out differences in the problem areas. Specifics will vary, yet the
general categories of problems and errors will be very similar.
You will have to draw your own conclusions based on your own
projects.
Class and Record Helpers 14
Class helpers have been available in Delphi for years, and more 14.1 Alternative Ap-
recently, have been added in support of records. We can also see proaches . . . . . . . . 204
them now in support of value types. However, you will find many 14.2 TStringHelper . . . . 204
warnings online about the use of class helpers. So should you use 14.3 Legacy Cleanup . . . 208
them, or not?
14.4 More Insights . . . . . 209
Over the years, there have been battles over the use of numerous
language features:
▶ goto
▶ with
▶ FreeAndNil
▶ Break
▶ Continue
▶ Class helpers
Any feature can be abused, and most can be used well, though
some need more caution than others. Even goto, which most would
agree should be avoided, has been supported in some usage.[12] [12]: Knuth (1974), ‘Structured Pro-
gramming with goto Statements’
The class or record helper is objected to by some because only
one such helper can be active for any class. It is a bit of syntactic You can define more than a single
sugar, not a first-class language feature like the class extension class helper for any given class, but
the compiler will apply the one near-
approach in C#. However, that is not a reason to condemn its use, est in scope. Not recommended. In
subject to some reasonable cautions. At heart, the helpers can be the most recent releases, you can also
very useful, especially as they may be used to overcome some create record helpers, and now that
unpleasant features—or their lack—in components. records are lightweight classes, that
is very useful.
Note that as string and Integer are not classes, their helpers are
in fact record helpers:

type
TStringHelper = record helper for string

There Can be Only One!

To avoid the problem that only one such helper may be active
at a time, let the unit which contains it be added to the end
of the implementation uses clause, ensuring that if there is
202 14 Class and Record Helpers

another helper for the same in scope, you control which will
win the contest. This may sound risky, but in practice seems
much less so. I have not found any cases where I considered
the benefits to be worth the risk.

Note that when you add methods Often, a compelling reason to use a class helper may be in pre-
with a class helper in this fashion, the senting a more consistent, or at least convenient interface to a
added methods appear to be a part
of the class, but the original methods
component, and one which may allow for added routines which
remain accessible. You will need to de- work around troublesome component functionality. For example,
sign your helpers with this in mind, a spreadsheet interface component I’ve used is inconsistent in
to avoid introducing new problems. its reference to rows and columns. In some calls, it will be (row,
column), but in others, (column, row). By using a class helper I
introduced methods which made the interface consistent.

Existing Class Helpers

When class helpers were introduced in Delphi 2005, we were


cautioned about their use, and the manner of that use. At the
time, of course, they were a new feature, and begged to be
used. The possibility of problems later was probably ignored
by many.
In 2020, there are class helpers in use in Delphi library code,
which may cause issues for some of us. My own use of class
helpers has been chiefly to extend or simplify the interface
to third-party components. In one case (which I won’t name)
there were inconsistencies in the ordering of row and column
parameters in some methods which made for annoying errors.
A class helper allowed a more regular use of parameters, and
less verbose interface. The result was increased productivity,
and simplified maintenance. And in the circumstance, the
likelihood of collision with another class helper was a close
approximation of zero.

We have long had the alternative If I seem to be harping on the issue of class helpers and conflicts,
of implementing our own compo- it’s only because this is an important issue. As Delphi adds more
nent which inherits from a commer-
cial component we wish to augment.
class and record helpers, the need to add your own is reduced, at
Though I have done this, I prefer least to augment Delphi libraries. Remember that there is no reason
not to, as there can be complications to implement class or record helpers to augment your own code,
when we update to a newer release as you are able to simply add the functionality to your own classes.
from the vendor. The added function-
ality in the class helper is less directly The most common use case is likely to be in extending or modifying
coupled, and somewhat less likely to the functionality of third-party code, such as components.
be affected by these updates.
203

Class and record helpers in new releases of Delphi create areas you
will do well to leave untouched. The TStringHelper is rather exten-
sive, and very useful. A partial list of record helpers includes:
▶ TBooleanHelper
▶ TByteHelper
▶ TCardinalHelper
▶ TCharHelper
▶ TDoubleHelper
▶ TExtendedHelper
▶ TGuidHelper
▶ TInt64Helper
▶ TIntegerHelper
▶ TNativeIntHelper
▶ TNativeUIntHelper
▶ TShortIntHelper
▶ TSingleHelper
▶ TSmallIntHelper
▶ TStringHelper
▶ TUInt64Helper
▶ TWordHelper
This list is not exhaustive, but suggests the rich possibilities now
supported by class and record helpers. Time spent exploring these
will be rewarded.
My experience has been that implementing a small number of
class helpers in application code brought a great return. Usually, The real beauty of the class helper is
the helpers have been used on complex classes, and have added that its methods appear to be mem-
bers of the class being helped, and
quite a few methods. The benefits were large because the classes it can access the public members of
were widely used, and the helpers made possible a large reduction that class. My methods in helpers are
in repetitious code without creating utilities to which an object usually very small snippets, but very
would have to be passed. useful.

A Helper is not a Partial Class

There is a tendency to think of a class helper as a partial class,


but it is nothing of the sort. A class or record helper cannot
define storage. And only one helper can apply to a class or
record at any point in your code.
204 14 Class and Record Helpers

14.1 Alternative Approaches

It is true that there are other ways to achieve what the class helper
provides: you could create your own component, for example,
descending from the one you wish to enhance. The creation of a
class helper, however, is less work, and if you really need to add
only a couple of routines, then the class helper will be much easier.
But even for larger collections of routines, the class helper may be a
very attractive solution, as it will usually offer lower maintenance
than designing your own component.
There is also the issue of testing the class helper, but the relative
difficulty of that usually depends on the challenge of testing the
class it helps.
In the end, you must evaluate for your own project(s) the relative
merits of using class helpers, but don’t merely write them off
because some have warned against their use.

14.2 TStringHelper

In Delphi 10.2 Tokyo you will find System.SysUtils, which contains


TStringHelper.

There is a lot going on in TStringHelper, and no reason to go into


it deeply here, but a few basics are worth reviewing, to give you a
sense of the value, if you have not used it.
To begin, note the declaration:
TStringHelper = record helper for string
Helpers look much the same, whether they are class helpers or
record helpers. Now I will take the liberty of showing a few
declarations from TStringHelper out of context:

function Trim : string ; overload ;


function IsEmpty : Boolean ;
function Contains ( const Value : string ): Boolean ;

// Note that you can call these using fluent syntax


if MyStr . Trim . IsEmpty then
MyStr := ‘ Nada ’;
14.2 TStringHelper 205

Many members of the TStringHelper class will be familiar, as they


duplicate routines which you have been accustomed to calling
from SysUtils or StrUtils. Now, consider the depth of coverage:

Table 14.1: TStringHelper Methods


Compare CompareOrdinal CompareText
CompareTo Contains Copy
CopyTo CountChar Create
DeQuotedString EndsText EndsWith
Equals Format GetHashCode
IndexOf IndexOfAny IndexOfAnyUnquoted
Insert IsDelimiter IsEmpty
IsNullOrEmpty IsNullOrWhiteSpace Join
LastDelimiter LastIndexOf LastIndexOfAny
LowerCase PadLeft PadRight
Parse QuotedString Remove
Replace Split StartsText
StartsWith Substring ToBoolean
ToCharArray ToDouble ToExtended
ToInt64 ToInteger ToLower
ToLowerInvariant ToSingle ToUpper
ToUpperInvariant Trim TrimEnd (deprecated)
TrimLeft TrimRight TrimStart (deprecated)
UpperCase

So the TStringHelper may seem pretty low key, but there is more
here than you may imagine. You will find that using helpers in
fluent coding your code will be dense and concise, without any
loss in readability—in fact, I would argue that the readability is
improved. In part, this comes from reducing the verbosity of the
multiple calls. Looking into the methods, there are some surprises,
as well.

function TStringHelper . ToInteger : Integer ;


begin
Result := Integer . Parse ( Self );
end ;

That’s odd. But looking closer, we can see that Integer.Parse()


is a member of the TIntegerHelper = record helper for Integer.
Consider what is implied. The native type Integer now gets a
record helper which permits a pretty unusual usage seen above.
What does that method look like?

class function TIntegerHelper . Parse (


const S: string ): Integer ;
begin
if not TryParse (S , Result ) then
ConvertErrorFmt ( @SInvalidInteger2 ,
206 14 Class and Record Helpers

[s , ’ Integer ’]) ;
end ;

Again, in only two lines of code, there is much to ponder. TryParse


attempts to convert the string into an Integer, and determines
whether it succeeded without throwing an exception. So if the
attempt fails, TryParse returns false, and then ConvertErrorFmt is
called. And this works because:

class function TIntegerHelper . TryParse (


const S: string ;
out Value : Integer ): Boolean ;
var
E: Integer ;
begin
Val (S , Value , E);
Result := (E = 0) ;
end ;

Val() is a very old member of the libraries, and tries to deliver the
desired conversion. If it works, then E will be zero. And again, this
doesn’t throw an exception on failure.
Keep in mind also that these helpers support the fluent coding
style, so you could easily write:

var
s: string ;
begin
s := ’my TEST string ; ’
if s. LowerCase . Contains ( ’ test ’) then
DoHandleTest ;
end ;

However, there are members of the helper which hold surprises


for the unwary:

var
idx : Integer ;
s: string ;
begin
s := ’my TEST string ; ’
// Chars [] is zero - based
for idx := 0 to s. Length - 1 do
if s. Chars [ idx ]. IsUpper then
s. Chars [ idx ] := s. Chars [ idx ]. ToLower ;
14.2 TStringHelper 207

// Instead
for idx := 0 to s. Length - 1 do
if s . Chars [ idx ]. IsUpper then
s [ idx + 1] := s. Chars [ idx ]. ToLower
else
s [ idx + 1] := s. Chars [ idx ]. ToUpper ;
end ;

Notice that the first loop will not compile, because Chars[] is a
read-only property. So the second loop reads from the Chars[idx]
for its test, but writes to the string itself, with an offset index.
By default, strings remain 1-based, but the Chars[] property is
zero-based. Also notice that the TStringHelper has LowerCase as a
member, TCharacter provides ToLower, instead.
Add the availability of the compiler directive $ZEROBASEDSTRINGS
and things get rather complex. Most who have turned on zero-
based strings have soon reverted to the traditional form. The
implementation of zero-based strings seems rather less sweeping
than would have been expected, so surprises lurk. But then you
find things like Chars[] which is always zero-based. The consensus
seems to be that staying with 1-based strings and dealing with the
occasional zero-based feature is more productive than trying to
commit to zero-based strings.
Note also that $ZEROBASEDSTRINGS has been a moving target. At
one point, it was the default in mobile applications, which led
to complications where code was shared between desktop and
mobile environments. In Delphi 10.4 Sydney the default is again to
off.

Base vs. Index


As complicated as is the issue with 1-based and zero-based
access, the reality is that $ZEROBASEDSTRINGS is not global in
impact, so there will be surprises in usage.
The strings in Delphi are always 1-based, and that’s unlikely
to change. The compiler directive changes how the strings are
indexed, but issues remain. The TStringHelper methods rely
on zero-based indexing independent of compiler switches. And
although you would expect that an exception would be thrown
when you exceed the range of an index, that turns out not to
be the case. Here there be dragons. Proceed with care.
208 14 Class and Record Helpers

Test everything!

These are just a few tidbits from a fairly large unit. There is so
much in there that you really should spend some time studying
and appreciating the powers it gives. And all the while, remember
that this near magic is delivered by class helpers with very small
chunks of code.

14.3 Legacy Cleanup

Judicious use of class and record helpers will allow you to add
safety to your code without adding clutter. The deceptive sim-
plicity of the class helper methods can encapsulate not only safe
conversions but elegant error handling, as well. As with most tools
of good coding, the cost is in good planning. But the reward will
be higher reliability and lower maintenance.
If you have not tried writing a class helper, do yourself a favor.
It’s easy to begin, and once you try them out, you are likely to
find they quickly become indispensable. If you are using a recent
version of Delphi, do keep in mind that there are already class and
record helpers in Delphi. Creating a conflict with them will add to
your challenges, rather than reducing them.
Using a class helper to add functionality to a third-party compo-
nent can be very beneficial. You cannot use them to hide existing
functionality in the component, but sometimes a component may
expose an irregular interface, and carefully adding functions can
increase the regularity, albeit at the expense of increasing the
interface footprint.
You cannot add variables in a class helper, but you may be able
to make use of a variable in the class helper unit, if there is no
risk of conflict. For example, in working with a spreadsheet export
component, adding a CurrentPage variable made it possible to
implement routines in which a page parameter was not needed.
Since the number of operations on a particular page generally
outnumbers the calls to set the CurrentPage, you can gain advantage
in reducing clutter.
Designing class helpers is pretty easy. Most routines will be small.
But you will need to adjust your coding habits a little to deal with
the inability to add instance variables. Also, expect to design and
14.4 More Insights 209

redesign, as you become accustomed to using them. All in all,


using them is better than not.

14.4 More Insights

There are many articles online which may help you to make class
and record helpers a part of your tool set. A basic overview is here:
https://www.thoughtco.com/understanding-delphi-class-
and-record-helpers-1058281
Using Datasets Well 15
Data components have been part of Delphi from the start. Capabili- 15.1 TField Properties . . 211
ties vary with the particular component, but most of the properties 15.2 Component Differ-
and operations are much the same from one to another. It is often ences . . . . . . . . . . 212
the case, however, that applications make only rudimentary use 15.3 Normalize Data . . . 212
of these components, leading to the need for more code to be
15.4 Less Specificity . . . . 213
written.
15.5 Think Before Coding 214

15.1 TField Properties

The Delphi TField has a tremendous number of properties, as seen


in the VCL Reference here: https://docs.embarcadero.com/p
roducts/rad_studio/delphiAndcpp2009/HelpUpdate2/EN/htm
l/delphivclwin32/!!MEMBERTYPE_Properties_DB_TField.html.
However, in everyday use, we will be most concerned with only a
few of these:
▶ Alignment: defaulted based on the type of the TField. You
can assign to it, to override the default.
▶ DisplayFormat: (TNumericField members only). Defines the
presentation of the value as it will appear in a data aware
control.
▶ DisplayLabel: Defines the caption to be applied to the column
in a data aware control.
▶ DisplayText: A property from which we can fetch the for-
matted text of the field.
▶ Visible: Controls the visibility of the field in a data aware
control.
Using these well may reduce the logic needed in your code.
Certainly, it has worked in my own projects. However, if you need
to stream your data to XML or JSON and pass to a COM server,
for example, be aware that these properties are not streamed, and
you will need to design your own solution to transfer that data.
212 15 Using Datasets Well

15.2 Component Differences

Component feature differences are to be expected, of course, else


there would be no need for different versions of the same general
functionality. However, in some cases, enhanced descendants of
TDataset fail to implement all of the same functionality. This can
be annoying if you are trying to replace one component with
another, but sometimes you get lucky. In one case, replacing one
vendor’s memory dataset (which was an incomplete descendant
of TDataset) with TClientDataset delivered a major performance
boost.
Having just suggested a single fam- After a quarter century of use, there are numerous publishers of
ily of data components will be best, dataset components, and each has its own special features. Unless
I must point out that there are good
reasons to use outliers, such as the
there is a compelling reason to do otherwise, you really should
TJvCsvDataSet from Project JEDI, try to use a single family of dataset components. This simplifies
which is available through GetIt. Con- maintenance issues, but the selection of which product to use is
versions to and from CSV are ubiq- one you will need to determine for yourself.
uitous, and this component satisfies
that particular need very well. One of the difficulties in an application which uses a large number
of components is in ensuring that they are all used well. In the
course of reducing the total number of different components used,
you may encounter unintended consequences, both good and bad.
When that happens, what you thought would be a few hours of
work may become a few days. Or, you may revert the component
changes, and reschedule the work.
No component is perfect, but some have issues which must be
understood and handled. There have been issues, for example, with
TClientDataset and memory leaks. These do not usually become an
issue unless your application keeps a TClientDataset in existence
for days or weeks. The simplest solution to the problem—if you are
using a recent version of Delphi—is to replace TClientDataset with
TFDMemTable, which is not known to have any such leak issue.

15.3 Normalize Data

In many legacy projects, datasets are often built as flat tables. If


you wish to export data for transfer to a reporting server, however,
then you may need to repackage the data to avoid high levels of
repetition, as well as to remove columns not relevant to the task of
the server. On the server side, reporting and exporting code will
15.4 Less Specificity 213

also tend to be cleaner and simpler when written to work with


well organized data.
The normalization need not be rigorous, but should usually ap- Third Normal Form focuses on not
proximate third normal form. (https://en.wikipedia.org/wik storing duplicate data unnecessarily.
Therefore, you would not put author
i/Third_normal_form) Although you may wish to go further, this and title into a single table, but would
will usually be sufficient. create a table of books which is linked
or joined on author.

15.4 Less Specificity

Now consider the TDBGrid or other data aware control. The great You the developer must always be
value of such controls is that they are able to present data without aware of content; your code, however,
should be as ignorant of content as
any special coding to manage the presentation. This is based on possible. The TDBGrid just presents,
the controls being aware of the TField type, as well as various of it does not provide business logic
its properties. For example, if the dataset does not populate the apart from what you apply in event
DisplayLabel property, then the column caption will be the field handlers.

name, but if the DisplayLabels have been filled, then the captions
for the columns will be taken from those properties.
When you write code to produce output from datasets, you will do
well to make use of the TField properties, so that you may reduce
the logic you would otherwise need to provide. You may also wish
to consider defining your own scheme for DisplayFormat values,
one which is easily parsed so that you can use a single value and
convert it as needed in report and export modules. This will reduce
the need for conditional logic in connection with presentation.
Production of output in reports and exports is another area in
which the use of constant records may help reduce logic in code.
In one instance, the production of a collection of summaries was
handled with simple loops which had no awareness of data content.
Instead, an array of constant records provided specifics for each
type of summary, and the data content was in a single dataset. The
array of constant records provided the varying column visibility
and other variant parts needed. Design and coding was a greater
effort than otherwise, but once complete, the array of constant
records is the only place changes have been needed to handle
changes in presentation.
214 15 Using Datasets Well

15.5 Think Before Coding

Yes, it’s obvious, I know. But not so obvious in legacy projects I


have seen.
If you consider carefully how to make the code less content-aware,
you will use the field properties to carry needed information,
and will rely on them in your code to direct operations without
resorting to specific content handling. For example, you could
write overloaded methods to handle TIntegerField, TFloatField,
TCurrencyField, and TStringField. Passing the field as a parameter
is sufficient to distinguish the particular version of the routine,
and avoids the use of if/then/else trees or case statements.
In some cases, there may be fields missing which would allow
further simplification of code. It may be possible to solve that lack
with the addition of a dataset linked to the first. In a case where
I had to deal with visibility by summary type, and by column
type, as well as by user option, an added dataset which managed
the visibility of summary type was sufficient to avoid convoluted
code.
With all that said, you may find that writing such code is more
challenging than in the old ways with much testing and branch-
ing. The newer approaches yield denser code, and require more
concentration as you compose them.
Pragmatism 16
It is easy to speak of all the various methods and strategies for 16.1 Assessing the Costs 216
code cleaning, refactoring, and redesign. In the commercial world, 16.2 Measuring Value . . 216
all this labor costs time and money; not everything is worth the 16.2.1 Use EurekaLog . . . . 217
expense. Part of the challenge is always in obtaining management 16.2.2 Use Analytics . . . . 217
buy-in. This kind of rework does not introduce new features, so 16.3 Missing Source Code 218
marketing is often unenthusiastic. Limiting the costs is a concern 16.4 Analytical Modules 219
in any commercial endeavor, but especially so when you have a
16.5 Little-Used Modules 219
couple of millions of lines of code to consider.
There may be modules which are best viewed as historic, still
“The challenge and the mission are to
needed, but nearing end of life. Some of these may present partic- find real solutions to real problems
ular challenges, as they may indulge in practices we might now on actual schedules with available
avoid. Or perhaps they contain complex and essential routines resources.” – Frederick P. Brooks Jr
(1975). The Mythical Man-Month: Es-
which are essential to data transfer with external systems, and
says on Software Engineering. Addison-
there is high risk in a rewrite. All sorts of particular cases may Wesley.
represent such high risk and low return that they can keep you
Contractors and consultants will
trapped in an old compiler release. know this all too well. Employ-
ees of companies may have less
It also happens that an essential component may be blocking you. experience—and patience—with this
The publisher may have disappeared, as well as the source code aspect of the project. Yet without buy-
you might have been able to adapt. This is another trap which is in, you can’t move forward.
far from unknown in Delphi projects.

Never Use a Component without Source

Even at the release of Delphi 1, it was clear that to use a com-


ponent, you would need the source code. The alternative was
to be trapped in a Delphi version if the publisher disappeared,
or elected not to update to support later versions. There are
abundant examples of orphaned components on Torry’s pages:
https://torry.net.
However, while working in Delphi 2, I broke my rule, and used
There are ways of using an old com-
a component to gain capabilities I desperately needed, even ponent, such as embedding in a DLL,
though there was no source. And sure enough, the component but of course, many will see the use
was never updated after Delphi 2. I was trapped for a consider- of a DLL as less than ideal.
able time, until I found an alternative and was able to update
to Delphi 5. Never again!
216 16 Pragmatism

16.1 Assessing the Costs

There are obvious costs in any rewrite:


▶ Coding Labor
▶ Debugging
▶ Replacing Old Code in the Application

“All programmers are optimists.” – Oddly, the cost of coding is often badly underestimated. You
Frederick P. Brooks Jr (1975). The probably have no unit tests for the existing code, and these must
Mythical Man-Month: Essays on Soft-
ware Engineering. Addison-Wesley.
be created. There is probably no design documentation for the old,
and the code is usually not well commented. Fully understanding
the existing code is a precondition to designing new. Creating unit
tests will have the side benefit of dragging you to a better under-
standing. You may also identify opportunities for improvement,
but these are best set aside until you have good test coverage and
can ensure that you will not damage existing behaviors.
Debugging is always difficult to estimate, but will be facilitated by
well implemented unit tests. You may identify defects in the old
code as you proceed. You may have to preserve some of these to
ensure system compatibility.
Finally, when the new code replicates the behavior of the old, and
passes all the unit tests, you will need to do the actual replacement.
In large applications, it will be best to make the replacement
incrementally; the temptation to replace globally will be great,
but can wreak havoc if some corner case has been missed in unit
testing, and is uncovered in the application behaviors. A new unit
can have a new name, as can a new component type, so they may
coexist in your application. Better safe than sorry.
All of these concerns will affect the real cost of proceeding. Man-
agement will need to be well educated on the trade-offs.

16.2 Measuring Value

Although it is essential to have a grip on costs, they are not much


help if you don’t know the relative value of various aspects of
the work. Value can only be assessed through the eyes of your
customers, and though some will be demanding new features,
others may be complaining about performance. So how can you
resolve these demands?
16.2 Measuring Value 217

As a developer, you usually can’t. Company management must


make those decisions, and then you act on them. However, you
can take steps to provide them with accurate context.
▶ Use EurekaLog to collect defect data.
▶ Use an analytics product to get data about what is most and
least used by your customers.
▶ Compare the EurekaLog numbers to the usage analytics to
find the hot spots.
▶ Discuss with managers how to balance the sometimes con-
flicting goals of performance, defect repair, and new features.

16.2.1 Use EurekaLog

It is rarely the case that legacy applications have full coverage


exception handling. To the extent that they do not, your customer is
assaulted with a dialog filled with apparently useless information.
That doesn’t help you, but getting back reports from EurekaLog
makes it possible to see what happened and where, and simplifies
the task of repairs.

16.2.2 Use Analytics

Your application has been in development for decades, and now


contains hundreds of forms. Do you know:
▶ How many forms are no longer used in your application?
▶ How many active forms are unused by customers?
▶ Which forms are most used by your customers?

If you do not know the answers, then you are wasting effort.
Maintaining a form which is no longer used is an obvious waste.
But maintaining a form which is used by a small fraction of
your customer base is also wasteful. Worst of all, if you are not
routinely improving the most used forms in your application, your
customers till take you to task, sooner or later.
Guessing at answers to these questions is useless; get data. Then
act on it.
Adding analytics will need management approval, because send-
ing data back to the software publisher makes customers nervous.
That has to be resolved first, and assurances given—perhaps in
218 16 Pragmatism

contracts—that these data will not be shared with outsiders. So


there is a process:
▶ Get management buy-in at conceptual level.
▶ Work with management on ensuring data security measures.
▶ Await customer agreement.
▶ With final approval, implement.
When it is time to release to customers, there may be still another
round of uncertainty on their part which must be resolved.
Finally, the tough part is that the customers will then expect
improvements to appear very quickly. It’s not reasonable, but
customers rarely are. In some areas, you may already have reworks
in mind, or even redesign. And when you are able to eliminate the
areas not popular with customers, that is equivalent to increasing
your development team.

16.3 Missing Source Code

Your application may use a component which is essential, but


Some legacy projects were first writ- an audit determines that the source code is not available. The
ten in Turbo Pascal or Borland Pascal 7. publisher has vanished. You are stuck. What to do? This is more
Many will have been in the works for
years without benefit of any source
often an issue with non-visual components. One such situation
control tools. There may also have involved encryption and decryption. It was “standard”, so the
been—or still may be—no systematic obvious first step was to obtain a modern commercial component,
management of component invento- replace the old, and move on. Unfortunately, a test app showed
ries. We must deal with the realities
of the project before us.
that the two components produced different output from identical
input. Reverse engineering would have been absurdly expensive
for the small team.
There are times when the right answer is to defer or completely
separate from the problem. With the component in question, that
There are many objections which reduced to putting it into a DLL, and letting it remain forever in
might be raised to this ”solution”, amber. Future work could approach moving to a new component,
but the subject here is pragmatism,
and we needed an expedient solu-
and reworking the streaming to contain versioning. But in the
tion in a corner of the application. short term, embedding the component in a DLL took less time
In your projects, you will make your than the work done to assess a possible replacement. Two hours,
own determinations. and the problem was fully isolated.
16.4 Analytical Modules 219

16.4 Analytical Modules

You may have a collection of mathematical routines in modules Mathematics is only an example; ev-
which were the domain of a single—now departed—developer. ery problem domain has its areas of
specialized knowledge. It is a useful
Although the same strategies would work here, building unit tests, example, as we are not all fluent in
recoding, and replicating behaviors, the costs may be very high. statistical analysis or Calculus.

▶ Can you be certain of adequate unit test coverage? Can you


even identify likely corner cases?
▶ Has any team member the specialized math skills?
▶ Does the code contain tweaked assembly code? Few of us are adept in X86 assembly
▶ Does the code rely on low-level bit manipulation, extensive coding, as Delphi has made it less
essential to most of what we do.
use of pointers, or ShortStrings?
The answers here may again suggest the use of a DLL. You could
then encapsulate the currently functional code, build the DLL with
the existing compiler, and consider it a solved problem. If later it
becomes a performance issue, then it may need to be reworked.
But if your goal is to move from Delphi 6 to Delphi 10.4 Sydney,
then you must remain focused on the main event.

16.5 Little-Used Modules

Once again, the matter of analytics comes into play. You may have
identified dozens or hundreds of modules you wish to rewrite,
but keep in mind that you need management buy-in to be able
to do any of them. Management (and sales) will generally choose
new features even over defect repair, and refactoring will not be
on their list at all.

Real Cost of Code


Little-used modules, it seems to me, are akin to deficit spending.
You commit time and money you can ill afford to things which
may have little or no ROI. Without analytics, it is difficult or even
impossible to identify these modules. With data from analytics,
product management is then able to reach an informed decision
as to the level of support to be offered in future.

With data from analytics, you will be in a position to show which


modules have the most defects, which enjoy the heaviest use,
220 16 Pragmatism

and which the least. You may occasionally wish to point out that
customer retention is also a measure of return on investment, albeit
one often overlooked. Removing modules no one actually uses
means removing maintenance burden which brings no return.
And that burden is there, always, whether or not it is understood
by product management or sales. If data handling changes, if
columns come and go, then all modules which make use of them
need maintenance, whether or not they are actively used. Keeping
unused features in place is never just noise.
Prioritize your limited resources where they will deliver the
greatest relative value. Analytics will not help much in assess-
ing customer satisfaction, but your support staff will know who
complains about performance, and that will be a great help.
You must find a way to juggle these different priorities, and will
need close coordination with management, support, and sales.
Interfaces 17
Delphi has provided support for interfaces since Delphi 3, when it 17.1 History . . . . . . . . 221
was introduced to link to COM servers. But interfaces in Delphi 17.2 Reducing Scope . . 222
have many more capabilities, and should be among your most used 17.2.1 Constants and Types 222
tools. Some will say you should always code to an interface, but 17.2.2 Properties . . . . . . 223
as with most generalizations, that is oversimplified. Nontheless, I 17.3 Managing Memory 227
will focus here not on COM, but on the value of interfaces inside 17.4 Examples . . . . . . 227
your application.
17.5 Going Deeper . . . 230
17.5.1 Variations . . . . . . 235

17.1 History

In 2000, we saw the introduction of the book Delphi COM Program-


ming[9], which was the first volume to attempt a deep dive into [9]: Harmon (2000), Delphi COM Pro-
interfaces. Given COM as its frame of reference, it may have been gramming
less effective as a general approach to using interfaces. Certainly
the unfamiliar topic of interfaces was given little clarity by being
explored through the lens of COM.
The base interface class was IUnknown through Delphi 5, but in
Delphi 6, IInterface was introduced, and further differentiated the
Delphi interface from the Microsoft COM device. Most of the time,
you will implement your class as a descendant of TInterfacedOb-
ject, which implements the _AddRef and _Release methods which
are essential to reference counting. It is possible to inherit instead
from IObject, or any other class, and to implement your own
_AddRef and _Release, and in doing so, you can disable reference
counting. That said, you should avoid that approach unless you
are absolutely certain that you need it, and are prepared to deal
with the problems which may arise.
▶ Descendants of TInterfacedObject are reference counted and
automatically destroyed.
▶ A class can inherit from only one class, but can implement
multiple interface types.
▶ All interfaces descend from IInterface, putting them in a
different hierarchy than TClass.
▶ Interfaces contain neither storage nor code.
▶ Interfaces are implemented by classes.
222 17 Interfaces

17.2 Reducing Scope

There should be universal agreement by now that global variables


are dangerous. Legacy code shows that we have not all learned
to hide everything we can, yet that is an essential strategy. Broad
scope is an invitation to complexity and to unmaintainable code.
When you specify a class, you should try to follow these precepts:
▶ Make members private, wherever practical.
▶ If a method is based on a particular context, use a nested
function, or create a class private to the unit to contain it.
▶ Keep lifetime management within the module.
▶ Use field variables where most methods in the class may
need access; otherwise, prefer passing parameters.
▶ Elevate visibility of any member only when you must.
▶ Carefully consider why you would define constants—even
In Delphi 2007, for example, such
constant declarations prevent Ctrl-
private ones—inside a class. In some versions of Delphi,
Shift-Up/Dn from navigating in sub- doing so will break internal navigation.
sequent members.

17.2.1 Constants and Types

Unlike variables, the scope of constants and types is less important,


as they do not generate code. Even so, the question remains
whether a constant or type needs wide visibility. Your options are
these:
▶ Global scope, in units created for the purpose. Best to keep
types in one, and constants in another. You may want a
number of each, if you have application areas which make
use of different groups of constants and types.
▶ Public to module. Some types and constants are essential to
the function of a particular module, and must be available
to consumers of that module. Declare these in the interface
section of your module.
▶ Private scope. Place these declarations in the implementation
section of your unit where they will be available to all
methods, but hidden from other modules.
17.2 Reducing Scope 223

17.2.2 Properties

In Delphi we have the benefit of properties which may be accessed


by consumers of our classes. It seems best to reference these
only from calling classes, and inside the module in which they
are declared, to reference the field variable or accessor which
corresponds to that member. A bit of searching shows there is
considerable disagreement on this, but consider:
▶ If you follow Delphi conventions for naming, field variable
names begin with F, as FMyInteger: Integer; and these are
easily recognized as local to the unit.
▶ A property name such as MyInteger could be anywhere; a
property, a variable—of any scope—or a constant. In legacy
code, the navigation to such a member may often be broken,
and it becomes a needle in the haystack issue.
▶ In certain circumstances, local access through a property
may introduce difficult defects, as warned by FixInsight.
▶ Properties are most often declared as public.

Discipline of Properties

Sometimes Delphi seems almost too permissive. The available


flexibility in approach has roots in the early decision not to
make Delphi a purely OOP language like Java. In general, this
has been a benefit, as it allowed for bringing forward many
libraries which had long been used in Turbo Pascal.
Properties, however, are purely OOP in their nature, and the
application of some discipline in their use yields substantial
benefits. Delphi properties implement a public coupling to
private implementations. That perspective leads me to say that:
▶ Properties should be declared as public class members.
Unless they are public, they are not available outside the
class, so would be of no great value.
▶ Properties should not be referenced inside the class in
which they are declared. It is generally advantageous to
reference instead the field members or accessors which
the properties expose. This is rarely the case in legacy
code, and by the time you have battled searches which
include many false hits based on property names, you
will likely come to appreciate this view. And again, if you
224 17 Interfaces

preface field names as Delphi conventions suggest, then


you can recognize by inspection that the call is local.

Discipline. Convention. Words which recommend, but do not enforce.


So let us consider further the advice above with respect to discipline
in properties. I’ll harp a bit in support of good habits.
The context of legacy code should by now suggest coding horrors.
But when it comes to properties, there are subtleties, and some of
them may be aggravated by things like inheritance, which is often
overused in legacy code. A bit of code will help.

type
TSomeClass = class
private
FHeight : Integer ;
procedure SetHeight ( Value : Integer );
public
property Height : Integer read FHeight write
SetHeight ;
end ;

implementation

procedure TSomeClass . SetHeight ( Value : Integer );


begin
if Height <> Value then
begin
Height := Value ; // Watch out !!
end ;
end ;

Simple code, and follows a pretty common pattern, making the


assignment only when there is a new value. So what is wrong with
this picture? The assignment is made to the property, which writes
through a call to SetHeight, this very routine. Endless recursion is
the result, and we obviously wish to avoid that. First, let’s fix the
code:

type
TSomeClass = class
private
FHeight : Integer ;
procedure SetHeight ( Value : Integer );
public
17.2 Reducing Scope 225

property Height : Integer read FHeight write


SetHeight ;
end ;

implementation

procedure TSomeClass . SetHeight ( Value : Integer );


begin
if FHeight <> Value then // check the field
begin
FHeight := Value ; // assign to field
end ;
end ;

The simple change to using the field variable removes an embar-


rassing defect. This is a minor example of why I say you should
reference the field variable where you are coding in the same
unit. But there are variations on this theme, and as I mentioned,
inheritance can be an issue. So what if we see a situation like
this?

type
TSomeClass = class ( TSomeAncestor )
private
FHeight : Integer ;
public
property Height : Integer read FHeight ;
end ;

implementation

var
SomeClass : TSomeClass ;

// somewhere in code
SomeClass . Height := 15;
// more code follows
end ;

This looks odd. the Height property is declared as read only, and
now we make an assignment. We can infer that TSomeAncestor
contains a Height property with read/write access. Delphi will
compile this without complaint. But if you run Pascal Analyzer Lite
over your project, it will issue a Strong Warning. This is partly in
the spirit of the original problem, but for a different reason. Delphi
226 17 Interfaces

will compile a call to the SetHeight in the ancestor, satisfying


the basic requirement. However, there could be an issue in that
implementation. Moreover, since we need to access the inherited
property, declaring the field variable in the descendant is just
wrong. So how do we resolve this issue?

type
TSomeClass = class ( TSomeAncestor )
private
function GetHeight : Integer ;
procedure SetHeight ( Value : Integer );
public
property Height : Integer read GetHeight write
SetHeight ;
end ;

implementation

var
SomeClass : TSomeClass ;

function TSomeClass . GetHeight : Integer ;


begin
Result := inherited Height ;
end ;

procedure TSomeClass . SetHeight ( Value : Integer );


begin
inherited Height := Value ;
end ;

// somewhere later in code


SomeClass . Height := 15;
// more code follows
end ;

Among other things, this example is a minor warning with respect


to inheritance. You certainly can benefit from inheritance, but you
must be fully aware of what you inherit. With these changes, Pascal
Analyzer Lite will be satisfied, and the Strong Warning will be gone.
But just as in the use of a hard cast, the burden is on the developer
to ensure that the code is correct.
17.3 Managing Memory 227

17.3 Managing Memory

Interfaced objects in Delphi are reference counted, and are freed


when the last reference goes out of scope. This is a reliable approach
to memory management of object instances, and another incentive
to the creation of interfaced classes.

17.4 Examples

Here is a fairly simple example of an interface:

type
IStrings = interface ( IInterface )
[ ’{ FC8E80A5 - A70B -4 BCC -9 BFC -5 BCB15ACF5E4 } ’]
function GetClassName : string ; stdcall ;
function GetCommaText : string ;
function GetCount : Integer ;
function GetStrings (
Index : Integer ): string ; stdcall ;
procedure SetCommaText ( const Value : string );
procedure SetStrings ( Index : Integer ;
const Value : string ); stdcall ;
property ClassName : string read GetClassName ;
property CommaText : string
read GetCommaText
write SetCommaText ;
property Strings [ Index : Integer ]: string
read GetStrings
write SetStrings ; default ;
end ;

Notice that in the interface, nothing is private; everything is public.


It makes the notion of properties seem a bit odd at that level, since
the accessor routines are equally visible. Still, this is simply an
example intended to illustrate the basic operations of the interface
in Delphi.
The GUID is not essential, but is almost always inserted, as it
provides support for testing through the Supports() call.
And here is a class declaration which implements that interface:

TIntfStrings = class ( TInterfacedObject , IStrings )


private
228 17 Interfaces

FStrings : TStrings ;
function GetClassName : string ; stdcall ;
function GetCommaText : string ;
function GetCount : Integer ;
function GetStrings (
Index : Integer ): string ; stdcall ;
procedure SetCommaText ( const Value : string );
procedure SetStrings ( Index : Integer ;
const Value : string ); stdcall ;
public
constructor Create ( AStrings : TStrings );
property ClassName : string read GetClassName ;
property CommaText : string
read GetCommaText
write SetCommaText ;
property Count : Integer read GetCount ;
property Strings [ Index : Integer ]: string
read GetStrings
write SetStrings ; default ;
end ;

As you can see, the class declaration is almost identical to that


of the interface. The one obvious difference is the FStrings object,
which provides storage. In this case, it is not local, but simply
a pointer to the TStrings object passed in the constructor. Also
observe that the constructor is not declared in the interface—it
need not be, as the interfaced object will be instantiated in this
fashion:

var
SomeStrings : IStrings ;
begin
SomeStrings := TIntfStrings . Create ( AStringList );

This interfaced class is not particularly useful, as it stands. But it


is convenient to implement something similar, perhaps to handle
strict delimited lists, because it affords the opportunity to:
▶ Make disposal automatic.
▶ Expose only the functionality needed for the intended use.

Why Limit Interfaced Functionality?

Anyone with significant Delphi experience knows that the


17.4 Examples 229

TStringList presents many capabilities. Usually, that is benefi-


cial, but it can also lead to tangled functionality, and that may
reduce testability. In the IDelimitedText interface below, I use a
TStringList, but make public only a handful of its properties.

IDelimitedText was created for a very specific and narrow


purpose: To process delimited text collections and to be mini-
mally intrusive in the caller’s code. It achieves these goals by
providing limited functionality, and because as an interfaced
object, we need not use try/finally in applying it. Whatever
specialized processing you may need will be better placed
elsewhere.

Now, let’s look at an example of usage:

var
List1 : TIntfStrings ;
List2 : IStrings ;
begin
// conventional class usage
List1 := TIntfStrings . Create ( AStringList );
try
// operating code here
finally
List1 . Free ;
end ;
// interfaced equivalent usage
List2 := TIntfStrings . Create ( AStringList );
// operating code here
end ;

Note that in this code, both the class and the interfaced class are
public. An alternative approach is to move the class declaration
into the implementation section of the unit. Doing so means that
you must implement a simple function to return an instance of the
interfaced class. Now the front end of the unit looks like this:

unit uInterfaced ;

interface

uses
Classes ;

type
IStrings = interface ( IInterface )
230 17 Interfaces

[ ’{ FC8E80A5 - A70B -4 BCC -9 BFC -5 BCB15ACF5E4 } ’]


function GetClassName : string ; stdcall ;
function GetCommaText : string ;
function GetCount : Integer ;
function GetStrings (
Index : Integer ): string ; stdcall ;
procedure SetCommaText ( const Value : string );
procedure SetStrings ( Index : Integer ;
const Value : string ); stdcall ;
property ClassName : string read GetClassName ;
property CommaText : string
read GetCommaText
write SetCommaText ;
property Strings [ Index : Integer ]: string
read GetStrings
write SetStrings ; default ;
end ;

function GetIStringsInstance (
AStrings : TStrings ): IStrings ;

implementation

GetIStringsInstance is needed because we have no other way to


obtain an instance of the class. Operationally, little has changed:

var
List : IStrings ;
begin
// interfaced equivalent usage
List := GetIStringsInstance ( AStringList );
// operating code here
end ;
Preventing a user from directly in-
stantiating your interfaced class is The big change is that there is no longer any access to the class
important. You designed the inter-
faced class for a reason, and keeping
type, so only the interfaced version can be used.
the class declaration private makes
The coding of the class itself is trivial, as it adds no capabilities,
clear your intention. This in turn sim-
plifies the design burden, as well as but simply surfaces a subset of the TStrings public interface.
the complexity of testing.

17.5 Going Deeper

The examples should have suggested to you some fairly obvious


benefits to using interfaces and interface objects. A useful appli-
17.5 Going Deeper 231

cation of interfaces is in creating specialized, managed objects,


without using inheritance. But further, a problem in inheritance is
that the ancestor’s entire public interface is exposed. An interfaced
object exposes only what you put in the interface, regardless of
what lies under the skin.
Most Delphi developer use the TStringList extensively, but in most
cases, we use very little of its public interface. And in using it, we
tend to repeat boilerplate:

var
sl : TStringList ;
begin
sl := TStringList . Create ;
try
sl . StrictDelimiter := True ;
sl . Delimiter := ’,’;
sl . DelimitedText := StringToDecode ;
// application code
finally
sl . Free ;
end ;
end ;

There is no good reason any longer to repeat that boilerplate


when you can provide instead an interfaced class to do the work
required. Once again, the interface will expose only what is really
needed. Refer to section 11.9 for the code used in this approach,
but the interface is repeated here, for the sake of discussion.

interface

uses
System . Classes ;

type
IDelimitedText = interface ( IInterface )
[ ’{ FB67C93C -8779 -4 F51 - A257 -37 BC89C4A813 } ’]
procedure Add ( const S: string );
procedure Clear ;
procedure Delete ( const Index : Integer );
function Find ( const S: string ;
var Index : Integer ): Boolean ;
function GetCommaText : string ;
function GetCount : Integer ;
function GetDelimitedText : string ;
232 17 Interfaces

function GetDelimiter : Char ;


function GetSorted : Boolean ;
function GetStrictDelimiter : Boolean ;
function GetStrings ( Index : Integer ): string ;
function GetText : string ;
procedure IgnoreDupes ( const State : Boolean );
function IndexOf ( const S: string ): Integer ;
procedure PutStrings ( Index : Integer ;
const Value : string );
procedure SetDelimitedText (
const Value : string );
procedure SetDelimiter ( const Value : Char );
procedure SetSorted ( const Value : Boolean );
procedure SetStrictDelimiter (
const Value : Boolean );
procedure SetText ( const Value : string );

property CommaText : string read GetCommaText ;


property Count : Integer read GetCount ;
property DelimitedText : string
read GetDelimitedText
write SetDelimitedText ;
property Delimiter : Char
read GetDelimiter
write SetDelimiter ;
property Sorted : Boolean
read GetSorted
write SetSorted ;
property StrictDelimiter : Boolean
read GetStrictDelimiter
write SetStrictDelimiter ;
property Strings [ Index : Integer ]: string
read GetStrings
write PutStrings ; default ;
property Text : string
read GetText
write SetText ;
end ;

function GetIDelimitedText : IDelimitedText ;

What is missing here, and is not needed, is the string list which
is used in the composition. It can’t be declared here because an
interface can’t define storage. Instead, we define what is needed
for the application of this limited class to our code. As a further
benefit in this design, I use a function to return an instance of the
17.5 Going Deeper 233

class, so that the public interface of the class is only the interface
declaration. That, plus some documentation in the unit which
implements it, will serve to make clear the particular intention of
the module, and warn that the interface should not be altered.
In the implementation section, some differences become appar-
ent:

implementation

uses
System . StrUtils ;

type
TDelimitedText = class (
TInterfacedObject , IDelimitedText )
private
FStringList : TStringList ;
function GetCommaText : string ;
function GetCount : Integer ;
function GetDelimitedText : string ;
function GetDelimiter : Char ;
function GetSorted : Boolean ;
function GetStrictDelimiter : Boolean ;
function GetStrings ( Index : Integer ): string ;
function GetText : string ;
procedure PutStrings ( Index : Integer ;
const Value : string );
procedure SetDelimitedText (
const Value : string );
procedure SetDelimiter ( const Value : Char );
procedure SetSorted ( const Value : Boolean );
procedure SetStrictDelimiter (
const Value : Boolean );
procedure SetText ( const Value : string );
public
constructor Create ;
destructor Destroy ; override ;
// details removed here which are identical
// to the interface
end ;

function GetIDelimitedText : IDelimitedText ;


begin
Result := TDelimitedText . Create ;
end ;
234 17 Interfaces

Again, to emphasize, this is hidden from the consumer, and avail-


able only through the interface and the function which instantiates
an object. This is important because:
▶ The interface is a contract which the class implements.
▶ The contract is guaranteed, if only the consumer does not
alter the module.
▶ The default delimiter, the comma, can be overridden, if a
different delimiter is needed.
▶ The interface isolates us from lifetime management.

So what happens to our original boilerplate?

var
sl : TStringList ;
begin
sl := TStringList . Create ;
try
sl . StrictDelimiter := True ;
sl . Delimiter := ’,’;
sl . DelimitedText := StringToDecode ;
// application code
finally
sl . Free ;
end ;
end ;

Reduces to this:

var
sl : IDelimitedText ;
begin
sl := GetIDelimitedText ;
sl . DelimitedText := StringToDecode ;
// application code
end ;

And if you needed multiple string lists, then reduction in manage-


ment is even more apparent. This:

procedure TForm1 . MultiError ;


begin
slOne := TStringList . Create ;
slTwo := TStringList . Create ;
slThree := TStringList . Create ;
try
// working code snipped for clarity
17.5 Going Deeper 235

finally
slThree . Free ;
slTwo . Free ;
slOne . Free ;
end ;
end ;

Is reduced to:

procedure TForm1 . MultiError ;


begin
slOne := GetIDelimitedText ;
slTwo := GetIDelimitedText ;
slThree := GetIDelimitedText ;
// working code snipped for clarity
end ;

For the modest effort of implementing a constrained solution, you


get cleaner code and managed lifetime.

17.5.1 Variations

As mentioned above, you may want to use other delimiters than


the comma, particularly if you are importing data from external
systems. An approach I have used is this:

function GetIDelimitedText (
ADelimiter : Char = ’,’): IDelimitedText ;
begin
Result := TDelimitedText . Create ;
if ADelim <> ’,’ then
Result . Delimiter := ADelimiter ;
end ;

The comma is probably the most commonly used delimiter in such


cases, but there is no great magic involved, and some may prefer
the use of the pipe (|) character. In one case, I elected to use the
venerable BEL (#7) character, for the simple reason that it is almost
never used, so unlikely to be confused with content in a foreign
source stream.
There may also be particular operations which you perform so
often in delimited lists that it makes sense to implement them in
your interfaced class. Do keep in mind, however, the Separation
of Concerns and Single Responsibility Principle. The IDelimitedText
236 17 Interfaces

class is a general tool, while your specialized version may better


be implemented as a separate class, for the sake of both clarity and
maintainability. It may seem initially as though you are splitting
hairs, but as proceed with your rework, these considerations will
become an ordinary part of your process.
Testability 18
Over the years, numerous software design theories have been 18.1 Testing Legacy Code 237
championed. These days, Test Driven Design (TDD) is de rigeur. 18.2 Layers of Difficulty 237
Testability is very important, and as the size of the application
18.3 Design Specifications 238
increases, I would argue that the importance of testing increases
18.4 Interdependency
exponentially.
Issues . . . . . . . . . 239
18.5 Coupling, Partition-
ing . . . . . . . . . . . 239
18.1 Testing Legacy Code 18.6 Code on Forms . . . 239
18.7 Testability Defined . 240
In expositions on TDD, you will read that the tests must be 18.8 Ensure Testability . 241
designed and written before coding. While that is a viable approach 18.9 Unit Test Frame-
to new design, it is utterly useless for existing code. Moreover, works . . . . . . . . . 241
there is an unwarranted assumption that all necessary tests can 18.10 Testing Practices . . 242
be conceived and implemented before coding, and that is rarely
true.
Back in the eighties, there was a book called Pascal Programming:
A Spiral Approach[1], which offered the notion that neither top- [1]: Brainerd (1982), Pascal Program-
down nor bottom-up design was very workable for any non-trivial ming: A Spiral Approach
application. In most systems on which I have worked, this has been
true. Defining with certainty all requirements of a complete system
prior to design is rarely practical, as things may be overlooked
very easily.
Legacy code generally has been written with no thought to The older the product, the less likely
testability. Nor, indeed, with any commitment to the design prin- it is to show evidence of testability.

ciples which we now try to follow, such as SOLID, Don’t Repeat


Yourself , YAGNI, and others.

18.2 Layers of Difficulty

Problems in legacy code include most of these issues:


▶ No design specifications.
▶ Unit interdependency prevents isolated testing,
▶ Tight coupling, aggravated by poor partitioning.
▶ Code on Forms.
238 18 Testability

▶ No Separation of Concerns.

We could extend the list with things like intermixing of business


logic, data manipulation, and form logic, but that is really a matter
of details.

18.3 Design Specifications

A design specification is a formal document which presents the


motivation and the requirements for a software feature. It is not a
napkin on which someone has scribbled “need file import.” It has
been my experience that on legacy projects, no such documents
exist.
If you explore the Wikipedia page on Software Requirements
Specification (https://en.wikipedia.org/wiki/Software_req
uirements_specification), you will probably recognize that you
have never worked from such a (fully developed) document, and
I’m afraid, few of us have. And in a rapidly moving and highly
competitive market, few of us ever will.
In the context of legacy projects, however, it would be a huge
benefit to have even some notes which explained the developer’s
perspective. Explanatory comment blocks would suffice. But too
often, the assumption of the developer appears to be that the
need for the routines in the unit is self-evident. Then, the lack of
explanation is compounded by the use of usually horrible naming
practices.
In an overly long routine which contains five levels of nested for
loops, index variables named i, j, k, l, and m will not be helpful, but
names such as monthIdx, and dayIdx will contribute to readability.
Such routines also present formidable obstacles to unit testing.
It will generally be better to convert the innermost loop to a
subroutine which can be tested. Then, similarly convert each level
I will repeatedly emphasize the im- above until you have extracted five testable subroutines. Each of
portance of good naming, both be- these will be more comprehensible—your naming, after all will be
cause it is usually absent, and be-
cause it is essential to understanding.
descriptive—and as each is testable, the ultimate outcome should
always be as expected.
18.4 Interdependency Issues 239

18.4 Interdependency Issues

Unit testing targets a single unit at a time. If the unit to be tested There will be a need to deal with your
is intimately coupled to several others (and likely, each of these Unit C which uses your Unit B, which
uses your Unit A. This is no different
to still others) then reaching the point of a testable unit will be a than dealing with your units which
long struggle. use Delphi library modules. The prob-
lem is solved by first unit testing the
If one or more of these units involves a live database, then obtaining lowest level unit, then the unit which
a reliable test result is problematic: what if the database has been uses that, and so on.
compromised by another developer?

18.5 Coupling, Partitioning

Coupling and partitioning of functionality also affect testability.


As in the example above with nested loops, we seek to test small
elements, not the more complex whole. Being able to run tests
on the innermost routines first brings confidence when those
are called from other routines. This requires that we avoid tight
coupling.
I use partitioning to refer to the placement of routines into their
respective units. For example, a function which performs some
formatting on a string fetched from a dataset is really a string
handler, not a data routine. The dataset field can be passed to
the routine, or the string value of the field can be passed as a
simple string. In either case, the function does not belong in a data
module. As the application size increases, this becomes ever more
important, because the simple string function is easily tested in
isolation, while the data module may not be.

18.6 Code on Forms


You may recall having seen Delphi
There should always be as little code as possible on forms. The demonstrated at a software show
in the early days. Double-click on
siren song of RAD is to double-click on a control and begin coding. a control and write code was always
If the code is simply a call to testable code in another unit, that’s the paradigm. Understandable, in
fine. But much, perhaps most, of the time in legacy projects, the a show demo environment. But in
event handler on a control will contain business logic and even our professional work, this casual ap-
proach should play little part.
data manipulation.
240 18 Testability

As a rule, I think an event handler should normally be only a


few lines long. A single line is great, a half-dozen will be okay,
but a dozen is moving into questionable territory. While adding
code to a form, you should be thinking about the implications
for testability; form code will have to be tested manually, or with
some sort of scripted tool.

18.7 Testability Defined

According to Wikipedia (https://en.wikipedia.org/wiki/Soft


ware_testability):
Software testability is the degree to which a software
artifact (i.e. a software system, software module, re-
quirements, or design document) supports testing in
a given test context. If the testability of the software
artifact is high, then finding faults in the system (if it
has any) by means of testing is easier.
When we speak of testability, it is generally a reference to unit
testing. Also from Wikipedia (https://en.wikipedia.org/wiki/
Unit_testing):
In computer programming, unit testing is a software
testing method by which individual units of source
code, sets of one or more computer program modules
together with associated control data, usage proce-
dures, and operating procedures, are tested to deter-
mine whether they are fit for use.
Emphasis here is on the individual units of source code.
An individual unit cannot be tested independently if it is depen-
dent on other units which may add their own dependencies. It
is essential to testability that we do what we can to avoid such
dependencies. Certainly, a unit will need to use others, but the
more unit dependencies are involved, the more difficult it will be
to construct a practical unit test environment.
18.8 Ensure Testability 241

18.8 Ensure Testability

To make sure your units are testable, there should be a clear list of
requirements. These will not be detailed, as they must be general.
But the list is realistic, and you may have difficulty achieving it.
▶ The unit should not depend on other units in your applica-
tion, or if it does, those units must be fully testable.
▶ The routines in the unit should present a simple interface
and a clear result. This allows the tests to be written logically
and with full coverage of the logical possibilities.
▶ If a routine to be tested relies on another routine in the
module, that routine must be fully tested in the sequence
first.
▶ The tests implemented must exercise the full range of possi-
bilities available in the routine under test.
▶ The coding is not complete until all tests are passed or failed
(both outcomes are designed into testing.)
▶ When the module under test is modified, the tests must be
updated to exercise the new possibilities.
This may initially sound like gobbledygook but as you begin
to implement unit tests, these need for these requirements will
become clear.

Dependencies and Testability

Just as all non-trivial programs have defects, all non-trivial


programs depend on multiple code modules. The question is
how we can intelligently manage dependencies in unit testing.
The simple answer is that we must approach things in layers.
Our unit testing will be informed by unit dependencies. If
our Unit A depends on Unit B, then we will need to certify
Unit B through unit tests before we attempt to unit test Unit
A. This reality should make clear an excellent reason to avoid unit
dependency cycles, as they clearly prevent us achieving clean, logical
unit certification.

18.9 Unit Test Frameworks

Unit test frameworks available to Delphi include:


242 18 Testability

▶ DUnit (See Chapter 33)


▶ DUnit2 (See Chapter 34)
▶ DUnitX (See Chapter 35. For Delphi 2010 and later)

If you are concerned only with Delphi 2010 and later, you may
prefer to use DUnitX, to take advantage of its particular features.
Some of us, however, still maintain projects in older versions, and
will therefore need to use DUnit, the original. Which framework
you use is less important than that you implement unit testing,
as soon as you are able to do so. For anyone new to the topic, I
1: The Art of Unit Testing: with Exam- recommend The Art of Unit Testing: with Examples in C# 1 , as most
ples in C#, by Roy Osherove, 2013 Delphi developers will find it somewhat easier to make sense of
C# than Java.

18.10 Testing Practices

Test coverage will verify not only suc- When you write unit tests, remember that your goal is to prove that
cess, but must also verify graceful your code does not fail to perform properly, regardless of input. You
failure, by which we mean that your
tests should verify not only success,
should be coding defensively, as someone will call your routines
but that in failure, you do not allow with bad data, sooner or later, and therefore you need to code
unhandled errors to escape. your tests to verify that such cases are covered. Exception handlers
(see section 4.5) should not be seen as regular program logic, but
as solutions to the handling of exceptional and unexpected error
conditions. Error conditions which we can reasonably anticipate,
such as a lack of network connection, or unfound URL, should not
be resolved through exception handlers.
Interactive testing often begins with the happy path, meaning that
you call with exactly the sort of parameters you expect to process.
Unit testing needs to consider not only the happy path, but the
ugly back roads, and should exercise those possibilities. Your goal
is to provide exhaustive testing, so that the routines can be used
with no risk of unhandled failure. If you coded the routines, you
are in a good position to implement full coverage in your tests. If
you inherited the routines from someone long gone, then you are
likely to find that some tests may need to be added later, if corner
cases were overlooked.
To simplify, you will write tests which need to pass, and tests
which need to fail. In some cases, you may also need tests to show
that failures happened in the expected manner, not merely that
they failed. Remember that the ultimate goal is perfect reliability
18.10 Testing Practices 243

in your delivered product, not an easily managed and limited set


of tests.
Performance 19
All code should be fast. The user will always experience delays 19.1 Realities . . . . . . . 245
waiting for the application to do its job, and we want to minimize 19.1.1 Profiling . . . . . . . 245
those annoyances. 19.1.2 Profiling Legacy Code246
19.1.3 Approaching Profil-
ing . . . . . . . . . . . 247

19.1 Realities 19.2 Painful Realities . . 248


19.3 You Need Tools . . . 248

Like it or not, developers are usually not good judges of where


work is needed on performance. There is an old aphorism along
the lines of “90% of developers are wrong 90% of the time about “Premature optimization is the root
where to optimize.” of all evil.” – Donald Knuth

Long ago, I received a simple and invaluable piece of advice: First


make it work, then make it fast.
Performance matters, and optimization of code is the path to
performance. But playing with performance before you have
shippable code is foolish. And guessing at where the performance
bottlenecks lie is equally foolish. Performance can only be assessed
by measurement.

19.1.1 Profiling

The tool for measuring performance is a profiler. There are several


capable products from which to choose which will be listed near
the end of this chapter.
Profilers are either sampling or instrumenting in nature. In theory,
the sampling profiler is easy to use, as you need to make no changes
in code. But it is a bit of a peephole, in practice. Instrumenting
profilers will change—and restore—your code, and in so doing,
are able to provide a great deal of useful data.
246 19 Performance

19.1.2 Profiling Legacy Code

Legacy projects present challenges at every turn, not least of these


being the often massive collection of code files in the project. Some
tools, ProDelphi in particular, limits the number of routines it will
track to <64,000. But it also provides a folder exclusion list which
you may use to focus on the areas of concern.
Another issue is that instrumenting and de-instrumenting opera-
tions take a bit of time. And you will want to de-instrument before
tweaking code for performance, as your changes may well alter
code structure in a way which needs changes in instrumentation.
It is very productive to isolate a unit or two for the sake of
performance tuning. However, in tightly coupled code, that may
be impractical, so you come back to dealing with things as you
find them.

Order of Actions
In a perfect world, we might suggest that you defer performance
tuning until you have fully restructured your code. But legacy
applications are generally shipping applications and users tend to
be impatient with performance issues. As with most activities
in reworking legacy projects, we must be pragmatic.
Even in legacy projects, some modules may be relatively de-
coupled, and we should seize the opportunity to advance our
cause in such cases. When you find a module which has min-
imal involvement in UDCs, try to eliminate those altogether.
Sometimes it will require only the relocation of a routine or two
to a different module. Then create unit tests for the module, and
try to make them as comprehensive as possible. Understand
that you will almost certainly need to revisit the unit tests as
you identify other cases which need testing.
Once you have reached that point, and if the module is widely
used, then you may wish to do some profiling work. Since the
module is now free of unit dependencies, and unit testable,
create a special project for the profiling work. As you alter the
code for performance, remember to repeat the unit tests, to be
sure you have not introduced defects.
19.1 Realities 247

19.1.3 Approaching Profiling

At first, you may think you will profile all of your code. It’s a
nice idea, but probably unrealistic, as you may have thousands
of modules, and each of them represents considerable work to
profile, modify, and retest. Instead, you should be guided by the
observed performance of your application, or by operations which
can be expected to take a long time.
Perhaps you have built a tool to analyze your project files for some
particular characteristic. You will iterate through your project
files, parsing and collecting the requisite data. Perhaps you will
also apply some corrections. If the tool is expected to process
large projects repeatedly, then this is an operation well worth
profiling.
Again, as you profile, you will need to make changes to code, so
you should be prepared for thorough unit testing.

Unit Testing & Profiling

Unit testing requires isolation, and that provides an excellent


environment for profiling. Plan to use these operations together,
as performance improvements require changes to code, and
that risks insertion of errors. Unit testing is an ideal and rapid
approach to resolving any such errors, and then you can profile
again.

However you attack the issue of profiling, you are likely to be


working in a narrow area until you have achieved significant
improvements. Performance rework is iterative, like any other
refactoring. You identify a problem area, consider alternative
approaches, and then make changes, rebuild, and profile again.
The results may be better or worse (especially in the beginning),
but you must keep in mind that you will be seeing data for the
number of calls and execution time of routines, so if you are
hoping to speed up a 1,000 line monster, you will need to rethink.
Refactor into manageable routines, verify you have not broken
anything, and then profile. The odds of making major performance
gains in large routines will be poor, and it will be luck more than
skill—your view is obscured by the long routines.
248 19 Performance

19.2 Painful Realities

There are things that you will not optimize with simple code
rework. And there are code optimizations which will be altogether
masked by the overhead of other resources, such as a database.
Program performance is sometimes very hard to improve. In a past
case, days of examination in code got me almost no improvement,
but replacing a memory dataset with a different variety of dataset
made a huge difference with no code changes.
Optimization requires that you identify the actual bottlenecks,
and ignore the supposed bottlenecks.

19.3 You Need Tools

You will not achieve dramatic results through guesswork. The tools
you select may be fancy and expensive, or basic and inexpensive,
but as long as they let you see where the major time consumers
are, you can accomplish what is needed. There are a number of
profilers for Delphi:
▶ AQTime (https://smartbear.com/product/aqtime-pro/
overview/) It’s pricey, but gets very positive reviews from
people who use it regularly.
▶ AsmProfiler (https://github.com/andremussche/asmpro
filer) Free and open source. On that basis alone, you may
want to try it.
▶ ProDelphi (https://www.prodelphi.de) Inexpensive and
capable, though with fewer features than some. I’ve used it
for years, and have been happy with it.
▶ Sampling Profiler (https://www.delphitools.info/sampl
ingprofiler/) Free, but closed source. Last updated in 2013,
but there are people who speak very favorably of it.
This list is sorted, not ranked.
It is not uncommon to discover through profiling that some routine
is called many times more than you thought, or than necessary,
because of some error in code flow. When code performance in a
routine is the issue, you may well discover the issue is not better
[6]: Gabrijelčič (2018), Delphi High Per- coding, but a better algorithm. See [6] for excellent coverage of
formance profiling in practice.
Disruptive Forces 20
With all this talk of refactoring, the task of updating legacy code 20.1 Code Details . . . . 249
sounds almost easy. But of course, it always appears simple in 20.1.1 Hints and Warnings 249
a book. Often, it is anything but simple. There are a number of 20.1.2 Static Analysis . . . 250
challenges which may multiply the difficulties you face. 20.2 Component Issues 251
20.2.1 Orphaned Compo-
nents . . . . . . . . . 252
20.2.2 Local Components . 252
20.1 Code Details
20.3 Dependency Cycles 255
20.3.1 The Gordian Knot . 256
There are cleanups which you can begin at any point, and which 20.3.2 Small Steps . . . . . 256
will reduce the level of noise as you proceed. These really should 20.3.3 Larger Steps . . . . . 257
be attacked before taking on any of the issues in the subsequent 20.3.4 Cycles in Compo-
sections. nents . . . . . . . . . 258
20.4 Compiler Versions 259
20.5 Issues You must
20.1.1 Hints and Warnings Find . . . . . . . . . 260

Apart from compiler errors, hints and warnings are among the
greatest value you will obtain from a build cycle. The correct num- Consider each hint or warning to
ber of each is zero. It is common in legacy projects for the numbers be a defect that simply hasn’t been
triggered yet.
to be in three or four digits. Each of these is a potential problem in
the field. Do yourself a favor, and make the elimination of these
problems—they are not potential problems—a high priority.
If there are messages about platform-specific routines, for example,
then make use of the conditionals provided by Delphi to suppress A routine specific to Windows, for
them. As they are expected, you don’t need to be reminded. On example, is not cause for concern if
you are designing only for Windows.
the other hand, messages which are not expected indicate clearly
the need for repairs.
You may see hints and warnings you think are unimportant. Think
again. When the compiler warns of an implicit string cast, for
example, that may be happening because you moved code from
an earlier version with no support for Unicode to a more recent
one. Sometimes it is a matter of a local variable that is (perhaps
needlessly) declared as an AnsiString, or a shortstring. Most of the
time, such things are easily resolved, and the sooner you clear up
your hints and warnings, the sooner you can focus entirely on the
major issues. If you brought hints and warnings with you from an
250 20 Disruptive Forces

earlier version of Delphi, they are no less important to clear than


they would have been before. If you have not yet migrated code,
however, you may wish to clear the hints and warnings in the
old code first—you gain the advantage of knowing that the ones
reported after migration are related to the compiler and language
changes, not basic logic.

20.1.2 Static Analysis

TMS offers FixInsight, and I recommend it without reservation.


You can freely alter the parameters it uses, but I suggest you use
the defaults to start. Get a full report on your entire project. You
may be surprised. You may also be discouraged. But in the end,
this is a snapshot of reality, and each report is an opportunity to
improve your code.
Unless you are very lucky, you will probably want to produce a
collection of reports from FixInsight which focus narrowly. Then
create cases for them, and assign to team members. Many of these
are at least as valuable as hints and warnings. Some are more so.
A report on empty finally and except clauses, for example, is very
valuable. Fixing those may avoid problems in your customers’
environment which may be very annoying to them, and difficult
for you to debug later.
Many of the issues reported you may think are not all that impor-
tant, but they can be excellent indicators of the areas where you
have much work to do.
▶ Long routines. The default setting is 50 lines. Routines which
are longer need rework because they are doing too much
directly. As a first attack, factor out nested routines. Later,
you will relocate those to data modules or business units.
▶ Long parameter lists. The default is 7. More parameters
suggest again a routine which is doing too many things.
Consider, however, that a reasonable alternative may include
passing one or more records as parameters, especially where
related data are involved. TPoint and TRect are two good
examples. Moreover, in new releases of Delphi, these contain
methods which may reduce your coding efforts.
▶ Too many variables. The default is again 7. This is easy to
exceed, and you may wish to increase it to 10, at least for now.
The tighter limit will lead you to much more refactoring.
20.2 Component Issues 251

▶ Empty then or empty else block. At best, these indicate


questionable logic.
The more you act on the reports from FixInsight, the better will be
your code. And the less often you will be blind-sided by your own
code.

FixInsight Threshold Settings

You may elect to change some of the settings in FixInsight


from their defaults. But I would suggest instead that you first
consider using comments to suppress some of the warnings.
Example:
begin //FI:C101

By being selective, you retain the full benefit of the default


settings, but you add comments which are very easy to find
later, when you are ready to tackle the concerns they flag.

20.2 Component Issues

It’s common that legacy applications will contain a cornucopia


of components. Setting aside the obvious issues of visual style
differences and behavioral differences, there are a number of
greater concerns:
▶ Orphaned components. When a component publisher van-
ishes, then you must assume responsibility at the source
level, or find a suitable replacement for the component.
▶ Homebrew Components. A homebrew component is often
an orphan in disguise. Creating it was fun, but maintaining
it may not be. The developer who created it may have moved
on. There may have been no design, and no useful comments.
▶ Tangled Components. The point of components has always
been code reuse. That is often lost in the market-speak, but
the other features really come down to reuse. If a component
makes use of modules which are specific to an application,
then reusability is lost, and other problems come into play.
252 20 Disruptive Forces

20.2.1 Orphaned Components

Open source is likely the largest contributor to orphaned compo-


nents. At some point, the publisher realizes that maintaining a
(possibly) complex component for no remuneration is not all that
much fun. And worse, people expect support for newer compilers
(which cost money) and again, lack of income rears its head.
There are strategies, and each has its motivators:
▶ Find an alternative component. Sounds easy, as there are
so many out there. But it may be difficult, for purely tech-
nical reasons, and may also require licensing a commercial
component (set or suite), which in a corporate environment
raises other issues.
▶ Update and maintain in-house. Abandonware is ripe to be
taken over, but with caution. First is the obvious requirement
to update it and then to maintain it. Often the component
was selected because no one in house had, or wanted to
have, expertise in the actions provided in the component.
Provenance becomes an issue in a corporate environment,
as there is always some risk of a lawsuit.
▶ Encapsulate. You may be surprised to find that you do not
have source to the component. At that point, as I briefly
covered early in the book, you may wish to wrap the compo-
nent into a DLL and get on the the main event. This is also a
reasonable path when the alternative components turn out
to deliver different results.
Whichever path you follow there will be issues to consider. Do not
rush to a decision, as you may have cause to regret it. Consider
what you would choose if you were building a new application,
not maintaining an old one.

20.2.2 Local Components

Sooner or later, all Delphi developers write a component or two.


Or perhaps more. Some do it well. If the ones you encounter
were well designed and implemented, proceed to the next section.
Otherwise, read on.
20.2 Component Issues 253

Given one or more components of questionable design, you must


evaluate your options. If the capabilities provided are fairly com-
mon, you may well find a suitable replacement on the commercial
market, possibly in a component suite you already license. That
would be the best case.
If you do not find suitable alternatives in commercial or open-
source components, then you will need to critically examine the
designs of those which you have.
▶ Is the component focused in what it does?
▶ Is the code behind the component well designed?
▶ Is the component code independent, or polluted with units
of the application?

Component Code Pollution

Although it should be pretty obvious that making use of appli-


cation modules in a component is bad practice, I would equally
warn against making use of other commercial components in
any that you write. Why?
▶ The vendor of the commercial component may cease
operations. Then you are dependent on an orphan, and
all future functionality rests on members of your team.
Sometimes the component in question is coded in a less
than wonderful fashion. Or perhaps the code seems fine,
but the comments are in a language no team member
understands.
▶ Your employer may decide to cease licensing components
from the vendor whose component you inherited. Just
another way you become dependent on an orphan. How-
ever, in this instance, you may be unable to continue to
use the orphan due to look and feel issues which are
significantly at odds with the newly selected corporate
standard.
▶ You may need to add support for platforms not supported
by the vendor. This is more challenging, and there may
be layers to the challenge, as one possibility is always to
rewrite. That removes the component as an issue, but
introduces dozens of other issues. Or, since writing new
code for new platforms will take significant time, you
are again facing the orphan issue, as the existing product
254 20 Disruptive Forces

will need to be maintained—probably for at least a year


or two—until it can be replaced.

Designing and writing components is a specialized endeavor, and


needs a person who understands the particular requirements of
that sort of coding. In-house designed components need one or
more champions, developers who are intimately familiar with
the process, and are determined to make their components defect
free. Relying entirely on commercial components returns the
bulk of your challenges to the realm of your product’s essential
functionality. And when a point is reached where changing to
other vendors is considered, the real impact on development may
be easier to assess.

Component Focus

A component comprises one or more classes which implement


some desired behavior. We have already seen that a class should
have a tight focus (Single Responsibility Principle), and this applies
even more to a component. That responsibility may be complex,
such as in a TDBGrid, or it may be simple, as in a TLabel, but it is
still viewed as a single, coherent responsibility.
The TDBGrid is a good example; it could have been designed to
couple directly to data, but that would have been very restric-
tive. Instead, it connects to a TDataSource, and that connects to
a data container, such as TTable, TQuery, or TClientDataset. The
TDataSource is arguably a better example, as it couples a TDataset
of any sort to a data control of any sort.
If your components participate in two or more unrelated or loosely
related activities, they are ripe for restructure or redesign. This
will almost certainly necessitate rework in modules which use the
components, but in the long haul, will be a new benefit.

Component Code Design

Components should be designed with even greater care than your


1: An excellent resource is Developing application.1 Many units in your application will be specific to a
Custom Delphi 3 Components, by Ray single purpose, but the components will usually be used more
Konopka, 1997, The Coriolis Group,
Inc. Yes, it’s old, but it’s excellent, and
widely. Any design and coding issues in a component will tend to
few authors now write about Delphi be more troublesome than in a form or a datamodule.
component design.
20.3 Dependency Cycles 255

Designing a component well is largely the same as designing


any class well. However, I would argue that application modules
should never be used in components. If a utility module is sufficiently
freestanding to be used in a component, that’s fine, but it should
then be made part of a library which is not contained within the
project tree. Opinions may vary, but this comes down once again
to the issue of coupling.

Component Pollution

Component pollution occurs when application code is made a


part of a component. This is a coupling issue, and also may
contribute to UDCs. Worse, when you attempt to clean up code in
the application, there may be collateral damage. As stated above,
this is a design issue, and needs repair.
A similar pollution occurs when some third-party component is
made a part of another component. This raises two other issues.
First, that later versions of Delphi are fussy about such things, and
you may see the message that your component package requires a
”never build” package to be rebuilt. Second, what if the embedded
component is from a set which you are trying to phase out, possibly
because it is no longer published?

Less is More
Early in the life of Delphi projects there seems to be an attraction
to using as many different components as possible. There is
some excitement to learning what has been done better than in
another similar component. But if the experiment shows it to
truly be better, should it not then replace all similar instances?
Use as few different component types as possible. To repeat
some advice:
Everything should be made as simple as possible,
but not simpler. – Albert Einstein

20.3 Dependency Cycles

I have repeatedly mentioned the issue of UDCs, and must do so


again here. UDCs are easily created, but can be very difficult to
256 20 Disruptive Forces

remove. They are worse than a code smell—they are a creeping


rot.

20.3.1 The Gordian Knot

The Gordian knot legend is useful. Once UDCs enter your application, they multiply. They are possibly
(https://en.wikipedia.org/wiki/ the ultimate manifestation of tight coupling, and will paint you
Gordian_Knot)
into a corner with breathtaking speed. We know (or should) that
tight coupling is a major enemy of clean code. Chains of unit
dependencies multiply your problems because the modules in
question are widely used, so the risk inherent in changing how
they work may impact hundreds of other modules.
You cannot unit test tightly coupled modules because they cannot
be isolated from their coupled brethren. That is a very serious
impediment which stands between you and reliable code. Since
There is a Catch-22 aspect to this, as unit testing is critical to the refactoring process, eliminating those
mentioned earlier in the book. You UDCs must be a high priority.
need unit testing, but can’t achieve it
without modifying code. This is why If you approach your Gordian knot as did Alexander, you will
the elimination of UDCs must be a
create a ruin. We have already explained why redesign and rewrite
high priority.
is not a practical alternative, and that is where a sword or machete
will lead you.

20.3.2 Small Steps

There is a series of small steps which you may use to approach


incrementally the problem of UDCs.
▶ Clean your uses clauses.
▶ Use Pascal Analyzer to identify uses references which can be
demoted to implementation level.
▶ Use MMX to collect unit dependencies data.
▶ Use the Cycles Analyzer tool to tabulate the MMX report
data.
When you have freed a module from UDCs, it is ready for unit
testing. Make that a priority. Run the tests at least as often as you
alter the unit(s) in question.
20.3 Dependency Cycles 257

20.3.3 Larger Steps

At some point, you will need to increase risk, to increase benefits.


This will still be incremental in approach, but with wider impact,
including the need to repair breakage in uses clauses and in
methods.
▶ Refactor utilities code.
▶ Refactor data handling to new data modules.
▶ Refactor business logic to new modules.

Refactor Utilities

Begin with the refactoring of utility modules, which is the least


risky of the larger steps. There are two major goals in this action:
▶ Don’t Repeat Yourself - Don’t repeat yourself. Eliminate code
which replicates library calls. Delphi has gained increased
library functionality in each new version. It is quite common
to find that legacy code implements routines which are now
found in the libraries. Retire your redundant routines.
▶ Refactor misplaced functions. Often a new routine is added
to a utility module because someone was working there
when they identified the need, but gave little thought to
whether that module was the right place for the routine. A
clue they missed was the need to add one or more module
references to the uses clauses. In particular, when Unit A
needs Unit B, which in turn needs Unit A, something has
been wrongly placed.
When you have freed a module from UDCs, it may be ready for
unit testing, but remaining dependencies may stand in the way if
the unit can’t be isolated. Make early unit testing a priority. Run Yes, I repeat myself; it’s important.
the tests at least as often as you alter the unit(s) in question.

Refactor Data Handling

Move data manipulation off your forms. Start small, with a form
which only contains one or two datasets. Create a datamodule for
them, and assign it a name which matches the form name but
with a different prefix. Follow whatever conventions are normal to
your project, or design new conventions if the old were irregular.
258 20 Disruptive Forces

Now replace all the direct code in the form with references to new
routines in the datamodule.

Limit your Risk

In this refactoring, limit your changes to code relocation, for


the most part. Do not dive into cleaning up logic in the data
handling yet. Even large steps can be cautiously taken, and the
goal is to minimize risk in this work, as you do not yet have
unit testing in place.

Since each of these actions affects only code which made direct
access to datasets, it is readily identified and easily isolated.

Refactor Business Logic

Move business logic off your forms. As with data manipulation,


start with relatively simple forms, and gain a feel for what you
need to do. You may debate what constitutes business logic, but to
simplify, anything more complex than the logical interlock among
visible controls should be removed from the form. And if that
control logic is dependent on data manipulation or business logic,
then put the details into the appropriate module which will be
called from the form.

Keeping it Simple

What should be on your form? As little code as possible. Form


testing should be only enough to verify the component events
fire as they should—it should not be fundamental to verification
of business logic and data manipulation.

20.3.4 Cycles in Components

UDCs in components are high value targets. How to eliminate


them may be a challenge, but eliminate them you must. You must
devise your own strategy for this work, based on the particular
components in your application. Consider:
▶ Do not alter third-party components. (You change them, and
you own them, for maintenance purposes.) At the very least,
20.4 Compiler Versions 259

you add burden to your team at the point of updating to


a new release from that vendor, as it may break your code.
And if they have changed their internal design, your rework
becomes non-trivial.
▶ Remove all application code from components. If the routines
are needed in both places, the better path would be to factor
out all those which could be library members, and put the
resulting libraries outside of the application project tree or in
a folder intended of utilities which are not project-specific.
▶ If you find it impossible to remove all application code from
a component, consider whether it might better be a class
or a frame rather than a component. Components should
really be kept as clean as possible.
▶ If you really consider it desirable to create a component
which is specific to the application, then try to follow the
spirit of the design rules as much as you are able. Eliminate
UDCs. Try to refactor shared code to application library
modules. Clearly warn in the code modules that maintainers
must understand the special nature of these component
modules.
After all that, if you still feel the need for application-specific
components, take a break. Ponder it away from the keyboard. Do
your best to find an alternative. One alternative is to see whether
you can remove the component code from the application and
replace it with instances of the component. Ultimately, the goal is
to clearly separate component code from application code.

20.4 Compiler Versions

Updating your application from an old Delphi version to one much


newer presents special challenges. Among them:
▶ Transition to Unicode. This is usually not a big problem, as
Delphi has simplified things in this area.
▶ Increased type checking. Latest versions of Delphi have
redefined some types in order to increase type safety. You
will find, for example, that where you had passed a Cardinal
as a parameter in the past, you now must pass a NativeUInt.
And on checking, you will discover NativeUInt = Cardinal;.
You have no alternative, and must repair these, but they are
easily recognized.
260 20 Disruptive Forces

▶ Component compatibility issues. One in particular I have


worked on is with respect to DevExpress when they changed
their architecture. The move from dx components to cx
presents some challenges. Nothing terrible, but it will affect
code.
▶ Component licensing issues. You may not be in possession
NOTE: Taking a lower price for a com-
ponent set which comes with only of current licenses for all the components in your application.
partial source may seem a bargain, Worse, some may have gone out of publication. Alternately,
but it limits your options going for- it may be a corporate budget and procurement issue.
ward.

Keeping it Simple

When you define a variable in future, avoid specialized types


where they are not needed. Use string for all strings; use
AnsiString and WideString only when necessary. This generally
means at a seam between your code and that of an outside
resource, like Windows.

20.5 Issues You must Find

I am inclined to refer to copy and paste as Design Pattern #1, as it


is pervasive. Finding these repeated patterns is something which
you will have to do for yourself, though refactoring is a good path
to follow. Each of these snippets should be elsewhere; in a utility
module, a data module, a business logic module, or in a constants
unit. Candidates include:
▶ Repeated strings. Move to modules of constants. If they are
captions, convert each to a resourcestring and put them in
their own constants module. Remember that global con-
stants are not inherently bad; they are certainly better than
repeated constants in separate modules where the name
or definition may be different. Keeping them together is
beneficial—redefining a constant name with a different
value in a different module is the prescription for endless
debugging headaches.
▶ Specialized types. Any type defined for the application will
be applicable either to the entire application or to some
segment of it. You may choose to define multiple modules,
but if so, it would be good to use unit qualifiers in code
20.5 Issues You must Find 261

which references them. The alternative is a need to ensure


that any given type declaration is uniquely named.
▶ Dataset initialization. This is best implemented in the rele-
vant data module. Never implement such code—however
simple—in a form. Assume that any data module may evolve
into being shared among units, and this will make that easier
to accomplish.
▶ Repeated logic. Often found inside an overly long routine,
and frequently resolved by making use of nested routines.
▶ Overly complex tests. When a conditional test uses more
than three of four terms, or where the terms used are object
members accessed through drill-down, then you will do
well to consider creating local variables for partial products.
It may also be appropriate to consider encapsulating that
logic in a nested routine. This will help keep the count of
local variables down, and also ensure the logic is easier to
analyze and verify.
▶ Multi-module tests. When a conditional test accesses terms
from other modules, then it will be even more beneficial to
use partial products in local variables as an aid to debugging.

Facilitate Maintenance
Code for easy readability and simple debugging. Very compact
code can hinder both. Good code is tight and expressive, not
terse and incomprehensible.

There is no automated tool which can replace thoughtful consid- Be open always to writing your own
eration and coding, or human recognition of patterns. There are tools. Keep them simple, and do not
try to build a super tool—the effort
commercial software tools which can help, but none of them I is smaller, and the payback larger.
have used provides all the elements I have found useful or even
necessary.
Patterns pasted into code can later be made to appear different
when someone inserts code into the middle of the pasted lines.
That’s less difficult for a human to recognize than for a utility
program. You certainly can try writing your own tool to recognize
the modified patterns, but will it be worth the effort? After all,
unless you use a formal parser like DelphiAST, you will be having
to deal with all the complexity of the source on your own. And
even with DelphiAST, there is much to handle. Then if you wish
it to apply corrections, you multiply the complexity. Most of the
tools available to assist with any sort of analysis do only that, the
262 20 Disruptive Forces

analysis. Then you are left with a large list of issues to resolve by
hand.
Each of the available analysis tools is limited in what it does, and
in how well it does the work. All of them which I have used leave
the rework to the developers. In the end, it is probably for the best.
An automated tool could wreak havoc in very little time.
Best Practices
Some Simple Principles 21
Principle of Least
21.1 Principle of Least Astonishment 21.1
Astonishment . . . . 265
21.2 DRY: Don’t Repeat
From (https://en.wikipedia.org/wiki/Principle_of_least Yourself . . . . . . . 266
_astonishment) Wikipedia: 21.2.1 The Rule of Three . 266
21.2.2 Causes of Repetition 267
The Principle of Least Astonishment, also called the
21.2.3 Using the Libraries . 267
principle of least surprise (alternatively a “law” or
21.2.4 String Utilities . . . 268
“rule”) applies to user interface and software design. 21.2.5 Date and Time Utili-
A typical formulation of the principle, from 1984, is: ties . . . . . . . . . . 268
“If a necessary feature has a high astonishment factor,
21.3 YAGNI: You Ain’t
it may be necessary to redesign the feature.” Gonna Need It . . . 269
A more general statement of the principle is that a 21.4 SOC: Separation of
component of a system should behave in a way that Concerns . . . . . . . 270
most users will expect it to behave; the behavior should
not astonish or surprise users.
In other words, the operation of a system element should be intu-
itive. At the development level, this means that to someone with Intuitive is a horribly overworked
reasonable experience in coding and with the problem domain, it word for a quality which is rarely
achieved.
works in obvious and sensible fashion.
Although this is very commonly applied in connection with the
perception of the application by users, it is worth application to
coding practice, as well. People’s expectations affect their comprehen-
sion. This is one reason that certain patterns become idiom: they
are so familiar as to be transparent to our thought processes.
We have all had the experience of reading some code and asking
“what were they thinking?” That sense of surprise is exactly the Many of us will even acknowledge
point here. I will include in this section a warning against clever having had that exact experience
while reading our own code, years
code, the sort of thing which may seem to illustrate a deep under- later. Such recognition is painful, but
standing of compiler internals, but provokes curses in maintenance a valuable lesson.
operations. Code should always be clear, even obvious in what it
is meant to achieve.

Any fool can write code that a computer can under- Computers have no difficulty un-
stand. Good programmers write code that humans derstanding, for example, GOSUB
11503.
can understand. – Martin Fowler
266 21 Some Simple Principles

Communicating clearly to those who follow us in our code is


every bit as important as implementing the required functionality.
Spaghetti code can—at high cost—achieve the needed functionality,
but maintenance will be a nightmare.

21.2 DRY: Don’t Repeat Yourself

This rule applies to a broad range of things, from simple to complex.


At the very simplest level it can be seen as a prohibition of copy
and paste. In the (hopefully) long life of a product, this becomes a
very important maintenance issue. A defect which is copied and
pasted, after all, must sooner or later be fixed in each instance of
that code.

21.2.1 The Rule of Three

The Rule of Three (https://en.wikipedia.org/wiki/Rule_o


f_three_(computer_programming)) stipulates that when you
recognize more than two instances of similar code, you should
recognize the need for a subroutine to encapsulate the needed
behavior.
In Delphi it is inevitable that you will use the TStringList, and very
likely you will use StrictDelimited mode. When that is true, you
will find yourself writing:

dl := TStringList . Create ;
try
dl . Delimiter := ’,’;
dl . StrictDelimited := True ;
dl . DelimitedText := MyText ;
// real work here
finally
dl . Free ;
end ;

For that reason, is is very attractive to create a class to solve this


problem. In fact, better still, create an interfaced class to solve the
problem. Then you can replace the code above with something
like this:
21.2 DRY: Don’t Repeat Yourself 267

dl := IDelimitedText . Create ;
dl . DelimitedText := MyText ;
// real work here

The repetitious initialization code is gone, and so is the try/finally


lifetime management. See section 11.9.1.

21.2.2 Causes of Repetition

In legacy code, things generally evolved as people thought of


new features, whether the suggestion was from a customer or
from in-house. New features tend to be added onto undesigned
applications without much thought as to whether a) the feature
could be designed and isolated from old code, or b) there could
be side-effects from the addition.
A classic cause is that as with the delimited list above, it’s easy As already mentioned, I think of
to copy and paste, and people simply didn’t think that there is a copy and paste as Design Pattern
#1, as it is so prevalent. But it is an
better way. Investing a bit more effort now yields a reduction of anti-pattern, a code smell. Repeated
effort later. And in the example given, automating disposal means code means repeated maintenance;
less effort in debug later, as well. such code should be converted to util-
ity routines and properly placed in
Unless the organization is very small, and the application quite testable utility modules.
new, there is a high likelihood that few if any current members
of the development team have comprehensive knowledge of the
entire code base. It is also likely that there exist multiple routines
which have overlapping or even duplicate functionality. When
the project contains hundreds, or even thousands, of units, such
problems are inevitable.
Consider, too, that the routine which might meet your needs exists
in a unit where you would not think to look. And has a name
which was not well considered. Some things will not yield to a
frontal assault, and you will need to accept that serendipitous
discoveries will be a normal part of the work.

21.2.3 Using the Libraries

When did you last read a library module? Or even skim it? There
is gold to be mined in those modules. Make time to explore, if not
in source, at least in the help modules.
268 21 Some Simple Principles

Many developers fail to explore the numerous libraries which


ship with Delphi. Or they did the exploration years ago, and have
not looked again as the libraries were updated. Some of the most
fruitful areas tend to be:
▶ String manipulation
▶ Date and time utilities
▶ File and directory utilities
▶ Registry utilities
▶ INI file utilities
▶ Protocol (CSV, JSON, XML) utilities

21.2.4 String Utilities

Everyone with significant Delphi experience uses the TStringList.


But in later releases, there is the TStringHelper record helper,
which offers many very useful small tools. But even in older
versions, there may be routines which you could use to replace
your own. Code you do not write is code you normally do not
have to maintain.
In older versions, I use TStringList for many simple parsing oper-
ations; in later releases, however, there is the TStringHelper.Split
routine, which is a lightweight and efficient way to parse a de-
limited string into an array of strings. The TStringHelper contains
many useful members, and will help you displace the much heavier
TStringList in updating your code.

21.2.5 Date and Time Utilities

In the early days, the DateTimeUtils library was relatively simple.


Many people added their own routines to handle particular re-
quirements in their applications. However, there is more than a
little opportunity to get things wrong, especially when dealing
with time zones, and the presentation formats used in other coun-
tries. If you have local DateTime utilities in use, then you should
plan to:
▶ Implement comprehensive unit tests.
▶ Remove routines which duplicate members of the library
DateTimeUtils.
▶ Recode remaining routines to use DateTimeUtils.
21.3 YAGNI: You Ain’t Gonna Need It 269

If you are not a specialist in the handling of DateTime values, and


particularly if your application crosses international boundaries,
then you really should take the time to familiarize yourself with
the available library library functions. In recent compilers, Delphi
fully implements ISO-8601 support, so you may be confident that
conversions will not produce unpleasant surprises.

21.3 YAGNI: You Ain’t Gonna Need It

From Wikipedia (https://en.wikipedia.org/wiki/You_aren’t


_gonna_need_it): At one time or another, we have probably all
fallen into the trap of adding functions to a class which are not yet
needed. It’s easy enough to do, as you are in the zone, and they fill
out a set of related functions. But to do so is a mistake, as YAGNI
reminds us.
▶ All code must be tested and maintained.
▶ You may never need the added functions.
▶ These functions are a distraction to those who follow you.
▶ Testing and maintenance has costs.
We need to keep in mind the task at hand, which is to implement a
coherent and reliable system. We need to implement the functions
required to produce that system, but no more. Remember:
▶ Classes should be small, tight, and focused.
▶ Keep everything private; expose as little as you can.
▶ Adding later to a well designed class is not difficult.
▶ Cleaning out unused code is tedious, and rarely done.

The Price of Temptation

Temptation is ever present, and there will be times when you


give in to it. It’s not fatal, though it may be a waste of time.
Remember, though, that at some point you will need to review
and trim.
Refactoring is an iterative process, not an event; we must always
be prepared to reconsider, and not only to polish our code to a
high shine, but also to throw away those methods which looked
like a good idea, but have never been used in production code.
Noise is tiresome, and keeping our code to only what is needed
270 21 Some Simple Principles

is the best way to reduce the noise level.

Simplify your code and your life: Do not implement what is not
needed. If requirements change tomorrow, then code to the new
requirements when they appear, not before. The real cost is not
simply in writing the routines, but in fully implementing the unit
testing needed to ensure they are reliable.

21.4 SOC: Separation of Concerns

From Wikipedia (https://en.wikipedia.org/wiki/Separation


_of_concerns):
In computer science, Separation of Concerns (SoC) is a
design principle for separating a computer program
into distinct sections such that each section addresses
a separate concern.
In practice, Separation of Concerns is related to the Single Responsi-
bility Principle, as they both address issues of focus and simplicity.
Single Responsibility Principle is applied at the class level, while
Separation of Concerns is applied to subroutines in any context. In
both, the rationale is essentially the same: do one thing and do it
well.
The virtues of small and well-focused routines include:
▶ They are more easily understood than are long and complex
ones.
▶ They are more easily maintained, and ultimately need less
maintenance.
▶ They are more easily made testable.
▶ They are more reusable.

Differing Views of SOC

Although I am presenting Separation of Concerns here as a


functional consideration, the term is also used in application
to frameworks, such as MVP, MVC, and others. I find the more
general use of the term to be preferable, as it really is something
which we should try to apply everywhere. For example, a
routine to export to a spreadsheet should not contain code to
21.4 SOC: Separation of Concerns 271

present a Save Dialog, but should either call a routine which


has that purpose, or be passed the filename in a parameter. This
thoughtful separation also supports the Don’t Repeat Yourself
principle.

Separation of Concerns should be a part of your normal thought


process.
SOLID 22
As (https://en.wikipedia.org/wiki/SOLID) Wikipedia tells us: 22.1 What is SOLID? . . 273
22.1.1 Single Responsibility 274
22.1.2 Open/Closed . . . . 275
In object-oriented computer programming, SOLID
22.1.3 Liskov Substitution 276
is a mnemonic acronym for five design principles 22.1.4 Interface Segregation
intended to make software designs more understand- Principle . . . . . . . 277
able, flexible and maintainable. It is not related to the 22.1.5 Dependency Inver-
GRASP software design principles. The principles are sion . . . . . . . . . . 278
a subset of many principles promoted by American
software engineer and instructor Robert C. Martin. I will not cover GRASP here. To para-
Though they apply to any object-oriented design, the phrase an old saw about standards,
the nice thing about methodologies
SOLID principles can also form a core philosophy for is that there are so many form which
methodologies such as agile development or adaptive to choose. SOLID seems very good
software development. The theory of SOLID principles to me, but you may have other pref-
was introduced by Martin in his 2000 paper Design erences.

Principles and Design Patterns, although the SOLID


acronym was introduced later by Michael Feathers.[4] [4]: Feathers (2004), Working Effec-
tively with Legacy Code

22.1 What is SOLID?

The mnemonic comprises these elements:


▶ Single Responsibility Principle: A class should only have a
single responsibility, that is, only changes to one part of
the software’s specification should be able to affect the
specification of the class.
▶ Open/Closed Principle: “Software entities ... should be open
for extension, but closed for modification.”
▶ Liskov Substitution Principle: “Objects in a program should be
replaceable with instances of their subtypes without altering
the correctness of that program.” See also design by contract.
▶ Interface Segregation Principle: “Many client-specific interfaces
are better than one general-purpose interface.”
▶ Dependency Inversion Principle: One should “depend upon
abstractions, [not] concretions.”
274 22 SOLID

22.1.1 Single Responsibility

Wikipedia (https://en.wikipedia.org/wiki/Single_respons
ibility_principle) again:
The single responsibility principle is a computer pro-
gramming principle that states that every module,
class, or function should have responsibility over a sin-
gle part of the functionality provided by the software,
and that responsibility should be entirely encapsu-
lated by the class. All its services should be narrowly
aligned with that responsibility. Robert C. Martin ex-
presses the principle as, “A class should have only
one reason to change,” although, because of confusion
around the word “reason” he more recently stated
“This principle is about people.”
As is so often the case, this principle is sufficiently abstract as to
allow for a great deal of interpretation. However, in the current
discussion, I think it makes sense to compare to what is often seen
in legacy code:

Table 22.1: Legacy Code vs. Single


Legacy Code Single Responsibility
Responsibility.
Long Routines Short Routines
Complex interactions Simple actions
Tangled code Short, simple code

Nothing extraordinary there, but in general, it means that a simple


routine in legacy might lead you to create several small classes,
each with its own responsibility. For example, you may have a
combobox which presents differently according to the current
state of the form. In legacy code, such changes would likely be
handled by calling a routine which is passed some value which
indicates the current state, and the routine would then thread
through various if/then/else and case statements, to yield the
desired fill for the combobox.
In a solution coded to the single responsibility principle, you
might design a small class which updates the combobox. It may be
passed one or more parameters for the form state, but internally,
it will for each variation implement as simply as possible the
required changes. Such a class also lends itself to more generalized
solutions, where the class is in a separate module, and is passed an
22.1 What is SOLID? 275

object which contains a variety of data, thus freeing the class from
specific knowledge of the particular combobox and its form.
Under the Single Responsibility Principle, however, the class would
not interact with other controls on the form, nor would it have
responsibility for interaction with a database.

22.1.2 Open/Closed

From Wikipedia (https://en.wikipedia.org/wiki/Open-clos


ed_principle):
In object-oriented programming, the open/closed
principle states
software entities (classes, modules, func-
tions, etc.) should be open for extension,
but closed for modification; that is, such an
entity can allow its behavior to be extended
without modifying its source code.

The name Open/Closed Principle has been used in two


ways. Both ways use generalizations (for instance, in-
heritance or delegate functions) to resolve the apparent
dilemma, but the goals, techniques, and results are
different.(Emphasis added.)

This principle has to do with the developer’s view of, and respon-
sibility to, a class. As Wikipedia states, credit to Bertrand Meyer
on this.[13]. [13]: Meyer (1988), Object-Oriented
Software Construction
▶ A module will be said to be open if it is still available for
extension. For example, it should be possible to add fields to
the data structures it contains, or new elements to the set of
functions it performs.
▶ A module will be said to be closed if [it] is available for use
by other modules. This assumes that the module has been
given a well-defined, stable description (the interface in the
sense of information hiding).

A class is open for extension, which means that internal fields


may be added, and the coding of methods may be altered. These
are implementation details which are of no concern to the class
consumer, but relate only to how the necessary result is achieved.
276 22 SOLID

A class is closed for redesign, meaning any change to the public


interface, when and because it is published for use. Altering the
public interface will likely introduce breaking changes which affect
the consuming classes in the application.

Open/Closed Principle Redesign

There may be times when the class really needs to be redesigned


in a way which breaks the existing interface. If the class is
internal to the product, and the changes do not affect other
products, that is one thing. But if you are talking about a
Delphi component, for example, which has been on the market
for years, and is widely used, then you must consider most
carefully the resulting damages to your customers. It may be
better to add a new version of the component, and to keep
the old, thus allowing the user to update at their convenience,
rather than forcing them into repairs in order to update their
system to your latest release. And even when breaking changes
appear to be the right and responsible move, they should be
made as seldom as possible.

22.1.3 Liskov Substitution

From (https://en.wikipedia.org/wiki/Liskov_substitutio
n_principle) Wikipedia:
Substitutability is a principle in object-oriented pro-
gramming stating that, in a computer program, if S is
a subtype of T, then objects of type T may be replaced
with objects of type S (i.e. an object of type T may be
substituted with any object of a subtype S) without
altering any of the desirable properties of the program
(correctness, task performed, etc.). More formally, the
Liskov substitution principle (LSP) is a particular defini-
tion of a subtyping relation, called (strong) behavioral
subtyping, that was initially introduced by Barbara
Liskov in a 1987 conference keynote address titled Data
abstraction and hierarchy. It is a semantic rather than
merely syntactic relation, because it intends to guar-
antee semantic interoperability of types in a hierarchy,
object types in particular. Barbara Liskov and Jeannette
22.1 What is SOLID? 277

Wing described the principle succinctly in a 1994 paper


as follows:

Let 𝜙(𝑥) be a property provable about objects x of type


T. Then 𝜙(𝑦) should be true for objects y of type S where
S is a subtype of T.

For those of us not accustomed to formal definitions of such things,


what this means to us in practice is that an object instance of
specialized type S may be assigned to a variable of less specialized
type T. Or more plainly, we can do this:

var
Fld : TField ;
begin
Fld := cdsSomeTable . FieldByName ( ’ Cost ’);
if Fld is TNumericField then
Fld . DisplayFormat := ’ 0.00 ’;

Since the Fld variable is undifferentiated, you will need to ensure


the use of a cast when accessing members which are specific to
certain TField specializations, such as DisplayFormat, which is only
present on the TNumericField types.

22.1.4 Interface Segregation Principle

From (https://en.wikipedia.org/wiki/Interface_segregat
ion_principle) Wikipedia:
In the field of software engineering, the interface seg-
regation principle (ISP) states that no client should be
forced to depend on methods it does not use. ISP splits As seems obvious, this principle ties
interfaces that are very large into smaller and more back to the notion of YAGNI, as well.
An interface which is larger than
specific ones so that clients will only have to know needed adds possible confusion, and
about the methods that are of interest to them. Such the certain need for maintenance.
shrunken interfaces are also called role interfaces. ISP
is intended to keep a system decoupled and thus easier
to refactor, change, and redeploy. ISP is one of the five
SOLID principles of object-oriented design...
This gives rise also to the question of how to subdivide the larger
interfaces. There are a number of options, depending on what fits
your requirements:
278 22 SOLID

▶ Create small classes with specialized capabilities and make


them members of the classes they serve, as needed.
▶ Create a base class, and inherit from it to add the detailed
capabilities. This is less flexible than using aggregation or
composition, but is sometimes the logical approach.
Software development remains more art than science, and is likely
to remain so for a long time. Following good principles helps to
reduce the level of art required, and moves closer toward science.

22.1.5 Dependency Inversion

In this section I will review both Dependency Inversion and Depen-


dency Injection. The former seems to me to require the use of a
Dependency Container, while the latter may or may not use the
container. Both seek to reduce coupling, and to simplify the han-
dling of dependencies. Dependency Inversion, however, actually
seeks to reduce the dependencies. To some extent, that becomes a
matter of semantics, as the use of the Dependency Container does
not appear to reduce dependencies; rather, it alters the process
by which they are made available. However, to be clear about the
matter of semantics, consider:
For this new breed of containers the inversion is about
how they lookup a plugin implementation. In my naive
example the lister looked up the finder implementation
by directly instantiating it. This stops the finder from
being a plugin. The approach that these containers use
is to ensure that any user of a plugin follows some
convention that allows a separate assembler module to
inject the implementation into the lister.
As a result I think we need a more specific name for this
pattern. Inversion of Control is too generic a term, and
thus people find it confusing. As a result with a lot of
discussion with various IoC advocates we settled on the
name Dependency Injection. – Martin Fowler (https:
//martinfowler.com/articles/injection.html)
As with so many issues in software design, there may be battles
over what these terms mean. It still makes sense to me to consider
them this way:
22.1 What is SOLID? 279

▶ Dependency Injection: The direct injection of dependent


objects into constructor, method, or property.
▶ Dependency Inversion: The use of a Dependency Container
to invert the dependency relationship.
Because of the interrelationship of the two concepts, I will begin Nick has also written a series of blog
with a summary discussion of Dependency Injection. For Del- posts on (https://medium.com/b
etter-programming/what-is-depe
phi, Nick Hodges has written a volume which amply covers the ndency-injection-b2671b1ea90a)
topic.[11] Dependency Injection which you
may wish to explore before commit-
From (https://en.wikipedia.org/wiki/Dependency_inversi ting to reading his book.
on_principle) Wikipedia:
[11]: Hodges (2017), Dependency Injec-
In object-oriented design, the dependency inversion tion in Delphi

principle is a specific form of decoupling software mod-


ules. When following this principle, the conventional
dependency relationships established from high-level,
policy-setting modules to low-level, dependency mod-
ules are reversed, thus rendering high-level modules
independent of the low-level module implementation
details. The principle states:
▶ High-level modules should not depend on low-
level modules. Both should depend on abstractions
(e.g. interfaces).
▶ Abstractions should not depend on details. Details
(concrete implementations) should depend on ab-
stractions.
That’s a lot to digest, and again the abstraction can be a bit
puzzling. If you look around for information on dependency
inversion frameworks, you will immediately be faced with the
concept of a Dependency Injection Container. But that is putting
the cart before the horse. To quote a Nick Hodges tweet:
Don’t even consider using a DI Container until you thor-
oughly and clearly understand Dependency Injection
itself.
Consider a simple example using the ubiquitous TStringList:

procedure FillList ( AList : TStrings );


begin
AList . Clear ;
// guts of the code will be here
end ;
280 22 SOLID

Notice that:
▶ FillList depends on the TStrings object
▶ FillList does not create the object, does not own it, does not
free it.
That is a trivial example of dependency injection. FillList was
written for a purpose, but does not participate in the lifetime man-
agement of the list object. Lifetime management is a responsibility
Always handle object lifetime by de- of the owner of the object. Obviously, in this case, that means
sign. It is simpler and much easier the caller of FillList or something at an even higher level. If an
than trying to fix it in the debug pro-
cess.
object is created in the constructor of its owning class, it should
be disposed of in the destructor of the owning class. Think of this
as a matter of symmetry: creation and destruction are handled at
the same level. Any other approach—other than with some sort of
automated management (interfaces or garbage collection)—will
lead to defects.
Another simple example could be when you pass a TRect to another
routine which then acts on that TRect. In the case of the TStrings
object, the routine altered its contents; with the TRect, it may
simply have read the contained values and left them unaltered.
In both cases, the objects in question were needed for the called
routines to do their work.
If there were no more to Dependency Injection than that, there
would be nothing to explain. Let’s consider why the owner should
create and destroy its owned objects. FillList is called from some
number of modules. One of those might have been responsible
to instantiate the class. Perhaps it was a form which instantiated
the class, and passed self as the owner. That guarantees that the
class will be freed eventually, and its destructor will dispose of its
owned objects. We don’t know or care when these actions occur,
only that they will. the Delphi runtime is responsible for these
actions; our code is not.
It is always best to take advantage of automated cleanup when
you can. Letting a form free its owned objects is one of those cases
where you can do so. And that reduces the responsibilities on
your code.
When an object is instantiated with nil as owner, then you must
ensure it is freed. If you instantiate it from module A, and free
it from module B, you have created a time bomb. Sometime in
the future, someone else will inevitably alter the code flow while
22.1 What is SOLID? 281

resolving some other issue, and the result is a memory leak. That’s
another aspect of tight coupling. In this case, the coupling is with
respect to the lifetime management. Reconsider your design, and
make module A responsible to instantiate and free, and let module
B simply make use of the object.

A Concrete Definition

At this point, we can present a much less abstract definition of


Dependency Injection:
A dependency is anything a routine needs in order to
do its work. This may include datasets, records, fields,
classes or arrays. In fact, anything which affects how
the called routine operates.
It is not important whether the dependency is altered by the
routine which uses it. The point is that we reduce coupling and
gain flexibility by passing in those dependencies on which the
code depends.

Categories of Dependency Injection

Citing again from Nick Hodges, there are three main categories of
dependency injection:
▶ Constructor injection. In this form, dependencies are passed
to the class constructor.
▶ Method injection. Here the dependencies are passed as pa-
rameters to a method.
▶ Property injection. In this type, a dependency is assigned to
a property of the consuming class. This is particularly useful
if the consuming class may not always need the dependency
item.
In most of these references, you will find the assumption that
dependency injection is used by classes, and often this is the
case. But as with the FillList example I showed at the outset,
procedural code can also employ dependent objects.
In all of these alternatives, the benefits gained are comparable. You
can make extensive use of these approaches in updating legacy
282 22 SOLID

code without ever considering the addition of a Dependency


Container.

The Dependency Container

[11]: Hodges (2017), Dependency Injec- I will give scant coverage to the Dependency Container here,
tion in Delphi and recommend highly that you read Nick’s[11] book for deep
coverage. He also provides sample code which will facilitate your
exploration.
In simple terms, the Dependency Container(DC) is a dictionary
which contains links to the registered dependency objects. The
DC is also responsible to manage the lifetime of the objects. Given
these responsibilities, it frees you from creating and destroying the
registered object types. On the other hand, if you do some modest
amount of searching online, you will also find criticisms of the DC
as a code smell or an anti-pattern, a topic Nick also discusses.
In your application initialization, you must register each of the
types which will be supported by the DC. Once that housekeep-
ing is accomplished, you may move on, largely oblivious to the
management of those objects.

The Smell of a Container


Generally, not using constructor injection can be considered a
code smell, in part because it is impossible for a dependency
injection container to satisfy a circular dependency graph
when constructors are the only injection points. In this way,
constructor injection prevents you from creating situations like
this.
Here you are using property injection to make a circular depen-
dency possible, but the prescribed fix for such a code smell is
to instead redesign your system to avoid the need for a circular
dependency.
From StackOverflow: (https://stackoverflow.com/questi
ons/5614121/is-it-a-code-smell-to-inject-a-dependen
cy-and-set-one-of-its-members-to-this#:~:text=Gener
ally%2Cnotusingconstructorinjectioncanbeconsidered,i
njectionpreventsyoufromcreatingsituationslikethis.)
22.1 What is SOLID? 283

In the context of this book, the rework needed on a legacy project


to reach the point where you would profit by using a DC is far
in the future. Legacy code is, by definition, unruly, and you must
impose a very consistent discipline on it to being order out of chaos.
The larger the project, the larger is the disorder you must quell. I
do not mean to suggest you should not use the DC; Nick would
suggest you should always use it in any non-trivial application.

Dependency Injection Summary

Consider the contrast to Dependency Injection with no container.


This is an approach you may begin using very quickly, as an
ordinary part of refactoring code. You can apply it to procedures,
or to classes, and can employ any of the three forms described
above. Each time you apply it you will bring an improvement to
your code, a reduction of coupling, and of complexity.
Keep in mind that you are in pursuit of a few important results:
▶ Cleaner more readable code.
▶ Easier maintenance.
▶ Increased testability, reliability, and so on.

Criticisms of Dependency Injection frameworks (which always in-


clude the DC) include complaints about difficulty in debugging.
Indirect code flow. Do some searching and draw your own conclu-
sions. One critic I read posted:
DI is the modern day equivalent of global values. Refers to DI with a container.

I think he meant to write global variables. Any time you approach


the bleeding edge in methodology you will encounter widely di-
vergent opinions. Ultimately, you must decide what is appropriate
for your own projects.
Every journey begins with small steps. Apply Dependency Injection
in simple ways, and ease your way into the rest.
Inheritance, Aggregation,
Composition 23
The matter of OOP relations comes down to a choice among 23.1 Inheritance . . . . . . 285
Inheritance, Aggregation, and Composition. 23.2 Composition . . . . . 286
23.3 Aggregation . . . . . 288

23.1 Inheritance

The first approach to relation we all learned was almost certainly


inheritance, the “is a” relation. The VCL builds extensively on
inheritance, and uses it very successfully. However, the deeper
the inheritance, the more constraining things become. It is not
uncommon to see class members made more visible in descendant
classes. This becomes an issue at times, as it is not directly possible
to make members less visible.
From Wikipedia (https://en.wikipedia.org/wiki/Inheritanc
e_(object-oriented_programming)):
In object-oriented programming, inheritance is the
mechanism of basing an object or class upon another ob-
ject (prototype-based inheritance) or class (class-based
inheritance), retaining similar implementation. Also de-
fined as deriving new classes (sub classes) from existing
ones such as super class or base class and then forming
them into a hierarchy of classes. In most class-based
object-oriented languages, an object created through
inheritance, a "child object", acquires all the properties
and behaviors of the "parent object", with the exception
of: constructors, destructor, overloaded operators and
friend functions of the base class. Inheritance allows
programmers to create classes that are built upon ex-
isting classes, to specify a new implementation while
maintaining the same behaviors (realizing an interface),
to reuse code and to independently extend original soft-
ware via public classes and interfaces. The relationships
of objects or classes through inheritance give rise to a
directed graph.
286 23 Inheritance, Aggregation, Composition

The obvious effect of inheritance is that each descendant is larger


and more complex than its ancestor. In Delphi there are some
classes which have a custom version designed to be used as an
ancestor. So TSomeClass and TCustomSomeClass contain the same
members, but in TCustomSomeClass, you elect which you will make
Lacking a TCustomxxx version of a public or published. This flexibility offers some benefit, but the
particular class, your only alternative work can be tedious if the ancestor has many members you wish
will be to create one in your applica-
tion modules, which you should not
to expose. Moreover, there are relatively few of the TCustomXXX
do lightly. classes, so it is by no means a universal solution to the problem.
To many of us, encapsulation and inheritance are tightly linked
concepts. Certainly, a class encapsulates, and an inheritance chain
implies even more encapsulated data and methods, but it is not a
universal panacea. Rather, over time it can become a very limiting
cage. The main issue is the ever increasing number of visible
members, whether public or published.
Some people bemoan the lack of multiple inheritance in Delphi,
even though practice has shown that multiple inheritance is very
difficult to use, and even more so to use well. A more usable
variation on the concept is obtained through the use of interfaces—
a class may inherit from several of them.
[7]: Gabrijelčič (2019), Hands-On De- In his book[7], Primož Gabrijelčič presents in Chapter 1 a great
sign Patterns with Delphi contrast between inheritance and composition, with downloadable
code in a project called CompositionVsInheritance.

23.2 Composition

Composition is an approach in which a class contains instances of


other classes which facilitate its work. These member classes are
owned by the containing class, and will be destroyed by it, when it
is destroyed. Unlike the situation with inheritance, a member class
may be entirely private to the containing class, and be referenced
only internally. Or, it may have some methods and properties
exposed, while keeping the rest private. This flexibility solves the
visibility problem mentioned above, in inheritance. Benefits of
composition include:
▶ Simpler relation to other classes
▶ Easily limited coupling
▶ No name collisions
▶ No functional conflicts
23.2 Composition 287

▶ Expose in your interface only the essentials

As an example, you may wish to implement some string list ma-


nipulations in a very specific way, without granting the exposure
of possibilities presented by using a TStringList. Useful as it is, the
TStringList is a bit of a Swiss Army knife which may be considered
to violate the Single Responsibility Principle. In this small fragment,
a comma delimited list is passed as a string to the class constructor,
which creates a TStringList instance. This object is owned by the
class instance. The class must therefore be responsible to release
the instance in its destructor.

type
TSomeClass = class
private
FStrings : TStrings ;
public
constructor Create ( AList : string );
destructor Destroy ; override ;
end ;

implementation

TSomeClass . Create ( AList : string );


begin
FStrings := TStringList . Create ;
FStrings . CommaText := AList ;
end ;

TSomeClass . Destroy ;
begin
FStrings . Free ;
inherited ;
end ;

Creating a class in which a TStringList is contained as a private


member lets you make specific use of the TStringList capabilities,
while maintaining strict control over what the class can do to your
list. You can also hide in the new class the initialization of the
TStringList options, ensuring that you have blocked misuse.

Whatever manipulation this class is called to perform, it will do


so on the FStrings object, which is local to the class instance. If
the result is wanted for use by the caller, the caller must explicitly
obtain it through the Strings property.
288 23 Inheritance, Aggregation, Composition

23.3 Aggregation

As commonly understood, aggregation is similar to composition,


but without ownership of the member classes. A simple example
could be that the class contains a pointer to a dataset which already
exists, but does not own the dataset. Making it a member simplifies
coding within the class, but the dynamic assignment makes it
possible to invoke the class as a processor of the data, without
ownership.
An example of this would be property injection. See section 22.1.5.
In this fragment, a TStrings object reference is passed in through
property injection.

type
TSomeClass = class
private
FStrings : TStrings ;
public
property Strings : TStrings
read FStrings
write FStrings ;
end ;

This small fragment shows a field FStrings in the class which


contains a reference to a TStrings object which was passed in via
a property. The class can manipulate the content of FStrings, but
must not free it. FStrings can be set to nil, if there is reason to do
so, as it is simply a pointer to an object instance outside the class,
but that pointer would allow the called routine to dispose of an
object it does not own by calling Free, and that is why you need to
establish a rigid discipline in lifetime management.
If this class alters the content of FStrings, those changes are made
to the TStrings object which the caller owns; this class has no copy
of the object, but holds a reference to it.
Design Patterns 24
This will in no way be more than a survey coverage of design 24.1 Anti-patterns . . . . 289
patterns, but will make mention of those which may offer the 24.2 Delphi Idioms . . . 290
greatest value in refactoring your project. [7]. 24.2.1 Create/Destroy . . . 290
24.2.2 If/Else . . . . . . . . 293
24.2.3 Helpers . . . . . . . 294
24.1 Anti-patterns 24.2.4 Smart Pointers . . . 295
24.3 Recommended
Practices . . . . . . . 298
Since the focus here is on legacy projects, it seems fitting to begin
24.4 Patterns of Interest 298
the consideration of patterns with a discussion of anti-patterns.
24.4.1 Adapter Pattern . . 299
As Wikipedia (https://en.wikipedia.org/wiki/Anti-pattern) 24.4.2 Facade Pattern . . . 299
offers: 24.4.3 Dependency Injection 300
An anti-pattern is a common response to a recurring 24.5 Summary . . . . . . 301
problem that is usually ineffective and risks being highly
counterproductive. The term, coined in 1995 by Andrew
Koenig, was inspired by a book, Design Patterns, which
highlights a number of design patterns in software [7]: Gabrijelčič (2019), Hands-On De-
development that its authors considered to be highly sign Patterns with Delphi

reliable and effective.


All very elegant and formal, but possibly the worst and most
common of anti-patterns is copy and paste! All sorts of nasty
issues can be associated with this activity, including:
▶ Replicated defects
▶ Multiple definitions of constants
▶ Very difficult to identify and replace all occurrences
▶ Often leads to large methods (big ball of mud)
In short, it is evil. Wikipedia (https://en.wikipedia.org/wik
i/Copy-and-paste_programming) agrees, though they use softer
terms.
Other anti-patterns include:
▶ Interface bloat
▶ Circular dependency (UDCs, for example)
▶ Too many functions in a single class
▶ Failure to minimize scope
▶ Sequential coupling (critical sequence)
290 24 Design Patterns

▶ Cargo cult programming (reuse without understanding)


▶ Error hiding
▶ Magic numbers and strings (hard-coded literals, unexplained)

This is in no way a comprehensive list. Anti-patterns are rife in


legacy code, and are always troublesome.

24.2 Delphi Idioms

Idioms are mini-patterns. Small conventions, if you will, which


are essential to good practice.

24.2.1 Create/Destroy

One such is the idiom for creating and destroying an object


instance:

var
SomeObj : TObject ;
begin
SomeObj := TObject . Create ;
try
// some code here
finally
SomeObj . Free ;
end ;
end ;

The key things here are:


▶ Object created outside of try/finally
▶ Object freed in finally clause
▶ Object freed by same routine or class that created it

Failing to use try/finally gives this uncluttered and utterly unsafe


form:

var
SomeObj : TObject ;
begin
SomeObj := TObject . Create ;
// some code here
SomeObj . Free ;
end ;
24.2 Delphi Idioms 291

Terrible! Was SomeObj successfully created? If not, then trying to We tend to assume that a construc-
tor succeeds. But what if it fails be-
free it will throw an exception. Did the code executed before the
cause of some external system? Or
call to free throw an exception? Then the object is not destroyed. because of bad coding? Our coding
must account for such possibilities,
Keeping construction and destruction in the same scope is impor- or the user will be greeted by excep-
tant. If SomeObj is to be created by ModuleA, then it should also be tion messages which should not be
freed in ModuleA. To do otherwise is very risky, and tedious to seen in the field.
debug. If that seems too heavy a constraint, you should consider
using interfaced objects, so you need not manually manage their
destruction.
And yes, you could FreeAndNil(SomeObj) if you prefer. But it is
difficult for me to like FreeAndNil, which seems to resemble an
ill-used try/except clause which merely eats its exceptions. As
FreeAndNil can be applied to an object already freed, then its
failure to thrown an exception may mask real issues which need
correction.
When multiple objects are to be managed, then the canonical form
of the try/finally idiom begins to look awkward:

var
FirstObj , SecondObj : TObject ;
begin
FirstObj := TObject . Create ;
try
SecondObj := TObject . Create ;
try
// some code
finally
SecondObj . Free ;
end ;
// some code here
finally
FirstObj . Free ;
end ;
end ;

Then someone decides to simplify, and uses this form:

var
FirstObj , SecondObj : TObject ;
begin
try
FirstObj := TObject . Create ;
292 24 Design Patterns

SecondObj := TObject . Create ;


// some code here
finally
SecondObj . Free ;
FirstObj . Free ;
end ;
end ;

This is simply wrong. If SecondObj creation fails, then both it


and FirstObj will be freed—but the pointer to SecondObj remains
uninitialized, so the call to Free will act on some random address,
and (if you are lucky) throw an access violation. What you can do,
however, is this:

var
FirstObj , SecondObj : TObject ;
begin
FirstObj := nil ;
SecondObj := nil ;
try
FirstObj := TObject . Create ;
SecondObj := TObject . Create ;
// some code here
finally
SecondObj . Free ;
FirstObj . Free ;
end ;
end ;

This is safe because you initialized the pointers to nil before at-
tempting to create instances. There is one issue remaining, which
is that if the call to SecondObj.Free throws an exception, then
FirstObj.Free is not called. But is a cardinal rule in Delphi that a
destructor may not throw an exception, so in developing your own
classes, make sure you prevent that possibility. How you accom-
plish that will vary, but this is a rigid requirement in Delphi for
good memory management. For more detail, see Dalija Prasnikar’s
[15]: Prasnikar et al. (2018), Delphi excellent book[15] on memory management.
Memory Management

The Value of Idiomatic Usage

Personal preference always comes into play when coding,


but there are idiomatic forms which are best honored. This is
particularly true when dealing with try/finally and try/except.
24.2 Delphi Idioms 293

The old expression says that "familiarity breeds contempt", and


that is often so — it may be a motivation for getting creative
in your own coding. But I would argue that familiarity breeds
certainty.
The reason for idiomatic usage in language is that it conveys
sometimes complex ideas in simple and well understood terms.
the same is true in code; we become accustomed to coding
idioms, and recognizing them becomes transparent to our
reading.

24.2.2 If/Else

It is very common to see in older code a very simple pattern:

var
s : string ;
begin
if Cond then
s := ’ True ’
else
s := ’ False ’;

Many developers have written this idiom so long they think


nothing of it. But a better way is:

var
s : string ;
begin
s := ’ False ’;
if Cond then
s := ’ True ’;
{ alternately }
s := IfThen ( Cond , ’ True ’, ’ False ’);

The result is the same, but in some versions of Delphi, you may
see a hint or warning that it may not have been initialized in
the first version. The cost of a string assignment that may not be
used is minuscule. If you are dealing with a situation in which an
assignment has a significant performance cost, then you should
prefer to assign initially from the one with the lower cost.
294 24 Design Patterns

24.2.3 Helpers

The Design Patterns which kick-started the discussion of such


things were derived from practices in Java. As expected, not all
such patterns are equally useful in other languages.
Delphi offers class and record helpers, and though not strictly a
match to the accepted definitions of “standard” (i.e. GoF) design
patterns, they are essentially variations on the Decorator Pattern.
However, because they have been designed to extend existing
classes, they avoid the tedium of implementing a decorator in the
conventional way.
Helpers do have limitations. From the Delphi Wiki: (https://do
cwiki.embarcadero.com/RADStudio/Tokyo/en/Class_and_Rec
ord_Helpers_(Delphi))
A class or a record helper is a type that - when associated
with another class or a record - introduces additional
method names and properties that may be used in
the context of the associated type (or its descendants).
Helpers are a way to extend a class without using
inheritance, which is also useful for records that do not
allow inheritance at all. A helper simply introduces a
wider scope for the compiler to use when resolving
identifiers. When you declare a class or a record helper,
you state the helper name, and the name of the type
you are going to extend with the helper. You can use the
helper any place where you can legally use the extended
class or record. The compiler’s resolution scope then
becomes the original type, plus the helper.
Class and record helpers provide a way to extend a
type, but they should not be viewed as a design tool
to be used when developing new code. For new code
you should always rely on normal class inheritance and
interface implementations.
Class helpers are very useful, but they have limitations.
▶ A class can have only a single class helper.
▶ A class helper can introduce class fields, but no instance
fields.
▶ Operator overloading is not supported.
▶ You must be aware of class helpers in Delphi libraries.
24.2 Delphi Idioms 295

Any class can have a class helper, but only one will be in scope
at any time. The documentation suggests there can be only one,
but that is not strictly true. You could have different helpers for
the same class, but only one will be active in any scope. For
example, you could write a class helper for strings, and put it in
the implementation uses clause of one of your modules. It will then
replace the Delphi string class helper, as it is the most recently seen
by the compiler. This is complicated and risky, and not suggested,
but possible.
You can introduce class fields in the helper, but no instance fields.
You are writing a helper, a supplement to the class, not a full class.
The logic here is that class fields become global to the class. But in
the class helper, there is otherwise no storage.
Operator overloading is not supported. Once again, this is a
limitation imposed on the class helper for a reason. Were it possible
to add operator overloading, that would have to alter the behavior
of the class to which it is attached. A class helper supplements
operations of the class, but cannot alter them.
A great benefit of class helpers is that they may be used to add
functionality to third-party classes and components, which may
be beneficial for many reasons.

24.2.4 Smart Pointers

It may be stretching a point to call a Smart Pointer a Delphi idiom,


as they don’t seem to have become all that popular. Still, they have
their place, and can solve some thorny problems.
An object should be freed in the scope in which it was created.
It could be created and destroyed inside a routine, or in the
constructor and destructor of a unit. But what is not good is for
UnitB to destroy what was created by UnitA. Such a design would
impose on the consumer requirements which can’t reasonably be
anticipated. And such an approach will almost guarantee memory
leaks, as the required management is easily overlooked.
In legacy code, it can be difficult to untangle the creation and
destruction of objects, but it remains important, as memory leaks
will degrade your program’s stability. It is one thing to have a leak
in an email editor which is freed when the mail is sent, and quite
296 24 Design Patterns

another to have a leak in a form or service which is supposed to


be operable 24/7 for months on end.
One approach which you can use to ensure objects are freed is the
smart pointer. It is created when the object is instantiated, and
it is responsible for freeing the object when all references have
gone out of scope. That may sound familiar, and it should, as
it is essentially what happens with an interfaced object. But the
smart pointer—which is an interfaced class—is useful with objects
which have not been interfaced. It does require, however, that you
have a fairly recent version of Delphi.
There are numerous implementations available, and Shared from
Spring4D is very capable, but I also like this simpler one.

unit u_SmartPtr ;

interface

uses
SysUtils , System . Generics . Collections ;

type
TSmartPointer <T: class > = record
private
FValue : T;
FFreeTheValue : IInterface ;
function GetValue : T;
public
constructor Create ( AValue : T) ; overload ;
property Value : T read GetValue ;
end ;

TFreeTheValue = class ( TInterfacedObject )


private
FObjectToFree : TObject ;
public
constructor Create ( anObjectToFree : TObject );
destructor Destroy ; override ;
end ;

implementation

{ TSmartPointer <T > }

constructor TSmartPointer <T >. Create ( AValue : T);


24.2 Delphi Idioms 297

begin
FValue := AValue ;
FFreeTheValue := TFreeTheValue . Create ( FValue );
end ;

function TSmartPointer <T >. GetValue : T;


begin
Result := FValue ;
end ;

{ TFreeTheValue }

constructor TFreeTheValue . Create (


anObjectToFree : TObject );
begin
FObjectToFree := anObjectToFree ;
end ;

destructor TFreeTheValue . Destroy ;


begin
FObjectToFree . Free ;
inherited ;
end ;

end .

In use, it is equally simple:

var
SomeList : TStringList ;
SomeList_ptr : TSmartPointer < TStringList >;

begin
SomeList := TStringList . Create ;
SomeList_ptr . Create ( SomeList );
// usage code here

If you use the Shared record from Spring4D, then it would look
like this:

var
SomeList : IShared < TStringList >;

begin
SomeList := Shared . New ( TStringList . Create );
// usage code here
298 24 Design Patterns

The list is created, then the smart pointer is created, with custody
of the list. When the list goes out of scope, the smart pointer will
free it.
If you wish to use the more comprehensive capabilities of Shared,
from (https://bitbucket.org/sglienke/spring4d/wiki/Home)
Spring4D , you will not go wrong.

24.3 Recommended Practices

Earlier, you read that inheritance is relatively brittle, and com-


position or aggregation is a better model to use. That remains
true, and in the consideration of design patterns, deserves to be
emphasized.
Designing with interfaced classes is another practice worthy of
emphasis. You will reduce coupling, and gain automated mem-
ory management by following that model. See, for example, the
discussion in section 11.9.1 The Public Interface.
In general, if you will follow Single Responsibility Principle and
Separation of Concerns, these will tend to lead you to more main-
tainable and practical solutions. Most often, the tangles in legacy
code result from routines which try to do too much, in hundreds
of lines of code. These run afoul of many pitfalls:
▶ Mismanagement of state variables
▶ Confused sequence of operations
▶ High noise levels making code incomprehensible
▶ Memory leaks
▶ Erratic interaction with GUI
▶ Impossible to thoroughly test

24.4 Patterns of Interest

Working on Legacy Projects, it may be difficult to imagine intro-


ducing design patterns. There are some, however, which can easily
be brought into the mix, and will facilitate cleanup.
24.4 Patterns of Interest 299

24.4.1 Adapter Pattern

The example in the interfaces section in which an interfaced object CSV stands for comma separated
operated on a contained TStrings is a reasonable starting point. If values, such as you will see in
the CommaText property of a
you have occasion to use CSV strings, then you almost certainly TStringList.
make use of a TStringList with StrictDelimited set to true. There
is some boilerplate code you must remember to apply, and of Setting StrictDelimited to True
course, you must manage the life of the object. and then using the DelimitedText
property of a TStringList over-
Alternately, you could create an interface to expose the operations comes some unfortunate defects
in the implementation of the
and properties you need, and then build the class to fulfill the
CommaText property. In general, it
interface. In its constructor, you would put the property settings for is best to use DelimitedText, as
the StrictDelimited operations. You might decide not to interface the results are reliable and consis-
the Text and CommaText properties, as this will be used only for CSV tent, whereas CommaText can sur-
prise you.
strings, and limiting the interface will ensure proper application.
Once that has been written—and tested, of course—you are on
your way to simplifying the handling of your CSV strings. And
you should be able to remove clutter from one or more units in
your project, as the replacement proceeds.
Deeper coverage may be found in Primož Gabrijelčič (see [7], page [7]: Gabrijelčič (2019), Hands-On De-
150). As with any pattern, there are details which are essential: sign Patterns with Delphi

Keep in mind that adapters should not be used when


designing code. If parts of the design don’t fit together,
either change the design or use the bridge pattern.
The Adapter Pattern is also a good tool for isolating your code from
classes which are known to be buggy, or are subject to frequent
change. Your adapter presents a stable interface, and although you
may have to rework it to deal with defects of, or updates to, the
contained class(es), such work is localized to your adapter, rather
than in multiple places in your project.

24.4.2 Facade Pattern

The Facade defines a unified, higher level interface to


a subsystem that makes it easier to use. Consumers
encounter a Facade when ordering from a catalog. The
consumer calls one number and speaks with a customer
service representative. The customer service representa-
tive acts as a Facade, providing an interface to the order
300 24 Design Patterns

fulfillment department, the billing department, and the


1: From CnBlogs: (https://www.cn shipping department.1
blogs.com/xiuyusoft/archive/20
11/06/27/2091288.html.) You may find many places where the Facade Pattern will help
to tame otherwise unruly code. Although the comparison to a
catalog is easily comprehensible, it also suggests the possibility
of a do everything class, and that would be a grave error. Consider,
instead, the possibility of exporting to a number of formats: CSV,
JSON, XML, and XLS. Each of these requires output to a file, and
each must implement a particular format in its output. From the
perspective of the calling routine, however, each must be given a
target file and a collection of data. The output format is implicit
in the routine itself, and though often assumed based on the
file extension, will probably be specified by a parameter to the
facade.
How you will implement the required classes is up to you; the
point is that the Facade Pattern will provide a uniform mechanism
through which the different possible outputs are supported. For
[7]: Gabrijelčič (2019), Hands-On De- more detail, you may wish to read Primož Gabrijelčič (see [7], page
sign Patterns with Delphi 179):
...the facade pattern wraps multiple components and
subsystems. In addition to that, facade is the only pattern
from this chapter that provides a reduced interface and
not a full feature set of wrapped components.
So the Facade Pattern provides an interface (the facade) through
which the caller obtains a result while remaining ignorant of
the messy details. Perhaps your facade will make use of TWriter
classes. The facade itself should be free from specific awareness
of, or responsibility for, any details of the services provided by
the writers. Further, this mechanism makes it easy to add to the
collection of writers in future, should it be decided that a writer is
needed for YAML, for example. As with any pattern, the goal is
to make the implementation independent of the specification, an
essential requirement for decoupling of program features.

24.4.3 Dependency Injection

Dependency Injection (DI) seems now to be always considered in the


context of a framework, but that is only one perspective. Consider
[7]: Gabrijelčič (2019), Hands-On De- this, from Primož Gabrijelčič (see [7], page 51):
sign Patterns with Delphi
24.5 Summary 301

Dependency Injection works by changing responsibility


for object creation. In classical OOP, each object creates
new objects that it needs for functioning. This makes
the program very rigid and hard to test. DI turns this on
its head. If an method will be passed other objects, the
caller (the code which creates the first objects) should
also create these other objects and pass them to the first
one. In short, a dependency injection says this: Don’t
create things yourself; your owner should provide them.
This decouples objects (makes interconnections between
them less tight) and simplifies testing.
This suggests that in a choice between creating a local object
instance, or passing in an existing instance, the latter is to be pre-
ferred. In a simple example, a routine to operate on a TStringList
could either be passed the content needed, and create its own
TStringList instance, and populate it, or it could simply be passed
a TStrings reference, and operate directly on the source instance.
The routine you must write is then thinner and simpler.
So in the simplest case, we see that DI can be based on passing
in references to objects, mere parameter passing. This is not to
suggest that you should overlook the use of a DI framework, such
as in Spring4D (https://bitbucket.org/sglienke/spring4d/wi
ki/Home , but a legacy project will generally need a good bit of
rework before you are ready to apply such a framework.
The subject of Dependency Injection needs more coverage than I
have given in this section, and will be treated in Chapter 25. It is,
however, a design pattern which seemed appropriate to introduce
here.

24.5 Summary

Design Patterns are an essential study in modern program devel-


opment, and a thorough coverage of the subject is well beyond
the scope of this volume. Happily, a great deal has been written
on the subject. For Delphi, most notably, Hands-On Design Patterns
with Delphi by Primož Gabrijelčič (cited above), is an excellent
resource.
Dependency Injection 25
This chapter will present what has been called by some ”pure 25.1 Starting Small . . . 303
DI”, meaning that it is handled directly in your coding, and does 25.2 Types of Injection . 306
not introduce the Dependency Injection Container. This is not 25.2.1 Constructor Injection 307
to suggest that using a DI container is to be avoided. Rather, it 25.2.2 Property Injection . 307
recognizes that the use of such a container requires a substantial 25.2.3 Method Injection . . 308
reconsideration of your design, and as such, is difficult to plug 25.3 Summary . . . . . . 310
in while you are recovering from inherited problems. And in an
application of significant complexity—any which have been in
production for more than a decade are likely to qualify—you must
expect that your refactoring efforts will be substantial for at least a
few months.
You may think that a large team can reduce the time needed, and
to an extent, that is true. However, do not overlook that not all team
members may have equal skills in refactoring, or comprehension
of all areas of the code. A large team may easily introduce uneven
code quality, and even fractious approaches.

25.1 Starting Small

The subject of Dependency Injection (DI) seems initially confusing,


even daunting, but approaching it through small steps will reveal
that it is not anything exotic, merely a different way of approaching
your coding. You may wish to avail yourself of Nick Hodges’
book[11] on the subject for broader coverage. [11]: Hodges (2017), Dependency Injec-
tion in Delphi
A dependency is simply something on which your routine depends
to perform its function. We have all seen a trivial example of DI
many times:

function FindItem ( const AItem : string ;


AList : TStrings ): Integer ;
begin
if not AList . Sorted then
AList . Sorted := True ;
AList . CaseSensitive := False ;
Result := AList . IndexOf ( AItem );
end ;
304 25 Dependency Injection

The example is contrived, but what we need to understand includes


that FindItem:
▶ Does not own AList, but depends on it for its function.
▶ Neither creates nor destroys the TStringList object.
▶ Needs no particular awareness of the content of the list, other
than that the items are strings.
This is an example of Method Injection because the dependency
is passed in as an argument to the method. This is a very useful
approach to Dependency Injection and is easily applied in refactoring
legacy code, so well worth your consideration.

DI Can Apply to Procedural Code

Although the focus here will be on DI with methods, as in the


example above, it is equally applicable to procedural code. Keep
this in mind, because the essential value of DI is not whether
the calling routine is part of a class or a simple function, rather
the value is in the separation of our code from responsibility
for the object lifetime.

Unfortunately, it is all too common in legacy code that management


of an object lifetime is split between modules, rather than kept
closely managed. How then can you be certain of freeing an object
after use? Or that the object has been instantiated before you
attempt to make use of it?
A good rule is to keep the lifetime management at a single level
and in one module. In other words:

interface

TUseList = class
private
FList : TStringList ;
function GetCommaText : string ;
procedure SetCommaText ( const Value : string );
public
constructor Create ;
destructor Destroy ; override ;
property CommaText : string
read GetCommaText
write SetCommaText ;
end ;
25.1 Starting Small 305

implementation

TUseList . Create ;
begin
FList := TStringList . Create ;
end ;

TUseList . Destroy ;
begin
FList . Free ;
end ;

Once again, this is contrived, and serves merely to illustrate that


FList is owned by the class, and is created and destroyed by
the class at the topmost level. The list can be filled through the
CommaText property. We might easily add our FindItem routine
to this class:

interface

type
TUseList = class
function GetCommaText : string ;
procedure SetCommaText ( const Value : string );
public
constructor Create ;
destructor Destroy ; override ;
function FindItem (
const AItem : string ): Integer ;
property CommaText : string
read GetCommaText
write SetCommaText ;
end ;

implementation

TUseList . Create ;
begin
FList := TStringList . Create ;
end ;

TUseList . Destroy ;
begin
FList . Free ;
end ;
306 25 Dependency Injection

function TUseList . FindItem (


const AItem : string ): Integer ;
begin
Result := -1;
if FList . Count > 0 then
begin
if not FList . Sorted then
FList . Sorted := True ;
FList . CaseSensitive := False ;
Result := FList . IndexOf ( AItem ) ;
end ;
end ;

Clearly, this will work, but it is not very remarkable. The lifetime
of the list is the responsibility of TUseList. On the other hand, the
content of TUseList is controlled by the user of the class, through
the CommaText property.
To emphasize just a bit, if TUseList exists, then CommaText exists,
and may safely be accessed from outside the class. The caller may
yet have neglected to populate the list, but that will result in a
failure of function, not in an access violation.
There is a very compact presentation on Dependency Injection on
this site: (https://marcogeuze.com/2020/11/11/dependency-
injection-in-delphi/.) The author shows a very simple example
with strong coupling, and then demonstrates how to first isolate
a dependency through use of Dependency Injection and then how
to remove the dependency on concrete implementation by using
interfaces. Clean, simple, and concise, which is why I linked it.

25.2 Types of Injection

There are three principle types of injection:


▶ Constructor Injection
▶ Property Injection
▶ Method Injection

None of these is to be viewed as the best type, and each has its
areas of application. Be skeptical of dogmatic arguments which
would exclude any of these, or which contend that DI without a
container is in some way irrelevant. Success in reworking legacy
25.2 Types of Injection 307

code depends heavily on pragmatism in approach. You can only


play the cards in your hand, and the alternative is to redesign and
rewrite—you are unlikely to find management support for that.

25.2.1 Constructor Injection

In Constructor Injection, the dependencies are passed as parameters


to the constructor. They will have been created and initialized
outside of the class. The class to which they are passed will neither
create nor destroy them, though it may be designed to modify what
they contain. The objects injected will remain available throughout
the life of the class instance.
The consuming class has full access to the public members of the
objects passed in, and will operate on them as needed, to fulfill its
functions.
As the dependency is satisfied during object construction, this
approach works well where you need a specialization which
remains unchanged during the life of the instance.

25.2.2 Property Injection

In Property Injection, the dependency is injected by assignment to


a property. Since a property is of a single type, this will be a single The property type will likely be a
object per property, but of course you are free to use multiple class or record, not a simple type.
properties. Generally, Property Injection is used where the object
may not be essential to the function of the class.
A dependency injected through a property is quite consistent with
the notions of composition and aggregation, and may be a perfect
fit in some situations. If the injection is by way of a setter method,
then a boolean field may be set at the same time, to indicate that
the member is present. This avoids the lately unfashionable test
for nil as a means of determining whether the object is present for
use.
Property injection works well where more than one specialization
will be applied during the life of the instance.
308 25 Dependency Injection

25.2.3 Method Injection

Method Injection is similar to Constructor Injection, in that the


dependencies are passed as arguments to the method. As such,
you are not limited to a single dependency being passed in.

Complex Dependencies

With injection through either the constructor or a method, you


may find your parameter list becoming large and ungainly.
Should that happen, you may wish to consign some of the
dependency items to a record which is then passed as a single
object. In recent versions of Delphi the record is a lightweight
class, so it can also contain methods. With or without methods,
the record may help to reduce clutter, and make it easier to keep
track of the required dependencies. Consider, for example:
type
TBookInfo = record
Title : string ;
Author : string ;
Year : Integer ;
Publisher : string ;
ISBN : string ;
end ;

Such a record holds data which are all clearly related to a single
book. Using such a record is more convenient than passing
each member in its own parameter.

A somewhat more complex use of Method Injection can be seen


in the use of the TComparer class. Generalizing a sort routine
necessitates that the comparer be provided to it. In this way, the
comparer can be written for the specific type to be compared, and
the sort routine needs no knowledge of that type. Delphi Help
offers an example similar to this:

uses
System . Generics . Defaults , System . Generics .
Collections , System . AnsiStrings ;

type
{ Declare a new custom comparer .}
TIntStringComparer = class ( TComparer < String >)
public
25.2 Types of Injection 309

function Compare (
const Left , Right : String ): Integer ; override ;
end ;

{ TIntStringComparer }
function TIntStringComparer . Compare (
const Left , Right : String ): Integer ;
var
LeftTerm , RightTerm : Integer ;
begin
{ Transform the strings into integers and perform
the comparison .}
try
LeftTerm := StrToInt ( Left );
RightTerm := StrToInt ( Right );
Result := LeftTerm - RightTerm ;
except
on E : Exception do
begin
writeln ( ’ Not a number ! ’);
Result := CompareStr ( Left , Right );
end ;
end ;
end ;

procedure SortMemos (
const Comparer : IComparer < String >) ;
var
List : TList < String >;
I : Integer ;

procedure FillList ( const Count : Integer );


begin
writeln ( ’ Initial list : ’);
{ Populate the list with random numbers .}
for I := 0 to Count do
begin
List . Add ( IntToStr ( Random ( Count )));
writeln ( List [I ]) ;
end ;
end ;

begin
Randomize ;
{ Create a new list of strings with the custom
comparer . }
310 25 Dependency Injection

List := TList < String >. Create ( Comparer );

FillList (5) ;

{ Sort the list . }


List . Sort ;

writeln ( ’ Sorted list : ’);


for I := 0 to List . Count - 1 do
writeln ( List [I ]) ;

{ Free resources .}
List . Free ;
end ;

var
Cmp : IStringComparer < string >;
begin
{ Use our custom comparer . }
Cmp := TIntStringComparer . Create ;
SortMemos ( Cmp );
readln ;
end .

Read with care the last few lines of code. Cmp is declared as being
type IStringComparer, and then populated. Cmp is then passed to
SortMemos, which requires a parameter of type IComparer<string>.
Since TIntStringComparer fulfills IComparer<string>, it is suitable.
Method Injection works well where the dependency fulfills a need
specific to the call, but is not related to the life of the object
instance.

25.3 Summary

In writing this chapter, the biggest challenge was in which aspects


of Dependency Injection to present, as there are arguments over
what is true DI, and wading into that swamp is not the purpose
of my writing. As the context here is the transformation of legacy
code, it makes sense to focus on the forms of Dependency Injection
which are most easily introduced to such an environment.
Dependency Injection comprises a spectrum of possibilities which
ranges from the simple property injection example above to full-
25.3 Summary 311

blown Inversion of Control. The simple approaches, sometimes


referred to as poor man’s dependency injection will be useful in
refactoring legacy code, almost form the start. The more com-
plex approaches, such as Inversion of Control, usually will require
significant redesign. Considering once again the matter of prag-
matism, and the assumption of large legacy projects as the starting
point, such rework or redesign is less useful than the simpler
approaches.
Limited testability in legacy code argues for incremental change
as the best overall policy. Such practice minimizes risk, and yet
affords room for great improvement. A general ranking by time
line might be:
▶ Factor out nested routines
▶ Identify private class candidates
▶ Factor code out of forms

In part, this rationale favors the gradual identification and isolation


of repeated code. Some may be repetitive within a module, some
in groups of modules, but the incremental grooming of existing
routines will lead to collecting these repetitive elements together.
That will make it much easier to identify what may be converted
to small classes or utility modules.
Inversion of Control has a place, to be sure, and in some future
where you have eliminated code on forms, and achieved 100%
testability, then redesigning and replacing subsystems will be
more practical. In the end, you must decide which approaches
are most applicable to your own projects, and when and how to
introduce them.
Unit Testing 26
Unit testing is a well developed field of activity, yet many legacy 26.1 DUnit . . . . . . . . . 315
projects make no use of it. The reasons vary, but likely the main 26.2 DUnit2 . . . . . . . . . 315
reasons are these:
26.3 DUnitX . . . . . . . . 316
▶ Project may have been initiated before DUnit was available. 26.4 TestInsight . . . . . . 317
▶ Early teams may have been unaware of DUnit and its alterna- 26.5 Delphi Mocks . . . . . 317
tive cousins.
▶ Later teams may have been unable to accomplish sufficient
separation.
▶ Possible confusion over adding unit testing to existing code.

All of these are understandable, but none of them is a good reason


not to put unit testing in place. Much of what I have already
presented addresses the issue of separation and how to achieve it
without starting over. It is certainly an issue, but like all aspects
of updating legacy projects, the key is to implement changes
incrementally.
Let me point out, too, that there is misunderstanding of what "unit
testing" means. In Delphi, as we have files called units, many people
likely assume that is what unit testing is about, but remember
that unit testing was first practiced in Java. So it is worth some
reflection on the meaning of "unit."
Really, the unit of interest is a functional unit, which might be a
procedure, function, or class. The essential feature is that we are
able to test these units in isolation, minimizing the complexity of
interaction with other units or modules. Such isolation is more
easily considered than achieved, and we must begin with lower
level modules which really can be isolated, and then build on
that start. Later, the subject of mocking comes into play, and this
is used to substitute proxies where module complexity makes
repeatability difficult.
Having tried to explain that a unit of code is not synonymous with
a Delphi unit file, I confess that in the following commentary, I will
not distinguish between them. If your code files are composed
well, they will have limited interdependency with others, and any
confusion between unit file and code unit will not reduce the sense
of this discussion. Even so, the recognition of the difference adds
314 26 Unit Testing

motivation for you to reorganize your modules to ensure that they


coherently apply to limited areas of operation.

Incremental in Unit Testing

Previously in this book, incremental change referred to small


changes in code. In the discussion of unit testing, however, I
must use the word in a different sense. It is not terribly practical
to have a unit partially tested. Rather, you should consider that
each module is either testable or not, and each is an increment
of unit testing. The rework needed to make a unit testable will
generally make it entirely testable, and alternate approaches
will, at best, be difficult to manage and track.

The practice of Test Driven Design (Test Driven Design) has perhaps
added some confusion. Some proponents of Test Driven Design in
their zeal may lead you to believe that if you have existing code, it
is too late. Too late to let Test Driven Design control the production
of existing code, certainly, but never too late to put unit testing
into practice.
Consider these steps:
▶ Select a unit you wish to unit test. It should be relatively
small, and without many dependencies on other modules.
▶ If your chosen module is tangled in unit dependencies, try
to minimize those through refactoring.
▶ Write tests for each of the routines in your test unit. Create
tests which should pass, but also tests which should fail. A
graceful failure is an important part of stable coding.
Plan for organization of your unit testing modules. Not the mod-
ules to be tested, but those which contain the tests. They should be
as well organized as your project code. And you will want them in
your source code repository, as well—they are assets, and will live
as long as the modules to which they apply. Moreover, they will
be subject to test and revision, as much as any other modules.
Writing your testing modules will be affected by the test framework
you select. If the framework you select supports using attributes
to define test values, that may lead you to increased coverage in
your testing, compared to the coding of individual test routines.
There are as many ways to approach the coding of tests as there
are to the coding of applications—take some time to experiment
26.1 DUnit 315

and determine which seems best in your team. There is value in


members of the team all supporting a consistent style in testing,
just as in the coding of your products.

Disclaimer
My experience has been almost exclusively with DUnit, so I
will not be offering any detailed coverage of the other unit
test tools. That said, there seems to be consensus that DUnit 2
and DUnitX bring enhancements, but that the original DUnit
remains perfectly serviceable.

26.1 DUnit

The oldest of the unit test tools for Delphi, DUnit was created on
the model of JUnit, and is found here: (http://dunit.source
forge.net/.) I’ve not found the date of its first release, but in
mid-2000 it was moved to SourceForge, and the introductory article
(https://web.archive.org/web/20111019190343/http://ww
w.suigeneris.org/writings/1999-11-29.html) by Juancarlo
Añez, who did the initial work, was published on 29 November
1999. This continues to be a viable framework for unit testing, and
if you need support for earlier releases of Delphi, then it may be
the one you must use.
A further value is that DUnit is a part of Delphi, and enjoys some
support within the newest Delphi releases. (https://docwiki.em
barcadero.com/RADStudio/Sydney/en/DUnit_Overview)
DUnit is straightforward to use, and getting started is easy. As
you proceed, you will develop your own strategies, but keep in
mind that tests should be simple, clear, and fast. You will run them
again any time you alter the code they test. More coverage will be
found in Chapter 33.

26.2 DUnit2

DUnit2 was developed after DUnit, on which it is based. It is


maintained now by Graeme Geldenhuys, and can be found on
github: (https://github.com/graemeg/dunit2) Though it is not
316 26 Unit Testing

extensive, the documentation for DUnit2 (https://graemeg.gi


thub.io/dunit2/) offers insights into the motivation behind its
development. Projects in the repository go back to Delphi 5, so you
may be sure it is tested and useful with that and later versions, at
least through Delphi 10.1 Berlin. Though I have not verified it, it
is likely that DUnit2 will be compatible also with the most recent
releases of Delphi. More coverage will be found in Chapter 34.

26.3 DUnitX

The most recent addition to Delphi unit testing frameworks, DUnitX


is from Vincent Parrett of VSoft Technologies, and is found here:
(https://github.com/VSoftTechnologies/DUnitX.) There is a
discussion group for it on Delphi-Praxis: (https://en.delphipra
xis.net/forum/36-dunitx/) Since DUnitX is in ordinary use at
VSoft Technologies, we may be confident that it is well maintained.
DUnitX uses features of the language which became available in
Delphi 2010, so that me be a limit for some legacy projects—you
will not be able to apply it to earlier releases.
DUnitX implements Attribute based testing, so you will need to be
comfortable with that feature. The list of features is impressive:
▶ Any class can contain tests
▶ Attribute based testing
▶ An extensive Assert Class
▶ Setup and TearDown per test method and per test fixture.
▶ API Documented using Xml-Doc
▶ Console Based Runner
▶ XML Logging
▶ Produces output compatible with NUnit (compatible with
CI servers like ContinuaCI)
▶ Produces output compatible with JUnit (compatible with
Gitlab CI)
▶ Cross platform currently supporting:
▶ Win32,Win64 and OSX Compilers.
▶ Limited backwards compatibility with DUnit test classes.
▶ Wizard for creating new tests.
More coverage will be found in Chapter 35.
26.4 TestInsight 317

26.4 TestInsight

If you are using Delphi XE or later, then you may wish to add
TestInsight to your IDE. It is a plug-in which provides support for
using DUnit, DUnit 2, and DUnitX. Written by Stefan Glienke, it
can be found on bitbucket (https://bitbucket.org/sglienke/t
estinsight/wiki/Home), and as explained on that page:
TestInsight integrates unit testing into the Delphi IDE
▶ No need to run your tests externally anymore - you can see
the results inside the IDE
▶ Run tests from the code editor and navigate to the failing
code from the results overview
▶ Continuously runs your tests bringing test driven develop-
ment in Delphi to the next level!
Stefan is very active on Delphi-Praxis, and the issues list shows
clearly that the code is in current maintenance. More coverage will
be found in Chapter 43.

26.5 Delphi Mocks

Another project from Vincent Parrett, Delphi Mocks can be found on


github: (https://github.com/VSoftTechnologies/Delphi-Mock
s.) It is a mocking framework compatible with Delphi XE2 or later.
There is no documentation offered on the github page, but there is
a blog( https://www.finalbuilder.com/resources/blogs), and
Vincent is active on Delphi-Praxis (https://en.delphipraxis.ne
t).
More coverage will be found in Chapter 30.
Appendix: Tools
Tools Overview 27
The tools presented here are grouped as plug-ins or standalone, 27.1 IDE Plug-ins . . . . . 321
and within the groups, alphabetically. Tools included here are 27.2 Standalone . . . . . . 322
those with which I have had experience, as I wanted to be able to
27.3 Some Disclaimers . . 322
offer some commentary.

27.1 IDE Plug-ins

CnPack is a free and open source plug-in which adds a large


number of operations to the IDE. See Chapter 28. My own use of it
is somewhat narrow, both because I came to it later than some, and
because there is a significant overlap of functionality among Delphi
plug-ins. That said, it is one that I always install. My coverage is
necessarily focused on the operations I find most useful.
DocumentationInsight is a major tool which makes XML documen-
tation easier to implement, as well as adding to the Code Insight
behaviors of the IDE. See Chapter 31. Although the intention is to
produce content which can be harvested to produce a manual, I
find it less useful in that regard than for the impact on the Code
Insight in the IDE. It has been my experience that such documen-
tation can never replace a thoughtfully written manual, though it
can be a good starting point.
FixInsight is a commercial plug-in which adds very specific func-
tionality to the IDE in support of static code analysis. See Chapter
36. Well-used, it will change how you write code, and definitely
for the better. It will also help you to identify areas in legacy code
which are badly in need of revision and repair.
GExperts is a free and open source plug-in which adds a large
number of operations to the IDE. See Chapter 37. There is overlap
with the functionality added by CnPack, and as I have used GExperts
far longer, I tend to use GExperts operations more often then those
from CnPack, where they offer comparable operations.
MMX is a formerly commercial, now free, plug-in to the IDE. See
Chapter 40. I find this the single most valuable for tool refactoring,
and it provides a very useful analysis of UDCs.
322 27 Tools Overview

27.2 Standalone

CodeSite is a commercial tool which provides very capable and com-


prehensive logging capabilities. See Chapter 29. It is a standalone
product, but adds functionality in debugging.
FixInsight is a plug-in, but also offers a standalone executable
which can be used as a part of the build process. See Chapter 36.
Pascal Analyzer is a commercial standalone application which
performs a static analysis of a Delphi project, and provides a large
amount of detailed information of value in rework. See Chapter 41.
Pascal Analyzer Lite is a free version of the tool, with more limited
features.
ProDelphi is a commercial standalone application which provides
very useful profiling data. See Chapter 42. There are other profilers
available, but ProDelphi is capable, and easy to use, as well as
inexpensive.

27.3 Some Disclaimers

First I should mention that I have no particular relationship to the


publishers of these programs apart from being a user.
As to omissions, I have certainly left out some things which a
reader might question.
▶ Castalia is no longer a plug-in, but is a part of the IDE, having
been acquired by Embarcadero at about the time of Delphi
XE7 being released. Historically, I had been an early user of
CodeRush, but found it brought my then quite powerful PC
to its knees. I shortly discovered Castalia, which provided
a replacement for structural highlighting, and some other
worthwhile features of CodeRush without heavily burdening
my CPU. At that point, I moved to Castalia, which I later did
not renew as the price rose beyond what it seemed to me to
be justified in my use. I have no ill will toward the Castalia
functionality, but CnPack had become my choice for structural
highlighting.
27.3 Some Disclaimers 323

▶ TestInsight is a plug-in which manages unit testing. Find it on


Bitbucket (https://bitbucket.org/sglienke/testinsig
ht/wiki/Home.) Looked at it some years ago, while working
in Delphi XE, and have not used it in some time. It does
require Delphi XE or later, and I have been working recently
in Delphi 2007, so it has not been an option. Now that I am
transitioning to Delphi 10.2 Tokyo, I shall need to look again.
▶ About other profilers I have omitted. I have not had occasion
to test many of them. ProDelphi has served my needs well for
years, and some of the other tools are quite expensive. All
require some investment of time to master their use. Need
has not yet driven me to any of the others.
CnPack 28
The CnPack Wizards have been around for years, and provide 28.1 Structural Highlight-
extended functionality to the Delphi IDE. In the context of legacy ing . . . . . . . . . . . 326
code issues, two of the most useful functions would be the Uses 28.2 Tab Order . . . . . . . 327
Cleaner, and the Tab Order display. As stated in the CnPack help: 28.3 Uses Cleaner . . . . . 328
▶ Non-visual tools set; 28.4 CnPack Summary . . 330
▶ User interface control;
▶ Database and Report tool;
▶ Network communication component;
▶ Localized extensive property editor and component editor;
▶ IDE Wizards;
▶ CnVCS version control system;
▶ Lots of reusable form template library.
There is a tremendous amount in this package, and I will not
attempt to do more than offer a minor indication of why you
should use it. In general, the quality of the writing in the help
file is quite good. There are some oddities of usage, as might be
expected, but given that the authors are all native speakers of
Chinese—as is my wife—it is very well done, and much less terse
than the help which I now find in so many packages from native
English speakers.
Two features from which I benefit every day are the structural
highlighting and the Uses Cleaner.
Structural highlighting is of less value in clean new code than in
legacy work, where routines may be very long and nested levels
of logic may be many. In that environment, it is a survival tool.
The Uses Cleaner advises which unit references are unneeded.
It does not notify when a reference can be demoted to the
implementation section, and it does give some false positives. More
detail below.
326 28 CnPack

Figure 28.1: CnPack Main menu

28.1 Structural Highlighting

Structural Highlighting of code is very useful, especially in long,


tangled, legacy routines. The CnPack highlighting seems a bit more
obvious than that of Castalia, but it is largely a matter of personal
preference. The more closely you approach clean and concise code
design, the less value the structural highlighting delivers, but in
legacy code, it is worth a great deal.
28.2 Tab Order 327

Figure 28.2: CnPack Structural High-


lighting

28.2 Tab Order

CnPack offers a visual presentation of the tab order of controls on


your form. This is very helpful in determining quickly whether
there are issues, and in ensuring you apply the corrections prop-
erly.

Figure 28.3: CnPack Tab Order

In the image above, the orange colored boxes are on the form itself,
and the yellow are on the panel. Different colors are assigned to
controls in each container on the form, for clarity.
There are many details handled in this wizard, including:
328 28 CnPack

▶ Components Selected: Sorts the tab order of currently selected


components. If a selected component is a container, all of its
subcomponents will be processed. Otherwise those at the
same level will be processed.
▶ All Components of Current Form: Sorts the tab order of all
components and their subcomponents in the current form
designer.
▶ All Opened Forms: Sorts the tab order of all components in
all opened forms.
▶ All Forms in Current Project: Sorts the tab order of all com-
ponents in all forms of current project.
▶ All Forms in Current ProjectGroup: Sorts the tab order of all
components in all forms of the current project group.
▶ Auto Update Tab Orders: If this is selected, the tab order will
be udpated automatically when components are moved.
▶ Display Tab Orders: Show/Hide Tab Order label on compo-
nents.
Note that I cannot say from experience whether applying the
changes to an entire project or project group will be successful,
nor how long it may take. Unlike the Uses Cleaner, the Tab Order
wizard does not offer to save files during the process.

28.3 Uses Cleaner

The CnPack Uses Cleaner is very helpful in removing from uses


clauses units which are not needed. It makes use of the map file in
its analysis, so it must do a compile as a first step toward listing
the units which can be removed.
There are units it will offer to remove which actually cannot be
removed. In my experience, these false positives crop up most
often in connection with units from DevExpress, and from Digital
Metaphors’ ReportBuilder (http://www.digital-metaphors.co
m/.) Some of your own units may also fall into this category, and I
think this is caught up with the complexities of form inheritance.
Once you have seen these issues a few times, you simply become
accustomed to ignoring some units. Should you forget, saving the
unit routinely reinserts them, so there is minimal risk, just some
wasted motion.
28.3 Uses Cleaner 329

Figure 28.4: CnPack Uses Cleaner

When you invoke the Uses Cleaner on the current unit, if it is not
in the project, the Current Unit radio button will be disabled. When
that happens, close the wizard, add the unit to the project, and
again select the Uses Cleaner.
It is good to do a compile before running the Uses Cleaner, because
if you have errors in code, the Uses Cleaner cannot continue.
Running on multiple units is more challenging, and on the whole
project still more challenging. In legacy projects, you will be well
advised to run on one unit at a time, or all open units.
As with other wizards in the package, the Uses Cleaner offers
many options, including:
▶ Current Unit: Search current unit.
▶ Opened Units in Current ProjectGroup: Search opened units
in the current Project Group.
▶ All Units in Current Project: Search all units in the current
project.
330 28 CnPack

▶ All Units in Current ProjectGroup: Search all units in the


current Project Group.
▶ Include Indirectly Used Units: If checked, the Uses Cleaner
will process the indirectly used units with source, not only
in the Project or Project Group.
▶ Skip Used Units including Initialization Part: Whether to skip
those units which contain an initialization clause during the
search. The initialization clause may contain some operations
and which depend on references otherwise unneeded in the
module.
▶ Skip Used Units including Register Procedure.: Whether skip
those units which contain Register procedure in searching.
This procedure may contain component register content.
▶ Skip Used Units Referred by Component Indirectly.: Whether
to skip those units which are referred by component indirectly,
such as ancestor definitions or components in other forms.
▶ Skip Used Units without Source: Whether to skip those units
without source code in searching.
▶ Auto Save/Close Unopened Files: By default, the wizard
will open files in the IDE and do the file changes without
saving and close. In huge projects, it may exhaust the memory
useable by the IDE. If this option is checked, the wizard will
auto save and close file after processing and will not then
support undo.
▶ Clean Units Directly: Clean the units below directly.
▶ Skip Units Directly: Skip the units below directly. They are
system library units.
▶ Process: Search unused reference units according to the
settings.
As indicated, there is an option in support of saving and closing
the files after changes are made. Without this, the more sweeping
versions of the Uses Cleaner will fail on large projects.

28.4 CnPack Summary

There are overlaps between CnPack and some other Delphi plug-ins,
but there are more than sufficient capabilities in it for you to make
CnPack a standard part of your toolset.
CodeSite 29
CodeSite (https://raize.com/codesite/) is a very capable 29.1 Details in Your Hands 331
logging tool. You don’t always need logging, but when you do, it 29.2 Data from the Field . 333
saves much time and confusion. As it has been around for quite a
29.3 Simple Example . . . 333
long time, it supports older versions of Delphi—I routinely use it
in Delphi 2007, where it has saved me many headaches.

29.1 Details in Your Hands

When you are deep within a complex application, the debugger


is often a blunt instrument. Often, you need to get some answers
from multiple modules, and using the debugger can make it a
challenge to keep track of where you are in a chain of actions.
Logging can be the key to finding the real area of the defect quickly,
and from there you will be able to use profitably the low-level
view of the debugger.

Figure 29.1: CodeSite Properties

There is an embarrassment of riches in the CodeSite features, and


usually, the simplest of logging work is sufficient to get the answers
332 29 CodeSite

Figure 29.2: CodeSite DataSets

needed. One of these days, it will be necessary to really put it


to the test, with its specialized visualizers, but so far, the simple
approach has been sufficient, and the press of other issues has
kept me from deeper study of the tool.
CodeSite Express is included in the code of RAD Studio, so it is
difficult to find a reason not to at least try it out.

Figure 29.3: CodeSite XML Data


29.2 Data from the Field 333

29.2 Data from the Field

CodeSite has tools you can deploy with your product, making it
a powerful tool in discovering the root cause of problems which
otherwise can be very difficult to isolate.

29.3 Simple Example

The images above are pretty, but it’s always best to begin with a
simple example. Some demo code I wrote for the book is a small
app that offers a Zip code lookup. For the sake of this discussion,
we will focus on a small routine which determines the maximum
number of characters in a city name:

function TdMain . GetMaxCityLength (


ZipData : TFDMemTable ;
const CityField : string ): Integer ;
var
s : string ;
begin
Result := 0;
if not ZipData . Active then
ZipData . Open ;
ZipData . First ;
while not ZipData . Eof do
begin
s := ZipData . FieldByName ( CityField ). AsString ;
if s . Length > Result then
CodeSite . Send ( ’ MaxLength is now ’ +
Result . ToString + ’ new length is ’ +
s . Length . ToString + ’ from ’ + s);
Result := Max ( Result , s. Length );
ZipData . Next ;
end ;
end ;

The routine loops through all the records in the dataset, and
for each string in the CityField column, determines whether the
string length is greater than the previously captured maximum. All
simple enough, but a slightly interesting application for logging.
The output in the CodeSite dialog looks like this:
334 29 CodeSite

Figure 29.4: CodeSite Messages

As there were over 50,000 records in the table, it’s easy to see
that this is much simpler than using the debugger. Trivial as this
sample may be, it should give you some ideas about ways in which
you can make use of CodeSite.
I have made good use of this sort of logging, especially in working
through problems with dynamic layout issues in complex PDF
reports. It is much simpler to obtain such a set of messages, and
then to sit down with the printout and a ruler to untangle the
issues than to battle through it with the debugger. All too often,
you need to obtain a collection of values in order to even begin
an analysis. Logging is not the only possible approach, but it has
been for me a very effective one.
Delphi Mocks 30
Delphi Mocks is an open source tool package from Vincent Parrett 30.1 Delphi Mocks in Use 336
of VSoft Technologies which can be found here: https://github 30.2 Why Mock? . . . . . . 338
.com/VSoftTechnologies/Delphi-Mocks 30.3 When to Mock . . . . 339
As the purpose of mocking is to facilitate unit testing, you may 30.4 Summary . . . . . . . 340
want to revisit this chapter when you have progressed in your
project rework sufficiently to begin assembling a suite of unit tests.
In addition, you are likely to find it very convenient to use DUnitX
as your unit test framework, as Delphi Mocks and DUnitX have the
same author.
Mocks are used as stand-ins for the actual object needed in normal
operation. They are designed to provide simplified stable unit
testing behaviors.
An example could be in database operations. Challenges in testing
against a database include:
▶ Connection to a server
▶ Database content may have changed
▶ Performance of the database will slow testing

It is fundamental to unit testing that tests be repeatable; a dynami-


cally changing dataset interferes with that goal. Similarly, the data
base performance may vary with loading, and while that does
not interfere with the test logic, it can adversely affect the time
required for testing.
In the case of database operations, one alternative is to construct a
dataset which is saved to disk independent of an RDBMS. It might
be saved to CSV, JSON, or XML, but regardless of format, when
loaded to a memory dataset, it will give repeatable results, as well
as high performance. Repeatability is the essential feature, though
of your testing requires a large set of data, then the performance
may also be important.
336 30 Delphi Mocks

30.1 Delphi Mocks in Use

At last check, Delphi Mocks requires the use of Delphi XE2 or later.
This requirement is in effect because of language features used
in Delphi Mocks, and though there is a suggestion that perhaps
earlier versions might be supported eventually, there has been no
progress in that direction.
Nick Hodges has written about Delphi Mocks in his book Coding in
[10]: Hodges (2014), Coding in Delphi Delphi.[10]
The home page of Delphi Mocks offers a compact example:

unit Delphi . Mocks . Examples . Interfaces ;

interface

uses
SysUtils , DUnitX . TestFramework , Delphi . Mocks ;

type
{ $M +}

TSimpleInterface = Interface
[ ’ {4131 D033 -2 D80 -42 B8 - AAA1 -3 C2DF0AC3BBD } ’]
procedure SimpleMethod ;
end ;

TSystemUnderTestInf = Interface
[ ’{5 E21CA8E - A4BB -4512 - BCD4 -22 D7F10C5A0B } ’]
procedure CallsSimpleInterfaceMethod ;
end ;
{$M -}

TSystemUnderTest = class (
TInterfacedObject , TSystemUnderTestInf )
private
FInternalInf : TSimpleInterface ;
public
constructor Create (
const ARequiredInf : TSimpleInterface );
procedure CallsSimpleInterfaceMethod ;
end ;

TMockObjectTests = class
published
30.1 Delphi Mocks in Use 337

procedure Simple_Interface_Mock ;
end ;

implementation

uses
Rtti ;

{ TMockObjectTests }

procedure TMockObjectTests . Simple_Interface_Mock ;


var
mock : TMock < TSimpleInterface >;
sutObject : TSystemUnderTestInf ;
begin
// SETUP : Create a mock of the interface that is
// required by our system under test object
mock := TMock < TSimpleInterface >. Create ;

// SETUP : Add a check that SimpleMethod is


// called atleast once .
mock . Setup . Expect . AtLeastOnce . When . SimpleMethod ;

// SETUP : Create the system under test object


// passing an instance of the mock
// interface it requires .
sutObject := TSystemUnderTest . Create (
mock . Instance );

// TEST : Call CallsSimpleInterfaceMethod on the


// system under test .
sutObject . CallsSimpleInterfaceMethod ;

// VERIFY : That our passed in interface was called


// at least once when
// CallsSimpleInterfaceMethod was called .
mock . Verify (
’ CallsSimpleInterfaceMethod should call
SimpleMethod ’);
end ;

{ TSystemUnderTest }

procedure TSystemUnderTest
. CallsSimpleInterfaceMethod ;
begin
338 30 Delphi Mocks

FInternalInf . SimpleMethod ;
end ;

constructor TSystemUnderTest . Create


( const ARequiredInf : TSimpleInterface );
begin
FInternalInf := ARequiredInf ;
end ;

end .

Clearly, this is not production code, but a simple example intended


only to give a sense of how the tool is used.

30.2 Why Mock?

If the words above didn’t persuade you of the value of mocks, let
me offer more specifically some key points:
▶ Repeatability is key in unit testing.
▶ Isolation from variability is essential to repeatability.
▶ Unit tests should run with minimum overhead.
▶ Unit tests should verify edge cases.
Repeatability is essential as it allows you to verify that your rework
in a module has not produced unexpected errors. You need a suite
of invariant test cases which provide this confidence. Although
some rework will necessitate adding test cases to verify newly
added routines or unforeseen defects not previously caught, this
is a matter of increasing the roster of tests, not starting over.
Isolation from outside factors is the only way you can achieve
repeatable test results. Your unit tests should provide all test
stimuli, and must correctly verify all results produced. A unit
test has no value unless it shows that for input X, the routine
tested always produces result Y. In a perfect world, the code under
test and the unit testing module would be the only participants
in the operation. As soon as another module is introduced, the
complexity of determining the source of a defect is multiplied.
However, in the real world, such perfect isolation may be difficult
to achieve. As in the above references to database operations, you
can’t test them without data. But you can provide fixed data which
is created to exercise thoroughly the logic of code under test.
30.3 When to Mock 339

Overhead matters because the likelihood of a developer to use


the unit test suites is inversely proportional to the time they take
to run. Again, in a perfect world, the developer would code a
change to a routine, then run the tests to ensure that no negative
effects had been generated. If the work at hand is simply to repair
a single isolated defect, this is less important than otherwise. In
reworking legacy projects, however, such changes and repeated
testing become a way of life. In such situations, low cycle times
are critically important.
Edge cases are always a challenge. In ordinary use of the applica-
tion, they are very difficult to discover, much less to repair. In test
driven refactoring they can be targeted. We understand how our
code is designed, so we can recognize which inputs may provoke
incorrect behavior. These will not always be obvious, but as we
increase our testing proficiency, they will be easier to locate. And
similarly, this work will affect how we code, making us more
thoughtful defensive coders. Happy path testing is of little value, be-
yond establishing that we can code a desired behavior—thorough
testing of a wide range of cases, the expected, unexpected, edge or
corner cases, all are the source of greatest value in unit testing.

30.3 When to Mock

You must determine when to apply mocks in your test strategies,


but the key indicators will be:
▶ Need isolation from other modules
▶ Code interacts with database operations
▶ Code interfaces outside services

Isolation was discussed above, but to stress it again, the goal


of repeatability in unit testing requires us to control the test
environment rigidly.
If you create test data, whatever its
Database operations are always a challenge to testing. Other team form, you will want to safeguard
such data in source control. That’s
members may be sharing our database, and in their own work, simply another aspect of repeatabil-
may have altered values which were essential to our testing. Not ity.
that it may happen—it will. Our best strategy will obviously be to
create our own surrogate for the database, and in that surrogate,
we can create all the test oddities we most need to exercise.
340 30 Delphi Mocks

Bear in mind that repeatability is Outside services such as email or cooperative applications from
not the sole reason to mock database which we obtain input are often more challenging of tests than
or outside providers. Network fail-
ure can leave us helpless to proceed,
a database. Clearly, if our purpose is to verify that our protocol
while using mocks isolates us from module correctly handles an outside provider’s communications,
random acts of the world around us. then we have no choice but to use the real service. But these days,
it has become commonplace for a provider to make available
a well documented set of APIs which serve to isolate us from
implementation issues. A DLL may not be your idea of paradise,
but if you are able to use a third-party provider’s DLL in which
the provider certifies the interface, you are back to the world of
local code operations.

30.4 Summary

Mocking has become a fundamental strategy for avoiding some


of the more difficult areas in testing. It cannot solve all problems,
but will keep us focused on our own areas of responsibility, and
will tend to reduce the detailed ”live” testing of fully connected
systems to verification of more routine behaviors.
Documentation Insight 31
DocumentationInsight (https://www.devjetsoftware.com/) is an
excellent plug-in for Delphi which facilitates the creation of XML
documentation and enhances the help you see in the IDE when
you hover over a call. The Documentation Explorer lets you write
plain text, as it inserts properly formatted XML help.

Figure 31.1: DocumentationInsight Ex-


plorer: IDE

Figure 31.2: DocumentationInsight: In-


spector
342 31 Documentation Insight

Figure 31.3: DocumentationInsight:


Sample XML
Delphi Unit Dependency
Scanner 32
Delphi Unit Dependency Scanner is an open source tool which
analyzes a Delphi project to discover unit dependency relations.
It is similar in its purpose to the MMX Unit Dependency Analyzer,
but provides an interactive tree display of the project, which offers
a different view than MMX. It has the advantages of being outside
of Delphi, and of being faster than the MMX analysis.

Figure 32.1: Delphi Unit Dependency


Scanner

Conceptually, UDCs are easy to understand. The challenge is in


rooting them out of your project. In a large, legacy project, you
will likely find that there are many cycles, and that many of them
are long chains. It would be nice to be able to find those units
which, in redesign, would most quickly reduce the cycle counts.
Instead, we must take small steps, and rely on our intuition and
our knowledge of our products, to discover which units are the
biggest offenders. Sometimes, you will be lucky, and with small
changes to a unit eliminate a few hundred cycles. More often
you will plod along, patiently removing units from uses clauses
and demoting from interface scope to implementation, and at the
end of that activity, run a fresh analysis and find that you have
removed a few thousand.
These problems did not develop overnight, and you must commit
to a continuing process to achieve the desired result.
Delphi Unit Dependency Scanner presents a different view of the
344 32 Delphi Unit Dependency Scanner

UDCs problem than other tools, and being able to see the tree
layout may bring insights. Originally written by Paul Thornton to
solve problems in his own work, the original project has not recently
been updated. However, the project has been forked, and that
version has enjoyed more recent updates. The original on github:
https://github.com/norgepaul/DUDS and a fork on github:
https://github.com/dontenwill/DUDS/tree/master/Docs on
github.

Delphi Unit Dependency Scanner Concerns

Although there are things about Delphi Unit Dependency Scanner


which I like, there are issues of concern.
▶ The original code has had no updates in 5+ years.
▶ The forked code has had no updates in 2+ years.
▶ Neither branch builds in Delphi 10.2 Tokyo, Delphi 10.4
Sydney, or Delphi 11.0 Olympus without errors.
▶ Delphi Unit Dependency Scanner analysis does not agree
with that performed by MMX.
The original branch uses an old branch of VirtualTrees. The
main impact of this is in the loss of the TVTHeader.Sort method,
but as it happens, that call can be commented out, and the
interactive sorting appears to be unaffected. There are also
some places where default parameters will not compile unless
they are qualified by their types.
The forked version is a larger challenge to make build. For
one thing, it replaces the parser modules with those from
DelphiAST, and refactors the code rather extensively from the
original. As such, it really needs to be approached as a different
project, rather than a variation. The UI remains the same, but
the code is much altered.
Finally, the results reported by Delphi Unit Dependency Scanner
appear to be incomplete, compared to those form MMX. They
do not appear to be incorrect, where problems are identified,
but the majority of the issues reported by MMX are absent.

Despite the concerns I have raised, a benefit from Delphi Unit


Dependency Scanner is in being open source, so someone could take
up the challenge and update, correct, and extend the tool. The fork
moving to DelphiAST was a good idea; lack of updates, however,
345

moots the value of that.


I consider the issue of Unit Dependency Cycles to be of major concern
in legacy projects. Reducing these cycles, even with Delphi Unit
Dependency Scanner or MMX, is a challenge; redesign is required,
but correct understanding of the real problem must come first.
Although redesign could be approached on general principles, the
ever present issue of cost vs. benefit is a concern. And that is more
easily resolved when we are able to constrain the scope of rework
we propose. It is a difficult issue, and the available tools provide
no silver bullet.
DUNIT 33
DUnit (https://sourceforge.net/projects/dunit/) is an 33.1 DUnit in Recent IDEs 348
Xtreme testing framework for Delphi programs. It was originally 33.2 Developing Test Cases352
inspired by the JUnit framework written in Java by Kent Beck and
33.3 Start with the Familiar355
Erich Gamma, but has evolved into a tool with very specific value
33.4 Coverage in Legacy
to Delphi developers. The value of unit tests, of course, depends
Projects . . . . . . . . 356
33.5 Test Driven Develop-
ment . . . . . . . . . . 357
33.6 Unit Test Lifetime . . 358
33.7 Summary . . . . . . . 359

Figure 33.1: DUnit GUI Runner

on how completely they cover the potential for problems. That is


a problem you will have to solve, but like other code, unit tests are
subject to revision. Quality and depth of test coverage will repay
you in quality of product results.

The Meaning of Unit

It is not unusual for a Delphi developer new to the subject to


348 33 DUNIT

think that unit testing refers to Delphi units, but as the practice
was originated by Java developers, this is clearly not the case.
Think instead of functional units, collections of code which
share a category of application. These may well be congruent
with Delphi units, but that is by no means a rule. Fitting unit
tests to legacy code will often be challenging, and is likely also
to cause you to reconsider the organization of code collections,
as well as the design of classes. The principles presented in the
discussion of SOLID (see Chapter 22), for example, mesh well
with the needs of good unit testing.

33.1 DUnit in Recent IDEs

It has never been easier to create a test project in Delphi than in


more recent versions of the IDE. All you need to do now is to click
File, New, Other. Near the bottom of the tree is the folder for Unit
Tests. There are two pages on the Test Project wizard.

Figure 33.2: DUnit Delphi Wizard

Start by selecting Test Project in the wizard and then save the
project. Give some thought to how to arrange your unit test
materials, else you will add substantial clutter as you develop
more and more unit tests. As with any other project, you can
put test projects into project groups. Keep in mind that you will
33.1 DUnit in Recent IDEs 349

Figure 33.3: DUnit Delphi Test Project


Wizard

Figure 33.4: DUnit Delphi Test Project


Wizard

probably want the organization of test modules to be similar to


the arrangement of your project modules.
If you now View Source on the test project, you will see something
like this:

program AnalyzeCompsTests ;
{
Delphi DUnit Test Project
-------------------------
This project contains the DUnit test framework
and
the GUI / Console test runners . Add
" CONSOLE_TESTRUNNER "
350 33 DUNIT

to the conditional defines entry in the project


options to use the console test runner . Otherwise
the GUI test runner will be used by default .
}

{ $IFDEF CONSOLE_TESTRUNNER }
{ $APPTYPE CONSOLE }
{ $ENDIF }

uses
DUnitTestRunner ;

{ $R *. RES }

begin
DUnitTestRunner . RunRegisteredTests ;
end .

Next you will want to open the wizard again and select Test Case.
Again, you will open a wizard with two pages.

Figure 33.5: DUnit Delphi Test Project


Wizard

Note that it does not matter whether you are building a test project
for a class, a form, or a procedural unit. What the wizard presents
may confuse you into thinking something is wrong, but just carry
on. Wizards help, but are not omniscient.
In the test project, a new unit has been added, and if you look at
the source, it will resemble this:
33.1 DUnit in Recent IDEs 351

Figure 33.6: DUnit Delphi Test Project


Wizard

unit TestfMain ;
{
Delphi DUnit Test Case
----------------------
This unit contains a skeleton test case class
generated by the Test Case Wizard . Modify the
generated code to correctly setup and call the
methods from the unit being tested .
}

interface

uses
TestFramework , System . SysUtils , Vcl . Graphics ,
Winapi . Windows , System . Variants , Vcl . Dialogs ,
Vcl . Controls , Vcl . Forms , Winapi . Messages ,
System . Classes , fMain ;

type
// Test methods for class TForm1

TestTForm1 = class ( TTestCase )


strict private
FForm1 : TForm1 ;
public
procedure SetUp ; override ;
procedure TearDown ; override ;
end ;
352 33 DUNIT

implementation

procedure TestTForm1 . SetUp ;


begin
FForm1 := TForm1 . Create ;
end ;

procedure TestTForm1 . TearDown ;


begin
FForm1 . Free ;
FForm1 := nil ;
end ;

initialization
// Register any test cases with the test runner
RegisterTest ( TestTForm1 . Suite );
end .

33.2 Developing Test Cases

In writing unit tests your goal will be to ensure full coverage of


your code. Each public routine will need to be tested. Although
your focus will likely be on what you want the routine to do, it
is good practice to include test which should fail. The DUnit test
framework includes quite a few routines which will facilitate the
writing of your tests. The following list is not comprehensive:
▶ Check - Checks to see if a condition was met.
▶ CheckEquals - Checks to see that two items are equal.
▶ CheckNotEquals - Checks to see if items are not equal.
▶ CheckNotNull - Checks to see that an item is not null.
▶ CheckNull - Checks to see that an item is null.
▶ CheckSame - Checks to see that two items have the same
value.
▶ EqualsErrorMessage - Checks to see that an error message
emitted by the application matches a specified error message.
▶ Fail - Checks that a routine fails.
▶ FailEquals - Checks to see that a failure equals a specified
failure condition.
▶ FailNotEquals - Checks to see that a failure condition does
not equal a specified failure condition.
33.2 Developing Test Cases 353

▶ FailNotSame - Checks to see that two failure conditions are


not the same.
▶ NotEqualsErrorMessage - Checks to see that two error mes-
sages are not the same.
▶ NotSameErrorMessage - Checks that one error message does
not match a specified error message.
Ultimately, you will need to explore the source code to fully
appreciate what is provided in DUnit.

DUnit Modified
As useful as DUnit is, writing tests can be tedious, and if
you are using Delphi 2010 or later, you may wish to look at
modifications by Stefan Glienke (https://stackoverflow.co
m/questions/8999945/can-i-write-parameterized-tests-
in-dunit/9006662#9006662.) As shown in the linked article,
Stefan has added support for attributes in which you can
specify test cases:
unit MyClassTests ;

interface

uses
MyClass ,
TestFramework ,
DSharp . Testing . DUnit ;

type
TMyClassTest = class ( TTestCase )
private
FSut : TMyClass ;
protected
procedure SetUp ; override ;
procedure TearDown ; override ;
published
[ TestCase ( ’ 2;4 ’)]
[ TestCase ( ’ 3;9 ’)]
procedure TestDoStuff (
Input , Output : Integer );
end ;

implementation
354 33 DUNIT

procedure TMyClassTest . SetUp ;


begin
inherited ;
FSut := TMyClass . Create ;
end ;

procedure TMyClassTest . TearDown ;


begin
inherited ;
FSut . Free ;
end ;

procedure TMyClassTest . TestDoStuff (


Input , Output : Integer ) ;
begin
CheckEquals ( Output , FSut . DoStuff ( Input ) );
end ;

initialization
RegisterTest ( TMyClassTest . Suite );

end .

As can be seen in the screenshot below, the effect is that each


attribute invokes a call with the specified values.

In situations where a given test routine may need a large number


of test cases, this can be a real benefit in coding tests.
33.3 Start with the Familiar 355

Figure 33.7: DUnit Delphi Test Project


Wizard

33.3 Start with the Familiar

Build your first test project to exercise a module which you know
well and believe to be solid. Create test cases which explore
thoroughly the range of operations each method should handle.
You may be surprised—as I was, years ago—to discover that your
solid module has some issues. Some may be obscure corner cases;
others may be embarrassingly major. If your experience with the
first few test projects uncovers defects, you will soon find yourself
committed to the value of unit testing.
Your first unit test code will not be completed in one sitting. No
matter how well you know your code, you are almost certain to
discover the need for tests you had not anticipated. Be patient
with this first round, as you will gain insights and skills in unit
testing, just as in any other facet of programming. You will also
find code which you can’t fully test because a routine is doing too
much—unit testing is a stimulus to better coding.
One reason for starting with familiar code is that devising tests
is initially awkward. It is not usual to try to break your code, but
truly, you need to do just that. It’s great to verify that the code
356 33 DUNIT

does as it should on all the primary cases, but even better when
you are confident that it behaves properly with improper input
values.

Useful Tricks in Legacy Code

When you are refactoring code, it can be useful to copy the


old routines to a reference module you can use in testing the
refactored versions. If the existing routines gave correct results,
but perhaps with much home-grown code which should be
replaced with library calls, then in your tests, you can call
the old routine to get the good result, and the new routine
to get the uncertain result. Then compare the two. This won’t
always be the right path, but when it is, it saves a good deal
compared with building and vetting routines to produce the
desired check result.

33.4 Coverage in Legacy Projects

You will find that achieving broad coverage of any sizable legacy
project with unit tests is very difficult. The existing code was not
designed with unit testing in mind, and you are certain to find
obstacles in your path:
▶ Routines are too large.
▶ Dependencies are difficult to satisfy.
▶ Forms contain too much business logic.

There will be others, but these are sufficient to begin.


You will be most successful if you begin in the lowest level modules,
such as utility routines. Consider how you would unit test some
very simple routines, such as TimeToSeconds() and SecondsToTime().
Sounds too easy, but for this discussion, the Seconds will cover
the full range of 24 hours, which is the range of the TDateTime
fractional part. There are many ways you might approach this
problem, but the way your application uses the results form these
functions may suggest a particular focus.
Given that the total range is a relatively small number: 24 * 60 * 60
= 86,400, it is entirely practical to test all possible seconds:
33.5 Test Driven Development 357

var
seconds : Integer ;
timefrac : Double ;
begin
for idx := 0 to 84600 - 1 do
begin
timefrac := SecondsToTime ( idx );
seconds := TimeToSeconds ( timefrac );
CheckEquals ( idx , seconds );
end ;

As tests such as this will run very quickly, there is little benefit to
reducing the coverage detail. Testing small utilities is an excellent
starting point because:
▶ The routines are easily understood.
▶ The tests are easily implemented.
▶ Low level routines are widely used, and impact much of your
code.
You must begin implementing unit tests somewhere, and begin-
ning with the low level routines, which we may also think of as
innermost, is not only easy, but helps to establish a solid foundation
for your more complex routines which rely on them.

33.5 Test Driven Development

In recent years it has been very fashionable to practice Test Driven


Development (TDD). From Wikipedia (https://en.wikipedia.o
rg/wiki/Test-driven_development):
Test-driven development (TDD) is a software develop-
ment process that relies on the repetition of a very short
development cycle: requirements are turned into very
specific test cases, then the code is improved so that
the tests pass. This is opposed to software development
that allows code to be added that is not proven to meet
requirements.
As the name suggests, the process demands that you first write
your test cases, then write code to fail and pass the tests. As with so
many methodologies, there have been flame wars over the rigidity
of the process.
358 33 DUNIT

We’re discussing legacy projects, and in that context, it is impossible


to write the test first, as the code already exists. We must be
pragmatic.
Testability is a goal, and you will often find that legacy code must
be refactored to be made testable. Start with the ubiquitous code
on forms problem. Unit testing is not well suited to testing forms,
but is excellent for testing methods. There are basic requirements
for testability:
▶ Modules must have minimal dependencies on other modules.
Procedural code is a good place to start. Look at Delphi’s
DateUtils library. The routines take parameters in and deliver
particular results.
▶ Tests must be repeatable. For any stimulus, you must be able
to predict the correct result. When you discover defects, the
test runner will be the environment in which you debug, far
from the operational complexities of your application.
▶ Dependencies will need to be satisfied by static substitutes.
In other words, it will be better to pass the value than to pass
in a dataset field. If you really need to pass the field, then
your test project needs to provide suitable, repeatable dataset
content.
As already mentioned, forms are not good targets for unit testing.
However, if you have removed all possible code from the forms,
then form testing becomes relatively simple. It will need to verify
correct user interaction, but will not be needed for verification of
data operations, which are unit tested.

33.6 Unit Test Lifetime

You will need your unit tests as long as you need your application
code, so they need to be well organized and protected in source
control. Any time you modify a unit for which you have unit tests,
you need to follow a small process:
▶ Run the unit tests on the modified code. Repair code as
needed.
▶ Examine closely your changes to determine whether new or
modified tests are needed. If so, add them, retest, and repeat
till you are confident your unit tests provide full coverage,
and all tests pass or fail as expected.
33.7 Summary 359

▶ Push your updated unit tests to source control, along with


your modified modules.

33.7 Summary

The more you refactor for testability, the more natural it becomes. Adopting a new methodology is al-
The more unit tests you write, the more natural that becomes. The ways a process. Until it becomes sec-
ond nature, it will feel awkward, and
more defects you find and fix through unit testing, the more you may slow down your development.
will come to rely on it every day. Initially, it may seem like more But in the end, you will gain.
work, but over time, it will actually reduce the work you must do,
especially in defect repair of your application.
Unit testing is proactive; fixing defects from user reports is reactive. Yes, I am cheerleading. I’ve gained
Although customers may take for granted the improved reliability enough benefit from unit testing that
I have no remaining doubts about its
which unit testing brings, they are likely to be very vocal and value.
unhappy over defects which get in their way.
DUnit is easy to use, and very capable. It is mature, but has not
been updated since 2007, so it may lack features which could
provide support to new features of Delphi. If you have not done
much unit testing in the past, it is a great place to begin. Also keep
in mind that is supports Delphi back to before Delphi 7, which
makes it an easy choice for old code.
DUnit2 34
Limited Experience

My experience has been almost exclusively with DUnit, so I


will not be offering any detailed coverage of the other unit
test tools. That said, there seems to be consensus that DUnit 2
and DUnitX bring enhancements, but that the original DUnit
remains perfectly serviceable. The information presented here
is taken from online information, as a convenience.

As DUnit 2 (https://github.com/graemeg/dunit2) is extended


from the foundation of DUnit, the focus here must be on how it
differs from its ancestor. The GUI test runner is an obvious area:

A difference here is in excluding tests, as described in the docu-


mentation:
▶ Individual tests can now be excluded separately from En-
abling and Disabling tests.
▶ Excluded test state is persisted in DUnit.ini and is not cleared
by globally enabling tests.
▶ Excluded tests are shown with a red X in the GUI test treeview.
▶ Excluded tests are stored in Sections in DUnit.ini and include
the project name.
▶ In console mode (TextTestRunner) excluded tests are shown
with an x in place of dots.
If your suite of tests is extensive, you may wish to exclude some
which already pass, to better focus on the areas in which you are
working. Of course, at some point, you will want to re-enable all,
and run the full suite before moving to another module.
As pointed out in the documentation:
▶ The addition of SetUpOnce and TearDownOnce adds signifi-
cant versatility.
▶ Testing integrity has been strengthened with potential soft
failures detected automatically.
▶ Named projects provide a pathway for future improvements
to run multi-threaded unit testing.
362 34 DUnit2

Figure 34.1: DUnit2 GUI Runner

▶ The test framework does not leak memory or resources.


▶ Decorated tests can be nested.
▶ Repeated tests execute significantly faster.
▶ TTestCase Constructors and Destructors only run once.
▶ New Int64 Check() procedures and EarlyExitCheck capability
have been added.
▶ An XML report generator has been added (.NET excluded).
▶ Individual tests can be excluded from execution.
▶ The code is still undergoing refinement in particular to im-
prove readability and maintainability.
363

Figure 34.2: DUnit2 Excluding Tests

My familiarity with DUnit2 is limited, but there is much here


beyond what was in DUnit, and as it still supports older versions
of Delphi, you may find this version is a good fit for your project.
DUnitX 35
Limited Experience
35.1 Looking at Some
My experience has been almost exclusively with DUnit, so I Code . . . . . . . . . 367
will not be offering any detailed coverage of the other unit 35.1.1 DUnit vs. DUnitX
test tools. That said, there seems to be consensus that DUnit 2 Features . . . . . . . 375
and DUnitX bring enhancements, but that the original DUnit 35.2 DUnit vs. DUnitX . 375
remains perfectly serviceable. The information presented here 35.3 Summary . . . . . . 377
is taken from online information, as a convenience.

DUnitX https://github.com/VSoftTechnologies/DUnitX is a
new test framework, taking ideas from DUnit, NUnit and other
test frameworks. It is designed to work with Delphi 2010 or later,
it makes use of language/RTL features that are not available in
older versions of Delphi. Its author, Vincent Parrett, provides this
summary:
Unit Testing in Delphi is not new, the DUnit framework
has been around for many years. So why create a new
unit test framework? My motivation for creating a new
framework was partially frustration with the confusion
over DUnit, DUnit2 and who was maintaining DUnit?
Sourceforge and subversion makes it very difficult to
make contributions to projects. I thought about forking
DUnit and putting it up on GitHub or Bitbucket, but my
feeling was that would make it even more confusing.
Add to that, DUnit was created a long time ago, it’s full
of IfDefs for CLR, LINUX etc, so it’s probably best left
alone (I don’t have Kylix or Delphi.NET installed).
DUnitX supports Delphi 2010 or later. If you need sup-
port for older versions of Delphi, I’m afraid you will
have to stick with DUnit, DUnitX makes use of Gener-
ics, Attributes and Anonymous methods. Delphi 2010
support has some limitations due to compiler bugs.
I work a great deal in Delphi 2007 where DUnitX cannot be used.
I also work in Delphi XE7, and now in Delphi 10.2 Tokyo, where
DUnitX is a viable alternative. However, while bridging these
366 35 DUnitX

versions, there is some value to keeping the test operations on the


same platform, so I remain in DUnit for the time being.
Also note that there is a very good video tutorial available by
Robert Love here: https://youtu.be/2blzztz_eNI
35.1 Looking at Some Code 367

35.1 Looking at Some Code

Given my own limited exposure to DUnitX I will not try to go very deeply into how it is used, but
the files available on GitHub provide examples such as this:

unit DUnitX . Examples . General ;

{ $I DUnitX . inc }

interface

uses
DUnitX . TestFramework ;

type
{ \ $M + }
[ TestFixture ( ’ ExampleFixture1 ’,’ General Example Tests ’)]
TMyExampleTests = class
public
// Run the same test with multiple parameters . ideally we would like to
// implement this using [ TestCase (’ Case 1 ’ ,[1 ,3 ,4]) ] but the delphi
// compiler will not accept arrays of TValue as parameters to attributes ,
// so we have to use a string constant .. the attribute will split the
// string and attempt to convert the values to the parameters of the
// test method .

[ Test ]
[ TestCase ( ’ Case 1 ’,’1 ,2 ’)]
[ TestCase ( ’ Case 2 ’,’3 ,4 ’)]
[ TestCase ( ’ Case 3 ’,’5 ,6 ’)]
procedure TestOne ( param1 : integer ; param2 : integer );

[ TestCase ( ’ Case 3 ’,’Blah ,1 ’)]


procedure AnotherTestMethod ( const a: string ; const b: integer );

[ Test ]
[ TestCase ( ’ Case4 ’,’ password ="" , password ="" ’)]
procedure TestCaseWithStrings ( const AInput , AResult : string );

[ Test ]
procedure TestTwo ;

[ Test ]
procedure TestError ;

[ Test ]
368 35 DUnitX

[ MaxTime (2000) ]
procedure TooLong ;

// Disabled test
[ Test ( false )]
procedure DontCallMe ;

[ Setup ]
procedure Setup ;

[ TearDown ]
procedure TearDown ;

procedure TestMeAnyway ;

[ Test ]
procedure LogMessageTypes ;

[ Test ]
[ Ignore ( ’ Because I said so !!! ’)]
procedure IgnoreMePublic ;
published
// Because this is a published method , it doesn ’t require the [ Test ]
// attribute [ Ignore (’ Because he said so !!! ’) ]
procedure IgnoreMePublished ;
end ;

[ TestFixture ]
TExampleFixture2 = class
private
FTestsRun : integer ;
public
[ SetupFixture ]
procedure SetupFixture ;
[ TeardownFixture ]
procedure TearDownFixture ;
published
procedure IAmATest ;
end ;

[ TestFixture ]
TExampleFixture3 = class
public
// will be used as SetupFixture
constructor Create ;
// will be used as TeardownFixture
35.1 Looking at Some Code 369

destructor Destroy ; override ;


published
procedure ATest ;
end ;

{ \ $M + }
TExampleFixture4 = class
protected
FObject : TObject ;
public
[ SetupFixture ]
procedure SetupFixture ;
[ TeardownFixture ]
procedure TearDownFixture ;
end ;

[ TestFixture ]
TExampleFixture5 = class ( TExampleFixture4 )
public
[ Test ]
procedure Testing ;
end ;

TExampleFixture6 = class
protected
FObject : TObject ;
public
constructor Create ;
destructor Destroy ; override ;
end ;

TExampleFixture7 = class ( TExampleFixture6 )


public
[ Test ]
procedure Testing ;
end ;

implementation

uses
{ \ $IFDEF USE_NS }
System . SysUtils ,
System . Classes ,
{ \ $ELSE }
SysUtils ,
Classes ,
370 35 DUnitX

{ \ $IFDEF DELPHI_2010_DOWN }
// D2010 doesn ’t have TThread . Sleep
Windows ,
{ \ $ENDIF }
{ \ $ENDIF }
DUnitX . DUnitCompatibility ;

{ TMyExampleTests }

procedure TMyExampleTests . DontCallMe ;


begin
TDUnitX . CurrentRunner . Status ( ’ DontCallMe called ’);
raise Exception . Create ( ’ DontCallMe was called ! ’);
end ;

procedure TMyExampleTests . IgnoreMePublic ;


begin
TDUnitX . CurrentRunner . Status ( ’ IgnoreMePublic called ’);
raise Exception . Create ( ’ IgnoreMePublic was called when ’ +
’it has IgnoreAttibute ! ’);
end ;

procedure TMyExampleTests . IgnoreMePublished ;


begin
TDUnitX . CurrentRunner . Status ( ’ IgnoreMePublished called ’);
raise Exception . Create ( ’ IgnoreMePublished was called when it ’ +
’ has IgnoreAttibute ! ’);
end ;

procedure TMyExampleTests . LogMessageTypes ;


begin
TDUnitX . CurrentRunner . Log ( TLogLevel . Information , ’ Information ’);
TDUnitX . CurrentRunner . Log ( TLogLevel . Warning , ’ Warning ’);
TDUnitX . CurrentRunner . Log ( TLogLevel . Error , ’ Error ’);
end ;

procedure TMyExampleTests . Setup ;


begin
TDUnitX . CurrentRunner . Status ( ’ Setup called ’);
end ;

procedure TMyExampleTests . TearDown ;


begin
TDUnitX . CurrentRunner . Status ( ’ TearDown called ’);
end ;
35.1 Looking at Some Code 371

procedure TMyExampleTests . AnotherTestMethod ( const a: string ;


const b: integer );
begin
TDUnitX . CurrentRunner . Status ( Format (
’ AnotherTestMethod called with %s %d ’ ,[a ,b ]) );
end ;

procedure TMyExampleTests . TestCaseWithStrings ( const AInput ,


AResult : string );
begin
TDUnitX . CurrentRunner . Status ( Format (
’ TestCaseWithStrings called with %s %s ’, [ AInput , AResult ]) );
end ;

procedure TMyExampleTests . TestError ;


begin
raise Exception . Create ( ’ Error . ’);
end ;

procedure TMyExampleTests . TestMeAnyway ;


begin
TDUnitX . CurrentRunner . Status ( ’ TestMeAnyway called ’);
end ;

procedure TMyExampleTests . TestOne ( param1 : integer ; param2 : integer );


begin
TDUnitX . CurrentRunner . Status ( Format ( ’ TestOnce called with %d %d ’,
[ param1 , param2 ]) );
end ;

procedure TMyExampleTests . TestTwo ;


{ \ $IFDEF DELPHI_XE_UP }
var
x : TStringList ;
{ \ $ENDIF }
begin
TDUnitX . CurrentRunner . Status ( ’ TestTwo called ’);
TDUnitX . CurrentRunner . Status ( ’ hello world ’);

// No longer compatible for Delphi2010


{ \ $IFDEF DELPHI_XE_UP }
x := TStringList . Create ;
Assert . IsType < TObject >( x); // / a bit pointless as it ’s strongly typed .
x . Free ;
{ \ $ENDIF }
end ;
372 35 DUnitX

procedure TMyExampleTests . TooLong ;


begin
{ \ $IFDEF DELPHI_XE_UP }
TThread . Sleep (5000) ;
{ \ $ELSE }
Windows . Sleep (5000) ;
{ \ $ENDIF }
end ;

{ TExampleFixture2 }

procedure TExampleFixture2 . IAmATest ;


begin
Inc ( FTestsRun );
end ;

procedure TExampleFixture2 . SetupFixture ;


begin
FTestsRun := 0;
TDUnitX . CurrentRunner . Log ( ’ Setting up ... ’);
end ;

procedure TExampleFixture2 . TearDownFixture ;


begin
Assert . AreEqual ( FTestsRun , 1) ;
TDUnitX . CurrentRunner . Log ( ’ Tearing down ’);
end ;

{ TExampleFixture3 }

procedure TExampleFixture3 . ATest ;


begin
Assert . IsTrue ( true );
end ;

constructor TExampleFixture3 . Create ;


begin
// Empty
end ;

destructor TExampleFixture3 . Destroy ;


begin
// Empty
inherited ;
end ;
35.1 Looking at Some Code 373

{ TExampleFixture4 }

procedure TExampleFixture4 . SetupFixture ;


begin
FObject := TObject . Create ;
end ;

procedure TExampleFixture4 . TearDownFixture ;


begin
FObject . Free ;
end ;

{ TExampleFixture5 }

procedure TExampleFixture5 . Testing ;


begin
Assert . IsNotNull ( FObject , ’ Problem with inheritance ’);
end ;

{ TExampleFixture6 }

constructor TExampleFixture6 . Create ;


begin
FObject := TObject . Create ;
end ;

destructor TExampleFixture6 . Destroy ;


begin
FObject . Free ;
inherited ;
end ;

{ TExampleFixture7 }

procedure TExampleFixture7 . Testing ;


begin
Assert . IsNotNull ( FObject , ’ Problem with inheritance ’);
end ;

initialization
TDUnitX . RegisterTestFixture ( TMyExampleTests );
TDUnitX . RegisterTestFixture ( TExampleFixture2 );
TDUnitX . RegisterTestFixture ( TExampleFixture3 );
TDUnitX . RegisterTestFixture ( TExampleFixture5 );
TDUnitX . RegisterTestFixture ( TExampleFixture7 );
374 35 DUnitX

end .
35.2 DUnit vs. DUnitX 375

This snippet illustrates an advantage of attribute-based testing.

[ Test ]
[ TestCase ( ’ Case 1 ’,’1 ,2 ’)]
[ TestCase ( ’ Case 2 ’,’3 ,4 ’)]
[ TestCase ( ’ Case 3 ’,’5 ,6 ’)]
procedure TestOne ( param1 : integer ; param2 :
integer ) ;

Often we need to test edge cases and may not benefit from exhaus-
tive tests. Or we may wish to implement absolutely repeatable
tests, often in database-related operations. Being able to define
datapoints for these tests in attributes offers an expressive and
succinct presentation.

35.1.1 DUnit vs. DUnitX Features

This example should help you decide whether to go further


with DUnitX. You will find the complete package on GitHub:
https://github.com/VSoftTechnologies/DUnitX

35.2 DUnit vs. DUnitX

This chart, from the DUnitX website, is helpful.


376 35 DUnitX

Table 35.1: DUnit vs. DUnitX


Feature DUnit DUnitX
Base Test Class TTestCase None
Test Method Published Published or decorated with
[Test]
Fixture Setup N/A Decorated with [SetupFixture]
Method or constructor*
Test Setup Method Override Setup from Decorated with [Setup]
base class
Test TearDown Override Teardown Decorated with [TearDown]
Method from base class
Namespaces Through registration Unit Names (periods delimit
parameter (string) namespaces).
Data driven Tests N/A Decorated with
[TestCase(parameters)]
Asserts Check(X) Assert class
Asserts on Manual Assert.Contains*,
Contain- Assert.DoesNotContain*,
ers(IEnumerable) Assert.IsEmpty*
Asserts using N/A Assert.IsMatch (XE2 or later).
Regular
Expressions
Stack Trace Jcl Jcl, madExcept 3, madExcept 4,
support Eurekalog 7 **
Memory Leak FastMM4 FastMM4 (under construction)
Checking **
IoC Container Use Spring or other Simple IoC container Built in.
Console Logging Built in Built in (quiet or verbose
modes).
XML Logging Built in (own Built in - Outputs NUnit
format) compatible xml.

* Not available in D2010 due to compiler bugs.


** Extensible, simple api.
35.3 Summary 377

35.3 Summary

DUnitX is free and open source, so your best option will be to


download it and try it for yourself. It will be time well spent.
FixInsight 36
FixInsight is a static code analyzer for Delphi. It can be used as a 36.1 Real World Data . . . 381
plug-in, or in the Pro version, as a standalone tool. The analysis
can be of the current file in the editor, or of the entire project. There
are options which allow you to determine what you wish to search
for, and what thresholds you prefer for things like the number of
parameters, or the number of local variables.
There are many settings, and by selecting only the ones on which
you wish to focus, you can get a report which is very narrow in its
view, but covers the entire project. Or, you can leave all or most
selected, and run it on the current file, then rework as needed. This
is a very powerful tool. In addition, there is a command-line version
of the tool in the Professional version which can be employed
as part of your build process. Sections of the settings deal with
Conventions, Warnings, and Optimizations: The conventions:
▶ C101 Method ’Foo’ is too long (N lines)
▶ C102 Too many parameters in ’Foo’ (N parameters)
▶ C103 Too many variables in ’Foo’ (N variables)
▶ C104 Class name should start with ’T’
▶ C105 Interface name should start with ’I’
▶ C106 Pointer type name should start with ’P’
▶ C107 Class field name should start with ’F’
▶ C108 Nested WITH statement
▶ C109 Unneeded boolean comparison
▶ C110 Getter or setter name is different from property declara-
tion
▶ C111 Class name should start with ’E’

The warnings:
▶ W501 Empty EXCEPT block
▶ W502 Empty FINALLY block
▶ W503 Assignment right hand side is equal to its left hand
side
▶ W504 Missing INHERITED call in destructor
▶ W505 Empty THEN block
▶ W506 Empty ELSE block
▶ W507 THEN statement is equal to ELSE statement
380 36 FixInsight

▶ W508 Variable is assigned twice successively


▶ W509 Unreachable code
▶ W510 Values on both sides of the operator are equal
▶ W511 Object ’Foo’ created in TRY block
▶ W512 Odd ELSE-IF condition
▶ W513 Format parameter count mismatch
▶ W514 Loop iterator could be out of range (missing -1?)
▶ W515 Suspicious Free call
▶ W517 Variable ’Foo’ hides a class field, method or property
▶ W519 Method ’Foo’ is empty
▶ W520 Parenthesis might be missing around IN operand
▶ W521 Return value of function ’Foo’ might be undefined
▶ W522 Destructor without an override directive
▶ W523 Interface ’Foo’ declared without a GUID
▶ W524 Generic interface ’Foo’ declared with a GUID
▶ W525 Missing INHERITED call in constructor
▶ W526 Pointer to a nested method
▶ W527 Property is referenced directly in its getter or setter
▶ W528 Loop variable is not used in FOR-loop
▶ W529 Should be ’raise’ instead of ’raise object’?
▶ W529 ”Foo” interface has the same GUID with ”Bar”
▶ W530 Interface GUIDs should not be duplicated across the
project.
▶ W531 Actual parameter ”Foo” of FreeAndNil() must be a
reference to class instance
▶ W534 Class instance ”Foo” passed to ”Bar” but interface
expected
▶ W535 Enumerated constant(s) missing in case statement: Foo
▶ W536 New class instance (’Foo’) passed to ’Bar’ as const
interface parameter.
▶ W538 ClassName property is compared with a string
The optimizations:
▶ O801 CONST missing for unmodified string parameter ’Foo’
▶ O802 ResourceString ’Foo’ is declared but never used
▶ O803 Constant ’Foo’ is declared but never used
▶ O804 Method parameter ”Foo” is declared but never used
▶ O805 Inline marked routine ”%s” comes after its call in the
same unit
The manual provides detailed information on each of these settings
though they should mostly be pretty obvious. Certainly after you
36.1 Real World Data 381

look at the issues raised, you should be able to appreciate the


sometimes painful value of the generated report(s).
Using this is going to change how you write code. At the very
least, it will make you focus more on the details of what you write.
Sometimes, and often under the pressure of deadlines, we may
be inclined to settle for code that just works. It really doesn’t take
that much effort to keep a high level of practice, if you make good
practices part of your normal style. FixInsight is excellent for that.
Legacy projects tend to contain many examples of bad code. FixIn-
sight will draw your attention to them. The coverage ranges from
small things to huge ones. In legacy code, you might otherwise
not find all occurrences of empty except clauses, but you can get a
full report on them from FixInsight.
Ideally, you will get all members of your team to use FixInsight,
and use it often. In my work, I apply it before I close a module for
check-in. We might wish that capabilities like this were built into
the IDE, but having them as plug-ins is near enough for practical
use. There is also a command line version which you can make
part of your build process.
Entropy increases. It is good to have tools that make us aware of
the decay, so we can correct it as soon as possible.

36.1 Real World Data

We are inclined to consider some units as stable, and in need of no


special attention. As a small example, I ran FixInsight on a unit test
project, and present here only some of the messages emitted for
TestFramework. The messages are verbose, even though I removed
the prefixes, so forgive the wrapped lines.

testframework . pas (1699) : C101 Method ’ TTestResult .


Run ’ is too long (83 lines )
testframework . pas (3598) : C101 Method ’
RegisterTestInSuite ’ is too long (53 lines )
testframework . pas (1686) : C103 Too many variables in
’ TTestResult . Run ’ (11 variables )
testframework . pas (3588) : C103 Too many variables in
’ RegisterTestInSuite ’ (8 variables )
testframework . pas (2794) : C109 Redundant boolean
comparison
382 36 FixInsight

testframework . pas (2996) : C109 Redundant boolean


comparison
testframework . pas (653) : C110 Getter or setter name
is different from property declaration
testframework . pas (1682) : W501 Empty EXCEPT block
testframework . pas (977) : W519 Method ’ CopyTmpFiles ’
is empty
testframework . pas (999) : W519 Method ’ DeleteTmpFiles
’ is empty
testframework . pas (3008) : W525 Missing INHERITED
call in constructor
testframework . pas (3224) : W525 Missing INHERITED
call in constructor
testframework . pas (3241) : W525 Missing INHERITED
call in constructor
testframework . pas (3250) : W525 Missing INHERITED
call in constructor
testframework . pas (3418) : W525 Missing INHERITED
call in constructor
testframework . pas (3423) : W525 Missing INHERITED
call in constructor
testframework . pas (3430) : W525 Missing INHERITED
call in constructor
testframework . pas (3435) : W525 Missing INHERITED
call in constructor
testframework . pas (3089) : W528 Variable ’i ’ not used
in FOR - loop
testframework . pas (3531) : W528 Variable ’i ’ not used
in FOR - loop
testframework . pas (1507) : W534 Class instance ’
TestFailureError ’ passed to ’ AddError ’ but
interface expected
testframework . pas (1527) : W534 Class instance ’
Failure ’ passed to ’ AddFailure ’ but interface
expected
testframework . pas (1938) : W534 Class instance ’
Failure ’ passed to ’ AddFailure ’ but interface
expected

The point is that everything which contributes to your product is


worth checking with your best tools. Your efforts can only benefit
your customers’ view of your product.
FixInsight. Get it, use it. It will improve the way you write code.
GExperts 37
The GExperts plug-in has been around for many years, and feels 37.1 Configuration . . . . . 384
like a core part of Delphi. There is significant overlap among CnPack, 37.2 Editor Experts . . . . 386
GExperts, and MMX, and I use all three, yet turn to each of them
37.3 Replace Components 387
for particular tasks. Each has its strengths and weaknesses, and
37.4 GREP Search . . . . . 388
that is part of what guides my selection, as well as simple force of
37.5 GExperts Summary . 389
habit.
GExperts has a quite good help file, yet it is not offered on the
GExperts menu in the IDE. Instead, there is help on the wizard
dialogs. Helpful as this is, it would be nice also to have a help item
on the GExperts menu.

Figure 37.1: GExperts Main menu


384 37 GExperts

The original website is here: https://www.gexperts.org/.


This site is no longer maintained, but has useful information,
screenshots, and other content. Documentation is found here:
https://www.gexperts.org/tour/.
For current releases, you will need to go to the Experimental
GExperts Version: https://blog.dummzeuch.de/experimental-
gexperts-version/. GExperts is maintained now by Thomas
Mueller, who is quite active on the project.

37.1 Configuration

A quick look at the configuration dialog pages will give you a


sense of how much is included in GExperts. The first page:

Figure 37.2: GExperts Configuration


Dialog page 1

As you can see, there are many tools, and quite a few offer custom
configuration.
The second page is called General, and contains only a few entries:

Note, however, that you have control over the font used in the UI,
as well as the few file paths which GExperts needs for operation.
37.1 Configuration 385

Figure 37.3: GExperts Configuration


Dialog General

The third page is for Editor Experts:

Figure 37.4: GExperts Configuration


Dialog Editor Experts

This is really just for the keyboard shortcut setup, however.


The GExperts documentation offers a list of Experts, only partly
shown here:
There’s a lot in this tool, and it will take time to become familiar
with it, but the benefits it offers are substantial. Complete coverage
of even the available list of experts is beyond the scope of this
book. Hopefully, this small offering will be sufficient to make you
take a look, if you are not already a GExperts user. There are a few
key features that I exercise often, and they make my tasks much
easier.
386 37 GExperts

Figure 37.5: GExperts Configuration


Dialog Experts List

37.2 Editor Experts

There are many useful operations available in the Editor Experts


submenu.
There are little things which are too useful not to have:
▶ Align Lines—A misleading name for a very nice tool. Select a
range of lines, assignment statements, perhaps, and Ctrl-Alt-
Z will offer to align them on the :=, or = or a number of other
ways, depending on what is contained in the selected lines.
▶ Insert Date/Time—Often modified to insert a string with
developer initials, date, and whatever else you want, to leave
fingerprints in code you modify.
▶ Sort Selected Lines—Does what it says, on the lines you select.
Less sweeping than the MMX capability to sort a class, but
very often useful in reducing the chaos in class declarations.
Once you are accustomed to sorted declarations, you will
find yourself more productive.
37.3 Replace Components 387

Figure 37.6: GExperts Editor Experts

37.3 Replace Components

If you find yourself wanting to replace some components, outliers


which might better come from your mainstream sources, this
wizard is a great help. As with so many things, the wizard is not
perfect, and will often fail, in particular, when the replacement
component is from DevExpress, as many of their components move
certain properties into the Properties tree of the component. The
wizard does offer custom mapping, however, and which will let
you establish custom definitions to resolve the incompatibilities.
Replacing a component with a different type can be tedious. The
wizard makes it a trivial exercise, and is highly recommended.
Setting up the custom mapping is very simple, too, especially with
the use of the error log which is provided.
388 37 GExperts

Figure 37.7: GExperts– Replace Com-


ponents wizard

37.4 GREP Search

The GREP search wizard is one of the items I use frequently. It has
great power and flexibility, and in large projects, is an essential
tool.

Figure 37.8: GExperts– Replace Com-


ponents wizard

It delivers results in a list, and greatly facilitates many operations.


In the refactoring of modules, it is not uncommon to create a new
module or group of modules, into which various routines are then
moved. You must then deal with the breakage these actions have
caused in your project. If uOldUnit has been replaced by uNewUnit,
37.5 GExperts Summary 389

then the mass replacement capability in the GREP results dialog


will make these changes quickly and easily. If uOldUnit is replaced
by uNewUnit1, uNewUnit2, and uNewUnit3, you can still use the mass
replace, but may later find that some of the new units are not
actually needed in some of the modules which referenced uOldUnit.

Figure 37.9: GExperts– Replace Com-


ponents wizard

In such an instance, the GREP results list is still a benefit, as it is


a list which will aid you in working through the modified units.
When you expand the list, you can double-click on any of the
lines in a file result to open that file in the IDE. Once you have
opened the files of interest, you can then use the CnPack Uses
Cleaner to process all the open files. That’s one reason I keep these
overlapping plug-ins in my IDE.

37.5 GExperts Summary

GExperts is one of the oldest of the Delphi plug-ins, and is updated


regularly. It should definitely have a place in your toolset.
Homebrew Tools 38
There comes a time when none of the available tools will meet 38.1 Cycles Analyzer . . 391
the specific needs of your legacy work. At that point, it is time to 38.1.1 Cycles Analyzer
write some tools for yourself. If you have any of the more recent Code . . . . . . . . . 395
versions of Delphi, you will find the expanded class libraries very 38.1.2 Dependency Cycles
helpful in such tasks. in Components . . . 405
38.1.3 Unit Dependencies: A
Closer View . . . . . 406
Homebrew?
38.2 Map Explorer . . . . 407
I have chosen the word ’homebrew’ to refer to tools we build 38.3 Component Collec-
which are not commercially available. The word is not used in tor . . . . . . . . . . . 409
any derogatory sense, but simply as a useful short name for 38.3.1 Collecting Installed
the class of tools. The characteristics of such tools are: Components . . . . 411
38.3.2 Collecting the DFM
▶ No commercial tool for the purpose exists. Files List . . . . . . . 415
▶ The construction of the tool is usually a small project. 38.3.3 Collect Component
▶ Such tools are generally not formal projects in our compa- Instances . . . . . . . 416
nies. 38.3.4 Producing the Ag-
▶ The designs may be rough and evolve according to need. gregate Component
Data . . . . . . . . . 421
38.3.5 Putting Together the
There is much you can do with simple tools, and it is a matter of Pieces . . . . . . . . . 423
what you find you need most, and how much time you can devote 38.4 Separating Wheat
to fashioning your own tools. If you focus on individual tasks, and and Chaff . . . . . . 423
where you may most easily find the data, then these tools will not
require a huge amount of work.

38.1 Cycles Analyzer

MMX provides the Unit Dependencies analysis tool, which is


useful, but I felt the need of something more easily viewed than
the Cartesian map. You have the option of generating a report,
which is a rather large text file in which each dependency cycle—a
chain of references, in fact—is presented as a CSV list. Although
this is not directly very useful, it is easy to parse, and I soon built
a small and simple tool which simply shows the modules and
cycle counts from two of the MMX reports, with a delta column,
column sorting, and a simple search capability.
392 38 Homebrew Tools

What this tool does not provide is any suggestion as to where you
might want to begin your cleanup efforts. Nothing I have found
will do that, nor have I conceived of a strategy to deliver such
suggestions myself.
The Cycles Analyzer uses no third-party components, and can
be built in Delphi 10.3 Rio—the tool I used for it—or in any fairly
recent Delphi, with no changes needed. It would also be fairly
simple to rework for older versions of Delphi, if you desire. The
complete project code is in the downloads for this book: https:
//github.com/wmeyer48/DL-code
Usage is simple, and I have tried to make it obvious. The essential
controls are:
▶ Reference Report edit box. Clicking on the button to the right
of this opens a File Open dialog box. Select your reference
report file and click open. It will be parsed and the results
presented in the grid.
▶ New Report edit box. Works in the same way as the one
for the reference report, but for the second report you will
compare to the first. As with the other, opening the file causes
it to be processed and its data to be added to the grid.
The remaining controls are not essential to operation, but add
functionality you may wish to use, such as searching and exporting
the data to a CSV file. The CSV file may then be imported easily
into Excel for whatever purpose you may find useful.
The form looks like this:

Figure 38.1: Cycles Analyzer


38.1 Cycles Analyzer 393

Looking more closely at the controls, we can consider additional


features.

Figure 38.2: Cycles Analyzer - Con-


trols

▶ The Search Module edit box allows you to enter a module


name, and will move the cursor in the grid to the first match
it finds. If that wording seems odd, it is because the search
is incremental and case-insensitive. Each module appears in
only a single row, but a number of modules may match the
incomplete search pattern.
▶ In the row below Search you will see the Records label, next to
which is the number of records in the grid. If only a reference
report has been loaded, the count is directly from that, but
with two reports loaded, the count may increase, as there
may have been cycles added in new modules.
▶ Next to the Ref Cycles label to the right of Records is the
count of UDCs found in the reference report. This report is
from a large project which has need of rework.
▶ To the right again is the label for New Cycles, the count
associated with the second report loaded.
▶ The last label is for Delta Cycles, the difference between
the Reference and New counts. This clearly indicates some
progress has been made.
In order to present further explanation of the grid display, it is
necessary to show data from a real project, and I have blurred
the module names, as no one will appreciate having such data
published about a recognizable product.
▶ In the Ref Count column, the first four entries show - - rather
than a number. These files were not present in the first report,
so no number is available in that column.
▶ If there were - - in some rows in the New column, that would
indicate that a module which was involved in dependency
cycles in the reference report is no longer involved.
394 38 Homebrew Tools

Figure 38.3: Cycles Analyzer - New


Files

▶ In the AbExcept module row, you can see that the cycle count
is the same for both columns, so zero in the Delta column.

Odd Language on Cycles

In discussing the UDCs issue I have found it necessary to be


careful in expressing what is happening. Some modules are
causing these problems, and some are simply dragged along
as innocent bystanders. I will usually refer to these modules as
being ’involved in’ or ’participating in’ unit dependency cycles.
It can be very difficult in large complex projects to determine
the real sources of the UDCs.
38.1 Cycles Analyzer 395

38.1.1 Cycles Analyzer Code

The Cycles Analyzer is a small project which embodies the principles I have been promoting in this
book. The main form code is minimal:

unit u_Main ;

interface

uses
// declarations omitted for the book

type
TfrmMain = class ( TForm )
// declarations omitted for the book
procedure btnExportDataClick ( Sender : TObject );
procedure btnNewFileSelectClick ( Sender : TObject );
procedure btnRefFileSelectClick ( Sender : TObject );
procedure edtSearchChange ( Sender : TObject );
procedure FormCreate ( Sender : TObject );
procedure grdMainMouseMove ( Sender : TObject ; Shift : TShiftState ;
X , Y: Integer );
procedure grdMainTitleClick ( Column : TColumn );
private
FPreviousColumnIndex : Integer ;
FRefCycles : Integer ;
FSortOrder : string ;
function ProcessInput ( const IsRefFile : Boolean ;
const FileName : string ): string ;
end ;

var
frmMain : TfrmMain ;

implementation

{ $R *. dfm }

uses
// declarations omitted for the book

procedure TfrmMain . btnExportDataClick ( Sender : TObject );


begin
dlgExport . InitialDir :=
IncludeTrailingPathDelimiter ( GetFolder ( CSIDL_PERSONAL ));
if dlgExport . Execute then
396 38 Homebrew Tools

begin
dmMain . ExportData ( dlgExport . FileName , chkIncludeFieldNames . Checked );
end ;
end ;

procedure TfrmMain . btnNewFileSelectClick ( Sender : TObject );


begin
if dlgOpen . Execute then
begin
if not string ( dlgOpen . FileName ). IsEmpty then
begin
edtNewReport . Text := dlgOpen . FileName ;
lblNewCycles . Caption := ProcessInput ( False , dlgOpen . FileName );
lblDeltaCount . Caption := ’ Delta Cycles : ’ +
IntToStr ( dmMain . NewCycles - FRefCycles );
end ;
end ;
end ;

procedure TfrmMain . btnRefFileSelectClick ( Sender : TObject );


begin
if dlgOpen . Execute then
begin
if not string ( dlgOpen . FileName ). IsEmpty then
begin
FRefCycles := 0;
lblDeltaCount . Caption := ’ -- ’;
edtReferenceReport . Text := dlgOpen . FileName ;
lblRefCycles . Caption := ProcessInput ( True , dlgOpen . FileName );
FRefCycles := dmMain . RefCycles ;
end ;
end ;
end ;

procedure TfrmMain . edtSearchChange ( Sender : TObject );


begin
if not string ( edtSearch . Text ). IsEmpty then
begin
dmMain . fdmCycles . LocateEx ( ’ Module ’, edtSearch . Text ,
[ lxoCaseInsensitive , lxoPartialKey ]) ;
end ;
end ;

procedure TfrmMain . FormCreate ( Sender : TObject );


begin
dmMain := TdmMain . Create ( Self );
38.1 Cycles Analyzer 397

end ;

procedure TfrmMain . grdMainMouseMove ( Sender : TObject ;


Shift : TShiftState ; X , Y: Integer );
var
pt : TGridcoord ;
begin
pt := grdMain . MouseCoord (x , y);
if pt . y = 0 then
grdMain . Cursor := crHandPoint
else
grdMain . Cursor := crDefault ;
end ;

procedure TfrmMain . grdMainTitleClick ( Column : TColumn );


var
fontColor : TColor ;

procedure UpdateColumnHeader ;
begin
grdMain . Columns [ FPreviousColumnIndex ]. Title . Font . Color := clBlack ;
grdMain . Columns [ FPreviousColumnIndex ]. Title . Font . Style :=
grdMain . Columns [ FPreviousColumnIndex ]. Title . Font . Style - [ fsBold ];
FPreviousColumnIndex := Column . Index ;
Column . Title . Font . Style := Column . Title . Font . Style + [ fsBold ];
end ;

procedure UpdateColumnSort ;
begin
if FPreviousColumnIndex = Column . Index then
if FSortOrder = ’’ then
begin
FSortOrder := ’D ’;
fontColor := clBlack ;
end
else
begin
FSortOrder := ’’;
fontColor := clRed ;
end ;
end ;

begin
UpdateColumnHeader ;
fontColor := clBlack ;
if FPreviousColumnIndex > -1 then
398 38 Homebrew Tools

begin
UpdateColumnSort ;
dmMain . fdmCycles . IndexFieldNames :=
dmMain . fdmCycles . FieldDefs [ FPreviousColumnIndex ]. Name +
IfThen ( FSortOrder . IsEmpty , ’:D ’, ’’);
grdMain . Columns [ FPreviousColumnIndex ]. Title . Font . Color := fontColor ;
dmMain . fdmCycles . First ;
end ;
end ;

function TfrmMain . ProcessInput ( const IsRefFile : Boolean ;


const FileName : string ): string ;
const
prefixes : array [ False .. True ] of string = ( ’ New ’, ’ Ref ’);
begin
Result := prefixes [ IsRefFile ] + ’ Cycles : ’ +
dmMain . ProcessInputFile ( FileName , IsRefFile );
grdMain . DataSource := dmMain . dsCycles ;
lblRecordsCount . Caption := dmMain . RecordCount . ToString ;
end ;

end .

The data module is where all the work is done, but it is also relatively light:

unit d_Main ;

interface

uses
// declarations omitted for the book

type
TdmMain = class ( TDataModule )
// declarations omitted for the book
procedure DataModuleCreate ( Sender : TObject );
procedure fdmCyclesCountGetText ( Sender : TField ; var Text : string ;
DisplayText : Boolean );
private
FNewCycles : Integer ;
FNewLoaded : Boolean ;
FRefCycles : Integer ;
FRefFile : Boolean ;
FRefLoaded : Boolean ;
procedure AddToData ( const IsRefFile : Boolean ; const AModuleName : string ;
const ACount : Integer ); overload ;
38.1 Cycles Analyzer 399

procedure AddToData ( const ModuleName , Count : string ); overload ;


function DoProcessInputFile ( const FileName : string ): string ;
function GetFieldNames : string ;
function GetRecordCount : Integer ;
procedure ProcessFinal ;
procedure ProcessLines ( sl : TStringList );
public
procedure ExportData ( const FileName : string ;
const IncludeFieldNames : Boolean );
function ProcessInputFile ( const FileName : string ;
const IsRefFile : Boolean ): string ;
property NewCycles : Integer read FNewCycles write FNewCycles ;
property RecordCount : Integer read GetRecordCount ;
property RefCycles : Integer read FRefCycles write FRefCycles ;
property RefFile : Boolean read FRefFile write FRefFile ;
end ;

var
dmMain : TdmMain ;

implementation

{ $R *. dfm }

uses
System . StrUtils ;

const
NotInRef = -1;
NotInNew = -2;

{ % CLASSGROUP ’ Vcl . Controls . TControl ’}

{ TdmMain }

procedure TdmMain . AddToData ( const IsRefFile : Boolean ;


const AModuleName : string ; const ACount : Integer );
var
found : Boolean ;
begin
fdmCycles . IndexFieldNames := ’ Module ’;
found := fdmCycles . Locate ( ’ Module ’, AModuleName , [ loCaseInsensitive ]) ;

if IsRefFile then // only adds


begin
fdmCycles . Append ;
400 38 Homebrew Tools

fdmCyclesRecID . AsInteger := fdmCycles . RecordCount ;


fdmCyclesModule . AsString := AModuleName ;
fdmCyclesRefCount . AsInteger := ACount ;
Inc ( FRefCycles , ACount );
end
else // else 2 nd file , so add & diff
begin
if found then
fdmCycles . Edit
else
begin
fdmCycles . Append ;
fdmCyclesRecID . AsInteger := fdmCycles . RecordCount ;
fdmCyclesModule . AsString := AModuleName ;
fdmCyclesRefCount . AsInteger := NotInRef ;
end ;
fdmCyclesNewCount . AsInteger := ACount ;
fdmCyclesDelta . AsInteger := fdmCyclesNewCount . AsInteger -
fdmCyclesRefCount . AsInteger ;
Inc ( FNewCycles , ACount );
end ;
fdmCycles . Post ;
end ;

procedure TdmMain . AddToData ( const ModuleName , Count : string );


var
sCount : string ;
begin
sCount := ReplaceText ( Copy ( Count , 2) , ’) ’, ’’) ;
dmMain . AddToData ( FRefFile , ModuleName , sCount . ToInteger );
end ;

procedure TdmMain . DataModuleCreate ( Sender : TObject );


begin
fdmCycles . CreateDataSet ;
end ;

function TdmMain . DoProcessInputFile ( const FileName : string ): string ;


var
sl : TStringList ;
begin
FNewCycles := 0;
FRefCycles := 0;
if FileExists ( FileName ) then
begin
sl := TStringList . Create ;
38.1 Cycles Analyzer 401

try
sl . LoadFromFile ( FileName );
if sl . Count > 0 then
begin
if FRefFile then
fdmCycles . EmptyDataSet ;
ProcessLines ( sl );
end ;
if FRefFile then
FRefLoaded := True
else
FNewLoaded := True ;
ProcessFinal ;
finally
sl . Free ;
end ;
end ;
Result := fdmCycles . RecordCount . ToString ;
end ;

procedure TdmMain . ExportData ( const FileName : string ;


const IncludeFieldNames : Boolean );
var
sl : TStringList ;
reader : TFDBatchMoveDataSetReader ;
writer : TFDBatchMoveTextWriter ;
begin
if not string ( FileName ). IsEmpty then
begin
if FileExists ( FileName ) then
DeleteFile ( FileName );
writer := nil ;
reader := TFDBatchMoveDataSetReader . Create ( fdBatchMove );
try
reader . DataSet := dmMain . fdmCycles ;
reader . Optimise := False ;

writer := TFDBatchMoveTextWriter . Create ( fdBatchMove );


writer . FileName := FileName ;

fdBatchMove . Execute ;
finally
reader . Free ;
writer . Free ;
end ;
402 38 Homebrew Tools

if IncludeFieldNames then
begin
sl := TStringList . Create ;
try
sl . LoadFromFile ( FileName );
sl . Insert (0 , GetFieldNames ) ;
sl . SaveToFile ( FileName );
finally
sl . Free ;
end ;
end ;
end ;
end ;

procedure TdmMain . fdmCyclesCountGetText ( Sender : TField ; var Text : string ;


DisplayText : Boolean );
begin
Text := Sender . AsString ;
if ( string ( Sender . Name ). Contains ( ’ Ref ’)) then
begin
if Pos ( ’-’, Text ) > 0 then
Text := ’ -- ’;
end
else if string ( Sender . Name ). Contains ( ’ New ’) then
if FNewLoaded then
begin
if Pos ( ’-’, Text ) > 0 then
Text := ’ -- ’;
end
else
Text := ’’;
end ;

function TdmMain . GetFieldNames : string ;


var
idx : Integer ;
begin
Result := ’’;
for idx := 0 to fdmCycles . FieldDefs . Count - 1 do
begin
if Result . IsEmpty then
Result := ’" ’ + fdmCycles . FieldDefs [ idx ]. Name + ’" ’
else
Result := Result + ’ ," ’ + fdmCycles . FieldDefs [ idx ]. Name + ’" ’;
end ;
end ;
38.1 Cycles Analyzer 403

function TdmMain . GetRecordCount : Integer ;


begin
Result := fdmCycles . RecordCount ;
end ;

procedure TdmMain . ProcessFinal ;


var
_delta : Integer ;
begin
// first resolve removed files
fdmCycles . First ;
while not fdmCycles . Eof do
begin
if fdmCyclesNewCount . IsNull then
begin
fdmCycles . Edit ;
fdmCyclesNewCount . AsInteger := NotInNew ;
fdmCycles . Post ;
end ;
fdmCycles . Next ;
end ;
// now calculate deltas
if FNewLoaded and FRefLoaded then
begin
fdmCycles . First ;
while not fdmCycles . Eof do
begin
if fdmCyclesRefCount . AsInteger < 0 then
_delta := fdmCyclesNewCount . AsInteger
else if fdmCyclesNewCount . AsInteger < 0 then
_delta := 0
else
_delta := fdmCyclesNewCount . AsInteger - fdmCyclesRefCount . AsInteger ;

begin
fdmCycles . Edit ;
fdmCyclesDelta . AsInteger := _delta ;
fdmCycles . Post ;
end ;
fdmCycles . Next ;
end ;
end ;
end ;

function TdmMain . ProcessInputFile ( const FileName : string ;


404 38 Homebrew Tools

const IsRefFile : Boolean ): string ;


begin
Result := ’’;
if IsRefFile then
begin
FNewLoaded := False ;
FRefLoaded := False ;
end ;
if not FileName . IsEmpty then
begin
FRefFile := IsRefFile ;
fdmCycles . DisableControls ;
DoProcessInputFile ( FileName );
fdmCycles . First ;
fdmCycles . EnableControls ;
Result := IfThen ( FRefFile , FRefCycles . ToString , FNewCycles . ToString );
end ;
end ;

procedure TdmMain . ProcessLines ( sl : TStringList );


var
arr : TArray < string >;
idx : Integer ;
begin
idx := 0;
while idx < sl . Count do
begin
if sl [ idx ]. Contains ( ’ Cycles for ’) then
begin
arr := sl [ idx ]. Split ([ ’ ’]) ;
AddToData ( arr [ Length ( arr ) - 2] , arr [ Length ( arr ) - 1]) ;
end ;
Inc ( idx );
end ;
end ;

end .
38.1 Cycles Analyzer 405

In fairness, this small program is much simpler to manage than


a large project, and was written from the start to serve a narrow
purpose.
Legacy projects seem invariably to be large or very large bundles
of code which lacks any sort of coherent design. To the degree
that any modules are well-written, well-commented, and well-
organized, you will likely find that they are relatively new in the
project. Inevitably, you will discover some very old modules which
are resistant to any rational approach to refactoring. They will
be complex, essential to some portion of the product, and were
generally written by a single developer who is no longer with the
company. Such is life in the real world.

38.1.2 Dependency Cycles in Components

Yes, it’s true, some components do contain dependency cycles. For


example:
Cycles for AbExcept (1)
AbExcept,AbUtils,AbExcept

Cycles for AbUtils (1)


AbUtils,AbExcept,AbUtils

Cycles for AdvOfficePager (1)


AdvOfficePager,AdvOfficePagerStylers,AdvOfficePager

Cycles for AdvOfficePagerStylers (1)


AdvOfficePagerStylers,AdvOfficePager,
AdvOfficePagerStylers
This is not a condemnation of the publishers, and giving proper
credit, where these exist, they are usually not long chains. To avoid
them altogether requires a compromise in structuring code, at
least. At some point, you find yourself considering current practice
principles which are themselves in conflict.
Ultimately, you will have to decide how to fit code to your priorities.
Absolute rules are often absolutely useless. Pragmatism will yield
greater benefits.
406 38 Homebrew Tools

38.1.3 Unit Dependencies: A Closer View

The MMX Cycles Report presents in text form a full map of


dependency issues. But reading megabytes of plain text has not
been particularly helpful to my understanding. On the other hand,
it took only modest effort to create an application which would
read and parse that report, collecting the unit names and cycle
counts into a dataset and presenting in a grid. I later added the
capability to read and parse a second report, allowing me to
display the first, second, and delta counts in columns, each of
which is sortable. That provides a very comprehensible assessment
of the progress made.
Having a quickly accessible view of the units most affected by
dependency issues is more useful to me than the raw report or
the Cartesian view. It is not helpful in assessing which units cause
the problems; that still requires human analysis. But at least it
delivers a shorter list of modules you must analyze on your own.
Sometimes you get lucky, and make a large difference with small
change. Other times you may battle for every single cycle. I do not
know of a more effective approach at this time.

Figure 38.4: Cycles Report Analyzer

A better view of the top band after two reports have been loaded.
Here you can see the two paths entered:
As the application is built to be a demo for the book, it uses
only components which ship with Delphi. Some functionality is
38.2 Map Explorer 407

Figure 38.5: Cycles Report Analyzer


- Top band

therefore more limited than it might otherwise be, but it leaves it


open for you to tweak as you wish.
With a pair of reports open, you can incrementally search for
modules:

Figure 38.6: Cycles Report Analyzer


- Search

With real data, blurred to protect the guilty:

Figure 38.7: Cycles Report Analyzer


- Data

38.2 Map Explorer

One good resource is the map file Delphi builds. Set the option to
produce the detailed map, and you will find it in the same folder
as your executable. It will be fairly large, but is easy to parse.
408 38 Homebrew Tools

Figure 38.8: Map File - Segments

As you can see, there is a lot of content which is specific to the


linking actions, but look at the column with M= as the prefix.
M stands for module, and you will recognize many there which
are Delphi library units. Should you wish to go beyond that, and
to collect not only the module names, but the methods from
each which are actually used—the map only contains what has
been linked into the EXE, and the smart linker removes unused
routines—then you will see that the next level of activity is no
more difficult: Where you see, for example, Data.DB..87, that is the

Figure 38.9: Map File - Publics

module itself. Data.DB..EDatabaseError is a method in the module.


So with very little difficulty, you could collect the modules and
methods into datasets in master/detail form, and make use of that
as you wish for a variety of analyses.
Here is a small example of data collected from a map file, which
offers the possibility of listing by segment or all.
The program also presents the Publics extracted form the map,
and subject to the same filtering. Although this application is very
basic, it may give you some ideas about what you may wish to
do with a tool of your own. In the next section, a more capable
application will be used to demonstrate other operations on data
extracted from the map file. Unless you are prepared to jump into
38.3 Component Collector 409

Figure 38.10: Map Explorer - Mod-


ules

Figure 38.11: Map Explorer - Publics

parsing source code, the map file is one of your best sources of
information.
The map file is not documented by Embarcadero, but is based on
an Intel format, and some searching will yield further explanations,
should you need them.
The Map Explorer is included here not for its usefulness, which is
small, but as a seed for ideas you may wish to explore in your own
tools. It illustrates that you cab obtain benefits from very simple
code, when you are able to process an existing simple text file.

38.3 Component Collector

Legacy projects tend to accumulate components as a ship accumu-


lates barnacles. Cataloging the set of components actually used in Full code for this small tool is avail-
able online, and you may freely use
it on your own projects, but may not
incorporate the code into a tool built
for sale.
410 38 Homebrew Tools

the deliverable executable is an essential step in the analysis. Oddly,


there seem to be no free or commercial tools which provide that
functionality. Happily, it is not too difficult to create an application
to deliver the results needed. There are a few top level operations
needed:
▶ Collect the inventory of all installed components in Delphi.
▶ Collect the list of all DFM files in your project.
▶ Scan each DFM file for component instances, and collect the
data. The components used could be found in the MAP file,
but the better way will be through parsing the DFM files, as
we will get the instance count per form, as well as the form
name and file path.
The result of these actions will be a list which includes almost
all components in your project, visible and invisible, whether
from Delphi, from any third-party, or built in-house. That makes
for a high noise level in the collected data; we will reduce that
noise with some organization. But let’s start with the environment
variables, which are systemic, not particular to the project.

Component Collection Challenges

In the paragraph above, the reference to almost all components


needs explanation. Understanding the issues is not essential to
proceeding, but may be helpful and interesting to you.
DFM files are easy to parse for component content, but the
components within them are obviously those — and only
those — which have been placed on the form. Components
instantiated at runtime need not be contained in the DFM file.
Some or all of those types may be in the DFM, but that would
be a matter of coincidence, not a certainty.
PAS files contain the declarations of all components found in
the form, but must also contain declarations of any which will
be instantiated at runtime. The PAS file is necessarily a more
complete source for these, but at the expense of more complex
parsing requirements, for which I suggest you use DelphiAST.
There is another detail to bear in mind here: Not all used
components will be found in either the DFM or the PAS files. It is
easy to overlook that the TForm is subject to special handling. You
will not find it declared in the PAS file as other components; it is
38.3 Component Collector 411

there instead as the class which contains the other components.


The same holds for TFrame and TDataModule. Each of these
top level components is a container for others. Moreover, top
level components are assigned a unique type name, so in
the DFM you may see, for example: object fSelectFolders:
TfSelectFolders, even though the type from which it inherits
is TForm. That, of course, is the point: Your form is not a TForm,
but a descendant of TForm.

The limitations inherent in collecting from the DFM files do not


impede us for the purposes to which we will put that information.
The much simpler parsing is more than sufficient compensation
for those limitations.

38.3.1 Collecting Installed Components

By collecting an inventory of all installed components, we can That sounds a bit twisted, but the is-
make use of the information in our collection of the components sue is how to link the components in
our forms with packages from which
actually used in our project. The easiest way to accomplish this they came. The simplest way is to
task seems to be through the use of a Wizard in the Delphi IDE. collect all known components, then
That may seem a daunting task, but there is a very useful starting collect the used components.
point available from David Hoyle, which may be found on his
website: https://www.davidghoyle.co.uk/WordPress/. David
has written a most welcome book explaining the Delphi ToolsAPI
and its application.
Better still for our purposes, David has created a number of instal-
lable applications using the ToolsAPI, and these can be obtained
from his site. The one most suited to our needs in this instance is
his Package Viewer: https://www.davidghoyle.co.uk/WordPres As with other projects, the modified
s/?page_id=1299. This can be modified to produce the inventory code for this is available on my site.

we need. David has graciously consented to my use of his code


here, which is much appreciated.
Initially, I used the TFDMemTable component in this work, but have
now reworked to use instead the TClientDataset simply because it
is useful in older versions of Delphi.

Why Harvest Component Info from old Delphi?

You will find value in collecting the complete component


inventory of the Delphi version from which you are migrating.
412 38 Homebrew Tools

First, because you need to know which components have


been used. But soon you will recognize that is insufficient for
migration. What you really need is:
▶ Inventory of all components now used in your legacy
application.
▶ Inventory of all components installed in the old Delphi, as
this will assist with context.
▶ A list of outliers—components which are only rarely
used—which you may wish to replace with something
else.
Once you work through the analysis of your components in
In some cases, you may find that you current use, and decide which you will keep and which you
are unable to make the component will not, then you are in a position to begin specifying the
conversions in the old project. For
example, you may be changing from
conversion work for that. You will want to make those changes
one vendor to another, and the new before migration, so that the components to be retired need
components do not support the older not be installed in the new Delphi environment.
compiler.

As can be seen on David’s blog, he produces a TreeList filled with


the information about the installed packages and their components.
Bear in mind that many packages do not contain components.
The TreeList is not what we need, but the code which produces it
offers a framework into which we can add something to get what
we want:

Procedure TfrmDGHPackageViewer . IteratePackages ;


var
fn : string ;
PS : IOTAPAckageServices ;
iPackage : Integer ;
P: TTreeNode ;
frm : TfrmDGHPackageViewerProgress ;
begin
tvPackages . Items . BeginUpdate ;
try
PS := ( BorlandIDEServices As
IOTAPAckageServices );
frm := TfrmDGHPackageViewerProgress . Create (
Application . MainForm );
try
frm . ShowProgress ( PS . PackageCount );
for iPackage := 0 To PS . PackageCount - 1 do
begin
P := tvPackages . Items . AddChildObject ( Nil ,
38.3 Component Collector 413

PS . PackageNames [ iPackage ],
TObject ( iPackage ));
if chkExportComponents . Checked then
begin
cdsPackages . Append ;
cdsPackagesRecID . AsInteger := iPackage ;
end ;
ProcessComponents (PS , iPackage , P);
ProcessPackageInfo (P ,
PS . Package [ iPackage ]) ;
if chkExportComponents . Checked then
begin
cdsPackages . Post ;
end ;
frm . UpdateProgress ( Succ ( iPackage ));
end ;
frm . HideProgress ;
finally
frm . Free ;
end ;
tvPackages . AlphaSort ( True );
finally
tvPackages . Items . EndUpdate ;
end ;

if chkExportComponents . Checked then


begin
fn := IncludeTrailingPathDelimiter (
GetSpecialFolder ( CSIDL_MYDOCUMENTS )) +
’ Packages . xml ’;
cdsPackages . SaveToFile (fn , dfXML );
fn := IncludeTrailingPathDelimiter (
GetSpecialFolder ( CSIDL_MYDOCUMENTS )) +
’ Components . xml ’;
cdsComponents . SaveToFile (fn , dfXML );
end ;
end ;

Note that the new code in this section is keyed to the testing of
the chkExportComponents check box. This is where we determine
whether the components data is to be collected, though at this
point, it will be, and the check box is not an active feature.
Inside of the ProcessPackageInfo method we have added more
code, to populate the package data into the dataset:
414 38 Homebrew Tools

if chkExportComponents . Checked then


begin
cdsPackagesFileName . AsString :=
APackage . FileName ;
cdsPackagesName . AsString := APackage . Name ;
cdsPackagesRunTimeOnly . AsBoolean :=
APackage . RuntimeOnly ;
cdsPackagesDesignTimeOnly . AsBoolean :=
APackage . DesigntimeOnly ;
cdsPackagesIDEPackage . AsBoolean :=
APackage . IDEPackage ;
cdsPackagesLoaded . AsBoolean :=
APackage . Loaded ;
cdsPackagesDescription . AsString :=
APackage . Description ;
cdsPackagesSymbolFile . AsString :=
APackage . SymbolFileName ;
cdsPackagesProducer . AsString :=
strProducer [ APackage . Producer ];
cdsPackagesConsumer . AsString :=
strConsumer [ APackage . Consumer ];
end ;

We can see a good deal of useful information in the package data.


As we will see below, there is much less information stored with
the components, but by linking the two we will get much that we
can use to advantage.
And again, in the ProcessComponents method:

procedure TfrmDGHPackageViewer . ProcessComponents (


Const PS : IOTAPAckageServices ;
Const iPackage : Integer ;
Const P: TTreeNode );
resourceString
strComponents = ’ Components ’;
var
N , NN : TTreeNode ;
iComponent : Integer ;
begin
if PS . ComponentCount [ iPackage ] > 0 then
begin
N := tvPackages . Items . AddChildObject (P ,
strComponents , Nil );
for iComponent :=
0 To PS . ComponentCount [ iPackage ] - 1 Do
38.3 Component Collector 415

begin
NN := tvPackages . Items . AddChildObject (N ,
PS . ComponentNames [ iPackage ,
iComponent ], Nil );
if chkExportComponents . Checked then
begin
cdsComponents . Append ;
cdsComponentsRecID . AsInteger :=
cdsComponents . RecordCount ;
cdsComponentsPackageID . AsInteger :=
cdsPackagesRecID . AsInteger ;
cdsComponentsName . AsString := NN . Text ;
cdsComponents . Post ;
end ;
end ;
end ;
end ;

As you can see, in the collection of components, we get only a


name, so it is essential that we link back the package in which it
is contained. From the package, we get the PackageName, FileName,
and Description. And inside the description we will normally find
the name of the publisher, though we may have to fiddle a bit with
that.

38.3.2 Collecting the DFM Files List

The next step is to collect the list of all DFM files in your project
tree. In recent versions of Delphi, this is a very simple operation:

function TdmMain . GetDFMFilesList (


const APath : string ;
const AList : TStrings ): Integer ;
var
count : Integer ;
arr : TStringDynArray ;
s : string ;
begin
AList . Clear ;
arr := TDirectory . GetFiles ( APath , ’*. dfm ’);
count := 0;
for s in arr do
begin
AList . Add (s);
416 38 Homebrew Tools

AddDFMToData (s);
Inc ( count );
end ;
Result := count ;
end ;

Simple as it is, the utility of the resulting list is low, as we get only
an unambiguous filename for each of the files. As it will be helpful
in other processing to have these filenames in a dataset, we will
add a routine to parse the strings and fill the dataset, adding a
unique numeric ID to each record. Although it is mostly concerned
with adding to a dataset.

procedure TdMain . AddDFMToData (


const AFileName : string );
begin
if AFileName <> ’’ then
begin
if not cdsDFMFiles . Active then
cdsDFMFiles . CreateDataSet ;
cdsDFMFiles . Append ;
cdsDFMFilesRecID . AsInteger :=
cdsDFMFiles . RecordCount ;
cdsDFMFilesName . AsString :=
ExtractFileName ( AFileName );
cdsDFMFilesPath . AsString :=
ExtractFilePath ( AFileName );
cdsDfmFilesUFN . AsString := AFileName ;
cdsDFMFiles . Post ;
end ;
end ;

38.3.3 Collect Component Instances

Now that we have a list of all the DFM files, we can scan them
for instances of components. This process is simple: We will scan
each DFM file looking for lines in which the first token is object
These data will accumulate to a dataset in which each record
contains a foreign key for the form, as well as a primary key for
the component type. The records will also contain an instance
count. After collecting these data, we will also populate a dataset
in which we will have one record per component type, with the
count of all instances of that type in the application.
38.3 Component Collector 417

Here you can see the list of DFM files is passed in, and we then
iterate through the list, calling GetUsedCompsFromForm(s); on each
form s:

function TdMain . GetUsedComponents (


AList : string ): Integer ;
var
s : string ;
sl : TStringList ;
begin
Result := 0;
if cdsUsedComps . Active then
begin
cdsUsedComps . Close ;
cdsUsedComps . CreateDataSet ;
end ;
sl := TStringList . Create ;
try
sl . CommaText := AList ;
for s in sl do
GetUsedCompsFromForm (s);
finally
sl . Free ;
end ;
GetStats ;
end ;

After completing this collection, we call GetStats to collect some


items of interest, as will be shown below.
The collection process is also relatively simple.
▶ We iterate through all lines the DFM file.
▶ We locate on the RecID of the current DFM file, and the
PackageID and Name of the component type. We will permit
only one record per type per package per DFM file.
▶ For each component type, if it has already been found we
increment the instance count. Otherwise, we add the compo-
nent type to our collection.
Here are the operations executed on each form to collect the
component types data:

procedure TdMain . GetUsedCompsFromForm (


const AForm : string );
var
dfmID : Integer ;
418 38 Homebrew Tools

arr : TArray < string >;


found : Boolean ;
s: string ;
sl : TStringList ;

procedure AddCompToData ( const AComp : string ;


found : Boolean );
var
pID : Integer ;
begin
// If can ’t find in a package , don ’ t log it .
pID := GetPackageID ( AComp );
dfmID := cdsDfmFilesRecID . AsInteger ;
if found and ( pId >= 0) then
begin
cdsUsedComps . Filter :=
’ DfmID = ’ + QuotedStr ( dfmID . ToString ) +
’ and PackageID = ’ + QuotedStr ( pID . ToString ) +
’ and Name = ’ + QuotedStr ( AComp );
cdsUsedComps . Filtered := True ;
if cdsUsedComps . RecordCount = 1 then
begin
cdsUsedComps . Edit ;
cdsUsedCompsInstances . AsInteger :=
cdsUsedCompsInstances . AsInteger + 1;
FMaxTypesPerForm := Max ( FMaxTypesPerForm ,
cdsUsedCompsInstances . AsInteger );
end
else
begin
cdsUsedComps . Filtered := False ;
cdsUsedComps . Append ;
cdsUsedCompsRecID . AsInteger :=
cdsUsedComps . RecordCount ;
cdsUsedCompsDfmID . AsInteger := dfmID ;
cdsUsedCompsPackageID . AsInteger := pID ;
cdsUsedCompsName . AsString := AComp ;
cdsUsedCompsInstances . AsInteger := 1;
Inc ( FUsedCompTypes );
end ;
cdsUsedComps . Post ;
end ;
end ;

begin
FUsedCompTypes := 0;
38.3 Component Collector 419

FMaxTypesPerForm := 0;

cdsComponents . IndexFieldNames := ’ Name ’;

sl := TStringList . Create ;
try
if FileExists ( AForm ) then
begin
cdsDfmFiles . DisableControls ;
try
cdsDfmFiles . IndexFieldNames := ’ UFN ’;
found := cdsDfmFiles . Locate ( ’ UFN ’, AForm ,
[ loCaseInsensitive ]) ;
finally
cdsDfmFiles . EnableControls ;
end ;
sl . LoadFromFile ( AForm ) ;
for s in sl do
begin
if s. Contains ( ’ object ’) then
begin
arr := s. Trim . Split ([ ’ ’]) ;
if Length ( arr ) = 3 then
AddCompToData ( arr [2] , found )
else
AddCompToData ( ’ DFM Error ’, found );
end ;
end ;
end ;
finally
sl . Free ;
end ;
end ;

This remains fairly simple.


▶ We iterate through all DFM files from cdsDfmFiles.
▶ For each file, we collect the components found.

An important step is in the call to GetPackageID, by which we verify


that the component is in our collection of installed components:

function TdMain . GetPackageID (


const AComp : string ): Integer ;
begin
cdsPackages . DisableControls ;
cdsComponents . DisableControls ;
420 38 Homebrew Tools

try
Result := -1;
if cdsComponents . Locate ( ’ Name ’, AComp ,
[ loCaseInsensitive ]) then
if cdsPackages . Locate ( ’ RecID ’,
cdsComponents . FieldByName (
’ PackageID ’). AsInteger , []) then
Result :=
cdsPackages . FieldByName (
’ RecID ’). AsInteger ;
finally
cdsComponents . EnableControls ;
cdsPackages . EnableControls ;
end ;
end ;

As already mentioned, the parsing is very simple. For each line in


the DFM file, we look for the word object, and then split the line
into the array of strings arr. We expect three items in the array:
the word object, the object name, and the object type. Anything
else is entered as ’DFM Error’, which should never happen.

sl . LoadFromFile ( AForm );
for s in sl do
begin
if s. Contains ( ’ object ’) then
begin
arr := s. Trim . Split ([ ’ ’]) ;
if Length ( arr ) = 3 then
AddCompToData ( arr [2] , found )
else
AddCompToData ( ’ DFM Error ’, found );
end ;

Complexity of Homebrew Tools

DFM parsing is used partly to limit the complexity of the tool,


lest this chapter become a book. You may wish to use it as a
starting point for a tool of your own in which you elect to parse
the PAS files instead.
Keep in mind that when we build tools for use on our projects,
we are rarely planning to make them commercial products, so
we can spend only limited hours on their development. On the
38.3 Component Collector 421

other hand, if you pursue such things as a hobby, or anticipate


the need to use them on several large projects, you may wish
to invest more heavily in their capabilities.

At this point, there is much value in our datasets, but we still need
to aggregate the per file component data into a combined dataset
to obtain the full application view.

38.3.4 Producing the Aggregate Component Data

Collecting the total data on components in the application re-


quires only that we aggregate what we have already stored in the
cdsUsedComps dataset. We start by filling the cdsCompTypes dataset
from the data in cdsUsedComps, and creating only a single record
per type:

procedure TdMain . GetStats ;


var
idx : Integer ;
begin
FMaxTypesPerForm := 0;
FUsedCompTypes := 0;

cdsCompTypes . Close ;
cdsCompTypes . CreateDataSet ;
cdsUsedComps . First ;
while not cdsUsedComps . Eof do
begin
cdsCompTypes . Filter := ’ Name = ’ +
QuotedStr ( cdsUsedCompsName . AsString );
cdsCompTypes . Filtered := True ;
if cdsCompTypes . RecordCount = 1 then
begin
cdsCompTypes . Edit ;
cdsCompTypesInstances . AsInteger :=
cdsCompTypesInstances . AsInteger +
cdsUsedCompsInstances . AsInteger ;
end
else
begin
cdsCompTypes . Filtered := False ;
cdsCompTypes . Append ;
cdsCompTypesRecId . AsInteger :=
cdsUsedCompsRecID . AsInteger ;
422 38 Homebrew Tools

cdsCompTypesName . AsString :=
cdsUsedCompsName . AsString ;
cdsCompTypesInstances . AsInteger :=
cdsUsedCompsInstances . AsInteger ;
end ;
cdsCompTypes . Post ;
cdsUsedComps . Next ;
end ;
cdsCompTypes . Filtered := False ;

cdsCompTypes . IndexFieldNames := ’ Instances ’;


cdsCompTypes . Last ;
FMostUsedType := cdsCompTypesName . AsString ;
FMaxTypesPerForm :=
cdsCompTypesInstances . AsInteger ;

// Get total comps


FUsedCompsTotal := 0;
cdsCompTypes . First ;
while not cdsCompTypes . Eof do
begin
FUsedCompsTotal := FUsedCompsTotal +
cdsCompTypesInstances . AsInteger ;
cdsCompTypes . Next ;
end ;

FUsedCompTypes := cdsCompTypes . RecordCount ;


end ;

After we have collected data into cdsCompTypes, we index the


cdsUsedComps dataset on the component instances, then go to the
last record, and take the type name and the instances count
there. Then, we loop through all the records in cdsUsedComps and
accumulate the instances to acquire the component total. Finally,
from the cdsUsedComps.RecordCount we get the number of types
used.
With all of this data in hand, we now turn to presentation of the
data. Bear in mind that this is a tool created not for sale, but to
benefit your own efforts, as well as to illustrate some of the coding
principles which help to make an application more maintainable
than in much legacy code.
38.4 Separating Wheat and Chaff 423

38.3.5 Putting Together the Pieces

As this project in its entirety is far outside the scope of this book, I
will not further develop the application here. I have demonstrated
the development of elements which could be used in a number of
ways to harvest useful information from your project source tree. In
the current context, the problem is that each level of developments
triggers further ideas, and the project scope explodes. My purpose
is the book, not a huge project, so we will move forward at this point,
and I may consider developing the project fully as a foundation
for another book.

38.4 Separating Wheat and Chaff

In some legacy projects, you may find multiple projects sharing


the same source tree. This adds layers of complexity, as:
▶ It is very difficult to know which units are needed in which
projects.
▶ There may be duplicate unit names with different function,
both active in projects.
I have developed an analysis tool which helps me approach this
sort of complexity. I am not able to share the code, and the user
interface demands some considerable understanding of the tool.
However, I can discuss strategies of attack, both for analysis and
for rework. Here is a broad outline:
▶ Collect the names and paths of all source files in your project
tree. Separate path from filename, and identify the filenames
for which there may be duplicates. If that is an issue in your
project, then you may also wish to collect the file size or line
count, and the last modified date. If there are doppelgangers,
you will have to resolve how to handle them, as they are a
barrier to simple tools.
▶ You may wish to add to the scan of files a check for any forms
which remain in binary format, list them, and provide for
converting them in your tool. All very easily done, and much
less tedious than the manual approach.
▶ Another action will be to check in your project for the presence
of any modules you have identified as not in the map file. If
so, these should be removed.
424 38 Homebrew Tools

▶ Collect from the map file all of the modules which are
contributing to the EXE. You will want to merge that info
with the data from the file tree, to begin to identify modules
which may no longer be needed.
▶ At this point, another possible strategy would be to add code
that can go through the project tree and prepend to each pos-
sibly unused module name a simple prefix, such as a double
underscore. Those modules will then be unknown to your
project. If you are able to build, then these renamed mod-
ules are not needed. Of course, if your source tree contains
multiple projects, then things are more complex.
MapFileStats 39
MapFileStats https://www.delphitools.info/other-tools/ma
pfilestats/ is a small but very effective tool written by Eric
Grange, to whose site the link will take you.

Figure 39.1: MapFileStats Tree

The beauty of the tool is the analysis it accomplishes, and the


simple user interface it presents. Drilling down in the tree affords
a simple view of what is actually being used:

Figure 39.2: MapFileStats StrUtils

Here, for example, we drill into the Delphi StrUtils library, and see
the small number of routines which are used from that:
However, as always, there is bad news to offset the good. Map-
FileStats is a standalone tool, published in executable form, and
source code is not available. As interesting as it may be for explor-
ing a map file, my experience has been that in legacy projects, it is
426 39 MapFileStats

Figure 39.3: MapFileStats fMain

important to be able to export data, as it will be referred to often


during code rework. Still, MapFileStats is a useful small tool, and
may inspire someone to take the concept and develop it further.
MMX 40
MMX (https://www.MMX-delphi.de/), formerly ModelMaker Code 40.1 Add Class . . . . . . . 429
Explorer, is easily one of the most important plug-ins in the Delphi 40.2 Add Field . . . . . . . 429
arsenal.
40.3 Add Method . . . . . 430
40.4 Sort Class . . . . . . . 431
40.5 Synchronize Signa-
tures . . . . . . . . . . 432
40.6 Swap Scopes . . . . . 432
40.7 Unit Dependency
Analyzer . . . . . . . . 433
40.8 Summary . . . . . . . 434

Figure 40.1: MMX Main Menu

The various Add operations have the particular benefit that you
may invoke them wherever you are in the class, and they will do
their work without your needing to go elsewhere in the code. The
428 40 MMX

MMX Code Explorer can be docked in the IDE, and presents a


very useful view of the form:

Figure 40.2: MMX Code Explorer

Among other things, the Code Explorer provides another way of


navigating the code in the editor.
40.1 Add Class 429

40.1 Add Class

It’s not that adding a class is a difficult thing, but using the wizard
from MMX makes the result more consistent, and it may help
you to remember details which might sometimes otherwise be
overlooked. The Add Class wizard is a simple and clean way to
ensure consistency in the details of your coding. Details can make
or break your programming, and the wizard helps with those. You
will see this, or similar:

Figure 40.3: MMX Add Class wizard

40.2 Add Field

Adding a field is simple, but with different details than for a class,
and once again, the wizard makes it more likely to be consistent
in your coding.
430 40 MMX

Figure 40.4: MMX Add Field Wizard

40.3 Add Method

Adding a method, we can see finally that there are sufficient


possibilities that the wizard really comes into its own. It has been
my experience that using these minor aids helps me to be more
consistent in the way I add to classes.
As you can see, the wizard presents the range of possibilities in
the creation of a method. You may find that this helps you to
remember the details in your method creation. It is also likely to
make your coding more consistent. Bear in mind, too, that the
global settings for MMX will affect some of the defaults you see in
the wizard.
40.4 Sort Class 431

Figure 40.5: MMX Add Method Wiz-


ard

40.4 Sort Class

I routinely sort my classes in new code. In legacy code, you will


certainly need to discuss with your team, as the effect on merging
changes is not trivial. And in spite of that, I find it hugely simpler
to keep my class members sorted in alphabetical order. The MMX
options, however, allow some choices:
You may wish to experiment a bit, especially with Sort Class
Interface by and Sort Class Implementation by, to find which
choices are most comfortable in practice. Less expected, perhaps,
are the options for Sorting hints, and Source Regions. Also note
that when you sort a class, the comments immediately above the
method signature will be relocated with the signature itself.
Consistency of form is a real value, especially in applications
432 40 MMX

Figure 40.6: MMX Sorting Options

where teams will be active now and for the foreseeable future. I
contend that asserting a set of options for the project is a benefit,
even when there may be team members who dissent from the
standard choices. Finding things where you expect them to be, in
unit after unit, is really helpful.

40.5 Synchronize Signatures

It is not uncommon to add a parameter to an existing routine.


MMX affords two tools which are beneficial in this area. One is to
edit the entity—the routine in question—and the other is to simply
add the parameter where you are, and then use the Ctrl-Alt-Y hot
key to synchronize the other (interface or implementation) to your
change. Once again, you are able to do this without moving tour
cursor.

40.6 Swap Scopes

MMX provides a very simple means of switching a unit reference


between the interface and implementation sections of a module.
40.7 Unit Dependency Analyzer 433

You place the cursor in the unit specifier, and use Ctrl-Alt-Shift-
Up/Dn. Note that when you are using RDP
to access a remote development en-
vironment, this hot key combination
is not functional. You may, however,
40.7 Unit Dependency Analyzer assign a different hot key to the oper-
ation.

Analysis of unit dependency relationships is always worthwhile.


Unit Dependency Cycles will degrade build times, and although
they may not always be avoided, most such result from design
errors.
MMX produces a lot of data from the Unit Dependencies tool,
and in a large legacy application, it may tend to be overwhelming.
Here is a map of such relations: The chart is a cartesian map of

Figure 40.7: MMX Unit Dependency


Analyzer

Uses and Used By relations, and I have not found it helpful in


determining where to take action. However I have not found any
tools which helped much in pointing the way to reduced cycles.
An example of content from the dependencies report is here:
Cycles for AbExcept (1)
AbExcept,AbUtils,AbExcept

Cycles for AbUtils (1)


434 40 MMX

AbUtils,AbExcept,AbUtils

Cycles for AdvOfficePager (1)


AdvOfficePager,AdvOfficePagerStylers,AdvOfficePager

Cycles for AdvOfficePagerStylers (1)


AdvOfficePagerStylers,AdvOfficePager,
AdvOfficePagerStylers
The file is easy to parse, and I have built a simple tool to do that,
which you can see in section 38.1.3.

40.8 Summary

MMX is such a rich tool that I can hope only to provoke your
interest enough that you will try it. Any attempt at complete
documentation would lead to the production of another book, and
is thoroughly impractical in this volume.
Peganza Products 41
41.1 Overview . . . . . . . 435
41.1 Overview
41.2 Pascal Analyzer . . . 436
41.3 Pascal Expert . . . . . 438
Peganza (https://peganza.com) is a Swedish software company
which specializes in tools for Delphi developers. At this writing, 41.4 Pascal Browser . . . . 439
they offer three products:
▶ Pascal Analyzer, which does what its name suggests, and was
first released in 2001.
▶ Pascal Expert, which is a plug-in to Delphi and provides
analytical operations inside the IDE. Released in 2015.
▶ Pascal Browser, which is a documentation tool, and was re-
leased in 2007.
▶ Pascal Analyzer Lite, a reduced version of Pascal Analyzer,
available for free.
Peganza offers trial versions of its products, making it very easy to
explore what they can do for you.
Pascal Analyzer provides some analyses not available from other
tools. In particular, it is the only tool I have used which attempts to
identify unit references in the interface uses clause which can be
demoted to the implementation uses clause. I say attempts because
I have found some cases in which the demotion is not possible,
but these soon become familiar, and as any action taken requires
human intervention, are easily left alone.
Pascal Expert provides information about your code which it
presents in the Messages pane in the IDE. It spirit, it is similar to
FixInsight, though it appears to cover a broader range of concerns.
The documentation declares that the results match those obtained
form Pascal Analyzer, but of course, with greater immediacy than
through the external operation of Pascal Analyzer.
Pascal Browser is a tool which analyzes all your code modules in a
project and creates from them a collection of html files—possibly
a massive collection—which you can then browse or use as a
starting point for creating a developer’s manual.
436 41 Peganza Products

41.2 Pascal Analyzer

Peganza has offered the Pascal Analyzer since 2001, and it can
provide an overwhelming collection of analytical information
about the state of your code. One report I have found most useful
flags the units in interface uses clauses which could be demoted
to the implementation uses clause. It is the only tool I have used
which can do this.

Figure 41.1: Pascal Analyzer

There is online documentation (https://peganza.com/PALHelp/


index.html), as well as a downloadable PDF.
An example of a single entry from a uses report is here:
Program EnumConst uses:

Used units:
System source not found
Forms source not found
fMain in implementation
==> FormLogic unnecessary
==> uInterfaced unnecessary
The FormLogic and uInterfaced members of the interface uses
clause are not needed, and may be removed.
41.2 Pascal Analyzer 437

▶ The ==> symbol is used in the report to indicate a unit


reference which may safely be removed.
▶ The –> symbol indicates the corresponding unit may be
moved to the implementation section of the unit.

In practice, you are likely to find that these symbols may be in


error. There seems, for example, to be a problem recognizing that
a class helper unit is actually needed.
If your project is large, you will want to use the 64-bit version of
Pascal Analyzer and ensure before you start that you have sufficient
RAM available. In my VM, I boosted the RAM to 8GB, which was
more than sufficient, and found that at peak, Pascal Analyzer con-
sumed about 4GB of memory with no other applications loaded,
and kept three cores at 3.9GHz saturated during production of
reports. Also note that producing reports will take more time than
the parsing of your project. In my experience, if I generate reports
based on the default settings, I will need to wait a while before
the job is complete, and for most of that time, the VM is not useful
for other activity. Do close your other apps, however, as it will
help. The latest releases appears to have significantly improved the
performance in report generation, which is welcome. Generating
32 reports took about six minutes, though in the previous release,
and with the IDE running, it took well over two hours.
Note that Pascal Analyzer Lite produces up to 30 different reports,
and presents an overwhelming collection of useful information.
Pascal Analyzer adds 20 more reports to that, and makes itself that
much more useful. In considering these tools, bear in mind that
running an analysis on a large project with many of the reports
selected may take several hours, depending on your machine
resources. If you anticipate frequent report updates, you will likely
want to reduce the selection to the most essential of the reports.
Support goes all the way back to Borland Pascal 7, so this is certainly
a tool for use with legacy projects. The product is frequently
updated, and the publisher is responsive to queries.
Pascal Analyzer presents an Optimal Uses List, which I have found
to be less than reliable. In fairness, my experience was on a
large project with over a hundred units which had initialization
sections, some of which contained calls to initialize other modules.
As I discussed earlier in the book, I found it necessary to use
logging to establish the actual initialization sequence. Once that
438 41 Peganza Products

was done, I built an initialization module to make that sequence


deterministic.
One of the reports available in Pascal Analyzer but not in Pascal
Analyzer Lite is the Control Index report. This report presents a
full list of the visual controls used in your project, which can be
useful in a number of contexts.
My point is not to criticize Pascal Analyzer, but to point out that
analytical tools for Delphi are operating on the fringe of what we
know about Delphi. Given the complexity of the language, it is
hardly surprising that you can create code which leads to incorrect
answers. I would suggest that the challenge is increased by the
presence of UDCs.

41.3 Pascal Expert

My experience with Pascal Expert is relatively recent, and I will


not attempt to render any detailed comparison between it and
FixInsight, as it would be less balanced than I would like.
The great value in a tool like Pascal Expert is that it is quick and
easy to use each time a module is altered, and before check-in. If
you make a practice of this, it will surely help you to avoid small
ills and prevent them from becoming larger issues. This step adds
very little burden in most cases, and is a very easy way to keep
checking the details, even as you may be distracted with the twists
and turns of awkward logic.
The Options dialog in Pascal Expert is extensive:
Also, Pascal Expert allows the individual items it would identify to
be enabled or disabled, making it convenient when you wish to
focus on a particular category of concern.
As there are also pages for Reductions, Optimizations, and Con-
ventions, there is much room here for you to make whatever
selections concern you most. An argument could be made that
all options should be enabled, at least as the project approaches
being production-ready, but that is likely to incite arguments, and
yield little value.
41.4 Pascal Browser 439

Figure 41.2: Pascal Expert Options,


General

Figure 41.3: Pascal Expert Options,


Alerts

41.4 Pascal Browser

Pascal Browser is the tool furthest from my daily interests and


concerns. It performs an extensive analysis of the project on which
it is run, and that means, for one thing, that it takes time to do
its work. On the large project which is my current focus, running
Pascal Browser took several hours. That is not a criticism, but
an observation. Any tool with so much overhead will be used
infrequently. On the other hand, when you need such a result,
then that overhead is a small price to pay.
As the Peganza website says, Pascal Browser can:
▶ Create a hyperlinked collection of HTML documents for your
source code
440 41 Peganza Products

▶ Create a full-text searchable CHM file for the hyperlinked


collection of HTML documents
▶ Help you understand and get an overview of your code
▶ Assist you in finding errors and anomalies in the source code
▶ Let new team members quickly get acquainted with the
source code
▶ Create your own customized documentation

Again, this is a tool which you will be unlikely to run often, but its
capabilities will give you a great start toward creating developer
documentation.

"Automatic" Documentation Generation


The following rant is not a criticism of Pascal Browser, but is
intended to be understood as a comment on contemporaneous
practice in many software shops.
A rant on documentation: In practice, it is very unusual to find
well commented code. Common failings include:
▶ Restating code in comments.
▶ Failing to explain motivation—not all code is obvious.
▶ Errors in spelling, grammar, and clarity.
▶ Dependence on badly named routines.
All of these make it unlikely that extraction from comments
will communicate well to new team members, or indeed, to
anyone. None of these is an impediment to the compiler; all are
impediments to humans who must follow in your footsteps.
Unless your codebase is remarkably different to most, the
content of comments will do no more than to signal the need
for someone human to actually explain the operation of, and
need for, the relevant code.
Embedded rant: It is hugely ironic that developers, who live and
die by correct syntax in their computer languages, seem oblivious to
the need for correct grammar and syntax in comments and documen-
tation.
It has been my experience that much contemporary "documen-
tation" is useful only as a reference for someone who already
understands the operation of the code. As an introduction to a
new team member, it is usually deficient.
41.4 Pascal Browser 441

End of rant.

As with Pascal Analyzer, you will want to provide substantial


resources to Pascal Browser, as it will exercise your CPU heavily,
and will benefit from large available memory. These cautions, of
course, are less a concern if you run directly on your PC than if
you are working in a VM.
ProDelphi 42
ProDelphi is a very good code profiling tool. In large projects, you 42.1 Profiling Some
must do some work to exclude files from the process, as it is limited Demo Code . . . . . 443
to 64,000 methods. It is an instrumenting analyzer, and reliably 42.1.1 Considering
removes its instrumentation, when cleanup is invoked. ExportDataSetToCSV 446
42.2 Profiling, in General 447
42.2.1 Why Profile, Why
Optimize? . . . . . . 447

Figure 42.1: ProDelphi Viewer

There are freeware and professional versions of the profiler, so


you can easily try it out. Versions are offered for both 32-bit and
64-bit, and updates are frequent.

42.1 Profiling Some Demo Code

Profiling is always interesting, but much more when the source


code is available. The ZipCodes project which is in the demo
repository for the book is small and simple, but worth looking at
in ProDelphi.
The application needs to load a CSV file of Zip codes (also in the
repository) and then do some minor processing on the loaded
dataset, finally populating a DBGrid with the three columns we
444 42 ProDelphi

extract from the input: Zip, City, and State. Note that the input
file contains multiples of some Zip codes where these are used for
multiple cities. The program copies only unique Zip/City pairs,
so there will be multiple copies of some Zip codes, but far fewer
than in the input file.
Using the 32-bit version of ProDelphi, and profiling the 32-bit
version of the ZipCodes project yields this result:

Figure 42.2: ProDelphi ZipCodes

The chart has been sorted to present in order of percentage of


compute time. We can ignore btnSelInFileClick, as the user inter-
action is the primary overhead in that routine. The first interesting
item is ProcessZipCodes, but in the existing code, there is so much
in the routine that all we really learn from the profiler is that a lot
of work is done there.

procedure TdMain . ProcessZipCodes ;


var
sCity , sZip : string ;
begin
fdmZipCodesRaw . First ;
fdmZipCodesRaw . DisableControls ;
try
fdmZipCodes . CreateDataSet ;
fdmZipCodes . IndexFieldNames := ’ Zip ; City ’;
while not fdmZipCodesRaw . Eof do
begin
sZip := fdmZipCodesRaw . FieldByName (
’ zip code ’). AsString ;
sCity := fdmZipCodesRaw . FieldByName (
42.1 Profiling Some Demo Code 445

’ city ’). AsString ;


if not fdmZipCodes . FindKey (
[ sZip , sCity ]) then
begin
fdmZipCodes . Append ;
fdmZipCodesZip . AsString := sZip ;
fdmZipCodesCity . AsString := sCity ;
fdmZipCodesState . AsString :=
fdmZipCodesRaw . FieldByName (
’ state ’). AsString ;
fdmZipCodes . Post ;
end ;
fdmZipCodesRaw . Next ;
end ;
finally
ExportDataSetToCSV ( fdmZipCodes ,
FFilePath + ’ WOZipCodes . csv ’);
fdmZipCodes . SaveToFile (
FFilePath + ’ WOZipCodes . json ’,
TFDStorageFormat . sfJSON );
fdmZipCodesRaw . First ;
fdmZipCodesRaw . EnableControls ;
end ;
end ;

So what can be done to improve the performance? Ignore for now


that as this routine would only rarely be used in the real world,
there is no compelling reason to tweak performance. The point
now is that the app is sufficiently small to be a useful discussion
piece.
There are a few items worth a closer look.
▶ FieldByName is used because we are not assigning fields in
fdmZipCodesRaw. If the CSV file columns will not change, we
can define columns statically, and then we can access columns
as Fields[index], which will be faster.
▶ There are alternatives to calling FindKey, and you may wish
to explore them. One option would be to append all records,
then index and remove the duplicates. It may be faster, but
you will need to profile to learn the answer.
▶ GetMaxCityLength may not be needed in your work. It is
arguably out of place here, as the sole purpose of the routine
is to support resizing the City column in the output data,
so that the grid columns will be no wider than necessary.
446 42 ProDelphi

Note that in the demo, however, it is called from the LoadData


routine, which is intended to serve the form in this app.
▶ The ExportDataSetToCSV routine may be worth some attention
with respect to optimization. See below.
▶ If you need only one form of output, then save either to CSV
or to JSON. The former will be much smaller than the latter.

42.1.1 Considering ExportDataSetToCSV

The ExportDataSetToCSV routine is taken from an article on Uwe


Raabe’s blog: https://www.uweraabe.de/Blog/2013/11/06/po
or-mans-csv-export/. Uwe does not mention whether he tuned
the performance, but he does point out that he could have used
the TFDDataMove component in its place, had there not been a bug
in that code. Though that defect was repaired long ago, I have
used Uwe’s code here.

procedure ExportDataSetToCSV ( DataSet : TDataSet ;


const FileName : string );
var
fld : TField ;
lst : TStringList ;
wasActive : Boolean ;
writer : TTextWriter ;
begin
writer := TStreamWriter . Create ( FileName );
try
lst := TStringList . Create ;
try
lst . QuoteChar := ’" ’;
lst . Delimiter := ’; ’;
wasActive := DataSet . Active ;
try
DataSet . Active := true ;
DataSet . GetFieldNames ( lst );
writer . WriteLine ( lst . DelimitedText );
DataSet . First ;
while not DataSet . Eof do begin
lst . Clear ;
for fld in DataSet . Fields do
lst . Add ( fld . Text );
writer . WriteLine ( lst . DelimitedText );
DataSet . Next ;
end ;
42.2 Profiling, in General 447

finally
DataSet . Active := wasActive ;
end ;
finally
lst . Free ;
end ;
finally
writer . Free ;
end ;
end ;

Having pointed out some possibilities, the rework and profiling is


left as an exercise for the reader.

42.2 Profiling, in General

As cited earlier, Donald Knuth has said that Premature optimization


is the root of all evil.
In most applications, there are areas in which absolute perfor-
mance is of no great consequence, and some areas in which the
user perception of the program may be adversely affected by
performance issues. Knuth’s point addresses the problem of jump-
ing in too soon — and this also touches on the issue of wrongly
targeting code for optimization.
I recall someone suggesting at one time that most developers are
wrong most of the time in guessing where to apply optimization,
but have been unable to find a source or a definite quote.
The reality is that investing in optimization without first profiling
is foolish and wasteful.

42.2.1 Why Profile, Why Optimize?

Although I have so far presented optimization as a matter of


execution speed or time. But there are also situations in which
you may be driven to optimize to conserve system resources. We
are easily spoiled these days by cheap RAM, but users do not all
invest in RAM as developers do.
A few decades ago, I used a very capable text editor in the Z80
environment which was capable of editing a file of up to a megabyte
448 42 ProDelphi

of text. These days, it is very common for developers to simply


assume the limit on editor file size is going to be the limit of
RAM space, or address range. That old editor made good use of
virtual memory to overcome the limits of an OS limited to 62KB
or RAM.
In approaching program optimization, you must determine the
driving factors for your application and customer base.
TestInsight 43
TestInsight is a powerful addition for testing. It is not a test frame-
work, but a plugin which supports the Delphi test frameworks:
▶ DUnit
▶ DUnit2
▶ DUnitX

As it says in the Wiki, you gain these benefits:


▶ No need to run tests externally anymore, and results are
shown in the IDE.
▶ Run tests from the code editor and navigate to the failing
code from the results overview.
▶ Continuously runs your tests.

In simple terms, TestInsight is an alternative to the old GuiRunner


which works with any of DUnit, DUnit2, or DUnitX, and adds
functionality not present in the test frameworks themselves. TestIn-
sight an IDE plug-in, as well, so is operationally different than the
GuiRunner in that respect.
An obvious benefit of this approach is to make the application
of tests so easy that team members are likely to use testing more
routinely.
The Wiki also offers this simple example:

program Project1 ;

uses
TestInsight . Client ,
TestInsight . DUnit ,
GUITestRunner ,
SysUtils ;

function IsTestInsightRunning : Boolean ;


var
client : ITestInsightClient ;
begin
client := TTestInsightRestClient . Create ;
client . StartedTesting (0) ;
Result := not client . HasError ;
450 43 TestInsight

end ;

begin
if IsTestInsightRunning then
TestInsight . DUnit . RunRegisteredTests
else
GUITestRunner . RunRegisteredTests ;
end .

When you click View, TestInsight Explorer, you get this:

Figure 43.1: TestInsight Explorer

Now lets look at a small sample program which you can download
from the repository for this book. The program is very similar to
one which Stefan Glienke used in a youtube video on TestInsight
in 2015. First, let’s look at the main code unit, where the test class
is declared.

unit u_Test ;

interface

uses
DUnitX . TestFramework ;

type
[ TestFixture ]
TwmTestObject = class ( TObject )
public
[ TestCase ( ’1 and 1 ’, ’1 ,1 ,2 ’)]
[ TestCase ( ’1 and 2 ’, ’1 ,2 ,3 ’)]
[ TestCase ( ’2 and 2 ’, ’2 ,2 ,4 ’)]
451

[ TestCase ( ’ -1 and 1 ’, ’ -1 ,1 ,0 ’)]


[ TestCase ( ’ -1 and -1 ’, ’ -1 , -1 , -2 ’)]
procedure TestAdd ( Val1 , Val2 ,
Expected : Integer );
[ TestCase ( ’’,’’)]
procedure TestFail ;
[ TestCase ( ’’,’’)]
procedure TestPass ;
end ;

var
wmTestObject : TwmTestObject ;

implementation

uses
u_UnderTest ;

procedure TwmTestObject . TestAdd ( Val1 , Val2 ,


Expected : Integer );
var
Actual : Integer ;
begin
Actual := DoAdd ( Val1 , Val2 );
Assert . AreEqual ( Expected , Actual , ’ Failed ! ’);
end ;

procedure TwmTestObject . TestFail ;


begin
Assert . Fail ( ’ Oops !! ’);
end ;

procedure TwmTestObject . TestPass ;


begin
Assert . Pass ( ’ This works ! ’);
end ;

initialization
TDUnitX . RegisterTestFixture ( TwmTestObject );
end .

Now we must look at the unit being tested, which implements


only the DoAdd procedure:

unit u_UnderTest ;
452 43 TestInsight

interface

function DoAdd ( const X , Y: Integer ): Integer ;

implementation

function DoAdd ( const X , Y: Integer ): Integer ;


begin
Result := 2;
end ;

end .

There is not much there, but it is sufficient for this discussion.


The test cases were registered in attributes:

[ TestCase ( ’1 and 1 ’, ’1 ,1 ,2 ’)]


[ TestCase ( ’1 and 2 ’, ’1 ,2 ,3 ’)]
[ TestCase ( ’2 and 2 ’, ’2 ,2 ,4 ’)]
[ TestCase ( ’ -1 and 1 ’, ’ -1 ,1 ,0 ’)]
[ TestCase ( ’ -1 and -1 ’, ’ -1 , -1 , -2 ’)]

The meaning of these is simple: Two strings are defined in each.


The first gives us the name of the test; the second provides the test
parameters. So in the first one, we see ’1 and 1’ as the test name,
and the parameters ’1,1,2’ where the first two are the inputs, and
the third is the expected result.
However, looking at DoAdd, we see it gives only a single answer,
ignoring the passed arguments altogether:

function DoAdd ( const X , Y: Integer ): Integer ;


begin
Result := 2;
end ;

Clearly, this does not resemble tests you are likely to write for
your projects. Yet we must begin somewhere, and it is useful to
present the simplest of code if it can still illustrate the process. You
may wish to add functions for subtraction, multiplication, and
division.
Now when we run the test in TestInsight, the first test passes
because 1 + 1 = 2. The other four tests of addition fail, because the
expected results are always something other than 2:
453

Figure 43.2: TestInsight Explorer

With this simple program in place, you can explore the behaviors
of the TestInsight Explorer very easily. It is a replacement for the
DUnit GUIRunner, but it is more. The icons at the top of the
Explorer allow you to alter the presentation. If, for example, we
change from the default of displaying result by type to displaying
by fixture, we see this:

Figure 43.3: TestInsight Explorer

A great benefit of TestInsight is that it supports all three of the unit


test frameworks: DUnit, DUnit2, and DUnitX. And doing so in
a single user interface brings consistency of use, so that the tool
becomes transparent to your work.
Bibliography

Here are the references in citation order.

[1] Walter S. Brainerd. Pascal Programming: A Spiral Approach. Boyd & Fraser Pub. Co., 1982.
[2] Julian Bucknall. ‘Creating Easily Testable User Interfaces’. In: The Delphi Magazine (Aug.
2005).
[3] Delphi Component Writer’s Guide. Borland Software Corporation, 1995.
[4] Michael C. Feathers. Working Effectively with Legacy Code. Prentice Hall, 2004.
[5] Martin Fowler. Refactoring. Addison-Wesley, 1999.
[6] Primož Gabrijelčič. Delphi High Performance. Packt, 2018.
[7] Primož Gabrijelčič. Hands-On Design Patterns with Delphi. Packt, 2019.
[8] David Gries. Compiler Construction for Digital Computers. John Wiley and Sons, 1971.
[9] Eric Harmon. Delphi COM Programming. Macmillan Technical Publishing, 2000.
[10] Nick Hodges. Coding in Delphi. Nepeta Enterprises, 2014.
[11] Nick Hodges. Dependency Injection in Delphi. Trenchant Publishing, 2017.
[12] Donald E. Knuth. ‘Structured Programming with goto Statements’. In: Computing Surveys
(Dec. 1974).
[13] Bertrand Meyer. Object-Oriented Software Construction. Prentice Hall, 1988.
[14] Charles Petzold. Programming Windows®, Fifth Edition (Developer Reference). Microsoft Press,
1998.
[15] Dalija Prasnikar and Neven Prasnikar Jr. Delphi Memory Management. CreateSpace Indepen-
dent Publishing Platform, 2018.
[16] Nicklaus Wirth. Algorithms + Data Structures = Programs. Prentice Hall, 1976.
Alphabetical Index

$ZEROBASEDSTRINGS, Error constructor, 306,


207 Out of Memory, 12 307
Error Insight, 9 method, 304, 306,
aggregation, 167, 278, EurekaLog, 217 308
285 Excel, 83 property, 306, 307
AOwner, 101 except, 53, 58 interface, 74, 85, 232
ISO-8601, 269
Buy-In, 11
finalization, 78
Castalia, 62 FinalizeUnit, 78 JUnit, 347
class and record helpers, finally, 53
FixInsight, 125, 130, 188 Language Server Protocol
201, 202, 204, 208,
(LSP), 9, 19
268, 294
GEXP, 80 logging, 74, 76
CnPack, 13, 62, 83
GExperts, 13 logic reduction, 162
CnPack Uses Cleaner, 85
Code Completion, 9 Replace Component,
magic numbers, 40
Code Insight, 9 13
MMX, 63, 83, 85, 88
code smell, 125 global variables, 94, 101
MMX Unit Dependency
CodeInsight, 13 goto, 201
Analyzer, 87
COM, 221 GUID, 227
MSBuild, 36
Component Writer’s
Guide, 145 hints and warnings, 90
naming
composition, 167, 278, prefixes, 223
285 Idioms
nested routines, 166
coupling, 239, 256, 289 Creation/Destruction,
CSV, 299 290 Object Inspector, 45
Helpers, 294 Object Pascal Style Guide,
Delphi If/Else, 293 61
Component Writer’s Smart Pointers, 295 OnGetText, 47
Guide, 12 IInterface, 221 OOP, 15, 285
Dependency Container, implementation, 45, 74, 83,
282 85 Pascal Analyzer, 74, 83,
Dependency Injection, 279, information hiding, 168 85
281 inheritance, 285, 286 Patterns
Dependency Inversion, inheritance, multiple, Adapter Pattern,
279 286 299
destructor, 57, 102 initialization, 78 Dependency Injection,
DLL, 218, 219 InitializeUnit, 78 300
DRY, 237 injection Facade Pattern, 299
Principles range checking, 37, 100, TFDMemTable, 212
Dependency 129 TField, 211
Inversion, 273, refactoring, 10, 11, 15, 20, TFloatField, 214
278 25, 48, 49, 59, 63, The Delphi Magazine, 46
GRASP, 273 71, 84, 89, 125, Third Normal Form, 213
Interface Segregation, 129, 131, 132, 143, TIntegerField, 214
273, 277 148, 151, 155–158, TInterfacedObject, 221
Least Astonishment, 161, 163, 164, 166, TNumericField, 277
265 178, 180, 181, TQuery, 254
Liskov Substitution, 183–185, 188–190, try/except, 90
273, 276 194, 215, 219, 247, try/finally, 38, 55, 57, 90,
Open/Closed, 273, 249, 257, 258, 269, 267, 290
275 277, 283, 289, 303, TStringField, 214
Separation of 311, 314, 321, 339, TStringHelper, 268
Concerns, 41, 359, 388 TStringList, 268, 287, 301
160 resourcestring, 260 Turbo Pascal, 39
Single Responsibility, ROI, 11
Unicode, 92
17, 153, 167, 254,
ShortString, 219 Unit dependency cycles, 15,
273–275, 287
SOLID, 237 18, 19, 49, 50, 71,
SOLID, 273
Spring4D, 298, 301 84, 86, 87, 142,
private, 45
Strict private, 45 150–153, 163, 172,
profiling, 245
subrange, 129 246, 255–258,
property, 102, 172, 307
289, 343, 344, 433,
DisplayFormat, 213, TClientDataset, 212, 254
438
277 TCurrencyField, 214
unit testing, 240
DisplayLabel, 213 TDataset, 212
protected, 45 TDataSource, 254 with, 201
public, 45 TDBGrid, 213, 254
published, 45 TDD, 237 YAGNI, 237, 269, 277

You might also like