This content was uploaded by our users and we assume good faith they have the permission to share this book. If you own the copyright to this book and it is wrongfully on our website, we offer a simple DMCA procedure to remove your content from our site. Start by pressing the button below!
About the Author Danijel Arsenovski is a software developer from Santiago, Chile. Currently, he works as Product and Solutions Architect at Excelsys S.A, designing Internet banking solutions for numerous clients in the region. He started experimenting with refactoring while overhauling a huge banking system, and he hasn’t lost interest in refactoring ever since. He pioneered the use of refactoring as a vehicle for a VB 6 code upgrade to VB .NET. Arsenovski is a contributing author for Visual Studio Magazine and Visual Systems Journal, holds a Microsoft Certified Solution Developer (MCSD) certification, and was named Visual Basic MVP in 2005. You can reach him at [email protected], and you can take a look at his blog at http://blog.vbrefactoring.com.
79796ffirs.qxd:WroxPro
2/22/08
4:57 PM
Page vii
Credits Acquisitions Editor
Vice President and Executive Group Publisher
Katie Mohr
Richard Swadley
Senior Development Editor
Vice President and Executive Publisher
Kevin Kent
Joseph B. Wikert
Technical Editor
Project Coordinator, Cover
Doug Holland
Lynsey Stanford
Copy Editors
Compositor
S. B. Kleinman Mildred Sanchez
Craig James Woods, Happenstance Type-O-Rama
Editorial Manager
Proofreader
Mary Beth Wakefield
Word One
Production Manager
Indexer
Tim Tate
Johnna VanHoose Dinse
79796ffirs.qxd:WroxPro
2/22/08
4:57 PM
Page viii
Acknowledgments A number of people deserve credit for helping me out in getting this book to the finish line. I am quite aware that dealing with a first-time writer is always a challenge and probably means much more work than usual. So let me start with my Wrox team. I want to thank Kevin Kent, my development editor, for always being there for me and for his meticulous work. He was always there with exactly the right dose of guidance. I want to thank Katie Mohr for believing in this project right from the outset. Thanks to Doug Holland, my technical editor, for providing that all-important safety net that every technical author needs. Also thanks to Chris Webb for putting me in contact with the right people at the right moment. I want to especially thank Mark Miller from Developer Express for reviewing a great portion of the manuscript and giving invaluable feedback, particularly on sections related to the Refactor! for VB add-in. Also thanks to the rest of the Developer Express team for giving me full access to their products and for responding to my queries. I want to thank Dusan Miloradovic for his review and excellent feedback. Other people that provided feedback and made me believe I was on the right track are Dan Mabbutt, Sandeep Joshi, Anthony Williams, Rod Stephens, and others. Thanks to Vukan Djurovic for convincing me that there is always a better way to write some code and to Diego Dagum for his interest in my work. Finally, thanks to Milos Milosavljevic for introducing me to the world of Visual Basic.
79796ftoc.qxd:WroxPro
2/23/08
8:45 AM
Page ix
Contents Acknowledgments Foreword Introduction
viii xix xxi
Part I: Introduction to Refactoring
1
Chapter 1: Refactoring: What’s All the Fuss About?
3
A Quick Refactoring Overview The Refactoring Process A Look at the Software Situation
The Refactoring Process: A Closer Look Using Code Smells Transforming the Code The Benefits of Refactoring Debunking Common Misconceptions
Visual Basic and Refactoring Visual Basic History and Legacy Issues Visual Basic Evolution Dealing with Legacy Issues Through Refactoring
Summary
Chapter 2: A First Taste of Refactoring Calories Calculator Sample Application Calories Calculator Application Growing Requirements: Calculating Ideal Weight Growing Requirements: Persisting Patient Data
Refactoring in Action
4 4 5
7 7 8 9 12
14 14 14 16
16
19 19 20 22 24
26
Decomposing the BtnCalculate_Click Method Discovering New Classes Narrowing the Patient Class Interface Putting Conditional Logic in the Patient Class Creating the Patient Class Hierarchy
27 29 32 34 37
Implementing the Persistence Functionality
43
Saving the Data Implementing Patient-History Display Functionality
Calories Calculator, Refactored Version Summary
44 52
56 58
79796ftoc.qxd:WroxPro
2/23/08
8:45 AM
Page x
Contents Chapter 3: Assembling a Refactoring Toolkit Using an Automated Refactoring Tool
60
ReSharper from JetBrains Visual Assist X from Whole Tomato Refactor! Pro from Developer Express Getting Started with Refactor! Exploring Refactor! for the VB User Interface Quick Tour: Available Refactorings
60 61 61 61 63 67
Unit-Testing Basics: The Testing Harness
69
Why a Unit-Testing Framework? Your First Taste of NUnit Installing NUnit Implementing Your First Test The Test-Driven Approach Other Test Tools to Consider
the Manager the Desk Receptionist the Parking Lot Attendant Maintenance Personnel
Taking the Initial Steps in the Rent-a-Wheels Project Actors and Use Cases Vehicle States First Sketch of the Main Application Window Rent-a-Wheels Team Meeting
Making the Prototype Work Examining the Database Model Examining the Visual Basic Code
Fast and Furious, a VB Approach to Programming Database-Driven Design GUI-Based Application Event-Driven Programming Rapid Application Development (RAD) Copy-Paste as a Code Reuse Mechanism
From Prototype to Delivery Through the Refactoring Process Summary
x
59
70 72 72 74 82 83
84 85
87 88 88 89 90 90
91 91 93 94 96
96 96 99
102 102 103 103 104 104
105 105
79796ftoc.qxd:WroxPro
2/23/08
8:45 AM
Page xi
Contents Part II: Preliminary VB Refactorings Chapter 5: Chameleon Language: From Weak Static Typing to Strong Dynamic Typing
107 109
Option Explicit and Option Strict, the .NET Effect Setting Option Explicit On in Relaxed Code
110 111
Understanding the Set Option Explicit On Refactoring Refactoring the Rent-a-Wheels Code to Explicit Form
112 114
Setting Option Strict On in Relaxed Code A Slightly Artificial Example of Permissive VB code Convoluted Use of Variables Resolved by the Definition of New Variables Inferring Variable Type Putting It All Together with Type-Conversion Functions Dealing with Methods, Fields, Properties, and Other Members Applying Set Option Strict On Refactoring to the Rent-a-Wheels Application
Static Versus Dynamic Typing and Visual Basic Late Binding in Visual Basic 6 and Prior Duck Typing Resetting Dynamic or Static Behavior at the File Level Providing a Statically Typed Wrapper for Dynamic Code
Activating Explicit and Strict Compiler Options Setting Options in the Project Properties Changing the Default Behavior of the Visual Basic Compiler Setting Options in Source Files Using Item Templates to Set Options
Summary
115 116 119 122 125 127 132
135 136 136 138 138
141 141 142 143 143
145
Chapter 6: Error Handling: From Legacy to Structured in a Few Easy Steps 147 Legacy Error Handling Versus Structured Error Handling Legacy (Unstructured) Error Handling Structured Error Handling
The Benefits of Structured Error Handling Structured Versus Unstructured Code Exceptions as Types, Not Numbers Error Filtering The Finally Block .NET Interoperability
Replacing the On Error Construct with Try-Catch-Finally Understanding the When Keyword Refactoring Steps for Replacing On Error with Try-Catch-Finally Replacing the On Error Goto Label with the Try-Catch-Finally Construct Replacing On Error Resume Next with the Try-Catch-Finally Construct
148 148 150
153 153 154 154 155 155
156 158 158 159 162
xi
79796ftoc.qxd:WroxPro
2/23/08
8:45 AM
Page xii
Contents Replacing Error Code with Exception Type Replacing System Error Codes with Exception Types Replacing Custom Error Codes with Exception Types
Error Handling in the Rent-a-Wheels Application Application-Level Events in VB 2009
Summary
Chapter 7: Basic Hygiene: Eliminating Dead Code, Reducing Scope, Using Explicit Imports, and Removing Unused References Eliminating Dead Code Types of Dead Code Common Sources of Dead Code
Reducing the Scope and Access Level of Unduly Exposed Elements Scope and Access Level Common Sources of Overexposure Dealing with Overexposure
Using Explicit Imports Imports Section Depicts Dependencies in Your System
Removing Unused Assembly References Basic Hygiene in the Rent-a-Wheels Application Summary
164 166 168
169 169
171
173 174 175 176
179 181 182 186
187 188
191 192 193
Part III: Getting Started with Standard Refactoring Transformations
195
Chapter 8: From Problem Domain to Code: Closing the Gap
197
Understanding the Problem Domain Step Step Step Step
One: Gathering the Information Two: Agreeing on the Vocabulary Three: Describing the Interactions Four: Building the Prototype
Naming Guidelines Capitalization Styles Simple Naming Guidelines Good Communication: Choosing the Right Words Rename Refactoring
Published and Public Interfaces Self-Contained Applications Versus Reusable Modules Modifying the Public Interfaces Safe Rename Refactoring in Refactor!
Rename and Safe Rename Refactoring in the Rent-a-Wheels Application Summary
xii
198 198 199 200 201
201 202 203 204 206
208 209 212 214
217 218
79796ftoc.qxd:WroxPro
2/23/08
8:45 AM
Page xiii
Contents Chapter 9: The Method Extraction Remedy for Duplicated Code
219
Why Keep Code Encapsulated and Details Hidden? Information and Implementation Hiding Decomposing Methods
219 220 223
Circumference Calculation — Long Method Example Extracting Circumference Length Calculation Code Extracting the Radius Calculation Code Extracting the “Wait for User to Close” Code Extracting the Read Coordinates Code Extract Method Refactoring in Refactor!
223 226 229 229 229 233
The Duplicated Code Smell
234
Sources of Duplicated Code Copy-Paste Programming
235 236
Magic Literals Introduce Constant Refactoring in Refactor!
237 239
Extract Method and Replace Magic Literal Refactoring in the Rent-a-Wheels Application Summary
240 240
Chapter 10: Method Consolidation and Extraction Techniques
243
Dealing with Temporary Variables
243
Move Declaration Near Reference Refactoring Move Initialization to Declaration Refactoring Split Temporary Variable Refactoring Inline Temp Refactoring Replace Temp with Query Refactoring
244 248 249 253 256
Method Reorganization Heuristics Method Reorganization and Rent-a-Wheels
258 259
Removing Duplication in Rent-a-Wheels Magic Literals, Comments, and Event-Handling Blindness in Rent-a-Wheels
Summary
Part IV: Advanced Refactorings Chapter 11: Discovering Objects A Quick Object-Oriented Programming Overview What Are Objects Anyway? Encapsulation and Objects Encapsulate Field Refactoring in Refactor! Object State Retention
261 263
267
269 271 272 272 272 274 276
xiii
79796ftoc.qxd:WroxPro
2/23/08
8:45 AM
Page xiv
Contents Classes Object Identity Objects as Basic Building Blocks Root Object Object Lifetime and Garbage Collection Messages
Designing Classes Classes Are Nouns, Operations Are Verbs Classes, Responsibilities, and Collaborators Entities and Relationships
Discovering Hidden Classes Dealing with Database-Driven Design Moving From Procedural to Object-Oriented Design Keeping Domain, Presentation, and Persistence Apart Discovering Objects and the Rent-a-Wheels Application
Summary
Chapter 12: Advanced Object-Oriented Concepts and Related Refactorings Inheritance, Polymorphism, and Genericity Inheritance Polymorphism Genericity
Inheritance Abuse and Refactoring Solutions Composition Mistaken for Inheritance and Other Misuses Refactoring for Inheritance — Print-System Illustration
Making Use of Generics Inheritance and Generic Types in the Rent-a-Wheels Application Extracting Super Employing Generics Extract Data Objects Provider Class
Summary
Chapter 13: Code Organization on a Large Scale Namespaces Naming Guidelines and Namespace Organization Nested Namespaces Changing the Root Namespace Name Using Import Statements
Visual Basic Project File Structure Organization Move Type to File Refactoring in Refactor! Partial Classes Inherited Form Abstract Form Inheritance Delegating Abstract Form Work to a Form Helper Class
Namespace Organization and Windows Forms Inheritance in Rent-a-Wheels
375 375 377 381
387 388 390 390 392 392
394
Extracting Parent Administration Form through Abstract Form Helper Pattern Application 394 Namespace and Assembly Reorganization 402
Summary
Part V: Refactoring Applied Chapter 14: Refactoring to Patterns Design Patterns: What’s All the Fuss About?
403
405 407 408
Defining Design Patterns Classifying Patterns Pattern Elements Weighing the Benefits of Design Patterns Using Patterns
408 409 409 410 410
Example Design Pattern: Abstract Factory
411
Name Problem Solution Consequences
Dependency Injection Pattern Problem Solution Constructor-Based vs. Property-Based Injection What Service Implementation to Inject Consequences Refactoring to DI
Refactoring to Patterns and Rent-a-Wheels Application Eliminating Code That Duplicates Functionality Available in .NET Framework Injecting Data Classes to GUI Classes via Dependency Injection CRUD Persistence Pattern
Summary
411 411 420 424
426 426 428 429 430 431 434
434 434 435 437
438
xv
79796ftoc.qxd:WroxPro
2/23/08
8:45 AM
Page xvi
Contents Chapter 15: LINQ and Other VB 2008 Enhancements Type Inference for Local Variables XML Productivity Enhancements XML Literals Navigating XML with XML Axis Properties Extract XML Literal to Resource in Refactor!
Querying the Objects with LINQ Old Example in New Robes Object-Relational Mapping with LINQ to SQL LINQ and the Rent-a-Wheels Application
Summary
Chapter 16: The Future of Legacy VB Code To Migrate or Not To Migrate Migration Cannot Be 100 Percent Automated VB 6 and VB .NET Code Can Interoperate Migration Tools and Libraries
Preliminary VB 6 Refactorings Breaking the Monolith Dealing with Conditional Compilation
Putting Your Migrated Code under a Testing Harness Introducing a Functional Testing Harness Implementing a Functional Testing Harness
Upgrading Your Legacy Code Strict Static Typing Moving Design from Procedural toward an Object-Oriented Paradigm Introducing Inheritance Making Use of Parameterized Constructor Using Generic Containers for Additional Type Safety Upgrading Exception Handling Implementing XML Comments Releasing Resources in .NET
Summary
439 439 440 440 444 444
445 448 451 454
464
465 466 467 467 469
470 470 472
472 472 473
477 477 477 478 479 479 481 481 482
482
Appendix A: Unleash Refactor!
483
Appendix B: Rent-a-Wheels Prototype Internals and Intricacies
487
Hand Over Button Click Event-Handling Code Receive Button Click Event-Handling Code
xvi
487 488
79796ftoc.qxd:WroxPro
2/23/08
8:45 AM
Page xvii
Contents Charge Button Click Event-Handling Code Change Branch Button Click Event-Handling Code To Maintenance and From Maintenance Button Click Event Code Administer Fleet Form
488 489 493 494
Delete Button Click Event-Handling Routine New Button Click Event-Handling Routine Reload Button Click Event-Handling Routine Form Load Event-Handling Routine Administer Fleet Form Class Code: Fields Left Button Click Event-Handling Routine Save Button Click Event-Handling Routine
Foreword “Premature optimization is the root of all evil.” — Sir Charles Antony Richard Hoare, a British computer scientist, later paraphrased by Donald Knuth in his book, The Art of Computer Programming
Days ago, watching a documentary about the life of South American movie director Fabián Bielinsky, I paid particular attention to a thought he shared during an interview. He said that, when filming scenes, it usually happens that a given scene isn’t taped as the director originally assumed it would be. Sometimes that scene is reworked until the director gets what was originally wanted, while on some other occasions it’s just left the way it came, as it’s considered exponentially better than the initial sketch. So he concluded that the real art of making movies is deciding wisely when to do it again and when to just take what was gotten the first time around. Surprisingly, if that is the case for making movies, coding components is very similar to taping scenes. It’s always possible to get better approaches for a given algorithm. So, with the manager hat on our heads, we must decide when to freeze an always possible improvement for the sake of the timeline, budget, delivery due dates, and general customer satisfaction, and when to go on and get more. So here is where we address the need for refactoring. Let’s start by defining — informally, as the author will do it better ahead — what that means in software industry terms: refactoring is a series of techniques and mechanisms used to improve the quality — understandability, maintainability, modularity, extensibility, and so on — of code segments by reformulating their sentences in such a way that the general behavior remains unchanged. In other words, the behavior of the affected components shouldn’t vary as a consequence of the process but their quality, and hopefully their longevity, should be increased. Experience shows us that some portions of our code will sooner or later be candidates for refactoring, and the reasons are numerous: ❑
From the user side, users aren’t completely sure about the exact application they want until they see installed and running what they asked for us originally. That’s not a joke! In the beginning, they start requiring something they envision, but vaguely, and that’s natural — I’m by no means accusing them. An undesirable side effect of this back and forth is that our code could start losing a certain degree of cohesion; its different modules may start being coupled above the acceptable levels, as a consequence of last-minute changes because of time-to-market pressure.
❑
From our own perspective, the development side, we don’t have a clear idea either about how coding will look while we are modeling. As do the users, we also think we have cool ideas (or just good ideas at the end) until we try to put some of them into practice. Failing isn’t bad. What’s bad is refusing to change our minds just to avoid admitting that what we had considered a great idea wasn’t, in fact, so easy to implement. And here, again, when time adds its pressure, delivering a component code the quickest we can may also harm its quality.
79796flast.qxd:WroxPro
2/25/08
9:23 AM
Page xx
Foreword ❑
From the technology perspective, finally, there’s an invisible, somewhat omnipresent pressure to go along with industry trends. Typical examples are evolved .NET or Java APIs — AJAX, Web Services, and so on — that make former versions, or general strategies like ServiceOriented Architecture (SOA), Model-View-Controller (MVC), Object/Relational Mapping (O/R-M), and so on, obsolete. Once again, while reacting to those trends, our existing code is subject to some handling that, as time goes by, may erode its quality.
In the meantime, the real world shows us that making an effort to improve the quality of our code is something we innately tend to do. Considering that, refactoring techniques are nothing but the highest degree of maturity of such spontaneous attempts, reinforced with some supporting tools available out there to guarantee the success of the process. The good news about this is that we may apply refactoring locally, at a component or method level, there where we are applying any other modification; or globally, at a module or application level, assigning it a range of the project. Deciding on the right refactoring dose will depend on the quality gap to close, the remaining time, the available budget — it will always be easier to justify the application of refactoring where we already have to do something else than where no updates have yet to be asked for — among other factors. In this book, the author will address refactoring topics from the envisioning of their benefits to the current ways of putting refactoring into practice. Danijel Arsenovski has been involved in refactoring techniques, both in the .NET and Java platforms, since their earliest versions. He has delivered speeches at several conferences, given talks, and held workshops on this subject, and driven successful refactoring projects in the banking industry. As one of the leaders in development tools, Microsoft has been committed to delivering best-of-breed resources to the people who deal daily with coding activities and software projects as a whole. Through its undisputably winning Visual Studio IDE, Microsoft makes refactoring an out-of-box facility just a right-click away from your code. In these pages, Danijel will show you how refactoring may be practiced in Visual Basic as easily as you do copy-paste or any other editing activities! I dare tell you, dear reader, that you have one of the most proven and fundamental guides on these techniques. Enjoy reading this book! Diego Dagum Technical Evangelist, Microsoft Corp. Kingsgate, Winter 2007
xx
79796flast.qxd:WroxPro
2/25/08
9:23 AM
Page xxi
Introduction Thank you for choosing this book and welcome to the fabulous world of refactoring. I hope you will find this book useful as you go about your daily programming chores, as you discuss different design solutions with your peers, when you are getting ready to attack some obscure legacy code, and even as you are going over some lines in your mind that are keeping you awake at night. If this is your first encounter with refactoring, I expect this book to profoundly change the way you program and think about code. This is not an easy task to complete, and, ultimately, you will be the judge of how successful I was. I adopted refactoring in a systematic manner after I read the book Refactoring: Improving the Design of Existing Code by Martin Fowler (Addison-Wesley, 1999). This book proved to be down-to-earth practical, helping me learn some indispensable techniques that I could apply in real-life projects right away. The book was not based on some complex theory, nor did it contain any complex mathematical formulas. It spoke in language immediately understood by anyone writing the code. Soon after I read the book, I noticed a number of changes in the way I program: ❑
I was able to detect with much greater certainty problematic code and design flows.
❑
I was able to think of solutions for those problems and resolve them effectively through refactoring.
❑
When talking to my peers, I was able to argue for my decisions in a clear and concise manner.
Finally, I stopped looking at the code as some solid structure constant in time, and started viewing it as a plastic, moldable form that I can fashion to my liking and in accordance with my needs. This provoked a fundamental change in the way I treat code. I realized that there is a way to modify code in an efficient, predictable manner and improve its design in the process. Soon, the word about refactoring started to spread inside the team I worked with, and I saw more and more of my coworkers taking the book from the shelf. A few even got their own copies. I was able to speak with them using refactoring terminology and introduce refactoring as an integral part of the software construction process. Even the management proved to be forward-looking in this respect. Partly because of my own interest in learning different languages and technologies, and partly because of the necessities that I am often presented with at my workplace, I get to work with different teams and program in different languages. I tried disseminating refactoring when working with teams that program in Visual Basic in a similar manner I did with Java or C# teams. This proved to not go that smoothly. Soon I realized there is very little information on refactoring available for Visual Basic programmers. While most of the refactorings can be applied in a similar manner in any object-oriented programming language, there are some subtle differences.There is no reason why a programmer should not learn to refactor by looking at code examples written in his language of preference. This inspired me to think more about refactoring in Visual Basic and finally to write this book. I am convinced that there is a real need for such a text and that this book will be of practical use to many who program in Visual Basic.
79796flast.qxd:WroxPro
2/25/08
9:23 AM
Page xxii
Introduction I hope this book will help you write better code faster. I hope it will increase your productivity and have a positive impact on your coding performance. I expect that as a result you will create better, more sophisticated designs. Most importantly, I hope it will ease the burden of everyday tasks and put some fun back into what we all like to do best: program some great code.
Whom This Book Is For This book is intended for experienced (intermediate to advanced) Visual Basic .NET developers that wish to be introduced to the world of refactoring. To get the most out of the book, you should have a good command of Visual Basic .NET and especially object-oriented programming in general. If you are a beginning programmer, you will not be able to use this book as your primary source. This book will not teach you the basics of programming in Visual Basic .NET. There is no reason, however, not to get acquainted with refactoring as early in your career as possible. As you learn to program your first classes, you can use this book to learn how to design them properly and how to correct any mistakes you might have introduced into your design. If you are a VB 6 programmer, then you will not be able to successfully apply many of the refactorings that I deal with in this book because of differences between Visual Basic 6 (or previous) programming languages and Visual Basic .NET. In the case, however, that you are trying to upgrade your VB 6 code to VB .NET, you might find this book of great use. I have dedicated a whole chapter, the last one in the book, to the subject of upgrading VB 6 code to .NET. The chapter itself, however, builds upon material exposed earlier on in the book. You might also find this book very useful as you are making the transition as a programmer from VB 6 to Visual Basic .NET. This book exposes some of the common mistakes that newcomers from VB 6 to VB .NET make and teaches you how to deal with those mistakes. This book makes no assumption on the type or domain of your application. It can be a typical web application, a web service, a framework, a component, a shopping cart application, a new Facebook widget, or a shooter game, but whatever it is, as long there is some VB code in it, you will find the techniques explained in this book valuable. Most of the refactorings I deal with in this book are standard refactorings applicable in any fully objectoriented language. This means that if you program in some other object-oriented language, for example C#, as long as you are familiar with VB .NET syntax and you are able to read code examples, you will be able to use and apply the information exposed in this book.
What This Book Covers I have tried to make this book a good introduction and thorough overview of refactoring in Visual Basic. This book teaches the basic refactoring concepts:
xxii
❑
Code Smells
❑
Refactoring code transformations
79796flast.qxd:WroxPro
2/25/08
9:23 AM
Page xxiii
Introduction ❑
Some basic object-oriented principles
❑
The use of a refactoring tool that can automate the refactoring process
The book uses a single case study, relatively large in size for a book case study, to demonstrate practical, real-life application of refactoring over a realistically large code base. In addition to standard refactorings applicable to any object-oriented language, this book teaches VB .NET–specific refactorings. It also demonstrates some special uses of refactoring. For example: ❑
Refactoring as an integral part of upgrade of VB 6 code to VB .NET
❑
Refactoring for upgrade of VB .NET 2005 code to VB .NET 2008
❑
Refactoring for implementation of design patterns
This book contains a good number of important smells and refactorings. However, this book does not represent a complete refactoring catalog; because of time and space limitations, some important refactorings had to be excluded. For example, it does not deal with refactorings such as Simplify Conditional or Reverse Conditional, already available for automation in the Refactor! add-in from Developer Express. It also does not deal with many “reverse” refactorings, like Inline Method or Inline Class. This book is intended first of all to be an introduction to refactoring. My experience tells me that the first problem that programmers deal with when starting out on the refactoring path is poorly structured code, and refactorings such as Extract Method or Extract Class deal with this problem. Their opposites, Inline Method and Inline Class refactorings, help you deal with excessively structured code. They will aid you in eliminating constructs (methods or classes, for example) that are not needed any more. This often happens after extensive refactoring is applied to the code base. This does not mean that “reverse” refactorings are less important. The need for them is more likely to appear later on in the refactoring acquisition, once you have already grasped the basics of refactoring. Part of the learning process is to understand that the knowledge acquisition process never really ends. For example, with each new version of the Refactor! add-in, new refactorings are added to the battery of supported refactorings. People invent and devise new refactorings continuously. As you become proficient in the technique, you will invent your own refactorings and might eventually decide to share them with others. So I invite you to go beyond this book; don’t feel limited by the refactorings listed in this book. Look for and try to invent new refactorings. This way, you will truly master the art of continuous code improvement.
How This Book Is Str uctured Because this is the first book that deals exclusively with refactoring in Visual Basic and will probably be the first book on refactoring for the reader, I hope this book will fulfill more than one purpose: ❑
Be a thorough introduction to refactoring in Visual Basic.
❑
Be a thorough reference on refactoring techniques and code smells that can be consulted during everyday programming sessions.
❑
Demonstrate how refactoring techniques can be applied in a real-life situation, by means of single case study, the Rent-a-Wheels application, that I analyze and modify at the end of each chapter throughout the book.
xxiii
79796flast.qxd:WroxPro
2/25/08
9:23 AM
Page xxiv
Introduction In order to accomplish that, the main book narrative reads just like those of any other technical books. New concepts are introduced and elaborated in logical progression from more basic to more complex. Simplified, illustrational code examples are given for each concept. This way, you can read this book in logical order from beginning to end. This is something that you should probably do once or twice after you acquire this book and open it for the first time. In addition to this main narrative, you will see that the book is sprinkled with so called “definition boxes” of smells, refactorings, and object-oriented design principles. The purpose of these definitions is to give a condensed overview of the subject. In the case of smells for example, this definition contains heuristics on how the smell can be discovered. In the case of refactorings, there is a section called “Mechanics” that contains a form of recipe and describes the steps you need to perform in order to execute certain refactoring effectively. You should be able to consult these during your everyday work, to remind yourself how the refactoring is performed, how a smell is discovered, and what refactoring can be used to eliminate it, and so on. At the end of the majority of chapters, you will find the discussion of how refactorings, smells, and principles I talked about in the chapter reflected on the case study included with the book. You can download the code for the chapter and browse it as you read about what happened to the case study in the current chapter. The purpose of the case study is to present you with a more life like application of refactoring. Often in technical books you will find a selection of code samples that have been unrealistically simplified in order to prove point. While this makes the examples clearer and easier for the writer to prove the point, it often means the reader will confront much more complex situations in real life, with many unexpected obstacles appearing if certain techniques are to be applied to production code. In this book, I tried to present reader with a much more realistic scenario by means of the “Rent-a-Wheels” study case. The book is divided into five major parts that lead the reader from more basic to more complex concepts in logical progression.
xxiv
❑
Part I, “Introduction to Refactoring,” lays the foundation for the book. For example, in Chapter 1 I talk about refactoring in some general terms. This chapter also dispels some common misconceptions about refactoring. Chapter 2 gives you a taste of refactoring in practice right away. Then Chapter 3 goes over the tools that you will find indispensable in order to automate your refactoring work. Chapter 4 presents you with a case study used throughout the book.
❑
Part II, “Preliminary VB Refactorings,” covers some preliminary refactorings such as dead code elimination and others that will help you prepare the code for major overhaul. You deal with some VB-only problems such as strict versus weak typing, legacy versus structured error handling, and so on.
❑
Part III, “Getting Started with Standard Refactoring Transformations,” deals with some standard, core refactorings. You learn about the importance of choosing names for your code constructs and about the devastating effects of duplicated code. You deal with standard refactorings such as Extract Method and go into the details of code structuring on a method level.
❑
Part IV, “Advanced Refactorings,” talks about some more advanced refactorings that let you get the most from object-oriented capabilities of your programming environment. Good objectoriented skills are essential for this part of the book. You will learn how to discover classes, create inheritance hierarchies, and reorganize your code on a large scale.
❑
Part V, “Refactoring Applied,” shows you how refactoring can be successfully applied in order to reach a more specific goal. For example, you will see how refactoring can be paired with design patterns to produce even more sophisticated design. You will see some of the new features that come with the Visual Studio 2008 version of VB and how refactoring can be used to get the most out of them. Finally, you will take a look back at VB 6, a Visual Basic .NET predecessor. You will learn how to apply refactoring in order to update your VB6 code to Visual Basic .NET.
79796flast.qxd:WroxPro
2/25/08
9:23 AM
Page xxv
Introduction
What You Need to Use This Book In order to successfully use this book, you will need the following software: ❑
Visual Studio .NET 2008 — You will need at least the Professional Edition in order to be able to use the Refactor! add-in. You will still be able to debug, execute code, and perform refactorings manually in the free Visual Basic 2008 Express Edition. If you use Visual Studio 2005, you will also be able to get some use out of most of the book, except for Chapter 15, which deals exclusively with features available in Visual Basic 9 and Visual Studio 2008 only.
❑
Refactor! for VB add-in from Developer Express — Download the latest version of this free Visual Studio add-in from the Developer Express site: www.devexpress.com/vbrefactor/.
❑
Microsoft SQL Server — You will need this in order to run the sample case study included in the book. This can be MS SQL Server 2000 or 2005, and the Express edition will suffice for the application that comes with this book. You can use any operating system, like Windows XP, Windows Server 2003, or Windows Vista, that can run the software I just listed.
Conventions To help you get the most from the text and keep track of what’s happening, a number of conventions are used throughout the book.
Boxes like this one hold important, not-to-be forgotten information that is directly relevant to the surrounding text. This information includes some important definitions you encounter throughout the book. You might also look for these definitions when using the book in a reference-like manner.
Tips, hints, tricks, and asides to the current discussion are offset and placed in italics like this. As for styles in the text: ❑
New terms and important words are highlighted when introduced.
❑
Keyboard strokes look like this: Ctrl+A.
❑
File names, URLs, and code within the text look like this: persistence.properties.
❑
Code is presented in two different ways:
Monofont type with no highlighting is used for most code examples. Gray highlighting is used to emphasize code that’s particularly important in the present context.
❑
Also, bold is occasionally used within code listings to emphasize parts of code.
xxv
79796flast.qxd:WroxPro
2/25/08
9:23 AM
Page xxvi
Introduction
Smell, Refactoring , and Object-Oriented Design Principle Boxes In addition to the conventions just mentioned, throughout the course of this book you will come across three other types of boxed text: Smells, Refactorings, and Object-Oriented Design Principles. ❑
❑
❑
Smells — These boxes contain condensed definitions of code smells. A code smell is an important refactoring concept, and each box contains three sections: ❑
Detecting the Smell — Describes some simple heuristic on how to detect the code smell in your code.
❑
Related Refactorings — Lists the refactorings you can use in order to eliminate the smell.
❑
Rationale — Explains the negative effect of the code smell in more theoretical terms.
Refactorings — The content these boxes gives comprises the bare basics of the refactoring. Each box contains the following sections: ❑
Motivation — Explains the beneficial effect this refactoring has on code and how the design is improved with its application.
❑
Related Smells — Lists the smells this refactoring can help you eliminate.
❑
Mechanics — Gives a step-by-step recipe on how to perform the refactoring.
Object-Oriented Design Principles — In these boxes I define some of the crucial object-oriented design principles and often use some short code samples to illustrate them.
Index of Smells For ease of reference, the following table indicates what smells are included in the book and the page number where the box dealing with that specific smell can be found. Smell
xxvi
Page Number
Comments
226
Cyclic Dependencies
384
Data Class
286
Database-Driven Design
288
Dead Code
174
Duplicated Code
235
Error Code
154
Event-Handling Blindness
232
Implicit Imports
372
79796flast.qxd:WroxPro
2/25/08
9:23 AM
Page xxvii
Introduction Smell
Page Number
Implicit Narrowing Conversions
125
Implicit Type Declaration
118
Implicit Variable Declaration
113
Large Class
298
Large Namespace
377
Legacy (Unstructured Error Handling)
153
Long Method
225
Magic Literals
236
Monolithic Application
470
Non-Coherent Namespace
378
Overburdened Temporary Variable
249
Overexposure
179
Procedural Design
302
Refused Bequest
337
Superfluous Temporary Variable
254
Unrevealing Names
201
Unused References
191
Using Fully-Qualified Names Outside Imports Section
188
XML String Literals
441
Index of Refactorings The following table indicates what refactorings are included in the book and the page number where the box dealing with that specific refactoring can be found. Refactoring Break Monolith
Page Number 471 Continued
xxvii
79796flast.qxd:WroxPro
2/25/08
9:23 AM
Page xxviii
Introduction Refactoring
Page Number
Convert Procedural Design to Objects
307
Eliminate Dead Code
176
Encapsulate Field
275
Explicit Imports
374
Extract Class
281
Extract Interface
346
Extract Method
221
Extract Namespace
382
Extract Super Class
352
Inline Temp
254
Move Class to Namespace
380
Move Declaration Near Reference
245
Move Element to a More Enclosing Region
185
Move Field
291
Move Initialization to Declaration
248
Move Method
289
Move Type to File
388
Pull Up Method
357
Reduce Access Level
182
Remove Unused References
192
Rename
206
Replace Complex VB Queries with LINQ
447
Replace Error Code with Exception Type
164
Replace Fully-Qualified Names with Explicit Imports
188
xxviii
79796flast.qxd:WroxPro
2/25/08
9:23 AM
Page xxix
Introduction Refactoring
Page Number
Replace General-Purpose Reference with Parameter Type
361
Replace Inheritance with Delegation
338
Replace Magic Literal with Constant
237
Replace “On Error” Construct with Try–Catch–Finally
237
Replace Programmatic Data Layer with LINQ to SQL
453
Replace Row with Data Class
299
Replace Temp with Query
257
Replace XML String Literals with XML Literal
442
Safe Rename
210
Set Option Explicit On (Enforce Variable Declaration)
113
Set Option Strict On (Enforce Variable Type Declaration and Explicit Type Conversions)
131
Split Temporary Variable
251
Index of Object-Oriented Design Principles The following table indicates the object-oriented design principles that are included in the book and the page number where the box dealing with that specific principle can be found. Object-Oriented Design Principle
Page Number
Acyclic Dependencies Principle
387
Favor Object Composition over Class Inheritance
331
Open-Closed Principle
215
Program to an Abstraction
331
Reuse-Release Equivalence
378
Single Responsibility
294
xxix
79796flast.qxd:WroxPro
2/25/08
9:23 AM
Page xxx
Introduction
Source Code As you work through the examples in this book, you may choose either to type in all the code manually or to use the source code files that accompany the book. All of the source code used in this book is available for download at http://www.wrox.com. Once at the site, simply locate the book’s title (either by using the Search box or by using one of the title lists) and click the Download Code link on the book’s detail page to obtain all the source code for the book. Because many books have similar titles, you may find it easiest to search by ISBN; this book’s ISBN is 978-0-470-17979-6. Once you download the code, just decompress it with your favorite compression tool. Alternately, you can go to the main Wrox code download page at http://www.wrox.com/dynamic/books/download .aspx to see the code available for this book and all other Wrox books.
Errata We make every effort to ensure that there are no errors in the text or in the code. However, no one is perfect, and mistakes do occur. If you find an error in one of our books, like a spelling mistake or faulty piece of code, we would be very grateful for your feedback. By sending in errata you may save another reader hours of frustration, and at the same time you will be helping us provide even higher quality information. To find the errata page for this book, go to http://www.wrox.com and locate the title using the Search box or one of the title lists. Then, on the book details page, click the Book Errata link. On this page you can view all errata that has been submitted for this book and posted by Wrox editors. A complete book list including links to each book’s errata is also available at www.wrox.com/misc-pages/booklist.shtml. If you don’t spot “your” error on the Book Errata page, go to www.wrox.com/contact/techsupport .shtml and complete the form there to send us the error you have found. We’ll check the information and, if appropriate, post a message to the book’s errata page and fix the problem in subsequent editions of the book.
p2p.wrox.com For author and peer discussion, join the P2P forums at p2p.wrox.com. The forums are a web-based system for you to post messages relating to Wrox books and related technologies and interact with other readers and technology users. The forums offer a subscription feature to e-mail you topics of interest of your choosing when new posts are made to the forums. Wrox authors, editors, other industry experts, and your fellow readers are present on these forums. At http://p2p.wrox.com you will find a number of different forums that will help you not only as you read this book, but also as you develop your own applications. To join the forums, just follow these steps:
1. 2.
xxx
Go to p2p.wrox.com and click the Register link. Read the terms of use and click Agree.
79796flast.qxd:WroxPro
2/25/08
9:23 AM
Page xxxi
Introduction 3.
Complete the required information to join as well as any optional information you wish to provide and click Submit.
4.
You will receive an e-mail with information describing how to verify your account and complete the joining process.
You can read messages in the forums without joining P2P but in order to post your own messages, you must join. Once you join, you can post new messages and respond to messages other users post. You can read messages at any time on the Web. If you would like to have new messages from a particular forum e-mailed to you, click the Subscribe to this Forum icon by the forum name in the forum listing. For more information about how to use the Wrox P2P, be sure to read the P2P FAQs for answers to questions about how the forum software works as well as many common questions specific to P2P and Wrox books. To read the FAQs, click the FAQ link on any P2P page.
xxxi
79796flast.qxd:WroxPro
2/25/08
9:23 AM
Page xxxii
79796c01.qxd:WroxPro
2/25/08
8:55 AM
Page 1
Part I: Introduction to Refactoring In this introductory part, you are going to see what refactoring is in general terms, why it is important, what benefits refactoring brings to the development process, and how it can be even more relevant to Visual Basic programmers than to programmers in some other languages. You are also going to see a small demonstration of the refactoring process at work, explore the tools relevant to refactoring, and, finally, take a look at a sample application I will use throughout this book to illustrate refactorings and the refactoring process as it is applied.
79796c01.qxd:WroxPro
2/25/08
8:55 AM
Page 2
79796c01.qxd:WroxPro
2/25/08
8:55 AM
Page 3
Refactoring: What’s All the Fuss About? Take a look at any major integrated development environment (IDE) today and you are bound to discover “refactoring” options somewhere at the tip of your fingers. And if you are following developments in the programming community, you have surely come across a number of articles and books on the subject. For some, it is the most important development in the way they code since the inception of design patterns. Unlike some other trends, refactoring is being embraced and spread eagerly by programmers and coders themselves because it helps them do their work better and be more productive. Without a doubt, applying refactoring has become an important part of programmers’ day-to-day labor no matter the tools, programming language, or type of program being developed. Visual Basic is a part of this: at this moment, the same wave of interest for refactoring in the programming community in general is happening inside the Visual Basic community. In this introduction, ❑
I start out by taking a look at what refactoring is and why it is important and then discuss a few of the benefits that refactoring delivers.
❑
I also address some of the most common misconceptions about refactoring.
❑
In the second part of this chapter, I want you to take a look at the specifics of Visual Basic as a programming language and how refactoring can be even more relevant for Visual Basic programmers because of some historic issues related to Visual Basic.
I’ll start with some background on refactoring in general.
79796c01.qxd:WroxPro
2/25/08
8:55 AM
Page 4
Part I: Introduction to Refactoring
A Quick Refactoring Over view When approaching some programming task, you have a number of ways in which you can go about it. You start off with one idea, but as you go along and get into more detail, you inevitably question your work along these lines: “Should I place this method in this class or maybe in this other class? Do I need a class to represent this data as a type or am I well off using the primitive? Should I break this class into more than one? Is there an inheritance relationship between these two classes or should I just use composition?” And if you share your thoughts with some of your peers, you are bound to hear even more options for designing your system. However, once you commit yourself to one approach, it may seem very costly to change these initial decisions later on. Refactoring teaches you how to efficiently modify your code in such a way that the impact of those modifications is kept at a minimum. It also helps you think about the design as something that can be dealt with at any stage of the project, not at all cast in stone by initial decisions. Design, in fact, can be treated in a very flexible way. Definition: Refactoring is a set of techniques used to identify the design flows and to modify the internal structure of code in order to improve the design without changing code’s visible behavior.
All design decisions are the result of your knowledge, experience, and creativity. However, programming is a vast playfield, and it’s easy to get tangled in contradictory arguments. In VB .NET you are, first and foremost, guided by object-oriented principles and rules. Unfortunately, very often it is not so clear how these rules work out in practice. Refactoring teaches you some simple heuristics that can help improve your design by inspecting some of the visible characteristics of your code. These guidelines that refactoring provides will set you on the right path in improving the design of your code.
The Refactoring Process Refactoring is an important programming practice and has been around for some time. Pioneered by the Smalltalk community, it has been applied in a great number of programming languages, and it has taken its place in many programmers’ bags of tricks. It will help you write your code in such a way that you will not dread code revision. Being a programmer myself, I know this is no small feat! So, how do you perform refactoring? The refactoring process is fairly simple and consists of three basic steps:
1.
Identify code smells. You’ll see what code smell means very soon, but, in short, this first step is concerned with identifying possible pitfalls in your code, and code smells are very helpful in identifying those pitfalls.
2.
Apply the appropriate refactoring. This second step is dedicated to changing the structure of your code by means of refactoring transformations. These transformations can often be automated and performed by a refactoring tool.
3.
Execute unit tests. This third step helps you rectify the state of your code after the transformations. Refactoring is not meant to change any behavior of your code observable from the “outside.” This step generally consists of executing appropriate unit tests that will prove the behavior of your code didn’t change after performing refactoring.
4
79796c01.qxd:WroxPro
2/25/08
8:55 AM
Page 5
Chapter 1: Refactoring: What’s All the Fuss About? You might have noticed the word design used in the refactoring definition earlier in the chapter. This is a broad term and can take on very different meanings depending on your background, programming style, and knowledge. Design in this sense simply means that refactoring builds upon object-oriented theory with the addition of some very simple heuristics dedicated to identifying shortcomings and weak spots in your code. These antipatterns are generally referred to as code smells and a great part of refactoring can be seen simply as an attempt to eliminate code smells. Definition: Code smell is a sensation you develop that tells you that there might be a flaw in your code.
The code smell can be something as simple as a very large method, a very large class, or a class consisting only of data and with no behavior. I’ll dedicate a lot of time to code smells in the book, because improving your sense of code smell can be very important in a successful refactoring process. The aim of refactoring is to improve the design of your code. You generally do this by applying modifications to your code. The refactoring methodology and its techniques help you in this task by making it easier to perform and even automate such modifications.
A Look at the Software Situation As software developers, your success depends on being able to fulfill different types of expectations. You have to keep in mind many different aspects of your development work; here are just a few of the concerns: ❑
Very often you will hear that the most important one is satisfying user requirements, generally meaning that you should create software that does what the client paid for.
❑
You also need to guarantee the quality of your product. You strive to reduce defects and to release a program that has the minimum number of bugs.
❑
You have to think about usability, making programs that are easy to understand and exploit.
❑
You tend to be especially concerned about performance, always inventing new ways to minimize memory usage and the number of cycles needed in order to solve some problem.
❑
You need to do all of this in a timely manner, so you are always looking for ways to augment productivity.
These issues cause us to focus, and rightly so, on the final product (also known as the binary) and how it will behave for the final user. However, in the process of producing the binary, you actually work with source code. You create classes, add properties and methods, organize them into the namespaces, write logic using loops and conditions, and so on. This source code, at a click of a button, is then transformed, compiled, or built into a deliverable, a component, an executable, or something similar. There is an inevitable gap between the artifacts you work on — the source — and the artifacts you are producing — the binary. In a way, this gap is awkward and not so common in the other areas of human activity. Take a look at stonemasonry, for example. While the mason chips away pieces of stone and polishes the edges, he or she can see the desired result slowly appearing under the effort. With software, the process is not at all as direct. You write source code that is then transformed into the desired piece of software. Even with the visual tools, which largely bridge this gap between source and binary, all you do in the end is create
5
79796c01.qxd:WroxPro
2/25/08
8:55 AM
Page 6
Part I: Introduction to Refactoring the source that is later on processed and turned into a compiled unit. Imagine a cook that can only write down a recipe and try the cooked meal, but is not allowed to handle the ingredients or taste the meal while it is being prepared. What’s more, there are many ways to write the source that will produce the same resulting binary. This can easily lead you to forget or sacrifice some qualities inherent to the source code itself, because the source code can be considered just a secondary artifact. While these qualities are not directly transformed to a final product, they have an immense impact on the whole process of creation and maintenance. This leads to the following question: Can we distinguish between well written and poorly written code, even if the final result is the same? In the following sections, I’ll explore this question, and you’ll see how refactoring can clarify doubts you might have.
Refactoring Encourages Solid Design No matter your previous programming experience, I am certain you will agree that you can indeed distinguish between good and bad code. Assessing code may begin on a visual level. Even with a simple glance you can see if the code is indented and formatted so it is pleasing to view, if the agreed naming conventions are used, and so on. At a less superficial level, you start to analyze code according to principles and techniques of software design. In Visual Basic, you follow the object-oriented software paradigm. You look into how well classes are structured and encapsulated, what their responsibilities are, and how they collaborate. You use language building blocks like classes and interfaces; and features like encapsulation, inheritance, and polymorphism in building a cohesive structure that describes the problem domain well. In a certain way you build your own ad-hoc language on top of a common language that will communicate your intentions and design decisions. There are a number of sophisticated principles you need to follow in order to achieve a solid design. When you create software that is reusable, extendible, and reliable, and that communicates its purpose well, you can say you have reached your goal of creating well-designed code. Refactoring gives you a number of recipes to ensure that your software conforms to the principles of well-designed code. And when you stray from your path, it helps you reorganize and impose the best design decisions with ease.
Refactoring Accommodates Change Popular software design techniques like object-oriented analysis and design, UML diagramming, usecase analysis, and others often overlook one very important aspect of the software creation process: constant change. From the first moment it is conceived, software is in continuous flux. Every so often, requirements will change even before the first release, new features will be added, defects corrected, and even some planned design decisions, when confronted with real-world demands, overruled. Software construction is a very complex activity, and it is futile attempting to come up with a perfect solution up front. Even if some more sophisticated techniques like modeling are used, you still come short of thinking about every detail and every possible scenario. It is this state of flux that is often the biggest challenge in the process of making software. You have no choice but to be ready to adapt, count on change, and react readily when it happens. If you are not ready to react, the design decisions you made are soon obscured, and the dangerous malaise of rotting design settles in.
6
79796c01.qxd:WroxPro
2/25/08
8:55 AM
Page 7
Chapter 1: Refactoring: What’s All the Fuss About? Refactoring is a relatively simple way to prepare for change, implement change, and control the adverse effects these changes can have on your design.
Refactoring Prevents Design Rot Software is definitely one of the more ephemeral human creations. Driven by new advances and technologies, software creations are soon replaced with more modern or advanced versions. Even so, during its lifetime software will journey through a number of reincarnations. It is constantly modified and updated, new features are added and old ones removed, defects resolved and adaptations performed. It is quite common that more than one person will put their hands on the same piece of software, each with his or her own style and preferences. Rarely will it be the same team of people that will see the software from the beginning to the end. Go back for a moment to the stonemason example. Now imagine that there is more than one person working on the same stone, that these people can change during the collaboration, and that the original plan is often itself changed with new shapes added or removed and different materials used. That may be a task for somebody of Michelangelo’s stature, but definitely not for the ordinary craftsman. No wonder then that initial ideas soon are forgotten, thought-out structure superseded by new solutions, and original design diluted. The initial intentions become less pronounced and the metaphors more difficult to comprehend, and the source is closer and closer to a meaningless cluster of symbols that still, but a lot less reliably, performs the intended function. This ailment steals in quietly, step by step, often unnoticed, and you end up with source that is difficult to maintain, modify, or upgrade. What I’ve just described are the symptoms of rotting design, something that can occur even before the first release lives to see the light of a day. Refactoring helps you prevent design rot. So, as you have moved along in this brief survey of the software landscape, I’ve pointed out several challenges that developers face and how refactoring can help. Next, I want to discuss refactoring in more detail.
The Refactoring Process: A Closer Look I just discussed a few key areas of software development that can often lead to poor code. You need to stand guard for the quality of your code constantly. In effect, you need to have the design qualities of your code in mind at all times. While this sounds sensible, thinking continuously about design and code quality can often be costly and quite complicated. The refactoring methodology and its techniques help you in this task by making it easier to perform and even automate modifications that will keep the design active. In this section I’m going to take a look at the refactoring activities you would typically complete during a software development cycle.
Using Code Smells As a first step in your refactoring activity, you take a look at the code in order to assess its design qualities. Refactoring teaches you a set of relatively simple heuristics called code smells that can help you with this task, along with well-known notions and principles of object-oriented design. Programming, being complex
7
79796c01.qxd:WroxPro
2/25/08
8:55 AM
Page 8
Part I: Introduction to Refactoring as it is, makes it difficult to impose precise rules or metrics, so these smells are more general guidelines and are susceptible to taste and interpretation. Along with gaining more experience and knowledge, you develop more expertise in identifying and eliminating bad smells in your code.
Transforming the Code The next step leads you to modifying the code’s internal structure. Here, refactoring theory has developed a set of formal rules that enable you to execute these transformations in such a way that, for a client, these modifications are transparent. You do not have to tackle the theory behind these rules. The toolmakers use these rules to make certain that refactoring modifies the code in a predictable way. For example, let me illustrate this modification that preserves the original behavior of the code with an example. In Table 1-1, imagine you transformed the code at the left side into the code on the right side.
Table 1-1: Two Forms of Writing the Code that Will Execute in the Same Way Free Literal Value
Literal as Constant
Public Class Employee
Public Class Employee Public Const OvertimeIndex _ As Decimal = 1.5
Private hoursWorked As Integer
Private hoursWorked As Integer
Private overtimeHoursWorked _ As Integer
Private overtimeHoursWorked _ As Integer
Private hourlyWage As Decimal
Private hourlyWage As Decimal
Public Function GetWage() _ As Decimal Return (hoursWorked * _ hourlyWage) + _ (overtimeHoursWorked * _ hourlyWage _ * 1.5) End Function End Class
Public Function GetWage() _ As Decimal Return (hoursWorked _ *hourlyWage) + _ (overtimeHoursWorked _ * hourlyWage _ * OvertimeIndex) End Function End Class
All you did here was to replace the literal value 1.5 with a constant OvertimeIndex. Executing the code on both sides provides identical results, but the one on the right can be a lot easier to maintain or modify. And it is definitely easier to understand. Now you understand that the literal 1.5 has a special meaning and has not been selected by chance.
Automating Refactoring Transformations Refactoring rules have one great consequence: it is possible to automate a large number of these transformations. Automation is really the key to letting refactoring show its best. Refactoring tools will check for the validity of what you are trying to perform and let you apply a transformation only if it doesn’t break the code. Even without a tool, refactoring is worth your while; however, manual refactoring can be slow and tedious.
8
79796c01.qxd:WroxPro
2/25/08
8:55 AM
Page 9
Chapter 1: Refactoring: What’s All the Fuss About? Figure 1-1 shows the Refactor! for VB Visual Studio add-in from Developer Express integrated with Visual Studio 2005.
Figure 1-1
The Benefits of Refactoring In light of all this, it is pertinent to ask what benefits refactoring brings. After all, it is not about adding new features or resolving bugs, and you end up with code that basically does what it used to, so why should you invest the time and money to perform this activity? What are the benefits of keeping your design optimal at all times? How does refactoring pay off?
Keeping the Code Simple With the fact that software development is a continuous, evolving process, refactoring can bring important qualities to your code. Keeping your code lean at all times can be challenging, especially when you’re under pressure to deliver the results quickly. So how does your code become overly complex? There are several ways this happens. ❑
In one typical scenario, you add a function here, a property there, another condition will crop up, and so on. This will soon produce a situation where classes and methods have grown and gone beyond their original purposes. They have too many responsibilities, communicate with many other elements, and are prone to change for many different reasons. It also becomes a breeding ground for duplicated code.
9
79796c01.qxd:WroxPro
2/25/08
8:55 AM
Page 10
Part I: Introduction to Refactoring ❑
In another scenario, you start off with a very thorough design that proves to be more than you really need. Simple code does only what it is supposed to do; you need not be so concerned much with trying to have your solution respond to any possible situation even before it happens. You can easily develop a tendency to overengineer your code, using complex structures when simple ones will do. (You can easily identify this school of thought by how many “what if” statements are used in discussions of the code.) This situation is motivated by an urge to anticipate future requirements even before they are expressed by the client.
❑
Performance has proved to be a lure for generations of programmers. You might spend numerous hours in order to obtain nanosecond gains in execution time. Without trying to lessen the importance of this key quality of software, you should bear in mind the right moment to deal with it. It can be very difficult to find the critical line you need to change in order to improve performance even for systems already in production; there is even less of a probability that you can find it while the system is in plain development and you are not sure what the rest of the pieces will end up looking like. Using the IDE as the performance-testing environment can be equally misleading.
How can you avoid such pitfalls? Once you become aware of them, you should deal with them quickly. Keeping things on the simple side will be greatly rewarded each time you need to add a feature, resolve a bug, or perform some optimization. ❑
If you see that a method has grown out of proportion, it is time to add a new method(s) that will take off some of the burden.
❑
If a class has too many members, maybe it can be restructured into a group of collaborating classes or into a class hierarchy.
❑
If a modification left some code without any use and you are certain that it will never be executed, there’s no need to keep it; it should be eliminated.
All these solutions represent typical refactorings. After a smell is discovered, the solution is a restructuring of the problematic code. When code is simple, it is easy to navigate — you don’t lose time in long debugging sessions in order to find the right spot. The names of classes, methods, and properties are meaningful; code purpose is easy to grasp. This type of code won’t have you reaching for documentation or desperately searching through the comments. Even after a short time spent with such code, you feel it does not hide any major mysteries. In simple words, you are in control.
Keeping the Code Readable Programming is intellectually a very intense activity. You are often so immersed in your work that you tend to have a deep and detailed understanding of your creation in order to maintain complete control over it. You may try to memorize every single detail of the code. You feel proud when you are able to immediately correct a bug or change some behavior. After all, it is what makes you good in the work you perform. As you become more productive, you develop strategies and gain your own programming style. There is nothing wrong with being expert with the code you create, unless that expertise becomes the only weapon you have in your arsenal. Unfortunately, sometimes you can forget one important fact; when developing software you seldom work alone. And in order to be able to work in a team, you must write code so it is easily comprehended
10
79796c01.qxd:WroxPro
2/25/08
8:55 AM
Page 11
Chapter 1: Refactoring: What’s All the Fuss About? by others. Others might need to modify, maintain, or optimize the code. In that case, if confronted with cryptic or hermetic code, others could lose numerous hours in a pure attempt to understand the code. Sooner or later you’ll have a computer do all your bidding, but until then, writing source in such way that it is easy for others to understand can prove to be a much more difficult task. Ironically, you can find yourself in the “other person” role even with your own code. Your memory has its limits, and after a while you may not be able to remember every detail of the code you yourself wrote. Readability can depend on different factors. Visual disposition is easily corrected and standardized with the IDE. Other factors, like the choice of identifier names, require a carefully thought-out approach. Because programmers often come from different backgrounds and have different experiences, the best bet is relying on natural language itself. You have to translate your decisions into code so they are easily understandable from a reading of the code, not only visible as a consequence of code execution. Code becomes really meaningful when a relation between it and a problem domain is correctly established. As a programmer, you continuously develop your vocabulary. Using well-known idioms, patterns, and accepted conventions can increase the clarity of your code. Reliance on comments and documentation can also affect the capacity of code to communicate with the reader. Because these artifacts never get executed, they are the first to suffer from obsolescence. Secondly, they are notorious for containing superfluous information. I will try to illustrate this with two code snippets that perform equally during execution. Try reading first the snippet on the left side in Table 1-2, and then the one on the right.
Table 1-2: An Example of Code that is Difficult to Read and the Same Code in a More Readable Form Difficult to Read
More Readable Code
Dim oXMLDom As _ New DOMDocument40 Dim oNodes As IXMLDOMNodeList ‘loads the file into XMLDom object oXMLDom.Load(App.Path + _ “\ portfl.xml”) oNodes = _ oXMLDom.selectNodes(“//stock[1]/*“)
Dim portfolio As _ New DOMDocument40 Dim stocks As IXMLDOMNodeList portfolio.Load(App.Path + _ “\portfl.xml”) stocks = portfolio.selectNodes(“//stock[1]/*“)
If I have proved my point, you will find the second snippet more to your liking. In case you still are not convinced, as an interesting experiment, you can try obfuscating your code with some obfuscation tool and then trying to find your way around it. Even with the smallest code base, it soon becomes impossible to understand the code. No wonder, because obfuscation is a process completely opposite to refactoring. Refactoring tools can help you improve readability by letting you rename identifiers in your code in a safe and systematic way and by letting you transform your code along well-known patterns and idioms — you use comments in a more profound manner. Strong structure in the code gives you confidence that the information you obtain from reading the code relates well to execution time. All this sounds very good. However, you can often hear arguments against refactoring. While some of those arguments are well founded, let me first deal with some opinions often heard that are not very constructive.
11
79796c01.qxd:WroxPro
2/25/08
8:55 AM
Page 12
Part I: Introduction to Refactoring
Debunking Common Misconceptions Like any topic that creates a huge amount of interest among developers, refactoring has produced an avalanche of opinions and contributions, some of more and others of less value. In certain cases I found those opinions so unfounded that I call them misconceptions. I feel it is worthwhile taking some time to debunk them, because they can add confusion and can lead you astray from a quest to adopt this valuable technique.
Refactoring Violates the Old Adage, “If It Ain’t Broke, Don’t Fix It” Often portrayed as longstanding engineering wisdom, this posture only promotes complacency. Refactoring does teach against it, but for a reason. Early on you learn how even a minuscule detail in code can make all the difference, often paying dearly for this knowledge. A small change can provoke software to break in a surprising manner and at the worst moment. So once you burn your hands you often become reluctant to make any change that is not absolutely necessary. This can work well for a moment, but then a situation comes up where bugs have to be resolved and petitions for new features cannot be evaded anymore. You are faced with the same code you tried not to confront. Those who adopt this “if it ain’t broke, don’t fix it” position look upon refactoring as unnecessary meddling with something that already serves its purpose. Actually, this conformist posture that tries to maintain the “status quo” is the result of an intent to rationalize the fear of confronting the code and the fact that you do not have control over it.
Refactoring Is Nothing New This misconception could be restated as, “Refactoring is just another word for what we all know already.” Which means you have all learned about good code, object-oriented design, style, good practices, and so on, and refactoring is just another buzzword that someone invented to sell some books. Okay, refactoring does not pretend to be imposing a radically new paradigm like object-oriented or aspectoriented programming. What it does do is radically change the way you program: it defines rules that make it possible to apply complex transformations to code at the click of a button. You do not look at your code as some frozen construct that is not susceptible to change. Instead, you see yourself as capable of maintaining the code in optimum shape, responding efficiently to any new condition.
Refactoring Is Rocket Science Programming is hard. It’s a complex activity that requires a lot of intellectual effort. Some of the knowledge can be very difficult to grasp. With Visual Basic .NET, VB programmers had to acquire the ability to work in a fully capable object-oriented language. For many, this was baffling at first. The good part is it definitely pays off. The great thing about refactoring is how simple it can be. It equips you with a very small set of simple rules to start off. This, coupled with a good tool, makes first steps in refactoring a breeze. Compared to other techniques an advanced programmer should know nowadays, like UML or design patterns, I’d say refactoring has the easiest learning curve, a lot like VB itself compared to other programming languages. Very soon, the time spent in learning refactoring will start to reap rewards. Of course, as with any other thing in life, gaining mastery requires a lot of time and effort.
12
79796c01.qxd:WroxPro
2/25/08
8:55 AM
Page 13
Chapter 1: Refactoring: What’s All the Fuss About? Refactoring Causes Poor Performance A longer way to state this might be, “Because after refactoring you usually end up with a larger number of more fine-grained elements like methods and classes, so much indirection must incur some performance cost.” If you go back in time a little, you’ll discover that this argument curiously sounds like the one used to voice initial skepticism toward object-oriented programming. The truth is that the differences between refactored and unstructured code are, at best, minimal. Except in some very specialized systems, this is not a concern. Experience shows that performance flows are generally afflicted by some precise spots in code. Fixing those during an optimization phase will get you the required levels of performance. Being able to easily identify the critical pieces of code can prove to be very valuable. By producing understandable code in which duplication and total size is minimized, refactoring greatly aids this task.
Refactoring Breaks Good Object-Oriented Design Well-structured and refactored code can look awkward to an untrained eye. Methods are so short that they often seem without substance. Classes seem without enough weight, consisting of only a few members. It seems as if nothing ever happens in our code. Having to manage a greater number of elements like classes and methods can imply that there is more complexity to deal with. This argument is actually misleading. The truth is that the same complexity was always present, only in refactored code it is expressed in such a cleaner, more structured way.
Refactoring Offers No Short-Term Benefits Refactoring actually makes you program faster. So far, I do not know of any study that I could call upon in order to prove what I just said, but my own experience tells me this is the case. All the same, it is only logical that this is so. Because we have a smaller quantity of code overall, less duplication, and a clearer picture, unless we are dealing with some trivial and unrealistically small scale code, benefits become apparent very soon.
Refactoring Works Only for Agile Teams Because it’s often mentioned as one of the pillar techniques in agile methodologies, refactoring is interpreted as working only for teams adhering to these principles. Refactoring is indispensable for agile teams. Even if your team has a different methodology, most of the time you are the one in charge charge of the way you code. Best results in refactoring are achieved if you adopt refactoring in small steps, performing it regularly while you code. Some practices, like strict code ownership or a waterfall process, can play against refactoring. If you can prove that refactoring makes sense from a programming point of view, you can start building your support base, first with your peers and then by spreading the word to the rest of your team. That dispenses with some of the common misconceptions surrounding refactoring. At this point, you may be wondering how all of this relates to Visual Basic. That is the topic of the next section.
13
79796c01.qxd:WroxPro
2/25/08
8:55 AM
Page 14
Part I: Introduction to Refactoring
V isual Basic and Refactoring It is fair to say that in the Visual Basic community, refactoring has had a slow start. One of the main reasons for this was the lack of proper tool support. While some tools with refactoring capabilities appeared on the market some years ago, only recently did dedicated refactoring tools for VB appear. Lack of tools coupled with lack of information and scarce literature suited for VB developers led to slow adoption of the technique. It seems, however, that in this case the developer community was ahead of the industry policy makers and commercial institutions. Refactoring support was voted the number-one desired feature for the 2005 edition of Visual Basic IDE. Realizing the importance this feature has for VB developers, Microsoft partnered with Developer Express to release a free Visual Basic refactoring add-in for Visual Studio 2005.
Visual Basic History and Legacy Issues While refactoring was slow to take over in the Visual Basic community, it can be argued that refactoring has even greater importance for Visual Basic programmers than for programmers in some other languages. Visual Basic longevity means that VB developers need to deal with a host of legacy issues even today. In this effort, refactoring can be a great help by providing the programmer with the tools for unobtrusive transformation of legacy constructs into more appropriate contemporary code. Visual Basic has been in existence for more than 15 years. During that time it has earned a huge following — it has become one of the most popular programming environments in existence. Thanks to its syntax based on the BASIC programming language and the graphical environment for drawing GUI elements, it has proven to be an easily accessible programming environment with a gradual learning curve. And thanks to Microsoft’s policy of spreading the use of Visual Basic in other forms and other environments like Windows Scripting, Visual Basic for Applications used for Office automation, Active Server Pages, and others, the circle of VB adopters has significantly increased.
Visual Basic Evolution Since its inception, Visual Basic has undergone a steady process of evolution and improvements. In version four, class constructs were added to Visual Basic, paving the way for object-oriented programming in Visual Basic. However, not until VB .NET did Visual Basic unleash the full power of object-oriented programming. In VB .NET, implementation inheritance support was added. With the advent of the .NET Framework, Microsoft decided to make a more significant overhaul of the language and to make it more appropriate for the new platform. Significant improvements were made: ❑
Added inheritance support
❑
Optional static type checking (Option Strict)
❑
Structured Error Handling (Try-Catch-Finally)
❑
Attributes
Many other programming elements were removed or replaced in an attempt to clean up the language syntax and make it more in the spirit of the .NET Framework. In the .NET Framework, all code gets compiled into Intermediate Language and then traduced into native binary. This is true for code programmed in VB .NET also. This makes it possible for VB code to interact with code programmed in other languages.
14
79796c01.qxd:WroxPro
2/25/08
8:55 AM
Page 15
Chapter 1: Refactoring: What’s All the Fuss About? VB programmers can use code programmed in C# or managed C++. The reverse is also true — C# or C++ programmers, or programmers in any other .NET language for that matter, can import and use assemblies programmed in Visual Basic .NET. This is not so different from the interoperability provided by COM in the pre-.NET era, with the distinction that it is also possible to inherit types programmed in different languages. This continuous work on language improvement continues even today. For example, in Visual Basic 2005, support for generic types was added. In the 2008 version of VB, new features like LINQ, XML Literals, Extension Methods, and Lambda Expressions continue to keep VB at the forefront of .NET programming languages. Along with market forces that are continuously moving Visual Basic forward in making it more powerful and advanced so that it stays as efficient as other programming languages, some frictional forces stand in the way of its progress. Its long history and success are the main reasons for keeping some of its older language elements that would normally be completely replaced with newer ones. With C# Microsoft had a clean slate for language design. With Visual Basic, it has to take into account a huge amount of existing code that should be migrated and a great number of developers who need to upgrade their skills.
Legacy Code The huge popularity and widespread adoption of Visual Basic resulted in a great amount of pre-.NET code still in production even today, almost six years after the advent of .NET. Migrating VB6 or prior code to .NET is not a simple affair. While Microsoft provided an automated migration tool, this upgrade can not be realistically performed without user interaction. Code produced by the Migration Wizard will often leave portions of code that should be upgraded manually. This in part demonstrates a somewhat brave decision by Microsoft not to subject VB .NET to upgrade necessities and backward-compatibility issues. But because a completely automated upgrade is not possible, a number of features were kept in VB .NET in order to provide at least some possibility of upgrade. Such features are: ❑
Old-style error handling (On error...)
❑
Optional static typing as opposed to mandatory static type checking (Option Strict...)
❑
Module language construct, etc.
Unfortunately, code with such features left over after the upgrade has not been fundamentally upgraded. It can execute in a new, .NET environment, but nevertheless it has kept old deficiencies.
Legacy Programming In the computer industry, new technologies are the order of the day. As programmers, you are destined to upgrade your skills continuously and to acquire new knowledge. Unless you do so, the spectrum of employment opportunities and chances for career advancement can be greatly reduced. VB programmers were exposed to this process of continuous skill improvement throughout the history and evolution of VB, but never was the challenge as great as with the release of VB .NET. By making VB a modern, fully object-oriented language with native access to the .NET Framework, Microsoft gave VB programmers a much more powerful tool. However, to be able to harness this power, programmers had to acquire new skills. It can be argued that with the advent of VB .NET a “paradigm shift” has been produced. The changes are significant and go beyond a simple upgrade. These changes require new skills and new ways of thinking.
15
79796c01.qxd:WroxPro
2/25/08
8:55 AM
Page 16
Part I: Introduction to Refactoring However, because of many old elements that were kept in VB .NET, it is possible to program in VB .NET in the “old style,” just as in VB6 or prior. But if that way is used, no benefits are gained from the new platform. This is not a new phenomenon in the history of programming. C++ and Java had their syntax based on C language syntax in order to attract and facilitate transfer of C programmers to these new languages. However, it was soon noticed that a number of these programmers started using new tools and environments in the old style, relying on old constructs and design applicable to the old programming language. With C++ and Java, this meant that programmers continued using procedural design instead of relying on new, object-oriented design. Admittedly, in VB the gap is not that great. Even in VB6 you can define a class, create an instance, or define and implement an interface. Nonetheless, using inheritance and generics and understanding the benefits of static typing are important challenges for someone coming from a classic VB background, and surmounting these challenges requires a major shift in the way programming is approached.
Legacy Language Elements So these backward forces I’ve discussed led to a situation in which many of the obsolete language elements were kept in VB .NET for the purpose of upgrades of existing code or with the intention of facilitating the transition of programmers to a new platform. Unfortunately, these features can just as easily be used in newly created VB .NET code, although there are other, much better alternatives. To be able to distinguish between the two, a solid command of VB .NET and object-oriented principles is required.
Dealing with Legacy Issues Through Refactoring Building on the fundamentals of object-oriented theory, refactoring goes a step further than a traditional approach to programming does. With refactoring, it is easy to identify weak spots in your code and apply small-scale transformations that do not change the behavior of code but can deal with the shortcomings. In that sense, some of the legacy language elements can be considered undesirable, and, just like any other smell, this legacy code smell can be dealt with through the refactoring process. By defining procedures that can transform legacy elements and fundamentally upgrade the code, refactoring can greatly alleviate issues related to upgrade and make your old code fully capable and equally useful in the .NET world.
Summar y This introductory chapter has given you a brief overview of refactoring, explaining its relevance and benefits. You have seen how refactoring helps you design your applications and prevent design rot, and at the same time accommodate any change that your software might be exposed to. You have learned the three important stages in each cycle of the refactoring process: smell identification, refactoring, and testing. These three stages are mandatory for successful refactoring. In order to make this process even more productive, you can rely on automated refactoring tools that take a lot of the drudgery and complexity out of refactoring, making it easily accessible and applicable. You have also also been presented with some of the most common misconceptions of refactoring that you might come across, just so you won’t be surprised by the diversity of opinions on the subject and can make your own informed choices.
16
79796c01.qxd:WroxPro
2/25/08
8:55 AM
Page 17
Chapter 1: Refactoring: What’s All the Fuss About? In the second part of the chapter, I put Visual Basic into focus. A very popular and successful tool, it was not immune to changes and advances in the programming world. While it has evolved significantly, its longevity means that a host of legacy and backward-compatibility characteristics were preserved inside the language in an attempt to make the upgrade to new versions less upsetting. Unfortunately, this also meant that a lot of programmers continued to program in the legacy style, not reaping the benefits of the advances of this fully object-oriented language that came with th advent of VB .NET. You have seen how refactoring can play a significant role in the transition and upgrade of legacy code to VB .NET. You can rely on it while you acquire new knowledge and sharpen your programming and design skills. Now it’s time to see some of this in practice. In the next chapter you are going to see refactoring at work. I will write some code and use a small application to illustrate the power of the refactoring process.
17
79796c01.qxd:WroxPro
2/25/08
8:55 AM
Page 18
79796c02.qxd:WroxPro
2/25/08
8:56 AM
Page 19
A F irst Taste of Refactoring Before I go into the details of refactoring procedures, the theory and mechanics behind it, it is a good idea to start with a very simple, yet complete application in order to gain perspective on the process I am trying to describe. This way, as I start going into detail about specific refactoring or code smells, you’ll have a better sense of where each of these elements of the refactoring process fits and of the purpose behind it. In this chapter you are going to see a simple application, consisting only of a single form and a single event-handling procedure, in its first incarnation. Soon, requirements will start to grow. As the application strives to respond to new requirements, flaws and imperfections in the design will materialize. I will identify the code smells and eliminate them as I progress. I’ll follow the refactoring process as I perform these modifications. As you have already seen, the refactoring process consists of three steps:
1. 2. 3.
Identifying the smell Using specific refactoring to eliminate the smell Executing tests to validate the changes
I’ll focus on the refactoring process, but I will explain and develop the complete application as I go along. Of course, since all I am trying to do in this chapter is give you the first taste of refactoring, the application will be moderate in size and detail.
Calories Calculator Sample Application The application I am going to develop belongs to the medical field. It starts out as an application for calculating one’s recommended daily intake of calories. In order to implement this first requirement, I wrote a single event-handling routine. However, as new requirements are added and the application
79796c02.qxd:WroxPro
2/25/08
8:56 AM
Page 20
Part I: Introduction to Refactoring grows, a series of refactorings will have to take place. In its final stage, the application consists of five classes, three of them organized as a part of a small inheritance hierarchy. It is important to observe how complex solutions and refined structures appear only once the requirements and code grow in quantity. This is in accord with the principle of promoting the simplicity of the solution as its most important characteristic. Almost all refactorings I perform will be initiated only once a code smell is detected. While I am eliminating the code smell, I’ll show you that some underlying abstractions appear related to the domain I am working on: Patient, Male Patient, Female Patient, etc. You’ll see how work on the elimination of code smells and work on the design based on object-oriented principles converge into the creation of an efficient and robust solution. More complex, large-scale systems might benefit from a more elaborately designed solution right from the outset. In this case the application is so simple that any complex design up front would probably indicate that I have overengineered the solution.
Calories Calculator Application My client, a doctor, needed an application to calculate a recommended daily amount of calories. He had the formula and could perform the calculation manually, but using a calculator is too tedious and errorprone. The calculation is based on some of the patient’s personal data and differs depending on gender. Here are the formulas: ❑
Male: 66 + (6.3 × body weight in lbs.) + (12.9 × height in inches) – (6.8 × age in years)
❑
Female: 655 + (4.3 × weight in lbs.) + (4.7 × height in inches) – (4.7 × age in years)
The formulas seem pretty straightforward. I opened VB and designed the Calories Calculator form shown in Figure 2-1.
Figure 2-1
This single form contains patient data input controls and displays the recommended daily amount of calories in the Recommended Daily Amount read-only text box. The code to calculate the recommended daily amount was not that complicated either. All that was required was a click on the Calculate button and the following event-handling code that I wrote (Listing 2-1).
20
79796c02.qxd:WroxPro
2/25/08
8:56 AM
Page 21
Chapter 2: A First Taste of Refactoring Listing 2-1: The BtnCalculate_Click Code Private Sub BtnCalculate_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles BtnCalculate.Click If (RbtnMale.Checked) Then TxtCalories.Text = 66 + (6.3 * TxtWeight.Text) + _ (12.9 * (TxtFeet.Text * 12 + TxtInches.Text)) - _ (6.8 * TxtAge.Text) Else TxtCalories.Text = 655 + (4.3 * TxtWeight.Text) + _ (4.7 * (TxtFeet.Text * 12 + TxtInches.Text)) - _ (4.7 * TxtAge.Text) End If End Sub
I experimented with the form for a while, and it proved to work reasonably well. However, I noticed I would need to add some user input-verification routines, or the program might display some unpleasant error messages and exit. Listing 2-2 shows the code for my first version of the application.
Listing 2-2: Calories Calculator First Try Private Sub BtnCalculate_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles BtnCalculate.Click ‘Validate user input: ‘Validate height (feet) is numeric value If Not IsNumeric(TxtFeet.Text) Then MsgBox(“Feet must be a numeric value!”) TxtFeet.Select() Return End If ‘Validate height (inches) is numeric value If Not IsNumeric(TxtInches.Text) Then MsgBox(“Inches must be a numeric value!”) TxtInches.Select() Return End If ‘Validating weight is numeric value If Not IsNumeric(TxtWeight.Text) Then MsgBox(“Weight must be a numeric value greater then zero!”) TxtWeight.Select() Return End If ‘Validate age is numeric value If Not IsNumeric(TxtAge.Text) Then MsgBox(“Age must be a numeric value greater then zero!”) TxtAge.Select() Return End If ‘calculate amount of calories If (RbtnMale.Checked) Then ‘Apply one formula for male patient TxtCalories.Text = 66 + (6.3 * TxtWeight.Text) + _ (12.9 * (TxtFeet.Text * 12 + TxtInches.Text)) - _ (6.8 * TxtAge.Text)
Continued
21
79796c02.qxd:WroxPro
2/25/08
8:56 AM
Page 22
Part I: Introduction to Refactoring Listing 2-2: Calories Calculator First Try (continued) Else ‘Apply another formula for female patient TxtCalories.Text = 655 + (4.3 * TxtWeight.Text) + _ (4.7 * (TxtFeet.Text * 12 + TxtInches.Text)) - _ (4.7 * TxtAge.Text) End If End Sub
I have used the IsNumeric function in order to validate that values inputted by the user are numeric. If the user is inputting some invalid characters, a message box will appear informing him of the mistake, and the method will exit. So far the application is so simple that it does not deserve more effort. But you can already see that there is a lot of code mixed up in the single method, and that if the application starts to grow, I will probably need to restructure it in order to make it more atomic. At this point I thought that the job was done, and I sent the executable to my client.
Growing Requirements: Calculating Ideal Weight Not to my surprise, some time after submitting the application, I received an e-mail from my client asking me to add more features to the Calories Calculator. The required feature is ideal-weight calculation. The application should calculate the ideal weight for the patient based on height. Again, calculation depends on gender and there are separate formulas for men and women. Here are the formulas: ❑
Male: 50 + 2.3 kg per inch over 5 feet
❑
Female: 45.5 + 2.3 kg per inch over 5 feet
I decided to implement the requested functionality by modifying the existing form. I added a few controls, as shown in Figure 2-2.
Figure 2-2
22
79796c02.qxd:WroxPro
2/25/08
8:56 AM
Page 23
Chapter 2: A First Taste of Refactoring As I programmed the formula for calculating ideal weight, I made the following changes: ❑
I noticed that, as written, it applies only to people standing at or above five feet tall. In order to take this limitation into account, I added the verification code for the patient’s height. If the entered height value is less then five feet, a message box with information on the height limit will be displayed, and the method will exit.
❑
I also noticed that I would need to clear old results before each calculation, because otherwise they might confuse the user. I added a few lines of code that that clean text boxes each time the Calculate button is pressed. This way, even when invalid data are entered, results will be cleared before the user is informed of erroneous input.
Listing 2-3 shows how I implemented the new feature.
Listing 2-3: Calories Calculator with Ideal-Body-Weight Calculation Private Sub BtnCalculate_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles BtnCalculate.Click ‘Clear old results TxtCalories.Text = “” TxtIdealWeight.Text = “” TxtDistance.Text = “” ‘Validate user input If Not IsNumeric(TxtFeet.Text) Then MsgBox(“Feet must be a numeric value!”) TxtFeet.Select() Return End If If Not IsNumeric(TxtInches.Text) Then MsgBox(“Inches must be a numeric value!”) TxtInches.Select() Return End If If Not IsNumeric(TxtWeight.Text) Then MsgBox( _ “Weight must be a numeric value greater then zero!”) TxtWeight.Select() Return End If If Not IsNumeric(TxtAge.Text) Then MsgBox( _ “Age must be a numeric value greater then zero!”) TxtAge.Select() Return End If ‘Ideal body weight works only if taller than 5 ft If Not (TxtFeet.Text >= 5) Then MsgBox(“Height has to be equals or greater than 5 feet!”) TxtFeet.Select() End If ‘calculate amount of calories and ideal body weight If (RbtnMale.Checked) Then ‘Calculates ideal weight for men according to formula
Continued
23
79796c02.qxd:WroxPro
2/25/08
8:56 AM
Page 24
Part I: Introduction to Refactoring Listing 2-3: Calories Calculator with Ideal-Body-Weight Calculation (continued) TxtCalories.Text = 66 + (6.3 * TxtWeight.Text) + _ (12.9 * (TxtFeet.Text * 12 + TxtInches.Text)) - _ (6.8 * TxtAge.Text) ‘calculate ideal body weight TxtIdealWeight.Text = (50 + (2.3 * (((TxtFeet.Text - 5) _ * 12) + TxtInches.Text))) * 2.2046 Else ‘ Calculates ideal weight for women according to formula TxtCalories.Text = 655 + (4.3 * TxtWeight.Text) + _ (4.7 * (TxtFeet.Text * 12 + TxtInches.Text)) - _ (4.7 * TxtAge.Text) ‘calculate ideal body weight TxtIdealWeight.Text = (45.5 + (2.3 * (((TxtFeet.Text - 5) _ * 12) + TxtInches.Text))) * 2.2046 End If ‘Calculate and display distance from ideal weight TxtDistance.Text = TxtWeight.Text - TxtIdealWeight.Text End Sub
That was not so difficult. Actually, I just went into the old code and added new code to fulfill the new requirement of calculating ideal weight. ❑
When applying the formula, I subtracted five feet from the feet value and then converted the resulting height into inches: (((TxtFeet.Text - 5) * 12) + TxtInches.Text))).
❑
Then I applied the formula by multiplying the height in inches by 2.3 and adding to that result 50 in the case of a male patient and 45.5 in the case of a female patient.
❑
Since the result obtained this way represents the value in kilograms, I had to multiply it by 2.2046 in order to convert it to pounds.
However, now you may notice that the sole method in my application (BtnCalculate_Click) has become alarmingly long, and the ideal-body-weight calculation is not easy to understand. Adding the necessary code to existing classes and methods is a very typical way to add new requirements to existing applications. This can easily lead to poorly structured and tangled code. While even this short sample could benefit from refactoring at this point, I used my own judgment, based on commercial circumstances, and decided not to spend any more time on improving the application unless I was asked to implement more functionality. Again, I sent the application to my client. Somehow, I had a feeling that this would not be the end of the story!
Growing Requirements: Persisting Patient Data The next requirement the client asked for confirmed the fact that the application has demonstrated its value and is really being used by the client. The client expressed a need to somehow save patients’ historical data so that he could easily track the patients’ progress. I want to stop a moment and analyze this latest requirement. If you take a look at the existing form, you’ll notice that it provides no means of identifying the patient for whom the calculation is being performed. So
24
79796c02.qxd:WroxPro
2/25/08
8:56 AM
Page 25
Chapter 2: A First Taste of Refactoring for this requirement I would need to capture some data to use to identify the patient. I also need to persist the data and to display it. In conclusion, the Calories Calculator application now needs to provide the following functionality: ❑
Identifying the patient. The first step is getting some data that will be used to identify the patient. I decided to use patient names and Social Security numbers (SSNs). Names make it easy for the doctor to identify the patient, and the SSN prevents any possible mix-up in case patients have the same names.
❑
Keeping the calculation operation and the operation of persisting patient data separate. At first I was tempted to add some code to the existing BtnCalculate_Click event-handling routine and to use it to save patient data. But looking at it from the users’ perspective, I decided that these two actions should be separated. This way, the users will be able to perform some ad hoc calculations without having to input a patient’s name and SSN. Also, the users can save data only when they are sure that all entries are correct.
❑
Displaying a patient’s history. Users will need some easy way to display the patient’s history.
All of this functionality requires additional changes to the form. I added a few controls (for patient data and history capability), and the form ended up looking like the one in Figure 2-3.
Figure 2-3
Once I made the changes, the form worked as follows: ❑
The Patient personal data section added data necessary to identify and differentiate patients.
❑
The Save and View buttons were added so the users can persist patient data or display patient history.
❑
The Calculate button maintained its function.
25
79796c02.qxd:WroxPro
2/25/08
8:56 AM
Page 26
Part I: Introduction to Refactoring There are a few alternatives for implementing persistence of a patient’s history. You could use a flat file, a database like Access, or even a spreadsheet. The simplest solution, however, is probably to use an XML file, so I decided to do that. But before I dedicate myself to implementing new requirements, the time has come to look at the code produced so far and to deal with any outstanding issues. I have been postponing this so far and have simply added more code to the existing method, since I wasn’t sure I was going to continue to work on the application. However, now that the application has grown and more complex requirements are to be implemented, I need to restructure the existing code. The first and very obvious problem is that I have a single method implementing the whole functionality of the application. This method has grown out of proportion. Long methods are difficult to understand and reuse. I’ll return to the issue of persisting patient data later in the chapter, but I want to deal first with the outstanding issues that can stand in the way of implementing the new functionality, and that requires some refactoring. You can see that I haven’t really gone that far with implementation, but some smells have already managed to creep inside the code. I will deal with these smells before I go any further.
Refactoring in Action So far I haven’t been paying too much attention to how I programmed the Calories Calculator application. It was rather simple, and this straightforward approach worked well. With new requirements, things can get more complicated; bearing in mind the interest the application created, it is an auspicious moment to give it some more thought. If I continue to simply add new functionality, the application will soon grow out of control and become impossible to maintain. I am going to reorganize the code so its internal structure is improved. I’ll do it without changing its behavior; the calorie calculation will happen just as it used to. If you remember the definition of refactoring (discussed thoroughly in Chapter 1), you’ll see that this is exactly what I plan to do with the Calories Calculator. Unit Testing: Since I am now starting to change the code in ways that can have adverse effects and even introduce bugs, I need to make sure that application consistency is maintained at all times. I can do this by developing a set of tests. I will write down a few imaginary patients’ data and the results of the calculations the application is performing. Then I will run the application each time I make a change to it and execute all those tests, making sure correct results are returned. I will also test for invalid data. This sounds tedious, but it’s necessary. Fortunately, there is a much better way to do this — as you’ll see in Chapter 3, in which I discuss unit testing.
In this section, I will start off by looking into the code created so far. The only routine, BtnCalculate _Click, looks alarmingly long. I used comments in order to separate it into several part; even when commented, long methods are much more difficult to understand. In refactoring terminology, this method displays a “long method smell.” The solution is to split it into several shorter methods.
26
79796c02.qxd:WroxPro
2/25/08
8:56 AM
Page 27
Chapter 2: A First Taste of Refactoring
Decomposing the BtnCalculate_Click Method One very natural way of dealing with a long method is to separate it into multiple shorter methods. Doing this with BtnCalculate_Click is really rather obvious. If you take a look at Listing 2-3, you’ll see that the code comments essentially segment the code into modules that indicate to you where you should extract the code into the new methods. This will not change the behavior of the code, but it will make the code better organized and make the BtnCalculate_Click method easier to read. I will move the code into the new method and make BtnCalculate_Click call the new method. The next sections show how I take each segment from the method and make that segment its own method.
The “Clear Old Results” Segment This segment is transformed into ClearResults private method: Private Sub ClearResults() TxtCalories.Text = “” TxtIdealWeight.Text = “” TxtDistance.Text = “” End Sub
The “Validate User Input” Segment This segment is transformed into the ValidateUserInput private method: Private Function ValidateUserInput() As Boolean If Not IsNumeric(TxtFeet.Text) Then MsgBox(“Feet must be a numeric value!”) TxtFeet.Select() Return False End If If Not IsNumeric(TxtInches.Text) Then MsgBox(“Inches must be a numeric value!”) TxtInches.Select() Return False End If If Not IsNumeric(TxtWeight.Text) Then MsgBox( _ “Weight must be a numeric value greater then zero!”) TxtWeight.Select() Return False End If If Not IsNumeric(TxtAge.Text) Then MsgBox( _ “Age must be a numeric value greater then zero!”) TxtAge.Select() Return False End If ‘Ideal body weight works only if taller than 5 ft If Not (TxtFeet.Text >= 5) Then MsgBox(“Height has to be equals or greater than 5 feet!”) TxtFeet.Select() Return False End If Return True End Function
27
79796c02.qxd:WroxPro
2/25/08
8:56 AM
Page 28
Part I: Introduction to Refactoring This method needs to return the result of user input verification. In case the data are not valid, I need to interrupt the execution of the BtnCalculate_Click method. You will see that approach when you take a look at the refactored BtnCalculate_Click method.
The “Calculate and Display Distance from Ideal Weight” Segment This segment can be transformed into the DistanceFromIdealWeight private method, so it receives two parameters, actual weight and ideal weight: Public Function DistanceFromIdealWeight( _ ByVal actualWeightInPounds As Decimal, _ ByVal idealWeightInPounds As Decimal) As Decimal Return actualWeightInPounds - idealWeightInPounds End Function
Notice that in order to avoid any confusion in regards to measurements used, I added the name of the measurement to the parameter names — actualWeightInPounds and idealWeightInPounds.
Calculating Calories and Ideal Weight by Gender The last remaining section is a little less self-explanatory than the others. The application will calculate calories or ideal weight, but it does so according to gender. So the easiest way to approach this issue is to turn the section into four private methods: Public Function DailyCaloriesRecommendedMan( _ ‘Parameter name includes measurement type info: pound, inch ByVal weightInPounds As Decimal, _ ByVal heightInInches As Decimal, _ ByVal age As Integer) As Decimal Return 66 + (6.3 * weightInPounds) + _ (12.9 * heightInInches) - (6.8 * age) End Function Public Function DailyCaloriesRecommendedWoman( _ ByVal weightInPounds As Decimal, _ ByVal heightInInches As Decimal, _ ByVal age As Integer) As Decimal Return 655 + (4.3 * weightInPounds) + _ (4.7 * heightInInches) - (4.7 * age) End Function Public Function IdealBodyWeightMan( _ ByVal heightInInches As Decimal) As Decimal Return (50 + (2.3 * (heightInInches - 60))) * 2.2046 End Function Public Function IdealBodyWeightWoman( _ ByVal heightInInches As Decimal) As Decimal Return (45.5 + (2.3 * (heightInInches - 60))) * 2.2046 End Function
Instead of using code comments to indicate the intention behind a few lines of code, I now use a method to achieve the same goal. Method names clearly describe their purpose. This way the code is a lot easier to understand and reuse. As I move code to new methods, I eliminate the comments.
28
79796c02.qxd:WroxPro
2/25/08
8:56 AM
Page 29
Chapter 2: A First Taste of Refactoring The technique of decomposing large methods by extracting pieces of method into the new methods is called extract method refactoring. I discuss this refactoring in detail in Chapter 9 and Chapter 10.
The BtnCalculate_Click Method after Method Extraction What does the BtnCalculate_Click routine look like now? You can see this in Listing 2-4.
Listing 2-4: The BtnCalculate_Click Method after the Decomposition Private Sub BtnCalculate_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles BtnCalculate.Click ClearResults() If ValidateUserInput() = False Then Return If (RbtnMale.Checked) Then TxtCalories.Text = DailyCaloriesRecommendedMan( _ TxtWeight.Text, TxtFeet.Text * 12 + TxtInches.Text, _ TxtAge.Text) TxtIdealWeight.Text = IdealBodyWeightMan(TxtAge.Text) Else TxtCalories.Text = DailyCaloriesRecommendedWoman( _ TxtWeight.Text, TxtFeet.Text * 12 + TxtInches.Text, _ TxtAge.Text) TxtIdealWeight.Text = IdealBodyWeightWoman(TxtAge.Text) End If TxtDistance.Text = DistanceFromIdealWeight( _ TxtWeight.Text, TxtIdealWeight.Text) End Sub
It’s definitely a lot shorter method now. Also, the comments are gone, since they are of no use any more. New method names are equally expressive. The result of this transformation is multiple, shorter methods that do exactly what the original single method did. The benefits of more granular code are improved clarity and greater possibility for reuse of new shorter methods.
Discovering New Classes In object-oriented programming you often use classes to establish links in code with real-world phenomena. This mapping between elements in the code and domain entities can greatly improve readability and comprehension of code. So far, I didn’t even attempt to model the application code according to object-oriented principles. Instead, I have a single class inheriting System.Windows.Forms.Form, a base class for a basic GUI element. This class now sports a few methods I created by decomposing the event-handling method. If you analyze for the moment the code created so far, you can see that the newly created methods can be divided into two distinct groups: ❑
Methods that are related to the behavior of the visual elements, the GUI, like the ClearResults method
❑
Methods that embed the medical formulas that perform the calculations, like IdealBodyWeightMan and DailyCaloriesRecommendedWoman
29
79796c02.qxd:WroxPro
2/25/08
8:56 AM
Page 30
Part I: Introduction to Refactoring So far the FrmCaloriesCalculator class is the only one in the project. It extends the Form class, a base class for windows or dialog boxes, which is a fundamental building block for the desktop interface. This class works well for our first group of methods, but the second group of methods, concerned with medical calculations, clearly does not belong to it. By running the Calories Calculator application, you can observe that its purpose is collecting and computing certain data. This data represents human characteristics. In this case, as you can observe on the form itself, this data refers to patients.
Defining the Patient Class Since this application deals with a medical problem, it makes sense to structure the data inside a new class called Patient. Listing 2-5 shows the class definition.
Listing 2-5: The New Patient Data Class Public Class Patient Private ssnValue As String Private firstNameValue As String Private lastNameValue As String Private heightInInchesValue As Decimal Private weightInPoundsValue As Decimal Private ageValue As Integer Public Property SSN() As String Get Return ssnValue End Get Set(ByVal Value As String) ssnValue = Value End Set End Property Public Property FirstName() As String Get Return firstNameValue End Get Set(ByVal Value As String) firstNameValue = Value End Set End Property Public Property LastName() As String Get Return lastNameValue End Get Set(ByVal Value As String) lastNameValue = Value End Set End Property Public Property HeightInInches() As Decimal Get Return heightInInchesValue End Get Set(ByVal Value As Decimal) heightInInchesValue = Value
30
79796c02.qxd:WroxPro
2/25/08
8:56 AM
Page 31
Chapter 2: A First Taste of Refactoring Listing 2-5: The New Patient Data Class (continued) End Set End Property Public Property WeightInPounds() As Decimal Get Return weightInPoundsValue End Get Set(ByVal Value As Decimal) weightInPoundsValue = Value End Set End Property Public Property Age() As Integer Get Return ageValue End Get Set(ByVal Value As Integer) ageValue = Value End Set End Property End Class
So far this class is a simple data structure, consisting only of properties. While grouping the data inside a new data type is definitely a step forward in attempting to improve the design of the application, having a class that has only data and no behavior is another hint that I need to continue with improvements on the design of the program. A class with only data and no behavior is referred to as a data class and is considered a code smell.
Moving Methods to the Patient Class If you now go back to the second group of methods, the ones performing medical computations, you can see they operate on exactly the same patient data that the Patient class is used to describe. So, according to some basic object-oriented principles, I should keep operations close to the data they refer to. I can do the following:
1.
Move the DailyCaloriesRecommendedMan, DailyCaloriesRecommendedWoman, IdealBodyWeightMan, and IdealBodyWeightWoman methods to the Patient class.
2.
Because, methods have access to private variables of the class, I can make these four methods use Patient class properties instead of receiving parameters, and eliminate parameters from the method declaration because the body of the method refers to the same names I have given to properties in the Patient class. Visual Studio takes care of adjusting the case of letters. Just to illustrate it: Public Function DailyCaloriesRecommendedWoman( _ ByVal weightInPounds As Decimal, _ ByVal heightInInches As Decimal, _ ByVal age As Integer) As Decimal Return 655 + (4.3 * weightInPounds) + _ (4.7 * heightInInches) - (4.7 * age) End Function
31
79796c02.qxd:WroxPro
2/25/08
8:56 AM
Page 32
Part I: Introduction to Refactoring is transformed into: Public Function DailyCaloriesRecommendedWoman() As Decimal Return 655 + (4.3 * WeightInPounds) + _ (4.7 * HeightInInches) - (4.7 * Age) End Function Weight, Height, and Age all now refer to properties of the Patient class. In this case, parameter and property names coincided by chance. Normally this would require changing the body of the methods so that they refer to properties and not method parameters. You can see how this works out in a more graphic way in Figure 2-4, where the DailyCaloriesRecommendedWoman method references the Height property in the same Patient class.
Public Class Patient Private heightInInchesValue As Decimal '... Public Property HeightInInches() As Decimal Get Return heightInInchesValue End Get Set(ByVal Value As Decimal) heightInInchesValue = Value End Set End Property Parameters eliminated '... Public Function DailyCaloriesRecommendedWoman() _ As Decimal Return 655 + (4.3 * WeightInPounds) + _ (4.7 * HeightInInches) - (4.7 * Age) End Function '... End Class HeightInInches references Patient class property
Figure 2-4
The Patient class is now a fully fledged class consisting of properties and methods, grouping the data and related behavior inside a cohesive whole. I discuss Extract class and Move Method refactoring in detail in Chapter 11.
Narrowing the Patient Class Interface If you have been paying attention so far, then you might have noticed that I didn’t treat a very important piece of data related to a patient in any special way. I am referring to gender. Unless I have this information available inside the Patient class along with the rest of the patient data, I will not be able to move the last function left, DistanceFromIdealWeight, to make it a method in the Patient class. The method needs to know both the actual and the ideal body weight in order to perform the calculation. But after the latest refactorings, I have two methods that can give me ideal body weight: IdealBodyWeightMan and IdealBodyWeightWoman. So which one of the two should the DistanceFromIdealWeight method call?
32
79796c02.qxd:WroxPro
2/25/08
8:56 AM
Page 33
Chapter 2: A First Taste of Refactoring Creating the Gender Enum I’ll set up the Patient class with another property, Gender. In this case, gender can have one of two values: male or female. To express this, I’ll be better off if I add another element to the project — the enumerator Gender. This way, DistanceFromIdealWeight can check the patient’s gender and call the correct calculation method for ideal body weight. Public Enum Gender Male Female End Enum
Adding a Gender Property to the Patient Class It is quite easy now to add a Gender property to the Patient class that is typed as Enum Gender. It will help discern a gender for each patient instance. Do what you usually do when you add a new property to a class:
1.
Add another private variable to keep the value of the Gender property: Private genderValue As Gender
2.
Add a typical body of the property Gender: Public Property Gender() As Gender Get Return genderValue End Get Set(ByVal Value As Gender) genderValue = Value End Set End Property
Moving DistanceFromIdealWeight to Patient Class Once the Gender enumerator is created and the Gender property is added, I can move the DistanceFromIdealWeight method to the Patient class. Again, I eliminate parameters and add a simple condition that uses the Gender property, so it works correctly: Public Function DistanceFromIdealWeight() As Decimal If Gender.Male Then Return WeightInPounds - IdealBodyWeightMan() Else Return WeightInPounds - IdealBodyWeightWoman() End If End Function
The method now works fine.
33
79796c02.qxd:WroxPro
2/25/08
8:56 AM
Page 34
Part I: Introduction to Refactoring At this point, you can see that I have succeeded in moving all the relevant logic to the Patient class. This is a great feat, since the potential reusability of the code has improved greatly. You can now easily imagine the Patient class being used in another module someone might develop, or even in some other, future application. This was not at all the case with a sole FrmCaloriesCalculator class.
Now, before I modify FrmCaloriesCalculator so it uses the Patient class, I want take another look at the methods in the new class. I suspect there’s more refactoring that can be performed here before I move on. Since I now have gender data available to code in the class, I wonder if having different methods for the male and female patients is really necessary. Maybe I could move the gender logic inside the class somehow and release the client from the burden of tracking the gender information? Shouldn’t the Gender class be able to manage gender-related logic on its own?
Putting Conditional Logic in the Patient Class A lot of method names in the Patient class, like DailyCaloriesRecommendedMan and IdealBodyWeightWoman, terminate with the word Man or Woman, indicating that the method is applicable to patients of a certain gender only. This will result in clients (and here when I say client, I mean anyone using the class) having to think about this condition and having to write code like this: If myPatient.Gender = Gender.Male Then idealWeight = myPatient. IdealBodyWeightMan() Else idealWeight = myPatient. IdealBodyWeightWoman() End If
If you compare this to the DistanceFromIdealWeight method, you can see quite a different approach there. The method itself contains this logic, because gender data are already available in the Patient class. There are two obvious benefits to moving this conditional logic into the Patient class itself, as with the DistanceFromIdealWeight method: ❑
There are fewer publicly visible methods in the Patient class, so things are a lot simpler for anyone using this class.
❑
Because classes are generally referenced from more than one place, moving conditional code inside the method prevents a lot of duplication in the client code. No doubt anywhere the Patient class is used, the code would contain a lot of Ifs and end up looking like this:
If myPatient.Gender = Gender.Male Then ‘... male patient related code Else ‘... female patient related code End If
In short, I’m going to be better off if I transform all the methods along the same lines as DistanceFromIdealWeight. So I’ll need to add a few new methods to the Patient class. The next couple of subsections takes a look at how I do that.
34
79796c02.qxd:WroxPro
2/25/08
8:56 AM
Page 35
Chapter 2: A First Taste of Refactoring Encapsulating DailyCaloriesRecommendedMan and DailyCaloriesRecommendedWoman First I’ll make the methods DailyCaloriesRecommendedMan and DailyCaloriesRecommendedWoman visible through a single method, DailyCaloriesRecommended: Public Function DailyCaloriesRecommended() As Decimal If Gender.Male Then Return DailyCaloriesRecommendedMan() Else Return DailyCaloriesRecommendedWoman() End If End Function
Encapsulating Ideal-Body-Weight Calculation Then the same fate awaits the ideal-body-weight methods. Methods IdealBodyWeightMan and IdealBodyWeightWoman are now visible through a single method, IdealBodyWeight: Public Function IdealBodyWeight() As Decimal If Gender.Male Then Return IdealBodyWeightMan() Else Return IdealBodyWeightWoman() End If End Function
Making Encapsulated Methods Private Finally, I can’t forget to reduce the visibility of all the SomeFunctionMan and SomeFunctionWoman methods to private. The declarations now look like this: Private Private Private Private
Function Function Function Function
DailyCaloriesRecommendedMan() As Decimal DailyCaloriesRecommendedWoman() As Decimal IdealBodyWeightMan() As Decimal IdealBodyWeightWoman() As Decimal
With those declarations in place, I have managed to further encapsulate the logic inside the Patient class. This way no unnecessary logic is exposed to the Patient class client. As you know, encapsulation is one of the pillars of object orientation, and taking this step will make your code more robust.
Restructuring the DistanceFromIdealWeight Method I still have one more method to deal with. The DistanceFromIdealWeight code can be written in a more expressive manner, so it is easier to read and understand. While containing the same condition, and in that sense being similar to other methods, DistanceFromIdealWeight does not yet use private methods. I will add two private methods, DistanceFromIdealWeightMan and DistanceFromIdealWeightWoman, structuring them just like the methods DailyCaloriesRecommended and IdealBodyWeight: Private Function DistanceFromIdealWeightMan() As Decimal Return WeightInPounds - IdealBodyWeightMan() End Function
35
79796c02.qxd:WroxPro
2/25/08
8:56 AM
Page 36
Part I: Introduction to Refactoring Private Function DistanceFromIdealWeightWoman() As Decimal Return WeightInPounds - IdealBodyWeightWoman() End Function DistanceFromIdealWeight now looks like this: Public Function DistanceFromIdealWeight() As Decimal If Gender.Male Then Return DistanceFromIdealWeightMan() Else Return DistanceFromIdealWeightWoman() End If End Function
Again, I performed method extraction. This way, the method is structured along the same lines as other methods, and you will see the benefits of this modification later in the chapter, when I create the Patient class hierarchy. In short, I have simplified the class for the client. In fact, you could say that class interface has been reduced, thereby simplifying the use of the classes. (Here by interface I refer to all public elements in the class.)
Making FrmCaloriesCalculator Use the Patient Class Now I can modify the FrmCaloriesCalculator and make it use the Patient class for all related calculations. I’ll add a Patient property to the class. The code looks like this: Public Class FrmCaloriesCalculator ‘ ... Private patientValue As Patient Private Property Patient() As Patient Get Return patientValue End Get Set(ByVal Value As Patient) patientValue = Value End Set End Property ‘ ...
This instance is created in the BtnCalculate_Click event-handling routine. I’ll add the following line to it: Me.Patient = New Patient
Now I need to set Patient’s properties: If (RbtnMale.Checked) Then Me.Patient.Gender = Gender.Male Else Me.Patient.Gender = Gender.Female End If Patient.HeightInInches = TxtHeight.Text
36
79796c02.qxd:WroxPro
2/25/08
8:56 AM
Page 37
Chapter 2: A First Taste of Refactoring Patient.Weight() = TxtWeight.Text Patient.Age = TxtAge.Text
Next, the Patient instance is used to perform the required calculations: TxtCalories.Text = Patient.DailyCaloriesRecommended TxtIdealWeight.Text = Patient.IdealBodyWeight TxtDistance.Text = Patient.DistanceFromIdealWeight
Finally, to see how BtnCalculate_Click ends up, take a look at Listing 2-6.
Listing 2-6: The BtnCalculate_Click Method After the New Patient Class Has Been Identified Private Sub BtnCalculate_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles BtnCalculate.Click ClearResults() If ValidateUserInput() = False Then Return ‘Creating new instance of Patient class Me.Patient = New Patient ‘Setting patient properties with data from form If (RbtnMale.Checked) Then Me.Patient.Gender = Gender.Male Else Me.Patient.Gender = Gender.Female End If Patient.HeightInInches = _ (TxtFeet.Text * 12) + TxtInches.Text Patient.WeightInPounds() = TxtWeight.Text Patient.Age = TxtAge.Text ‘Outputting calculated values to form TxtCalories.Text = Patient.DailyCaloriesRecommended TxtIdealWeight.Text = Patient.IdealBodyWeight TxtDistance.Text = Patient.DistanceFromIdealWeight End Sub
By extracting the Patient class and moving the logic related to medical calculation to this newly created class, I have laid the foundations of object-oriented design in the application. I have moved from an exclusively event-driven paradigm to an object-oriented paradigm. While solely event-driven programming can be effective on the small scale, it has to be coupled with object-oriented design to be as effective for more complex applications.
Creating the Patient Class Hierarchy Now that I have made all the preceding important steps in redesigning the application, it is time to take another look at the code I ended up with. After the latest changes, a certain uniformity in all of the methods is becoming apparent. They all contain the same conditional code: If Gender.Male Then ‘Some code Else ‘Some code End If
37
79796c02.qxd:WroxPro
2/25/08
8:56 AM
Page 38
Part I: Introduction to Refactoring In effect, this code — condition tests for a very specific attribute of Patient, the gender — has been duplicated. I might as well have used copy-paste to create it. This is a very good indication that some further refinements are required. I have just identified another major code smell. Code duplication generally indicates a serious flaw in your design: you wrote the same code more than once, meaning that if you need to modify this logic, you will need to find all occurrences of it in your code. It is all too easy to miss other occurrences when the section that is of interest at that moment is modified. It also means that you have more code than necessary to worry about. There is a way to go about fixing this problem. In this case I’ll create two new classes: MalePatient and FemalePatient. They will inherit the Patient class. Then I’ll move all gender-specific code to either MalePatient or FemalePatient, as appropriate. (You can take a look at Chapter 12, where I deal with Extract Super Class and other inheritance-related refactorings.) The mechanics for this transformation are a bit more complicated. They are as follows:
1.
First, I need to add two new classes that inherit the Patient class: Public Class MalePatient Inherits Patient
and Public Class FemalePatient Inherits Patient
2.
The next step involves moving private methods with names ending with Male to the MalePatient class, and private methods with names ending with Female to the FemalePatient class. This is an example of pull-down method refactoring. Pulling down refers to moving a method from a class in the hierarchy to another class lower in the inheritance hierarchy.
3.
Next the Patient class declares methods that will have their implementation in its subclasses, MalePatient and FemalePatient. Since only some of the Patient class members are implemented, this class is abstract and must be marked by the MustInherit keyword: Public MustInherit Class Patient
4.
Methods in the Patient class that are implemented in MalePatient and FemalePatient need to be marked as abstract by means of the MustOverride keyword. Once these methods are marked as abstract, their bodies have to be removed. DistanceFromIdealWeight, DailyCaloriesRecommended, and IdealBodyWeight in the Patient class now look like this: Public MustOverride Function DistanceFromIdealWeight() As Decimal Public MustOverride Function DailyCaloriesRecommended() As Decimal Public MustOverride Function IdealBodyWeight() As Decimal
Now I have the two new classes I need, MalePatient and FemalePatient, each containing the code specific to the appropriate gender.
38
79796c02.qxd:WroxPro
2/25/08
8:56 AM
Page 39
Chapter 2: A First Taste of Refactoring Dealing with Gender-Specific Methods Now, in the MalePatient and FemalePatient subclasses, I still have methods — methods that provide implementation for abstract methods in the Patient class — ending with the words Man and Woman. It is time to change this. I will eliminate Man from method names in the MalePatient class and Woman from method names in the FemalePatient class. This way, the names of the methods in MalePatient and FemalePatient will coincide with method names in the Patient class. I’ll also change the visibility of the methods to public. Also, at this point I am getting a timely reminder from our IDE, visible in the Task List window: “Class ‘FemalePatient’ must either be declared ‘MustInherit’ or override the following inherited ‘MustOverride’ member(s): Public MustOverride Function DistanceFromIdealWeight() As Decimal, Public MustOverride Function DailyCaloriesRecommended() As Decimal, Public MustOverride Function IdealBodyWeight() As Decimal” The same error is present for the MalePatient class. The warning is spot on, and I’ll do exactly what the IDE suggests — add the Overrides keyword to all the methods in the MalePatient and FemalePatient classes (see Figure 2-5).
Public MustInherit Class Patient '... Public MustOverride Function DailyCaloriesRecommended() _ As Decimal Public Class MalePatient Inherits Patient
If Patient Male, method implemented in MalePatient
Public Overrides Function DailyCaloriesRecommended() _ As Decimal Return 66 + (6.3 * WeightInPounds) + _ (12.9 * HeightInInches) - (6.8 * Age) End Function '... Public Class FemalePatient Inherits Patient Public Overrides Function DailyCaloriesRecommended() _ As Decimal Return 655 + (4.3 * WeightInPounds) + _ (4.7 * HeightInInches) - (4.7 * Age) If Patient Female, method End Function implemented in FemalePatient '...
Figure 2-5
So I started with some conditional code that was gender-specific and managed to replace it with an inheritance hierarchy. This kind of refactoring is called replacing conditional logic with polymorphism. You can take a look at Chapter 12 for more on polymorphism.
39
79796c02.qxd:WroxPro
2/25/08
8:56 AM
Page 40
Part I: Introduction to Refactoring Removing Gender-Specific Code from the DistanceFromIdealWeight Method One more step is needed to eliminate the last error present in the code. Since the DistanceFromIdealWeight method refers to IdealBodyWeightMan in the MalePatient class and to IdealBodyWeightWoman in the FemalePatient class, it has to be changed so that it refers to the IdealBodyWeight method. In the previous section I eliminated gender-specific methods; now, in both classes, I modify the method DistanceFromIdealWeight so it looks the same: Public Overrides Function DistanceFromIdealWeight() As Decimal Return WeightInPounds - IdealBodyWeight() End Function
Looking at the Patient Classes Hierarchy Now it’s time to have a look at the complete hierarchy code. I end up with the classes looking like this (Listing 2-7).
Listing 2-7: Patient Classes Hierarchy Public Class FemalePatient Inherits Patient Public Overrides Function DailyCaloriesRecommended() As Decimal Return 655 + (4.3 * WeightInPounds) + _ (4.7 * HeightInInches) - (4.7 * Age) End Function Public Overrides Function IdealBodyWeight() As Decimal Return (45.5 + (2.3 * (HeightInInches - 60))) * 2.2046 End Function End Class Public Class MalePatient Inherits Patient Public Overrides Function DailyCaloriesRecommended() As Decimal Return 66 + (6.3 * WeightInPounds) + _ (12.9 * HeightInInches) - (6.8 * Age) End Function Public Overrides Function IdealBodyWeight() As Decimal Return (50 + (2.3 * (HeightInInches - 60))) * 2.2046 End Function End Class Public MustInherit Class Patient Private ssnValue As String Private firstNameValue As String
40
79796c02.qxd:WroxPro
2/25/08
8:56 AM
Page 41
Chapter 2: A First Taste of Refactoring Listing 2-7: Patient Classes Hierarchy (continued) Private Private Private Private Private
lastNameValue As String heightInInchesValue As Decimal weightInPoundsValue As Decimal ageValue As Integer genderValue As Gender
Public Property Gender() As Gender Get Return genderValue End Get Set(ByVal Value As Gender) genderValue = Value End Set End Property Public Property SSN() As String Get Return ssnValue End Get Set(ByVal Value As String) ssnValue = Value End Set End Property Public Property FirstName() As String Get Return firstNameValue End Get Set(ByVal Value As String) firstNameValue = Value End Set End Property Public Property LastName() As String Get Return lastNameValue End Get Set(ByVal Value As String) lastNameValue = Value End Set End Property Public Property HeightInInches() As Decimal Get Return heightInInchesValue End Get Set(ByVal Value As Decimal) heightInInchesValue = Value End Set End Property Public Property WeightInPounds() As Decimal Get Return weightInPoundsValue End Get
Continued
41
79796c02.qxd:WroxPro
2/25/08
8:56 AM
Page 42
Part I: Introduction to Refactoring Listing 2-7: Patient Classes Hierarchy (continued) Set(ByVal Value As Decimal) weightInPoundsValue = Value End Set End Property Public Property Age() As Integer Get Return ageValue End Get Set(ByVal Value As Integer) ageValue = Value End Set End Property Public MustOverride Function DailyCaloriesRecommended() _ As Decimal Public MustOverride Function IdealBodyWeight() As Decimal Public Function DistanceFromIdealWeight() As Decimal Return WeightInPounds - IdealBodyWeight() End Function End Class
This is a large listing, but notice that no comments are really necessary. The design is simple and the intent is understandable from the names used for the classes’ methods and properties. Now it’s time to go back to the FrmCaloriesCalculator class and make use of the newly created classes.
The BtnCalculate_Click Method Using a Patient Classes Hierarchy To complete the latest changes I now need to modify the client, the FrmCaloriesCalculator class. In the BtnCalculate_Click method I should create the correct subclass of the Patient hierarchy, based on user input and the value of the gender radio button. This is demonstrated in Listing 2-8.
Listing 2-8: The BtnCalculate_Click Method, After the MalePatient and FemalePatient Classes Are Identified Private Sub BtnCalculate_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles BtnCalculate.Click ClearResults() If ValidateUserInput() = False Then Return ‘Making use of MalePatient and FemalePatient classes: ‘creating new instance If (RbtnMale.Checked) Then Me.Patient = New MalePatient Else Me.Patient = New FemalePatient End If Patient.HeightInInches = _ (TxtFeet.Text * 12) + TxtInches.Text Patient.WeightInPounds() = TxtWeight.Text Patient.Age = TxtAge.Text
42
79796c02.qxd:WroxPro
2/25/08
8:56 AM
Page 43
Chapter 2: A First Taste of Refactoring Listing 2-8: The BtnCalculate_Click Method, After the MalePatient and FemalePatient Classes Are Identified (continued) TxtCalories.Text = Patient.DailyCaloriesRecommended TxtIdealWeight.Text = Patient.IdealBodyWeight TxtDistance.Text = Patient.DistanceFromIdealWeight End Sub
This looks a lot better. I’d say I’m almost done. Still, before calling it a day, I notice two details. Since gender information is now represented by subclasses, I have no need for a Gender property in the Patient class. If you look carefully, you’ll see it is not used in any of the other classes. I can eliminate it. This leaves the Gender enumeration redundant. Dead code is yet another smell I need to remedy, so I’ll eliminate the Gender enumerator in addition to the Gender property. I call this eliminate dead code refactoring. Eliminating dead code is discussed in more detail in Chapter 7.
“Pulling Up” the DistanceFromIdealWeight Method When I made modifications to the body of the DistanceFromIdealWeight method in the FemalePatient and MalePatient classes, I realized one detail. Both methods ended up looking the same: Public Overrides Function DistanceFromIdealWeight() As Decimal Return WeightInPounds - IdealBodyWeight() End Function
This is actually duplicate code, a smell we must try to eliminate. The solution is to move the method implementation back to the Patient class. I’ll eliminate the method from the FemalePatient and MalePatient classes and copy it back to the Patient class: Public Function DistanceFromIdealWeight() As Decimal Return WeightInPounds - IdealBodyWeight() End Function
Pulling methods and properties up in the hierarchy is another type of refactoring that is very useful in eliminating duplicated code. It’s generally referred to as pull-up method or pull-up property refactoring. I deal with these refactorings in detail in Chapter 12. I have just created an object-oriented structure for the domain-related logic and separated it from the GUI-related logic. I have also grouped patient data inside a single structure, and this is exactly the data I need to persist. So all this work has prepared a good starting point for the implementation of persistence functionality in the application, which is the subject of the next section.
Implementing the Persistence Functionality Generally, when I program, I refactor all the time. Each time I add a method or a property, or resolve a bug, I take a look at the code, searching for the code smells. In this specific case the application was very simple, and I was postponing refactoring for a while. So the smells accumulated, and I had to dedicate more time to refactoring procedures in a single go. Normally, refactoring is an integral part of the coding process.
43
79796c02.qxd:WroxPro
2/25/08
8:56 AM
Page 44
Part I: Introduction to Refactoring By any account, the time used to refactor the application was well spent. Now the code is better organized and a lot easier to read, comprehend, and maintain. Since it is more modular, changes I apply from now on will be more isolated and will have a more limited effect. Also, I have managed to group and gather the data related to the patient — again, exactly the data I need to persist — into a single structure, simplifying the job of implementing this functionality. The client has given me a blank slate as to how to implement the persistence functionality, and there are a number of ways I can go about saving patients’ history: I can use a database like Access, a textual file, even an Excel worksheet. Since .NET has very good support for XML, I have decided to save patient history into an XML file. This will save me some work during the implementation, and given that XML is also easily readable by humans, it can save me even more work in implementing GUI elements for viewing the data.
Saving the Data As usual, I start off by filling in the event-handling methods. Remembering the recent experience I had with the application, I decide to put data-validation code in a separate ValidatePatientPersonalData private method, much like the existing ValidateUserInput method.
Validating a Patient’s Personal Data This ValidatePatientPersonalData method is called from the BtnSave_Click method, and it checks that a valid Social Security number and last and first name are entered. It looks just as you would expect: Private Function ValidatePatientPersonalData() As Boolean If Not IsNumeric(TxtSSNFirstPart.Text) Or _ Not IsNumeric(TxtSSNSecondPart.Text) Or _ Not IsNumeric(TxtSSNThirdPart.Text) Then MsgBox(“You must enter valid SSN!”) Return False End If If Not TxtFirstName.Text.Trim.Length > 0 Then MsgBox(“You must enter patient’s first name!”) Return False End If If Not TxtLastName.Text.Trim.Length > 0 Then MsgBox(“You must enter patient’s last name!”) Return False End If Return True End Function
Next I must make sure that the calculation has been performed before I save the measurement. For that purpose it’s easiest if I directly call the BtnCalculate_Click method. It will check the validity of the data, create a patient instance, perform the calculation, and visualize it on the form.
Creating the XML File Used for Patient Data Persistence Now I must take care of persisting the measurement to the file. Since I am not going to distribute a separate XML file, I must create the file programmatically. So, when saving the data, first I must check for the existence of the XML file. If it exists, I want to append the data so the existing history is not lost. In this case I also need to look for the previous measurements by the same patient. It is best if I structure the XML so all measurements by one patient are under the same patient element.
44
79796c02.qxd:WroxPro
2/25/08
8:56 AM
Page 45
Chapter 2: A First Taste of Refactoring For that purpose I must define the format of the XML file. The format I am going to use is shown in Listing 2-9.
Listing 2-9: The Patient-History XML File Format <patientsHistory> <patient ssn=”33-333-3333” firstName=”John” lastName=”Doe”> <measurement date=”05/27/2006”> 72 <weight>210 231043.4205.379884.62012
I’ll save the XML file in the same place as the executable. Now it’s time to churn out the code. You can take a look at code created to persist patient data in Listing 2-10.
Listing 2-10: The BtnSave_Click Method Containing Persistence-Related Code Private Sub BtnSave_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles BtnSave.Click If Not ValidatePatientPersonalData() Then Return If patientValue Is Nothing Then BtnCalculate_Click(Nothing, Nothing) End If Dim document As New XmlDocument Dim fileCreated As Boolean = True Try document.Load(System.Reflection.Assembly. _ GetExecutingAssembly.Location.Replace( _ “DoctorsOrders.exe”, “patientHistory.xml”)) Catch noFile As IO.FileNotFoundException ‘If file not found, set fileCreated to false and continue fileCreated = False End Try If Not fileCreated Then document.LoadXml(“<patientsHistory>” + _ “<patient ssn=”“” + patientValue.SSN + “”“” + _ “ firstName=”“” + patientValue.FirstName + “”“” + _ “ lastName=”“” + patientValue.LastName + “”“>” + _ “<measurement date=”“” + DateTime.Today + “”“>” + _ “” + patientValue.HeightInInches.ToString + _ “” + _
Continued
45
79796c02.qxd:WroxPro
2/25/08
8:56 AM
Page 46
Part I: Introduction to Refactoring Listing 2-10: The BtnSave_Click Method Containing Persistence-Related Code (continued) “<weight>” + patientValue.WeightInPounds.ToString + _ “” + _ “” + patientValue.Age.ToString + “” + _ “” + _ patientValue.DailyCaloriesRecommended.ToString + _ “” + _ “” + _ patientValue.IdealBodyWeight.ToString + _ “” + _ “” + _ patientValue.DistanceFromIdealWeight.ToString + _ “” + _ “” + _ “” + _ “”) Else ‘Search for existing node for this patient Dim patientNode As XmlNode = Nothing For Each node As XmlNode _ In document.FirstChild.ChildNodes For Each attrib As XmlAttribute _ In node.Attributes ‘We will use SSN to _ uniquely identify patient If (attrib.Name = “ssn” And _ attrib.Value = patientValue.SSN) Then patientNode = node Exit For End If Next Next If patientNode Is Nothing Then ‘just clone any patient node _ and use it for the new Dim thisPatient As XmlNode = _ document.DocumentElement. _ FirstChild.CloneNode(True) thisPatient.Attributes(“ssn”).Value = _ patientValue.SSN thisPatient.Attributes(“firstName”).Value = _ patientValue.FirstName thisPatient.Attributes(“lastName”).Value = _ patientValue.LastName Dim measurement As XmlNode = _ thisPatient.FirstChild measurement.Attributes(“date”).Value = _ DateTime.Today measurement.Item(“height”).FirstChild.Value = _ patientValue.HeightInInches measurement.Item(“weight”).FirstChild.Value = _
46
79796c02.qxd:WroxPro
2/25/08
8:56 AM
Page 47
Chapter 2: A First Taste of Refactoring Listing 2-10: The BtnSave_Click Method Containing Persistence-Related Code (continued) patientValue.WeightInPounds measurement.Item(“age”).FirstChild.Value = _ patientValue.Age measurement.Item(“dailyCaloriesRecommended”). _ FirstChild.Value = _ patientValue.DailyCaloriesRecommended measurement.Item(“idealBodyWeight”). _ FirstChild.Value = patientValue.IdealBodyWeight measurement.Item(“distanceFromIdealWeight”). _ FirstChild.Value = _ patientValue.DistanceFromIdealWeight document.FirstChild.AppendChild(thisPatient) Else ‘If patient node found just clone any measurement ‘and use it for the new measurement Dim measurement = patientNode.FirstChild. _ CloneNode(True) measurement.Attributes(“date”).Value = _ DateTime.Today measurement.Item(“height”).FirstChild.Value = _ patientValue.HeightInInches measurement.Item(“weight”).FirstChild.Value = _ patientValue.WeightInPounds measurement.Item(“age”).FirstChild.Value = _ patientValue.Age measurement.Item(“dailyCaloriesRecommended”). _ FirstChild.Value = _ patientValue.DailyCaloriesRecommended measurement.Item(“idealBodyWeight”). _ FirstChild.Value = patientValue.IdealBodyWeight measurement.Item(“distanceFromIdealWeight”). _ FirstChild.Value = _ patientValue.DistanceFromIdealWeight patientNode.AppendChild(measurement) End If End If ‘Finally, save the xml to file document.Save(System.Reflection.Assembly. _ GetExecutingAssembly.Location.Replace( _ “DoctorsOrders.exe”, “patientHistory.xml”)) End Sub
This code doesn’t look so complicated. Nevertheless, let me address it with a few comments. The BtnSave_Click method never checks for file existence explicitly. Instead, a FileNotFoundException is caught and a flag set that indicates that this is probably the first time a patient history is to be saved. In that case the XML structure is created by concatenating and loading a string representing the XML structure as a way to get around the rather verbose DOM API.
47
79796c02.qxd:WroxPro
2/25/08
8:56 AM
Page 48
Part I: Introduction to Refactoring In case the file exists, the code searches for this specific patient entry by comparing Social Security numbers. If the patient entry is not present, it is created by the cloning of any patient’s node, thus saving the trouble of using the DOM API to create it. Since the file exists, we can assume at least one entry exists. If the patient is found, a new measurement entry is created by the cloning of the measurement node. Since the patient node is found, it is safe to assume at least one measurement was saved. While this time I was careful to place data-validation code in a separate function, I still ended up with a very, very long method. I dare say this is an excellent specimen of what is generally better known by the name spaghetti code. So, before I am told to change my career in the direction of Italian cuisine, I had better do something about refactoring this method.
Method Decomposition Revisited In studying the method, one thing that might really bother your eye is that there are two completely identical pieces of code involved in setting measurement values to XmlNode. This code can be moved into a new method called SetMeasurementValues: Private Function SetMeasurementValues( _ ByVal measurement As XmlNode) As XmlNode measurement.Attributes(“date”).Value = _ DateTime.Today measurement.Item(“height”).FirstChild.Value = _ patientValue.HeightInInches measurement.Item(“weight”).FirstChild.Value = _ patientValue.WeightInPounds measurement.Item(“age”).FirstChild.Value = _ patientValue.Age measurement.Item(“dailyCaloriesRecommended”). _ FirstChild.Value = _ patientValue.DailyCaloriesRecommended measurement.Item(“idealBodyWeight”). _ FirstChild.Value = patientValue.IdealBodyWeight measurement.Item(“distanceFromIdealWeight”). _ FirstChild.Value = _ patientValue.DistanceFromIdealWeight End Function
Also, I can further decompose the BtnSave_Click method, much as I did BtnCalculate_Click. New methods are created: LoadPatientHistoryFile, CreateXmlDocumentFirstTime, FindPatientNode, and AddNewPatient. You can see those methods in Listing 2-11.
Listing 2-11: Decomposing the BtnSave_Click Method Private Function LoadPatientHistoryFile() As XmlDocument Dim document As New XmlDocument document.Load(System.Reflection.Assembly. _ GetExecutingAssembly.Location.Replace( _ “DoctorsOrders.exe”, “patientHistory.xml”)) Return document End Function Private Function CreateXmlDocumentFirstTime() As XmlDocument
48
79796c02.qxd:WroxPro
2/25/08
8:56 AM
Page 49
Chapter 2: A First Taste of Refactoring Listing 2-11: Decomposing the BtnSave_Click Method (continued) Dim document As New XmlDocument document.LoadXml(“<patientsHistory>” + _ “<patient ssn=”“” + patientValue.SSN + “”“” + _ “ firstName=”“” + patientValue.FirstName + “”“” + _ “ lastName=”“” + patientValue.LastName + “”“>” + _ “<measurement date=”“” + DateTime.Today + “”“>” + _ “” + patientValue.HeightInInches.ToString + _ “” + _ “<weight>” + patientValue.WeightInPounds.ToString + _ “” + _ “” + patientValue.Age.ToString + “” + _ “” + _ patientValue.DailyCaloriesRecommended.ToString + _ “” + _ “” + _ patientValue.IdealBodyWeight.ToString + _ “” + _ “” + _ patientValue.DistanceFromIdealWeight.ToString + _ “” + _ “” + _ “” + _ “”) Return document End Function Private Function FindPatientNode(ByVal document _ As XmlDocument) As XmlNode Dim patientNode As XmlNode = Nothing For Each node As XmlNode In document.FirstChild.ChildNodes For Each attrib As XmlAttribute In node.Attributes ‘We will use SSN to uniquely identify patient If (attrib.Name = “ssn” And _ attrib.Value = patientValue.SSN) Then patientNode = node End If Next Next Return patientNode End Function Private Function AddNewPatient(ByVal document _ As XmlDocument) As XmlDocument ‘just clone any patient node and use it for new Dim thisPatient As XmlNode = _ document.DocumentElement.FirstChild.CloneNode(True) thisPatient.Attributes(“ssn”).Value = _ patientValue.SSN thisPatient.Attributes(“firstName”).Value = _ patientValue.FirstName thisPatient.Attributes(“lastName”).Value = _ patientValue.LastName
Continued
49
79796c02.qxd:WroxPro
2/25/08
8:56 AM
Page 50
Part I: Introduction to Refactoring Listing 2-11: Decomposing the BtnSave_Click Method (continued) Dim measurement As XmlNode = _ thisPatient.FirstChild measurement = SetMeasurementValues(measurement) document.FirstChild.AppendChild(thisPatient) End Function
You have just witnessed a very immediate benefit of method extraction. Because I am able to extract two identical pieces of code into the method SetMeasurementValues, I have managed to eliminate duplication in the code and reduce total line count in a significant way.
Finding the Right Place for Methods As in the aftermath of the BtnCalculate_Click method decomposition, I’m getting the feeling that these data-persistence methods do not really belong to the FrmCaloriesCalculator class. Rather, they are interested in patient data, so at this point I am tempted to move them to a Patient class. So what do you think — is that the right move at this moment? Or are there arguments against moving newly created LoadPatientHistoryFile, CreateXmlDocumentFirstTime, FindPatientNode, and AddNewPatient methods to the Patient class? The Patient class is performing medical calculations. So far, it has nothing to do with managing the measurements of different patients. If I were to move these new methods to the Patient class, it would unnecessarily encumber this class with persistence code. Some future client of our Patient class, interested in the calculation that this class provides, might prefer persisting data in some other form — a database, for example. If I were to move XML persistence methods to the Patient class I would be obliged to deliver this functionality as well. Now imagine I discover a bug in my persistence code. I would need to distribute a new version to the client just in case that client uses XML persistence. The reasoning I have just used is expressed in more generic terms as the single responsibility principle (SRP), and I discuss this design principle in Chapter 11.
Extracting a New Persistence Class As the arguments in the previous section show, I need a new class in the project. I’ll call it PatientHistoryXMLStorage, and I’ll move all the new persistence-related methods from FrmCaloriesCalculator to this class. This is another example of move method refactoring, and it is an integral part of the more complex extract class refactoring, discussed in Chapter 12. Now, once I have all those methods in PatientHistoryXMLStorage, I soon observe that they all reference a patient instance. To make the code compile, the methods need to somehow get hold of a patient object. There are two possible ways to do this: ❑
I can add a parameter to each and every method except LoadPatientHistoryFile
❑
I can add a field named patientValue to the class
As you can guess, the second option requires me to write a lot less code. That way, a patientValue will form part of the PatientHistoryXMLStorage state. So I’ll go for that solution.
50
79796c02.qxd:WroxPro
2/25/08
8:56 AM
Page 51
Chapter 2: A First Taste of Refactoring While I’m at it, there is another parameter that is repeated in quite a few methods. It is the document parameter. Again, I can make this parameter a field of the PatientHistoryXMLStorage class. In order to keep names consistent, I change the name to documentValue. FindPatientNode needs to be adjusted so it references documentValue instead of document. In addition to this change, AddNewPatient, LoadPatientHistoryFile, and CreateXmlDocumentFirstTime are not returning values any more, so their declaration is changed and now looks like this: Private Sub LoadPatientHistoryFile() Private Sub AddNewPatient() Private Sub CreateXmlDocumentFirstTime()
Also, the fields in the class now look like this: Private patientValue As Patient Private documentValue As XmlDocument = New XmlDocument
All I need at this point is the public method in the PatientHistoryXMLStorage class that the client code could use. Going back to FrmCaloriesCalculator class, you can see that the BtnSave_Click event handler contains exactly the code I need. I’ll extract the Save method from this routine and move it to PatientHistoryXMLStorage: Public Function Save(ByVal patient As Patient) patientValue = patient Dim fileCreated As Boolean = True Try LoadPatientHistoryFile() Catch noFile As IO.FileNotFoundException fileCreated = False End Try If Not fileCreated Then CreateXmlDocumentFirstTime() Else Dim patientNode As XmlNode = FindPatientNode() If patientNode Is Nothing Then AddNewPatient() Else ‘just clone any measurement and use it for new Dim measurement As XmlNode = _ patientNode.FirstChild.CloneNode(True) measurement = SetMeasurementValues(measurement) patientNode.AppendChild(measurement) End If End If documentValue.Save(System.Reflection.Assembly. _ GetExecutingAssembly.Location.Replace( _ “DoctorsOrders.exe”, “patientHistory.xml”)) End Function
51
79796c02.qxd:WroxPro
2/25/08
8:56 AM
Page 52
Part I: Introduction to Refactoring This leaves me with the BtnSave_Click event handler looking like this: Private Sub BtnSave_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles BtnSave.Click If Not ValidatePatientPersonalData() Then Return BtnCalculate_Click(Nothing, Nothing) storage.Save(patientValue) End Sub
In case you are wondering where the storage variable came from, it is now another field in the FrmCaloriesCalculator class, and its declaration looks like this: Private storage As PatientHistoryXMLStorage = _ New PatientHistoryXMLStorage
With this, I have effectively implemented persistence functionality in a separate, persistence-dedicated class.
Implementing Patient-History Display Functionality Finally, I have only one functionality pending — to somehow visualize patient data. Internet Explorer does a pretty good job at visualizing XML data, so instead of adding a new form to my project I am simply going to open the Internet Explorer instance, pass the location of the patientHistory.xml file to it, and let it display the file content. If I wanted to improve on this, I could write my own XSL template and visualize the data by using Internet Explorer’s built-in XSL engine, but at this point I’ll stick with IE’s default XML stylesheet. Calling upon Internet Explorer is easily done through the Shell function from the Microsoft .VisualBasic namespace. All I need now is the location of the file. In the Save method of the PatientHistoryXMLStorage class, I programmed the application so it saves the XML file next to a Calories Calculator exe. I could easily copy-paste this line, but this would mean creating a duplicate entry in our code. As a matter of fact, I already have duplicated code because I used the same line in the LoadPatientHistoryFile method. Instead of copying the code — and you can read about all the headaches duplicated code can give you in Chapter 9 — I decide to add a public field to the PatientHistoryXMLStorage class. Since I need to access it easily from another class, I’ll make this field static using the Shared keyword. Here it is: Public Shared patientHistoryXmlFile = _ System.Reflection.Assembly. _ GetExecutingAssembly.Location.Replace( _ “DoctorsOrders.exe”, “patientHistory.xml”)
I also manage to simplify LoadPatientHistoryFile. Now it looks like this: Private Sub LoadPatientHistoryFile() documentValue.Load(patientHistoryXmlFile) End Sub
Also, the related line in the Save method now looks like this: documentValue.Save(patientHistoryXmlFile)
52
79796c02.qxd:WroxPro
2/25/08
8:56 AM
Page 53
Chapter 2: A First Taste of Refactoring Finally, the BtnView_Click method looks like this: Private Sub BtnView_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles BtnView.Click Try Shell(IEExeFile + “ “ _ + PatientHistoryXMLStorage.patientHistoryXmlFile, _ AppWinStyle.NormalFocus) Catch noFile As IO.FileNotFoundException MsgBox(“Internet Explorer not found”) End Try End Sub
As you can see in the preceding code, and going along the same lines, I also replaced the Internet Explorer executable location literal with a static field in the FrmCaloriesCalculator class, IEExeFile: Private Shared IEExeFile = _ “C:\Program Files\Internet Explorer\IEXPLORE.EXE”
This is an example of replace magic number with symbolic constant refactoring. While in this case it was not a number that got replaced — I replaced a file location string literal — it is still just a variation of this important type of refactoring. Check out Chapter 9 for more background on replace magic literal with constant refactoring and the magic literals smell. For the final code for the PatientHistoryXMLStorage class, take a look at Listing 2-12.
Listing 2-12: The Final Version of the PatientHistoryXMLStorage Class Option Strict Off Imports System.Xml Imports System.IO Public Class PatientHistoryXMLStorage Private patientValue As Patient Private documentValue As XmlDocument = New XmlDocument Public Shared patientHistoryXmlFile = _ System.Reflection.Assembly. _ GetExecutingAssembly.Location.Replace( _ “CaloriesCalculator.EXE”, “patientHistory.xml”)
Private Function SetMeasurementValues( _ ByVal measurement As XmlNode) As XmlNode measurement.Attributes(“date”).Value = _ DateTime.Today measurement.Item(“height”).FirstChild.Value = _ patientValue.HeightInInches measurement.Item(“weight”).FirstChild.Value = _ patientValue.WeightInPounds measurement.Item(“age”).FirstChild.Value = _ patientValue.Age
Continued
53
79796c02.qxd:WroxPro
2/25/08
8:56 AM
Page 54
Part I: Introduction to Refactoring Listing 2-12: The Final Version of the PatientHistoryXMLStorage Class (continued) measurement.Item(“dailyCaloriesRecomended”). _ FirstChild.Value = _ patientValue.DailyCaloriesRecomended measurement.Item(“idealBodyWeight”). _ FirstChild.Value = patientValue.IdealBodyWeight measurement.Item(“distanceFromIdealWeight”). _ FirstChild.Value = _ patientValue.DistanceFromIdealWeight Return measurement End Function Private Sub LoadPatientHistoryFile() documentValue.Load(patientHistoryXmlFile) End Sub Private Sub CreateXmlDocumentFirstTime() documentValue.LoadXml(“<patientsHistory>” + _ “<patient ssn=”“” + patientValue.SSN + “”“” + _ “ firstName=”“” + patientValue.FirstName + “”“” + _ “ lastName=”“” + patientValue.LastName + “”“>” + _ “<measurement date=”“” + DateTime.Today + “”“>” + _ “” + patientValue.HeightInInches.ToString + _ “” + _ “<weight>” + patientValue.WeightInPounds.ToString + _ “” + _ “” + patientValue.Age.ToString + “” + _ “” + _ patientValue.DailyCaloriesRecomended.ToString + _ “” + _ “” + _ patientValue.IdealBodyWeight.ToString + _ “” + _ “” + _ patientValue.DistanceFromIdealWeight.ToString + _ “” + _ “” + _ “” + _ “”) End Sub Private Function FindPatientNode() As XmlNode Dim patientNode As XmlNode = Nothing For Each node As XmlNode In documentValue.FirstChild.ChildNodes For Each attrib As XmlAttribute In node.Attributes ‘We will use SSN to uniquely identify patient If (attrib.Name = “ssn” And _ attrib.Value = patientValue.SSN) Then patientNode = node End If Next Next Return patientNode
54
79796c02.qxd:WroxPro
2/25/08
8:56 AM
Page 55
Chapter 2: A First Taste of Refactoring Listing 2-12: The Final Version of the PatientHistoryXMLStorage Class (continued) End Function Private Sub AddNewPatient() ‘just clone any patient node and use it for new Dim thisPatient As XmlNode = _ documentValue.DocumentElement.FirstChild.CloneNode(True) thisPatient.Attributes(“ssn”).Value = _ patientValue.SSN thisPatient.Attributes(“firstName”).Value = _ patientValue.FirstName thisPatient.Attributes(“lastName”).Value = _ patientValue.LastName Dim measurement As XmlNode = _ thisPatient.FirstChild measurement = SetMeasurementValues(measurement) documentValue.FirstChild.AppendChild(thisPatient) End Sub Public Sub Save(ByVal patient As Patient) patientValue = patient Dim fileCreated As Boolean = True Try LoadPatientHistoryFile() Catch noFile As IO.FileNotFoundException fileCreated = False End Try If Not fileCreated Then CreateXmlDocumentFirstTime() Else Dim patientNode As XmlNode = FindPatientNode() If patientNode Is Nothing Then AddNewPatient() Else ‘just clone any measurement and use it for new Dim measurement As XmlNode = _ patientNode.FirstChild.CloneNode(True) measurement = SetMeasurementValues(measurement) patientNode.AppendChild(measurement) End If End If documentValue.Save(patientHistoryXmlFile) End Sub End Class
It is a rather long listing, but it is here for a purpose. Before this book ends, I will have another go at this code from the Visual Basic 2008 perspective. (If you can’t wait to see what happens to this code in the VB 2008 version, take a look at Chapter 15.) With this, I have concluded implementing the latest requirements of the Calories Calculator application.
55
79796c02.qxd:WroxPro
2/25/08
8:56 AM
Page 56
Part I: Introduction to Refactoring
Calories Calculator, Refactored Version Now it’s time to take a look at the new structure of the application. A class diagram (Figure 2-6) can give you a good overview of the code I ended up with.
I now have a lot more classes and methods than when I started. The impression is that the code is more balanced and better organized, with each class dedicated to a single and specific purpose. I have also used inheritance, possibly opening the door for future extensions. What does this tell you? What could you observe during the process of developing this sample application? Have I managed to improve the code as I implemented the requirements? What are the benefits of all this? Take a look at some of the answers (Table 2-1 and the list that follows).
Table 2-1: Refactoring Techniques Applied and Benefits Obtained
56
Refactoring Technique
Result
Extract method
Atomic, more reusable methods with clear purpose
Extract class
Single-purpose classes improve reuse, maintainability, and readability of code
Move method
Better structured code when used in conjunction with extract class
Reduce method visibility
Improves encapsulation, thus making code simpler and more clear-cut
79796c02.qxd:WroxPro
2/25/08
8:56 AM
Page 57
Chapter 2: A First Taste of Refactoring Table 2-1: Refactoring Techniques Applied and Benefits Obtained (continued) Refactoring Technique
Result
Replace conditional logic with polymorphism
Reduces duplication in the code, improving maintainability and extensibility of the code
Pull-up method
Reduces duplication in code
Eliminate dead code
Simplifies code and makes it easier to read
Replace magic number with symbolic constant
Code is easier to maintain, because the changes need be applied only in a single place
❑
Initially I decomposed the long methods; this improved the code in several ways. By giving explicit names to pieces of code I improved the readability of the code and made it simpler. It is easier to understand the purpose of each method; the intent is explicitly stated, and so the maintainability of the code has been improved. Any new programmer starting out on the code will need less time to get acquainted with it. Even experienced programmers can forget a specific detail of the code they developed, so they will benefit from improved comprehensibility.
❑
By making the code more granular and structured, I improved the reuse potential of the code. This simplifies the code by making the code-base smaller. I ended up with less code.
❑
Reuse potential was also improved when I extracted the SetMeasurementValues method. This immediately resulted in another benefit. I replaced another piece of code with the call to this method, thus eliminating duplication in the code. This improves the maintainability of the code, because if ever I need to modify some behavior, I need only perform the modification in a single place. This results in code less prone to bugs, as I am less likely to face a situation in which I modified behavior in one place but failed to modify it in another place containing the same duplicated code.
❑
By eliminating code that ended up unused, I made the application easier to read and to debug, improving simplicity of the code. With this refactoring, I have reduced the overall quantity of the code.
❑
I have managed to improve maintainability of the code by replacing literals scattered throughout the code with a constant. This way, if a value ever needs to be changed, the change can be performed in a single place.
❑
Extracting new classes also improves the reuse in the code, and organizing classes in a hierarchy again helped me avoid duplication, but on a different scale. At the same time, extensibility of the code was enhanced. Now that I have the MalePatient and FemalePatient classes extending the Patient class, it is not so far-fetched to imagine, for example, ChildPatient as an addition to the hierarchy. This means I can add more behavior without modifying existing code — meaning less to test, to recompile, or to redeploy.
That about wraps it up for this demonstration. In the following chapters we’ll study refactoring techniques in greater detail.
57
79796c02.qxd:WroxPro
2/25/08
8:56 AM
Page 58
Part I: Introduction to Refactoring
Summar y Visual Basic programmers typically like to dip into the code right from a project’s outset. This no-nonsense, goal-oriented approach can have a lot of benefits, allowing for early prototyping and increased early productivity. However, as the application progresses and new requirements appear, it becomes necessary to restructure the code so the optimum design is maintained. In this chapter I tried to follow this typical approach. In the example, I constructed a simple application that can perform some medical calculations, like estimating ideal body weight and a recommended daily amount of calories. Initially, I didn’t put too much emphasis on the design and quality of the code. However, as I responded to growing requirements from the client, I also spent some time on refactoring the code in order to keep it in shape and proved how this kind of refactoring pays off. This example has shown the ways refactoring improves the design and makes code simpler and easier to understand. Making methods more granular improved the reusability of the code. I also improved reusability by making each class responsible for a single task. Eliminating duplications in the code made it smaller and easier to modify. Organizing classes into a logical hierarchy also dealt with duplicate code and made the code more extensible. Refactored source is easier to reuse, extend, and maintain. It is less prone to bugs and easier to optimize. With this example I tried to provide insight into how individual refactoring techniques work together toward improving the design of the code. With this practical demonstration, a general context for individual refactoring techniques should become apparent. This should help you understand where each of these refactoring techniques fits inside the global picture and should prepare you for examining each of these techniques in detail. While some of the refactoring techniques I demonstrated can be performed much more efficiently by means of an automated refactoring tool, in this chapter I tried to stay focused on the code and the refactoring techniques as they are performed manually. In the next chapter I’ll talk about the tools that can help you refactor much more efficiently and securely. I’ll also talk about a refactoring toolkit and especially about an automated refactoring tool.
58
79796c03.qxd:WroxPro
2/22/08
4:58 PM
Page 59
Assembling a Refactoring Toolkit Many techniques and methodologies make you produce and maintain artifacts, in addition to source code, as an integral part of the project — things like documentation, diagrams, and so on. Some approaches are very strict in controlling the way you code, imposing rules and guidelines that confine the way you program in order to make your code more uniform. In all these cases, you may often feel that an additional burden has encumbered this already demanding line of work. It can easily make you reluctant to adopt new techniques. With refactoring, the rules are not cast in stone, nor do have you to deal with any additional baggage. On the contrary, the more expert you become in refactoring techniques, the more confident you are with the code. What refactoring does is to make you feel you are in charge. The productivity gain and coding proficiency that come from refactoring are further enhanced by the right set of tools. When I talk about assembling a refactoring toolkit, which is the topic of this chapter, I consider the following three pieces of software mandatory, no matter the size of your team or project, your methodology, or the type of software you are making: ❑
An automated refactoring tool
❑
A unit-testing framework
❑
A source-code version-control system
Only one is directly related to performing refactoring transformations, while the other two are in my opinion indispensable for any type of serious refactoring work. The refactoring tool can automate certain refactorings but only when it is used together with unit testing and source-code control will you have the freedom and security to perform refactoring continuously and on a large scale. In this chapter I’ll discuss the first two items in detail. Since I haven’t met many teams that don’t use some kind of version-control system, I will not go into any great detail about it other than to dedicate just enough space to treat version-control issues relevant to refactoring.
79796c03.qxd:WroxPro
2/22/08
4:58 PM
Page 60
Part I: Introduction to Refactoring As you will see, there is no single tool that encompasses all the functionality you need for a successful refactoring process. Visual Studio is on the road to providing a unified solution, but for the time being you have to make an effort to assemble this refactoring toolkit yourself. If you would like to learn about refactoring techniques right away, you can skip this chapter and come back to this discussion of tools later on, after you have read about each individual refactoring.
Using an Automated Refactoring Tool Wider adoption of refactoring in any programming language has generally been influenced by the availability of an automated refactoring tool. It is one thing to understand the theory, the pros and cons, and even to be an eager devotee of the technique, and quite another to start meddling with your code in the face of an ever-closer deadline. Even the smallest change to your code can break it, so you need a really cool head and audacity to vouch for some intangible characteristic like design and code quality when there are pending requirements waiting to be implemented. Wouldn’t it be great if you had a tool with enough intelligence to know how to perform changes in your code without breaking it? For example, if you renamed a method, the tool would search for all the client code that calls this method and replace its name in the client code. Or, if you extracted a piece of code and put it into a new method, the tool would make sure that all the necessary parameters were included in the method declaration. With Visual Studio 2008, there is such a tool available for Visual Basic programmers. While you must download and install it separately, the basic version of the tool is free of charge. With Visual Studio 2008 and 2005, VB developers can obtain the Refactor! Visual Studio add-in from Developer Express (http://www.devexpress.com/vbrefactor) free of charge. Refactor! for VB sports a nice set of basic refactorings, and the number of supported refactorings is growing. This is not the only tool on the market. Even Developer Express has a more advanced pro version of Refactor!. Other tools might provide a larger number of refactorings and cross-language support. However, I find that once you learn to use basic refactoring techniques, adopting new ones is not so difficult. And taking into account that Refactor! for VB is free, I decided to use it as a standard tool in this book. Please note that Refactor! for VB does not work with the Visual Studio Express edition. Once refactoring becomes an integral part of your development process, as I hope it will, you can test some other tools that are available on the market and pick the one that best fits your taste and budget. The majority of these tools include free trial versions, so you will be able to test them before you buy them. The next sections touch on some of the tools that are available today.
ReSharper from JetBrains JetBrains (www.jetbrains.com) made its name with the excellent IntelliJ IDEA Java IDE that brought another level of productivity to often nonresponsive Java IDEs, and pioneered comprehensive refactoring support for Java. Its first venture into the .NET arena is ReSharper, a refactoring and developer productivity add-in for Visual Studio. They started by supporting C#, hence the name, but ReSharper is now a cross-language tool that supports Visual Basic XML, XAML, and ASP.NET.
60
79796c03.qxd:WroxPro
2/22/08
4:58 PM
Page 61
Chapter 3: Assembling a Refactoring Toolkit ReSharper sports a number of useful refactoring transformations: ❑
Comprehensive, project-wide Rename refactoring
❑
Extract Method with dialog window preview
❑
Move Class refactoring
❑
Explicit Imports refactoring, called “Optimize Using” in ReSharper
❑
Use Base Type Where Possible refactoring
❑
Change Scope
It has an integrated NUnit test runner, advanced navigation and formatting capabilities, and a lot of other interesting features that will make your programming life a lot easier.
Visual Assist X from Whole Tomato It’s probably not the “whole tomato” of VB refactoring, since it is limited in the number of refactorings it offers, but Visual Assist X still features some important VB refactorings, the first on the list being the allimportant Extract Method. It is also able to perform Rename and Encapsulate Field refactoring. Like other developer productivity tools, it sports a number of navigation, advanced syntax highlighting, enhanced IntelliSense, spell checking, and autocomplete features. See www.wholetomato.com for more detail.
Refactor! Pro from Developer Express You can pay to upgrade your free Refactor! to Refactor! Pro. Refactor! Pro includes over 150 refactorings and supports C#, VB, ASP.NET, C++, JavaScript, and more. It also works with previous editions of Visual Studio, namely 2002 and 2003. Further, it enables you to customize the way refactorings are performed and even extend the tool by programming your own refactorings. Now you are going to see how you can use Refactor! for VB, the basics of its user interface, and the list of refactorings this tool supports. Finally, you’ll see some advanced options that this tool provides but that are hidden from the user at the first glance.
Getting Started with Refactor! Refactor! uses Visual Studio add-in architecture to integrate seamlessly into the Visual Studio environment. The new features become apparent once you open a Visual Basic file for editing. There is also a documentation section that gets merged with rest of the Visual Studio documentation when you install the add-in. There are four ways to activate Refactor! and invoke the operations it provides: ❑
Using smart tags
❑
Right-clicking
❑
Using the Ctrl+` (backtick) keyboard shortcut
❑
Using cut and paste
61
79796c03.qxd:WroxPro
2/22/08
4:58 PM
Page 62
Part I: Introduction to Refactoring Using Smart Tags A smart tag takes the form of a small line under the first character in the identifier. When you place the caret on some identifier in your code or select the piece of code, the smart tag appears, indicating a possible refactoring that you can activate by opening the menu and selecting it from the list. Smart tags are context-sensitive, so you see only refactorings available for the selected code or for the identifier on which the caret is placed, and the smart tag will appear only if there are refactorings available at the position indicated by the caret. You can see smart tag at the first line in the ClearResults method in Figure 3-1.
Figure 3-1
Right-Clicking the Mouse You can make the context menu appear by right-clicking on a certain identifier or selected piece of code in your source-code editor. If you have the Refactor! add-in installed, a new option called Refactor! appears in the menu. This happens only if there is a refactoring available for that identifier or selected portion of code. The item in the menu leads to a new submenu containing all the refactoring options available for that selection. Again, the submenu content is filtered so only refactoring relevant to the selected code appears. You can see a context menu containing the Refactor! menu item in Figure 3-2.
Figure 3-2
62
79796c03.qxd:WroxPro
2/22/08
4:58 PM
Page 63
Chapter 3: Assembling a Refactoring Toolkit Using the Keyboard Shortcut Ctrl+` (One-Key Refactoring) This key combination activates the refactoring mode in Visual Studio. First you have to place a caret over the identifier or select the code related to the refactoring we are trying to perform. When you hit Ctrl+`, a menu appears with all refactorings available for that selection or caret position. However, this means of activating Refactor! is different in one respect from right-clicking. It provides fast access to Refactor! functionality and does not require the use of a mouse, so in case a single refactoring is available for the active selection or caret position, the refactoring is invoked immediately. No additional input or intermediate screen is presented. This is why it is also referred to as one-key refactoring. It is a good option once you become more comfortable with the tool. On the U.S. keyboard, the ` (backtick) key is the same as the ~ (tilde) key and is generally placed above the left tab key. On some European keyboards this combination is not easy to hit, so you can rebind Refactor! to some other, more accessible key combination. In order to do this, open Tools ➪ Customize in Visual Studio and press the Keyboard... button. In the Options window define a new shortcut for the CodeRush.Refactor command.
Using Cut and Paste You can use cut and paste to extract a method. Just cut the code that you want for the new method and paste it inside the class between methods. Refactor! will add the appropriate calling code at the cut point and build the method-signature wrapper around the code you paste. You can also introduce an explaining variable with cut and paste. Just cut an expression to the clipboard and paste it on an empty line above the cut point. Refactor! will declare a new variable of the appropriate type and assign it the expression you’re pasting. It will also add a reference to that variable at the cut point. Now that you have seen how to invoke Refactor! from the Visual Studio IDE, I want to take a look at the basic elements of Refactor! for the VB user interface.
Exploring Refactor! for the VB User Interface In one way, Refactor! for VB is quite different from the rest of the tools on the market. In order to help programmers always work at maximum speed and to have source code in front of them at all times, the creators of Refactor! have avoided placing modal windows in front of the user. Instead they created a set of new visual features that let users exploit the Refactor! functionality. Those features are hints, markers, linked identifiers, target pickers, and replace progress indicators. All have very distinctive and colorful visual styles and even some amusing animation effects. In this section I’ll walk through each of these features.
Hints Until you really master Refactor! for VB, you will need some help in finding your way around it. Hints serve exactly this purpose; they help you see all available options and locate newly created code. There are three types of hints: ❑
Action hints
❑
Big hints
❑
Shortcut hints
63
79796c03.qxd:WroxPro
2/22/08
4:58 PM
Page 64
Part I: Introduction to Refactoring Action Hints Action hints appear after one-key refactoring is invoked and only one refactoring is available. They generally have the form of a large arrow pointing to a place where refactoring will happen. The arrow shows the name of the refactoring that was applied. After a few seconds this type of hint disappears automatically. Figure 3-3 shows an action hint.
Figure 3-3
Big Hints Big hints take the form of a tool tip window. When the refactoring menu is displayed, the content of the window provides you with the concise description of the currently selected option in the menu. As you move through the menu the old window will close and a new one will appear containing the description of the currently selected refactoring. Once the menu option is selected and the refactoring applied, the big hint window closes automatically. You can see the big hint for Symbolic Rename refactoring in Figure 3-4.
Figure 3-4
64
79796c03.qxd:WroxPro
2/22/08
4:58 PM
Page 65
Chapter 3: Assembling a Refactoring Toolkit Shortcut Hints The purpose of a shortcut hints window is to list all available options and the keys to invoke them once a certain refactoring has been selected. It is a floating window containing a two-column table. The left column contains the key and the right column describes the behavior that will result from pressing that key. The window can be moved, minimized, or closed. Shortcut hints appear for refactorings that have an interactive state and disappear automatically when you leave the interactive state (for example, by committing or cancelling your changes). Once you’re familiar with the shortcuts for a particular state, you can click the close button at the upper right of the shortcuts hint to suppress future appearances. Figure 3-5 demonstrates shortcut hints for the Extract Method refactoring.
Figure 3-5
Markers When working on large files, you might need some help in getting around. For example, if you extract a piece of a method into a new method, you might wish to return to the place where the original method is located. When performing the refactoring, Refactor! will automatically place a marker in the form of a small triangle. Once refactoring has been committed, you can return to the starting point by pressing the Escape key. The caret will be moved back to the place where the marker was left, followed with the animated circle to help you locate the caret. Once you return to the marker, it is collected and removed. You can have multiple markers, and you can move between them in the reverse of the order in which they were created (stack-like). This functionality is similar to the bookmark functionality in Visual Studio, with the difference that markers are created automatically as you perform certain refactorings. Figure 3-6 shows a marker in place.
Figure 3-6
65
79796c03.qxd:WroxPro
2/22/08
4:58 PM
Page 66
Part I: Introduction to Refactoring Linked Identifiers This feature permits you to simultaneously edit all the occurrences of an identifier. In certain type of refactorings it is possible to automatically identify all the instances of the identifier that needs to be changed. This is the case with Rename Local refactoring, for example. Additionally, all linked items are highlighted, and it is possible to navigate through them using the Tab key. In the context menu you can choose to break the linkage. Once the edit is finished, you commit the refactoring by pressing the Enter key, and items are delinked. You can see linked identifiers after the SSNValue field name has been changed in Figure 3-7.
Figure 3-7
Target Pickers Target pickers have the form of a horizontal line with an arrow. They permit you to select the location in the file for the code resulting from active refactoring. You can move the picker by using the up- and down-arrow keys. Once you commit the refactoring, newly generated code is placed in the location you selected with the picker, and the picker disappears. Figure 3-8 shows a target picker.
Figure 3-8
66
79796c03.qxd:WroxPro
2/22/08
4:58 PM
Page 67
Chapter 3: Assembling a Refactoring Toolkit The Replace Progress Indicator Certain refactorings cannot be executed without some user interaction. In same cases, the programmer has to decide what items refactoring should be applied to. Take an Introduce Constant refactoring, for example: the tool can identify all the appearances of a certain literal value, but has no way of knowing which one of them you want to replace with a constant. For example: Private Function CalculateTax(ByVal amount As Decimal) As Decimal txtTax.MaxLength = 20 Return amount / 100 * 20 End Function
If you apply Introduce Constant refactoring on literal 20, Refactor! for VB will search the file for all other occurrences of this number. However, only you can decide which one should be replaced with a constant. In this case, one occurrence of the number is related to tax calculation and should be replaced with an appropriate tax-calculation-related constant, while the other is concerned with a control property and should not be replaced with the same constant, regardless of the fact that in this specific case those values coincided. The tool uses the replace progress indicator to let the user pick items suitable for replacement during the operation. In this mode you can cycle through all occurrences identified by Refactor! and apply a replacement as you go along. See Figure 3-9.
Figure 3-9
You have now seen how you can invoke and interact with Refactor! for VB, and you have seen all the major visual and interface features. Next it’s time for you to see the refactorings this tool has to offer.
Quick Tour: Available Refactorings At the moment, Refactor! supports close to 30 refactorings. An additional four are available if you register at the Developer Express site (http://www.devexpress.com/vbrefactor), and they are certainly worth the trouble. Developer Express started out with the most basic refactorings, so only a few of the ones you will see span more than one class. This list is not definitive; at Developer Express they continue to work on the tool, and hopefully with time more refactorings will be released and supported by Refactor! for VB. In Table 3-1 you can see some of the key refactorings available at the moment (and also where in this book those refactorings are discussed).
67
79796c03.qxd:WroxPro
2/22/08
4:58 PM
Page 68
Part I: Introduction to Refactoring Table 3-1: Refactorings Supported by Refactor! for VB and Covered in This Book
68
Refactoring Name
Purpose
Chapter
Encapsulate Field
Turns a field into a property
11
Extract Interface
Creates a new interface that defines all public members of a class and makes class implement this newly created interface
12
Extract Method
Extracts a block of code into a new method
9
Extract Property
Extracts a block of code into a new property
9
Inline Temp
Replaces a temporary variable with an expression
10
Introduce Constant
Replaces a literal value with a constant
9
Introduce Local
Introduces a new explaining local variable
Make Explicit
Adds As part to a variable declaration statement
Method to Property
Converts a method declaration to a property
Move Declaration Near Reference
Moves a declaration statement closer to the first use of the variable
10
Move Initialization to Declaration
Moves initialization code to a declaration statement
10
Rename Local
Renames all occurrences of a local variable
8
Replace Temp with Query
Replaces a temporary variable with a method
10
Reverse Conditional
Changes order of Then and Else blocks
Safe Rename
Renames a method, while keeping the old name marked by the Obsolete attribute, and delegates the call to a new method
Simplify Conditional
Writes a condition in a simplified way
Split Initialization from Declaration
Splits the initialization code from the declaration statement
14
Split Temporary Variable
Introduces a new variable for an overburdened temporary variable
10
5
8
79796c03.qxd:WroxPro
2/22/08
4:58 PM
Page 69
Chapter 3: Assembling a Refactoring Toolkit I will explain in detail each refactoring that Refactor! supports throughout the book; the table shows the chapter where each refactoring has been applied and demonstrated. You will also notice that the list of refactorings that Refactor! supports is a lot smaller than the list of refactorings I deal with in the book. Not all refactorings are easy to automate, and as I already mentioned, work on Refactor! is continuously in progress. I hope that in time the two lists will start to converge, and the number of automated refactorings will close upon the list of refactorings I deal with in this book. Refactor! for VB is a scaled-down version of Refactor! Pro, a commercial tool. Some of the hidden options in the Pro version can be accessed with a free version with a little bit of tweaking. Take a look at Appendix A of this book to see how to activate hidden options in Refactor! for VB. That concludes this quick look at refactoring tools and Refactor! for VB specifically. Now it’s time to take a look at another very popular technique (one close in philosophy to refactoring) and another important tool in your refactoring toolkit — unit testing and NUnit.
Unit-Testing Basics: The Testing Harness Refactoring can be addictive. Once you start, it’s difficult to stop! However, one step too far, and you won’t be able to find your way back. Make one simple mistake, and suddenly the program is not doing what it was supposed to. You try reversing the latest changes but to no avail. You made a mistake somewhere along the way, and you just can’t figure out where. The only solution is to go all the way back to the beginning. The scenario I just described is not so uncommon for a novice to refactoring. Refactoring has to be performed in a disciplined manner, but that alone will often not suffice. Refactoring has to be performed carefully, step by step, and each step has to be followed by verification. You have to make sure your changes do not influence the behavior of your program. Remember the definition of refactoring? You are improving the design without changing the behavior. So how can you verify the behavior in a simple yet efficient way? The solution is unit testing. As some of you might already know, the Visual Studio Team System comes with integrated unit-testing support. At this point you might ask yourself why I chose to talk about NUnit instead. First of all, Team System is the top of the line of Visual Studio products and that comes at a price. If I had used it, this section would have had a smaller audience. Secondly, NUnit is more applicable to a test-driven development style (something a lot of you might decide to pursue). At any rate, exposure to NUnit is also a good starting point for unit testing with Visual Studio. Because NUnit is less integrated with Visual Studio and less GUI-based than Microsoft’s tool, by pursing unit testing with NUnit you will acquire a generic and more universal understanding of the unit-testing process. On a side note, there are other alternatives to unit testing in VB. For example, two other accomplished open-source unit-testing frameworks are MbUnit (http://www.mbunit.com) and csUnit (http://www.csunit.org). In this section you will learn more about unit testing in general and the benefits it brings. You’ll explore in more detail one open-source unit-testing framework, NUnit. You will learn how to install it and use it and how to write unit tests with its help of NUnit. Finally, I’ll take you back to the Calories Calculator application and write tests for some of the classes in the application so you can see unit testing in practice. But I want to start by taking a look at how unit testing as a discipline came about.
69
79796c03.qxd:WroxPro
2/22/08
4:58 PM
Page 70
Part I: Introduction to Refactoring
Why a Unit-Testing Framework? One of the ways you deal with software complexity is through testing. Testing is your attempt to discover bugs and errors in the software you are creating before it reaches a final user. In an attempt to ensure software quality, companies often have teams, even departments, dedicated to different forms of testing. The types of tests these teams perform vary and can be geared toward ensuring different qualities of the software. They might perform functional, usability, integration, load, stress, and other types of tests. All of these are important parts of the software-development process.
Manual Data Entry In order to perform thoroughly any type of test, a lot of time and resources are needed. It might take days, even weeks, before results are obtained and feedback ends up in the hands of a programmer. But it is of utmost importance that the coder be able to test the code every step of the way during the development process. Each time the most minimum functionality is added, changed, or removed, programmers have a natural tendency to want to assure themselves that the software still works correctly and that no fresh bugs have been introduced. The most obvious way to go about this is for the programmers themselves to simulate users. An IDE makes it easy to start the execution, enter the desired data, and verify the result. In case anything unexpected occurs, the process can be repeated and followed through with a debugger, enabling you to observe the execution depicted in the source code, line by line, and to interactively review and change variable data states. This makes the task of identifying the offending bug a lot easier. At this point, you might remember that this is exactly the approach I used to test the Calories Calculator application in Chapter 2. With all these capabilities and tools, programmers still produce very costly and difficult-to-identify bugs. Why does this happen? Partly because the type of ad hoc testing I just described, performed by programmers manually, has some very serious drawbacks:
70
❑
It requires a lot of manual data input, making the whole process very tedious. (This drawback is directly related to the next one.)
❑
Because it is tedious and time-consuming, coders limit their testing exploits to scenarios most directly related to their current tasks. However, there is no guarantee that only the tested functionality has been affected by the current changes.
❑
For the same reason, the pool of test data is often insufficient, failing to test for invalid and limit values.
❑
Because they are not automated, the tests are difficult to repeat in a reliable manner. In one sweep you might test for some important values and in the next those values are forgotten or misspelled.
❑
The cost in time associated with this type of testing means that the frequency at which tests are executed is low, leading to a situation in which a lot of changes have been introduced without test assurance in between. If the bug is identified after substantial changes have been applied, it is a lot more difficult to locate it. This leads to a vicious circle wherein more and more time is lost because of an unreliable development process, while the lack of time discourages a systematic approach to testing.
79796c03.qxd:WroxPro
2/22/08
4:58 PM
Page 71
Chapter 3: Assembling a Refactoring Toolkit A lot of programmers identified the drawbacks I just mentioned and reached the following conclusion: automating the tests would greatly improve their quality and would make them less costly and easier to perform. So what can we do about it?
A First Attempt at Automated Unit Testing: Ad Hoc Unit Testing The easiest and the most direct way to test a piece of code in an automated and repeatable manner is to write a program to exercise the targeted code. For example, if you write a dynamic link library, you can program an executable that will consume the functionality offered by the dll and make sure that dll behaves according to the specification. While this approach to automating testing is a great improvement compared to manual testing, a number of problems still become apparent once you adopt it: ❑
It gives you no way to write tests in a systematic and standardized way, making it difficult for you to maintain the quality of tests and promote their team-wide adoption. There are no real guidelines for programming the tests.
❑
If you do not perform the testing in a focused way, by isolating one piece of code from the rest of the application state, it may still be possible to discover the bug, but it can be very difficult to pin down the offending line of code. This approach to test automation is similar to the recording of user interaction: it is based on a surface view of tested code.
❑
In certain setups, you might want unit testing to play an integral part in a wider development process. This would require better tools integration, standardized output, reports, and the like.
No wonder programmers tried to find the solution to all these problems. Programmers needed the ability to write their own tests, counting on detailed knowledge of tested code. These tests would have to be executed and repeated at will. They would have to exercise small modules of code in isolation so no time would be lost in search for the source of the bug. Finally, the solution needed to encompass standardized means of capturing and displaying results.
Viola! Unit-Testing Frameworks All of this led to the appearance of unit-testing frameworks, which granted programmers new levels of freedom and productivity. While solving many of the previously mentioned problems, the tests written in this manner had another very interesting side effect. It became apparent that these tests could serve as a very good tool for expressing user requirements in an explicit, unambiguous manner. Diagrams, storyboards, use cases, and the rest of the traditional requirements-taking tools are all subject to human interpretation. Because unit tests are written in code and then executed as a program, it is the computer that decides whether a requirement has been satisfied — that is, whether the test has failed or not. Mind you, it is still programmers who have to write the right test, one embodying the right requirement. This style simplifies the development in another way — it eliminates the constant need to decide what exactly needs to be implemented, what methods and classes are actually needed. Programmers often speculate about scope of the program: “We might need this feature in the future” or “What if the customer needs to solve this problem later on?” If you actually write each test before you implement the code, this dilemma is no more. You implement only the code that makes all tests execute successfully, and nothing more. If you don’t have the test that uses it, it means you don’t need the code. This approach to programming is test-driven development.
71
79796c03.qxd:WroxPro
2/22/08
4:58 PM
Page 72
Part I: Introduction to Refactoring Next I am going to help you examine a popular unit-testing framework in more detail. This framework is arguably one of the most popular in the .NET world. However, I am just going to scratch the surface. As a matter of fact, there are already books dedicated to unit-testing test-driven development in .NET exclusively, so you can look at the following paragraphs as an encouragement and an invitation to dip into this practice further.
Your First Taste of NUnit NUnit framework is an open-source unit-testing framework for .NET. Thanks to the language neutrality of .NET programming languages, NUnit can be as easily used in Visual Basic as in C#, managed C++, or any other .NET language. NUnit was developed as a port of the JUnit framework, written by Erich Gamma and Kent Beck for unit testing in Java. JUnit sparked a wave of tools and frameworks that resulted in the porting of JUnit to a variety of languages, and in the development of various types of extensions that facilitate testing in diverse environments: Web applications, mock-object development (see the “NMock Object-Mocking Framework” section later on in this chapter), GUI testing tools, and others. The sheer size of the response this framework provoked in the developer community is a testimony to its success and usefulness. As with any framework, NUnit provides a lot of features that can be reused. This streamlines the test implementation and its execution. When you write tests with NUnit, you program plain old VB classes. The first version of NUnit required you to extend (inherit) classes provided within the framework, but since version 2.0, an approach more in line with .NET programming style has been adopted. You need to apply different attributes provided by NUnit to the classes and methods, so that NUnit knows that these contain unit tests. When you use NUnit to run your tests, it searches for classes and methods marked with the NUnit attributes to execute them and report the results. Now, before you get into the details of implementing tests with NUnit, I want to dedicate a few lines to installation procedures.
Installing NUnit NUnit is available for free from the NUnit site, www.nunit.com. NUnit is distributed under an open-source license and installation comes in different flavors, with the source code available for download in addition to the standard windows installation package. Since work on NUnit is permanent, you should always download the most recent stable version. On the web site, this version is generally marked as “Recommended.” After the installation, NUnit will place an item in your Start menu. One of the menu subitems is called NUnit-GUI and is used to activate the NUnit graphical interface. The NUnit GUI is used to run tests: it’s one of NUnit’s test runners. There is also a console test runner available, which is recommended for more advanced and integrated build setups. As a first step after the installation, I recommend you open the NUnit GUI and execute the sample tests provided with the NUnit installation. By running these samples, you can achieve two objectives:
72
❑
You verify that installation of NUnit went smoothly. One possible problem you might face is the version of .NET framework you have installed. If you have Visual Studio 2005, .NET Framework 2.0 comes with it by default and should work with the latest version of NUnit available at the moment of this writing, 2.4.5.
❑
You familiarize yourself with NUnit’s graphical user interface. While there are other ways to run NUnit, this is the most common place to start.
79796c03.qxd:WroxPro
2/22/08
4:58 PM
Page 73
Chapter 3: Assembling a Refactoring Toolkit You are now going to take a look at the GUI after the successful installation of NUnit. In order to get things going you can try running some of the samples that come with it. VB samples are placed in the Samples/VB folder inside the NUnit installation folder. These tests are distributed as source code, so you should build the samples first with the Visual Studio IDE. Don’t worry about the code for the moment; you will get to that later. For now, just build the dll. After that, using the File ➪ Open option in the NUnit GUI, locate the dll you just created and hit the Run button. Figure 3-10 shows the NUnit GUI after the execution of a sample VB project.
Figure 3-10
On the left side of the window you can see a tree view representing all available tests. Once you get to writing tests you will see that nodes represent a hierarchical organization of assembly, namespaces, classes, and methods and properties in your test project. By selecting a certain node in the tree, you can choose to run only a selected test or group of tests. This is a useful feature once the number of tests grows such that it would take too long to execute all the tests each step of the way. On the right-hand side are two buttons. You can use the Run button to start and the Stop button to interrupt the execution of tests at any time. Below the buttons is the progress bar, which indicates the state of test execution. Under the area with the Run and Stop buttons and the progress bar you can see a set of tabs that display different information related to test execution. ❑ ❑
Errors and Failures lists all the tests where an error was encountered. Tests Not Run lists all tests that didn’t get executed. These can be tests that are marked with the Ignore attribute. If a test is marked with this attribute, it will not be taken into account by the test
runner — for example, if you didn’t finish this particular test yet and you’d rather skip it. (I’ll get into the details of attribute use with NUnit once you start implementing your own tests.)
73
79796c03.qxd:WroxPro
2/22/08
4:58 PM
Page 74
Part I: Introduction to Refactoring ❑
Console.Out displays console output from the program. Generally, this is the debug information left in the code by Console.WriteLine.
❑
Console.Error displays all unhandled exceptions encountered during the test execution.
❑
Trace displays trace output written by your tests using the Trace class from the System .Diagnostics namespace.
❑
Log displays NUnit internal log messages. NUnit uses the log4net logging framework as an internal logging mechanism.
Finally, in the area below the tabs, a detailed location of each error or failure is displayed. Color plays an important part in communicating the success or failure of test execution. Nodes in the tree are displayed in different colors depending on the execution’s success (or lack of it). Table 3-2 shows the meaning of each color.
Table 3-2: NUnit Color Legend Color
Meaning
Green
Success
Yellow
Ignored
Red
Failure
Gray
Test not executed yet
The progress bar will be rendered in a similar way. There is one difference, however: it is enough for a single test to fail for the progress bar to turn red. Now it’s time for you to take a look at the code and write your first test.
Implementing Your First Test In order to implement your first test, you’ll go back for a moment to the application I refactored in Chapter 2, the Calories Calculator. As you might remember, the core logic ended up in three classes: Patient, MalePatient, and FemalePatient. You’ll use Patient and FemalePatient classes for the NUnit demo. You will write NUnit tests that can verify the behavior of these two classes. After you implement the tests, each time you modify one of these classes — to change or add functionality, to resolve a bug, or to refactor it — you will be able to check that no undesired effect has been produced by the changes and that the code still behaves as intended.
Creating the Test Project You need to start out by creating a new project for your tests. This is because you do not want to distribute any of the tests with the production code. You’ll use the existing Class Library Visual Studio template to create your new project. That means you’ll end up with a dynamic link library once you build the project. You have already seen how you can use the NUnit GUI application to execute tests in such a library. You can see the Visual Studio New Project dialog window in Figure 3-11.
74
79796c03.qxd:WroxPro
2/22/08
4:58 PM
Page 75
Chapter 3: Assembling a Refactoring Toolkit
Figure 3-11
Now you need to add a reference to the NUnit dynamic link library to our project. The name of the dll is nunit.framework, and you can locate it on the .NET tab in the Add Reference dialog. This dialog window is shown in Figure 3-12, and you can open it by selecting the Project ➪ Add Reference option.
Figure 3-12
75
79796c03.qxd:WroxPro
2/22/08
4:58 PM
Page 76
Part I: Introduction to Refactoring Add the existing Patient, MalePatient, and FemalePatient classes to the project by using Project ➪ Add ➪ Existing Item. Once the Add Existing Item window opens, you need to locate .vb source files containing the classes you are going to test. The Add Existing Item dialog is shown in Figure 3-13.
Figure 3-13
You need to repeat this step for each class, because each resides in its own .vb file. Now that you have access to all the classes you are going to test, it’s time to add a first test class to the project.
Creating a Test Fixture As I already mentioned, writing unit tests is akin to any other type of programming. As always, the first step is adding a new class to the project. Call it TestFemalePatient. This class needs to be marked with a NUnit attribute: TestFixture. By marking the class with the TestFixture attribute, you are telling the NUnit test runner that the class contains tests that need to be executed. You can group different tests in the same class if they share runtime resources. This often means that you’ll end up having one TestFixture per tested class. Take a quick look at the class declaration: Imports NUnit.Framework Public Class TestFemalePatient End Class
76
79796c03.qxd:WroxPro
2/22/08
4:58 PM
Page 77
Chapter 3: Assembling a Refactoring Toolkit You can see how the NUnit.Framework namespace is imported and how the TestFixture attribute is applied to the TestFemalePatient class. If you build the project now and open the dll using the NUnit GUI, you will see the class in the test tree view. You can even run the tests. Because the class is still empty, the progress bar and test tree are colored yellow: the test runner wasn’t able to find any test methods yet. Next it is time to implement your first test method.
Writing Tests You have now prepared the first class that will become part of your test suite. In NUnit, each test is written in the form of a method. This method has to be public. It has no parameters and does not return any values. In order to be identified as a test method to be executed by the test runner, it has to be marked with another NUnit attribute: Test. Here you are going to add such a method to your class, and you are going to call it IdealBodyWeight. It means you are going to test the IdealBodyWeight functionality offered by the FemalePatient class. In this particular case you are going to test the method for the most common situation, wherein valid parameters are provided for the calculation. (Just to remind you, the formula provided for ideal weight calculation is not suitable for persons shorter than five feet.) The mechanics for writing tests are the following: ❑
Prepare all parameters and property values the object needs for execution.
❑
Create an instance of the tested class and set all the necessary properties of the instance.
❑
Execute the tested method with parameters that were already created.
❑
Compare the effect or value returned by the tested method to an expected value or effect.
In the case of FemalePatient, you need to set the Age, Height, and Weight properties of the FemalePatient instance. Then you need to call the IdealBodyWeight method of an object. This method expects no parameters, but it does return the Decimal type value. As a last step, you will tell NUnit to assume that the expected result and real result are equals. If they are not equal, NUnit will report the error in test execution. Listing 3-1 shows the code for the process just described.
Listing 3-1: Testing the IdealBodyWeight Method ‘NUnit Test attribute applied to a test method Public Sub IdealBodyWeight() ‘Instance of object under test created and properties set Dim femalePatient As FemalePatient _ = New FemalePatient femalePatient.HeightInInches = 72 femalePatient.WeightInPounds = 110 femalePatient.Age = 30 Dim expectedResult As Decimal = 161.15626 Dim realResult As Decimal realResult = femalePatient.IdealBodyWeight ‘Result of test defined through NUnit assertion ‘by comparing that expected and returned result are equal Assert.AreEqual(expectedResult, realResult) End Sub
77
79796c03.qxd:WroxPro
2/22/08
4:58 PM
Page 78
Part I: Introduction to Refactoring This code listing should not raise any eyebrows. Except maybe for the last line, where the Assert class of the NUnit framework is used. The next section takes a look at it in detail, since it is the crucial line in the listing.
Using Asserts NUnit includes a class, Assert, that provides a number of shared methods. These methods are used to communicate to NUnit how you expect the program to behave. By asserting, you are making an assumption or claim about something. This assumption can prove to be true, and in that case a green progress bar is displayed. Or it can fail, resulting in a red node and progress bar in the NUnit GUI. In this specific case, here is what I did:
1. 2.
I used a formula for the ideal weight calculation to obtain the expected value.
3.
I used an instance of FemalePatient to obtain the result of a calculation performed in the program and stored it in local variable realResult.
4.
I established that these two values are equal.
I wrote down this literal value in the test code by assigning it to a local variable called expectedResult.
Now it’s time to run NUnit again. Since the code is behaving as expected, you should see green in the NUnit GUI. The Assert class contains a number of methods. They all do more or less similar things. If you think about it, you only need one method to verify about any type of assertion you might think of — a method to check if a certain Boolean statement is true. Unsurprisingly, there is exactly such a method in the Assert class. It is called IsTrue. Another method, similar to AreEqual, is AreSame. This method is used to assert that two references are pointing to the same instance. The following code snippet illustrates its use: Dim femalePatient1 As FemalePatient = New FemalePatient Dim femalePatient2 As FemalePatient = New FemalePatient Dim femalePatient3 As FemalePatient = femalePatient1 Assert.AreSame(femalePatient1, femalePatient2) Assert.AreSame(femalePatient1, femalePatient3)
Have you guessed already? In this case the first assert will fail, while the second will pass. I have initialized femalePatient3 with a reference to femalePatient1. Both variables point to the same object, and ultimately to the same memory space. This is why the second assert passes the test. In the first assert, I am comparing two completely different objects. This results in the AreSame method returning a value of False, making the first assert fail the test. If you want more information about the results of the Assert.AreSame test shown in the code illustration, take a look at Chapter 11, where I talk in more detail about object identity and the differences between value and reference types. Yet another method available in the Assert class is the method IsNull. This method lets you assert that a certain variable does not refer to any instance.
78
79796c03.qxd:WroxPro
2/22/08
4:58 PM
Page 79
Chapter 3: Assembling a Refactoring Toolkit A number of other methods are added to the class for your convenience. A lot of them are negative forms of methods you have already seen — for example, AreNotEqual, AreNotSame, IsFalse, IsNotNull, and so on. It’s worth exploring the available methods in the Assert class further yourself. For now you need to continue writing tests for the FemalePatient class. We need to test another method in our application. This method is called DailyCaloriesRecommended. To test it, you can repeat the steps you took to test IdealBodyWeight. The method I came up with is available in Listing 3-2.
Listing 3-2: Testing the DailyCaloriesRecommended Method Public Sub DailyCaloriesRecommended() Dim femalePatient As FemalePatient _ = New FemalePatient femalePatient.Height = 72 femalePatient.Weight = 110 femalePatient.Age = 30 Dim expectedResult As Decimal = 1015.2 Dim realResult As Decimal realResult = _ femalePatient.DailyCaloriesRecommended() Assert.AreEqual(expectedResult, realResult) End Sub
You can now run the NUnit GUI again and see both of the methods pass. It means that the second test method was written well also. Still, there is one detail that is not to my liking. Both methods have large portions of code that are completely identical. You are better off writing that code in only one place. The next section shows how you can.
Using SetUp and TearDown In the NUnit framework it is possible to mark a method with a Setup attribute. This will signal to NUnit that this method should be executed before any of the test methods are run. You can use this attribute to mark a method that can contain code common to both test methods, and to create objects that will represent the test fixture. In order to do this you need to perform two modifications to the code:
1.
Add a new method and mark it with the SetUp attribute. The method creates an instance of the FemalePatient class and assigns valid values to its properties. The code you need is already contained in both test methods, so you can move it to the new CreateFemalePatientInstance method.
2.
Promote the femalePatient local variable to an instance variable.
Remember, TestFixture is just another plain old VB class, so you can add instance variables and methods at will. It’s time to take a look at the test class. TestFemalePatient is shown in Listing 3-3.
79
79796c03.qxd:WroxPro
2/22/08
4:58 PM
Page 80
Part I: Introduction to Refactoring Listing 3-3: An Example of SetUp Usage Imports NUnit.Framework Public Class TestFemalePatient Private femalePatient As FemalePatient Public Sub IdealBodyWeight() Dim expectedResult As Decimal = 161.15626 Dim realResult As Decimal realResult = femalePatient.IdealBodyWeight Assert.AreEqual(expectedResult, realResult) End Sub Public Sub DailyCaloriesRecommended() Dim expectedResult As Decimal = 1325.4 Dim realResult As Decimal realResult = _ femalePatient.DailyCaloriesRecommended() Assert.AreEqual(expectedResult, realResult) End Sub ‘SetUp attribute used to mark a method <SetUp()> Public Sub CreateFemalePatientInstance() femalePatient = New FemalePatient femalePatient.HeightInInches = 72 femalePatient.WeightInPounds = 110 femalePatient.Age = 30 End Sub End Class
At this point you are probably thinking that the last step is quite familiar, similar to some transformations you saw in Chapter 2. You’ve guessed it. When you created the CreateFemalePatientInstance method you refactored the TestFemalePatientClass. You extracted one method and promoted one local variable to an instance variable. Unit tests are an integral part of your code base, so you need to maintain them in optimal shape. Because of this, they are also subjected to refactoring. One of biggest objections to unit testing is that it makes you write a lot of code that never gets to be exploited in production. It’s just another artifact that you use to better your procedures and to protect yourself from your own mistakes. While this might be true, the benefits of unit testing greatly outweigh any drawbacks. However, in order to keep those shortcomings to a minimum, it is very important to maintain and refactor your test code. Going back to the SetUp method, you have seen that it gets executed before each method marked with the Test attribute is called. Following simple logic, you might ask for a way to write a method that gets executed after each test method is performed. NUnit provides for such a case. If a method is to be executed after each test method, it should be marked by the TearDown attribute. Similar to the SetUp and TearDown attributes are the SetUpFixture and TearDownFixture attributes. If you mark a method with SetUpFixture, this method gets executed once the test class or fixture is created. A method marked with TearDownFixture is the last method that will get executed in the test class, after the execution of all test methods.
80
79796c03.qxd:WroxPro
2/22/08
4:58 PM
Page 81
Chapter 3: Assembling a Refactoring Toolkit Dealing with Exceptions: the ExpectedException Attribute As I mentioned earlier in this chapter, you have now tested a few methods of the class in the most common situations. However, you have seen that some of the formulas are not applicable to people whose height is less than five feet. If someone tries to use the class to calculate ideal body weight for such a person, you would expect it to throw an exception. Testing your code for boundary and exceptional circumstances is a very important part of your testing methodology. You cannot write tests for every possible value, so you need to try to exercise the code for the most sensitive values. These are generally boundary and invalid values. Experience teaches that bugs are especially common in such situations. Programmers often fail to take into account less common values. In the case of the Patient class, the most logical place to control for invalid height value is the Height property of the Patient class. Since FemalePatient inherits Patient, for demonstration purposes you can use the same FemalePatient instance to test whether an exception is thrown. The way to tell NUnit that you expect an exception to happen is to mark the test method with another attribute, ExpectedException, in addition to Test. The ExpectedException attribute needs a single parameter, a type name of the exception you expect. What happens once the test runner executes the method marked with ExpectedException is a check that the exception is thrown. If the exception is thrown, then everything went as expected, and the green light is shown in the NUnit GUI. However, in case no exception is thrown or the exception thrown is of a different type from the one specified in the ExpectedException attribute-line creation, the test has failed, and the signal shows red. You can expect an ArgumentOutOfRangeException to be thrown if you try to assign a value of less than 5 to the Height property: _ Public Sub HeightLessThan5Ft() femalePatient.HeightInInches = 59 End Sub
After adding this method, I execute the tests again. However, the HeightLessThan5Ft test has failed. I now take a look at the Get part of the Height property in the Patient class, and sure enough, I forgot to limit the value of height in code. I now have to modify the Height property in the Patient class. It ends up looking like this: Public Property Height() As Decimal Get Return heightInInchesValue End Get Set(ByVal Value As Decimal) If Value <= 60 Then Throw New System.ArgumentOutOfRangeException( _ “Height has to be greater than five feet (60 inches)“) End If heightInInchesValue= Value End Set End Property
I hit the Run button in NUnit again, and everything’s green.
81
79796c03.qxd:WroxPro
2/22/08
4:58 PM
Page 82
Part I: Introduction to Refactoring This sums up the basic usage of NUnit, but we have a few more important details regarding unit testing to cover before you’re done with this chapter.
The Test-Driven Approach The simple test class illustrates the basic concepts behind unit testing and the NUnit framework well. You used familiar code and provided a testing harness that can verify its correctness. However, this is a rather rudimentary approach to unit testing. With the wider acceptance of unit testing by developers, a very important change in the “chicken or egg” dilemma in software testing occurred. Programmers note that they often lack explicit means to define requirements. Requirements are often ambiguous, expressed in text in the form of storyboards, diagrams, and so on. Even more often, in an attempt to produce more complete solutions that can also resolve any future requirements, programmers go to great lengths in producing code that finally never gets used. This is a great loss of time and resources. Unit tests are quite useful in resolving these problems. There isn’t too much competition when it comes to explicitly defining the behavior of your code. The code itself is as explicit as it gets. Also, if you write the test, it must be because there is a requirement for your code to behave in a certain way. You will not waste your time testing the code for behavior that is not required. Wouldn’t be nice if you could have tests for your code even before you wrote it? All you would need to do is program the classes in such a way that none of the test fails, and you would be done. If there is no test for certain functionality, it must be because that functionality is not really needed. In test-driven development, you actually start out by writing tests first. Since no code exists to implement the tested functionality at this point, the test will obviously fail on the first run. You can then write code to make the first test execute without error. You continue by adding the next test and then the code to make that test pass, and so on. Being guided by requirements embodied explicitly in test code, you maintain your code lean, relevant, and clear of bugs. So, how does this work out in practice? Go back to the Calories Calculator application for the moment. Imagine there is another method you would like to add to the FemalePatient class. This method is called BodyFatContent, and all needed data is already available in the FemalePatient class in the form of properties. You are going to start by adding a Test method to the TestFemalePatient class. Call it BodyFatContent, and mark it with the Test attribute. In the body of the method, you are going to call the BodyFatContent method on the femalePatient instance. Since this method still does not exist in the FemalePatient class, Visual Studio reports an error. In C#, Visual Studio 2008 has a Smart Tag feature that can generate a new method stub in the FemalePatient class automatically, saving you a few keyboard strokes. It’s a nice feature and helpful for writing tests in test-driven way. Unfortunately, this feature is not present in Visual Basic. Take a look at the code you have so far: Public Sub BodyFatContent() Dim expectedResult As Decimal = 36 Dim realResult As Decimal = _
82
79796c03.qxd:WroxPro
2/22/08
4:58 PM
Page 83
Chapter 3: Assembling a Refactoring Toolkit femalePatient.BodyFatContent() Assert.AreEqual(expectedResult, realResult) End Sub
Now you can go back to the FemalePatient class and implement the BodyFatContent method. You can run the tests again and check that all have passed. Test-driven development is an important practice in agile teams. It goes hand in hand with refactoring as a means to achieve new levels of productivity and software quality. There is much more to test-driven development than what I can cover here, but hopefully I have managed to at least awaken your interest.
Other Test Tools to Consider There are a plethora of other tools that you might find useful when writing unit tests. However, I don’t have space to cover all these tools exhaustively in this chapter. Instead, I’ve chosen a handful of tools that I have found complement my refactoring efforts quite nicely.
The TestDriven.NET Visual Studio Add-In You can get a much better integration between NUnit and Visual Studio if you install the TestDriven.NET open-source Visual Studio add-in. It will permit you to run and stop tests directly from Visual Studio. Also, it will display the test results inside the Visual Studio window. This way you do not need to switch between the NUnit test runner and Visual Studio while you program. You can download this add-in from www.testdriven.net. Take note that the free personal license is intended for trial users, students, and open-source developers.
The NUnitForms GUI-Testing Framework When testing parts of the GUI, you need an approach that’s a bit different from the one you take in testing plain old VB classes. You need to simulate user interaction with the program. This is not so easy to do with NUnit. NUnitForms is an extension of NUnit that was made to facilitate the task of testing GUIs made with Windows Forms. It can clean forms, detect and manipulate dialog windows, and so on. While NUnitForms can help you in your task of creating standard unit tests, this framework can also be helpful in producing another type of test. NUnitForms can be used to simulate user interaction or even to record it and replay it later. If you run these tests not against a single GUI class, but against the complete application, you can create a sort of integration of acceptance tests. I found this approach very useful when faced with a completed application that comes without any unit tests. You cannot always dedicate the time necessary to introduce unit tests a posteriori. However, in the process of maintenance you may need to refactor the application, and in those cases this type of test is the next best thing. NUnitForms can be downloaded from SourceForge.net at http://nunitforms .sourceforge.net.
The NMock Object-Mocking Framework As I already mentioned, when you write unit tests you need to test objects in isolation. This is a very important feature of unit testing because it can help you test exactly the desired functionality and pin down a failure to an exact method or property. However, not all the code you need to test is as simple
83
79796c03.qxd:WroxPro
2/22/08
4:58 PM
Page 84
Part I: Introduction to Refactoring as the example you have seen in this chapter. Your code might use some very complex classes and depend on them to behave properly. This makes writing unit tests much more difficult to accomplish. Mock objects to the rescue! Instead of using real objects, you can use substitute objects that take on the interface of a real object. This substitute, a mock object, can verify the way the object being tested is interacting with it. If the interaction is not as expected, the test will fail. Also, a mock object can provide the object being tested with expected return values. For more information on the subject and for an open-source mock-objects framework, check out http://nmock.truemesh.com/index.html.
The NCover Test Coverage Tool You should always attempt to write your tests as thoroughly and with as broad a focus as possible. During the execution of the complete suite of unit tests for your application, you should strive to get every single line of your code exercised. However, you cannot always be as systematic as you would like to be. So instead of leaving the quality of your tests in human hands, you can use a tool that reports in detail what lines of your code were executed and what lines you failed to test. Such a tool is available at http://ncover.org. NCover can be a great help in completing your suite of tests with all relevant cases.
A Few Words about Version Control While unit testing is not so widespread, source-code version control is today universally accepted. I don’t know a team or company without some type of version control installed. This is why I will not dedicate much time to the subject, except to discuss the way it relates to refactoring. From the refactoring perspective, the importance of version-control systems is two-fold. ❑
First of all, they grant you another level of protection. No matter how disciplined you are in refactoring gradually, applying small changes and then verifying correctness with test execution, sooner or later you are bound to go one step too far. An error is introduced, and it’s just too costly to debug your way out of it. In that case, you can count on version control to provide a backup. So, in the worst-case scenario, you can start all over again — fortunately, not from the beginning, but from the last snapshot of the code you took. If you version code regularly, such a setback will probably not be catastrophic.
❑
Another way in which version-control systems affect refactoring has to do with code ownership. A lot of teams, as the first step out of the primordial chaos of team development, choose to implement individual code ownership. In this model, programmers have their own classes, components, or other code units that belong only to them: no other programmer is authorized to change them. Some version-control systems, namely SourceSafe, promote this model by default. When one programmer checks out a file, no other programmer is allowed to edit it. The idea behind this form of team organization is to promote order and field expertise. Programmers can become experts in their own area, thus being more efficient and productive. They can be sure that nobody will meddle with their code, which gives them a feeling of control and confidence. For new and inexperienced teams this form of concurrent version control is definitely a step forward. However, it also has various drawbacks that become more apparent as refactoring practices are adopted. With focused expertise, the phenomenon of programmer-owners can appear. The
84
79796c03.qxd:WroxPro
2/22/08
4:58 PM
Page 85
Chapter 3: Assembling a Refactoring Toolkit code is written in such a manner that it is very difficult for other programmers to take over. This leads to the proliferation of diverse, often cryptic, coding styles and standards. Because you are allowed to change only your own code, this model can cause stagnation. And then, it can play directly against refactoring. Some refactoring techniques require widespread changes to source code. These changes are not limited to a single method or a class. In this case, individual code ownership can work against you. For example, how can you rename a class if you are not allowed to change all the code that is making use of that class? For that reason, agile teams tend to adopt collective code ownership. In this model, any member of a team is allowed to modify any project item. Also, it is possible for more than one programmer to work on a single file simultaneously. When this happens, if conflicting changes are discovered at the moment of check-in, these are resolved by merger.
Summar y Effective tools are very important for the successful adoption of refactoring practices. These tools can greatly improve your productivity. The most important tool from a refactoring perspective is one that you can use to automate the application of refactoring transformations. With Visual Studio 2005, Visual Basic programmers have such a tool available to them free of charge. Refactor! for VB from Developer Express integrates seamlessly with Visual Studio 2005 using Visual Studio add-in architecture. You have now taken a look at installing and using this add-in. Refactor! has a number of novel GUI widgets that help you interact with the user in an unobtrusive way. You have also seen how to activate each of the more than 20 refactorings this tool provides. Another programming practice that almost always goes hand in hand with refactoring is unit testing. By unit testing your code, you can easily verify its correctness after any modification is applied to the code base. You have seen in action an open-source unit-testing framework that can help you streamline test creation and execution. This framework is called NUnit. You used familiar code from the sample application in the previous chapter and provided it with unit tests. You also saw how to install and run NUnit. There are other open-source tools that can help you in your refactoring tasks. I have mentioned some of them. I also mentioned a very popular practice among agile teams often referred to as test-driven development. Finally, I reserved a few lines for source-code version control and its role in the refactoring process. These tools can be used to promote some form of code ownership, and with agile teams the preferred form is collective code ownership. After this chapter, you are armed with all the necessary tools for your refactoring process. In the next chapter you start to deal with some of the basic refactoring techniques very relevant to VB programmers.
85
79796c03.qxd:WroxPro
2/22/08
4:58 PM
Page 86
79796c04.qxd:WroxPro
2/23/08
7:46 AM
Page 87
Rent-a-Wheels Application Prototype Refactoring is a very practical, hands-on type of art. Therefore, it is difficult to talk about the mechanics, motivation, and benefits of refactoring unless there is a good illustration to accompany it. While small snippets of code serve well to prove a specific point, you might be left wanting a look at the bigger picture, or an example of of refactoring in a real-life situation. In order to remedy this problem, I have decided to present you with a real-world scenario. I will go on from here to implement the complete and fully functional application. I will apply refactoring as I go and explain the logic behind the refactoring process in detail. That way, not only will each refactoring technique be illustrated with code samples, but you will see the refactoring process applied throughout the lifetime of this sample application. I will end each chapter in the book with a demonstration of refactoring in practice. I will use the Rent-a-Wheels application for this purpose. Throughout the book you will witness the progress of the application from a simple but fully functional GUI-based prototype to a full-blown tiered enterprise application. Since I will refer to the Rent-a-Wheels application in almost every chapter that is to follow, you should try to analyze the application while reading this chapter and think of possible weaknesses in the design and implementation. This way, as you progress with the book you will be able to compare your initial judgment with weaknesses and smells I will expose and deal with later on. My intention is to present you with a realistic scenario. I have even written down a few conversations with the client so you can put yourself in the same situation — that of analyzing and distilling raw information from typical laymen who might want to use your product. I also introduce another character in this chapter. Tim is our typical apprentice programmer, already in command of basic Visual Basic programming techniques, but not at all familiar with refactoring or some of the more complex design principles. He will eagerly get the job done, but, as you will see throughout the book, this is not sufficient for well-designed, refined, industrialstrength applications. In this chapter I will let Tim explain in his own words the work he has performed on the Rent-a-Wheels prototype implementation.
79796c04.qxd:WroxPro
2/23/08
7:46 AM
Page 88
Part I: Introduction to Refactoring I suggest you download the complete project from www.wrox.com and use the IDE to follow as we go along. I have included a lot of code listings in the text, but you’ll have an easier time finding your way around if you use Visual Studio with the complete source code at hand. I’ll start presenting the application right from the inception. This way, you can see the business case for the application and observe work on the application in progress. Bear in mind that this chapter is not meant to teach you software design or analysis. While you are reading it, I hope this chapter does two things: ❑
I hope it helps you feel as if you’re having a typical day in the office and using some very familiar techniques and styles.
❑
Then, I hope to use this familiarity as an entry point to discuss some typical VB approaches to programming and how they relate to refactoring and software design in general.
Inter viewing the Client The client is a vehicle rental company. It needs to automate and computerize its operation to make its process more efficient. It needs to cut the time it takes to serve each client, to speed up the vehicle reception process, and to get rid of unnecessary paperwork. The first task I undertook was to speak to different people employed at the company. This way I gathered the information I needed to define the requirement and start the project. The next sections show the transcripts of my conversations with Rent-a-Wheels employees.
Interviewing the Manager My first meeting was with the manager. His opinion has the most weight on the project since he will make all the final decisions regarding the application. Here is what he had to say: “Rent-a-Wheels is a successful car rental company. We are more than 10 years in the market and have managed to position ourselves well. Our biggest strength is a loyal clientele and a new fleet, since we had a big investment come in earlier this year. At the same time, we started another location. We opened up the airport branch. “This challenged our operations in a new way. You see, there is a lot of competition at the airport, and more than a few rental desks are placed next to each other. We have noticed that as soon as a line forms the clients start going over to the competition. The time we take to serve a single client is just too long. I spoke to our staff and they tell me that our existing system, based on pen and paper, is just too tedious. The competition does everything on the computer. One company recently acquired big flat-panel monitors. There is a free wireless Internet connection covering the airport, so we can rely on that for connectivity. “We also waste time on reception. The desk is informed that a vehicle is available only after the inspection and cleaning of that vehicle is done. It doesn’t take long, but still we lose customers in the process. We need to do something about it; we need to sell more!”
88
79796c04.qxd:WroxPro
2/23/08
7:46 AM
Page 89
Chapter 4: Rent-a-Wheels Application Prototype This talk with the manager provided some insight into the task facing me: ❑
First of all, it seems the company needs an efficient system to manage the fleet and keep track of the availability of the vehicles, and to present this information at the reception desk.
❑
Another important point I noticed is that the company has multiple branches. The system will have to consider that also.
However, while talking to the manager gave me more insight into my objectives, it failed to provide detailed information on the way business is conducted. So the next person I had to interview was the receptionist.
Interviewing the Desk Receptionist The desk receptionist works with clients directly. She gave me more information about the business process. Me: I guess the first thing you look at is what vehicles are available when clients call or come over? Desk Receptionist: Yes, we have all vehicles divided into different categories. Clients generally ask for a category or specific model — that’s what the price depends on. Other details, like color, come up less frequently. I maintain a list of all the vehicles in Excel, and I mark them as they get rented or returned. The problem is that clients can return their vehicles to any branch they want, so I end up updating the list all the time. Me: Okay, great, let me make a note of that . . . I understand you have a lot of paperwork? Desk Receptionist: Oh, yes. You mean you want to know more about the receipts? Me: Yes, right, what kind of receipts? Desk Receptionist: “When I finish taking all the data from the client, I issue him a receipt that he has to deliver to our people at the parking lot in order to get the car. And when he returns the car, they use the same receipt to write down additional charges that might apply, like empty tank, mileage, et cetera. I guess that’s about it. Me: That was very helpful. Thanks a lot! I have more material to work with now. ❑
I know I need to manage vehicles and mark when they are rented or returned.
❑
I know I need to maintain vehicle information like model and color.
❑
I know that all vehicles belong to a certain category, and that the price of the rental depends on it.
If PCs are in all the right places, the application I provide should be able to eliminate a lot of paperwork. Next, I determine it’s best that I talk to another party intimately involved with this process, one of the parking lot attendants.
89
79796c04.qxd:WroxPro
2/23/08
7:46 AM
Page 90
Part I: Introduction to Refactoring
Interviewing the Parking Lot Attendant It is sometimes the case that no single employee is familiar with the whole business process in detail. Sometimes the best way to collect requirements is to hear the facts from different sources in order to piece together the whole story. This is the reason I decided to speak to employees in any position that might benefit from the application. A parking lot attendant should provide some insight into the way the lot is operated. Me: The customers pick up the cars here? Parking Lot Attendant: Yes, my duty is to hand over the car to the customer and to receive it back. The customer has to give me a receipt so I know he finished all the proceedings at the reception desk. I then write down the mileage and put the receipt in the “Rented Vehicles” folder. When the customer returns the vehicle, I check the mileage and the tank and write the numbers on the receipt. We give the customer a bonus if he returns the car with the tank full. Me: And what happens if the tank is not full? Parking Lot Attendant: Oh, the maintenance guy has to fill up the tank, and this is charged in addition to other items. No bonus in that case, of course. Plus, the customer gets to wait if he is not satisfied with what I wrote down. I can only write down full, three-quarters, one-half, one-quarter, or empty from looking at the indicator. If the customer thinks this is not correct, he has to wait for the maintenance guy to come back with the receipt from the filling station. After the reception, the vehicle has to pass through a regular inspection by our maintenance personnel. I check for any obvious damage when the vehicle is returned, but I am not in charge of the maintenance. The story seems to be more complete now. ❑
The application needs to keep information on mileage for each rental.
❑
The application needs to keep information on the tank level after the vehicle has been returned.
But my conversation with the parking lot attendant has made me realize that to get the complete story, I must speak to still another party — a member of the maintenance personnel.
Interviewing Maintenance Personnel This is the last employee I need to speak with. By talking with this employee, I hope to learn more about vehicle inspection and maintenance procedures. Me: Your job is to inspect vehicles when they are returned? Maintenance Worker: Yes. I have to check for any damage, and I need to make sure that the tank is full. I also keep records on regular maintenance that is performed after a certain number of miles. If the time is up, I have to take the car to the service center. I have to let the people at the desk know so they are aware that that particular car is no longer available. Since we let our customers return the vehicle to any branch they like, sometimes at the end of the day I have to take cars back to the branch they came from. Me: Okay, thank you for your time.
90
79796c04.qxd:WroxPro
2/23/08
7:46 AM
Page 91
Chapter 4: Rent-a-Wheels Application Prototype From this final conversation it is clear that vehicles are regularly removed from the fleet in order to receive routine maintenance. The inspection performed after the customer returns the vehicle is mandatory. Now it is time to compile all the information I have gathered and to prepare the proposal I am going to present to the folks back in my office.
Taking the Initial Steps in the Rent-a-Wheels Project I decided to start the work by identifying the principal use cases. This will serve as a starting point for the prototype that I will use to obtain the final go-ahead from the client. While I am at it, I might as well sketch a few diagrams — it always gets a nod of approval from the boss.
Actors and Use Cases The first step is to identify all the future users of the system, or the actors. This is not so difficult. All I need to do is follow the “paper trail” I discovered in my interviews. The receipt was traveling from the desk to the parking lot and back. Additionally, the maintenance personnel had to make a report if the car had to be filled up or sent to a service center. I decided to depict actors and principal use cases with a diagram. Figure 4-1 shows the Rent-a-Wheels use case diagram I came up with.
Rent vehicle
Hand over vehicle
Receive payment
Receive vehicle from customer
Receptionist
Parking Lot Attendant
Inspect vehicle
Receive vehicle from other branch Maintenance Personnel
Figure 4-1
After I identified the principal use cases, I decided to write down all the steps the actors have to perform while interacting with the application in order to reach the final goal. This will provide a good breakdown of the functionality the Rent-a-Wheels application will have to offer.
91
79796c04.qxd:WroxPro
2/23/08
7:46 AM
Page 92
Part I: Introduction to Refactoring Rent Vehicle 1. 2. 3.
Receptionist selects the vehicle from the list of available vehicles. Receptionist inputs the customer data: ID type and number. Receptionist marks the vehicle as “hand over.”
Receive Payment 1. 2. 3.
Receptionist selects the vehicle from the list of “payment pending” vehicles. System displays the charges. After the payment has been made, receptionist marks the vehicle as “available.” Precondition: Customer has to return the vehicle to the parking lot first. (See the use case “Receive the Vehicle from the Customer.”)
Hand Over Vehicle to Customer 1. 2.
Parking lot attendant searches for vehicle marked as “hand over” that has customer’s ID. Parking lot attendant marks vehicle as “rented.” Precondition: Customer has to finish all formalities at the desk first. (See the use case “Rent a Vehicle.”)
Receive Vehicle from Customer 1. 2. 3.
Parking lot attendant searches for rented vehicle in the vehicle list. Parking lot attendant inputs mileage, tank level, and any additional comments. Parking lot attendant marks vehicle as “payment pending.”
Inspect Vehicle 1. 2.
Maintenance personnel select vehicle being inspected from the list. Maintenance personnel mark vehicle as “in operation.” Alternative:
2a.
If maintenance is due, vehicle is marked as “in maintenance.”
Receive Vehicle from Other Branch 1. 2.
Parking lot attendant searches on list of vehicles for vehicle coming from another branch. Parking lot attendant marks vehicle as stationed in the receiving branch.
Receive Vehicle from Service Center 1.
When vehicle is returned from the service center, it is marked as “in operation.”
These use cases give a pretty good overview both of the business process at the company and of the way in which users should interact with the future system.
92
79796c04.qxd:WroxPro
2/23/08
7:46 AM
Page 93
Chapter 4: Rent-a-Wheels Application Prototype One thing became rather obvious after all this: the word vehicle was present in all the use cases. Understandably, most of the business in the Rent-a-Wheels company revolves around this entity. The vehicle has characteristics like license plate number, category, make, model, and color. As a part of their work, employees have to maintain information about each vehicle and, especially important, mark the vehicle as available, hand over, payment pending, or in maintenance as it goes through various business processes. Since this state of the vehicle is at the center of the business process I am analyzing, I have decided to investigate it a little bit further.
Vehicle States As a vehicle is selected and becomes involved in the business flow, it passes through a few states in a well-defined order. As you analyze those states, you can see that their nature is twofold: some states are concerned with the vehicle in regard to its being rented to a customer and others in regard to its going to maintenance. I decided to use two diagrams to represent these states and transitions. In Figure 4-2 you can see the states the vehicle goes through during a normal rental process.
Available
Hand over
Payment pending
Rented
Figure 4-2
Rules related to vehicle states and state transitions embody the core of the business process in the Renta-Wheels company. The following list explains these states and the transitions related to normal vehicle use in more detail: ❑
The receptionist can rent only available vehicles.
❑
The parking lot attendant can hand over to customers only the vehicles marked hand over.
❑
The parking lot attendant can receive only rented vehicles.
❑
The receptionist can charge for only payment pending vehicles.
In Figure 4-3 you can see the states related to vehicle maintenance.
93
79796c04.qxd:WroxPro
2/23/08
7:46 AM
Page 94
Part I: Introduction to Refactoring
In operation
In maintenance
Figure 4-3
States and transitions describing the maintenance process represent another important set of business rules. The following list explains states related to the maintenance operation in more detail: ❑
The receptionist can rent only vehicles marked in operation.
❑
If a vehicle is due for maintenance or presents problems, maintenance personnel mark it as in maintenance.
❑
Once the vehicle is back from maintenance, it’s marked as in operation.
So, is there any relation between these two sets of states and transitions? Naturally, only operational vehicles can be rented, handed over to customers, received, and charged for. So available, hand over, rented, and payment pending are inner states of the in operation state. Or, to put it differently, in operation is a superstate, and this can be represented with a single state diagram using nested states. The single state diagram is presented in Figure 4-4. With all that work completed, I knew it would be good to finalize the work I had done by materializing all these ideas into some sort of user interface prototype.
First Sketch of the Main Application Window With all this information in hand, it’s time to take a shot at drawing the main application window. The central point of the application is the fleet and the different states of the vehicles. I can show vehicles in a certain amount of detail in a grid, and let users operate on a specific vehicle by selecting a single row in the grid. Each row represents a single vehicle. The user will be able to operate on the vehicle by selecting it on the grid and then pressing the appropriate button — Rent, Hand Over, Charge, or similar. I also need to provide for filtering of the vehicles in the list. That will make jobs a lot easier for the users, as they will be able to restrict the display to vehicles they are interested in, based on vehicle state or some other characteristic. Figure 4-5 represents the main application window.
94
79796c04.qxd:WroxPro
2/23/08
7:46 AM
Page 95
Chapter 4: Rent-a-Wheels Application Prototype
In Operation
Available
Hand over
Payment pending
Rented
In maintenance
Figure 4-4
Figure 4-5
95
79796c04.qxd:WroxPro
2/23/08
7:46 AM
Page 96
Part I: Introduction to Refactoring I thought that use cases, state diagrams, and a main window prototype gave a good starting point for the application I was about to build, so for the next step I decided to present these artifacts at the team meeting.
Rent-a-Wheels Team Meeting The Rent-a-Wheels team at this stage consisted of three persons. ❑
The developer — Me, of course.
❑
A project manager — He was not dedicated full-time to the project.
❑
A VB programmer — In this case it was Tim, a novice VB programmer, but one quite up to the task, and very efficient as a coder.
I started the meeting by presenting the information gathered so far. First, I gave them some background information about the company, the meetings I had had, and the people I had met. Then I presented the work I had done on the analysis; I showed them the diagrams and talked about use cases. I even presented my first try at a main application window. Because I was about to go on my vacation, we agreed that Tim should take over the application. He would continue to work on the analysis and take a first shot at the prototype. When I got back, I would join the team again and act as lead programmer. Upon my return, I got back to working on the Rent-a-Wheels application. During my absence, the prototype had been completed, presented to the client, and initial approval was received. While assembling the prototype, Tim went on to implement most of the functionality initially described. Tim’s idea was to try to reuse the code from the prototype in order to cut implementation time. This is fine, as long as the code is up to standards and well designed. To make sure it is so, I have to take a detailed look at the application.
Making the Prototype Wor k To begin, I asked Tim to fill me in on the progress he had made while I was away. Tim said, “I used the information you prepared during the analysis to build the prototype. I built a standalone application that connects to a Microsoft SQL database over ADO .NET. I decided not to use data binding; I had enough time to go for a more robust solution. Basically, there is one central window, based on the drawing you made. But I guess you are really interested in the database model. We had better start off there.”
Examining the Database Model Tim continued to fill me in: “The database engine is MS SQL 2005. The database name is RENTAWHEELS. It has the following tables: Vehicle, Category, Model, and Branch. I chose to use the license plate number for the primary key in the Vehicle table. The rest of the tables have an automatic, auto-incrementing primary key. The relationships are easier to observe in the diagram.” Tim produced the Rent-a-Wheels database diagram shown in Figure 4-6.
96
79796c04.qxd:WroxPro
2/23/08
7:46 AM
Page 97
Chapter 4: Rent-a-Wheels Application Prototype
Figure 4-6
He said, “Each vehicle pertains to a single branch. Each vehicle model is placed under a certain category. Category has attributes that contain the vehicle’s price information: daily, weekly, and monthly price. You can find out the price for a certain vehicle by looking at its model — Model belongs to a category and Category has the price information. When writing SQL code, this translates into an Inner Join of the Vehicle table with the Model table and an Inner Join of the Model table with the Category table. “The Vehicle table also contains information related to the rental state of the vehicle: it’s the column named Available. Information related to the operational state of the vehicle is held in the column named Operational. I used the tinyint type for these two columns. In the case of Available, the meaning of the data is shown in this table.” Tim showed me Table 4-1.
Table 4-1: Connotation of Column “Available” tinyint Values in the Vehicle Table Available Column’s tinyint Value
Meaning
0
Available
1
Hand Over
2
Rented
3
Charge
97
79796c04.qxd:WroxPro
2/23/08
7:46 AM
Page 98
Part I: Introduction to Refactoring Tim said; “You will notice that I decided to use a more compact name for the Payment Pending state. Charge fits better on the fleet display grid on the Fleet View form.” Then Tim went on, “The Operational column can have only two values.” Then he showed me Table 4-2.
Table 4-2: Connotation of the Operational Column’s tinyint Values in the Vehicle Table Operational Column’s tinyint Value
Meaning
0
In Operation
1
In Maintenance
Tim then said, “Other attributes of the vehicle are related to customer information about the customer currently renting the vehicle: names, document type, and numbers. These are used by the parking lot attendant to identify the customer at the moment when the vehicle is handed over. When the vehicle is returned, tank level and mileage are saved in Tank and Mileage columns. Of course, this data is related to a single rental, and the current customer has to be erased once the customer has paid for the vehicle. “Like the Available and Operational columns, the Tank column uses different tinyint values with special meanings.” Tim then showed me Table 4-3.
Table 4-3: Connotation of the Tank Column’s tinyint Values in the Vehicle Table Tank Column’s tinyint Value
Meaning
0
In Operation
1
In Maintenance
2
1/4 Full
3
1/2 Full
4
Full
Tim concluded, “Those were the most important facts in regard to the database.” I asked, “What about other database objects? Did you use any stored procedures, views, or triggers?” Tim said, “No, this is it. I spoke to the client and he was adamant about keeping the application as database-independent as possible. They still have to buy software licenses and they want to be able to look for the best price on the market. I thought that minimizing the code on the database side will help us not depend on any particular database model. That’s why I have placed all the SQL code inside the VB app.” With all that clear, I could then examine the VB code.
98
79796c04.qxd:WroxPro
2/23/08
7:46 AM
Page 99
Chapter 4: Rent-a-Wheels Application Prototype
Examining the Visual Basic Code Tim explained, “There are a few forms in the project, but the main form is the one with the Fleet view, enabling the user to monitor vehicles and select them in order to perform the most important operations for the business. It is the same form you designed in the analysis phase of the project. The only new functionality I added is the administration menu. It is used to invoke forms that serve to administer different tables in the database. This way the user can add a vehicle, delete it, or edit existing vehicles. The same goes for categories, models, and branches. Let me show you the menu.” Tim opened Visual Studio IDE and ran the application. He showed me the Fleet view with the data administration menu he had added (Figure 4-7).
Figure 4-7
Tim continued, “On the right side are the buttons that employees will use to perform their everyday work — a Rent button, a Hand over button, and so on. When they press the button, in some cases, the operation is directly performed, and in other cases, when additional information is needed, a new form is displayed. For example, we can take a look at the code being executed when the user presses the Rent button.” The code that handles the Rent button click event is presented in Listing 4-1.
Listing 4-1: Event-Handling Routine for BtnRent_Click from the Fleet View Private Sub BtnRent_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles BtnRent.Click
Continued
99
79796c04.qxd:WroxPro
2/23/08
7:46 AM
Page 100
Part I: Introduction to Refactoring Listing 4-1: Event-Handling Routine for BtnRent_Click from the Fleet View (continued) ‘Check that user has made a selection If DGridFleetView.SelectedRows.Count > 0 Then ‘Read value from first cell as Vehicle Id and assign ‘it to Text property of TxtLP textobox in FrmRent FrmRt.TxtLP.Text = _ DGridFleetView.SelectedRows.Item(0) _ .Cells.Item(0).Value ‘Show FrmRt FrmRt.Show() Else ‘Warn user if no selection made in table and exit MsgBox(“Please select vehicle first!”) End If End Sub
Tim said, “It is easy to follow the code because I spent a lot of time commenting on it in detail. First, a check is made in order to ensure that the user has selected a vehicle in the grid. Then, a license plate number from the first cell in the selected row is assigned to a text box in the FrmRt form, and this form is displayed. And just in case the user didn’t make his or her selection before pressing the button, a message box with a reminder is displayed. Take a look at the FrmRt form.” He showed me the rental form he had created, named FrmRt, which is shown in Figure 4-8.
Figure 4-8
Tim said, “After entering the required data, the user performs the operation by pressing the Rent button.” Listing 4-2 shows the code for handling the button click from this form.
Listing 4-2: Event-Handling Routine for BtnRent_Click from the Rent Form Private Sub BtnRent_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles BtnRent.Click ‘Declare variables Dim oCn Dim oCmd ‘double-check with user If MsgBox(“Are you sure?”, MsgBoxStyle.OKCancel) Then ‘Activate error handling On Error GoTo ErrorHandler
100
79796c04.qxd:WroxPro
2/23/08
7:46 AM
Page 101
Chapter 4: Rent-a-Wheels Application Prototype Listing 4-2: Event-Handling Routine for BtnRent_Click from the Rent Form (continued) ‘Create SqlConnection oCn = New SqlConnection(“Data Source=TESLA-DAN;” + _ “Initial Catalog=RENTAWHEELS;User ID=sa”) oCmd = New SqlCommand ‘Create Sql String with parameter @SelectedLP strSql = “Update Vehicle “ + _ “Set Available = 1,” + _ “CustomerFirstName = @CustomerFirstName,” + _ “CustomerLastName = @CustomerLastName,” + _ “CustomerDocNumber = @CustomerDocNumber,” + _ “CustomerDocType = @CustomerDocType “ + _ “WHERE LicensePlate = @SelectedLP” ‘open connection oCn.Open() ‘Set connection to command oCmd.Connection = oCn ‘set Sql string to command object oCmd.CommandText = strSql ‘Add parameter to command oCmd.Parameters.AddWithValue( _ “@CustomerFirstName”, TxtFirstName.Text) oCmd.Parameters.AddWithValue( _ “@CustomerLastName”, TxtLastName.Text) oCmd.Parameters.AddWithValue( _ “@CustomerDocNumber”, TxtDocumentNo.Text) oCmd.Parameters.AddWithValue( _ “@CustomerDocType”, TxtDocumentType.Text) oCmd.Parameters.AddWithValue( _ “@SelectedLP”, TxtLP.Text) ‘execute command oCmd.ExecuteNonQuery() ‘close connection oCn.Close() ‘destroy objects oCmd = Nothing oCn = Nothing Exit Sub ErrorHandler: MsgBox(“A problem occurred and “ + _ the application can not recover! “ + _ “Please contact the technical support.”) Err.Clear() End If End Sub
Tim explained his approach in this code: “As a first step, I give the user a chance to change his mind, just in case he pressed the button by mistake. Then I initialize the connection. I am using a .NET Framework Data Provider for SQL Server to connect to the database; since the company has Microsoft SQL Server 2005, this is the most efficient provider. After that I build SQL code as a string and assign it to a command. Then the data is recollected from the form, and values entered by the user are added as command parameters. Finally, the query is executed. At the end, I close the connection and clean up the objects.”
101
79796c04.qxd:WroxPro
2/23/08
7:46 AM
Page 102
Part I: Introduction to Refactoring When I asked Tim about error handling, he said, “Below the ErrorHandler label you can see the code that gets executed in case the error occurs. This can happen if the database server is down, if there is a connectivity problem with the database, or something like that. Basically, users are informed that an error has occurred and that they should contact technical support. The whole method is quite straightforward really, just a SQL query executed against the database. “So, basically, that’s it. If you look at rest of the code, it pretty much follows the same pattern.” I said, “Yes, thanks, Tim. I’ll take another look at the code later on, but I think I have a pretty good idea of what is going on in the application. Good job!” With that, my initial conversation with Tim about Rent-a-Wheels ended, and at this point you, too, should have a pretty good idea of what is going on in the application that I will use as an example throughout the book. If you are interested in digging further into the example application at this point, more conversations with Tim about the details of the application can be found in Appendix B, and you are welcome to take a look at those. Of course, the source code for the application is available for download at www.wrox.com, and again I encourage you to download it if you haven’t already. However, for the purposes of using this example as a real-world scenario for discussion, this is enough to start with. Accordingly, in the next section I want to talk about what this example shows and how it applies to the topic of refactoring. In that section I want to list only a few typical pitfalls VB programmers can be dragged into with their desire to get results immediately. While I have great respect for pragmatic programmers and I am not keen on wasting my time on some vague philosophical discussions, I am very interested in producing the best possible code. For that purpose, it is crucial you understand the fundamental principles behind object-oriented programming and how sometimes the fastest approach is not the best approach, nor even the most simple.
Fast and Furious, a VB Approach to Programming In many respects, the application you just saw is a very typical VB application. Not too much time has been spent on formally designing the application before, during, or after construction. Most of the design time was invested in blueprinting the database. In this case, you can assume from the lack of any other design-related artifacts that programmers embarked upon coding from the outset. This is not necessarily a mistake if it is followed by design-related improvements during the development process; otherwise you can end up with some serious pitfalls in your code. Here are some of the typical approaches used on Rent-a-Wheels that can lead to drawbacks in your code if not counteracted by the application of objectoriented design principles.
Database-Driven Design Since the database was the first component of the application to be given definite shape, the rest of the code had to comply with the decisions Tim made in constructing the database. The bulk of VB code is, in fact, interested only in interacting with the data store. I call this a database-driven design.
102
79796c04.qxd:WroxPro
2/23/08
7:46 AM
Page 103
Chapter 4: Rent-a-Wheels Application Prototype Relational database design is governed by a different set of principles from object-oriented design. When designing databases, a designer is concerned with providing efficient data storage and retrieval, enforcing data integrity, providing transactional consistency, and so on. By putting effort into database design without structuring your VB code according to object-oriented principles, and being driven by data design instead, you will end up with poorly designed VB code. Often it is SQL code that is used to express business logic. Such a design can have an efficient data backend, but your VB code will be inefficient and difficult to maintain. Modern applications use an object-oriented layer as the core layer for implementing business logic. Objectoriented code is much more powerful and efficient for expressing business rules. Later on in this book you will see how you can express business logic in object-oriented terms and keep the database layer only for data storage and retrieval, an area where databases are on their own turf.
GUI-Based Application In essence, this application is a standalone executable connecting to a remote database. It is a typical client-server application. The .NET platform provides a toolkit called Windows Forms for writing elements of the user interface, and it is exactly this toolkit that Tim used to construct the user interface for this application. As a matter of fact, the GUI was probably the only other part of the application besides the database to go through a more elaborate design process. You can see that most of the administration forms are very similar in appearance, because they follow the same pattern. Another interesting detail crops up if we look at the type of classes used in the application. All classes that were created to extend the Form class (Inherits System.Windows.Forms.Form), are part of the user interface, and have been designed in Windows Forms Designer. This is apparent when you open the code generated by Windows Forms Designer by clicking the class name in the Class View window. Windows Forms Designer generates partial classes and all code generated by the tool is in a separate file. All the code programmed was added to form classes; no other classes or structures were added. That is why we can say that this application is GUI-based. Modern applications are often tiered and component-based. Such a modular design is a vast improvement over the legacy client-server design. You will benefit from improved reuse, simplified maintenance, simplified distribution, application modularization, and even shorter testing and compilation time. In order for you to be able to construct applications in a modular manner, the typical approach is to divide an application physically in the form of components or dynamically linked libraries across tiers. When you are depending only on GUI classes produced by Windows Forms or some other visual designer, such organization is not possible.
Event-Driven Programming Visual Basic programmers are very familiar with the concept of event-driven programming. This style was present in VB a long time before the .NET platform appeared. In essence, the tool is capable of generating a hook, an empty event-handling routine declaration. It is up to a programmer to fill in the body with the code that gets executed when a certain event, a consequence of user interaction with the application, occurs.
103
79796c04.qxd:WroxPro
2/23/08
7:46 AM
Page 104
Part I: Introduction to Refactoring This is exactly the approach taken in constructing the Rent-a-Wheels application. Most of the code is placed inside event-handling routines, in most cases a Click event of some button. Rent-a-Wheels is typical of event-driven programming. The problem arises when the programmer does not go beyond the routines generated by the tool. If you are not structuring code further, into separate methods and classes, there is bound to be a lot of repetitive and duplicated code. Such code will make your maintenance much more difficult, and your design bloated and inefficient. Again, the solution is in using the object-oriented approach and restructuring your code in keeping with object-oriented principles.
Rapid Application Development (RAD) The Rapid Application Development (RAD) approach is meant to empower the programmer with tools and techniques that can radically cut development time. This is possible thanks to visual designers, wizards, and code generators. Another benefit is the tool capacity to create prototypes early in the development cycle. You have seen this approach at work with the Rent-a-Wheels application. Tim was able to construct a fully functional prototype in a short time. This prototype will be reused to create a final version for production. While this approach is very suitable for constructing quick and dirty prototypes and some very simple applications, it shows weaknesses at different scales when you are working with more complex applications. This is because of the code proliferation and reduced code reuse often found in RAD code: these weaknesses can easily outweigh the benefits of RAD if they are not mitigated with thoughtful, objectoriented design.
Copy-Paste as a Code Reuse Mechanism As you gain more experience, you soon begin identifying the repetitious patterns and recurring problems you deal with while you program. Naturally, you soon start developing techniques that can help improve your productivity and avoid repetition. One of the first techniques you adopt for this purpose is copying and reusing sections of code that can be adapted to a new use with a minimum of modifications. This can be a great productivity booster and is an important confidence-builder for a novice programmer. There is almost nothing as satisfying to an inexperienced programmer as being able to finish a new task quickly by reusing work done on some other project, done in some other part of the application, or simply found on the Internet. While this is an important improvement in programming technique and productivity for an aspiring programmer, it has serious downsides. Copy-paste as a reuse mechanism leads to code proliferation and duplication that can soon turn into a real maintenance nightmare. Copy-paste can be seen as a first step on a programmer’s path to advanced skill acquisition. The same capacity for pattern recognition can lead to more advanced reuse mechanisms. Instead of copying a code section, the programmer can method and call extract a method call it from multiple locations. An object can be instantiated and used, a class inherited, and so on. Standard object-oriented reuse mechanisms are more powerful and bring greater benefits than simple code copying. Refactoring deals with this issue and teaches you how to use your pattern-recognition skills in a more powerful way.
104
79796c04.qxd:WroxPro
2/23/08
7:46 AM
Page 105
Chapter 4: Rent-a-Wheels Application Prototype
From Prototype to Deliver y Through the Refactoring Process All in all, the prototype was fully functional and had already received user approval. This means that the important work had already been completed successfully. The prototype represented a valuable base for the final delivery. However, at this stage it was important to address the design issues that this application presented. It needed to be refactored. You will see the results of that refactoring work as I return to the Rent-a-Wheels example throughout the book.
Summar y VB programmers often create prototypes very early in the development phase. They are able to materialize the application early on and present it to the client. This way a lot of risk regarding requirements fulfillment can be mitigated quickly. This chapter presented a real-life scenario that will serve as an example application for the book. It showed how the prototype for the Rent-a-Wheels application was created. First, requirements-gathering included interviewing the employees of the company. A manager, receptionist, parking lot attendant, and maintenance worker all had their say about the work they perform and the most important issues they expect the future application to resolve. Facts gathered from the interviews were formalized into the most relevant use cases. The vehicle was determined to be a central entity in the business process. The vehicle’s different states result from the operations that employees perform. Based on the analysis performed, a prototype for Rent-a-Wheels application was created, addressing most of the functionality needed. The application was implemented as a standalone executable that connects to a Microsoft SQL database. Upon examination, it became obvious that the prototype was constructed in line with the tradition of VB programming. The database was designed up front, the GUI was then designed with the Windows Forms Designer in Visual Studio, and in the end the coding was performed by filling in the event-handling routines generated by the tool. While the work performed is valuable and the prototype resolved most of the business-related problems the application was meant to address, it nevertheless has numerous flaws. It will have to be refactored before it can reach the final phase. As you move forward in this book and learn new refactoring techniques and code smells, I’ll come back to the Rent-a-Wheels code created in this chapter and apply your newly acquired refactoring knowledge to this close-to-real-life sample application.
105
79796c04.qxd:WroxPro
2/23/08
7:46 AM
Page 106
79796c05.qxd:WroxPro
2/23/08
8:07 AM
Page 107
Part II: Preliminary VB Refactorings In the next few chapters I am going to explore preliminary VB refactorings. I call them preliminary because you can execute them even without deeper knowledge of the problem domain the code is meant to resolve. They are mostly performed on the syntactic level and deal with problems that have their origin in VB backward compatibility or are related to good programming practices that I call “basic programming hygiene.” These refactorings let you clean and prepare the code for the standard refactoring techniques we’ll talk about in Part III.
79796c05.qxd:WroxPro
2/23/08
8:07 AM
Page 108
79796c05.qxd:WroxPro
2/23/08
8:07 AM
Page 109
Chameleon Language: From Weak Static Typing to Strong Dynamic Typing Almost all high-level programming languages implement the concept of types. By classifying values and expressions into types, you achieve a number of benefits: safety, optimization, abstraction, and modularity. Most modern programming languages can be placed into one of two categories: statically typed or dynamically typed. ❑
In statically typed languages, type resolution is performed at compile time, and type information is provided explicitly in the code by the programmer himself in the form of a variable declaration.
❑
In dynamically typed languages, data types are not declared, and type information is not available until execution.
Visual Basic can be statically or dynamically typed. This behavior of VB’s compiler is controlled by the Option Explicit statement. Languages are also differentiated by the level of type safety they provide. Strongly typed languages disallow operations on arguments that have a wrong type. Other type languages permit these operations by implicitly casting the types of arguments so that the operations in question can be performed, guided by rules that take both operands into account. The disallowing or allowing of implicit conversions in VB code is controlled by VB’s Option Strict statement. In Visual Basic 2008 there is new and different type of Option statement. Option Infer lets you omit the As clause of your local variable declaration, but the code is still statically typed. In that sense it is more of a productivity feature than an additional type-system behavior. Since I deal with VB 2008 novelties in Chapter 15, I have left the discussion of Option Infer for that chapter.
79796c05.qxd:WroxPro
2/23/08
8:07 AM
Page 110
Part II: Preliminary VB Refactorings Table 5-1 shows the kinds and levels of typing enforced in VB by the compiler when the Option Explicit and Option Strict options are active or inactive.
Table 5-1: VB’s Option Statement in Relation to Typing Enforced by the Compiler Option Explicit On
Option Explicit Off
Option Strict On
Static strong
Dynamic strong (rare)
Option Strict Off
Static weak (problematic)
Dynamic weak
This versatility of VB is unique and enables the effective use of VB for variety of purposes, from fast prototyping to industrial-strength applications. While .NET as a platform is mostly statically oriented (other languages like C# and managed C++ are statically typed), the flexibility of VB lets it fill the gap in the dynamically typed compartment. However, in some cases, when programmers are not completely aware of the effect that Option statements can have on the code, or when they are working with legacy code upgraded to VB .NET, they can produce potentially problematic code. Such code comes in two flavors: ❑
Code written in statically and strongly typed style, but without compiler enforcement of static or strong typing (Option Explicit Off and Option Strict Off but type declarations present)
❑
Code written in statically and strongly typed style, but without compiler enforcement of strong typing (Option Explicit On and Option Strict Off but type conversions not present)
In this chapter you are going to see in detail the effect that Option Strict and Option Explicit statements have on your code. ❑
You will see the benefits of strong static typing.
❑
I will show you ways to transform the problematic code, written in strong static style but without compiler enforcement, into code in which compiler checking is activated.
❑
You will also see how dynamically typed code can be useful in some circumstances.
❑
You will see how to combine statically and dynamically typed code in the same project, reaping the benefits of both styles.
❑
Finally, I will show you the alternatives you have when setting Option Strict and Option Explicit, and the role Visual Studio can play in this task.
Option Explicit and Option Strict, the .NET Effect The effect that activating the VB compiler options Option Strict and Option Explicit has on the way you write your code is profound, but it is often overlooked. This is probably because of default settings — they let you program without having to take care of some type-conversion issues. The creators of VB have
110
79796c05.qxd:WroxPro
2/23/08
8:07 AM
Page 111
Chapter 5: Chameleon Language given you the ability to ignore a number of issues as you write code so that you can be productive fast, but such an approach can have some very negative sides to it. If you take a look at the official VB documentation, these issues are seldom discussed in detail. I will explain them and give you sufficient information so you are aware of the full impact of your decision to activate or deactivate these options. When creating Visual Basic for the .NET platform, language makers had to take into an account a vast existing code base written in VB6 and previous versions of VB. It was certainly important to provide some upgrade path for existing legacy code. On the other hand, one of the most sought-after features of VB was strong static typing. Using strong static typing essentially means that you need to declare explicitly all your variables and their types and that you have to take care of all type conversions. In order to accomplish both goals, some type of upgrade path and support for static typing, the following approach was adopted: VB .NET can work both with and without static typing, depending on a compiler option value: ❑
If you deactivate Option Explicit and Option Strict, you do not have to declare variable type, or declare variables at all. Also, all type conversions are performed automatically, without your having to write any code that will deal with type conversions. This is dynamic, weakly typed code.
❑
If you activate Option Explicit and Option Strict, you are obliged by the compiler to declare all your variables and their types, and all type conversions have to be performed deliberately and written down explicitly in code. This is static, strongly typed code. I will refer to such code as strict (Option Strict On) or explicit (Option Explicit On).
The problem is that you can still write your code in a strongly and statically typed style even without activating Option Explicit and Option Strict, but you don’t get any tool support for it. If you forget to declare a variable or its type, or perform some potentially hazardous implicit conversion, the compiler does not complain. Instead, it tries to work things out to the best of its abilities. I will refer to such code — written in a statically and strongly typed style, but with compiler type checking deactivated — as relaxed or permissive. On the other hand, once these options are activated, you can count on the compiler to perform all the necessary verifications and to compile the code only after all variables and types are declared and all type conversions performed explicitly. Needless to say, this is the preferred way to write code. If you are writing strongly and statically typed code, it only makes sense to count on the tool to provide you with compile-time type checking.
Setting Option Explicit On in Relaxed Code The purpose of the Option Explicit statement is to tell the compiler to enforce variable declaration. After this option is set, if any variable is encountered that has not been previously declared with a Dim, Private, Public, or ReDim statement, a compilation error will be reported, the code will not compile, and the offending identifier will be underlined in the VB editor. Note that even if you do not place Option Explicit On, you can still declare variables. The difference in this case is that you do not get the compiler to enforce the declaration of each and every variable.
111
79796c05.qxd:WroxPro
2/23/08
8:07 AM
Page 112
Part II: Preliminary VB Refactorings Now I want to discuss why omitting to declare variables can be considered bad style, how you can convert code with omitted variables into explicit code, and the you can apply this method to the Rent-aWheels application.
Understanding the Set Option Explicit On Refactoring At this point you might rightfully ask, “Why declare variables at all?” Because you have seen that you can write code without declaring variables, there must be some good reason to do so. Otherwise, you are only writing some redundant, unnecessary code. A number of reasons exist for declaring variable type, but the most obvious one is that a number of bugs, a lot of times just simple typos, can be discovered at compile time. Imagine you have the following code snippet: printers = GetPrinters() printersTested = 0 For Each printer In printers printer.PrintTestPage() printersTeted += 1 Next MsgBox(printersTested & “ printers tested.”, _ MsgBoxStyle.Information, “Printer test status”)
Take a good look at this code. Do you see any problem with it? It is fairly easy to understand what this code is doing. However, what is not so easy is to see that it contains a simple typing error that can cause you to spend a lot of time with a debugger in search of this very nasty type of insect. Take look at this code again. This time, the offending line is shown in bold so you don’t have to spend any more time in futile hunt for this evasive enemy of every bug-free, code-loving programmer. printers = GetPrinters() printersTested = 0 For Each printer In printers printer.PrintTestPage() ‘Offending line printersTeted += 1 Next MsgBox(printersTested & “ printers tested.”, _ MsgBoxStyle.Information, “Printer test status”)
If you do not enforce explicit variable declaration, the compiler creates a new variable each time a corresponding new identifier is encountered in the code. With the sample I have just shown, a compiler has no way to guess that I actually meant to use the printersTested variable and not to create a new printersTeted variable. So it goes on quietly minding its own business and creates a new variable. And I end up with a very undesirable bug in my code.
112
79796c05.qxd:WroxPro
2/23/08
8:07 AM
Page 113
Chapter 5: Chameleon Language Smell: Implicit Variable Declaration Detecting the Smell Use the compiler to detect variables that are declared implicitly. Place Option Explicit On at the top of the file, compile the project, and look for a “Name someVariable is not declared” error in the Error window. The IDE will also underline the identifier in the Editor window if an undeclared variable is present.
Related Refactoring Use Set Option Explicit On refactoring to eliminate this smell.
Rationale Failing to declare variables explicitly can lead to difficult-to-detect bugs resulting from simple typing mistakes. It can also lead to a convoluted use of variables. Code readability can benefit from explicit variable declaration.
So how does Option Explicit On help? Once you include the statement in your code, the offending line is underlined as an error in the editor, and in the error list an appropriate error description is displayed: “Name printersTeted is not declared.” Very clear, precise, and very helpful. You can then correct the error right away. Finally, this is how the snippet should look once I use Option Explicit On and correct the bug. Option Explicit On ‘... Dim printers = GetPrinters() Dim printersTested = 0 For Each printer In printers printer.PrintTestPage() printersTested += 1 Next MsgBox(printersTested & “ printers tested.”, _ MsgBoxStyle.Information, “Printer test status”)
Once you have declared all the variables, you should proceed by assigning the corresponding type to the variable. More about this in the next section.
Refactoring: Set Option Explicit On (Enforce Variable Declaration) Motivation Explicit variable declaration can prevent number of typo-related bugs early on at compile time. It can improve code readability by stressing variable scope.
Related Smells Use this refactoring to eliminate the Implicit Variable Declaration smell.
Continued
113
79796c05.qxd:WroxPro
2/23/08
8:07 AM
Page 114
Part II: Preliminary VB Refactorings Mechanics The Option Explicit statement is module-level only. Option statements have to be the first statements in the file. When transforming a project written in a relaxed variable-declaration style, start by placing the Option Explicit On statement in a single file. (If you set this option on at the project level, you are bound to receive a significant number of error messages, and increased clutter might slow you down.) After you declare all the variables in one file and no errors are reported by the IDE, move to the next file. Complete this refactoring on the whole project before starting. Set Option Strict On refactoring. After you have turned Option Explicit on, the IDE will underline all undeclared variables and report errors (“Name VariableX is not declared”) in the error list. Work to eliminate errors one by one by declaring undeclared variables using Dim, Private, Public, or ReDim.
Before Public Function BasketTotal() For Each item In Basket total += item.Price Next Return total End Function
After ‘Option Explicit On statement added Option Explicit On ‘... Public Function BasketTotal() ‘Variable declaration added Dim total Dim item For Each item In Basket total += item.Price Next Return total End Function
Refactoring the Rent-a-Wheels Code to Explicit Form Performing this refactoring on the Rent-a-Wheels application proves to be rather simple. After you place Option Explicit On in a single file, Visual Studio IDE is very helpful in identifying undeclared variables. It seems that the application was written in an explicit style, but because the style was not enforced by the compiler a few implicit declarations need to be taken care of. Take a look at one sample function and the code that results once the refactoring is performed. Listing 5-1 shows the code for the BtnDelete_Click method. Code that changed after the refactoring is shown in bold.
114
79796c05.qxd:WroxPro
2/23/08
8:07 AM
Page 115
Chapter 5: Chameleon Language Listing 5-1: The Rent-a-Wheels FrmBranch Class BtnDelete_Click Method After the Option Explicit is Set to On ByVal e As System.EventArgs) Handles BtnDelete.Click ‘Declare variables Dim oCn Dim oCmd On Error GoTo ErrorHandler ‘Create SqlConnection oCn = New SqlConnection(“Data Source=TESLA-DAN;” + _ “Initial Catalog=RENTAWHEELS;User ID=sa”) oCmd = New SqlCommand ‘add parameter name Dim strSql = “Delete Branch “ + _ “Where BranchId = @Id” oCmd.Parameters.AddWithValue(“@Id”, TxtId.Text) ‘open connection oCn.Open() ‘Set connection to command oCmd.Connection = oCn ‘set Sql string to command object oCmd.CommandText = strSql ‘exexute command oCmd.ExecuteNonQuery() ‘close connection oCn.Close() ‘destroy objects oCmd = Nothing oCn = Nothing FrmBranch_Load(Nothing, Nothing) Exit Sub ErrorHandler: MsgBox(“A problem occurred and the application can not recover! “ + _ “Please contact the technical support.”) Err.Clear() End Sub
A single implicit declaration was resolved by the addition of a Dim statement at the beginning of the line. However, the variable type is yet not present. Now take a look at a second, albeit more complicated, compiler option and the refactoring work you need to perform in order to activate this option and successfully compile the code, written in a permissive style.
Setting Option Strict On in Relaxed Code Setting Option Strict On will affect your code in a number of ways. It will make you consider some aspects of programming you might have taken for granted. You will gain more control over the behavior of your program, but you will also have to put a little more work into the coding. By doing so, you will address some potential pitfalls that can cause some very subtle bugs in your code. You will put the compiler to work
115
79796c05.qxd:WroxPro
2/23/08
8:07 AM
Page 116
Part II: Preliminary VB Refactorings to disallow certain programming styles and constructs. Therefore, a lot of potentially hazardous situations will have to be dealt with before the code can compile. When you impose strong typing as a way to write code, Visual Basic as a programming language becomes in this sense very similar to other strongly typed languages like C# and Java. The following are some of the benefits of activating this option: ❑
Visual Studio IntelliSense and Dynamic Help are activated. Visual Studio is not capable of providing these two very productive features unless variable type is explicitly declared.
❑
Code readability is improved by variable type information. This can help you think about your code in the form of known abstractions and entities.
❑
Unwanted type coercion leading to precision loss, rounding errors, and possible overflow errors is dealt with early. All implicit narrowing conversions are disallowed.
❑
Some performance gains result from function-call resolution being limited to compile time.
So how do you go about activating this option? While setting Option Explicit On was relatively easy, because we could count on the compiler to find all variables not declared explicitly and then just add the missing declaration statement, deciding on type is not that straightforward. You start out by using the compiler to discover all variables that are declared without the type. Once you place the Option Strict On statement at the beginning of the file, an error window will display the following message for every variable for which type information is missing: “Option Strict On requires all variable declarations to have an ‘As’ clause.”
A Slightly Artificial Example of Permissive VB code Before you go on with the task of converting the code into its strict form, you need to take a look at possible behaviors of Visual Basic code when dealing with code written with Option Strict set to Off. You need to have a good understanding of such code in order to be able to convert it to strict form without altering the code’s behavior. You’ll see the convoluted use of variables, the implicit type conversions, and the problems that can crop up as a result of such code. Unless you understand well what is going on in the VB code written in relaxed form, you can unwittingly introduce bugs and errors into the code. For an illustration of the behavior of permissive code, take a look at the code snippet in Listing 5-2.
Listing 5-2: An Example of Code with Option Strict Set to Off Option Strict Off Module Module1 Sub Main() ‘ Initial type Integer (Int32) Dim variableX = 10 Console.WriteLine(“Type: “ & _ variableX.GetType.Name.ToString) ‘Current type Long (Int64) variableX = 100000000000000000 Console.WriteLine(“Type: “ & _
116
79796c05.qxd:WroxPro
2/23/08
8:07 AM
Page 117
Chapter 5: Chameleon Language Listing 5-2: An Example of Code with Option Strict Set to Off (continued) variableX.GetType.Name.ToString) ‘Current type Double variableX = 11.11 Console.WriteLine(“Type: “ & _ variableX.GetType.Name.ToString) ‘Current type String variableX = “” & variableX Console.WriteLine(“Type: “ & _ variableX.GetType.Name.ToString) ‘Late bound call Console.WriteLine(variableX.Length) Console.ReadLine() End Sub End Module
Note the Option Strict Off statement at the beginning of the listing. Now, this code will demonstrate a radically different behavior of VB runtime under relaxed mode, when Option Strict and Explicit are set to Off. After you run this code, the following output appears in the console window: Type: Type: Type: Type: 5
Int32 Int64 Double String
Dynamically Changing the Underlying Variable Type What you can see here is that the underlying variable type has been changing dynamically. Or, to be more precise, the object at which variableX points has been changed. First, the variable is Integer, after assigning it a literal value of 10. After the addition of a huge number, the type is changed to Long, in order to accommodate the result of this addition. Next, after the addition of the decimal value 11.11, the type is again changed in order to preserve the decimal part of the number. You can see that VB runtime is performing the necessary operations and accommodating its behavior so it best suits the code. Finally, after the concatenation of an empty string with variableX, the variable type is changed for the last time, to String. “Under the hood,” each time a variable type is changed, a new object is created and the old one left unreachable, waiting for garbage collection.
Late Bound Calls The last line in the console output has the value 5, representing the string length. The length of the string is 5 because the Double is represented in its exponential form, as 1E+17. What is significant about this property call is the fact that you have not defined the variable as String. This is not something that the compiler can know at compile time. However, it assumes during execution that variableX will have the property Length. That’s why this call is late bound, which means that if the object referenced by variableX has some member called Length, the call will succeed, and if the object does not have such a member, a runtime error will occur. Since variableX is pointing to the String object once you reach the line where the Length property is called (and String, of course, has a property named Length), the code executes successfully.
117
79796c05.qxd:WroxPro
2/23/08
8:07 AM
Page 118
Part II: Preliminary VB Refactorings This is possible because a lot of work that is normally done at compile time is delayed until the program is run and performed at runtime. What we are seeing in this sample is essentially a dynamically typed property of Visual Basic. However, once Option Strict is set to On, Visual Basic behaves just like any other statically typed language, and calling the Length property on variableX without declaring it as a String is no longer possible. I am now going to set Option Strict to On and perform all the necessary modifications to the sample code so that it can compile and execute properly. While this example is very simple, it serves to illustrate situations you might encounter in real production code.
Smell: Implicit Type Declaration Detecting the Smell After resolving the Implicit Variable Declaration, use the compiler to detect variables and members that are declared without their types being specified. Place Option Strict On at the top of the file, compile the project, and look for the following errors in the Error window. ❑
“Option Strict On requires all variable declarations to have an ‘As’ clause.”
❑
“Option Strict On requires all Function, Property, and Operator declarations to have an ‘As’ clause.”
❑
“Option Strict On prohibits operands of type Object for operator ‘-‘ [or other operator].”
❑
“Option Strict On disallows late binding.”
The IDE will also underline the identifier(s) in the Editor window corresponding to the preceding error.
Related Refactoring Use Set Option Strict On refactoring to eliminate this smell.
Rationale Failing to declare variable type explicitly means that you will not be able to exploit some very productive Visual Studio features, namely IntelliSense and Dynamic Help. Code in which variable and member type are declared explicitly outperforms code in which type is resolved at runtime. Code readability benefits from explicit type declaration. Ultimately, variable and member type being declared explicitly facilitates structuring and organizing your code by the means of standard object-oriented constructs like classes, interfaces, inheritance, and so on.
First Attempt at Inferring a Variable’s Initial Type The first error that the compiler reports once Option Strict is set to On is telling you that you need to assign a type to variableX. Here is the message: “Option Strict On requires all variable declarations
118
79796c05.qxd:WroxPro
2/23/08
8:07 AM
Page 119
Chapter 5: Chameleon Language to have an ‘As’ clause.” In this case, based on first literal value in the code, you can declare variableX as Integer. The declaration line now looks like this: Dim variableX As Integer = 10
After this is done, you receive the following error: “Option Strict On disallows implicit conversions from ‘Long’ to ‘Integer.’” It marks the following line of code: variableX = 100000000000000000
This huge literal value is of type Long, and the compiler refuses to assign it to variableX of type Integer because Integer is not capable of keeping such a huge value. At this point the IDE offers to resolve this problem by enclosing the number inside the CInt function. However, you cannot just squeeze this number into the Integer. So either you go back and declare variableX as Long, or you introduce a new variable. Declaring variableX as Long will work in this case. However, in some other, albeit rare, cases, in which reflection is used and execution logic depends on underlying variable type, this might introduce a bug. I am referring to cases in which code similar to the following line is present: If TypeOf variableX Is Integer Then
So, in this case, I decide to play it completely safe. The next section shows how.
Convoluted Use of Variables Resolved by the Definition of New Variables In cases where the variable has been overused and can represent different things depending on the context, a new variable should be introduced to compile the code in a strictly typed environment. By going back to the example I will show you how this can work out in practice.
Introducing a New Variable for a New Type: the Long Instead of changing the variableX’s type to Long, I will declare a new variable. As you will see later on in the book, the fact that a variable’s type is changed can often be a sign that the variable is being overused and is probably the result of diverse roles being given to that variable. This can be very confusing for the code reader. Misuse of a local variable can go hand in hand with the Long Method smell. You have already seen an example of the Long Method smell in Chapter 2, and it is described in detail in Chapter 9. I will use Refactor!’s Split Temporary Variable refactoring to perform this transformation. I will name our new variable variableXLong. Take a look at the Visual Studio IDE and how you can activate this option of the Refactor! add-in. Using the Split Temporary Variable refactoring by the Refactor! add-in to introduce a new variable for a new type is shown in Figure 5-1. Here I have used Split Temporary Variable refactoring in the context of converting the permissive code to strict code. Split Temporary Variable refactoring is also refactoring in its own right, and I’ll deal with it later on in Chapter 10.
119
79796c05.qxd:WroxPro
2/23/08
8:07 AM
Page 120
Part II: Preliminary VB Refactorings
Figure 5-1
The line I have changed now looks like this: Dim variableXLong As Long = 100000000000000000
Also, in all subsequent lines, Refactor! has replaced variableX with variableXLong. I have moved another step forward in introducing Option Strict.
Dealing with the Double Next on the list to deal with is the following line: variableXLong = 11.11
Here the following error is reported: “Option Strict On disallows implicit conversions from ‘Double’ to ‘Long.’” Again, IDE is offering the following solution: “Replace 11.11 with CLng(11.11).” While this solution eliminates the compilation error, it introduces a bug whereby the decimal part of the number is simply lost. I am trying here to preserve the original behavior of the code, so once again I do not trust the IDE. Yet again, I’ll introduce another new variable. The name of the variable this time is variableXDouble and its type is Double. Refactor! replaces all instances of variableXLong with variableXDouble for the rest of the code in the method. That’s one error more eliminated, and one more to go.
120
79796c05.qxd:WroxPro
2/23/08
8:07 AM
Page 121
Chapter 5: Chameleon Language Dealing with the String Now I can deal with the remaining error. At the following line: variableXDouble = “” & variableXDouble
I am receiving the following error: “Option Strict On disallows implicit conversions from ‘String’ to ‘Double.” The solution is the same: I’ll introduce another variable called variableXStr. However, this time the transformation has to be performed manually, because I have reached the last assignment to variableXDouble. The offending line now looks like this: Dim variableXStr As String = “” & variableXDouble
As another consequence, the error has disappeared from the following line: Console.WriteLine(variableXStr.Length) variableXStr is declared as a String, so the compiler can check that this type contains the property Length.
Listing 5-3 shows the complete sample so you can see the final result.
Listing 5-3: Sample Code After it Has Been Modified So That Option Strict is Activated and the Code Converted to Strict Form Option Strict On Module Module1 Sub Main() Dim variableX As Integer = 10 Console.WriteLine(“Type: “ & _ variableX.GetType.Name.ToString) Dim variableXLong As Long = 100000000000000000 Console.WriteLine(“Type: “ & _ variableXLong.GetType.Name.ToString) Dim variableXDouble As Double = 11.11 Console.WriteLine(“Type: “ & _ variableXDouble.GetType.Name.ToString) Dim variableXStr As String = “” & variableXDouble Console.WriteLine(“Type: “ & _ variableXStr.GetType.Name.ToString) Console.WriteLine(variableXStr.Length) Console.ReadLine() End Sub End Module
121
79796c05.qxd:WroxPro
2/23/08
8:07 AM
Page 122
Part II: Preliminary VB Refactorings Finally, after we execute the code, the resulting Console output is identical to the output from the beginning: Type: Type: Type: Type: 5
Int32 Int64 Double String
In converting this code to statically typed, I used a very simple methodology. I started assigning types to variables based on the initial type and literal values. Once this type was changed, I introduced a new variable to hold this new value. Now I want to look at the other ways you can discover the initial type for the variable.
Inferring Variable Type When you are confronted with the permissive code, the underlying variable type is not always obvious. You saw an attempt to infer variable type in the sample code in the previous section. Now I want to investigate different techniques you can apply in order to infer the type of the variables used in relaxed code.
Using Literals to Infer Variable Type First on the list are variables that have literal values assigned to them. The types of these variables are relatively easy to decipher; take a look at Table 5-2 for some samples. I have marked with bold the typical choice that will work on most occasions. You should make sure to take the type range into account when deciding on the right type.
Table 5-2: Different Forms of Literals Usage and Type Inference
122
Declaration without the Type
Complete Variable Declaration
Dim name = “John”
Dim name As String = “John”
Dim year = 1997
Dim year As Byte = 1997 Dim year As Short = 1997 Dim year As Int16 = 1997 Dim year As UInt16 = 1997 Dim year As Integer = 1997 Dim year As Int32 = 1997 Dim year As UInt32 = 1997 Dim year As Long = 1997 Dim year As Int64 = 1997 Dim year As UInt64 = 1997
Dim price = 185.56
Dim price As Single = 185.56 Dim price As Double = 185.56
Dim price = 185.56D
Dim price As Decimal = 185.56D
Dim initial = “G”c
Dim initial As Char = “G”c
Dim success = True
Dim success As Boolean = True
79796c05.qxd:WroxPro
2/23/08
8:07 AM
Page 123
Chapter 5: Chameleon Language You may notice that in some cases more than one applicable type is available. How do you decide which one to use? The solution is to take type range into account. Generally, you are a lot less likely to go wrong if you choose a type with a wider range. For example, if you try to declare the year as Byte, you will receive a compiler error stating “Constant expression not representable in type Byte.” This is because the Byte type can only represent values between 0 and 255. While you might choose to represent the variable year with the Single type, because it is not likely that it will come out of range when representing the year, you can’t always make such assumptions. Another danger involved in making assumptions like this, as I already mentioned, is winding up with code that uses type information as part of execution logic. If you are in doubt about the range of different numeric types in VB, this is a good moment to consult MSDN. You can take a look at the Visual Basic Data Type Summary at the following URL: http://msdn2.microsoft.com/en-us/library/47zceaw7(VS.90).aspx.
In case a variable range is not sufficient to contain a certain value, an Overflow exception is thrown at runtime. If this error is not dealt with, the application crashes. When Option Strict is set to Off and a variable is initialized with a whole-number literal, the type used is Int32. This, of course, is only if the number in question can be contained in Int32. For a very big literal value, out of the range of Int32, an Int64 type is used. Unless you are working on some very memory-intensive application, there is no need to reduce the range and declare such variables as Short or Int16. Next, I want to examine another very simple way to infer initial variable type.
Using the Conversion Function to Infer Initial Variable Type Visual Basic has a number of predefined conversion functions. These functions accept a single value and return a value converted to another type. Table 5-3 examines those functions and the types they return.
Table 5-3: Types Returned by Conversion Functions Function
Function Return Type
CBool
Boolean
CByte
Byte
CChar
Char
CDate
Date
CDbl
Double
CDec
Decimal
CInt
Integer
CLng
Long
Continued
123
79796c05.qxd:WroxPro
2/23/08
8:07 AM
Page 124
Part II: Preliminary VB Refactorings Table 5-3: Types Returned by Conversion Functions (continued) Function
Function Return Type
CObj
Object
CSByte
SByte
CShort
Short
CSng
Single
CStr
String
CUInt
UInteger
CULng
ULong
CUShort
UShort
This is really quite straightforward. Take a look at the following line: Dim variableX = CLng(variableY)
According to the table, this line is transformed into the following once Option Strict is set to On: Dim variableX As Long = CLng(variableY)
Another function, similar to one you have just seen in action, is CType. This function accepts two parameters — the first is the variable, and the second is the targeted type. This makes the return type quite obvious: it is stated as a second parameter in the function call. The code: Dim variableX variableX = CType(variableY, SqlConnection)
is modified into this: Dim variableX as SqlConnection variableX = CType(variableY, SqlConnection)
For one final way to infer variable type, I want to take a look at external assemblies and the way those assemblies are consumed.
Using External Assemblies to Infer Initial Variable Type When you use external assemblies, you create objects and receive objects as the return values of methods and properties. Thanks to .NET’s strongly typed nature, these assemblies are strongly typed. Therefore, the type is easily inferred. Consider this example: Dim Conn = New SqlConnection Dim Cmd = Conn.CreateCommand()
124
79796c05.qxd:WroxPro
2/23/08
8:07 AM
Page 125
Chapter 5: Chameleon Language Moving to the strict mode modifies this code as follows: Dim Conn As SqlConnection = New SqlConnection Dim Cmd As SqlCommand = Conn.CreateCommand()
However, in most cases you should strive not to depend on specific implementation, but on more generic constructs. Often interfaces are used to separate abstraction from implementation. In this specific case SqlConnection and SqlCommand are types belonging to a specific implementation of the ADO .NET data provider for MS SQL Server. If your code is not using any other specific members belonging to SqlConnection or SqlCommand, and all invoked members are also defined in interfaces that these two classes implement, you are better off declaring your variables by using the corresponding interfaces as a type. The code will then look like this: Dim Conn As IDbConnection = New SqlConnection Dim Cmd As IDbCommand = Conn.CreateCommand()
Now, in case you need to replace the data provider because you are using a different data store, you need to modify only the single line where the connection is created. The rest of the code is guaranteed to work with any ADO .NET data provider.
Smell: Implicit Narrowing Conversions Detecting the Smell Use the compiler to detect implicit narrowing conversions. After resolving the Implicit Type Declaration smell, place Option Strict On at the top of the file, compile the project, and look for the “Option Strict On disallows implicit conversions from ‘typeX‘ to ‘typeY‘” error in the Error window. The IDE will also underline the identifier in the Editor window if an undeclared variable is present.
Related Refactoring Use Set Option Strict On refactoring to eliminate this smell.
Rationale If narrowing conversions are allowed and performed quietly, a number of bugs can arise as a result of narrowing and rounding errors. For example, the following lines will compile under Option Strict Off: Dim varX As Double = 1234.99 Dim varY As Long = varX Console.WriteLine(varY)
However, the output will show 1235 because of rounding during the transformation of the number into an integer.
Putting It All Together with Type-Conversion Functions After you have done your best to infer all variable types, you might still end up with some problems stemming from implicit type conversion. After Option Strict is set to On, only widening conversions are allowed in implicit form, and any narrowing conversion has to be explicitly written in code.
125
79796c05.qxd:WroxPro
2/23/08
8:07 AM
Page 126
Part II: Preliminary VB Refactorings You have already seen predefined conversion functions and how they can be used to infer initial variable type. Now they can come in handy to perform explicit conversion. I want to illustrate that with some code. The following snippet compiles with Option Strict set to Off: Option Strict Off ‘... Dim varX = 123 Dim varY = “456” Dim varZ = 123456.789 varX = varZ Dim result = varX + varY Dim varLong As Long = varX
However, after you set Option Strict to On and declare all variable types, you still receive a few errors: Option Strict On ‘... Dim varX As Integer = 123 Dim varY As String = “456” Dim varZ As Double = 123456.789 ‘Implicit conversion from Double to Integer not allowed varX = varZ ‘Implicit conversion from String to Integer not allowed Dim result As Integer = varX + varY ‘Widening conversion from Integer to Long is allowed Dim varLong As Long = varX
In order to resolve these errors, you use conversion functions to perform conversions explicitly. In this case, for the sake of argument, consider that you are allowed to ignore the decimal part of varZ. You can use CInt to convert both varZ and varY to Integer. The final, compilable version looks like this: Dim varX As Integer = 123 Dim varY As String = “456” Dim varZ As Double = 123456.789 varX = CInt(varZ) ‘CInt conversion function at work Dim result As Integer = varX + CInt(varY) Dim varLong As Long = varX
Explicit Variable-Type Conversion with CType I have already mentioned the CType function, which can perform all the conversions that other built-in conversion functions like CStr and CInt can perform. It actually makes little difference whether you use CInt(varX) or CType(varX, Integer). However, CType can actually do a bit more. It enables you to convert between any two types of objects, if such a conversion is defined by means of a conversion operator. This is accomplished through the operator-overloading feature of Visual Basic. The typical use of CType in strictly typed code is to retrieve an instance from a nongeneric collection. (Standard, nongeneric collections are found in the System.Collections namespace.) Because a collection always returns an object, you can
126
79796c05.qxd:WroxPro
2/23/08
8:07 AM
Page 127
Chapter 5: Chameleon Language use CType to cast an instance into the correct type. For example, in the following snippet I cast an object obtained from ArrayList into the Client type: Dim clients As System.Collections.ArrayList ‘... Dim client As Client = CType(clients.Item(id), Client)
Explicit Variable-Type Conversion with DirectCast and TryCast Two other interesting functions from this group of type-conversion functions are DirectCast and TryCast. They perform identically, except upon failure at runtime. In case of a conversion error, DirectCast throws InvalidCastException while TryCast will return nothing. Basically, by using DirectCast or TryCast you can accomplish a downcast. A downcast is a conversion based on inheritance or interface implementation. Remember the classes Patient, MalePatient, and FemalePatient from Chapter 2? Both MalePatient and FemalePatient inherited Patient. Again, take a look at the code sample: Dim patient As Patient = GetMalePatient() Dim malePatient As MalePatient = patient
This code provokes a compiler to report an error when Option Strict is turned on. However, if you are certain that the patient variable points to an instance of MalePatient, you can write the code without inciting a compiler error, like this: Dim patient As Patient = GetMalePatient() Dim malePatient As MalePatient = DirectCast(patient, MalePatient)
For this code to work, MalePatient must inherit the Patient class, or implement the Patient interface — if Patient was defined as an interface. It is worth mentioning that the upcasting is performed implicitly, so the following code will work without any need for DirectCast: Dim malePatient As MalePatient = GetMalePatient() Dim patient As Patient = patient
Now you have investigated issues of type related to local variables and, more importantly, become familiar with various ways to move your use of variable type from implicit to explicit to create stricter code. Of course, type doesn’t relate only to variables, so now as you move on in the chapter you will see how all this relates to other typed class elements.
Dealing with Methods, Fields, Properties, and Other Members So far, most of the samples have used local variables to illustrate type inference. While all the described techniques apply to class and instance members, the situation can get a bit more complicated when you’re dealing with properties, methods, and events. Visual Basic with Option Strict set to Off enables you to write methods without having to specify parameter types or the return type of the method. After using all the previously described techniques to decipher types in a method signature, you can still end up in a situation where the solution requires some radical changes to the way a method is written. To show some typical cases, I’ll start off with the sample code in Listing 5-4.
127
79796c05.qxd:WroxPro
2/23/08
8:07 AM
Page 128
Part II: Preliminary VB Refactorings Listing 5-4: A Method with Ambiguous Parameter Types Public Function NumberOfRecords(ByVal data, ByVal dataFormat) If dataFormat = “xmlNode” Then Return data.ChildNodes.Count ElseIf dataFormat = “dataTable” Then Return data.Rows.Count End If End Function
In this case the parameter data can be of the System.Xml.XmlNode or System.Data.DataTable data type, depending really on what the client code sends to the method. Another parameter, dataFormat, indicates the type of parameter, so based on that additional information, the correct branch of code is executed and desired result obtained.
Overloading Methods to Resolve an Ambiguous Parameter-Type Situation You can start out by inferring the return type of the method. In order to do that, you inspect the signature of the Count property of the ChildNodes property of XmlNode. Next, you inspect the Count property of the Rows property of DataTable. You can do this easily with the Object Browser. In both cases the type is the same — Integer. Now you need to decide on dataFormat’s type. It’s easy to do this based on comparisons of the dataFormat parameter with string literals. So far so good — the method’s signature should look like this: Public Function NumberOfRecords(ByVal data, _ ByVal dataFormat As String) As Integer
All that is left to do is to decipher the type of the first parameter. As I already said, this parameter can be either XmlNode or DataTable. To determine which, you can divide this method into two new overloaded methods. One contains code applicable to XmlNode, and the other code applicable to DataTable. (The process of splitting the method into two is explained in detail when we look at Extract Method refactoring in Chapter 9.) After you do that, you end up with the code shown in Listing 5-5.
Listing 5-5: A Method with Ambiguous Parameter Types Split into Two Overloaded Methods Public Function NumberOfRecords(ByVal data As DataTable, _ ByVal dataFormat As String) As Integer Return data.Rows.Count End Function Public Function NumberOfRecords(ByVal data As XmlNode, _ ByVal dataFormat As String) As Integer Return data.ChildNodes.Count End Function
This way, once the client code is strongly typed, the compiler will use information on the data parameter type to dispatch the call to the correct overloaded version of the method. Going one step further, you can now notice that the dataFormat parameter has no use for us anymore. If you have access to all client code you can simply eliminate this parameter. If you do not have access to all client code you can maintain methods with the dataFormat parameter, but you can also mark them with the Obsolete attribute and redirect the call to the overloaded versions of the method without the dataFormat parameter. The resulting code is shown in Listing 5-6.
128
79796c05.qxd:WroxPro
2/23/08
8:07 AM
Page 129
Chapter 5: Chameleon Language Listing 5-6: Overloaded and Obsolete Methods as Solution to Ambiguous Parameter Types Method _ Public Function NumberOfRecords(ByVal data As DataTable, _ ByVal dataFormat As String) As Integer Return NumberOfRecords(data) End Function _ Public Function NumberOfRecords(ByVal data As XmlNode, _ ByVal dataFormat As String) As Integer Return NumberOfRecords(data) End Function
Public Function NumberOfRecords(ByVal data As DataTable) As Integer Return data.Rows.Count End Function Public Function NumberOfRecords(ByVal data As XmlNode) As Integer Return data.ChildNodes.Count End Function
This takes care of resolving a situation in which parameter type is ambiguous, but the method return type can be ambiguous also. The next section shows how to act in that case.
Introducing New Methods to Resolve an Ambiguous Return Type Visual Basic will not let you overload methods based solely on the return type. In cases where a method returns different types, you cannot use method overloading as a solution. Check out the following method, which has an ambiguous return type: Public Function GetData(ByVal store) If store = “Xml” Then Dim node As XmlNode node = GetNode() Return XmlNode ElseIf store = “ADO” Then Dim table As DataTable table = GetTable() Return table End If End Function
The solution in this case is again to first split this method into two. One method will return XmlNode and the other DataTable. You can take a look at the related Extract Method refactoring in Chapter 9 in order to understand the mechanics in detail, but in this case each method has to have a different name. Also, once you split GetData method into two, the store parameter becomes redundant. You end up with code looking like this: Public Function GetDataAsXmlNode() As XmlNode Dim node As XmlNode node = GetNode()
129
79796c05.qxd:WroxPro
2/23/08
8:07 AM
Page 130
Part II: Preliminary VB Refactorings Return node End Function Public Function GetDataAsDataTable() As DataTable Dim table As DataTable table = GetTable() Return table End Function
If you have access to all client code, you can replace all calls to GetData with calls to GetDataAsXmlNode or GetDataAsDataTable, depending on the type that the client code expects. With that, you have resolved the issue. In case you do not have access to all client code, you can keep the original GetData method. You modify the method so it delegates the call to the new methods GetDataAsXmlNode and GetDataAsDataTable. The last issue is deciding on the return type of this method. Because it is now in a statically typed environment, you need to add an As clause to the GetData method declaration. The problem is that in one case it returns XmlNode and in another DataTable. So you can opt for the common base type of XmlNode and DataTable. Because in .NET everything derives from System.Object, you can make this method return Object. And you can infer that the store parameter type is String. The final version of the GetData method is shown in Listing 5-7.
Listing 5-7: A Method with an Ambiguous Return Type is Marked with the Obsolete Attribute and Returns an Object for Compatibility Purposes _ Public Function GetData(ByVal store As String) As Object If store = “Xml” Then Return GetDataAsXmlNode() ElseIf store = “ADO” Then Return GetDataAsDataTable() End If End Function
This way you have resolved a situation in which a method can dynamically change its return type. A similar approach to the one we just used in dealing with methods can be applied to properties and events. With properties, overloading can be applied only in cases of parameterized properties. However, parameterized properties are a lot less common, so if a property is ambiguous in relation to its type, you generally end up splitting properties into two. Of course, this section does not cover all possible situations you can come upon. Some situations will require more inventiveness. However, the rule of thumb is to start by resolving the simplest situations, those in which type can be inferred by literal assignment, other assembly, or conversion function. That way you scale down your problem and solution to more problematic situations that then become evident. In my experience, it seems that in most cases programmers do declare the variable type, and after Option Strict has been set to On, only a small number of undeclared type variables will crop up.
130
79796c05.qxd:WroxPro
2/23/08
8:07 AM
Page 131
Chapter 5: Chameleon Language Refactoring: Set Option Strict On (Enforce Variable Type Declaration and Explicit Type Conversions) Motivation Visual Studio IntelliSense and Dynamic Help can be activated only if the variable and member type are explicitly declared in the declaration by the means of an As clause. After Option Strict On is activated, the compiler prevents all implicit narrowing type conversions. This means that all narrowing and rounding has to be performed deliberately, by the programmer, by means of conversion functions. This means that a number of bugs resulting from rounding and narrowing errors can be avoided. By explicitly declaring types you improve code readability, because you can think in the form of known abstractions and types. Finally, declaring types explicitly leads to some performance gains as a result of early binding. Once Option Strict is set to On, Visual Basic behaves as a statically typed language, just like C# or Java, and similar styles and idioms can be applied.
Related Smells Use this refactoring to eliminate Implicit Type Declaration and Implicit Narrowing Conversions smells.
Mechanics The Option Strict statement is module-level only. Option statements have to be the first statements in the file. Set Option Explicit On refactoring should precede this refactoring. Start by inferring and declaring types explicitly for local variables. First deal with the simplest situations, those in which type can be inferred by: ❑
Literal value assignments
❑
Using types from other assemblies
❑
Using conversion functions to decipher variable type
Introduce new local variables to resolve situations in which variable type is ambiguous, where one variable’s underlying type is changed during the variable lifetime. After resolving local variable type, declare class and instance member types. Since type information for the local variable is now available, you have more information to go by in order to infer typed member declaration. Overload methods in order to resolve ambiguous method-parameter situations. Split methods into two in order to accommodate situations involving an ambiguous method return type. Use the Obsolete attribute to mark method versions for which the Object type is used, in order to maintain client compatibility. The Obsolete method should only delegate calls to strongly typed method versions. Finally, use a conversion function to explicitly convert between types. Explicit conversion is required for narrowing conversions only.
Continued
131
79796c05.qxd:WroxPro
2/23/08
8:07 AM
Page 132
Part II: Preliminary VB Refactorings Before Option Explicit On ‘... Public Function BasketTotal() Dim total Dim item For Each item In Basket total += item.Price Next BasketLog.WriteEntry(total, _ EventLogEntryType.Information, myeventid) Return total End Function
After Option Explicit On ‘Option Strict On statement added Option Strict On ‘ ... ‘Declare method return type Public Function BasketTotal() As Double ‘Declare local variable type Dim total As Double Dim item As Product For Each item In Basket total += item.Price Next ‘Explicit conversion through CStr conversion function BasketLog.WriteEntry(CStr(total), _ EventLogEntryType.Information, myeventid) Return total End Function
Make Explicit Refactoring in Refactor! If you are using Visual Studio 2008, you will be able to use Refactor! to automate the refactoring I just described. Refactor! can infer type and add the As part of the declaration statement for any variable that has been initialized with the value. This refactoring can be applied to both local, method-level variables and fields. In order to activate it, place the cursor on the variable name in the variable declaration statement without the As clause. Take a look at Figure 5-2 to see this refactoring in action.
Applying Set Option Strict On Refactoring to the Rent-a-Wheels Application Now it’s time to see how this refactoring can apply to the Rent-a-Wheels application. I’ll stick with BtnDelete_Click of the FrmBranch class, the method I used as an example earlier in the chapter (Listing 5-1, when Set Option Explicit On refactoring was introduced). You can turn back in order to refresh your memory about this method and the form it took after Option Explicit was introduced.
132
79796c05.qxd:WroxPro
2/23/08
8:07 AM
Page 133
Chapter 5: Chameleon Language
Figure 5-2
While the code seemed to promise an easy time when it came to activating Option Strict, a surprise was waiting for me. Because I wanted to keep the code as data-store-neutral as possible, I chose to declare oCn as IDbConnection from the System.Data namespace, hence limiting the dependency on the Microsoft SQL data provider to Connection and Command creation code. So far, so good. However, once I tried to declare oCmd as IDbCommand, the compiler reported the following error: “‘AddWithValue’ is not a member of ‘System.Data.IDataParameterCollection.’” The class that implements a certain interface has to implement all the members declared in that interface, but it is free to declare additional members. In this case, the AddWithValue method is a member of System.Data.SqlClient.SqlCommand, but it is not a member of System.Data.IDbCommand. It seems that Microsoft provided a convenient way to add command parameters, but it is exposed only if you work directly with the SqlCommand type. So I need another way to add parameters to a command object in a provider-neutral way. So the code is using some MS SQL provider-specific methods. While adding the parameter to a command this way takes a single statement, thanks to a convenient AddWithValue method, depending on a specific provider implementation this way can soon grow out of proportion and can prevent you from making your code data-agnostic. I decided to try to modify the code so that it uses IDbCommand methods only. This means adding a few lines and using the IDbCommand.CreateParamter and IDbCommand.Parameters.Add methods. Finally, I ended up with code listed in Listing 5-8.
Listing 5-8: The FrmBranch Class BtnDelete_Click Method from the Rent-a-Wheels Application, After Option Strict is Set to On Private Sub BtnDelete_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles BtnDelete.Click ‘oCn declared as IDbConnection
133
79796c05.qxd:WroxPro
2/23/08
8:07 AM
Page 134
Part II: Preliminary VB Refactorings Listing 5-8: The FrmBranch Class BtnDelete_Click Method from the Rent-a-Wheels Application, After Option Strict is Set to On (continued) Dim oCn As IDbConnection ‘ oCmd declared as IDbCommand Dim oCmd As IDbCommand On Error GoTo ErrorHandler ‘Create SqlConnection oCn = New SqlConnection(“Data Source=TESLA-DAN;” + _ “Initial Catalog=RENTAWHEELS;User ID=sa”) oCmd = New SqlCommand ‘strSql declared as String Dim strSql As String = “Delete Branch “ + _ “Where BranchId = @Id” ‘Adding command paramter in provider-neutral manner Dim Id As IDbDataParameter = oCmd.CreateParameter() Id.ParameterName = “@Id” Id.DbType = DbType.Int32 Id.Value = CInt(TxtId.Text) oCmd.Parameters.Add(Id) ‘open connection oCn.Open() ‘Set connection to command oCmd.Connection = oCn ‘set Sql string to command object oCmd.CommandText = strSql ‘execute command oCmd.ExecuteNonQuery() ‘close connection oCn.Close() ‘destroy objects oCmd = Nothing oCn = Nothing FrmBranch_Load(Nothing, Nothing) Exit Sub ErrorHandler: MsgBox(“A problem occurred and the application can not recover! “ + _ “Please contact the technical support.”) Err.Clear() End Sub
This code is actually longer than the previous version. While this is not a good development, you often need to weigh pros and cons. In this case I think it is more important to keep the application database neutral. Also, I have the feeling I’ll find a way to write this code more efficiently once I have the application well through the refactoring process. Now that you have looked into converting code written without type declarations and conversions into code for which types and conversions are declared explicitly, I want to take a look at some other interesting Visual Basic characteristics related to the problems of strong static code versus weak dynamic code. In next section I cover some situations where it can actually be beneficial to write code without declaring variable and member type.
134
79796c05.qxd:WroxPro
2/23/08
8:07 AM
Page 135
Chapter 5: Chameleon Language
Static Versus Dynamic Typing and Visual Basic In Visual Basic .NET you can achieve late binding by combining the use of the Option Strict Off statement with declaring variables as Object. Essentially, late binding means that variable type is not known at compile time. This results in type checking being performed at runtime, which is why late binding is also often referred to as dynamic typing. Because the type checking is limited to verifying that a certain class member is present in a late bound object, you can compile the code that will produce a type-related error at runtime. Take a look at the example in Listing 5-9.
Listing 5-9: In Dynamically Typed Code Certain Errors Can Be Discovered Only at Runtime ‘ Static type checking deactivated Option Strict Off
#1
Module Module1 Sub Main() Dim engine As Engine = New Engine Dim tree As Object = New Tree engine.Start() ‘Calling Start method on Tree instance tree.Start() End Sub End Module Public Class Engine Public Sub Start() ‘... End Sub End Class ‘Tree class does not have Start member Public Class Tree Public Sub Grow() ‘... End Sub End Class
You can compile this code successfully, but once you try to run it a MissingMemberException will be thrown. Because the Tree class does not have a Start method, the last line in Main is the source of an error. If you now activate the Strict option (replace Option Strict Off with Option Strict On), you get a compile-time error with the following message: “Option Strict On disallows late binding.” You are no longer able to compile such code. This can be a great help in preventing typos and similar errors. Just imagine that I have written engine.Stat() by mistake. The compiler is now helping me identify the error right away, during compilation: “Stat is not member of Engine.” And because IntelliSense is also present, there is only a slim chance I’ll lose too much time on this type of error. However, traditional Visual Basic programmers have a lot of experience in using late binding, and there is a reason for that. To get some historic perspective on late binding, you need to look at its role in Visual Basic 6.
135
79796c05.qxd:WroxPro
2/23/08
8:07 AM
Page 136
Part II: Preliminary VB Refactorings
Late Binding in Visual Basic 6 and Prior In the COM era (Visual Basic 6 and prior), late binding often presented a workaround for COM-versioning and binary-compatibility issues. When performing early binding in referencing COM components, Visual Basic would bind to a specific version of a component. In COM, a different set of GUIDs was used to identify a component. Each time a component interface was changed, meaning that a signature of any nonprivate member, a method, or a property was changed, a completely new GUID for that component was created. This version of the component had no relation to the previous version of that same component. This means that if an application was performing early binding, any other binary incompatible version of the component would provoke the infamous “Error 429 — ActiveX component can’t create object.” In the case of late binding, as long as a new version of a component had all the same methods that the previous version had, even when containing new non-private methods the application would continue to work. For this to be accomplished, a variable had to be declared as Object and created using the CreateObject function with ProgId as a parameter. That meant that Visual Basic would check for the existence of a method or a property only at runtime, during the execution. As long as the object supported the function or property in question, the code could execute without any problems.
Duck Typing This style of programming, in which you rely on type being checked by member signature comparison at runtime, is also known as duck typing. The name originates from the saying “If it walks like a duck and quacks like a duck, it must be a duck.” As long as the object has the member that corresponds to what the client code is expecting, everything works out fine. A number of modern languages, like Ruby and Python, and some older ones, like Smalltalk, permit this style of programming. Devotees of dynamically typed languages emphasize the flexibility and productivity that can be obtained from this approach and praise it over the benefits that static type-checking can bring. This style generally works well when paired with unit testing that can create a much deeper safety net than static typing can. In dynamically typed languages function calls are resolved at runtime, which permits treating objects in a polymorphic manner, but without any interface- or implementation-inheritance relationship between those objects. A similar style is possible in VB .NET, but can be turned off at will by means of the Option Strict On statement. This option did not exist in pre-.NET versions of Visual Basic. This means that in pre-.NET versions of VB, anything declared as Object is late bound. However, in VB .NET, once you place the Option Strict On statement at the top of your file, you have effectively turned off late binding in that file. Now take a look at a sample in which this style can come in handy. Consider the following snippet: Option Strict Off ‘... Public Sub CheckSpellingAndSave(ByVal officeObject As Object) officeObject.CheckSpelling() officeObject.SaveAs(FileName) End Sub
For this code to compile, you need to set Option Strict to Off. As long as the officeObject parameter was created as Excel.Worksheet or Word.Document, this code executes fine thanks to the fact that both of these objects have both the CheckSpelling and SaveAs methods.
136
79796c05.qxd:WroxPro
2/23/08
8:07 AM
Page 137
Chapter 5: Chameleon Language It is also possible to write this code with Option Strict on. However, as you’ll see, it is a lot more verbose — it requires you to write much more code to accomplish the same behavior, and it’s based on the reflective capabilities of Visual Basic. Again, take a look at the code: Option Strict On ‘... Public Sub CheckSpellingAndSave(ByVal officeObject As Object) ‘Object array of parameters is passed to Invoke method ‘of System.Reflection.MethodInfo class Dim saveAsParams As Object() = {FileName} ‘Invoking method without parameters using reflection officeObject.GetType.GetMethod(“CheckSpelling”). _ Invoke(officeObject, Nothing) ‘Invoking method with parameter using reflection officeObject.GetType.GetMethod(“SaveAs”). _ Invoke(officeObject, saveAsParams) End Sub
This code is based on reflection, and all that you can do with late binding in VB — and even more — you can also do with reflection. However, the quantity of code written will be very different. In this case, the first sample has only two lines, but the second, reflection-based version is significantly more verbose. Now imagine you have to write a lot of code similar to this. You will definitely wish to use the less prolix variant. Finally, without using reflection, the only solution in strict mode is to have two overloaded methods: Option Strict On ‘... Public Sub CheckSpellingAndSave(ByVal officeObject As Worksheet) officeObject.CheckSpelling() officeObject.SaveAs(FileName) End Sub Public Sub CheckSpellingAndSave(ByVal officeObject As Document) officeObject.CheckSpelling() officeObject.SaveAs(FileName) End Sub
The problem with this solution is that you have identical code in both methods. It means that the code is duplicated. As you will see in Chapter 9, duplicated code is the number-one smell and has to be eliminated. So obviously, this solution does not make me happy either. Now, in more a common situation you could resolve this problem by making both Document and Worksheet inherit the same base class or implement the same interface. As a matter of fact, Extract Interface is a standard refactoring technique that I will discuss in Chapter 12. It means no duplication and strict code at the same time. However, in this case you do not have a source for the Office automation libraries, so working the problem out this way is not possible. You cannot modify the Document or Worksheet class. You need to look for a different solution.
137
79796c05.qxd:WroxPro
2/23/08
8:07 AM
Page 138
Part II: Preliminary VB Refactorings
Resetting Dynamic or Static Behavior at the File Level As I already mentioned, once Option Strict is activated, VB behaves as a completely statically typed language, on par with C# or Java. However, as a unique feature of VB compared to other static or dynamic languages, VB lets you specify this option on the file level. This means that you can mix files written in static and in dynamic style in the same project. You have another level of flexibility when you write code. The VB team from Microsoft sees this as a distinctive advantage of VB over static-only or dynamic-only languages, and some further enhancements of VB as a dynamically typed language are planned for the next release of Visual Basic. Microsoft’s position on the issue is expressed in the following sentence by Erik Meijer and Peter Drayton: “Static typing where possible, dynamic typing when needed.” Erik Meijer’s and Peter Drayton’s full paper “Static Typing Where Possible, Dynamic Typing When Needed: The End of the Cold War Between Programming Languages” can be accessed at http://research.microsoft.com/~emeijer/Papers/RDL04Meijer.pdf. This leads to an interesting way of looking at this problem. Maybe there is some way to isolate dynamic from static code? This way, you could keep code strict in all but a minimal part of the application. But first, a few words about VB as a dynamic language. Some dynamic languages go further than VB with the dynamic paradigm. In these languages not only do you not need to know the exact type at compile time, but you are also able to modify types by adding properties and methods “on the fly” at runtime. Something similar is not possible in VB without the use of services provided by the Reflection.Emit namespace. So you can be pretty sure that all types you are going to encounter existed at the moment of compiling the program. Even if some types are loaded dynamically, they were most probably compiled and created as static types. Consequently, there is actually a physical assembly or dll file containing these types. It is highly probable that these types were created intentionally to represent a certain defined entity, along usual OO design and analysis guidelines. With all this in mind, the following conclusion can be reached: in VB, even when writing dynamic code, you are still interacting with static types underneath. Going back to the CheckSpellingAndSave code snippet, you have seen that this code interacted with two types: Word.Document and Excel.Worksheet from the Office automation library. Knowing all this, it makes sense to provide a statically typed wrapper over the dynamic code. The rest of the code will never know that for the moment static type checking was turned off. Client code will treat the wrapper just as it would any other static type and will never know that you have used it to encapsulate dynamic style implementation.
Providing a Statically Typed Wrapper for Dynamic Code In the sample you are dealing with two types, Document and Worksheet, and they are exhibiting some common behavior. In the sample those are the CheckSpelling and SaveAs methods. However, since they do not share any common type, except the ultimate base type Object common to any other type in .NET, you can not access the services these objects provide in a common way. Now, if you had source code for these two classes, you could just declare an interface — for example, you could name it IOfficeWrapper — declare all common methods for Document and Worksheet in that interface, and make Document and Worksheet implement this interface. This is typical Extract Interface refactoring, and I’ll talk about it in Chapter 12. This way, Document and Worksheet can be seen through a common interface and treated uniformly. So you can start by doing just that, writing an IOfficeWrapper interface to contain all the methods used in a common way (see Listing 5-10).
138
79796c05.qxd:WroxPro
2/23/08
8:08 AM
Page 139
Chapter 5: Chameleon Language Listing 5-10: A Statically Typed Interface for the Office Wrapper Option Explicit On ‘Static type checking turned on Option Strict On Public Interface IOfficeWrapper Sub CheckSpelling() Sub SaveAs(ByVal fileName As String) End Interface
Since you can change neither the Excel.Worksheet nor the Word.Document class, you need to create a new class to implement this interface. This new class will delegate calls to the original officeObject, and to be able to treat both the Worksheet and Document objects in the same way it will be written in dynamically typed style. This means you need to place the class in a separate file and deactivate Option Strict. You provide a wrapper with a constructor, so you can receive an officeObject and keep a reference to the officeObject in a private field. The CheckSpelling and SaveAs methods will only delegate a call to a reference of the original officeObject maintained as a field in the OfficeWrapper class. Listing 5-11 shows the resulting code.
Listing 5-11: A Dynamically Typed Class Wrapper for the Office Wrapper Implementing the IOfficeWrapper Interface Option Explicit On ‘Note Option Strict deactivated Option Strict Off ‘ Class is implementing IOfficeWrapper interface Public Class OfficeWrapper Implements IOfficeWrapper ‘Private field maintains a reference to Document or Worksheet object Private wordDocOrExcelWorksheet As Object Public Sub New(ByRef wordDocOrExcelWorksheet As Object) ‘Constructor accepts reference to Document or Worksheet instance ‘and keeps it in a private field Me.wordDocOrExcelWorksheet = wordDocOrExcelWorksheet End Sub Public Sub CheckSpelling() Implements _ IOfficeWrapper.CheckSpelling ‘CheckSpelling method delegates call to Document or Worksheet wordDocOrExcelWorksheet.CheckSpelling() End Sub Public Sub SaveAs(ByVal fileName As String) _ Implements IOfficeWrapper.SaveAs ‘SaveAs method delegates call to Document or Worksheet wordDocOrExcelWorksheet.SaveAs(fileName) End Sub End Class
139
79796c05.qxd:WroxPro
2/23/08
8:08 AM
Page 140
Part II: Preliminary VB Refactorings Basically, the wrapper class delegates calls only to the original Document or Worksheet object. The Document or Worksheet object is passed to the wrapper at the moment of wrapper creation. Of course, another very important detail here is that in the wrapper class you have deactivated Option Strict. This way you can treat Document and Worksheet in a polymorphic manner. However, in the wrapper interface, Option Strict is set to On. So now take a look at how you can make use of the wrapper. The following code refactors the original CheckSpellingAndSave method so the code is not duplicated and strict typing is maintained. ‘Activate Option Strict in the wrapper client file Option Strict On ‘... Public Sub CheckSpellingAndSave(ByVal officeObject As Object) ‘Create wrapper around officeObject Dim officeWrapper as IOfficeWrapper = _ New OfficeWrapper(officeObject) ‘Use wrapper to perform officeObject services officeWrapper.CheckSpelling() officeWrapper.SaveAs(FileName) End Sub
The only code you had to add to the method was the wrapper creation code. You also had to redirect calls from the officeObject parameter to officeWrapper. In short, the dynamically typed wrapper class implements a statically typed wrapper interface and delegates all calls to an instance of a polymorphic Document or Worksheet object. The Document or Worksheet object is accepted as a parameter by the wrapper constructor method at the moment of wrapper creation. Figure 5-3 shows a statically typed wrapper interface implemented by a dynamically typed wrapper class. <