Professional Refactoring in Visual Basic
-
Upload
danijel-arsenovski -
Category
Documents
-
view
281 -
download
19
Transcript of Professional Refactoring in Visual Basic
Professional
Refactoring in Visual Basic®
Danijel Arsenovski
79796ffirs.qxd:WroxPro 2/22/08 4:57 PM Page iii
To Paola
79796ffirs.qxd:WroxPro 2/22/08 4:57 PM Page v
Professional
Refactoring in Visual Basic®
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xxi
Part I: Introduction to Refactoring
Chapter 1: Refactoring: What’s All the Fuss About? . . . . . . . . . . . . . . . . . . . . 3Chapter 2: A First Taste of Refactoring . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19Chapter 3: Assembling a Refactoring Toolkit . . . . . . . . . . . . . . . . . . . . . . . . 59Chapter 4: Rent-a-Wheels Application Prototype . . . . . . . . . . . . . . . . . . . . . . 87
Part II: Preliminary VB Refactorings
Chapter 5: Chameleon Language: From Weak Static Typing to Strong Dynamic Typing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
Chapter 6: Error Handling: From Legacy to Structured in a Few Easy Steps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
Chapter 7: Basic Hygiene: Eliminating Dead Code, Reducing Scope, Using Explicit Imports, and Removing Unused References . . . . . 173
Part III: Getting Started with Standard RefactoringTransformations
Chapter 8: From Problem Domain to Code: Closing the Gap. . . . . . . . . . . . . 197Chapter 9: The Method Extraction Remedy for Duplicated Code . . . . . . . . . 219Chapter 10: Method Consolidation and Extraction Techniques . . . . . . . . . . 243
Part IV: Advanced Refactorings
Chapter 11: Discovering Objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 271Chapter 12: Advanced Object-Oriented Concepts and
Related Refactorings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 323Chapter 13: Code Organization on a Large Scale. . . . . . . . . . . . . . . . . . . . . 371
Continues
79796ffirs.qxd:WroxPro 2/22/08 4:57 PM Page i
Part V: Refactoring Applied
Chapter 14: Refactoring to Patterns. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 407Chapter 15: LINQ and Other VB 2008 Enhancements . . . . . . . . . . . . . . . . . 439Chapter 16: The Future of Legacy VB Code. . . . . . . . . . . . . . . . . . . . . . . . . 465Appendix A: Unleash Refactor!. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 483Appendix B: Rent-a-Wheels Prototype Internals and Intricacies . . . . . . . . . 487Index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 507
79796ffirs.qxd:WroxPro 2/22/08 4:57 PM Page ii
Professional
Refactoring in Visual Basic®
Danijel Arsenovski
79796ffirs.qxd:WroxPro 2/22/08 4:57 PM Page iii
Professional Refactoring in Visual Basic®Published byWiley Publishing, Inc.10475 Crosspoint BoulevardIndianapolis, IN 46256www.wiley.com
Copyright © 2008 by Wiley Publishing, Inc., Indianapolis, IndianaPublished simultaneously in CanadaISBN: 978-0-470-17979-6Manufactured in the United States of America10 9 8 7 6 5 4 3 2 1
Library of Congress Cataloging-in-Publication DataArsenovski, Danijel, 1973–Professional refactoring in Visual Basic / Danijel Arsenovski.
p. cm.Includes bibliographical references and index.ISBN 978-0-470-17979-6 (paper/website)1. Microsoft Visual BASIC. 2. Computer software—Development 3. Source code (Computer science) I. Title.QA76.76.D47A772 2008005.2'762—dc22
2008001376No part of this publication may be reproduced, stored in a retrieval system or transmitted in any form orby any means, electronic, mechanical, photocopying, recording, scanning or otherwise, except as permittedunder Sections 107 or 108 of the 1976 United States Copyright Act, without either the prior written permis-sion of the Publisher, or authorization through payment of the appropriate per-copy fee to the CopyrightClearance Center, 222 Rosewood Drive, Danvers, MA 01923, (978) 750-8400, fax (978) 646-8600. Requests to the Publisher for permission should be addressed to the Legal Department, Wiley Publishing, Inc., 10475 Crosspoint Blvd., Indianapolis, IN 46256, (317) 572-3447, fax (317) 572-4355, or online athttp://www.wiley.com/go/permissions.
Limit of Liability/Disclaimer of Warranty: The publisher and the author make no representations or war-ranties with respect to the accuracy or completeness of the contents of this work and specifically disclaim allwarranties, including without limitation warranties of fitness for a particular purpose. No warranty may becreated or extended by sales or promotional materials. The advice and strategies contained herein may not besuitable for every situation. This work is sold with the understanding that the publisher is not engaged in ren-dering legal, accounting, or other professional services. If professional assistance is required, the services of acompetent professional person should be sought. Neither the publisher nor the author shall be liable for dam-ages arising herefrom. The fact that an organization or Website is referred to in this work as a citation and/ora potential source of further information does not mean that the author or the publisher endorses the infor-mation the organization or Website may provide or recommendations it may make. Further, readers shouldbe aware that Internet Websites listed in this work may have changed or disappeared between when thiswork was written and when it is read.
For general information on our other products and services please contact our Customer Care Departmentwithin the United States at (800) 762-2974, outside the United States at (317) 572-3993 or fax (317) 572-4002.
Trademarks: Wiley, the Wiley logo, Wrox, the Wrox logo, Wrox Programmer to Programmer, and relatedtrade dress are trademarks or registered trademarks of John Wiley & Sons, Inc. and/or its affiliates, in theUnited States and other countries, and may not be used without written permission. Visual Basic is a regis-tered trademark of Microsoft Corporation in the United States and/or other countries. All other trademarksare the property of their respective owners. Wiley Publishing, Inc., is not associated with any product orvendor mentioned in this book.
Wiley also publishes its books in a variety of electronic formats. Some content that appears in print may notbe available in electronic books.
79796ffirs.qxd:WroxPro 2/22/08 4:57 PM Page iv
To Paola
79796ffirs.qxd:WroxPro 2/22/08 4:57 PM Page v
About the AuthorDanijel Arsenovski is a software developer from Santiago, Chile. Currently, he works as Product andSolutions 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 lostinterest in refactoring ever since. He pioneered the use of refactoring as a vehicle for a VB 6 code upgrade toVB .NET. Arsenovski is a contributing author for Visual Studio Magazine and Visual Systems Journal, holds aMicrosoft 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 athttp://blog.vbrefactoring.com.
79796ffirs.qxd:WroxPro 2/22/08 4:57 PM Page vi
Acquisitions EditorKatie Mohr
Senior Development EditorKevin Kent
Technical EditorDoug Holland
Copy EditorsS. B. KleinmanMildred Sanchez
Editorial ManagerMary Beth Wakefield
Production ManagerTim Tate
Vice President and Executive Group PublisherRichard Swadley
Vice President and Executive PublisherJoseph B. Wikert
Project Coordinator, CoverLynsey Stanford
CompositorCraig James Woods, Happenstance Type-O-Rama
ProofreaderWord One
IndexerJohnna VanHoose Dinse
Credits
79796ffirs.qxd:WroxPro 2/22/08 4:57 PM Page vii
Acknowledgments
A number of people deserve credit for helping me out in getting this book to the finish line. I am quite awarethat dealing with a first-time writer is always a challenge and probably means much more work than usual. Solet me start with my Wrox team. I want to thank Kevin Kent, my development editor, for always being there forme and for his meticulous work. He was always there with exactly the right dose of guidance. I want to thankKatie Mohr for believing in this project right from the outset. Thanks to Doug Holland, my technical editor, forproviding that all-important safety net that every technical author needs. Also thanks to Chris Webb for puttingme 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 themanuscript and giving invaluable feedback, particularly on sections related to the Refactor! for VBadd-in. Also thanks to the rest of the Developer Express team for giving me full access to their prod-ucts and for responding to my queries.
I want to thank Dusan Miloradovic for his review and excellent feedback. Other people that provided feed-back 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 toDiego Dagum for his interest in my work. Finally, thanks to Milos Milosavljevic for introducing me to theworld of Visual Basic.
79796ffirs.qxd:WroxPro 2/22/08 4:57 PM Page viii
Contents
Acknowledgments viiiForeword xixIntroduction xxi
Part I: Introduction to Refactoring 1
Chapter 1: Refactoring: What’s All the Fuss About? 3
A Quick Refactoring Overview 4The Refactoring Process 4A Look at the Software Situation 5
The Refactoring Process: A Closer Look 7Using Code Smells 7Transforming the Code 8The Benefits of Refactoring 9Debunking Common Misconceptions 12
Visual Basic and Refactoring 14Visual Basic History and Legacy Issues 14Visual Basic Evolution 14Dealing with Legacy Issues Through Refactoring 16
Summary 16
Chapter 2: A First Taste of Refactoring 19
Calories Calculator Sample Application 19Calories Calculator Application 20Growing Requirements: Calculating Ideal Weight 22Growing Requirements: Persisting Patient Data 24
Refactoring in Action 26Decomposing the BtnCalculate_Click Method 27Discovering New Classes 29Narrowing the Patient Class Interface 32Putting Conditional Logic in the Patient Class 34Creating the Patient Class Hierarchy 37
Implementing the Persistence Functionality 43Saving the Data 44Implementing Patient-History Display Functionality 52
Calories Calculator, Refactored Version 56Summary 58
79796ftoc.qxd:WroxPro 2/23/08 8:45 AM Page ix
x
Contents
Chapter 3: Assembling a Refactoring Toolkit 59
Using an Automated Refactoring Tool 60ReSharper from JetBrains 60Visual Assist X from Whole Tomato 61Refactor! Pro from Developer Express 61Getting Started with Refactor! 61Exploring Refactor! for the VB User Interface 63Quick Tour: Available Refactorings 67
Unit-Testing Basics: The Testing Harness 69Why a Unit-Testing Framework? 70Your First Taste of NUnit 72Installing NUnit 72Implementing Your First Test 74The Test-Driven Approach 82Other Test Tools to Consider 83
A Few Words about Version Control 84Summary 85
Chapter 4: Rent-a-Wheels Application Prototype 87
Interviewing the Client 88Interviewing the Manager 88Interviewing the Desk Receptionist 89Interviewing the Parking Lot Attendant 90Interviewing Maintenance Personnel 90
Taking the Initial Steps in the Rent-a-Wheels Project 91Actors and Use Cases 91Vehicle States 93First Sketch of the Main Application Window 94Rent-a-Wheels Team Meeting 96
Making the Prototype Work 96Examining the Database Model 96Examining the Visual Basic Code 99
Fast and Furious, a VB Approach to Programming 102Database-Driven Design 102GUI-Based Application 103Event-Driven Programming 103Rapid Application Development (RAD) 104Copy-Paste as a Code Reuse Mechanism 104
From Prototype to Delivery Through the Refactoring Process 105Summary 105
79796ftoc.qxd:WroxPro 2/23/08 8:45 AM Page x
xi
Contents
Part II: Preliminary VB Refactorings 107
Chapter 5: Chameleon Language: From Weak Static Typing to Strong Dynamic Typing 109
Option Explicit and Option Strict, the .NET Effect 110Setting Option Explicit On in Relaxed Code 111
Understanding the Set Option Explicit On Refactoring 112Refactoring the Rent-a-Wheels Code to Explicit Form 114
Setting Option Strict On in Relaxed Code 115A Slightly Artificial Example of Permissive VB code 116Convoluted Use of Variables Resolved by the Definition of New Variables 119Inferring Variable Type 122Putting It All Together with Type-Conversion Functions 125Dealing with Methods, Fields, Properties, and Other Members 127Applying Set Option Strict On Refactoring to the Rent-a-Wheels Application 132
Static Versus Dynamic Typing and Visual Basic 135Late Binding in Visual Basic 6 and Prior 136Duck Typing 136Resetting Dynamic or Static Behavior at the File Level 138Providing a Statically Typed Wrapper for Dynamic Code 138
Activating Explicit and Strict Compiler Options 141Setting Options in the Project Properties 141Changing the Default Behavior of the Visual Basic Compiler 142Setting Options in Source Files 143Using Item Templates to Set Options 143
Summary 145
Chapter 6: Error Handling: From Legacy to Structured in a Few Easy Steps 147
Legacy Error Handling Versus Structured Error Handling 148Legacy (Unstructured) Error Handling 148Structured Error Handling 150
The Benefits of Structured Error Handling 153Structured Versus Unstructured Code 153Exceptions as Types, Not Numbers 154Error Filtering 154The Finally Block 155.NET Interoperability 155
Replacing the On Error Construct with Try-Catch-Finally 156Understanding the When Keyword 158Refactoring Steps for Replacing On Error with Try-Catch-Finally 158Replacing the On Error Goto Label with the Try-Catch-Finally Construct 159Replacing On Error Resume Next with the Try-Catch-Finally Construct 162
79796ftoc.qxd:WroxPro 2/23/08 8:45 AM Page xi
xii
Contents
Replacing Error Code with Exception Type 164Replacing System Error Codes with Exception Types 166Replacing Custom Error Codes with Exception Types 168
Error Handling in the Rent-a-Wheels Application 169Application-Level Events in VB 2009 169
Summary 171
Chapter 7: Basic Hygiene: Eliminating Dead Code, Reducing Scope, Using Explicit Imports, and Removing Unused References 173
Eliminating Dead Code 174Types of Dead Code 175Common Sources of Dead Code 176
Reducing the Scope and Access Level of Unduly Exposed Elements 179Scope and Access Level 181Common Sources of Overexposure 182Dealing with Overexposure 186
Using Explicit Imports 187Imports Section Depicts Dependencies in Your System 188
Removing Unused Assembly References 191Basic Hygiene in the Rent-a-Wheels Application 192Summary 193
Part III: Getting Started with Standard RefactoringTransformations 195
Chapter 8: From Problem Domain to Code: Closing the Gap 197
Understanding the Problem Domain 198Step One: Gathering the Information 198Step Two: Agreeing on the Vocabulary 199Step Three: Describing the Interactions 200Step Four: Building the Prototype 201
Naming Guidelines 201Capitalization Styles 202Simple Naming Guidelines 203Good Communication: Choosing the Right Words 204Rename Refactoring 206
Published and Public Interfaces 208Self-Contained Applications Versus Reusable Modules 209Modifying the Public Interfaces 212Safe Rename Refactoring in Refactor! 214
Rename and Safe Rename Refactoring in the Rent-a-Wheels Application 217Summary 218
79796ftoc.qxd:WroxPro 2/23/08 8:45 AM Page xii
xiii
Contents
Chapter 9: The Method Extraction Remedy for Duplicated Code 219
Why Keep Code Encapsulated and Details Hidden? 219Information and Implementation Hiding 220Decomposing Methods 223
Circumference Calculation — Long Method Example 223Extracting Circumference Length Calculation Code 226Extracting the Radius Calculation Code 229Extracting the “Wait for User to Close” Code 229Extracting the Read Coordinates Code 229Extract Method Refactoring in Refactor! 233
The Duplicated Code Smell 234Sources of Duplicated Code 235Copy-Paste Programming 236
Magic Literals 237Introduce Constant Refactoring in Refactor! 239
Extract Method and Replace Magic Literal Refactoring in the Rent-a-Wheels Application 240Summary 240
Chapter 10: Method Consolidation and Extraction Techniques 243
Dealing with Temporary Variables 243Move Declaration Near Reference Refactoring 244Move Initialization to Declaration Refactoring 248Split Temporary Variable Refactoring 249Inline Temp Refactoring 253Replace Temp with Query Refactoring 256
Method Reorganization Heuristics 258Method Reorganization and Rent-a-Wheels 259
Removing Duplication in Rent-a-Wheels 261Magic Literals, Comments, and Event-Handling Blindness in Rent-a-Wheels 263
Summary 267
Part IV: Advanced Refactorings 269
Chapter 11: Discovering Objects 271
A Quick Object-Oriented Programming Overview 272What Are Objects Anyway? 272Encapsulation and Objects 272Encapsulate Field Refactoring in Refactor! 274Object State Retention 276
79796ftoc.qxd:WroxPro 2/23/08 8:45 AM Page xiii
xiv
Contents
Classes 276Object Identity 277Objects as Basic Building Blocks 278Root Object 278Object Lifetime and Garbage Collection 279Messages 280
Designing Classes 281Classes Are Nouns, Operations Are Verbs 284Classes, Responsibilities, and Collaborators 288Entities and Relationships 297
Discovering Hidden Classes 298Dealing with Database-Driven Design 299Moving From Procedural to Object-Oriented Design 302Keeping Domain, Presentation, and Persistence Apart 308Discovering Objects and the Rent-a-Wheels Application 313
Summary 320
Chapter 12: Advanced Object-Oriented Concepts and Related Refactorings 323
Inheritance, Polymorphism, and Genericity 324Inheritance 324Polymorphism 329Genericity 332
Inheritance Abuse and Refactoring Solutions 334Composition Mistaken for Inheritance and Other Misuses 337Refactoring for Inheritance — Print-System Illustration 342
Making Use of Generics 360Inheritance and Generic Types in the Rent-a-Wheels Application 364
Extracting Super 364Employing Generics 364Extract Data Objects Provider Class 365
Summary 369
Chapter 13: Code Organization on a Large Scale 371
Namespaces 371Naming Guidelines and Namespace Organization 372Nested Namespaces 372Changing the Root Namespace Name 372Using Import Statements 373
79796ftoc.qxd:WroxPro 2/23/08 8:45 AM Page xiv
xv
Contents
Assemblies 375Binary Reuse 375Namespace Organization Guidelines 377Dependency Considerations 381
Visual Basic Project File Structure Organization 387Move Type to File Refactoring in Refactor! 388Partial Classes 390Inherited Form 390Abstract Form Inheritance 392Delegating Abstract Form Work to a Form Helper Class 392
Namespace Organization and Windows Forms Inheritance in Rent-a-Wheels 394Extracting Parent Administration Form through Abstract Form Helper Pattern Application 394Namespace and Assembly Reorganization 402
Summary 403
Part V: Refactoring Applied 405
Chapter 14: Refactoring to Patterns 407
Design Patterns: What’s All the Fuss About? 408Defining Design Patterns 408Classifying Patterns 409Pattern Elements 409Weighing the Benefits of Design Patterns 410Using Patterns 410
Example Design Pattern: Abstract Factory 411Name 411Problem 411Solution 420Consequences 424
Dependency Injection Pattern 426Problem 426Solution 428Constructor-Based vs. Property-Based Injection 429What Service Implementation to Inject 430Consequences 431Refactoring to DI 434
Refactoring to Patterns and Rent-a-Wheels Application 434Eliminating Code That Duplicates Functionality Available in .NET Framework 434Injecting Data Classes to GUI Classes via Dependency Injection 435CRUD Persistence Pattern 437
Summary 438
79796ftoc.qxd:WroxPro 2/23/08 8:45 AM Page xv
xvi
Contents
Chapter 15: LINQ and Other VB 2008 Enhancements 439
Type Inference for Local Variables 439XML Productivity Enhancements 440
XML Literals 440Navigating XML with XML Axis Properties 444Extract XML Literal to Resource in Refactor! 444
Querying the Objects with LINQ 445Old Example in New Robes 448Object-Relational Mapping with LINQ to SQL 451LINQ and the Rent-a-Wheels Application 454
Summary 464
Chapter 16: The Future of Legacy VB Code 465
To Migrate or Not To Migrate 466Migration Cannot Be 100 Percent Automated 467VB 6 and VB .NET Code Can Interoperate 467Migration Tools and Libraries 469
Preliminary VB 6 Refactorings 470Breaking the Monolith 470Dealing with Conditional Compilation 472
Putting Your Migrated Code under a Testing Harness 472Introducing a Functional Testing Harness 472Implementing a Functional Testing Harness 473
Upgrading Your Legacy Code 477Strict Static Typing 477Moving Design from Procedural toward an Object-Oriented Paradigm 477Introducing Inheritance 478Making Use of Parameterized Constructor 479Using Generic Containers for Additional Type Safety 479Upgrading Exception Handling 481Implementing XML Comments 481Releasing Resources in .NET 482
Summary 482
Appendix A: Unleash Refactor! 483
Appendix B: Rent-a-Wheels Prototype Internals and Intricacies 487
Hand Over Button Click Event-Handling Code 487Receive Button Click Event-Handling Code 488
79796ftoc.qxd:WroxPro 2/23/08 8:45 AM Page xvi
xvii
Contents
Charge Button Click Event-Handling Code 488Change Branch Button Click Event-Handling Code 489To Maintenance and From Maintenance Button Click Event Code 493Administer Fleet Form 494
Delete Button Click Event-Handling Routine 495New Button Click Event-Handling Routine 495Reload Button Click Event-Handling Routine 495Form Load Event-Handling Routine 495Administer Fleet Form Class Code: Fields 498Left Button Click Event-Handling Routine 499Save Button Click Event-Handling Routine 500
Display Button Click Event-Handling Routine 502Summary 506
Index 507
79796ftoc.qxd:WroxPro 2/23/08 8:45 AM Page xvii
79796flast.qxd:WroxPro 2/25/08 9:23 AM Page xviii
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 paidparticular attention to a thought he shared during an interview. He said that, when filming scenes, it usuallyhappens that a given scene isn’t taped as the director originally assumed it would be. Sometimes that sceneis reworked until the director gets what was originally wanted, while on some other occasions it’s just leftthe way it came, as it’s considered exponentially better than the initial sketch. So he concluded that the realart of making movies is deciding wisely when to do it again and when to just take what was gotten the firsttime 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 ourheads, 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 willdo it better ahead — what that means in software industry terms: refactoring is a series of techniques andmechanisms used to improve the quality — understandability, maintainability, modularity, extensibility, andso on — of code segments by reformulating their sentences in such a way that the general behavior remainsunchanged. In other words, the behavior of the affected components shouldn’t vary as a consequence of theprocess 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 theysee 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 meansaccusing them. An undesirable side effect of this back and forth is that our code could start los-ing a certain degree of cohesion; its different modules may start being coupled above the accept-able 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 howcoding will look while we are modeling. As do the users, we also think we have cool ideas (or justgood 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 ideawasn’t, in fact, so easy to implement. And here, again, when time adds its pressure, delivering acomponent code the quickest we can may also harm its quality.
79796flast.qxd:WroxPro 2/25/08 9:23 AM Page xix
❑ From the technology perspective, finally, there’s an invisible, somewhat omnipresent pressureto 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 Service-Oriented 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 thehighest 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, therewhere 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, theremaining time, the available budget — it will always be easier to justify the application of refactoringwhere we already have to do something else than where no updates have yet to be asked for — amongother 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 refactoringtechniques, both in the .NET and Java platforms, since their earliest versions. He has delivered speechesat several conferences, given talks, and held workshops on this subject, and driven successful refactor-ing projects in the banking industry.
As one of the leaders in development tools, Microsoft has been committed to delivering best-of-breedresources to the people who deal daily with coding activities and software projects as a whole. Throughits undisputably winning Visual Studio IDE, Microsoft makes refactoring an out-of-box facility just aright-click away from your code. In these pages, Danijel will show you how refactoring may be practicedin 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 DagumTechnical Evangelist, Microsoft Corp.Kingsgate, Winter 2007
Foreword
xx
79796flast.qxd:WroxPro 2/25/08 9:23 AM Page xx
Introduction
Thank you for choosing this book and welcome to the fabulous world of refactoring. I hope you will findthis book useful as you go about your daily programming chores, as you discuss different design solutionswith your peers, when you are getting ready to attack some obscure legacy code, and even as you are goingover some lines in your mind that are keeping you awake at night. If this is your first encounter with refac-toring, I expect this book to profoundly change the way you program and think about code. This is not aneasy 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 ofExisting 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. Thebook 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 aplastic, 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 andmore of my coworkers taking the book from the shelf. A few even got their own copies. I was able tospeak with them using refactoring terminology and introduce refactoring as an integral part of the soft-ware 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 ofthe necessities that I am often presented with at my workplace, I get to work with different teams and pro-gram in different languages. I tried disseminating refactoring when working with teams that program inVisual Basic in a similar manner I did with Java or C# teams. This proved to not go that smoothly. Soon Irealized there is very little information on refactoring available for Visual Basic programmers. While mostof the refactorings can be applied in a similar manner in any object-oriented programming language, thereare some subtle differences.There is no reason why a programmer should not learn to refactor by lookingat 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 amconvinced that there is a real need for such a text and that this book will be of practical use to manywho program in Visual Basic.
79796flast.qxd:WroxPro 2/25/08 9:23 AM Page xxi
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, moresophisticated designs. Most importantly, I hope it will ease the burden of everyday tasks and put somefun back into what we all like to do best: program some great code.
Whom This Book Is ForThis book is intended for experienced (intermediate to advanced) Visual Basic .NET developers thatwish to be introduced to the world of refactoring. To get the most out of the book, you should have agood 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. Thisbook 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 yourfirst classes, you can use this book to learn how to design them properly and how to correct any mis-takes 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 refactoringsthat I deal with in this book because of differences between Visual Basic 6 (or previous) programminglanguages 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 thebook, to the subject of upgrading VB 6 code to .NET. The chapter itself, however, builds upon materialexposed 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 toVisual Basic .NET. This book exposes some of the common mistakes that newcomers from VB 6 to VB .NETmake 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 appli-cation, 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 techniquesexplained in this book valuable.
Most of the refactorings I deal with in this book are standard refactorings applicable in any fully object-oriented language. This means that if you program in some other object-oriented language, for exampleC#, as long as you are familiar with VB .NET syntax and you are able to read code examples, you will beable to use and apply the information exposed in this book.
What This Book CoversI have tried to make this book a good introduction and thorough overview of refactoring in Visual Basic.This book teaches the basic refactoring concepts:
❑ Code Smells
❑ Refactoring code transformations
Introduction
xxii
79796flast.qxd:WroxPro 2/25/08 9:23 AM Page xxii
❑ 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 notrepresent a complete refactoring catalog; because of time and space limitations, some important refactor-ings had to be excluded. For example, it does not deal with refactorings such as Simplify Conditional orReverse 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 structuredcode, 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 willaid you in eliminating constructs (methods or classes, for example) that are not needed any more. Thisoften 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 appearlater on in the refactoring acquisition, once you have already grasped the basics of refactoring. Part of thelearning 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 refac-torings. 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 andtry to invent new refactorings. This way, you will truly master the art of continuous code improvement.
How This Book Is StructuredBecause this is the first book that deals exclusively with refactoring in Visual Basic and will probably bethe 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 duringeveryday programming sessions.
❑ Demonstrate how refactoring techniques can be applied in a real-life situation, by means of singlecase study, the Rent-a-Wheels application, that I analyze and modify at the end of each chapterthroughout the book.
Introduction
xxiii
79796flast.qxd:WroxPro 2/25/08 9:23 AM Page xxiii
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 inlogical order from beginning to end. This is something that you should probably do once or twice afteryou 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” ofsmells, refactorings, and object-oriented design principles. The purpose of these definitions is to give a con-densed overview of the subject. In the case of smells for example, this definition contains heuristics on howthe smell can be discovered. In the case of refactorings, there is a section called “Mechanics” that contains aform 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 isperformed, 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 princi-ples I talked about in the chapter reflected on the case study included with the book. You can downloadthe code for the chapter and browse it as you read about what happened to the case study in the currentchapter. 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 simplifiedin 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 unexpectedobstacles appearing if certain techniques are to be applied to production code. In this book, I tried to pres-ent 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 conceptsin logical progression.
❑ Part I, “Introduction to Refactoring,” lays the foundation for the book. For example, in Chapter 1I talk about refactoring in some general terms. This chapter also dispels some common misconcep-tions about refactoring. Chapter 2 gives you a taste of refactoring in practice right away. ThenChapter 3 goes over the tools that you will find indispensable in order to automate your refactor-ing work. Chapter 4 presents you with a case study used throughout the book.
❑ Part II, “Preliminary VB Refactorings,” covers some preliminary refactorings such as deadcode elimination and others that will help you prepare the code for major overhaul. You dealwith some VB-only problems such as strict versus weak typing, legacy versus structured errorhandling, 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 andabout the devastating effects of duplicated code. You deal with standard refactorings such asExtract 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 youget the most from object-oriented capabilities of your programming environment. Good object-oriented 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 designpatterns to produce even more sophisticated design. You will see some of the new features thatcome with the Visual Studio 2008 version of VB and how refactoring can be used to get the mostout of them. Finally, you will take a look back at VB 6, a Visual Basic .NET predecessor. You willlearn how to apply refactoring in order to update your VB6 code to Visual Basic .NET.
Introduction
xxiv
79796flast.qxd:WroxPro 2/25/08 9:23 AM Page xxiv
What You Need to Use This BookIn 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 touse the Refactor! add-in. You will still be able to debug, execute code, and perform refactoringsmanually in the free Visual Basic 2008 Express Edition. If you use Visual Studio 2005, you willalso be able to get some use out of most of the book, except for Chapter 15, which deals exclu-sively 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 freeVisual 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 thebook. This can be MS SQL Server 2000 or 2005, and the Express edition will suffice for the appli-cation that comes with this book. You can use any operating system, like Windows XP, WindowsServer 2003, or Windows Vista, that can run the software I just listed.
ConventionsTo help you get the most from the text and keep track of what’s happening, a number of conventions areused throughout the book.
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 thepresent context.
❑ Also, bold is occasionally used within code listings to emphasize parts of code.
Boxes like this one hold important, not-to-be forgotten information that is directlyrelevant to the surrounding text. This information includes some important defini-tions you encounter throughout the book. You might also look for these definitionswhen using the book in a reference-like manner.
Introduction
xxv
79796flast.qxd:WroxPro 2/25/08 9:23 AM Page xxv
Smell, Refactoring, and Object-OrientedDesign Principle Boxes
In addition to the conventions just mentioned, throughout the course of this book you will come acrossthree 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 impor-tant refactoring concept, and each box contains three sections:
❑ Detecting the Smell — Describes some simple heuristic on how to detect the code smellin 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. Eachbox contains the following sections:
❑ Motivation — Explains the beneficial effect this refactoring has on code and how thedesign 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-orienteddesign principles and often use some short code samples to illustrate them.
Index of SmellsFor ease of reference, the following table indicates what smells are included in the book and the pagenumber where the box dealing with that specific smell can be found.
Smell 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
Introduction
xxvi
79796flast.qxd:WroxPro 2/25/08 9:23 AM Page xxvi
Index of RefactoringsThe following table indicates what refactorings are included in the book and the page number where thebox dealing with that specific refactoring can be found.
Continued
Refactoring Page Number
Break Monolith 471
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
Introduction
xxvii
79796flast.qxd:WroxPro 2/25/08 9:23 AM Page xxvii
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
Introduction
xxviii
79796flast.qxd:WroxPro 2/25/08 9:23 AM Page xxviii
Index of Object-Oriented Design PrinciplesThe following table indicates the object-oriented design principles that are included in the book and thepage 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
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 Declarationand Explicit Type Conversions)
131
Split Temporary Variable 251
Introduction
xxix
79796flast.qxd:WroxPro 2/25/08 9:23 AM Page xxix
Source CodeAs you work through the examples in this book, you may choose either to type in all the code manuallyor to use the source code files that accompany the book. All of the source code used in this book is avail-able for download at http://www.wrox.com. Once at the site, simply locate the book’s title (either byusing the Search box or by using one of the title lists) and click the Download Code link on the book’sdetail 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 is978-0-470-17979-6.
Once you download the code, just decompress it with your favorite compression tool. Alternately, youcan 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.
ErrataWe make every effort to ensure that there are no errors in the text or in the code. However, no one is per-fect, and mistakes do occur. If you find an error in one of our books, like a spelling mistake or faulty pieceof code, we would be very grateful for your feedback. By sending in errata you may save another readerhours 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 Searchbox or one of the title lists. Then, on the book details page, click the Book Errata link. On this page you canview all errata that has been submitted for this book and posted by Wrox editors. A complete book listincluding 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 informationand, if appropriate, post a message to the book’s errata page and fix the problem in subsequent editionsof the book.
p2p.wrox.comFor author and peer discussion, join the P2P forums at p2p.wrox.com. The forums are a web-based systemfor you to post messages relating to Wrox books and related technologies and interact with other readersand technology users. The forums offer a subscription feature to e-mail you topics of interest of your choos-ing when new posts are made to the forums. Wrox authors, editors, other industry experts, and your fellowreaders 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 youread this book, but also as you develop your own applications. To join the forums, just follow these steps:
1. Go to p2p.wrox.com and click the Register link.
2. Read the terms of use and click Agree.
Introduction
xxx
79796flast.qxd:WroxPro 2/25/08 9:23 AM Page xxx
3. Complete the required information to join as well as any optional information you wish to pro-vide and click Submit.
4. You will receive an e-mail with information describing how to verify your account and com-plete the joining process.
You can read messages in the forums without joining P2P but in order to post your own messages, youmust join.
Once you join, you can post new messages and respond to messages other users post. You can read mes-sages at any time on the Web. If you would like to have new messages from a particular forum e-mailedto 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 ques-tions about how the forum software works as well as many common questions specific to P2P and Wroxbooks. To read the FAQs, click the FAQ link on any P2P page.
Introduction
xxxi
79796flast.qxd:WroxPro 2/25/08 9:23 AM Page xxxi
79796flast.qxd:WroxPro 2/25/08 9:23 AM Page xxxii
Part I: Introduction to Refactoring
In this introductory part, you are going to see what refactoring is in general terms, whyit is important, what benefits refactoring brings to the development process, and how itcan be even more relevant to Visual Basic programmers than to programmers in someother languages. You are also going to see a small demonstration of the refactoringprocess 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 therefactoring process as it is applied.
79796c01.qxd:WroxPro 2/25/08 8:55 AM Page 1
79796c01.qxd:WroxPro 2/25/08 8:55 AM Page 2
Refactoring: What’s All the Fuss About?
Take a look at any major integrated development environment (IDE) today and you are bound todiscover “refactoring” options somewhere at the tip of your fingers. And if you are followingdevelopments in the programming community, you have surely come across a number of articlesand books on the subject. For some, it is the most important development in the way they codesince the inception of design patterns.
Unlike some other trends, refactoring is being embraced and spread eagerly by programmers andcoders themselves because it helps them do their work better and be more productive. Without adoubt, applying refactoring has become an important part of programmers’ day-to-day labor nomatter the tools, programming language, or type of program being developed. Visual Basic is apart of this: at this moment, the same wave of interest for refactoring in the programming commu-nity 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 discussa 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 Basicas a programming language and how refactoring can be even more relevant for VisualBasic 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 3
A Quick Refactoring OverviewWhen 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 yourwork 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 intomore than one? Is there an inheritance relationship between these two classes or should I just use com-position?” And if you share your thoughts with some of your peers, you are bound to hear even moreoptions for designing your system. However, once you commit yourself to one approach, it may seemvery costly to change these initial decisions later on. Refactoring teaches you how to efficiently modifyyour code in such a way that the impact of those modifications is kept at a minimum. It also helps youthink about the design as something that can be dealt with at any stage of the project, not at all cast instone by initial decisions. Design, in fact, can be treated in a very flexible way.
All design decisions are the result of your knowledge, experience, and creativity. However, programmingis a vast playfield, and it’s easy to get tangled in contradictory arguments. In VB .NET you are, first andforemost, guided by object-oriented principles and rules. Unfortunately, very often it is not so clear howthese rules work out in practice. Refactoring teaches you some simple heuristics that can help improveyour design by inspecting some of the visible characteristics of your code. These guidelines that refactor-ing provides will set you on the right path in improving the design of your code.
The Refactoring ProcessRefactoring is an important programming practice and has been around for some time. Pioneered by theSmalltalk community, it has been applied in a great number of programming languages, and it has takenits place in many programmers’ bags of tricks. It will help you write your code in such a way that youwill 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 identi-fying 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 trans-formations. 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 generallyconsists of executing appropriate unit tests that will prove the behavior of your code didn’t changeafter performing refactoring.
Definition: Refactoring is a set of techniques used to identify the design flows and tomodify the internal structure of code in order to improve the design without changingcode’s visible behavior.
4
Part I: Introduction to Refactoring
79796c01.qxd:WroxPro 2/25/08 8:55 AM Page 4
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, programmingstyle, and knowledge. Design in this sense simply means that refactoring builds upon object-orientedtheory with the addition of some very simple heuristics dedicated to identifying shortcomings and weakspots in your code. These antipatterns are generally referred to as code smells and a great part of refactor-ing can be seen simply as an attempt to eliminate code smells.
The code smell can be something as simple as a very large method, a very large class, or a class consistingonly of data and with no behavior. I’ll dedicate a lot of time to code smells in the book, because improvingyour 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 modifi-cations to your code. The refactoring methodology and its techniques help you in this task by making iteasier to perform and even automate such modifications.
A Look at the Software SituationAs software developers, your success depends on being able to fulfill different types of expectations. Youhave 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, generallymeaning 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 torelease 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 minimizememory 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 itwill behave for the final user. However, in the process of producing the binary, you actually work withsource code. You create classes, add properties and methods, organize them into the namespaces, write logicusing loops and conditions, and so on. This source code, at a click of a button, is then transformed, com-piled, or built into a deliverable, a component, an executable, or something similar. There is an inevitablegap 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 atstonemasonry, for example. While the mason chips away pieces of stone and polishes the edges, he orshe can see the desired result slowly appearing under the effort. With software, the process is not at allas direct. You write source code that is then transformed into the desired piece of software. Even withthe visual tools, which largely bridge this gap between source and binary, all you do in the end is create
Definition: Code smell is a sensation you develop that tells you that there might be aflaw in your code.
5
Chapter 1: Refactoring: What’s All the Fuss About?
79796c01.qxd:WroxPro 2/25/08 8:55 AM Page 5
the source that is later on processed and turned into a compiled unit. Imagine a cook that can only writedown a recipe and try the cooked meal, but is not allowed to handle the ingredients or taste the mealwhile it is being prepared.
What’s more, there are many ways to write the source that will produce the same resulting binary. This caneasily lead you to forget or sacrifice some qualities inherent to the source code itself, because the sourcecode can be considered just a secondary artifact. While these qualities are not directly transformed to a finalproduct, 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 seehow refactoring can clarify doubts you might have.
Refactoring Encourages Solid DesignNo 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 indentedand 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 softwaredesign. In Visual Basic, you follow the object-oriented software paradigm. You look into how well classesare structured and encapsulated, what their responsibilities are, and how they collaborate. You use lan-guage building blocks like classes and interfaces; and features like encapsulation, inheritance, and poly-morphism in building a cohesive structure that describes the problem domain well. In a certain way youbuild your own ad-hoc language on top of a common language that will communicate your intentionsand 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 purposewell, 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 ofwell-designed code. And when you stray from your path, it helps you reorganize and impose the bestdesign decisions with ease.
Refactoring Accommodates Change Popular software design techniques like object-oriented analysis and design, UML diagramming, use-case 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, andeven some planned design decisions, when confronted with real-world demands, overruled. Softwareconstruction is a very complex activity, and it is futile attempting to come up with a perfect solution upfront. Even if some more sophisticated techniques like modeling are used, you still come short of think-ing about every detail and every possible scenario. It is this state of flux that is often the biggest chal-lenge 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 soonobscured, and the dangerous malaise of rotting design settles in.
6
Part I: Introduction to Refactoring
79796c01.qxd:WroxPro 2/25/08 8:55 AM Page 6
Refactoring is a relatively simple way to prepare for change, implement change, and control the adverseeffects these changes can have on your design.
Refactoring Prevents Design RotSoftware is definitely one of the more ephemeral human creations. Driven by new advances and technolo-gies, software creations are soon replaced with more modern or advanced versions. Even so, during itslifetime 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 quitecommon that more than one person will put their hands on the same piece of software, each with his orher own style and preferences. Rarely will it be the same team of people that will see the software fromthe beginning to the end.
Go back for a moment to the stonemason example. Now imagine that there is more than one personworking on the same stone, that these people can change during the collaboration, and that the originalplan is often itself changed with new shapes added or removed and different materials used. That maybe 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 solu-tions, and original design diluted. The initial intentions become less pronounced and the metaphorsmore difficult to comprehend, and the source is closer and closer to a meaningless cluster of symbolsthat still, but a lot less reliably, performs the intended function. This ailment steals in quietly, step bystep, 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 thefirst 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 chal-lenges that developers face and how refactoring can help. Next, I want to discuss refactoring in more detail.
The Refactoring Process: A Closer LookI just discussed a few key areas of software development that can often lead to poor code. You need tostand guard for the quality of your code constantly. In effect, you need to have the design qualities ofyour code in mind at all times.
While this sounds sensible, thinking continuously about design and code quality can often be costly andquite complicated. The refactoring methodology and its techniques help you in this task by making iteasier 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 duringa software development cycle.
Using Code SmellsAs 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 thistask, along with well-known notions and principles of object-oriented design. Programming, being complex
7
Chapter 1: Refactoring: What’s All the Fuss About?
79796c01.qxd:WroxPro 2/25/08 8:55 AM Page 7
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, youdevelop more expertise in identifying and eliminating bad smells in your code.
Transforming the CodeThe next step leads you to modifying the code’s internal structure. Here, refactoring theory has devel-oped 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 tool-makers 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 anexample. 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
All you did here was to replace the literal value 1.5 with a constant OvertimeIndex. Executing thecode on both sides provides identical results, but the one on the right can be a lot easier to maintain ormodify. And it is definitely easier to understand. Now you understand that the literal 1.5 has a specialmeaning and has not been selected by chance.
Automating Refactoring TransformationsRefactoring rules have one great consequence: it is possible to automate a large number of these trans-formations. Automation is really the key to letting refactoring show its best. Refactoring tools will checkfor the validity of what you are trying to perform and let you apply a transformation only if it doesn’tbreak the code. Even without a tool, refactoring is worth your while; however, manual refactoring can beslow and tedious.
Free Literal Value Literal as Constant
Public Class Employee
Private hoursWorked As Integer
Private overtimeHoursWorked _As Integer
Private hourlyWage As Decimal
Public Function GetWage() _As Decimal
Return (hoursWorked * _hourlyWage) + _(overtimeHoursWorked * _hourlyWage _* 1.5)
End FunctionEnd Class
Public Class Employee
Public Const OvertimeIndex _As Decimal = 1.5
Private hoursWorked As Integer
Private overtimeHoursWorked _As Integer
Private hourlyWage As Decimal
Public Function GetWage() _As Decimal
Return (hoursWorked _*hourlyWage) + _(overtimeHoursWorked _* hourlyWage _* OvertimeIndex)
End FunctionEnd Class
8
Part I: Introduction to Refactoring
79796c01.qxd:WroxPro 2/25/08 8:55 AM Page 8
Figure 1-1 shows the Refactor! for VB Visual Studio add-in from Developer Express integrated withVisual Studio 2005.
Figure 1-1
The Benefits of RefactoringIn light of all this, it is pertinent to ask what benefits refactoring brings. After all, it is not about addingnew features or resolving bugs, and you end up with code that basically does what it used to, so whyshould you invest the time and money to perform this activity? What are the benefits of keeping yourdesign optimal at all times? How does refactoring pay off?
Keeping the Code SimpleWith the fact that software development is a continuous, evolving process, refactoring can bring impor-tant qualities to your code. Keeping your code lean at all times can be challenging, especially whenyou’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 cropup, and so on. This will soon produce a situation where classes and methods have grown andgone beyond their original purposes. They have too many responsibilities, communicate withmany other elements, and are prone to change for many different reasons. It also becomes abreeding ground for duplicated code.
9
Chapter 1: Refactoring: What’s All the Fuss About?
79796c01.qxd:WroxPro 2/25/08 8:55 AM Page 9
❑ In another scenario, you start off with a very thorough design that proves to be more than youreally need. Simple code does only what it is supposed to do; you need not be so concernedmuch with trying to have your solution respond to any possible situation even before it hap-pens. You can easily develop a tendency to overengineer your code, using complex structureswhen simple ones will do. (You can easily identify this school of thought by how many “whatif” statements are used in discussions of the code.) This situation is motivated by an urge toanticipate future requirements even before they are expressed by the client.
❑ Performance has proved to be a lure for generations of programmers. You might spend numer-ous hours in order to obtain nanosecond gains in execution time. Without trying to lessen theimportance of this key quality of software, you should bear in mind the right moment to dealwith it. It can be very difficult to find the critical line you need to change in order to improveperformance even for systems already in production; there is even less of a probability that youcan find it while the system is in plain development and you are not sure what the rest of thepieces will end up looking like. Using the IDE as the performance-testing environment can beequally 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, resolvea 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 willtake off some of the burden.
❑ If a class has too many members, maybe it can be restructured into a group of collaboratingclasses 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 restructuringof the problematic code.
When code is simple, it is easy to navigate — you don’t lose time in long debugging sessions in order to findthe 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 com-ments. Even after a short time spent with such code, you feel it does not hide any major mysteries. In simplewords, you are in control.
Keeping the Code ReadableProgramming is intellectually a very intense activity. You are often so immersed in your work that youtend to have a deep and detailed understanding of your creation in order to maintain complete controlover it. You may try to memorize every single detail of the code. You feel proud when you are able toimmediately correct a bug or change some behavior. After all, it is what makes you good in the workyou perform. As you become more productive, you develop strategies and gain your own programmingstyle. There is nothing wrong with being expert with the code you create, unless that expertise becomesthe only weapon you have in your arsenal.
Unfortunately, sometimes you can forget one important fact; when developing software you seldomwork alone. And in order to be able to work in a team, you must write code so it is easily comprehended
10
Part I: Introduction to Refactoring
79796c01.qxd:WroxPro 2/25/08 8:55 AM Page 10
by others. Others might need to modify, maintain, or optimize the code. In that case, if confronted withcryptic 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 waythat it is easy for others to understand can prove to be a much more difficult task. Ironically, you can findyourself in the “other person” role even with your own code. Your memory has its limits, and after awhile 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 bestbet is relying on natural language itself. You have to translate your decisions into code so they are easilyunderstandable from a reading of the code, not only visible as a consequence of code execution. Codebecomes 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, andaccepted conventions can increase the clarity of your code.
Reliance on comments and documentation can also affect the capacity of code to communicate with thereader. 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 readingfirst 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
If I have proved my point, you will find the second snippet more to your liking. In case you still are notconvinced, as an interesting experiment, you can try obfuscating your code with some obfuscation tooland then trying to find your way around it. Even with the smallest code base, it soon becomes impossibleto 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 asafe and systematic way and by letting you transform your code along well-known patterns andidioms — you use comments in a more profound manner. Strong structure in the code gives you confi-dence 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 thosearguments are well founded, let me first deal with some opinions often heard that are not very constructive.
Difficult to Read More Readable Code
Dim oXMLDom As _New DOMDocument40Dim oNodes As IXMLDOMNodeList‘loads the file into XMLDom objectoXMLDom.Load(App.Path + _“\ portfl.xml”)oNodes = _oXMLDom.selectNodes(“//stock[1]/*“)
Dim portfolio As _New DOMDocument40Dim stocks As IXMLDOMNodeList
portfolio.Load(App.Path + _“\portfl.xml”)stocks = portfolio.selectNodes(“//stock[1]/*“)
11
Chapter 1: Refactoring: What’s All the Fuss About?
79796c01.qxd:WroxPro 2/25/08 8:55 AM Page 11
Debunking Common MisconceptionsLike 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 Ifound those opinions so unfounded that I call them misconceptions. I feel it is worthwhile taking sometime to debunk them, because they can add confusion and can lead you astray from a quest to adoptthis 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 dearlyfor this knowledge. A small change can provoke software to break in a surprising manner and at theworst moment. So once you burn your hands you often become reluctant to make any change that is notabsolutely necessary. This can work well for a moment, but then a situation comes up where bugs haveto be resolved and petitions for new features cannot be evaded anymore. You are faced with the samecode you tried not to confront.
Those who adopt this “if it ain’t broke, don’t fix it” position look upon refactoring as unnecessary meddlingwith 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 donot have control over it.
Refactoring Is Nothing NewThis 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 soon, 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 aspect-oriented programming. What it does do is radically change the way you program: it defines rules thatmake it possible to apply complex transformations to code at the click of a button. You do not look at yourcode as some frozen construct that is not susceptible to change. Instead, you see yourself as capable ofmaintaining the code in optimum shape, responding efficiently to any new condition.
Refactoring Is Rocket ScienceProgramming is hard. It’s a complex activity that requires a lot of intellectual effort. Some of the knowl-edge can be very difficult to grasp. With Visual Basic .NET, VB programmers had to acquire the ability towork in a fully capable object-oriented language. For many, this was baffling at first. The good part is itdefinitely pays off.
The great thing about refactoring is how simple it can be. It equips you with a very small set of simplerules to start off. This, coupled with a good tool, makes first steps in refactoring a breeze. Compared toother techniques an advanced programmer should know nowadays, like UML or design patterns, I’dsay refactoring has the easiest learning curve, a lot like VB itself compared to other programming lan-guages. Very soon, the time spent in learning refactoring will start to reap rewards. Of course, as withany other thing in life, gaining mastery requires a lot of time and effort.
12
Part I: Introduction to Refactoring
79796c01.qxd:WroxPro 2/25/08 8:55 AM Page 12
Refactoring Causes Poor PerformanceA longer way to state this might be, “Because after refactoring you usually end up with a larger num-ber 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 tovoice initial skepticism toward object-oriented programming. The truth is that the differences betweenrefactored and unstructured code are, at best, minimal. Except in some very specialized systems, this isnot a concern.
Experience shows that performance flows are generally afflicted by some precise spots in code. Fixingthose during an optimization phase will get you the required levels of performance. Being able to easilyidentify the critical pieces of code can prove to be very valuable. By producing understandable code inwhich duplication and total size is minimized, refactoring greatly aids this task.
Refactoring Breaks Good Object-Oriented DesignWell-structured and refactored code can look awkward to an untrained eye. Methods are so short that theyoften seem without substance. Classes seem without enough weight, consisting of only a few members. Itseems 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 morecomplexity to deal with. This argument is actually misleading. The truth is that the same complexity wasalways present, only in refactored code it is expressed in such a cleaner, more structured way.
Refactoring Offers No Short-Term BenefitsRefactoring actually makes you program faster. So far, I do not know of any study that I could call uponin order to prove what I just said, but my own experience tells me this is the case. All the same, it is onlylogical that this is so. Because we have a smaller quantity of code overall, less duplication, and a clearerpicture, unless we are dealing with some trivial and unrealistically small scale code, benefits becomeapparent very soon.
Refactoring Works Only for Agile TeamsBecause it’s often mentioned as one of the pillar techniques in agile methodologies, refactoring is interpretedas working only for teams adhering to these principles.
Refactoring is indispensable for agile teams. Even if your team has a different methodology, most of thetime you are the one in charge charge of the way you code. Best results in refactoring are achieved if youadopt refactoring in small steps, performing it regularly while you code. Some practices, like strict codeownership or a waterfall process, can play against refactoring. If you can prove that refactoring makessense from a programming point of view, you can start building your support base, first with your peersand then by spreading the word to the rest of your team.
That dispenses with some of the common misconceptions surrounding refactoring. At this point, youmay be wondering how all of this relates to Visual Basic. That is the topic of the next section.
13
Chapter 1: Refactoring: What’s All the Fuss About?
79796c01.qxd:WroxPro 2/25/08 8:55 AM Page 13
Visual Basic and RefactoringIt is fair to say that in the Visual Basic community, refactoring has had a slow start. One of the main rea-sons for this was the lack of proper tool support. While some tools with refactoring capabilities appearedon the market some years ago, only recently did dedicated refactoring tools for VB appear. Lack of toolscoupled with lack of information and scarce literature suited for VB developers led to slow adoption ofthe technique. It seems, however, that in this case the developer community was ahead of the industrypolicy makers and commercial institutions. Refactoring support was voted the number-one desired fea-ture 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 VisualStudio 2005.
Visual Basic History and Legacy IssuesWhile 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. Inthis effort, refactoring can be a great help by providing the programmer with the tools for unobtrusivetransformation 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 basedon the BASIC programming language and the graphical environment for drawing GUI elements, it hasproven to be an easily accessible programming environment with a gradual learning curve. And thanks toMicrosoft’s policy of spreading the use of Visual Basic in other forms and other environments like WindowsScripting, 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 pro-gramming in Visual Basic. However, not until VB .NET did Visual Basic unleash the full power ofobject-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 thelanguage 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 syn-tax and make it more in the spirit of the .NET Framework. In the .NET Framework, all code gets compiledinto 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
Part I: Introduction to Refactoring
79796c01.qxd:WroxPro 2/25/08 8:55 AM Page 14
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 assembliesprogrammed in Visual Basic .NET. This is not so different from the interoperability provided by COM in thepre-.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 programminglanguages.
Along with market forces that are continuously moving Visual Basic forward in making it more powerfuland advanced so that it stays as efficient as other programming languages, some frictional forces stand inthe way of its progress. Its long history and success are the main reasons for keeping some of its older lan-guage elements that would normally be completely replaced with newer ones. With C# Microsoft had aclean slate for language design. With Visual Basic, it has to take into account a huge amount of existingcode that should be migrated and a great number of developers who need to upgrade their skills.
Legacy CodeThe huge popularity and widespread adoption of Visual Basic resulted in a great amount of pre-.NETcode still in production even today, almost six years after the advent of .NET. Migrating VB6 or priorcode to .NET is not a simple affair. While Microsoft provided an automated migration tool, this upgradecan not be realistically performed without user interaction. Code produced by the Migration Wizard willoften 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 upgradenecessities and backward-compatibility issues. But because a completely automated upgrade is notpossible, a number of features were kept in VB .NET in order to provide at least some possibility ofupgrade. 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 ProgrammingIn 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 ofemployment opportunities and chances for career advancement can be greatly reduced. VB programmerswere 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 “paradigmshift” has been produced. The changes are significant and go beyond a simple upgrade. These changesrequire new skills and new ways of thinking.
15
Chapter 1: Refactoring: What’s All the Fuss About?
79796c01.qxd:WroxPro 2/25/08 8:55 AM Page 15
However, because of many old elements that were kept in VB .NET, it is possible to program in VB .NETin the “old style,” just as in VB6 or prior. But if that way is used, no benefits are gained from the newplatform. This is not a new phenomenon in the history of programming. C++ and Java had their syntaxbased on C language syntax in order to attract and facilitate transfer of C programmers to these new lan-guages. However, it was soon noticed that a number of these programmers started using new tools andenvironments in the old style, relying on old constructs and design applicable to the old programminglanguage. With C++ and Java, this meant that programmers continued using procedural design insteadof relying on new, object-oriented design. Admittedly, in VB the gap is not that great. Even in VB6 youcan define a class, create an instance, or define and implement an interface. Nonetheless, using inheri-tance and generics and understanding the benefits of static typing are important challenges for someonecoming from a classic VB background, and surmounting these challenges requires a major shift in theway programming is approached.
Legacy Language ElementsSo these backward forces I’ve discussed led to a situation in which many of the obsolete language elementswere kept in VB .NET for the purpose of upgrades of existing code or with the intention of facilitating thetransition of programmers to a new platform.
Unfortunately, these features can just as easily be used in newly created VB .NET code, although thereare 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 RefactoringBuilding on the fundamentals of object-oriented theory, refactoring goes a step further than a traditionalapproach to programming does. With refactoring, it is easy to identify weak spots in your code and applysmall-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 othersmell, this legacy code smell can be dealt with through the refactoring process. By defining procedures thatcan transform legacy elements and fundamentally upgrade the code, refactoring can greatly alleviate issuesrelated to upgrade and make your old code fully capable and equally useful in the .NET world.
SummaryThis introductory chapter has given you a brief overview of refactoring, explaining its relevance andbenefits. You have seen how refactoring helps you design your applications and prevent design rot, andat 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 makethis process even more productive, you can rely on automated refactoring tools that take a lot of thedrudgery 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 thatyou might come across, just so you won’t be surprised by the diversity of opinions on the subject andcan make your own informed choices.
16
Part I: Introduction to Refactoring
79796c01.qxd:WroxPro 2/25/08 8:55 AM Page 16
In the second part of the chapter, I put Visual Basic into focus. A very popular and successful tool, it wasnot immune to changes and advances in the programming world. While it has evolved significantly, itslongevity 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 alsomeant that a lot of programmers continued to program in the legacy style, not reaping the benefits of theadvances 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 anddesign 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
Chapter 1: Refactoring: What’s All the Fuss About?
79796c01.qxd:WroxPro 2/25/08 8:55 AM Page 17
79796c01.qxd:WroxPro 2/25/08 8:55 AM Page 18
A First Taste of Refactoring
Before I go into the details of refactoring procedures, the theory and mechanics behind it, it is agood idea to start with a very simple, yet complete application in order to gain perspective on theprocess I am trying to describe. This way, as I start going into detail about specific refactoring orcode smells, you’ll have a better sense of where each of these elements of the refactoring processfits 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. Asthe application strives to respond to new requirements, flaws and imperfections in the design willmaterialize. I will identify the code smells and eliminate them as I progress. I’ll follow the refactor-ing process as I perform these modifications. As you have already seen, the refactoring processconsists of three steps:
1. Identifying the smell
2. Using specific refactoring to eliminate the smell
3. Executing tests to validate the changes
I’ll focus on the refactoring process, but I will explain and develop the complete application as I goalong. 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 ApplicationThe application I am going to develop belongs to the medical field. It starts out as an application forcalculating 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 19
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 requirementsand code grow in quantity. This is in accord with the principle of promoting the simplicity of the solution asits 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 appearrelated to the domain I am working on: Patient, Male Patient, Female Patient, etc. You’ll see how work onthe elimination of code smells and work on the design based on object-oriented principles converge into thecreation of an efficient and robust solution.
More complex, large-scale systems might benefit from a more elaborately designed solution right fromthe outset. In this case the application is so simple that any complex design up front would probablyindicate that I have overengineered the solution.
Calories Calculator ApplicationMy client, a doctor, needed an application to calculate a recommended daily amount of calories. He hadthe formula and could perform the calculation manually, but using a calculator is too tedious and error-prone. 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 formshown in Figure 2-1.
Figure 2-1
This single form contains patient data input controls and displays the recommended daily amount ofcalories in the Recommended Daily Amount read-only text box.
The code to calculate the recommended daily amount was not that complicated either. All that wasrequired was a click on the Calculate button and the following event-handling code that I wrote(Listing 2-1).
20
Part I: Introduction to Refactoring
79796c02.qxd:WroxPro 2/25/08 8:56 AM Page 20
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) ThenTxtCalories.Text = 66 + (6.3 * TxtWeight.Text) + _(12.9 * (TxtFeet.Text * 12 + TxtInches.Text)) - _(6.8 * TxtAge.Text)
ElseTxtCalories.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 Iwould need to add some user input-verification routines, or the program might display some unpleas-ant 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 valueIf 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 valueIf 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 patientTxtCalories.Text = 66 + (6.3 * TxtWeight.Text) + _(12.9 * (TxtFeet.Text * 12 + TxtInches.Text)) - _(6.8 * TxtAge.Text)
Continued
21
Chapter 2: A First Taste of Refactoring
79796c02.qxd:WroxPro 2/25/08 8:56 AM Page 21
Listing 2-2: Calories Calculator First Try (continued)
Else ‘Apply another formula for female patientTxtCalories.Text = 655 + (4.3 * TxtWeight.Text) + _(4.7 * (TxtFeet.Text * 12 + TxtInches.Text)) - _(4.7 * TxtAge.Text)
End IfEnd Sub
I have used the IsNumeric function in order to validate that values inputted by the user are numeric. Ifthe 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 thereis a lot of code mixed up in the single method, and that if the application starts to grow, I will probablyneed 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 WeightNot to my surprise, some time after submitting the application, I received an e-mail from my client ask-ing 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, calculationdepends 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 con-trols, as shown in Figure 2-2.
Figure 2-2
22
Part I: Introduction to Refactoring
79796c02.qxd:WroxPro 2/25/08 8:56 AM Page 22
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 theentered height value is less then five feet, a message box with information on the height limitwill be displayed, and the method will exit.
❑ I also noticed that I would need to clear old results before each calculation, because otherwisethey might confuse the user. I added a few lines of code that that clean text boxes each time theCalculate button is pressed. This way, even when invalid data are entered, results will be clearedbefore 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 resultsTxtCalories.Text = “” TxtIdealWeight.Text = “” TxtDistance.Text = “” ‘Validate user inputIf Not IsNumeric(TxtFeet.Text) Then
MsgBox(“Feet must be a numeric value!”)TxtFeet.Select()Return
End IfIf Not IsNumeric(TxtInches.Text) Then
MsgBox(“Inches must be a numeric value!”)TxtInches.Select()Return
End IfIf Not IsNumeric(TxtWeight.Text) Then
MsgBox( _“Weight must be a numeric value greater then zero!”)TxtWeight.Select()Return
End IfIf 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 weightIf (RbtnMale.Checked) Then
‘Calculates ideal weight for men according to formulaContinued
23
Chapter 2: A First Taste of Refactoring
79796c02.qxd:WroxPro 2/25/08 8:56 AM Page 23
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 newrequirement of calculating ideal weight.
❑ When applying the formula, I subtracted five feet from the feet value and then converted theresulting 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 result50 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 by2.2046 in order to convert it to pounds.
However, now you may notice that the sole method in my application (BtnCalculate_Click) hasbecome 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 require-ments to existing applications. This can easily lead to poorly structured and tangled code. While eventhis short sample could benefit from refactoring at this point, I used my own judgment, based on com-mercial 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 ofthe story!
Growing Requirements: Persisting Patient DataThe 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’llnotice that it provides no means of identifying the patient for whom the calculation is being performed. So
24
Part I: Introduction to Refactoring
79796c02.qxd:WroxPro 2/25/08 8:56 AM Page 24
for this requirement I would need to capture some data to use to identify the patient. I also need to persistthe 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 thepatient. I decided to use patient names and Social Security numbers (SSNs). Names make iteasy for the doctor to identify the patient, and the SSN prevents any possible mix-up in casepatients have the same names.
❑ Keeping the calculation operation and the operation of persisting patient data separate. At firstI was tempted to add some code to the existing BtnCalculate_Click event-handling routineand to use it to save patient data. But looking at it from the users’ perspective, I decided thatthese two actions should be separated. This way, the users will be able to perform some ad hoccalculations without having to input a patient’s name and SSN. Also, the users can save data onlywhen 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 dataand 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
Chapter 2: A First Taste of Refactoring
79796c02.qxd:WroxPro 2/25/08 8:56 AM Page 25
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 XMLfile, so I decided to do that. But before I dedicate myself to implementing new requirements, the timehas 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 Iwasn’t sure I was going to continue to work on the application. However, now that the application hasgrown 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 functionalityof the application. This method has grown out of proportion. Long methods are difficult to understandand reuse.
I’ll return to the issue of persisting patient data later in the chapter, but I want to deal first with the out-standing issues that can stand in the way of implementing the new functionality, and that requires somerefactoring. You can see that I haven’t really gone that far with implementation, but some smells havealready managed to creep inside the code. I will deal with these smells before I go any further.
Refactoring in ActionSo 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 canget more complicated; bearing in mind the interest the application created, it is an auspicious moment togive it some more thought.
If I continue to simply add new functionality, the application will soon grow out of control and becomeimpossible to maintain. I am going to reorganize the code so its internal structure is improved. I’ll do itwithout changing its behavior; the calorie calculation will happen just as it used to. If you remember thedefinition of refactoring (discussed thoroughly in Chapter 1), you’ll see that this is exactly what I plan todo with the Calories Calculator.
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 whencommented, long methods are much more difficult to understand. In refactoring terminology, thismethod displays a “long method smell.” The solution is to split it into several shorter methods.
Unit Testing: Since I am now starting to change the code in ways that can haveadverse effects and even introduce bugs, I need to make sure that application consis-tency is maintained at all times. I can do this by developing a set of tests. I will writedown a few imaginary patients’ data and the results of the calculations the applica-tion is performing. Then I will run the application each time I make a change to itand execute all those tests, making sure correct results are returned. I will also testfor invalid data. This sounds tedious, but it’s necessary. Fortunately, there is a muchbetter way to do this — as you’ll see in Chapter 3, in which I discuss unit testing.
26
Part I: Introduction to Refactoring
79796c02.qxd:WroxPro 2/25/08 8:56 AM Page 26
Decomposing the BtnCalculate_Click MethodOne 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’llsee that the code comments essentially segment the code into modules that indicate to you where youshould extract the code into the new methods. This will not change the behavior of the code, but it willmake the code better organized and make the BtnCalculate_Click method easier to read. I will movethe code into the new method and make BtnCalculate_Click call the new method. The next sectionsshow how I take each segment from the method and make that segment its own method.
The “Clear Old Results” SegmentThis 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 BooleanIf Not IsNumeric(TxtFeet.Text) Then
MsgBox(“Feet must be a numeric value!”)TxtFeet.Select()Return False
End IfIf Not IsNumeric(TxtInches.Text) Then
MsgBox(“Inches must be a numeric value!”)TxtInches.Select()Return False
End IfIf Not IsNumeric(TxtWeight.Text) Then
MsgBox( _“Weight must be a numeric value greater then zero!”)TxtWeight.Select()Return False
End IfIf 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 ftIf Not (TxtFeet.Text >= 5) Then
MsgBox(“Height has to be equals or greater than 5 feet!”)TxtFeet.Select()Return False
End IfReturn True
End Function
27
Chapter 2: A First Taste of Refactoring
79796c02.qxd:WroxPro 2/25/08 8:56 AM Page 27
This method needs to return the result of user input verification. In case the data are not valid, I need tointerrupt the execution of the BtnCalculate_Click method. You will see that approach when you takea 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 receivestwo parameters, actual weight and ideal weight:
Public Function DistanceFromIdealWeight( _ByVal actualWeightInPounds As Decimal, _ByVal idealWeightInPounds As Decimal) As Decimal
Return actualWeightInPounds - idealWeightInPoundsEnd Function
Notice that in order to avoid any confusion in regards to measurements used, I added the name of themeasurement to the parameter names — actualWeightInPounds and idealWeightInPounds.
Calculating Calories and Ideal Weight by GenderThe last remaining section is a little less self-explanatory than the others. The application will calculatecalories or ideal weight, but it does so according to gender. So the easiest way to approach this issue is toturn the section into four private methods:
Public Function DailyCaloriesRecommendedMan( _‘Parameter name includes measurement type info: pound, inchByVal 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.2046End Function
Public Function IdealBodyWeightWoman( _ ByVal heightInInches As Decimal) As Decimal
Return (45.5 + (2.3 * (heightInInches - 60))) * 2.2046End Function
Instead of using code comments to indicate the intention behind a few lines of code, I now use a methodto achieve the same goal. Method names clearly describe their purpose. This way the code is a lot easier tounderstand and reuse. As I move code to new methods, I eliminate the comments.
28
Part I: Introduction to Refactoring
79796c02.qxd:WroxPro 2/25/08 8:56 AM Page 28
The technique of decomposing large methods by extracting pieces of method into the new methods iscalled extract method refactoring. I discuss this refactoring in detail in Chapter 9 and Chapter 10.
The BtnCalculate_Click Method after Method ExtractionWhat 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 ReturnIf (RbtnMale.Checked) Then
TxtCalories.Text = DailyCaloriesRecommendedMan( _TxtWeight.Text, TxtFeet.Text * 12 + TxtInches.Text, _TxtAge.Text)TxtIdealWeight.Text = IdealBodyWeightMan(TxtAge.Text)
ElseTxtCalories.Text = DailyCaloriesRecommendedWoman( _TxtWeight.Text, TxtFeet.Text * 12 + TxtInches.Text, _TxtAge.Text)TxtIdealWeight.Text = IdealBodyWeightWoman(TxtAge.Text)
End IfTxtDistance.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 singlemethod did. The benefits of more granular code are improved clarity and greater possibility for reuse of new shorter methods.
Discovering New ClassesIn 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 com-prehension of code. So far, I didn’t even attempt to model the application code according to object-orientedprinciples. Instead, I have a single class inheriting System.Windows.Forms.Form, a base class for a basicGUI 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 bedivided into two distinct groups:
❑ Methods that are related to the behavior of the visual elements, the GUI, like the ClearResultsmethod
❑ Methods that embed the medical formulas that perform the calculations, likeIdealBodyWeightMan and DailyCaloriesRecommendedWoman
29
Chapter 2: A First Taste of Refactoring
79796c02.qxd:WroxPro 2/25/08 8:56 AM Page 29
So far the FrmCaloriesCalculator class is the only one in the project. It extends the Form class, abase class for windows or dialog boxes, which is a fundamental building block for the desktop inter-face. This class works well for our first group of methods, but the second group of methods, concernedwith medical calculations, clearly does not belong to it.
By running the Calories Calculator application, you can observe that its purpose is collecting and com-puting certain data. This data represents human characteristics. In this case, as you can observe on theform itself, this data refers to patients.
Defining the Patient ClassSince this application deals with a medical problem, it makes sense to structure the data inside a newclass called Patient. Listing 2-5 shows the class definition.
Listing 2-5: The New Patient Data Class
Public Class PatientPrivate ssnValue As StringPrivate firstNameValue As StringPrivate lastNameValue As StringPrivate heightInInchesValue As DecimalPrivate weightInPoundsValue As DecimalPrivate ageValue As IntegerPublic Property SSN() As String
GetReturn ssnValue
End GetSet(ByVal Value As String)
ssnValue = ValueEnd Set
End PropertyPublic Property FirstName() As String
GetReturn firstNameValue
End GetSet(ByVal Value As String)
firstNameValue = ValueEnd Set
End PropertyPublic Property LastName() As String
GetReturn lastNameValue
End GetSet(ByVal Value As String)
lastNameValue = ValueEnd Set
End PropertyPublic Property HeightInInches() As Decimal
GetReturn heightInInchesValue
End GetSet(ByVal Value As Decimal)
heightInInchesValue = Value
30
Part I: Introduction to Refactoring
79796c02.qxd:WroxPro 2/25/08 8:56 AM Page 30
Listing 2-5: The New Patient Data Class (continued)
End SetEnd PropertyPublic Property WeightInPounds() As Decimal
GetReturn weightInPoundsValue
End GetSet(ByVal Value As Decimal)
weightInPoundsValue = ValueEnd Set
End PropertyPublic Property Age() As Integer
GetReturn ageValue
End GetSet(ByVal Value As Integer)
ageValue = ValueEnd Set
End PropertyEnd Class
So far this class is a simple data structure, consisting only of properties. While grouping the data inside anew data type is definitely a step forward in attempting to improve the design of the application, havinga class that has only data and no behavior is another hint that I need to continue with improvements onthe design of the program. A class with only data and no behavior is referred to as a data class and is con-sidered a code smell.
Moving Methods to the Patient ClassIf you now go back to the second group of methods, the ones performing medical computations, you cansee 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 theyrefer 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 toillustrate 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
Chapter 2: A First Taste of Refactoring
79796c02.qxd:WroxPro 2/25/08 8:56 AM Page 31
is transformed into:
Public Function DailyCaloriesRecommendedWoman() As DecimalReturn 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 andproperty names coincided by chance. Normally this would require changing the body of the methods sothat they refer to properties and not method parameters. You can see how this works out in a more graphicway in Figure 2-4, where the DailyCaloriesRecommendedWoman method references the Height propertyin the same Patient class.
Figure 2-4
The Patient class is now a fully fledged class consisting of properties and methods, grouping the dataand related behavior inside a cohesive whole. I discuss Extract class and Move Method refactoring indetail in Chapter 11.
Narrowing the Patient Class InterfaceIf you have been paying attention so far, then you might have noticed that I didn’t treat a very impor-tant piece of data related to a patient in any special way. I am referring to gender. Unless I have thisinformation 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 Patientclass. 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 bodyweight: IdealBodyWeightMan and IdealBodyWeightWoman. So which one of the two should theDistanceFromIdealWeight method call?
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 '... Public Function DailyCaloriesRecommendedWoman() _ As Decimal Return 655 + (4.3 * WeightInPounds) + _ (4.7 * HeightInInches) - (4.7 * Age) End Function '...End Class
Parameters eliminated
HeightInInches references Patient class property
32
Part I: Introduction to Refactoring
79796c02.qxd:WroxPro 2/25/08 8:56 AM Page 32
Creating the Gender EnumI’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 enumeratorGender. This way, DistanceFromIdealWeight can check the patient’s gender and call the correct calcula-tion method for ideal body weight.
Public Enum GenderMaleFemale
End Enum
Adding a Gender Property to the Patient ClassIt is quite easy now to add a Gender property to the Patient class that is typed as Enum Gender. It willhelp discern a gender for each patient instance. Do what you usually do when you add a new propertyto 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 GenderGet
Return genderValueEnd GetSet(ByVal Value As Gender)
genderValue = ValueEnd Set
End Property
Moving DistanceFromIdealWeight to Patient ClassOnce the Gender enumerator is created and the Gender property is added, I can move theDistanceFromIdealWeight 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 DecimalIf Gender.Male Then
Return WeightInPounds - IdealBodyWeightMan()Else
Return WeightInPounds - IdealBodyWeightWoman()End If
End Function
The method now works fine.
33
Chapter 2: A First Taste of Refactoring
79796c02.qxd:WroxPro 2/25/08 8:56 AM Page 33
Now, before I modify FrmCaloriesCalculator so it uses the Patient class, I want take another look atthe methods in the new class. I suspect there’s more refactoring that can be performed here before I moveon. Since I now have gender data available to code in the class, I wonder if having different methods for themale and female patients is really necessary. Maybe I could move the gender logic inside the class some-how and release the client from the burden of tracking the gender information? Shouldn’t the Gender classbe able to manage gender-related logic on its own?
Putting Conditional Logic in the Patient ClassA lot of method names in the Patient class, like DailyCaloriesRecommendedMan andIdealBodyWeightWoman, 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 ThenidealWeight = myPatient. IdealBodyWeightMan()
ElseidealWeight = myPatient. IdealBodyWeightWoman()
End If
If you compare this to the DistanceFromIdealWeight method, you can see quite a different approachthere. The method itself contains this logic, because gender data are already available in the Patientclass. There are two obvious benefits to moving this conditional logic into the Patient class itself, aswith the DistanceFromIdealWeight method:
❑ There are fewer publicly visible methods in the Patient class, so things are a lot simpler foranyone using this class.
❑ Because classes are generally referenced from more than one place, moving conditional codeinside the method prevents a lot of duplication in the client code. No doubt anywhere thePatient 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 codeElse‘... female patient related codeEnd If
In short, I’m going to be better off if I transform all the methods along the same lines asDistanceFromIdealWeight. 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.
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 hasimproved greatly. You can now easily imagine the Patient class being used in anothermodule someone might develop, or even in some other, future application. This wasnot at all the case with a sole FrmCaloriesCalculator class.
34
Part I: Introduction to Refactoring
79796c02.qxd:WroxPro 2/25/08 8:56 AM Page 34
Encapsulating DailyCaloriesRecommendedMan andDailyCaloriesRecommendedWoman
First I’ll make the methods DailyCaloriesRecommendedMan and DailyCaloriesRecommendedWomanvisible through a single method, DailyCaloriesRecommended:
Public Function DailyCaloriesRecommended() As DecimalIf Gender.Male Then
Return DailyCaloriesRecommendedMan()Else
Return DailyCaloriesRecommendedWoman()End If
End Function
Encapsulating Ideal-Body-Weight CalculationThen the same fate awaits the ideal-body-weight methods. Methods IdealBodyWeightMan andIdealBodyWeightWoman are now visible through a single method, IdealBodyWeight:
Public Function IdealBodyWeight() As DecimalIf Gender.Male Then
Return IdealBodyWeightMan()Else
Return IdealBodyWeightWoman()End If
End Function
Making Encapsulated Methods PrivateFinally, I can’t forget to reduce the visibility of all the SomeFunctionMan and SomeFunctionWomanmethods to private. The declarations now look like this:
Private Function DailyCaloriesRecommendedMan() As DecimalPrivate Function DailyCaloriesRecommendedWoman() As DecimalPrivate Function IdealBodyWeightMan() As DecimalPrivate Function IdealBodyWeightWoman() As Decimal
With those declarations in place, I have managed to further encapsulate the logic inside the Patientclass. This way no unnecessary logic is exposed to the Patient class client. As you know, encapsulationis one of the pillars of object orientation, and taking this step will make your code more robust.
Restructuring the DistanceFromIdealWeight MethodI still have one more method to deal with. The DistanceFromIdealWeight code can be written in a moreexpressive manner, so it is easier to read and understand. While containing the same condition, and in thatsense being similar to other methods, DistanceFromIdealWeight does not yet use private methods. I willadd two private methods, DistanceFromIdealWeightMan and DistanceFromIdealWeightWoman, struc-turing them just like the methods DailyCaloriesRecommended and IdealBodyWeight:
Private Function DistanceFromIdealWeightMan() As DecimalReturn WeightInPounds - IdealBodyWeightMan()
End Function
35
Chapter 2: A First Taste of Refactoring
79796c02.qxd:WroxPro 2/25/08 8:56 AM Page 35
Private Function DistanceFromIdealWeightWoman() As DecimalReturn WeightInPounds - IdealBodyWeightWoman()
End Function
DistanceFromIdealWeight now looks like this:
Public Function DistanceFromIdealWeight() As DecimalIf 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 othermethods, and you will see the benefits of this modification later in the chapter, when I create the Patientclass 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 ClassNow I can modify the FrmCaloriesCalculator and make it use the Patient class for all related calcu-lations. 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 PatientGet
Return patientValueEnd GetSet(ByVal Value As Patient)
patientValue = ValueEnd Set
End Property‘ ...
This instance is created in the BtnCalculate_Click event-handling routine. I’ll add the following lineto it:
Me.Patient = New Patient
Now I need to set Patient’s properties:
If (RbtnMale.Checked) ThenMe.Patient.Gender = Gender.Male
ElseMe.Patient.Gender = Gender.Female
End IfPatient.HeightInInches = TxtHeight.Text
36
Part I: Introduction to Refactoring
79796c02.qxd:WroxPro 2/25/08 8:56 AM Page 36
Patient.Weight() = TxtWeight.TextPatient.Age = TxtAge.Text
Next, the Patient instance is used to perform the required calculations:
TxtCalories.Text = Patient.DailyCaloriesRecommendedTxtIdealWeight.Text = Patient.IdealBodyWeightTxtDistance.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 classMe.Patient = New Patient‘Setting patient properties with data from formIf (RbtnMale.Checked) Then
Me.Patient.Gender = Gender.MaleElse
Me.Patient.Gender = Gender.FemaleEnd IfPatient.HeightInInches = _(TxtFeet.Text * 12) + TxtInches.TextPatient.WeightInPounds() = TxtWeight.TextPatient.Age = TxtAge.Text‘Outputting calculated values to formTxtCalories.Text = Patient.DailyCaloriesRecommendedTxtIdealWeight.Text = Patient.IdealBodyWeightTxtDistance.Text = Patient.DistanceFromIdealWeight
End Sub
By extracting the Patient class and moving the logic related to medical calculation to this newly createdclass, I have laid the foundations of object-oriented design in the application. I have moved from an exclu-sively event-driven paradigm to an object-oriented paradigm. While solely event-driven programming canbe effective on the small scale, it has to be coupled with object-oriented design to be as effective for morecomplex applications.
Creating the Patient Class HierarchyNow that I have made all the preceding important steps in redesigning the application, it is time to takeanother look at the code I ended up with. After the latest changes, a certain uniformity in all of the methodsis becoming apparent. They all contain the same conditional code:
If Gender.Male Then‘Some code
Else‘Some code
End If
37
Chapter 2: A First Taste of Refactoring
79796c02.qxd:WroxPro 2/25/08 8:56 AM Page 37
In effect, this code — condition tests for a very specific attribute of Patient, the gender — has beenduplicated. I might as well have used copy-paste to create it. This is a very good indication that somefurther refinements are required. I have just identified another major code smell. Code duplication gener-ally indicates a serious flaw in your design: you wrote the same code more than once, meaning that ifyou need to modify this logic, you will need to find all occurrences of it in your code. It is all too easy tomiss other occurrences when the section that is of interest at that moment is modified. It also means thatyou 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 andFemalePatient. They will inherit the Patient class. Then I’ll move all gender-specific code to eitherMalePatient or FemalePatient, as appropriate. (You can take a look at Chapter 12, where I deal withExtract 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 MalePatientInherits Patient
and
Public Class FemalePatientInherits Patient
2. The next step involves moving private methods with names ending with Male to the MalePatientclass, and private methods with names ending with Female to the FemalePatient class. This is anexample of pull-down method refactoring. Pulling down refers to moving a method from a class inthe 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 imple-mented, 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 FemalePatientneed to be marked as abstract by means of the MustOverride keyword. Once these methodsare 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 codespecific to the appropriate gender.
38
Part I: Introduction to Refactoring
79796c02.qxd:WroxPro 2/25/08 8:56 AM Page 38
Dealing with Gender-Specific MethodsNow, in the MalePatient and FemalePatient subclasses, I still have methods — methods that provideimplementation for abstract methods in the Patient class — ending with the words Man and Woman. It istime to change this. I will eliminate Man from method names in the MalePatient class and Woman frommethod names in the FemalePatient class. This way, the names of the methods in MalePatient andFemalePatient will coincide with method names in the Patient class. I’ll also change the visibility ofthe methods to public. Also, at this point I am getting a timely reminder from our IDE, visible in the TaskList 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 MustOverrideFunction 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 allthe methods in the MalePatient and FemalePatient classes (see Figure 2-5).
Figure 2-5
So I started with some conditional code that was gender-specific and managed to replace it with aninheritance hierarchy. This kind of refactoring is called replacing conditional logic with polymorphism. Youcan take a look at Chapter 12 for more on polymorphism.
Public MustInherit Class Patient '... Public MustOverride Function DailyCaloriesRecommended() _ As Decimal
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 Class FemalePatient Inherits Patient
Public Overrides Function DailyCaloriesRecommended() _ As Decimal Return 655 + (4.3 * WeightInPounds) + _ (4.7 * HeightInInches) - (4.7 * Age) End Function
'...
If Patient Male, methodimplemented in
MalePatient
If Patient Female, methodimplemented in FemalePatient
39
Chapter 2: A First Taste of Refactoring
79796c02.qxd:WroxPro 2/25/08 8:56 AM Page 39
Removing Gender-Specific Code from the DistanceFromIdealWeight Method
One more step is needed to eliminate the last error present in the code. Since theDistanceFromIdealWeight method refers to IdealBodyWeightMan in the MalePatientclass 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 DecimalReturn WeightInPounds - IdealBodyWeight()
End Function
Looking at the Patient Classes HierarchyNow 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 FemalePatientInherits Patient
Public Overrides Function DailyCaloriesRecommended() As DecimalReturn 655 + (4.3 * WeightInPounds) + _(4.7 * HeightInInches) - (4.7 * Age)
End Function
Public Overrides Function IdealBodyWeight() As DecimalReturn (45.5 + (2.3 * (HeightInInches - 60))) * 2.2046
End FunctionEnd Class
Public Class MalePatientInherits Patient
Public Overrides Function DailyCaloriesRecommended() As DecimalReturn 66 + (6.3 * WeightInPounds) + _(12.9 * HeightInInches) - (6.8 * Age)
End Function
Public Overrides Function IdealBodyWeight() As DecimalReturn (50 + (2.3 * (HeightInInches - 60))) * 2.2046
End FunctionEnd Class
Public MustInherit Class PatientPrivate ssnValue As StringPrivate firstNameValue As String
40
Part I: Introduction to Refactoring
79796c02.qxd:WroxPro 2/25/08 8:56 AM Page 40
Listing 2-7: Patient Classes Hierarchy (continued)
Private lastNameValue As StringPrivate heightInInchesValue As DecimalPrivate weightInPoundsValue As DecimalPrivate ageValue As IntegerPrivate genderValue As Gender
Public Property Gender() As GenderGet
Return genderValueEnd GetSet(ByVal Value As Gender)
genderValue = ValueEnd Set
End PropertyPublic Property SSN() As String
GetReturn ssnValue
End GetSet(ByVal Value As String)
ssnValue = ValueEnd Set
End PropertyPublic Property FirstName() As String
GetReturn firstNameValue
End GetSet(ByVal Value As String)
firstNameValue = ValueEnd Set
End PropertyPublic Property LastName() As String
GetReturn lastNameValue
End GetSet(ByVal Value As String)
lastNameValue = ValueEnd Set
End PropertyPublic Property HeightInInches() As Decimal
GetReturn heightInInchesValue
End GetSet(ByVal Value As Decimal)
heightInInchesValue = ValueEnd Set
End PropertyPublic Property WeightInPounds() As Decimal
GetReturn weightInPoundsValue
End GetContinued
41
Chapter 2: A First Taste of Refactoring
79796c02.qxd:WroxPro 2/25/08 8:56 AM Page 41
Listing 2-7: Patient Classes Hierarchy (continued)
Set(ByVal Value As Decimal)weightInPoundsValue = Value
End SetEnd PropertyPublic Property Age() As Integer
GetReturn ageValue
End GetSet(ByVal Value As Integer)
ageValue = ValueEnd Set
End Property
Public MustOverride Function DailyCaloriesRecommended() _As Decimal
Public MustOverride Function IdealBodyWeight() As Decimal
Public Function DistanceFromIdealWeight() As DecimalReturn WeightInPounds - IdealBodyWeight()
End FunctionEnd Class
This is a large listing, but notice that no comments are really necessary. The design is simple and theintent 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 HierarchyTo complete the latest changes I now need to modify the client, the FrmCaloriesCalculator class. Inthe BtnCalculate_Click method I should create the correct subclass of the Patient hierarchy, basedon 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 FemalePatientClasses 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 instanceIf (RbtnMale.Checked) Then
Me.Patient = New MalePatientElse
Me.Patient = New FemalePatientEnd IfPatient.HeightInInches = _(TxtFeet.Text * 12) + TxtInches.TextPatient.WeightInPounds() = TxtWeight.TextPatient.Age = TxtAge.Text
42
Part I: Introduction to Refactoring
79796c02.qxd:WroxPro 2/25/08 8:56 AM Page 42
Listing 2-8: The BtnCalculate_Click Method, After the MalePatient and FemalePatientClasses Are Identified (continued)
TxtCalories.Text = Patient.DailyCaloriesRecommendedTxtIdealWeight.Text = Patient.IdealBodyWeightTxtDistance.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 thePatient 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’lleliminate the Gender enumerator in addition to the Gender property. I call this eliminate dead code refac-toring. 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 FemalePatientand MalePatient classes, I realized one detail. Both methods ended up looking the same:
Public Overrides Function DistanceFromIdealWeight() As DecimalReturn WeightInPounds - IdealBodyWeight()
End Function
This is actually duplicate code, a smell we must try to eliminate. The solution is to move the methodimplementation back to the Patient class. I’ll eliminate the method from the FemalePatient andMalePatient classes and copy it back to the Patient class:
Public Function DistanceFromIdealWeight() As DecimalReturn WeightInPounds - IdealBodyWeight()
End Function
Pulling methods and properties up in the hierarchy is another type of refactoring that is very useful ineliminating 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 theGUI-related logic. I have also grouped patient data inside a single structure, and this is exactly the data Ineed to persist. So all this work has prepared a good starting point for the implementation of persistencefunctionality in the application, which is the subject of the next section.
Implementing the Persistence FunctionalityGenerally, 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 wasvery simple, and I was postponing refactoring for a while. So the smells accumulated, and I had todedicate more time to refactoring procedures in a single go. Normally, refactoring is an integral part of the coding process.
43
Chapter 2: A First Taste of Refactoring
79796c02.qxd:WroxPro 2/25/08 8:56 AM Page 43
By any account, the time used to refactor the application was well spent. Now the code is better organizedand a lot easier to read, comprehend, and maintain. Since it is more modular, changes I apply from now onwill be more isolated and will have a more limited effect. Also, I have managed to group and gather thedata related to the patient — again, exactly the data I need to persist — into a single structure, simplifyingthe job of implementing this functionality.
The client has given me a blank slate as to how to implement the persistence functionality, and there are anumber 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 anXML file. This will save me some work during the implementation, and given that XML is also easily readableby humans, it can save me even more work in implementing GUI elements for viewing the data.
Saving the DataAs usual, I start off by filling in the event-handling methods. Remembering the recent experience I hadwith the application, I decide to put data-validation code in a separate ValidatePatientPersonalDataprivate method, much like the existing ValidateUserInput method.
Validating a Patient’s Personal DataThis ValidatePatientPersonalData method is called from the BtnSave_Click method, and it checksthat a valid Social Security number and last and first name are entered. It looks just as you would expect:
Private Function ValidatePatientPersonalData() As BooleanIf Not IsNumeric(TxtSSNFirstPart.Text) Or _Not IsNumeric(TxtSSNSecondPart.Text) Or _Not IsNumeric(TxtSSNThirdPart.Text) Then
MsgBox(“You must enter valid SSN!”)Return False
End IfIf Not TxtFirstName.Text.Trim.Length > 0 Then
MsgBox(“You must enter patient’s first name!”)Return False
End IfIf Not TxtLastName.Text.Trim.Length > 0 Then
MsgBox(“You must enter patient’s last name!”)Return False
End IfReturn True
End Function
Next I must make sure that the calculation has been performed before I save the measurement. For thatpurpose it’s easiest if I directly call the BtnCalculate_Click method. It will check the validity of thedata, 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 sepa-rate XML file, I must create the file programmatically. So, when saving the data, first I must check for theexistence of the XML file. If it exists, I want to append the data so the existing history is not lost. In thiscase I also need to look for the previous measurements by the same patient. It is best if I structure theXML so all measurements by one patient are under the same patient element.
44
Part I: Introduction to Refactoring
79796c02.qxd:WroxPro 2/25/08 8:56 AM Page 44
For that purpose I must define the format of the XML file. The format I am going to use is shown inListing 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”><height>72</height><weight>210</weight><age>23</age><dailyCaloriesRecommended>1043.4</dailyCaloriesRecommended><idealBodyWeight>205.37988</idealBodyWeight><distanceFromIdealWeight>4.62012</distanceFromIdealWeight><!--Another measurement -->
</measurement></patient><!--Another patient -->
</patientsHistory>
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 inListing 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 ThenBtnCalculate_Click(Nothing, Nothing)
End If
Dim document As New XmlDocumentDim fileCreated As Boolean = TrueTry
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 continuefileCreated = False
End Try If Not fileCreated Then
document.LoadXml(“<patientsHistory>” + _“<patient ssn=”“” + patientValue.SSN + “”“” + _“ firstName=”“” + patientValue.FirstName + “”“” + _“ lastName=”“” + patientValue.LastName + “”“>” + _
“<measurement date=”“” + DateTime.Today + “”“>” + _“<height>” + patientValue.HeightInInches.ToString + _“</height>” + _
Continued
45
Chapter 2: A First Taste of Refactoring
79796c02.qxd:WroxPro 2/25/08 8:56 AM Page 45
Listing 2-10: The BtnSave_Click Method Containing Persistence-Related Code (continued)
“<weight>” + patientValue.WeightInPounds.ToString + _“</weight>” + _“<age>” + patientValue.Age.ToString + “</age>” + _“<dailyCaloriesRecommended>” + _patientValue.DailyCaloriesRecommended.ToString + _“</dailyCaloriesRecommended>” + _“<idealBodyWeight>” + _patientValue.IdealBodyWeight.ToString + _“</idealBodyWeight>” + _“<distanceFromIdealWeight>” + _patientValue.DistanceFromIdealWeight.ToString + _“</distanceFromIdealWeight>” + _“</measurement>” + _“</patient>” + _“</patientsHistory>”)
Else‘Search for existing node for this patientDim 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 newDim thisPatient As XmlNode = _document.DocumentElement. _FirstChild.CloneNode(True)thisPatient.Attributes(“ssn”).Value = _patientValue.SSNthisPatient.Attributes(“firstName”).Value = _patientValue.FirstNamethisPatient.Attributes(“lastName”).Value = _patientValue.LastNameDim measurement As XmlNode = _thisPatient.FirstChildmeasurement.Attributes(“date”).Value = _DateTime.Todaymeasurement.Item(“height”).FirstChild.Value = _patientValue.HeightInInchesmeasurement.Item(“weight”).FirstChild.Value = _
46
Part I: Introduction to Refactoring
79796c02.qxd:WroxPro 2/25/08 8:56 AM Page 46
Listing 2-10: The BtnSave_Click Method Containing Persistence-Related Code (continued)
patientValue.WeightInPoundsmeasurement.Item(“age”).FirstChild.Value = _patientValue.Agemeasurement.Item(“dailyCaloriesRecommended”). _FirstChild.Value = _patientValue.DailyCaloriesRecommendedmeasurement.Item(“idealBodyWeight”). _FirstChild.Value = patientValue.IdealBodyWeightmeasurement.Item(“distanceFromIdealWeight”). _FirstChild.Value = _patientValue.DistanceFromIdealWeightdocument.FirstChild.AppendChild(thisPatient)
Else‘If patient node found just clone any measurement‘and use it for the new measurementDim measurement = patientNode.FirstChild. _ CloneNode(True) measurement.Attributes(“date”).Value = _DateTime.Todaymeasurement.Item(“height”).FirstChild.Value = _patientValue.HeightInInchesmeasurement.Item(“weight”).FirstChild.Value = _patientValue.WeightInPoundsmeasurement.Item(“age”).FirstChild.Value = _patientValue.Agemeasurement.Item(“dailyCaloriesRecommended”). _FirstChild.Value = _patientValue.DailyCaloriesRecommendedmeasurement.Item(“idealBodyWeight”). _FirstChild.Value = patientValue.IdealBodyWeightmeasurement.Item(“distanceFromIdealWeight”). _FirstChild.Value = _patientValue.DistanceFromIdealWeightpatientNode.AppendChild(measurement)
End IfEnd If‘Finally, save the xml to filedocument.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 FileNotFoundExceptionis caught and a flag set that indicates that this is probably the first time a patient history is to be saved. Inthat case the XML structure is created by concatenating and loading a string representing the XML structureas a way to get around the rather verbose DOM API.
47
Chapter 2: A First Taste of Refactoring
79796c02.qxd:WroxPro 2/25/08 8:56 AM Page 47
In case the file exists, the code searches for this specific patient entry by comparing Social Security num-bers. If the patient entry is not present, it is created by the cloning of any patient’s node, thus saving thetrouble 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 avery, very long method. I dare say this is an excellent specimen of what is generally better known by thename spaghetti code. So, before I am told to change my career in the direction of Italian cuisine, I had bet-ter do something about refactoring this method.
Method Decomposition RevisitedIn studying the method, one thing that might really bother your eye is that there are two completelyidentical pieces of code involved in setting measurement values to XmlNode. This code can be movedinto a new method called SetMeasurementValues:
Private Function SetMeasurementValues( _ByVal measurement As XmlNode) As XmlNode
measurement.Attributes(“date”).Value = _DateTime.Todaymeasurement.Item(“height”).FirstChild.Value = _patientValue.HeightInInchesmeasurement.Item(“weight”).FirstChild.Value = _patientValue.WeightInPoundsmeasurement.Item(“age”).FirstChild.Value = _patientValue.Agemeasurement.Item(“dailyCaloriesRecommended”). _FirstChild.Value = _patientValue.DailyCaloriesRecommendedmeasurement.Item(“idealBodyWeight”). _FirstChild.Value = patientValue.IdealBodyWeightmeasurement.Item(“distanceFromIdealWeight”). _FirstChild.Value = _patientValue.DistanceFromIdealWeight
End Function
Also, I can further decompose the BtnSave_Click method, much as I did BtnCalculate_Click. Newmethods 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 XmlDocumentDim document As New XmlDocumentdocument.Load(System.Reflection.Assembly. _GetExecutingAssembly.Location.Replace( _“DoctorsOrders.exe”, “patientHistory.xml”))Return document
End Function
Private Function CreateXmlDocumentFirstTime() As XmlDocument
48
Part I: Introduction to Refactoring
79796c02.qxd:WroxPro 2/25/08 8:56 AM Page 48
Listing 2-11: Decomposing the BtnSave_Click Method (continued)
Dim document As New XmlDocumentdocument.LoadXml(“<patientsHistory>” + _
“<patient ssn=”“” + patientValue.SSN + “”“” + _“ firstName=”“” + patientValue.FirstName + “”“” + _“ lastName=”“” + patientValue.LastName + “”“>” + _
“<measurement date=”“” + DateTime.Today + “”“>” + _“<height>” + patientValue.HeightInInches.ToString + _
“</height>” + _“<weight>” + patientValue.WeightInPounds.ToString + _“</weight>” + _“<age>” + patientValue.Age.ToString + “</age>” + _“<dailyCaloriesRecommended>” + _patientValue.DailyCaloriesRecommended.ToString + _“</dailyCaloriesRecommended>” + _“<idealBodyWeight>” + _patientValue.IdealBodyWeight.ToString + _“</idealBodyWeight>” + _“<distanceFromIdealWeight>” + _patientValue.DistanceFromIdealWeight.ToString + _“</distanceFromIdealWeight>” + _“</measurement>” + _“</patient>” + _
“</patientsHistory>”)Return document
End Function
Private Function FindPatientNode(ByVal document _As XmlDocument) As XmlNode
Dim patientNode As XmlNode = NothingFor Each node As XmlNode In document.FirstChild.ChildNodes
For Each attrib As XmlAttribute In node.Attributes‘We will use SSN to uniquely identify patientIf (attrib.Name = “ssn” And _attrib.Value = patientValue.SSN) Then
patientNode = nodeEnd If
NextNextReturn patientNode
End Function
Private Function AddNewPatient(ByVal document _As XmlDocument) As XmlDocument
‘just clone any patient node and use it for newDim thisPatient As XmlNode = _document.DocumentElement.FirstChild.CloneNode(True)thisPatient.Attributes(“ssn”).Value = _patientValue.SSNthisPatient.Attributes(“firstName”).Value = _patientValue.FirstNamethisPatient.Attributes(“lastName”).Value = _patientValue.LastName
Continued
49
Chapter 2: A First Taste of Refactoring
79796c02.qxd:WroxPro 2/25/08 8:56 AM Page 49
Listing 2-11: Decomposing the BtnSave_Click Method (continued)
Dim measurement As XmlNode = _thisPatient.FirstChildmeasurement = 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 twoidentical pieces of code into the method SetMeasurementValues, I have managed to eliminate duplica-tion in the code and reduce total line count in a significant way.
Finding the Right Place for MethodsAs in the aftermath of the BtnCalculate_Click method decomposition, I’m getting the feeling thatthese data-persistence methods do not really belong to the FrmCaloriesCalculator class. Rather, theyare interested in patient data, so at this point I am tempted to move them to a Patient class. So what doyou think — is that the right move at this moment? Or are there arguments against moving newly cre-ated LoadPatientHistoryFile, CreateXmlDocumentFirstTime, FindPatientNode, andAddNewPatient methods to the Patient class?
The Patient class is performing medical calculations. So far, it has nothing to do with managing themeasurements of different patients. If I were to move these new methods to the Patient class, it wouldunnecessarily encumber this class with persistence code. Some future client of our Patient class, inter-ested in the calculation that this class provides, might prefer persisting data in some other form — adatabase, for example. If I were to move XML persistence methods to the Patient class I would beobliged to deliver this functionality as well. Now imagine I discover a bug in my persistence code. Iwould 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 ClassAs the arguments in the previous section show, I need a new class in the project. I’ll call itPatientHistoryXMLStorage, and I’ll move all the new persistence-related methods fromFrmCaloriesCalculator 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 referencea 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 willform part of the PatientHistoryXMLStorage state. So I’ll go for that solution.
50
Part I: Introduction to Refactoring
79796c02.qxd:WroxPro 2/25/08 8:56 AM Page 50
While I’m at it, there is another parameter that is repeated in quite a few methods. It is the documentparameter. Again, I can make this parameter a field of the PatientHistoryXMLStorage class. In order tokeep names consistent, I change the name to documentValue. FindPatientNode needs to be adjusted soit references documentValue instead of document.
In addition to this change, AddNewPatient, LoadPatientHistoryFile, andCreateXmlDocumentFirstTime 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 PatientPrivate documentValue As XmlDocument = New XmlDocument
All I need at this point is the public method in the PatientHistoryXMLStorage class that the clientcode 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 toPatientHistoryXMLStorage:
Public Function Save(ByVal patient As Patient)patientValue = patientDim fileCreated As Boolean = TrueTry
LoadPatientHistoryFile()Catch noFile As IO.FileNotFoundException
fileCreated = FalseEnd TryIf 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 newDim measurement As XmlNode = _patientNode.FirstChild.CloneNode(True)measurement = SetMeasurementValues(measurement)patientNode.AppendChild(measurement)
End IfEnd IfdocumentValue.Save(System.Reflection.Assembly. _GetExecutingAssembly.Location.Replace( _“DoctorsOrders.exe”, “patientHistory.xml”))
End Function
51
Chapter 2: A First Taste of Refactoring
79796c02.qxd:WroxPro 2/25/08 8:56 AM Page 51
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 ReturnBtnCalculate_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 theFrmCaloriesCalculator 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 simplygoing to open the Internet Explorer instance, pass the location of the patientHistory.xml file to it, andlet it display the file content. If I wanted to improve on this, I could write my own XSL template and visual-ize the data by using Internet Explorer’s built-in XSL engine, but at this point I’ll stick with IE’s defaultXML 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 thePatientHistoryXMLStorage class, I programmed the application so it saves the XML file next to aCalories Calculator exe. I could easily copy-paste this line, but this would mean creating a duplicateentry 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 toaccess 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
Part I: Introduction to Refactoring
79796c02.qxd:WroxPro 2/25/08 8:56 AM Page 52
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
TryShell(IEExeFile + “ “ _+ PatientHistoryXMLStorage.patientHistoryXmlFile, _AppWinStyle.NormalFocus)
Catch noFile As IO.FileNotFoundExceptionMsgBox(“Internet Explorer not found”)
End TryEnd Sub
As you can see in the preceding code, and going along the same lines, I also replaced the Internet Explorerexecutable 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 nota number that got replaced — I replaced a file location string literal — it is still just a variation of thisimportant type of refactoring. Check out Chapter 9 for more background on replace magic literal withconstant 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.XmlImports System.IO
Public Class PatientHistoryXMLStorage
Private patientValue As PatientPrivate documentValue As XmlDocument = New XmlDocumentPublic 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.Todaymeasurement.Item(“height”).FirstChild.Value = _patientValue.HeightInInchesmeasurement.Item(“weight”).FirstChild.Value = _patientValue.WeightInPoundsmeasurement.Item(“age”).FirstChild.Value = _patientValue.Age
Continued
53
Chapter 2: A First Taste of Refactoring
79796c02.qxd:WroxPro 2/25/08 8:56 AM Page 53
Listing 2-12: The Final Version of the PatientHistoryXMLStorage Class (continued)
measurement.Item(“dailyCaloriesRecomended”). _FirstChild.Value = _patientValue.DailyCaloriesRecomendedmeasurement.Item(“idealBodyWeight”). _FirstChild.Value = patientValue.IdealBodyWeightmeasurement.Item(“distanceFromIdealWeight”). _FirstChild.Value = _patientValue.DistanceFromIdealWeightReturn 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 + “”“>” + _“<height>” + patientValue.HeightInInches.ToString + _
“</height>” + _“<weight>” + patientValue.WeightInPounds.ToString + _“</weight>” + _“<age>” + patientValue.Age.ToString + “</age>” + _“<dailyCaloriesRecomended>” + _patientValue.DailyCaloriesRecomended.ToString + _“</dailyCaloriesRecomended>” + _“<idealBodyWeight>” + _patientValue.IdealBodyWeight.ToString + _“</idealBodyWeight>” + _“<distanceFromIdealWeight>” + _patientValue.DistanceFromIdealWeight.ToString + _“</distanceFromIdealWeight>” + _“</measurement>” + _“</patient>” + _
“</patientsHistory>”)End Sub
Private Function FindPatientNode() As XmlNodeDim patientNode As XmlNode = NothingFor Each node As XmlNode In documentValue.FirstChild.ChildNodes
For Each attrib As XmlAttribute In node.Attributes‘We will use SSN to uniquely identify patientIf (attrib.Name = “ssn” And _attrib.Value = patientValue.SSN) Then
patientNode = nodeEnd If
NextNextReturn patientNode
54
Part I: Introduction to Refactoring
79796c02.qxd:WroxPro 2/25/08 8:56 AM Page 54
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 newDim thisPatient As XmlNode = _documentValue.DocumentElement.FirstChild.CloneNode(True)thisPatient.Attributes(“ssn”).Value = _patientValue.SSNthisPatient.Attributes(“firstName”).Value = _patientValue.FirstNamethisPatient.Attributes(“lastName”).Value = _patientValue.LastNameDim measurement As XmlNode = _thisPatient.FirstChildmeasurement = SetMeasurementValues(measurement)documentValue.FirstChild.AppendChild(thisPatient)
End Sub
Public Sub Save(ByVal patient As Patient)patientValue = patientDim fileCreated As Boolean = TrueTry
LoadPatientHistoryFile()Catch noFile As IO.FileNotFoundException
fileCreated = FalseEnd TryIf 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 newDim measurement As XmlNode = _patientNode.FirstChild.CloneNode(True)measurement = SetMeasurementValues(measurement)patientNode.AppendChild(measurement)
End IfEnd IfdocumentValue.Save(patientHistoryXmlFile)
End SubEnd Class
It is a rather long listing, but it is here for a purpose. Before this book ends, I will have another go at thiscode from the Visual Basic 2008 perspective. (If you can’t wait to see what happens to this code in the VB2008 version, take a look at Chapter 15.) With this, I have concluded implementing the latest requirementsof the Calories Calculator application.
55
Chapter 2: A First Taste of Refactoring
79796c02.qxd:WroxPro 2/25/08 8:56 AM Page 55
Calories Calculator, Refactored VersionNow it’s time to take a look at the new structure of the application. A class diagram (Figure 2-6) can giveyou a good overview of the code I ended up with.
Figure 2-6
I now have a lot more classes and methods than when I started. The impression is that the code is morebalanced and better organized, with each class dedicated to a single and specific purpose. I have alsoused inheritance, possibly opening the door for future extensions.
What does this tell you? What could you observe during the process of developing this sample applica-tion? Have I managed to improve the code as I implemented the requirements? What are the benefits ofall 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
Refactoring Technique Result
Extract method Atomic, more reusable methods with clear purpose
Extract class Single-purpose classes improve reuse, maintainability, andreadability 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
FemalePatient
+DailyCaloriesRecommended+IdealBodyWeight()
MalePatient
+DailyCaloriesRecommended+IdealBodyWeight()
FrmCaloriesCalculator
-BtnCalculate_Click()-BtnSave_Click()-BtnView_Click()-ValidateUserInput()-ValidatePatientPersonalData()-ClearResults()
Patient+SSN+FirstName+LastName+HeightInInches+Weight+Age+DailyCaloriesRecommended+IdealBodyWeight()+DistanceFromIdealWeight()
PatientHistoryXMLStorage+Document+Save(in patient : Patient)+LoadPatientHistoryFile()+CreateXMLDocumentFirstTime()+AddNewPatient()+FindPatientNode()+SetMeasurement()
56
Part I: Introduction to Refactoring
79796c02.qxd:WroxPro 2/25/08 8:56 AM Page 56
Table 2-1: Refactoring Techniques Applied and Benefits Obtained (continued)
❑ Initially I decomposed the long methods; this improved the code in several ways. By givingexplicit names to pieces of code I improved the readability of the code and made it simpler. It iseasier to understand the purpose of each method; the intent is explicitly stated, and so the main-tainability of the code has been improved. Any new programmer starting out on the code willneed less time to get acquainted with it. Even experienced programmers can forget a specificdetail 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 tothis method, thus eliminating duplication in the code. This improves the maintainability of thecode, 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 inwhich I modified behavior in one place but failed to modify it in another place containing thesame 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 ofthe code.
❑ I have managed to improve maintainability of the code by replacing literals scattered through-out the code with a constant. This way, if a value ever needs to be changed, the change can beperformed in a single place.
❑ Extracting new classes also improves the reuse in the code, and organizing classes in a hierarchyagain helped me avoid duplication, but on a different scale. At the same time, extensibility of thecode 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 — meaningless to test, to recompile, or to redeploy.
That about wraps it up for this demonstration. In the following chapters we’ll study refactoring tech-niques in greater detail.
Refactoring Technique Result
Replace conditional logic with polymorphism
Reduces duplication in the code, improving maintainabilityand 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 beapplied only in a single place
57
Chapter 2: A First Taste of Refactoring
79796c02.qxd:WroxPro 2/25/08 8:56 AM Page 57
SummaryVisual 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 pro-ductivity. However, as the application progresses and new requirements appear, it becomes necessary torestructure 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 applicationthat 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 refactoringthe 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 improvedreusability by making each class responsible for a single task. Eliminating duplications in the code madeit smaller and easier to modify. Organizing classes into a logical hierarchy also dealt with duplicate codeand made the code more extensible. Refactored source is easier to reuse, extend, and maintain. It is lessprone to bugs and easier to optimize.
With this example I tried to provide insight into how individual refactoring techniques work togethertoward improving the design of the code. With this practical demonstration, a general context for indi-vidual refactoring techniques should become apparent. This should help you understand where each ofthese 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 meansof an automated refactoring tool, in this chapter I tried to stay focused on the code and the refactoring tech-niques as they are performed manually. In the next chapter I’ll talk about the tools that can help you refactormuch more efficiently and securely. I’ll also talk about a refactoring toolkit and especially about an auto-mated refactoring tool.
58
Part I: Introduction to Refactoring
79796c02.qxd:WroxPro 2/25/08 8:56 AM Page 58
Assembling a Refactoring Toolkit
Many techniques and methodologies make you produce and maintain artifacts, in addition tosource 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 thatconfine the way you program in order to make your code more uniform. In all these cases, you mayoften feel that an additional burden has encumbered this already demanding line of work. It caneasily 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 becomein refactoring techniques, the more confident you are with the code. What refactoring does is tomake you feel you are in charge. The productivity gain and coding proficiency that come fromrefactoring 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, yourmethodology, 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 myopinion indispensable for any type of serious refactoring work. The refactoring tool can automatecertain refactorings but only when it is used together with unit testing and source-code control willyou 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’tuse some kind of version-control system, I will not go into any great detail about it other than todedicate just enough space to treat version-control issues relevant to refactoring.
79796c03.qxd:WroxPro 2/22/08 4:58 PM Page 59
As you will see, there is no single tool that encompasses all the functionality you need for a successfulrefactoring process. Visual Studio is on the road to providing a unified solution, but for the time beingyou 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 comeback to this discussion of tools later on, after you have read about each individual refactoring.
Using an Automated Refactoring ToolWider adoption of refactoring in any programming language has generally been influenced by the avail-ability of an automated refactoring tool. It is one thing to understand the theory, the pros and cons, andeven to be an eager devotee of the technique, and quite another to start meddling with your code in theface of an ever-closer deadline. Even the smallest change to your code can break it, so you need a reallycool head and audacity to vouch for some intangible characteristic like design and code quality whenthere 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 theclient code that calls this method and replace its name in the client code. Or, if you extracted a piece ofcode and put it into a new method, the tool would make sure that all the necessary parameters wereincluded in the method declaration.
With Visual Studio 2008, there is such a tool available for Visual Basic programmers. While you mustdownload 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 fromDeveloper Express (http://www.devexpress.com/vbrefactor) free of charge. Refactor! for VB sportsa nice set of basic refactorings, and the number of supported refactorings is growing. This is not the onlytool on the market. Even Developer Express has a more advanced pro version of Refactor!. Other toolsmight provide a larger number of refactorings and cross-language support. However, I find that once youlearn to use basic refactoring techniques, adopting new ones is not so difficult. And taking into accountthat 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 testsome 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 buythem. The next sections touch on some of the tools that are available today.
ReSharper from JetBrainsJetBrains (www.jetbrains.com) made its name with the excellent IntelliJ IDEA Java IDE that broughtanother level of productivity to often nonresponsive Java IDEs, and pioneered comprehensive refactor-ing support for Java. Its first venture into the .NET arena is ReSharper, a refactoring and developer pro-ductivity add-in for Visual Studio. They started by supporting C#, hence the name, but ReSharper isnow a cross-language tool that supports Visual Basic XML, XAML, and ASP.NET.
60
Part I: Introduction to Refactoring
79796c03.qxd:WroxPro 2/22/08 4:58 PM Page 60
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 ofother interesting features that will make your programming life a lot easier.
Visual Assist X from Whole TomatoIt’s probably not the “whole tomato” of VB refactoring, since it is limited in the number of refactorings itoffers, but Visual Assist X still features some important VB refactorings, the first on the list being the all-important Extract Method. It is also able to perform Rename and Encapsulate Field refactoring. Like otherdeveloper productivity tools, it sports a number of navigation, advanced syntax highlighting, enhancedIntelliSense, spell checking, and autocomplete features. See www.wholetomato.com for more detail.
Refactor! Pro from Developer ExpressYou can pay to upgrade your free Refactor! to Refactor! Pro. Refactor! Pro includes over 150 refactor-ings 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 areperformed and even extend the tool by programming your own refactorings. Now you are going tosee how you can use Refactor! for VB, the basics of its user interface, and the list of refactorings thistool supports. Finally, you’ll see some advanced options that this tool provides but that are hiddenfrom the user at the first glance.
Getting Started with Refactor! Refactor! uses Visual Studio add-in architecture to integrate seamlessly into the Visual Studio environ-ment. The new features become apparent once you open a Visual Basic file for editing. There is also adocumentation section that gets merged with rest of the Visual Studio documentation when you installthe 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
Chapter 3: Assembling a Refactoring Toolkit
79796c03.qxd:WroxPro 2/22/08 4:58 PM Page 61
Using Smart TagsA smart tag takes the form of a small line under the first character in the identifier. When you place thecaret on some identifier in your code or select the piece of code, the smart tag appears, indicating a pos-sible refactoring that you can activate by opening the menu and selecting it from the list. Smart tags arecontext-sensitive, so you see only refactorings available for the selected code or for the identifier onwhich the caret is placed, and the smart tag will appear only if there are refactorings available at theposition indicated by the caret. You can see smart tag at the first line in the ClearResults method inFigure 3-1.
Figure 3-1
Right-Clicking the MouseYou can make the context menu appear by right-clicking on a certain identifier or selected piece of code inyour source-code editor. If you have the Refactor! add-in installed, a new option called Refactor! appears inthe 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 selec-tion. Again, the submenu content is filtered so only refactoring relevant to the selected code appears. Youcan see a context menu containing the Refactor! menu item in Figure 3-2.
Figure 3-2
62
Part I: Introduction to Refactoring
79796c03.qxd:WroxPro 2/22/08 4:58 PM Page 62
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 theidentifier or select the code related to the refactoring we are trying to perform. When you hit Ctrl+`, a menuappears with all refactorings available for that selection or caret position. However, this means of activatingRefactor! is different in one respect from right-clicking. It provides fast access to Refactor! functionality anddoes not require the use of a mouse, so in case a single refactoring is available for the active selection or caretposition, 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 comfortablewith the tool.
On the U.S. keyboard, the ` (backtick) key is the same as the ~ (tilde) key and is generally placed abovethe 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 theKeyboard... button. In the Options window define a new shortcut for the CodeRush.Refactor command.
Using Cut and PasteYou can use cut and paste to extract a method. Just cut the code that you want for the new method andpaste it inside the class between methods. Refactor! will add the appropriate calling code at the cut pointand build the method-signature wrapper around the code you paste. You can also introduce an explainingvariable with cut and paste. Just cut an expression to the clipboard and paste it on an empty line above thecut point. Refactor! will declare a new variable of the appropriate type and assign it the expression you’repasting. 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 thebasic elements of Refactor! for the VB user interface.
Exploring Refactor! for the VB User InterfaceIn one way, Refactor! for VB is quite different from the rest of the tools on the market. In order to help pro-grammers always work at maximum speed and to have source code in front of them at all times, the creatorsof Refactor! have avoided placing modal windows in front of the user. Instead they created a set of newvisual features that let users exploit the Refactor! functionality. Those features are hints, markers, linkedidentifiers, target pickers, and replace progress indicators. All have very distinctive and colorful visual stylesand even some amusing animation effects. In this section I’ll walk through each of these features.
HintsUntil you really master Refactor! for VB, you will need some help in finding your way around it. Hintsserve exactly this purpose; they help you see all available options and locate newly created code. Thereare three types of hints:
❑ Action hints
❑ Big hints
❑ Shortcut hints
63
Chapter 3: Assembling a Refactoring Toolkit
79796c03.qxd:WroxPro 2/22/08 4:58 PM Page 63
Action HintsAction hints appear after one-key refactoring is invoked and only one refactoring is available. They generallyhave the form of a large arrow pointing to a place where refactoring will happen. The arrow shows the nameof the refactoring that was applied. After a few seconds this type of hint disappears automatically. Figure 3-3shows an action hint.
Figure 3-3
Big HintsBig hints take the form of a tool tip window. When the refactoring menu is displayed, the content ofthe 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 thedescription of the currently selected refactoring. Once the menu option is selected and the refactoringapplied, the big hint window closes automatically. You can see the big hint for Symbolic Rename refac-toring in Figure 3-4.
Figure 3-4
64
Part I: Introduction to Refactoring
79796c03.qxd:WroxPro 2/22/08 4:58 PM Page 64
Shortcut Hints The purpose of a shortcut hints window is to list all available options and the keys to invoke them oncea certain refactoring has been selected. It is a floating window containing a two-column table. The leftcolumn contains the key and the right column describes the behavior that will result from pressing thatkey. The window can be moved, minimized, or closed. Shortcut hints appear for refactorings that havean interactive state and disappear automatically when you leave the interactive state (for example, bycommitting 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
MarkersWhen working on large files, you might need some help in getting around. For example, if you extract apiece of a method into a new method, you might wish to return to the place where the original methodis located. When performing the refactoring, Refactor! will automatically place a marker in the form of asmall triangle. Once refactoring has been committed, you can return to the starting point by pressing theEscape key. The caret will be moved back to the place where the marker was left, followed with the ani-mated circle to help you locate the caret. Once you return to the marker, it is collected and removed. Youcan have multiple markers, and you can move between them in the reverse of the order in which theywere created (stack-like). This functionality is similar to the bookmark functionality in Visual Studio, withthe difference that markers are created automatically as you perform certain refactorings. Figure 3-6shows a marker in place.
Figure 3-6
65
Chapter 3: Assembling a Refactoring Toolkit
79796c03.qxd:WroxPro 2/22/08 4:58 PM Page 65
Linked IdentifiersThis feature permits you to simultaneously edit all the occurrences of an identifier. In certain type ofrefactorings it is possible to automatically identify all the instances of the identifier that needs to bechanged. This is the case with Rename Local refactoring, for example. Additionally, all linked items arehighlighted, and it is possible to navigate through them using the Tab key. In the context menu you canchoose to break the linkage. Once the edit is finished, you commit the refactoring by pressing the Enterkey, and items are delinked. You can see linked identifiers after the SSNValue field name has beenchanged in Figure 3-7.
Figure 3-7
Target PickersTarget pickers have the form of a horizontal line with an arrow. They permit you to select the location in thefile for the code resulting from active refactoring. You can move the picker by using the up- and down-arrowkeys. Once you commit the refactoring, newly generated code is placed in the location you selected with thepicker, and the picker disappears. Figure 3-8 shows a target picker.
Figure 3-8
66
Part I: Introduction to Refactoring
79796c03.qxd:WroxPro 2/22/08 4:58 PM Page 66
The Replace Progress IndicatorCertain refactorings cannot be executed without some user interaction. In same cases, the programmerhas to decide what items refactoring should be applied to. Take an Introduce Constant refactoring, forexample: the tool can identify all the appearances of a certain literal value, but has no way of knowingwhich one of them you want to replace with a constant.
For example:
Private Function CalculateTax(ByVal amount As Decimal) As DecimaltxtTax.MaxLength = 20Return amount / 100 * 20
End Function
If you apply Introduce Constant refactoring on literal 20, Refactor! for VB will search the file for all otheroccurrences of this number. However, only you can decide which one should be replaced with a constant. Inthis case, one occurrence of the number is related to tax calculation and should be replaced with an appro-priate tax-calculation-related constant, while the other is concerned with a control property and should notbe 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 theoperation. In this mode you can cycle through all occurrences identified by Refactor! and apply a replace-ment 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 themajor 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 worththe trouble. Developer Express started out with the most basic refactorings, so only a few of the ones youwill see span more than one class. This list is not definitive; at Developer Express they continue to work onthe tool, and hopefully with time more refactorings will be released and supported by Refactor! for VB. InTable 3-1 you can see some of the key refactorings available at the moment (and also where in this bookthose refactorings are discussed).
67
Chapter 3: Assembling a Refactoring Toolkit
79796c03.qxd:WroxPro 2/22/08 4:58 PM Page 67
Table 3-1: Refactorings Supported by Refactor! for VB and Covered in This Book
Refactoring Name Purpose Chapter
Encapsulate Field Turns a field into a property 11
Extract Interface Creates a new interface that defines all public mem-bers of a class and makes class implement this newlycreated 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 5
Method to Property Converts a method declaration to a property
Move Declaration NearReference
Moves a declaration statement closer to the first useof the variable
10
Move Initialization toDeclaration
Moves initialization code to a declaration statement 10
Rename Local Renames all occurrences of a local variable 8
Replace Temp withQuery
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 thecall to a new method
8
Simplify Conditional Writes a condition in a simplified way
Split Initialization fromDeclaration
Splits the initialization code from the declarationstatement
14
Split Temporary Variable Introduces a new variable for an overburdened temporary variable
10
68
Part I: Introduction to Refactoring
79796c03.qxd:WroxPro 2/22/08 4:58 PM Page 68
I will explain in detail each refactoring that Refactor! supports throughout the book; the table shows thechapter where each refactoring has been applied and demonstrated. You will also notice that the list ofrefactorings 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 continuouslyin progress. I hope that in time the two lists will start to converge, and the number of automated refac-torings 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 hiddenoptions in the Pro version can be accessed with a free version with a little bit of tweaking. Take a look atAppendix 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 takea look at another very popular technique (one close in philosophy to refactoring) and another importanttool in your refactoring toolkit — unit testing and NUnit.
Unit-Testing Basics: The Testing HarnessRefactoring can be addictive. Once you start, it’s difficult to stop! However, one step too far, and youwon’t be able to find your way back. Make one simple mistake, and suddenly the program is not doingwhat it was supposed to. You try reversing the latest changes but to no avail. You made a mistake some-where along the way, and you just can’t figure out where. The only solution is to go all the way back tothe beginning. The scenario I just described is not so uncommon for a novice to refactoring. Refactoringhas 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 defini-tion of refactoring? You are improving the design without changing the behavior. So how can you verifythe 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-testingsupport. At this point you might ask yourself why I chose to talk about NUnit instead. First of all, TeamSystem is the top of the line of Visual Studio products and that comes at a price. If I had used it, this sec-tion would have had a smaller audience. Secondly, NUnit is more applicable to a test-driven developmentstyle (something a lot of you might decide to pursue). At any rate, exposure to NUnit is also a good start-ing point for unit testing with Visual Studio. Because NUnit is less integrated with Visual Studio and lessGUI-based than Microsoft’s tool, by pursing unit testing with NUnit you will acquire a generic and moreuniversal understanding of the unit-testing process. On a side note, there are other alternatives to unittesting 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 inmore detail one open-source unit-testing framework, NUnit. You will learn how to install it and use it andhow to write unit tests with its help of NUnit. Finally, I’ll take you back to the Calories Calculator applica-tion and write tests for some of the classes in the application so you can see unit testing in practice. But Iwant to start by taking a look at how unit testing as a discipline came about.
69
Chapter 3: Assembling a Refactoring Toolkit
79796c03.qxd:WroxPro 2/22/08 4:58 PM Page 69
Why a Unit-Testing Framework?One of the ways you deal with software complexity is through testing. Testing is your attempt to discoverbugs and errors in the software you are creating before it reaches a final user. In an attempt to ensure soft-ware quality, companies often have teams, even departments, dedicated to different forms of testing. Thetypes of tests these teams perform vary and can be geared toward ensuring different qualities of the soft-ware. They might perform functional, usability, integration, load, stress, and other types of tests. All ofthese 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 takedays, 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 thedevelopment process.
Each time the most minimum functionality is added, changed, or removed, programmers have a natu-ral tendency to want to assure themselves that the software still works correctly and that no fresh bugshave been introduced. The most obvious way to go about this is for the programmers themselves tosimulate 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 debug-ger, enabling you to observe the execution depicted in the source code, line by line, and to interactivelyreview 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 CaloriesCalculator application in Chapter 2.
With all these capabilities and tools, programmers still produce very costly and difficult-to-identifybugs. Why does this happen? Partly because the type of ad hoc testing I just described, performed byprogrammers manually, has some very serious drawbacks:
❑ It requires a lot of manual data input, making the whole process very tedious. (This drawback isdirectly related to the next one.)
❑ Because it is tedious and time-consuming, coders limit their testing exploits to scenarios mostdirectly related to their current tasks. However, there is no guarantee that only the tested func-tionality 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 onesweep 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 areexecuted is low, leading to a situation in which a lot of changes have been introduced withouttest assurance in between. If the bug is identified after substantial changes have been applied, itis a lot more difficult to locate it. This leads to a vicious circle wherein more and more time islost because of an unreliable development process, while the lack of time discourages a system-atic approach to testing.
70
Part I: Introduction to Refactoring
79796c03.qxd:WroxPro 2/22/08 4:58 PM Page 70
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 toperform. So what can we do about it?
A First Attempt at Automated Unit Testing: Ad Hoc Unit TestingThe easiest and the most direct way to test a piece of code in an automated and repeatable manner is to writea program to exercise the targeted code. For example, if you write a dynamic link library, you can program anexecutable that will consume the functionality offered by the dll and make sure that dll behaves according tothe specification.
While this approach to automating testing is a great improvement compared to manual testing, a numberof 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 foryou to maintain the quality of tests and promote their team-wide adoption. There are no realguidelines for programming the tests.
❑ If you do not perform the testing in a focused way, by isolating one piece of code from the restof the application state, it may still be possible to discover the bug, but it can be very difficult topin down the offending line of code. This approach to test automation is similar to the recordingof 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 developmentprocess. 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 towrite their own tests, counting on detailed knowledge of tested code. These tests would have to be executedand repeated at will. They would have to exercise small modules of code in isolation so no time would belost in search for the source of the bug. Finally, the solution needed to encompass standardized means ofcapturing and displaying results.
Viola! Unit-Testing FrameworksAll of this led to the appearance of unit-testing frameworks, which granted programmers new levels offreedom and productivity. While solving many of the previously mentioned problems, the tests writtenin 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 anexplicit, unambiguous manner. Diagrams, storyboards, use cases, and the rest of the traditional require-ments-taking tools are all subject to human interpretation. Because unit tests are written in code and thenexecuted 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, oneembodying 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. Programmersoften speculate about scope of the program: “We might need this feature in the future” or “What if thecustomer needs to solve this problem later on?” If you actually write each test before you implementthe code, this dilemma is no more. You implement only the code that makes all tests execute success-fully, and nothing more. If you don’t have the test that uses it, it means you don’t need the code. Thisapproach to programming is test-driven development.
71
Chapter 3: Assembling a Refactoring Toolkit
79796c03.qxd:WroxPro 2/22/08 4:58 PM Page 71
Next I am going to help you examine a popular unit-testing framework in more detail. This frameworkis 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 .NETexclusively, so you can look at the following paragraphs as an encouragement and an invitation to dipinto this practice further.
Your First Taste of NUnitNUnit 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 anyother .NET language. NUnit was developed as a port of the JUnit framework, written by Erich Gamma andKent Beck for unit testing in Java. JUnit sparked a wave of tools and frameworks that resulted in the portingof JUnit to a variety of languages, and in the development of various types of extensions that facilitate test-ing in diverse environments: Web applications, mock-object development (see the “NMock Object-MockingFramework” section later on in this chapter), GUI testing tools, and others. The sheer size of the responsethis 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 testimplementation 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, butsince version 2.0, an approach more in line with .NET programming style has been adopted. You need toapply different attributes provided by NUnit to the classes and methods, so that NUnit knows that thesecontain unit tests. When you use NUnit to run your tests, it searches for classes and methods markedwith 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 toinstallation procedures.
Installing NUnitNUnit is available for free from the NUnit site, www.nunit.com. NUnit is distributed under an open-sourcelicense and installation comes in different flavors, with the source code available for download in addition tothe standard windows installation package. Since work on NUnit is permanent, you should always down-load 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 calledNUnit-GUI and is used to activate the NUnit graphical interface. The NUnit GUI is used to run tests: it’sone of NUnit’s test runners. There is also a console test runner available, which is recommended for moreadvanced and integrated build setups. As a first step after the installation, I recommend you open theNUnit GUI and execute the sample tests provided with the NUnit installation. By running these samples,you can achieve two objectives:
❑ 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, .NETFramework 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 torun NUnit, this is the most common place to start.
72
Part I: Introduction to Refactoring
79796c03.qxd:WroxPro 2/22/08 4:58 PM Page 72
You are now going to take a look at the GUI after the successful installation of NUnit. In order to getthings going you can try running some of the samples that come with it. VB samples are placed in theSamples/VB folder inside the NUnit installation folder. These tests are distributed as source code, soyou should build the samples first with the Visual Studio IDE. Don’t worry about the code for themoment; you will get to that later. For now, just build the dll. After that, using the File ➪ Open optionin the NUnit GUI, locate the dll you just created and hit the Run button. Figure 3-10 shows the NUnitGUI 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 canchoose to run only a selected test or group of tests. This is a useful feature once the number of testsgrows 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 inter-rupt the execution of tests at any time. Below the buttons is the progress bar, which indicates the state oftest execution.
Under the area with the Run and Stop buttons and the progress bar you can see a set of tabs that displaydifferent 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 theIgnore attribute. If a test is marked with this attribute, it will not be taken into account by the testrunner — for example, if you didn’t finish this particular test yet and you’d rather skip it. (I’ll getinto the details of attribute use with NUnit once you start implementing your own tests.)
73
Chapter 3: Assembling a Refactoring Toolkit
79796c03.qxd:WroxPro 2/22/08 4:58 PM Page 73
❑ Console.Out displays console output from the program. Generally, this is the debug informa-tion 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 aninternal 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 thetree are displayed in different colors depending on the execution’s success (or lack of it). Table 3-2 showsthe meaning of each color.
Table 3-2: NUnit Color Legend
The progress bar will be rendered in a similar way. There is one difference, however: it is enough for asingle 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 TestIn order to implement your first test, you’ll go back for a moment to the application I refactored inChapter 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 forthe NUnit demo. You will write NUnit tests that can verify the behavior of these two classes. After youimplement the tests, each time you modify one of these classes — to change or add functionality, toresolve a bug, or to refactor it — you will be able to check that no undesired effect has been producedby the changes and that the code still behaves as intended.
Creating the Test ProjectYou need to start out by creating a new project for your tests. This is because you do not want to distributeany of the tests with the production code. You’ll use the existing Class Library Visual Studio template tocreate 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. Youcan see the Visual Studio New Project dialog window in Figure 3-11.
Color Meaning
Green Success
Yellow Ignored
Red Failure
Gray Test not executed yet
74
Part I: Introduction to Refactoring
79796c03.qxd:WroxPro 2/22/08 4:58 PM Page 74
Figure 3-11
Now you need to add a reference to the NUnit dynamic link library to our project. The name of the dll isnunit.framework, and you can locate it on the .NET tab in the Add Reference dialog. This dialog win-dow is shown in Figure 3-12, and you can open it by selecting the Project ➪ Add Reference option.
Figure 3-12
75
Chapter 3: Assembling a Refactoring Toolkit
79796c03.qxd:WroxPro 2/22/08 4:58 PM Page 75
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 con-taining 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 FixtureAs I already mentioned, writing unit tests is akin to any other type of programming. As always, the firststep 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 oneTestFixture per tested class.
Take a quick look at the class declaration:
Imports NUnit.Framework
<TestFixture()> Public Class TestFemalePatient
End Class
76
Part I: Introduction to Refactoring
79796c03.qxd:WroxPro 2/22/08 4:58 PM Page 76
You can see how the NUnit.Framework namespace is imported and how the TestFixture attributeis applied to the TestFemalePatient class. If you build the project now and open the dll using theNUnit GUI, you will see the class in the test tree view. You can even run the tests. Because the class isstill empty, the progress bar and test tree are colored yellow: the test runner wasn’t able to find anytest methods yet.
Next it is time to implement your first test method.
Writing TestsYou have now prepared the first class that will become part of your test suite. In NUnit, each test is writ-ten in the form of a method. This method has to be public. It has no parameters and does not return anyvalues. In order to be identified as a test method to be executed by the test runner, it has to be markedwith another NUnit attribute: Test. Here you are going to add such a method to your class, and you aregoing to call it IdealBodyWeight. It means you are going to test the IdealBodyWeight functionalityoffered by the FemalePatient class. In this particular case you are going to test the method for the mostcommon situation, wherein valid parameters are provided for the calculation. (Just to remind you, theformula 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 theFemalePatient instance. Then you need to call the IdealBodyWeight method of an object. Thismethod expects no parameters, but it does return the Decimal type value. As a last step, you will tellNUnit to assume that the expected result and real result are equals. If they are not equal, NUnit willreport 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<Test()> 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 equalAssert.AreEqual(expectedResult, realResult)
End Sub
77
Chapter 3: Assembling a Refactoring Toolkit
79796c03.qxd:WroxPro 2/22/08 4:58 PM Page 77
This code listing should not raise any eyebrows. Except maybe for the last line, where the Assert classof the NUnit framework is used. The next section takes a look at it in detail, since it is the crucial line inthe 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 anassumption or claim about something. This assumption can prove to be true, and in that case a greenprogress 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. I used a formula for the ideal weight calculation to obtain the expected value.
2. I wrote down this literal value in the test code by assigning it to a local variable calledexpectedResult.
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.
Now it’s time to run NUnit again. Since the code is behaving as expected, you should see green in theNUnit GUI.
The Assert class contains a number of methods. They all do more or less similar things. If you thinkabout it, you only need one method to verify about any type of assertion you might think of — a methodto check if a certain Boolean statement is true. Unsurprisingly, there is exactly such a method in theAssert class. It is called IsTrue.
Another method, similar to AreEqual, is AreSame. This method is used to assert that two references arepointing to the same instance. The following code snippet illustrates its use:
Dim femalePatient1 As FemalePatient = New FemalePatientDim femalePatient2 As FemalePatient = New FemalePatientDim 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 initial-ized 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 firstassert, I am comparing two completely different objects. This results in the AreSame method returning avalue 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 illus-tration, take a look at Chapter 11, where I talk in more detail about object identity and the differencesbetween value and reference types.
Yet another method available in the Assert class is the method IsNull. This method lets you assert thata certain variable does not refer to any instance.
78
Part I: Introduction to Refactoring
79796c03.qxd:WroxPro 2/22/08 4:58 PM Page 78
A number of other methods are added to the class for your convenience. A lot of them are negative forms ofmethods you have already seen — for example, AreNotEqual, AreNotSame, IsFalse, IsNotNull, and soon. It’s worth exploring the available methods in the Assert class further yourself. For now you need tocontinue 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 isavailable in Listing 3-2.
Listing 3-2: Testing the DailyCaloriesRecommended Method
<Test()> Public Sub DailyCaloriesRecommended()Dim femalePatient As FemalePatient _= New FemalePatientfemalePatient.Height = 72femalePatient.Weight = 110femalePatient.Age = 30
Dim expectedResult As Decimal = 1015.2
Dim realResult As DecimalrealResult = _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 testmethod was written well also.
Still, there is one detail that is not to my liking. Both methods have large portions of code that are completelyidentical. You are better off writing that code in only one place. The next section shows how you can.
Using SetUp and TearDownIn the NUnit framework it is possible to mark a method with a Setup attribute. This will signal to NUnitthat this method should be executed before any of the test methods are run. You can use this attribute tomark a method that can contain code common to both test methods, and to create objects that will repre-sent 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 theFemalePatient class and assigns valid values to its properties. The code you need is already con-tained in both test methods, so you can move it to the new CreateFemalePatientInstancemethod.
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 methodsat will. It’s time to take a look at the test class. TestFemalePatient is shown in Listing 3-3.
79
Chapter 3: Assembling a Refactoring Toolkit
79796c03.qxd:WroxPro 2/22/08 4:58 PM Page 79
Listing 3-3: An Example of SetUp Usage
Imports NUnit.Framework
<TestFixture()> Public Class TestFemalePatientPrivate femalePatient As FemalePatient
<Test()> Public Sub IdealBodyWeight()Dim expectedResult As Decimal = 161.15626Dim realResult As DecimalrealResult = femalePatient.IdealBodyWeightAssert.AreEqual(expectedResult, realResult)
End Sub
<Test()> Public Sub DailyCaloriesRecommended()Dim expectedResult As Decimal = 1325.4Dim realResult As DecimalrealResult = _femalePatient.DailyCaloriesRecommended()Assert.AreEqual(expectedResult, realResult)
End Sub‘SetUp attribute used to mark a method<SetUp()> Public Sub CreateFemalePatientInstance()
femalePatient = New FemalePatientfemalePatient.HeightInInches = 72femalePatient.WeightInPounds = 110femalePatient.Age = 30
End SubEnd Class
At this point you are probably thinking that the last step is quite familiar, similar to some transforma-tions you saw in Chapter 2.
You’ve guessed it. When you created the CreateFemalePatientInstance method you refactored theTestFemalePatientClass. You extracted one method and promoted one local variable to an instancevariable. Unit tests are an integral part of your code base, so you need to maintain them in optimalshape. 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 beexploited in production. It’s just another artifact that you use to better your procedures and to protectyourself from your own mistakes. While this might be true, the benefits of unit testing greatly outweighany drawbacks. However, in order to keep those shortcomings to a minimum, it is very important tomaintain and refactor your test code.
Going back to the SetUp method, you have seen that it gets executed before each method marked withthe Test attribute is called. Following simple logic, you might ask for a way to write a method thatgets executed after each test method is performed. NUnit provides for such a case. If a method is to beexecuted 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. Ifyou 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, afterthe execution of all test methods.
80
Part I: Introduction to Refactoring
79796c03.qxd:WroxPro 2/22/08 4:58 PM Page 80
Dealing with Exceptions: the ExpectedException AttributeAs I mentioned earlier in this chapter, you have now tested a few methods of the class in the most commonsituations. However, you have seen that some of the formulas are not applicable to people whose height isless than five feet. If someone tries to use the class to calculate ideal body weight for such a person, youwould expect it to throw an exception.
Testing your code for boundary and exceptional circumstances is a very important part of your testingmethodology. You cannot write tests for every possible value, so you need to try to exercise the code for themost sensitive values. These are generally boundary and invalid values. Experience teaches that bugs areespecially 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 Heightproperty of the Patient class. Since FemalePatient inherits Patient, for demonstration purposes youcan 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 anotherattribute, ExpectedException, in addition to Test. The ExpectedException attribute needs a singleparameter, a type name of the exception you expect. What happens once the test runner executes themethod marked with ExpectedException is a check that the exception is thrown. If the exception isthrown, then everything went as expected, and the green light is shown in the NUnit GUI. However, incase no exception is thrown or the exception thrown is of a different type from the one specified in theExpectedException 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 lessthan 5 to the Height property:
<Test(), ExpectedException(GetType( _ArgumentOutOfRangeException))> _Public Sub HeightLessThan5Ft()
femalePatient.HeightInInches = 59End 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 forgotto limit the value of height in code. I now have to modify the Height property in the Patient class. Itends up looking like this:
Public Property Height() As DecimalGet
Return heightInInchesValueEnd GetSet(ByVal Value As Decimal)
If Value <= 60 ThenThrow New System.ArgumentOutOfRangeException( _“Height has to be greater than five feet (60 inches)“)
End IfheightInInchesValue= Value
End SetEnd Property
I hit the Run button in NUnit again, and everything’s green.
81
Chapter 3: Assembling a Refactoring Toolkit
79796c03.qxd:WroxPro 2/22/08 4:58 PM Page 81
This sums up the basic usage of NUnit, but we have a few more important details regarding unit testingto cover before you’re done with this chapter.
The Test-Driven ApproachThe 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 arather rudimentary approach to unit testing.
With the wider acceptance of unit testing by developers, a very important change in the “chicken oregg” dilemma in software testing occurred. Programmers note that they often lack explicit means todefine 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 alsoresolve any future requirements, programmers go to great lengths in producing code that finally nevergets 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 comesto explicitly defining the behavior of your code. The code itself is as explicit as it gets. Also, if you writethe test, it must be because there is a requirement for your code to behave in a certain way. You will notwaste 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 todo is program the classes in such a way that none of the test fails, and you would be done. If there is notest 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 imple-ment the tested functionality at this point, the test will obviously fail on the first run. You can then writecode to make the first test execute without error. You continue by adding the next test and then the codeto make that test pass, and so on. Being guided by requirements embodied explicitly in test code, youmaintain 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 iscalled BodyFatContent, and all needed data is already available in the FemalePatient class in theform 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 goingto call the BodyFatContent method on the femalePatient instance. Since this method still does notexist 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 theFemalePatient class automatically, saving you a few keyboard strokes. It’s a nice feature and help-ful 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:
<Test()> Public Sub BodyFatContent()Dim expectedResult As Decimal = 36Dim realResult As Decimal = _
82
Part I: Introduction to Refactoring
79796c03.qxd:WroxPro 2/22/08 4:58 PM Page 82
femalePatient.BodyFatContent()Assert.AreEqual(expectedResult, realResult)
End Sub
Now you can go back to the FemalePatient class and implement the BodyFatContent method. Youcan 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-drivendevelopment 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’thave space to cover all these tools exhaustively in this chapter. Instead, I’ve chosen a handful of toolsthat I have found complement my refactoring efforts quite nicely.
The TestDriven.NET Visual Studio Add-InYou can get a much better integration between NUnit and Visual Studio if you install the TestDriven.NETopen-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 betweenthe 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 isintended for trial users, students, and open-source developers.
The NUnitForms GUI-Testing FrameworkWhen testing parts of the GUI, you need an approach that’s a bit different from the one you take in test-ing plain old VB classes. You need to simulate user interaction with the program. This is not so easy todo with NUnit. NUnitForms is an extension of NUnit that was made to facilitate the task of testing GUIsmade 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 behelpful in producing another type of test. NUnitForms can be used to simulate user interaction or even torecord it and replay it later. If you run these tests not against a single GUI class, but against the completeapplication, 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 unittests. You cannot always dedicate the time necessary to introduce unit tests a posteriori. However, in theprocess 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 FrameworkAs I already mentioned, when you write unit tests you need to test objects in isolation. This is a veryimportant feature of unit testing because it can help you test exactly the desired functionality and pindown a failure to an exact method or property. However, not all the code you need to test is as simple
83
Chapter 3: Assembling a Refactoring Toolkit
79796c03.qxd:WroxPro 2/22/08 4:58 PM Page 83
as the example you have seen in this chapter. Your code might use some very complex classes anddepend 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 theinterface of a real object. This substitute, a mock object, can verify the way the object being tested is inter-acting with it. If the interaction is not as expected, the test will fail. Also, a mock object can provide theobject being tested with expected return values.
For more information on the subject and for an open-source mock-objects framework, check outhttp://nmock.truemesh.com/index.html.
The NCover Test Coverage ToolYou 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 getevery single line of your code exercised. However, you cannot always be as systematic as you would liketo be. So instead of leaving the quality of your tests in human hands, you can use a tool that reports indetail what lines of your code were executed and what lines you failed to test. Such a tool is available athttp://ncover.org. NCover can be a great help in completing your suite of tests with all relevant cases.
A Few Words about Version ControlWhile unit testing is not so widespread, source-code version control is today universally accepted. Idon’t know a team or company without some type of version control installed. This is why I will notdedicate 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 inrefactoring 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 costlyto 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 willprobably 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 imple-ment 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 pro-grammer checks out a file, no other programmer is allowed to edit it. The idea behind this formof team organization is to promote order and field expertise. Programmers can become experts intheir own area, thus being more efficient and productive. They can be sure that nobody will med-dle 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 for-ward. However, it also has various drawbacks that become more apparent as refactoring practicesare adopted. With focused expertise, the phenomenon of programmer-owners can appear. The
84
Part I: Introduction to Refactoring
79796c03.qxd:WroxPro 2/22/08 4:58 PM Page 84
code is written in such a manner that it is very difficult for other programmers to take over. Thisleads to the proliferation of diverse, often cryptic, coding styles and standards. Because you areallowed to change only your own code, this model can cause stagnation. And then, it can playdirectly against refactoring.
Some refactoring techniques require widespread changes to source code. These changes are notlimited to a single method or a class. In this case, individual code ownership can work againstyou. For example, how can you rename a class if you are not allowed to change all the code thatis making use of that class? For that reason, agile teams tend to adopt collective code ownership. Inthis model, any member of a team is allowed to modify any project item. Also, it is possible formore than one programmer to work on a single file simultaneously. When this happens, if con-flicting changes are discovered at the moment of check-in, these are resolved by merger.
SummaryEffective tools are very important for the successful adoption of refactoring practices. These tools cangreatly improve your productivity. The most important tool from a refactoring perspective is one thatyou 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 VisualStudio add-in architecture. You have now taken a look at installing and using this add-in. Refactor! hasa number of novel GUI widgets that help you interact with the user in an unobtrusive way. You havealso 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. Byunit 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 creationand execution. This framework is called NUnit. You used familiar code from the sample application in theprevious 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 ofthem. I also mentioned a very popular practice among agile teams often referred to as test-driven develop-ment. 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 formis collective code ownership.
After this chapter, you are armed with all the necessary tools for your refactoring process. In the nextchapter you start to deal with some of the basic refactoring techniques very relevant to VB programmers.
85
Chapter 3: Assembling a Refactoring Toolkit
79796c03.qxd:WroxPro 2/22/08 4:58 PM Page 85
79796c03.qxd:WroxPro 2/22/08 4:58 PM Page 86
Rent-a-Wheels Application Prototype
Refactoring is a very practical, hands-on type of art. Therefore, it is difficult to talk about themechanics, motivation, and benefits of refactoring unless there is a good illustration to accom-pany it. While small snippets of code serve well to prove a specific point, you might be leftwanting 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 goon 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 refac-toring technique be illustrated with code samples, but you will see the refactoring process appliedthroughout the lifetime of this sample application.
I will end each chapter in the book with a demonstration of refactoring in practice. I will use theRent-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 tieredenterprise application.
Since I will refer to the Rent-a-Wheels application in almost every chapter that is to follow, youshould try to analyze the application while reading this chapter and think of possible weaknessesin the design and implementation. This way, as you progress with the book you will be able tocompare 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 conversationswith the client so you can put yourself in the same situation — that of analyzing and distilling rawinformation 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 withrefactoring 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, industrial-strength 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 87
I suggest you download the complete project from www.wrox.com and use the IDE to follow as we goalong. I have included a lot of code listings in the text, but you’ll have an easier time finding your wayaround 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 casefor the application and observe work on the application in progress. Bear in mind that this chapter isnot meant to teach you software design or analysis. While you are reading it, I hope this chapter doestwo 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 toprogramming and how they relate to refactoring and software design in general.
Inter viewing the ClientThe 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 vehiclereception 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 gatheredthe information I needed to define the requirement and start the project. The next sections show the tran-scripts 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 willmake 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 havemanaged to position ourselves well. Our biggest strength is a loyal clientele and a new fleet, since wehad a big investment come in earlier this year. At the same time, we started another location. We openedup 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 lineforms the clients start going over to the competition. The time we take to serve a single client is justtoo 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 acquiredbig 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 inspec-tion 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
Part I: Introduction to Refactoring
79796c04.qxd:WroxPro 2/23/08 7:46 AM Page 88
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 ofthe 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 willhave to consider that also.
However, while talking to the manager gave me more insight into my objectives, it failed to providedetailed information on the way business is conducted. So the next person I had to interview was the receptionist.
Interviewing the Desk ReceptionistThe 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 lessfrequently. 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 hasto 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, etcetera. 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 theparking lot attendants.
89
Chapter 4: Rent-a-Wheels Application Prototype
79796c04.qxd:WroxPro 2/23/08 7:46 AM Page 89
Interviewing the Parking Lot AttendantIt 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 topiece together the whole story. This is the reason I decided to speak to employees in any position thatmight 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. Thecustomer has to give me a receipt so I know he finished all the proceedings at the reception desk. I thenwrite down the mileage and put the receipt in the “Rented Vehicles” folder. When the customer returnsthe vehicle, I check the mileage and the tank and write the numbers on the receipt. We give the customera 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 additionto other items. No bonus in that case, of course. Plus, the customer gets to wait if he is not satisfied withwhat I wrote down. I can only write down full, three-quarters, one-half, one-quarter, or empty fromlooking at the indicator. If the customer thinks this is not correct, he has to wait for the maintenance guyto come back with the receipt from the filling station. After the reception, the vehicle has to pass througha regular inspection by our maintenance personnel. I check for any obvious damage when the vehicle isreturned, 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 PersonnelThis is the last employee I need to speak with. By talking with this employee, I hope to learn more aboutvehicle 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 timeis up, I have to take the car to the service center. I have to let the people at the desk know so they areaware that that particular car is no longer available. Since we let our customers return the vehicle to anybranch 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
Part I: Introduction to Refactoring
79796c04.qxd:WroxPro 2/23/08 7:46 AM Page 90
From this final conversation it is clear that vehicles are regularly removed from the fleet in order to receiveroutine 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 topresent 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 forthe prototype that I will use to obtain the final go-ahead from the client. While I am at it, I might as wellsketch a few diagrams — it always gets a nod of approval from the boss.
Actors and Use CasesThe first step is to identify all the future users of the system, or the actors. This is not so difficult. All Ineed to do is follow the “paper trail” I discovered in my interviews. The receipt was traveling from thedesk to the parking lot and back. Additionally, the maintenance personnel had to make a report if the carhad to be filled up or sent to a service center. I decided to depict actors and principal use cases with adiagram. Figure 4-1 shows the Rent-a-Wheels use case diagram I came up with.
Figure 4-1
After I identified the principal use cases, I decided to write down all the steps the actors have to performwhile interacting with the application in order to reach the final goal. This will provide a good breakdownof the functionality the Rent-a-Wheels application will have to offer.
Receptionist Parking Lot Attendant
Maintenance Personnel
Rent vehicle
Receive payment
Hand over vehicle
Receive vehiclefrom customer
Inspect vehicle
Receive vehiclefrom other branch
91
Chapter 4: Rent-a-Wheels Application Prototype
79796c04.qxd:WroxPro 2/23/08 7:46 AM Page 91
Rent Vehicle1. Receptionist selects the vehicle from the list of available vehicles.
2. Receptionist inputs the customer data: ID type and number.
3. Receptionist marks the vehicle as “hand over.”
Receive Payment1. Receptionist selects the vehicle from the list of “payment pending” vehicles.
2. System displays the charges.
3. 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 Customer1. Parking lot attendant searches for vehicle marked as “hand over” that has customer’s ID.
2. 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 Customer1. Parking lot attendant searches for rented vehicle in the vehicle list.
2. Parking lot attendant inputs mileage, tank level, and any additional comments.
3. Parking lot attendant marks vehicle as “payment pending.”
Inspect Vehicle1. Maintenance personnel select vehicle being inspected from the list.
2. Maintenance personnel mark vehicle as “in operation.”
Alternative:
2a. If maintenance is due, vehicle is marked as “in maintenance.”
Receive Vehicle from Other Branch1. Parking lot attendant searches on list of vehicles for vehicle coming from another branch.
2. Parking lot attendant marks vehicle as stationed in the receiving branch.
Receive Vehicle from Service Center1. 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 wayin which users should interact with the future system.
92
Part I: Introduction to Refactoring
79796c04.qxd:WroxPro 2/23/08 7:46 AM Page 92
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. Thevehicle has characteristics like license plate number, category, make, model, and color. As a part oftheir 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 variousbusiness 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 StatesAs a vehicle is selected and becomes involved in the business flow, it passes through a few states in awell-defined order. As you analyze those states, you can see that their nature is twofold: some states areconcerned with the vehicle in regard to its being rented to a customer and others in regard to its going tomaintenance. I decided to use two diagrams to represent these states and transitions. In Figure 4-2 youcan see the states the vehicle goes through during a normal rental process.
Figure 4-2
Rules related to vehicle states and state transitions embody the core of the business process in the Rent-a-Wheels company. The following list explains these states and the transitions related to normal vehicleuse 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.
Available Hand over
Payment pending Rented
93
Chapter 4: Rent-a-Wheels Application Prototype
79796c04.qxd:WroxPro 2/23/08 7:46 AM Page 93
Figure 4-3
States and transitions describing the maintenance process represent another important set of businessrules. 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 vehiclescan be rented, handed over to customers, received, and charged for. So available, hand over, rented, and pay-ment pending are inner states of the in operation state. Or, to put it differently, in operation is a superstate, andthis can be represented with a single state diagram using nested states. The single state diagram is presentedin Figure 4-4.
With all that work completed, I knew it would be good to finalize the work I had done by materializingall these ideas into some sort of user interface prototype.
First Sketch of the Main Application WindowWith all this information in hand, it’s time to take a shot at drawing the main application window. Thecentral point of the application is the fleet and the different states of the vehicles. I can show vehicles in acertain amount of detail in a grid, and let users operate on a specific vehicle by selecting a single row inthe grid. Each row represents a single vehicle. The user will be able to operate on the vehicle by selectingit on the grid and then pressing the appropriate button — Rent, Hand Over, Charge, or similar. I also needto provide for filtering of the vehicles in the list. That will make jobs a lot easier for the users, as they willbe able to restrict the display to vehicles they are interested in, based on vehicle state or some other char-acteristic. Figure 4-5 represents the main application window.
In operation
In maintenance
94
Part I: Introduction to Refactoring
79796c04.qxd:WroxPro 2/23/08 7:46 AM Page 94
Figure 4-4
Figure 4-5
Available Hand over
Payment pending Rented
In maintenance
In Operation
95
Chapter 4: Rent-a-Wheels Application Prototype
79796c04.qxd:WroxPro 2/23/08 7:46 AM Page 95
I thought that use cases, state diagrams, and a main window prototype gave a good starting point for theapplication I was about to build, so for the next step I decided to present these artifacts at the team meeting.
Rent-a-Wheels Team MeetingThe 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 thetask, and very efficient as a coder.
I started the meeting by presenting the information gathered so far. First, I gave them some backgroundinformation about the company, the meetings I had had, and the people I had met. Then I presented thework I had done on the analysis; I showed them the diagrams and talked about use cases. I even presentedmy 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. Hewould 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 prototypehad been completed, presented to the client, and initial approval was received. While assembling the proto-type, Tim went on to implement most of the functionality initially described. Tim’s idea was to try to reusethe code from the prototype in order to cut implementation time. This is fine, as long as the code is up tostandards and well designed. To make sure it is so, I have to take a detailed look at the application.
Making the Prototype WorkTo 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 astandalone application that connects to a Microsoft SQL database over ADO .NET. I decided not to usedata 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 ModelTim 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 num-ber for the primary key in the Vehicle table. The rest of the tables have an automatic, auto-incrementingprimary key. The relationships are easier to observe in the diagram.”
Tim produced the Rent-a-Wheels database diagram shown in Figure 4-6.
96
Part I: Introduction to Refactoring
79796c04.qxd:WroxPro 2/23/08 7:46 AM Page 96
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 categoryand Category has the price information. When writing SQL code, this translates into an Inner Join ofthe 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 columnnamed Available. Information related to the operational state of the vehicle is held in the column namedOperational. I used the tinyint type for these two columns. In the case of Available, the meaning ofthe 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
Chapter 4: Rent-a-Wheels Application Prototype
79796c04.qxd:WroxPro 2/23/08 7:46 AM Page 97
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
Tim then said, “Other attributes of the vehicle are related to customer information about the customercurrently renting the vehicle: names, document type, and numbers. These are used by the parking lotattendant to identify the customer at the moment when the vehicle is handed over. When the vehicle isreturned, tank level and mileage are saved in Tank and Mileage columns. Of course, this data is relatedto 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 withspecial meanings.” Tim then showed me Table 4-3.
Table 4-3: Connotation of the Tank Column’s tinyint Values in the Vehicle Table
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 data-base-independent as possible. They still have to buy software licenses and they want to be able to lookfor the best price on the market. I thought that minimizing the code on the database side will help us notdepend 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.
Tank Column’s tinyint Value Meaning
0 In Operation
1 In Maintenance
2 1/4 Full
3 1/2 Full
4 Full
Operational Column’s tinyint Value Meaning
0 In Operation
1 In Maintenance
98
Part I: Introduction to Refactoring
79796c04.qxd:WroxPro 2/23/08 7:46 AM Page 98
Examining the Visual Basic CodeTim 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 operationsfor the business. It is the same form you designed in the analysis phase of the project. The only new func-tionality I added is the administration menu. It is used to invoke forms that serve to administer differenttables in the database. This way the user can add a vehicle, delete it, or edit existing vehicles. The samegoes 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 dataadministration 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 everydaywork — a Rent button, a Hand over button, and so on. When they press the button, in some cases, the oper-ation is directly performed, and in other cases, when additional information is needed, a new form is dis-played. 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
Chapter 4: Rent-a-Wheels Application Prototype
79796c04.qxd:WroxPro 2/23/08 7:46 AM Page 99
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 FrmRentFrmRt.TxtLP.Text = _ DGridFleetView.SelectedRows.Item(0) _ .Cells.Item(0).Value ‘Show FrmRtFrmRt.Show()
Else‘Warn user if no selection made in table and exitMsgBox(“Please select vehicle first!”)
End IfEnd 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 numberfrom 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 areminder 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 variablesDim oCnDim oCmd‘double-check with userIf MsgBox(“Are you sure?”, MsgBoxStyle.OKCancel) Then
‘Activate error handlingOn Error GoTo ErrorHandler
100
Part I: Introduction to Refactoring
79796c04.qxd:WroxPro 2/23/08 7:46 AM Page 100
Listing 4-2: Event-Handling Routine for BtnRent_Click from the Rent Form (continued)
‘Create SqlConnectionoCn = New SqlConnection(“Data Source=TESLA-DAN;” + _ “Initial Catalog=RENTAWHEELS;User ID=sa”) oCmd = New SqlCommand ‘Create Sql String with parameter @SelectedLPstrSql = “Update Vehicle “ + _
“Set Available = 1,” + _ “CustomerFirstName = @CustomerFirstName,” + _ “CustomerLastName = @CustomerLastName,” + _ “CustomerDocNumber = @CustomerDocNumber,” + _ “CustomerDocType = @CustomerDocType “ + _ “WHERE LicensePlate = @SelectedLP”
‘open connectionoCn.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 commandoCmd.ExecuteNonQuery() ‘close connectionoCn.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 incase he pressed the button by mistake. Then I initialize the connection. I am using a .NET Framework DataProvider for SQL Server to connect to the database; since the company has Microsoft SQL Server 2005, this isthe most efficient provider. After that I build SQL code as a string and assign it to a command. Then the datais recollected from the form, and values entered by the user are added as command parameters. Finally, thequery is executed. At the end, I close the connection and clean up the objects.”
101
Chapter 4: Rent-a-Wheels Application Prototype
79796c04.qxd:WroxPro 2/23/08 7:46 AM Page 101
When I asked Tim about error handling, he said, “Below the ErrorHandler label you can see the codethat gets executed in case the error occurs. This can happen if the database server is down, if there is aconnectivity problem with the database, or something like that. Basically, users are informed that anerror has occurred and that they should contact technical support. The whole method is quite straight-forward 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 ideaof 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 examplethroughout the book.
If you are interested in digging further into the example application at this point, more conversationswith Tim about the details of the application can be found in Appendix B, and you are welcome to take alook 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 usingthis example as a real-world scenario for discussion, this is enough to start with. Accordingly, in the nextsection I want to talk about what this example shows and how it applies to the topic of refactoring. Inthat section I want to list only a few typical pitfalls VB programmers can be dragged into with theirdesire to get results immediately. While I have great respect for pragmatic programmers and I am notkeen on wasting my time on some vague philosophical discussions, I am very interested in producingthe best possible code. For that purpose, it is crucial you understand the fundamental principles behindobject-oriented programming and how sometimes the fastest approach is not the best approach, noreven 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 hasbeen spent on formally designing the application before, during, or after construction. Most of the designtime was invested in blueprinting the database. In this case, you can assume from the lack of any otherdesign-related artifacts that programmers embarked upon coding from the outset. This is not necessarilya mistake if it is followed by design-related improvements during the development process; otherwiseyou can end up with some serious pitfalls in your code. Here are some of the typical approaches used onRent-a-Wheels that can lead to drawbacks in your code if not counteracted by the application of object-oriented design principles.
Database-Driven DesignSince the database was the first component of the application to be given definite shape, the rest of thecode had to comply with the decisions Tim made in constructing the database. The bulk of VB code is, infact, interested only in interacting with the data store. I call this a database-driven design.
102
Part I: Introduction to Refactoring
79796c04.qxd:WroxPro 2/23/08 7:46 AM Page 102
Relational database design is governed by a different set of principles from object-oriented design. Whendesigning databases, a designer is concerned with providing efficient data storage and retrieval, enforcingdata integrity, providing transactional consistency, and so on. By putting effort into database design with-out structuring your VB code according to object-oriented principles, and being driven by data designinstead, you will end up with poorly designed VB code. Often it is SQL code that is used to express busi-ness logic. Such a design can have an efficient data backend, but your VB code will be inefficient and diffi-cult to maintain.
Modern applications use an object-oriented layer as the core layer for implementing business logic. Object-oriented code is much more powerful and efficient for expressing business rules. Later on in this book youwill see how you can express business logic in object-oriented terms and keep the database layer only fordata storage and retrieval, an area where databases are on their own turf.
GUI-Based ApplicationIn essence, this application is a standalone executable connecting to a remote database. It is a typicalclient-server application. The .NET platform provides a toolkit called Windows Forms for writing ele-ments of the user interface, and it is exactly this toolkit that Tim used to construct the user interface forthis application. As a matter of fact, the GUI was probably the only other part of the application besidesthe database to go through a more elaborate design process. You can see that most of the administrationforms 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 classesthat were created to extend the Form class (Inherits System.Windows.Forms.Form), are part of theuser interface, and have been designed in Windows Forms Designer. This is apparent when you openthe 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 iswhy we can say that this application is GUI-based.
Modern applications are often tiered and component-based. Such a modular design is a vast improve-ment 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. Inorder for you to be able to construct applications in a modular manner, the typical approach is to dividean application physically in the form of components or dynamically linked libraries across tiers. Whenyou are depending only on GUI classes produced by Windows Forms or some other visual designer,such organization is not possible.
Event-Driven ProgrammingVisual Basic programmers are very familiar with the concept of event-driven programming. This stylewas present in VB a long time before the .NET platform appeared. In essence, the tool is capable of gen-erating a hook, an empty event-handling routine declaration. It is up to a programmer to fill in the bodywith the code that gets executed when a certain event, a consequence of user interaction with the appli-cation, occurs.
103
Chapter 4: Rent-a-Wheels Application Prototype
79796c04.qxd:WroxPro 2/23/08 7:46 AM Page 103
This is exactly the approach taken in constructing the Rent-a-Wheels application. Most of the code is placedinside event-handling routines, in most cases a Click event of some button. Rent-a-Wheels is typical ofevent-driven programming.
The problem arises when the programmer does not go beyond the routines generated by the tool. If youare not structuring code further, into separate methods and classes, there is bound to be a lot of repeti-tive and duplicated code. Such code will make your maintenance much more difficult, and your designbloated and inefficient. Again, the solution is in using the object-oriented approach and restructuringyour code in keeping with object-oriented principles.
Rapid Application Development (RAD)The Rapid Application Development (RAD) approach is meant to empower the programmer withtools and techniques that can radically cut development time. This is possible thanks to visual design-ers, wizards, and code generators. Another benefit is the tool capacity to create prototypes early in thedevelopment 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 simpleapplications, it shows weaknesses at different scales when you are working with more complex applica-tions. This is because of the code proliferation and reduced code reuse often found in RAD code: theseweaknesses can easily outweigh the benefits of RAD if they are not mitigated with thoughtful, object-oriented design.
Copy-Paste as a Code Reuse MechanismAs you gain more experience, you soon begin identifying the repetitious patterns and recurring prob-lems you deal with while you program. Naturally, you soon start developing techniques that can helpimprove your productivity and avoid repetition. One of the first techniques you adopt for this purposeis copying and reusing sections of code that can be adapted to a new use with a minimum of modifica-tions. This can be a great productivity booster and is an important confidence-builder for a novice pro-grammer. There is almost nothing as satisfying to an inexperienced programmer as being able to finisha new task quickly by reusing work done on some other project, done in some other part of the applica-tion, or simply found on the Internet.
While this is an important improvement in programming technique and productivity for an aspiringprogrammer, it has serious downsides. Copy-paste as a reuse mechanism leads to code proliferationand duplication that can soon turn into a real maintenance nightmare. Copy-paste can be seen as a firststep on a programmer’s path to advanced skill acquisition. The same capacity for pattern recognitioncan lead to more advanced reuse mechanisms. Instead of copying a code section, the programmer canmethod 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 bringgreater benefits than simple code copying. Refactoring deals with this issue and teaches you how to useyour pattern-recognition skills in a more powerful way.
104
Part I: Introduction to Refactoring
79796c04.qxd:WroxPro 2/23/08 7:46 AM Page 104
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 thatthe important work had already been completed successfully. The prototype represented a valuable basefor the final delivery. However, at this stage it was important to address the design issues that this appli-cation presented. It needed to be refactored. You will see the results of that refactoring work as I returnto the Rent-a-Wheels example throughout the book.
SummaryVB programmers often create prototypes very early in the development phase. They are able to materializethe application early on and present it to the client. This way a lot of risk regarding requirements fulfillmentcan be mitigated quickly.
This chapter presented a real-life scenario that will serve as an example application for the book. It showedhow the prototype for the Rent-a-Wheels application was created. First, requirements-gathering includedinterviewing the employees of the company. A manager, receptionist, parking lot attendant, and mainte-nance worker all had their say about the work they perform and the most important issues they expect thefuture application to resolve.
Facts gathered from the interviews were formalized into the most relevant use cases. The vehicle wasdetermined to be a central entity in the business process. The vehicle’s different states result from theoperations 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 inline with the tradition of VB programming. The database was designed up front, the GUI was then designedwith the Windows Forms Designer in Visual Studio, and in the end the coding was performed by filling inthe event-handling routines generated by the tool.
While the work performed is valuable and the prototype resolved most of the business-related problemsthe application was meant to address, it nevertheless has numerous flaws. It will have to be refactoredbefore 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 tothe Rent-a-Wheels code created in this chapter and apply your newly acquired refactoring knowledge to thisclose-to-real-life sample application.
105
Chapter 4: Rent-a-Wheels Application Prototype
79796c04.qxd:WroxPro 2/23/08 7:46 AM Page 105
79796c04.qxd:WroxPro 2/23/08 7:46 AM Page 106
Part II: Preliminary VB Refactorings
In the next few chapters I am going to explore preliminary VB refactorings. I callthem preliminary because you can execute them even without deeper knowledgeof 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 backwardcompatibility or are related to good programming practices that I call “basic pro-gramming hygiene.” These refactorings let you clean and prepare the code for thestandard refactoring techniques we’ll talk about in Part III.
79796c05.qxd:WroxPro 2/23/08 8:07 AM Page 107
79796c05.qxd:WroxPro 2/23/08 8:07 AM Page 108
Chameleon Language: FromWeak Static Typing to
Strong Dynamic Typing
Almost all high-level programming languages implement the concept of types. By classifyingvalues 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 ordynamically typed.
❑ In statically typed languages, type resolution is performed at compile time, and typeinformation 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 notavailable until execution.
Visual Basic can be statically or dynamically typed. This behavior of VB’s compiler is controlled bythe Option Explicit statement.
Languages are also differentiated by the level of type safety they provide. Strongly typed languagesdisallow operations on arguments that have a wrong type. Other type languages permit these opera-tions 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 conver-sions 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 letsyou omit the As clause of your local variable declaration, but the code is still statically typed. In thatsense it is more of a productivity feature than an additional type-system behavior. Since I deal withVB 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 109
Table 5-1 shows the kinds and levels of typing enforced in VB by the compiler when the OptionExplicit and Option Strict options are active or inactive.
Table 5-1: VB’s Option Statement in Relation to Typing Enforced by the Compiler
This versatility of VB is unique and enables the effective use of VB for variety of purposes, from fast pro-totyping to industrial-strength applications. While .NET as a platform is mostly statically oriented (otherlanguages like C# and managed C++ are statically typed), the flexibility of VB lets it fill the gap in thedynamically typed compartment.
However, in some cases, when programmers are not completely aware of the effect that Option state-ments can have on the code, or when they are working with legacy code upgraded to VB .NET, they canproduce potentially problematic code. Such code comes in two flavors:
❑ Code written in statically and strongly typed style, but without compiler enforcement of static orstrong 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 strongtyping (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 Explicitstatements 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 with-out 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, reapingthe benefits of both styles.
❑ Finally, I will show you the alternatives you have when setting Option Strict and OptionExplicit, 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 wayyou 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
Option Explicit On Option Explicit Off
Option Strict On Static strong Dynamic strong (rare)
Option Strict Off Static weak (problematic) Dynamic weak
110
Part II: Preliminary VB Refactorings
79796c05.qxd:WroxPro 2/23/08 8:07 AM Page 110
given you the ability to ignore a number of issues as you write code so that you can be productive fast, butsuch an approach can have some very negative sides to it. If you take a look at the official VB documenta-tion, these issues are seldom discussed in detail. I will explain them and give you sufficient information soyou 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 vastexisting code base written in VB6 and previous versions of VB. It was certainly important to providesome upgrade path for existing legacy code. On the other hand, one of the most sought-after features ofVB was strong static typing. Using strong static typing essentially means that you need to declare explic-itly 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 followingapproach was adopted: VB .NET can work both with and without static typing, depending on a compileroption value:
❑ If you deactivate Option Explicit and Option Strict, you do not have to declare vari-able 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 todeclare all your variables and their types, and all type conversions have to be performed delib-erately and written down explicitly in code. This is static, strongly typed code. I will refer to suchcode 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 withoutactivating Option Explicit and Option Strict, but you don’t get any tool support for it. If you for-get to declare a variable or its type, or perform some potentially hazardous implicit conversion, the com-piler does not complain. Instead, it tries to work things out to the best of its abilities. I will refer to suchcode — 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 thenecessary verifications and to compile the code only after all variables and types are declared and alltype conversions performed explicitly. Needless to say, this is the preferred way to write code. If you arewriting strongly and statically typed code, it only makes sense to count on the tool to provide you withcompile-time type checking.
Setting Option Explicit On in Relaxed CodeThe 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 differencein this case is that you do not get the compiler to enforce the declaration of each and every variable.
111
Chapter 5: Chameleon Language
79796c05.qxd:WroxPro 2/23/08 8:07 AM Page 111
Now I want to discuss why omitting to declare variables can be considered bad style, how you can con-vert code with omitted variables into explicit code, and the you can apply this method to the Rent-a-Wheels application.
Understanding the Set Option Explicit On RefactoringAt this point you might rightfully ask, “Why declare variables at all?” Because you have seen that youcan write code without declaring variables, there must be some good reason to do so. Otherwise, you areonly writing some redundant, unnecessary code.
A number of reasons exist for declaring variable type, but the most obvious one is that a number ofbugs, a lot of times just simple typos, can be discovered at compile time. Imagine you have the followingcode snippet:
printers = GetPrinters()printersTested = 0For Each printer In printers
printer.PrintTestPage()printersTeted += 1
NextMsgBox(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 con-tains a simple typing error that can cause you to spend a lot of time with a debugger in search of thisvery 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 spendany more time in futile hunt for this evasive enemy of every bug-free, code-loving programmer.
printers = GetPrinters()printersTested = 0For Each printer In printers
printer.PrintTestPage()‘Offending lineprintersTeted += 1
NextMsgBox(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 corre-sponding new identifier is encountered in the code. With the sample I have just shown, a compiler hasno way to guess that I actually meant to use the printersTested variable and not to create a newprintersTeted 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
Part II: Preliminary VB Refactorings
79796c05.qxd:WroxPro 2/23/08 8:07 AM Page 112
So how does Option Explicit On help? Once you include the statement in your code, the offendingline is underlined as an error in the editor, and in the error list an appropriate error description is dis-played: “Name printersTeted is not declared.” Very clear, precise, and very helpful. You can then cor-rect the error right away. Finally, this is how the snippet should look once I use Option Explicit Onand correct the bug.
Option Explicit On‘...Dim printers = GetPrinters()Dim printersTested = 0For Each printer In printers
printer.PrintTestPage()printersTested += 1
NextMsgBox(printersTested & “ printers tested.”, _MsgBoxStyle.Information, “Printer test status”)
Once you have declared all the variables, you should proceed by assigning the corresponding type to thevariable. More about this in the next section.
Continued
Refactoring: Set Option Explicit On (Enforce Variable Declaration)MotivationExplicit variable declaration can prevent number of typo-related bugs early on at com-pile time. It can improve code readability by stressing variable scope.
Related SmellsUse this refactoring to eliminate the Implicit Variable Declaration smell.
Smell: Implicit Variable DeclarationDetecting the SmellUse the compiler to detect variables that are declared implicitly. Place Option ExplicitOn at the top of the file, compile the project, and look for a “Name someVariable is notdeclared” error in the Error window. The IDE will also underline the identifier in theEditor window if an undeclared variable is present.
Related RefactoringUse Set Option Explicit On refactoring to eliminate this smell.
RationaleFailing to declare variables explicitly can lead to difficult-to-detect bugs resulting fromsimple typing mistakes. It can also lead to a convoluted use of variables. Code read-ability can benefit from explicit variable declaration.
113
Chapter 5: Chameleon Language
79796c05.qxd:WroxPro 2/23/08 8:07 AM Page 113
Refactoring the Rent-a-Wheels Code to Explicit FormPerforming this refactoring on the Rent-a-Wheels application proves to be rather simple. After youplace Option Explicit On in a single file, Visual Studio IDE is very helpful in identifying undeclaredvariables. It seems that the application was written in an explicit style, but because the style was notenforced by the compiler a few implicit declarations need to be taken care of. Take a look at one samplefunction and the code that results once the refactoring is performed. Listing 5-1 shows the code for theBtnDelete_Click method. Code that changed after the refactoring is shown in bold.
MechanicsThe Option Explicit statement is module-level only. Option statements have to bethe first statements in the file. When transforming a project written in a relaxed vari-able-declaration style, start by placing the Option Explicit On statement in a singlefile. (If you set this option on at the project level, you are bound to receive a significantnumber of error messages, and increased clutter might slow you down.) After youdeclare all the variables in one file and no errors are reported by the IDE, move to thenext file. Complete this refactoring on the whole project before starting. Set OptionStrict On refactoring.
After you have turned Option Explicit on, the IDE will underline all undeclaredvariables and report errors (“Name VariableX is not declared”) in the error list. Work toeliminate errors one by one by declaring undeclared variables using Dim, Private,Public, or ReDim.
BeforePublic Function BasketTotal()
For Each item In Baskettotal += item.Price
NextReturn total
End Function
After‘Option Explicit On statement addedOption Explicit On ‘...Public Function BasketTotal()
‘Variable declaration addedDim total Dim item For Each item In Basket
total += item.PriceNextReturn total
End Function
114
Part II: Preliminary VB Refactorings
79796c05.qxd:WroxPro 2/23/08 8:07 AM Page 114
Listing 5-1: The Rent-a-Wheels FrmBranch Class BtnDelete_Click Method After theOption Explicit is Set to On
ByVal e As System.EventArgs) Handles BtnDelete.Click‘Declare variablesDim oCnDim oCmdOn Error GoTo ErrorHandler‘Create SqlConnectionoCn = New SqlConnection(“Data Source=TESLA-DAN;” + _“Initial Catalog=RENTAWHEELS;User ID=sa”)oCmd = New SqlCommand‘add parameter nameDim strSql = “Delete Branch “ + _
“Where BranchId = @Id”oCmd.Parameters.AddWithValue(“@Id”, TxtId.Text)‘open connectionoCn.Open()‘Set connection to commandoCmd.Connection = oCn‘set Sql string to command objectoCmd.CommandText = strSql‘exexute commandoCmd.ExecuteNonQuery()‘close connectionoCn.Close()‘destroy objectsoCmd = NothingoCn = NothingFrmBranch_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 needto perform in order to activate this option and successfully compile the code, written in a permissive style.
Setting Option Strict On in Relaxed CodeSetting Option Strict On will affect your code in a number of ways. It will make you consider someaspects of programming you might have taken for granted. You will gain more control over the behavior ofyour program, but you will also have to put a little more work into the coding. By doing so, you will addresssome potential pitfalls that can cause some very subtle bugs in your code. You will put the compiler to work
115
Chapter 5: Chameleon Language
79796c05.qxd:WroxPro 2/23/08 8:07 AM Page 115
to disallow certain programming styles and constructs. Therefore, a lot of potentially hazardous situationswill have to be dealt with before the code can compile. When you impose strong typing as a way to writecode, Visual Basic as a programming language becomes in this sense very similar to other strongly typedlanguages 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 pro-viding 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 yourcode in the form of known abstractions and entities.
❑ Unwanted type coercion leading to precision loss, rounding errors, and possible overflow errorsis 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 themissing 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 youplace the Option Strict On statement at the beginning of the file, an error window will display thefollowing message for every variable for which type information is missing: “Option Strict On requiresall variable declarations to have an ‘As’ clause.”
A Slightly Artificial Example of Permissive VB codeBefore you go on with the task of converting the code into its strict form, you need to take a look at pos-sible 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 with-out 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 onin the VB code written in relaxed form, you can unwittingly introduce bugs and errors into the code. Foran 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 Module1Sub 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
Part II: Preliminary VB Refactorings
79796c05.qxd:WroxPro 2/23/08 8:07 AM Page 116
Listing 5-2: An Example of Code with Option Strict Set to Off (continued)
variableX.GetType.Name.ToString)
‘Current type DoublevariableX = 11.11 Console.WriteLine(“Type: “ & _variableX.GetType.Name.ToString)
‘Current type StringvariableX = “” & variableX Console.WriteLine(“Type: “ & _variableX.GetType.Name.ToString)
‘Late bound callConsole.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 demon-strate a radically different behavior of VB runtime under relaxed mode, when Option Strict andExplicit are set to Off. After you run this code, the following output appears in the console window:
Type: Int32Type: Int64Type: DoubleType: String5
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, afterassigning it a literal value of 10. After the addition of a huge number, the type is changed to Long, in orderto accommodate the result of this addition. Next, after the addition of the decimal value 11.11, the type isagain changed in order to preserve the decimal part of the number. You can see that VB runtime is perform-ing the necessary operations and accommodating its behavior so it best suits the code. Finally, after the con-catenation 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 leftunreachable, waiting for garbage collection.
Late Bound CallsThe 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 prop-erty call is the fact that you have not defined the variable as String. This is not something that the compilercan know at compile time. However, it assumes during execution that variableX will have the propertyLength. That’s why this call is late bound, which means that if the object referenced by variableX has somemember called Length, the call will succeed, and if the object does not have such a member, a runtime errorwill occur. Since variableX is pointing to the String object once you reach the line where the Lengthproperty is called (and String, of course, has a property named Length), the code executes successfully.
117
Chapter 5: Chameleon Language
79796c05.qxd:WroxPro 2/23/08 8:07 AM Page 117
This is possible because a lot of work that is normally done at compile time is delayed until the programis run and performed at runtime. What we are seeing in this sample is essentially a dynamically typedproperty of Visual Basic. However, once Option Strict is set to On, Visual Basic behaves just like anyother statically typed language, and calling the Length property on variableX without declaring it as aString is no longer possible.
I am now going to set Option Strict to On and perform all the necessary modifications to the samplecode so that it can compile and execute properly. While this example is very simple, it serves to illustratesituations you might encounter in real production code.
First Attempt at Inferring a Variable’s Initial TypeThe first error that the compiler reports once Option Strict is set to On is telling you that you need toassign a type to variableX. Here is the message: “Option Strict On requires all variable declarations
Smell: Implicit Type DeclarationDetecting the SmellAfter resolving the Implicit Variable Declaration, use the compiler to detect variables andmembers that are declared without their types being specified. Place Option Strict Onat the top of the file, compile the project, and look for the following errors in the Errorwindow.
❑ “Option Strict On requires all variable declarations to have an ‘As’ clause.”
❑ “Option Strict On requires all Function, Property, and Operator declara-tions 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 thepreceding error.
Related RefactoringUse Set Option Strict On refactoring to eliminate this smell.
RationaleFailing to declare variable type explicitly means that you will not be able to exploitsome very productive Visual Studio features, namely IntelliSense and Dynamic Help.Code in which variable and member type are declared explicitly outperforms code inwhich type is resolved at runtime. Code readability benefits from explicit type declara-tion. Ultimately, variable and member type being declared explicitly facilitates struc-turing and organizing your code by the means of standard object-oriented constructslike classes, interfaces, inheritance, and so on.
118
Part II: Preliminary VB Refactorings
79796c05.qxd:WroxPro 2/23/08 8:07 AM Page 118
to have an ‘As’ clause.” In this case, based on first literal value in the code, you can declare variableXas 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 typeInteger 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 justsqueeze this number into the Integer. So either you go back and declare variableX as Long, or youintroduce a new variable.
Declaring variableX as Long will work in this case. However, in some other, albeit rare, cases, in whichreflection 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 con-text, a new variable should be introduced to compile the code in a strictly typed environment. By goingback to the example I will show you how this can work out in practice.
Introducing a New Variable for a New Type: the LongInstead 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 overusedand is probably the result of diverse roles being given to that variable. This can be very confusing for thecode reader. Misuse of a local variable can go hand in hand with the Long Method smell. You have alreadyseen 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 nameour new variable variableXLong. Take a look at the Visual Studio IDE and how you can activate thisoption of the Refactor! add-in. Using the Split Temporary Variable refactoring by the Refactor! add-in tointroduce 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 codeto strict code. Split Temporary Variable refactoring is also refactoring in its own right, and I’ll deal withit later on in Chapter 10.
119
Chapter 5: Chameleon Language
79796c05.qxd:WroxPro 2/23/08 8:07 AM Page 119
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 movedanother step forward in introducing Option Strict.
Dealing with the DoubleNext 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 solutioneliminates 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 isDouble. Refactor! replaces all instances of variableXLong with variableXDouble for the rest of the codein the method.
That’s one error more eliminated, and one more to go.
120
Part II: Preliminary VB Refactorings
79796c05.qxd:WroxPro 2/23/08 8:07 AM Page 120
Dealing with the StringNow 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, thistime the transformation has to be performed manually, because I have reached the last assignment tovariableXDouble. 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 Activatedand the Code Converted to Strict Form
Option Strict On
Module Module1Sub Main()
Dim variableX As Integer = 10Console.WriteLine(“Type: “ & _variableX.GetType.Name.ToString)
Dim variableXLong As Long = 100000000000000000Console.WriteLine(“Type: “ & _variableXLong.GetType.Name.ToString)
Dim variableXDouble As Double = 11.11Console.WriteLine(“Type: “ & _variableXDouble.GetType.Name.ToString)
Dim variableXStr As String = “” & variableXDoubleConsole.WriteLine(“Type: “ & _variableXStr.GetType.Name.ToString)
Console.WriteLine(variableXStr.Length)
Console.ReadLine()End Sub
End Module
121
Chapter 5: Chameleon Language
79796c05.qxd:WroxPro 2/23/08 8:07 AM Page 121
Finally, after we execute the code, the resulting Console output is identical to the output from the beginning:
Type: Int32Type: Int64Type: DoubleType: String5
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 newvariable to hold this new value. Now I want to look at the other ways you can discover the initial typefor the variable.
Inferring Variable TypeWhen 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 inves-tigate different techniques you can apply in order to infer the type of the variables used in relaxed code.
Using Literals to Infer Variable TypeFirst 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 typicalchoice that will work on most occasions. You should make sure to take the type range into account whendeciding on the right type.
Table 5-2: Different Forms of Literals Usage and Type Inference
Declaration without the Type Complete Variable Declaration
Dim name = “John” Dim name As String = “John”
Dim year = 1997 Dim year As Byte = 1997Dim year As Short = 1997Dim year As Int16 = 1997Dim year As UInt16 = 1997Dim year As Integer = 1997Dim year As Int32 = 1997Dim year As UInt32 = 1997Dim year As Long = 1997Dim year As Int64 = 1997Dim year As UInt64 = 1997
Dim price = 185.56 Dim price As Single = 185.56Dim 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
122
Part II: Preliminary VB Refactorings
79796c05.qxd:WroxPro 2/23/08 8:07 AM Page 122
You may notice that in some cases more than one applicable type is available. How do you decidewhich one to use? The solution is to take type range into account. Generally, you are a lot less likely togo 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 isbecause the Byte type can only represent values between 0 and 255. While you might choose to repre-sent the variable year with the Single type, because it is not likely that it will come out of range whenrepresenting the year, you can’t always make such assumptions. Another danger involved in makingassumptions like this, as I already mentioned, is winding up with code that uses type information aspart 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 atruntime. 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 thenumber in question can be contained in Int32. For a very big literal value, out of the range of Int32, anInt64 type is used. Unless you are working on some very memory-intensive application, there is noneed 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 TypeVisual 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
Continued
Function Function Return Type
CBool Boolean
CByte Byte
CChar Char
CDate Date
CDbl Double
CDec Decimal
CInt Integer
CLng Long
123
Chapter 5: Chameleon Language
79796c05.qxd:WroxPro 2/23/08 8:07 AM Page 123
Table 5-3: Types Returned by Conversion Functions (continued)
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 twoparameters — the first is the variable, and the second is the targeted type. This makes the return typequite obvious: it is stated as a second parameter in the function call. The code:
Dim variableXvariableX = CType(variableY, SqlConnection)
is modified into this:
Dim variableX as SqlConnectionvariableX = CType(variableY, SqlConnection)
For one final way to infer variable type, I want to take a look at external assemblies and the way thoseassemblies are consumed.
Using External Assemblies to Infer Initial Variable TypeWhen you use external assemblies, you create objects and receive objects as the return values of methodsand 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 SqlConnectionDim Cmd = Conn.CreateCommand()
Function Function Return Type
CObj Object
CSByte SByte
CShort Short
CSng Single
CStr String
CUInt UInteger
CULng ULong
CUShort UShort
124
Part II: Preliminary VB Refactorings
79796c05.qxd:WroxPro 2/23/08 8:07 AM Page 124
Moving to the strict mode modifies this code as follows:
Dim Conn As SqlConnection = New SqlConnectionDim Cmd As SqlCommand = Conn.CreateCommand()
However, in most cases you should strive not to depend on specific implementation, but on moregeneric constructs. Often interfaces are used to separate abstraction from implementation. In this spe-cific case SqlConnection and SqlCommand are types belonging to a specific implementation of theADO .NET data provider for MS SQL Server. If your code is not using any other specific membersbelonging to SqlConnection or SqlCommand, and all invoked members are also defined in interfacesthat these two classes implement, you are better off declaring your variables by using the correspon-ding interfaces as a type. The code will then look like this:
Dim Conn As IDbConnection = New SqlConnectionDim Cmd As IDbCommand = Conn.CreateCommand()
Now, in case you need to replace the data provider because you are using a different data store, youneed 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.
Putting It All Together with Type-Conversion FunctionsAfter you have done your best to infer all variable types, you might still end up with some problemsstemming from implicit type conversion. After Option Strict is set to On, only widening conversionsare allowed in implicit form, and any narrowing conversion has to be explicitly written in code.
Smell: Implicit Narrowing ConversionsDetecting the SmellUse the compiler to detect implicit narrowing conversions. After resolving the ImplicitType Declaration smell, place Option Strict On at the top of the file, compile theproject, 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 theEditor window if an undeclared variable is present.
Related RefactoringUse Set Option Strict On refactoring to eliminate this smell.
RationaleIf narrowing conversions are allowed and performed quietly, a number of bugs canarise as a result of narrowing and rounding errors. For example, the following lineswill compile under Option Strict Off:
Dim varX As Double = 1234.99Dim varY As Long = varXConsole.WriteLine(varY)
However, the output will show 1235 because of rounding during the transformation ofthe number into an integer.
125
Chapter 5: Chameleon Language
79796c05.qxd:WroxPro 2/23/08 8:07 AM Page 125
You have already seen predefined conversion functions and how they can be used to infer initial variabletype. Now they can come in handy to perform explicit conversion. I want to illustrate that with somecode. The following snippet compiles with Option Strict set to Off:
Option Strict Off‘...Dim varX = 123Dim varY = “456”Dim varZ = 123456.789varX = varZDim result = varX + varYDim 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 = 123Dim varY As String = “456”Dim varZ As Double = 123456.789 ‘Implicit conversion from Double to Integer not allowedvarX = varZ ‘Implicit conversion from String to Integer not allowedDim result As Integer = varX + varY ‘Widening conversion from Integer to Long is allowedDim varLong As Long = varX
In order to resolve these errors, you use conversion functions to perform conversions explicitly. Inthis 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 = 123Dim varY As String = “456”Dim varZ As Double = 123456.789varX = CInt(varZ) ‘CInt conversion function at workDim result As Integer = varX + CInt(varY) Dim varLong As Long = varX
Explicit Variable-Type Conversion with CTypeI 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 useCInt(varX) or CType(varX, Integer). However, CType can actually do a bit more. It enables you to con-vert 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 instrictly typed code is to retrieve an instance from a nongeneric collection. (Standard, nongeneric collectionsare found in the System.Collections namespace.) Because a collection always returns an object, you can
126
Part II: Preliminary VB Refactorings
79796c05.qxd:WroxPro 2/23/08 8:07 AM Page 126
use CType to cast an instance into the correct type. For example, in the following snippet I cast an objectobtained 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 TryCastTwo 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 throwsInvalidCastException while TryCast will return nothing.
Basically, by using DirectCast or TryCast you can accomplish a downcast. A downcast is a conversionbased on inheritance or interface implementation. Remember the classes Patient, MalePatient, andFemalePatient 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 youare certain that the patient variable points to an instance of MalePatient, you can write the codewithout 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 withoutany 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, becomefamiliar with various ways to move your use of variable type from implicit to explicit to create strictercode. Of course, type doesn’t relate only to variables, so now as you move on in the chapter you will seehow all this relates to other typed class elements.
Dealing with Methods, Fields, Properties, and Other MembersSo far, most of the samples have used local variables to illustrate type inference. While all the describedtechniques apply to class and instance members, the situation can get a bit more complicated whenyou’re dealing with properties, methods, and events.
Visual Basic with Option Strict set to Off enables you to write methods without having to specifyparameter 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 requiressome radical changes to the way a method is written. To show some typical cases, I’ll start off with thesample code in Listing 5-4.
127
Chapter 5: Chameleon Language
79796c05.qxd:WroxPro 2/23/08 8:07 AM Page 127
Listing 5-4: A Method with Ambiguous Parameter Types
Public Function NumberOfRecords(ByVal data, ByVal dataFormat)If dataFormat = “xmlNode” Then
Return data.ChildNodes.CountElseIf dataFormat = “dataTable” Then
Return data.Rows.CountEnd If
End Function
In this case the parameter data can be of the System.Xml.XmlNode or System.Data.DataTable datatype, 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 isexecuted and desired result obtained.
Overloading Methods to Resolve an Ambiguous Parameter-Type SituationYou can start out by inferring the return type of the method. In order to do that, you inspect the signatureof the Count property of the ChildNodes property of XmlNode. Next, you inspect the Count property ofthe 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 oncomparisons of the dataFormat parameter with string literals. So far so good — the method’s signatureshould 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 beeither XmlNode or DataTable. To determine which, you can divide this method into two new overloadedmethods. One contains code applicable to XmlNode, and the other code applicable to DataTable. (Theprocess of splitting the method into two is explained in detail when we look at Extract Method refactoringin 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.CountEnd Function
Public Function NumberOfRecords(ByVal data As XmlNode, _ByVal dataFormat As String) As Integer
Return data.ChildNodes.CountEnd Function
This way, once the client code is strongly typed, the compiler will use information on the data parametertype to dispatch the call to the correct overloaded version of the method. Going one step further, you cannow notice that the dataFormat parameter has no use for us anymore. If you have access to all clientcode you can simply eliminate this parameter. If you do not have access to all client code you can main-tain methods with the dataFormat parameter, but you can also mark them with the Obsolete attributeand redirect the call to the overloaded versions of the method without the dataFormat parameter. Theresulting code is shown in Listing 5-6.
128
Part II: Preliminary VB Refactorings
79796c05.qxd:WroxPro 2/23/08 8:07 AM Page 128
Listing 5-6: Overloaded and Obsolete Methods as Solution to Ambiguous Parameter Types Method
<Obsolete(“dataFormat parameter not needed”)> _Public Function NumberOfRecords(ByVal data As DataTable, _ByVal dataFormat As String) As Integer
Return NumberOfRecords(data)End Function
<Obsolete(“dataFormat parameter not needed”)> _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 IntegerReturn data.Rows.Count
End Function
Public Function NumberOfRecords(ByVal data As XmlNode) As IntegerReturn data.ChildNodes.Count
End Function
This takes care of resolving a situation in which parameter type is ambiguous, but the method returntype can be ambiguous also. The next section shows how to act in that case.
Introducing New Methods to Resolve an Ambiguous Return TypeVisual Basic will not let you overload methods based solely on the return type. In cases where a methodreturns different types, you cannot use method overloading as a solution. Check out the followingmethod, which has an ambiguous return type:
Public Function GetData(ByVal store)If store = “Xml” Then
Dim node As XmlNodenode = GetNode()Return XmlNode
ElseIf store = “ADO” ThenDim table As DataTabletable = GetTable()Return table
End IfEnd Function
The solution in this case is again to first split this method into two. One method will return XmlNode andthe other DataTable. You can take a look at the related Extract Method refactoring in Chapter 9 in orderto 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 withcode looking like this:
Public Function GetDataAsXmlNode() As XmlNodeDim node As XmlNodenode = GetNode()
129
Chapter 5: Chameleon Language
79796c05.qxd:WroxPro 2/23/08 8:07 AM Page 129
Return nodeEnd Function
Public Function GetDataAsDataTable() As DataTableDim table As DataTabletable = GetTable()Return table
End Function
If you have access to all client code, you can replace all calls to GetData with calls to GetDataAsXmlNodeor GetDataAsDataTable, depending on the type that the client code expects. With that, you have resolvedthe issue.
In case you do not have access to all client code, you can keep the original GetData method. You modifythe 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 environ-ment, you need to add an As clause to the GetData method declaration. The problem is that in one case itreturns XmlNode and in another DataTable. So you can opt for the common base type of XmlNode andDataTable. Because in .NET everything derives from System.Object, you can make this method returnObject. And you can infer that the store parameter type is String. The final version of the GetDatamethod is shown in Listing 5-7.
Listing 5-7: A Method with an Ambiguous Return Type is Marked with the ObsoleteAttribute and Returns an Object for Compatibility Purposes
<Obsolete(“Use GetDataAsXmlNode or “ & _“GetDataAsDataTable instead”)> _Public Function GetData(ByVal store As String) As Object
If store = “Xml” ThenReturn GetDataAsXmlNode()
ElseIf store = “ADO” ThenReturn GetDataAsDataTable()
End IfEnd 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 andevents. 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, yougenerally end up splitting properties into two.
Of course, this section does not cover all possible situations you can come upon. Some situations willrequire 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. Thatway you scale down your problem and solution to more problematic situations that then become evi-dent. In my experience, it seems that in most cases programmers do declare the variable type, and afterOption Strict has been set to On, only a small number of undeclared type variables will crop up.
130
Part II: Preliminary VB Refactorings
79796c05.qxd:WroxPro 2/23/08 8:07 AM Page 130
Continued
Refactoring: Set Option Strict On (Enforce Variable Type Declaration and Explicit Type Conversions)
MotivationVisual Studio IntelliSense and Dynamic Help can be activated only if the variable andmember 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 narrowingtype conversions. This means that all narrowing and rounding has to be performeddeliberately, by the programmer, by means of conversion functions. This means that a number of bugs resulting from rounding and narrowing errors can be avoided. Byexplicitly declaring types you improve code readability, because you can think in theform of known abstractions and types. Finally, declaring types explicitly leads to someperformance 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 SmellsUse this refactoring to eliminate Implicit Type Declaration and Implicit NarrowingConversions smells.
MechanicsThe Option Strict statement is module-level only. Option statements have to be thefirst statements in the file. Set Option Explicit On refactoring should precede thisrefactoring. Start by inferring and declaring types explicitly for local variables. Firstdeal 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 ambigu-ous, where one variable’s underlying type is changed during the variable lifetime.After resolving local variable type, declare class and instance member types. Since typeinformation for the local variable is now available, you have more information to go byin 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 ambiguousmethod return type.
Use the Obsolete attribute to mark method versions for which the Object type isused, in order to maintain client compatibility. The Obsolete method should only del-egate calls to strongly typed method versions.
Finally, use a conversion function to explicitly convert between types. Explicit conver-sion is required for narrowing conversions only.
131
Chapter 5: Chameleon Language
79796c05.qxd:WroxPro 2/23/08 8:07 AM Page 131
Make Explicit Refactoring in Refactor!If you are using Visual Studio 2008, you will be able to use Refactor! to automate the refactoring I justdescribed. Refactor! can infer type and add the As part of the declaration statement for any variable thathas been initialized with the value. This refactoring can be applied to both local, method-level variablesand fields.
In order to activate it, place the cursor on the variable name in the variable declaration statement with-out 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 withBtnDelete_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 Explicitwas introduced.
BeforeOption Explicit On ‘...Public Function BasketTotal()
Dim total Dim item For Each item In Basket
total += item.PriceNextBasketLog.WriteEntry(total, _EventLogEntryType.Information, myeventid)Return total
End Function
AfterOption Explicit On‘Option Strict On statement added Option Strict On‘ ...‘Declare method return type Public Function BasketTotal() As Double
‘Declare local variable typeDim total As DoubleDim item As ProductFor Each item In Basket
total += item.PriceNext‘Explicit conversion through CStr conversion functionBasketLog.WriteEntry(CStr(total), _ EventLogEntryType.Information, myeventid)Return total
End Function
132
Part II: Preliminary VB Refactorings
79796c05.qxd:WroxPro 2/23/08 8:07 AM Page 132
Figure 5-2
While the code seemed to promise an easy time when it came to activating Option Strict, a surprise waswaiting for me. Because I wanted to keep the code as data-store-neutral as possible, I chose to declare oCnas IDbConnection from the System.Data namespace, hence limiting the dependency on the MicrosoftSQL data provider to Connection and Command creation code. So far, so good. However, once I tried todeclare oCmd as IDbCommand, the compiler reported the following error: “‘AddWithValue’ is not a memberof ‘System.Data.IDataParameterCollection.’”
The class that implements a certain interface has to implement all the members declared in that inter-face, but it is free to declare additional members. In this case, the AddWithValue method is a member ofSystem.Data.SqlClient.SqlCommand, but it is not a member of System.Data.IDbCommand. It seemsthat Microsoft provided a convenient way to add command parameters, but it is exposed only if youwork directly with the SqlCommand type. So I need another way to add parameters to a command objectin a provider-neutral way.
So the code is using some MS SQL provider-specific methods. While adding the parameter to a commandthis way takes a single statement, thanks to a convenient AddWithValue method, depending on a specificprovider implementation this way can soon grow out of proportion and can prevent you from making yourcode data-agnostic. I decided to try to modify the code so that it uses IDbCommand methods only. This meansadding a few lines and using the IDbCommand.CreateParamter and IDbCommand.Parameters.Addmethods. Finally, I ended up with code listed in Listing 5-8.
Listing 5-8: The FrmBranch Class BtnDelete_Click Method from the Rent-a-WheelsApplication, 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
Chapter 5: Chameleon Language
79796c05.qxd:WroxPro 2/23/08 8:07 AM Page 133
Listing 5-8: The FrmBranch Class BtnDelete_Click Method from the Rent-a-WheelsApplication, After Option Strict is Set to On (continued)
Dim oCn As IDbConnection ‘ oCmd declared as IDbCommandDim oCmd As IDbCommand On Error GoTo ErrorHandler‘Create SqlConnectionoCn = New SqlConnection(“Data Source=TESLA-DAN;” + _“Initial Catalog=RENTAWHEELS;User ID=sa”)oCmd = New SqlCommand‘strSql declared as StringDim strSql As String = “Delete Branch “ + _
“Where BranchId = @Id”‘Adding command paramter in provider-neutral mannerDim Id As IDbDataParameter = oCmd.CreateParameter() Id.ParameterName = “@Id” Id.DbType = DbType.Int32 Id.Value = CInt(TxtId.Text) oCmd.Parameters.Add(Id) ‘open connectionoCn.Open()‘Set connection to commandoCmd.Connection = oCn‘set Sql string to command objectoCmd.CommandText = strSql‘execute commandoCmd.ExecuteNonQuery()‘close connectionoCn.Close()‘destroy objectsoCmd = NothingoCn = NothingFrmBranch_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 oftenneed to weigh pros and cons. In this case I think it is more important to keep the application databaseneutral. Also, I have the feeling I’ll find a way to write this code more efficiently once I have the applica-tion well through the refactoring process.
Now that you have looked into converting code written without type declarations and conversions intocode for which types and conversions are declared explicitly, I want to take a look at some other interestingVisual Basic characteristics related to the problems of strong static code versus weak dynamic code. In nextsection I cover some situations where it can actually be beneficial to write code without declaring variableand member type.
134
Part II: Preliminary VB Refactorings
79796c05.qxd:WroxPro 2/23/08 8:07 AM Page 134
Static Versus Dynamic Typing and Visual BasicIn Visual Basic .NET you can achieve late binding by combining the use of the Option Strict Offstatement with declaring variables as Object. Essentially, late binding means that variable type is notknown at compile time. This results in type checking being performed at runtime, which is why latebinding is also often referred to as dynamic typing. Because the type checking is limited to verifying thata certain class member is present in a late bound object, you can compile the code that will produce atype-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 deactivatedOption Strict Off #1
Module Module1Sub Main()
Dim engine As Engine = New EngineDim tree As Object = New Treeengine.Start()‘Calling Start method on Tree instancetree.Start()
End SubEnd Module
Public Class EnginePublic Sub Start()
‘...End Sub
End Class
‘Tree class does not have Start memberPublic Class Tree
Public Sub Grow()‘...
End SubEnd Class
You can compile this code successfully, but once you try to run it a MissingMemberException will bethrown. 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), youget a compile-time error with the following message: “Option Strict On disallows late binding.” You areno longer able to compile such code.
This can be a great help in preventing typos and similar errors. Just imagine that I have writtenengine.Stat() by mistake. The compiler is now helping me identify the error right away, duringcompilation: “Stat is not member of Engine.” And because IntelliSense is also present, there is only aslim 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, andthere is a reason for that. To get some historic perspective on late binding, you need to look at its rolein Visual Basic 6.
135
Chapter 5: Chameleon Language
79796c05.qxd:WroxPro 2/23/08 8:07 AM Page 135
Late Binding in Visual Basic 6 and PriorIn the COM era (Visual Basic 6 and prior), late binding often presented a workaround for COM-versioningand binary-compatibility issues. When performing early binding in referencing COM components, VisualBasic would bind to a specific version of a component. In COM, a different set of GUIDs was used to iden-tify a component. Each time a component interface was changed, meaning that a signature of any non-private member, a method, or a property was changed, a completely new GUID for that component wascreated. 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 ofthe 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 theprevious version had, even when containing new non-private methods the application would continueto work. For this to be accomplished, a variable had to be declared as Object and created using theCreateObject function with ProgId as a parameter. That meant that Visual Basic would check forthe existence of a method or a property only at runtime, during the execution. As long as the objectsupported the function or property in question, the code could execute without any problems.
Duck TypingThis 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 andquacks like a duck, it must be a duck.” As long as the object has the member that corresponds to what theclient 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 lan-guages emphasize the flexibility and productivity that can be obtained from this approach and praise itover the benefits that static type-checking can bring. This style generally works well when paired with unittesting that can create a much deeper safety net than static typing can. In dynamically typed languagesfunction calls are resolved at runtime, which permits treating objects in a polymorphic manner, but withoutany 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 Onstatement. This option did not exist in pre-.NET versions of Visual Basic. This means that in pre-.NET ver-sions of VB, anything declared as Object is late bound. However, in VB .NET, once you place the OptionStrict 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 parameterwas created as Excel.Worksheet or Word.Document, this code executes fine thanks to the fact that bothof these objects have both the CheckSpelling and SaveAs methods.
136
Part II: Preliminary VB Refactorings
79796c05.qxd:WroxPro 2/23/08 8:07 AM Page 136
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 classDim saveAsParams As Object() = {FileName} ‘Invoking method without parameters using reflectionofficeObject.GetType.GetMethod(“CheckSpelling”). _ Invoke(officeObject, Nothing) ‘Invoking method with parameter using reflectionofficeObject.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 — youcan also do with reflection. However, the quantity of code written will be very different. In this case, thefirst sample has only two lines, but the second, reflection-based version is significantly more verbose. Nowimagine 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 isduplicated. As you will see in Chapter 9, duplicated code is the number-one smell and has to be elimi-nated. 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 andWorksheet inherit the same base class or implement the same interface. As a matter of fact, ExtractInterface is a standard refactoring technique that I will discuss in Chapter 12. It means no duplicationand strict code at the same time.
However, in this case you do not have a source for the Office automation libraries, so working theproblem out this way is not possible. You cannot modify the Document or Worksheet class. You needto look for a different solution.
137
Chapter 5: Chameleon Language
79796c05.qxd:WroxPro 2/23/08 8:07 AM Page 137
Resetting Dynamic or Static Behavior at the File LevelAs I already mentioned, once Option Strict is activated, VB behaves as a completely staticallytyped language, on par with C# or Java. However, as a unique feature of VB compared to other staticor dynamic languages, VB lets you specify this option on the file level. This means that you can mixfiles written in static and in dynamic style in the same project. You have another level of flexibilitywhen you write code. The VB team from Microsoft sees this as a distinctive advantage of VB overstatic-only or dynamic-only languages, and some further enhancements of VB as a dynamically typedlanguage are planned for the next release of Visual Basic. Microsoft’s position on the issue is expressedin the following sentence by Erik Meijer and Peter Drayton: “Static typing where possible, dynamictyping 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 athttp://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 dynamicfrom 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 withthe 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. Somethingsimilar is not possible in VB without the use of services provided by the Reflection.Emit namespace. Soyou can be pretty sure that all types you are going to encounter existed at the moment of compiling the pro-gram. Even if some types are loaded dynamically, they were most probably compiled and created as statictypes. Consequently, there is actually a physical assembly or dll file containing these types. It is highly prob-able that these types were created intentionally to represent a certain defined entity, along usual OO designand 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 codesnippet, you have seen that this code interacted with two types: Word.Document and Excel.Worksheetfrom the Office automation library. Knowing all this, it makes sense to provide a statically typed wrapperover the dynamic code. The rest of the code will never know that for the moment static type checking wasturned off. Client code will treat the wrapper just as it would any other static type and will never knowthat you have used it to encapsulate dynamic style implementation.
Providing a Statically Typed Wrapper for Dynamic CodeIn the sample you are dealing with two types, Document and Worksheet, and they are exhibiting somecommon behavior. In the sample those are the CheckSpelling and SaveAs methods. However, sincethey 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 inthat interface, and make Document and Worksheet implement this interface. This is typical ExtractInterface refactoring, and I’ll talk about it in Chapter 12. This way, Document and Worksheet can beseen through a common interface and treated uniformly. So you can start by doing just that, writing anIOfficeWrapper interface to contain all the methods used in a common way (see Listing 5-10).
138
Part II: Preliminary VB Refactorings
79796c05.qxd:WroxPro 2/23/08 8:07 AM Page 138
Listing 5-10: A Statically Typed Interface for the Office Wrapper
Option Explicit On‘Static type checking turned onOption Strict On
Public Interface IOfficeWrapperSub 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 anew class to implement this interface. This new class will delegate calls to the original officeObject, andto be able to treat both the Worksheet and Document objects in the same way it will be written in dynami-cally 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 tothe officeObject in a private field. The CheckSpelling and SaveAs methods will only delegate a call toa reference of the original officeObject maintained as a field in the OfficeWrapper class. Listing 5-11shows the resulting code.
Listing 5-11: A Dynamically Typed Class Wrapper for the Office Wrapper Implementingthe IOfficeWrapper Interface
Option Explicit On‘Note Option Strict deactivatedOption Strict Off
‘ Class is implementing IOfficeWrapper interfacePublic Class OfficeWrapper
Implements IOfficeWrapper ‘Private field maintains a reference to Document or Worksheet objectPrivate wordDocOrExcelWorksheet As Object
Public Sub New(ByRef wordDocOrExcelWorksheet As Object)‘Constructor accepts reference to Document or Worksheet instance ‘and keeps it in a private fieldMe.wordDocOrExcelWorksheet = wordDocOrExcelWorksheet
End Sub
Public Sub CheckSpelling() Implements _IOfficeWrapper.CheckSpelling
‘CheckSpelling method delegates call to Document or WorksheetwordDocOrExcelWorksheet.CheckSpelling()
End Sub
Public Sub SaveAs(ByVal fileName As String) _Implements IOfficeWrapper.SaveAs
‘SaveAs method delegates call to Document or WorksheetwordDocOrExcelWorksheet.SaveAs(fileName)
End SubEnd Class
139
Chapter 5: Chameleon Language
79796c05.qxd:WroxPro 2/23/08 8:08 AM Page 139
Basically, the wrapper class delegates calls only to the original Document or Worksheet object. TheDocument 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 wrapperinterface, Option Strict is set to On. So now take a look at how you can make use of the wrapper. Thefollowing code refactors the original CheckSpellingAndSave method so the code is not duplicated andstrict typing is maintained.
‘Activate Option Strict in the wrapper client fileOption Strict On ‘...Public Sub CheckSpellingAndSave(ByVal officeObject As Object)
‘Create wrapper around officeObjectDim officeWrapper as IOfficeWrapper = _ New OfficeWrapper(officeObject) ‘Use wrapper to perform officeObject servicesofficeWrapper.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 redirectcalls from the officeObject parameter to officeWrapper.
In short, the dynamically typed wrapper class implements a statically typed wrapper interface anddelegates all calls to an instance of a polymorphic Document or Worksheet object. The Document orWorksheet object is accepted as a parameter by the wrapper constructor method at the moment ofwrapper creation. Figure 5-3 shows a statically typed wrapper interface implemented by a dynami-cally typed wrapper class.
Figure 5-3
Presuming that this is not the only place you’ll use the Office automation objects, the cost of creating andusing the wrapper in order to activate strict typing is quite reasonable. Now you can place Option StrictOn on any file except the file containing the OfficeWrapper source. You have managed to isolate dynamiccode from the rest of the application, minimizing its impact while preserving the benefits of dynamic codeand avoiding code duplication.
<<delegates>>
1 1
<<Option Strict On>>IOfficeWrapper
+CheckSpelling()+SaveAs()
<<Option Strict Off>>OfficeWrapper
+New(in documentOrWorksheet : Object)
Document or Worksheet?
+CheckSpelling()+SaveAs()
140
Part II: Preliminary VB Refactorings
79796c05.qxd:WroxPro 2/23/08 8:08 AM Page 140
Activating Explicit and Strict Compiler Options
So far, you have seen in great detail the effect Option Strict and Option Explicit have on code.However, I didn’t dedicate too much time to the ways you can control these options. There are few alter-natives available to you in this regard, and I will investigate them in section to follow.
Just imagine you are starting to work on a new project. You have no need to permit undesirable style inyour code if there is a way to prohibit it right from the beginning. However, a number of alternatives areavailable to you for setting the behavior of VB the compiler in regards to Option Explicit and OptionStrict, to help you regulate the compiler’s permissive behavior.
Further, there is more than one place in which you can set compiler options. However, the effect will not always be the same. Therefore, now is a good time to clarify how those settings work and which take precedence.
Setting Options in the Project PropertiesA Visual Basic project has a set of properties that you can configure in the Project Properties window inVisual Studio. You can open this window by right-clicking the project item in the Solution Explorer windowand selecting the Properties menu item. Alternatively, you can select Visual Studio ➪ Project ➪ Properties.
Once the Project Properties window is open, you need to select the Compile tab from the list of the tabson the left-hand side. The window shown in Figure 5-4 will appear. You can observe how the ProjectProperties window enables the configuration of project-wide compiler option settings.
Figure 5-4
141
Chapter 5: Chameleon Language
79796c05.qxd:WroxPro 2/23/08 8:08 AM Page 141
In the “All configurations” section on this page, you will set Option Explicit to On and Option Strictto On. Once you save the new settings, you can inspect the content of the ProjectName.vbproj file in thetop project folder. You need to open this file with some XML or text editor like Notepad — but not withVisual Studio, because this will only load the whole project into the Visual Studio environment and whatyou want is to inspect the actual content of the file. If you take the look at the file’s Last Modification date,you can see that it was modified after you changed the project properties. The file is written in XML format,and it is fairly easy to see how the options in the window relate to the tags in the file. However, after a care-ful search, the only setting related to compiler options is Option Strict written in this form:
<OptionStrict>On</OptionStrict>
Changing the Default Behavior of the Visual Basic CompilerAt this point you might wonder what happened to Option Explicit. This option was also turned on,wasn’t it? Well, the answer has to do with the default behavior of the VB compiler. In case you do not specifyeither of these two options, the VB compiler treats Option Explicit as On and Option Strict as Off bydefault. Therefore, you only need to specify Option Explicit to the compiler if you want to turn it off andOption Strict if you want to turn it on.
Fortunately, there is a way to change the default option values that are loaded when the project is created. That way you do not have to open and set Option Strict and Explicit On each time you create a new project; these options will be turned on by default. To do this, you need to go into VisualStudio and select the Tools ➪ Options. This will open a window for setting different options in theVisual Studio environment. In the tree on the left side of the window you need to locate Projects andSolutions ➪ VB Defaults. Now you can set the project default for Option Explicit and OptionStrict to On. Figure 5-5 shows the Options window in Visual Studio that permits the configuration of default compiler option settings.
This way, each time you create a new project, that project comes up with Option Strict and OptionExplicit set to On by default.
However, the story does not end there. You still have a couple of other ways to control Option Strictand Option Explicit.
Figure 5-5 142
Part II: Preliminary VB Refactorings
79796c05.qxd:WroxPro 2/23/08 8:08 AM Page 142
Setting Options in Source FilesThere is another place beside the project file to specify Option Explicit and Option Strict. As yousaw earlier in this chapter, you can write these settings in code as a first statement in each source codefile. The files in question have a vb extension. In this case, settings will affect only the code contained inthe same file as the statement. In other words, the scope of the Option statement is limited to the sourcecode file in which the statement is placed.
Visual Studio automatically adds a new file each time you add a class using the Add ➪ New Item optionin the Project menu. This is fine, because having one file per class is the most common way of organizingthe code.
The exception to this model of organizing code comes when a Visual Studio forms designer splits theclass into two by means of partial classes. This way, code generated by the tool is separated from codewritten by the programmer, and this can have a positive effect on code readability.
The fact that you have to repeatedly place statements might make you think that setting options in a sourcefile is impractical. You can have a large number of source code files in the project, so a lot of writing couldbe circumvented by relying on project settings.
However, relying on a project properties file is not always the best option. When you’re working in teamenvironment you might not have the necessary permissions to change the project properties. In anothersituation you might decide to reuse some of your classes at the source level by adding them as sourcefiles to an existing project. In that particular project, options might not enforce strict syntax. So the onlyoption left to you would be to add the Option Explicit On and Option Strict On statements ineach of the source files.
Using Item Templates to Set OptionsIn order to make the task of setting these options less tedious, there is a little trick you can use. When youadd an item to the project using Visual Studio, it comes with a few lines of code that are pre-generated byVisual Studio. This code is based on the item templates that come with the Visual Studio installation. Tohave the Option Explicit On and Option Strict On statements added to a new source file automati-cally, you can modify item templates in Visual Studio so they include these statements. (Another option permits the addition of new templates to the tool, but for the purpose of adding these statements you canmodify existing templates.) You’ll see this is not very complicated.
There’s no need to go into the details about the inner working of Visual Studio templates. Suffice it to saythat templates used to generate VB source code have at least two files, one with a vstemplate extensionand one with a vb extension. All you need to do in order to include the Option Explicit On and OptionStrict On statements in the desired template is to edit the file with the vb extension and place these state-ments on top of the file.
1. First, you need to locate the item templates. They are placed in your Visual Studio installationfolder along this path:
\Microsoft Visual Studio 8\Common7\IDE\ItemTemplatesCache\VisualBasic\1033
where Microsoft Visual Studio 8 is the Visual Studio 2005 installation folder.
143
Chapter 5: Chameleon Language
79796c05.qxd:WroxPro 2/23/08 8:08 AM Page 143
2. Once you open this location in Windows Explorer, you will see a list of directories. Each file represents a single VB item template. Browse the Class.zip folder. It contains two files:
❑ Class.vb
❑ Class.vstemplate
3. You want to edit the Class.vb file. Open it with Notepad, Visual Studio, or some other editorand add Option Explicit On and Option Strict On to the beginning of the file. Table 5-4shows the Class.vb file with and without the option settings.
Table 5-4: Class Item Template Content Before and After the Options Section is Added
4. Then you save the file.
Now each time you add a new class to the project that class will come with Option statements already inthe file. You can repeat this procedure for each .vb file you find inside any of the item template folders.
Class.vb without Option Settings Class.vb with Option Settings
Public Class $safeitemname$
End Class
Option Explicit OnOption Strict On
Public Class $safeitemname$
End Class
A few final words may be in order to address some doubts that might have beenaroused during the procedure I just described. You have probably noticed that thefiles I talked about are placed in the ItemTemplatesCache folder. And if you take alook in Windows Explorer, you will also see the ItemTemplates folder right next tothe ItemTemplatesCache folder. Does this mean that we have performed our modi-fications on some type of cache? If yes, is this cache temporary?
If you open for browsing the following folder:
ProgramFiles\Microsoft Visual Studio 8\Common7\IDE\ItemTemplates\VisualBasic\1033
you will see a number of zipped files. The name of each file coincides with a foldername in the ItemTemplatesCache directory. As a matter of fact, the Cache folder wascreated by extraction of the ItemTemplates folder during Visual Studio setup. VisualStudio lets you repeat some of the last steps performed during the Visual Studioinstallation process by invoking the Setup command on devenv.exe. In that case,ItemTemplatesCache will be replaced with the content from the ItemTemplatesfolder. If you for some reason need to repeat the Visual Studio setup process, youmight lose the changes we have just performed. In order to avoid this, you can unzipthe template file in the ItemTemplates folder, apply modifications on the vb files,and create the zip archive again. This way, even if you have to repeat some of the installation steps, you will not lose the changes that included the addition of the Option Explicit On and Option Strict On statements to the vb files.
Finally, a word of warning: if you do decide to modify the ItemTemplates folder,back it up before modifying the files, just in case something goes wrong.
144
Part II: Preliminary VB Refactorings
79796c05.qxd:WroxPro 2/23/08 8:08 AM Page 144
SummaryYou have just seen a number of implications that activating Option Strict and Option Explicit canhave on your code. When these options are activated, you must declare all variables and their types andperform all narrowing conversions explicitly. You have seen different ways to set these options in codeon the project level, and even how to change default settings in VB IDE.
You have seen a number of benefits that activating these two options can bring. However, in many situa-tions you are faced with existing code that has been written without these two options being active. It ispossible to convert this code into statically and strongly typed code. This requires a methodical and sys-tematic approach and can be cumbersome work, as it requires you to infer variable type based on con-text. Fortunately, you can count on some help from the compiler. You have even seen the first practicaluse of the Refactor! add-in and how it can help you in the undertaking by automating the task of split-ting the local variable.
Finally, you have seen some situations in which dynamic typing can actually be beneficial. I have alsodemonstrated how to make dynamically and statically typed code cohabitate, so you can benefit fromthe best of the both worlds.
In the next chapter you’ll continue to investigate refactorings specific to Visual Basic. I’ll discuss more sit-uations in which Visual Basic is giving you a lot of choice in the way you can program — but remember,not all choices are equally good. Often certain language elements are remnants from the past, and youshould avoid them. This is the topic of discussion in the next chapter, which deals with error handling.
145
Chapter 5: Chameleon Language
79796c05.qxd:WroxPro 2/23/08 8:08 AM Page 145
79796c05.qxd:WroxPro 2/23/08 8:08 AM Page 146
Error Handling: From Legacy to Structured
in a Few Easy Steps
Error handling in software is similar to an emergency exit in real life. Nobody usually gives it toomuch thought, since you almost never use it. However, once you need it, you definitely want it toperform impeccably. Emergencies and unexpected situations happen in software with more fre-quency than in real life, so the effect a badly handled exception can have on user confidence can be quite devastating. Nothing is more annoying for users than an application simply disappearingfrom the screen for apparently no reason at all, or receiving a number of cryptic messages insteadof having tasks performed.
Error handling is an essential element of well designed and robust code. While it deals with atypicaland unexpected situations, it is a crucial ingredient for providing a robust and reliable application.Successfully implemented error handling permits execution to resume without the application halt-ing or crashing. It will give the programmer a chance to inform a user about the current state of theapplication in an understandable way, instead of showing some cryptic system messages. In somecases the user can be consulted on the way the application should continue. Coupled with logging,error handling can also be used to save the information on the state of the system when the erroroccurred, providing the programmer with valuable insight into the cause of the error and the loca-tion in the code where it occurred. This greatly simplifies debugging and maintenance.
In this chapter you are going to examine two different styles of error handling: legacy and struc-tured. You will see why I prefer the structured style and how you can refactor legacy style intostructured. Here is what I plan to talk about in this chapter:
❑ I’ll start by recapping how legacy error handling is implemented in VB. This style of errorhandling uses error codes and the On Error statement.
❑ Then I’ll take you through a look at structured error handling. Since it uses exceptiontypes instead of error codes, it is also referred to as exception handling. Structured errorhandling is realized through the use of structured Try-Catch-Finally blocks.
79796c06.qxd:WroxPro 2/23/08 8:15 AM Page 147
❑ After I compare the two styles, you’ll see how structured error handling is superior and whyyou should always strive to stick with this style of error handling.
❑ Since you are bound to come across code that is still making use of legacy error handling, I willdemonstrate how you can convert legacy to structured error handling. You do this through tworefactorings. The first one involves replacing the On Error statement with Try-Catch-Finallyblocks and is accordingly called Replace On Error Construct with Try-Catch-Finally. Thesecond refactoring you’ll deal with in this chapter replaces error code with exception type. It’scalled Replace Error Codes with Exception Type.
I want to start by taking a look at the different ways error handling in VB is implemented and how thesestyles compare.
Legacy Error Handling Versus StructuredError Handling
In VB .NET you have two distinct styles of exception handling available to you. The first style has its origins in pre-.NET versions of VB, so I will refer to it as legacy or unstructured error handling. It uses thefamiliar On Error construct in combination with the Goto or Resume Next keyword.
With VB .NET you can also use the Try-Catch-Finally keywords. This style of error handling is calledstructured because error-handling code is organized in the form of structured blocks instead of by meansof a Goto statement that controls the flow of execution.
While both styles can be used and even mixed in VB .NET, only a single style can be used in a single pro-cedure. You can still use both styles in a single class, as long as they are applied to different procedures.Later on in this chapter, when you start dealing with the conversion of legacy error handling, you’ll seehow this coexistence can be very useful for performing conversion gradually.
First I want to take a look now at how each of these error-handling styles works.
Legacy (Unstructured) Error HandlingThis style of error handling predates VB .NET. Error information is available through the global Errobject. The most important properties of the Err object are Number, related with the cause of error, andthe description. You frequently encounter two flavors of this style of error handling.
The On Error Construct in Combination with the Goto Statement, Label, and Exit Sub Flavor
Placing an On Error Goto label line inside the method results in the execution flow continuing on thefirst line after the label in the case of an error. The label is generally placed just after the Exit Sub state-ment, so in normal flow, code below the label is never executed. It will be best if I illustrate this withsome code. Figure 6-1 shows unstructured error handling using the On Error Goto label.
148
Part II: Preliminary VB Refactorings
79796c06.qxd:WroxPro 2/23/08 8:15 AM Page 148
Figure 6-1
This is fairly simple code; all it does is write a few characters into a file. Two possible problems can occurwith this code:
❑ The file can be missing
❑ The file can be marked as read-only
In case of error, execution continues on the first line after the ErrorHandler label. This happens for anytype of error, so Err.Number has to be used to determine the cause of the error. This way, error handlingcan be adjusted depending on the type of error. In this case, depending on the type of error, a differentmessage is displayed.
The On Error Resume Next Statement FlavorPlacing an On Error Resume Next statement at the top of method results in execution resuming normalexecution flow in the case of an error, just as if the error never happened. Fortunately, error information iskept in the Err object, so Err.Number and other properties can be inspected for the presence of an error.Take a look at the previous example written in the Resume Next flavor (Figure 6-2).
In this case you can see If Err.Number =... blocks scattered throughout the code. As a matter of fact,this block should follow any line you are suspicious of. If you think an error can occur at some line, youshould check if Err.Number is different from zero on the following line.
The Resume Next flavor has error-handling code scattered throughout the procedure, while with theGoto label flavor, error-handling code is generally placed at the bottom of the procedure. This is the maindifference between these two flavors. It is time now to take a look at structured error handling.
Imports System.IO
Module OnErrorDemo
Sub Main() On Error GoTo ErrorHandler Dim writeTo As Stream = _ New FileStream("C:\SomeFile.txt", _ FileMode.Open) Dim fileWriter As TextWriter = _ New StreamWriter(writeTo) fileWriter.WriteLine("Hello!") fileWriter.Close() Exit SubErrorHandler: Select Case Err.Number Case 5 MsgBox("File access denied. " & _ "Check file and " & _ "directory write permissions.") Case 53 MsgBox("File SomeFile.txt not found.") End Select End SubEnd Module
Error handlingflow
File not found orread-only, jump to
label
Normal flow
Terminates normal flowCheck error type
149
Chapter 6: Error Handling
79796c06.qxd:WroxPro 2/23/08 8:15 AM Page 149
Figure 6-2
Structured Error HandlingWith structured error handling you use the Try-Catch-Finally keywords to organize code into the struc-tured blocks. Inside the Try block you place statements that can potentially produce error. In the Catchblock you place the code that deals with error. In the Finally block you place the code that you want toexecute always, whether an error occurs or not. This is generally cleanup code, used for closing and releas-ing different resources.
The Exception Is Represented by a Single ObjectIn structured error handling you do not use a global Err object as a source of error information. Instead,each error is represented by its own object. This object is referred to as an exception. This is why, as I statedearlier, in VB .NET you more commonly use the term exception handling to refer to error handling. Since eachof these objects is an instance of a specific exception class, you can use the type of the exception to distin-guish different causes of error. This, combined with capacity to catch specific exceptions in a Catch block,enables you to react to a specific exception without conditional code that inspects an Err.Number, as inunstructured error handling. In legacy error handling you had to check the type of exception with code:Select Case Err.Number... and If Err.Number = SomeNumber Then .... This is no longer neces-sary in structured error handling because a Catch block can be related to a certain exception type by meansof an As keyword. This way you can filter errors in Catch blocks.
If you do not specify the error class or if you catch System.Exception, any exception that occurs iscaught in that block. In this case, it is important to watch the order of Catch blocks. More specific excep-tions should be caught first. Accordingly, System.Exception as the most generic should be placed last. If you place System.Exception as the first Catch block by mistake, other Catch blocks will never be
Imports System.IO
Module OnErrorDemo
Sub Main() On Error Resume Next Dim writeTo As Stream = _ New FileStream("C:\SomeFile.txt", _ FileMode.Open) If Err.Number = 5 Then MsgBox("File access denied. " & _ "Check file and " & _ "directory write permissions.") Exit Sub ElseIf Err.Number = 53 Then MsgBox("File SomeFile.txt not found.") Exit Sub EndIf Dim fileWriter As TextWriter = _ New StreamWriter(writeTo) fileWriter.WriteLine("Hello!") fileWriter.Close() End SubEnd Module
Terminates Error Handling Flow
Error handlingflow
Normal flow,jump to bottomTerminates Error Handling Flow
150
Part II: Preliminary VB Refactorings
79796c06.qxd:WroxPro 2/23/08 8:15 AM Page 150
executed. System.Exception will be executed under any exceptional condition, and because only a singleCatch block is ever executed in a Try-Catch-Finally construct, other blocks will become redundant.
I just mentioned that System.Exception is the most generic exception. In the next section you’ll seehow exceptions can be more or less generic and the way the exception hierarchies can be formed.
Exception HierarchiesException classes are generally organized in inheritance hierarchies. At the root of the hierarchy isalways the System.Exception class. This organization enables you to catch exceptions at different levels of abstraction. Going back to the code sample used in this chapter, in some cases you might not care whether the exception was provoked because the file was read-only or the file was missing. It is enough to know that some type of file-access error occurred. In this case, both System.IO.FileNotFoundException and System.UnauthorizedAccessException have a common super-type in their hierarchies. It’s System.SystemException. You can deal with both errors in a singleCatch block if you catch System.SystemException. Take a look at the exception classes inheritancehierarchy in Figure 6-3.
Figure 6-3
You can clearly see in the figure how the first common base class for both System.IO.FileNotFoundException and System.UnauthorizedAccessException is System.SystemException. If you catch this exception in the form of the Catch exception AsSystem.SystemException, the Catch block will be executed no matter which of the two exceptions occurred.
System.Exception
System.SystemException System.ApplicationException
System.UnauthorizedAccessExceptionSystem.IO.IOException
System.IO.FileNotFoundException
151
Chapter 6: Error Handling
79796c06.qxd:WroxPro 2/23/08 8:15 AM Page 151
You can implement your own exceptions by writing classes that inherit from System.ApplicationException. On the other hand, the .NET Framework exception classes generallyinherit System.SystemException, and these errors are thrown by the Common Language Runtime(CLR). You can just as well inherit System.Exception, but the convention is that you should inheritApplicationException when implementing your own custom exceptions.
Now take another look at the code sample and see how it can be implemented by means of structurederror handling. This is illustrated in Listing 6-1.
Listing 6-1: A Sample of Structured Error Handling
Imports System.IO
Public Class OnErrorDemo
Shared Sub Main()
Dim fileWriter As TextWriter = NothingDim writeTo As Stream = Nothing‘Activate error handlingTry
writeTo = _New FileStream(“C:\SomeFile.txt”, _FileMode.Open)fileWriter = New StreamWriter(writeTo)fileWriter.WriteLine(“Hello!”)
‘Catch FileNotFoundExceptionCatch fileNotFound As FileNotFoundException
MsgBox(“File SomeFile.txt not found.”)‘Catch UnauthorizedAccessExceptionCatch unauthorizedAccess As UnauthorizedAccessException
MsgBox(“File access denied. “ & _“Check file and “ & _“directory write permissions.”)
‘Catch any other IO exception, beside FileNotFoundExceptionCatch IOException As System.IO.IOException
MsgBox(“Some file related exception occurred”)‘Catch any other exception, if neither FileNotFoundException ‘nor UnauthorizedAccessException nor any IOException occurredCatch exception As System.Exception
MsgBox(“Unexpected error occurred”)‘Execute always, no matter if exception occurred or notFinally
If fileWriter IsNot Nothing _Then fileWriter.Close()If writeTo IsNot Nothing Then _writeTo.Close()
End TryEnd Sub
End Class
152
Part II: Preliminary VB Refactorings
79796c06.qxd:WroxPro 2/23/08 8:15 AM Page 152
Right away this code is easier to follow and understand. Further, Try-Catch-Finally blocks are scoped.This means that variables declared inside these blocks are not visible to code on the outside. That is whyI had to declare fileWriter and writeTo variables outside of the Try block, because I accessed thosevariables in the Finally block. This helps you structure and modularize your code further.
And the story about structured error handling does not end here. For example, error-handling blockscan be nested. However, for the purpose of this chapter, you have enough detail about structured errorhandling. It’s time to compare it to legacy error handling.
Now you are basically familiar with both unstructured and structured error handling. So how do theymeasure up? Why was the new style of error handling introduced and is it really superior? Why shouldyou prefer structured error handling? Should you replace unstructured error handling with structurederror handling? The next section answers these questions.
The Benefits of Structured Error Handling
Structured Versus Unstructured CodeIn VB .NET, specific control structure for error handling was introduced. Instead of using an awkwardconstruct like a Goto keyword in combination with a label, you can use Try–Catch–Finally blocksinstead. The Goto keyword in combination with a label is an example of legacy, unstructured program-ming and can result in complicated spaghetti code with awkward jumps in execution flow that are diffi-cult to understand and follow. Similar problems are inherent to the Resume Next construct, in whicherror-handling code is scattered and can easily be left unimplemented. In contrast to this, if a Try blockis placed, at least one Catch block has to be implemented in order for code to compile.
Smell: Legacy (Unstructured) Error HandlingDetecting the SmellUse a simple text search to locate On Error keywords in your code.
Related RefactoringUse Replace On Error with Try-Catch-Finally refactoring to eliminate this smell.
RationaleLegacy (unstructured) error handling relies on unstructured Goto statements and labelsin code for execution flow control, resulting in awkward code that is difficult to under-stand and maintain. Additional effort has to be introduced in order to filter errors, addingunnecessary conditional statements to error-handling code. Unstructured error handlingdoes not permit easy implementation of cleanup code, achieved by means of Finallyblocks in structured error handling.
Improved error handling was for quite some time on the list of desired features of VB. Moving VB forward on the path of object orientation and making it fully .NET-interoperable required a new solution for error handling. New keywords were intro-duced, error handling was implemented in a structured manner, and errors wererepresented as class instances. This has significantly improved the way error handlingis implemented. The next sections take a look at the major advances achieved in VB .NET.
153
Chapter 6: Error Handling
79796c06.qxd:WroxPro 2/23/08 8:15 AM Page 153
Exceptions as Types, Not Numbers
In legacy error handling, error information was provided at runtime through a global Err object. In order todifferentiate between different types of errors, you had to look up an Err.Number property. The only way to throw and signal a custom error was to use a specific error number. Even when you are using enumera-tors to make these numbers more meaningful, code readability still suffers.
With structured error handling, exceptions are fully fledged programmable objects. The name of the classof the exception is much more descriptive than some error code. Furthermore, IntelliSense will naturallylend help when you are writing Catch exception As ... code.
Related to the programmability of exception classes is the manner in which custom exceptions can beimplemented. Custom exceptions are implemented by the extension of System.Exception or someother derived class within the Exception inheritance hierarchy. Some of the Exception members, likeMessage property, are then generally overridden in order to provide custom error information. Now,going even further in developing robust design of your system, if some elaborate error-handling mecha-nism is applied, custom exceptions can be enriched with some new members. For example, you mightadd the Time property to all exceptions in your application that could tell you the exact time the excep-tion was produced. In order to do this, it is enough to add this property to the custom Exception classat the top of your Exception inheritance hierarchy.
This results in another benefit. By being able to catch any exception along the inheritance hierarchy, you can avoid a lot of code duplication. If you can react in the same way to a group of errors, and theseerrors are all children of the same base class, it is enough to catch the base class exception instance onetime and to write the error-handling code just once.
Error FilteringRelated to the previous issue and the structured organization of error-handling code is the filteringcapacity of multiple Catch blocks. If we specify an exception type in the Catch block in the form of
Smell: Error CodeDetecting the SmellUse a simple text search to locate Err.Raise() and Catch When Err.Numberstatements in your code.
Related RefactoringUse Replace Error Code with Error Type refactoring to eliminate this smell.
RationaleThe legacy style of VB error handling uses error codes to signal the type of error. Thecodes are simple numbers and much more difficult to interpret than exception types.This has a negative impact on code comprehension even when error constants or enu-merations are used to make error codes more meaningful. Using the global Err object toraise an error limits the capacity to customize error objects, because the Err object can-not be extended. This style can also have a negative impact on the .NET interoperabilityof reusable components and libraries. Error code information cannot cross assemblyboundaries unless a predefined system error is raised, resulting in any but a systemerror appearing as a generic System.Exception in client code.
154
Part II: Preliminary VB Refactorings
79796c06.qxd:WroxPro 2/23/08 8:15 AM Page 154
Catch exception As ExceptionType, only if the specified exception type coincides with the cur-rent exception will the execution flow continue in that specific Catch block. Because of this, controlstructures like Select Case Err.Number ... and If Err.Number Then ... are no longer neces-sary. This adds to code simplicity and readability.
The Finally BlockIn unstructured error handling there is no simple way to have a piece of code execute both when anerror occurs and during normal execution flow. Such code, generally used for object cleanup and therelease of resources, can be crucial for providing robust and stable code that does not leak memory.With structured error handling, you can easily set up this type of code using the Finally block.
In legacy error handling, you cannot do this without using multiple Goto statements and labels, adding to code complexity and awkwardness. So, as a more simple approach, programmers working with legacyerror handling often ended up placing cleanup code both in normal execution and in error-handling code.Duplicated code is one of the most undesirable features in code. With the Finally block in structurederror handling, it can easily be avoided.
.NET InteroperabilityImagine you are developing a certain reusable piece of code, like a component or a class library. In thiscase exceptions should be a part of your contract with the library or component users. You should pro-vide them with comprehensive error information, so that they can react appropriately to any exceptionyour code can raise.
In legacy error handling you use the Raise method of the Err object in order to throw an error. This methodaccepts the Number parameter as the only non-optional parameter. Now, as long as you know the systemerror numbers, you can throw system exceptions by using Err.Raise(SystemErrorNumber). For exam-ple, Err.Raise(53) results in a FileNotFoundException exception thrown to the client code. VB runtimeautomatically translates Err.Raise(53) to the more .NET-friendly Throw New FileNotFoundException.This way, clients can use structured error handling and specify the type of error they wish to handle in aCatch block by using a Catch exception As FileNotFoundException statement.
However, once you try to raise any custom error using your own nonsystem error code, for example inthe form of Err.Raise(vbObjectError +10), a basic System.Exception is thrown and clients haveto handle System.Exception for any custom error code. There is no way for VB runtime to translatevbObjectError +10 to any meaningful exception type, so the generic System.Exception is thrown.Not really helpful to the client programmers.
On the other hand, if you stick to structured error handling and program your custom exceptions byinheriting System.Exception, the client receives much more complete information and will be able to filter different custom exceptions in multiple Catch blocks. That’s definitely a much more friendlyoption for client programmers.
The benefits of structured error handling do not end here, but all the same I think you have seen enougharguments in its favor. Are there any cases where legacy error handling is preferable? I don’t know of any.However, the chances are you’ll come across it. One sure source of this style of error handling is pre-.NETVB code. Migration tools leave error handling as is, because there is no way to automatically transformlegacy to structured error handling in a meaningful way. That’s all the more reason why you should
155
Chapter 6: Error Handling
79796c06.qxd:WroxPro 2/23/08 8:15 AM Page 155
explore the ways you can perform these transformations yourself. Because such transformations are con-cerned with improving the code without changing its behavior, they can rightfully be called refactorings.I’ll start off by examining how the On Error construct can be replaced with Try–Catch–Finally.
Replacing the On Error Construct with Tr y-Catch-F inally
Refactoring: Replace the On Error Construct with Try-Catch-FinallyMotivationStructured error handling is in many ways superior to legacy-style error handling:
❑ It uses structured constructs for execution flow control, as opposed to theunstructured Goto or Resume Next.
❑ It has dedicated a error-filtering facility in the form of the Catch excep-tion As ExceptionType construct.
❑ It uses types, not numbers, to represent exceptions.
❑ It improves the .NET interoperability of Visual Basic code.
Related SmellsUse this refactoring to eliminate the Legacy (Unstructured) Error Handling smell.
MechanicsStart by identifying error-protected and error-handling code. After that, perform thefollowing steps:
1. Replace On Error ... with a Try statement. Mark the end of the pro-tected code with a Catch statement. Move the error-handling code insidethe Catch block.
2. If the error-handling code is conditioned by the type of error with the com-bination of Select Case or If statements and Err.Number, replace con-ditional code with Catch When Err.Number = ... blocks.
3. Because Try–Catch–Finally blocks are scoped, variables have to bedeclared outside the Try block if you need to access them as a part of yourerror-handling routine in Catch or Finally blocks. You might need tomove the variable declaration outside the Try block.
4. If duplicated statements are inside the error-protected and error-handlingcode, add a Finally block and move these duplicated statements to theFinally block. This way you eliminate some duplication in your code.
BeforeImports System.IO
Public Class OnErrorDemo
156
Part II: Preliminary VB Refactorings
79796c06.qxd:WroxPro 2/23/08 8:15 AM Page 156
Shared Sub Main()On Error GoTo ErrorHandlerDim writeTo As Stream = _New FileStream(“C:\SomeFile.txt”, _FileMode.Open)Dim fileWriter As TextWriter = _New StreamWriter(writeTo)fileWriter.WriteLine(“Hello!”)fileWriter.Close()Exit Sub
ErrorHandler:Select Case Err.Number
Case 5MsgBox(“File access denied. “ & _“Check file and “ & _“directory write permissions.”)
Case 53MsgBox(“File SomeFile.txt not found.”)
End SelectEnd Sub
End Class
AfterImports System.IO
Public Class OnErrorDemo
Shared Sub Main()Dim fileWriter As TextWriter = NothingDim writeTo As Stream = NothingTry
writeTo = _New FileStream(“C:\SomeFile.txt”, _FileMode.Open)fileWriter = _New StreamWriter(writeTo)fileWriter.WriteLine(“Hello!”)fileWriter = NothingfileWriter.Close()
Catch When Err.Number = 5MsgBox(“File access denied. “ & _“Check file and “ & _“directory write permissions.”)
Catch When Err.Number = 53MsgBox(“File SomeFile.txt not found.”)
FinallyIf fileWriter IsNot Nothing Then _fileWriter.Close()If writeTo IsNot Nothing Then _writeTo.Close()
End TryEnd Sub
End Class
157
Chapter 6: Error Handling
79796c06.qxd:WroxPro 2/23/08 8:15 AM Page 157
There is no doubt that structured error handling is making our code more robust, more maintainable,and easier to comprehend. Therefore, it is worthwhile to invest time to replace legacy with structurederror handling. At this point, the only question is how difficult and costly this can be.
I want to demonstrate how this can be performed in a few relatively simple steps and can actually be simpler than it might look at first glance. I’ll illustrate these steps on both flavors of legacy error handling.Before I can do this, however, I want you to examine one less common Visual Basic keyword that will beof huge help in this task.
Understanding the When KeywordYou have already seen how it is possible to write different Catch blocks for different types of exceptions.The only block that is executed is the one in which the declared exception type coincides with the type of exception that has been produced. You have used Catch exception As SomeException syntax toachieve this.
It is also possible to condition the execution of the Catch block. You do this by combining the Catchblock with the When keyword. In this case the Catch block executes only if the expression following theWhen keyword evaluates to true. For example, the following Catch block will be executed if the vari-able NumberOfTries represents a value greater than three:
Try‘Some error-protected codeCatch When NumberOfTries > 3‘Some error handling code
Because the Err object is always present, even if you do not use the On Error construct, you can combinethe When keyword with the Err.Number property in order to condition Catch blocks. So you can write thecode similar to this:
Try‘Some error-protected codeCatch When Err.Number = 5‘Some error handling codeCatch When Err.Number = 53‘Some error handling code
At this point you may already be guessing what use you are going to give to the When keyword in thischapter, so now I want to turn to the steps you need to perform in order to convert legacy error-handlingcode to structured error-handling code.
Refactoring Steps for Replacing On Error with Try-Catch-Finally
More complex refactorings are often composed from simple ones. In that case, it is important to bear in mind that those smaller refactorings also have to behave as complete and atomic refactorings. Thismeans that each step of the way during a complex refactoring, you want to still end up with code youare able to compile and that performs in the same way the original code did. Remember the refactoringdefinition from Chapter 1? No change in behavior in the code will take place. This focus on atomic and
158
Part II: Preliminary VB Refactorings
79796c06.qxd:WroxPro 2/23/08 8:15 AM Page 158
complete smaller refactorings also helps you in being able to execute tests and ensure progress eachstep of the way of complex refactoring.
What then are the steps you need to perform when transforming legacy error handling to structurederror handling? You start out by identifying the error-protected code that executed under normal circumstances and the code that deals with error. After that you perform the following steps:
1. Replace the On Error structure with Try–Catch so the code that was protected from the error isnow placed inside a Try block and the code that dealt with error is placed inside a Catch block.
2. Replace error-filtering code that uses the Select Case or If construct with the Catch Whenconstruct.
3. This step deals with two situations related to cleanup code:
❑ Cleanup code is duplicated and present both inside error-protected code executed dur-ing normal execution and inside error-handling code. Place code that should be executedboth during normal execution flow and in case of error inside the Finally block. In legacyerror handling, this placing of the cleanup code both inside the error-protected code andinside the error-handling code was a typical way of implementing error handling. If code is present in both places, you can extract this code and place it inside the Finally block.
❑ Cleanup code is not present or is present only in normal, error-protected code.Optionally, you can add or move the cleanup code to the Finally block. This step isoptional and in a way controversial because it does not follow the strict definition ofrefactoring. If you are adding new code, then the way the code behaves will change.Now, if you know very well what you are doing, you might prevent some memoryleaks and similar problems by placing cleanup code inside a Finally block. If youhave any doubts, leave it be for now and do it as part of some maintenance work, butnot as part of the refactoring process. Even if you leave this step out, your code willstill perform as it used to, and if you didn’t notice any problems so far the issue doesnot need any urgent attention.
Now I want to turn in detail to how these steps should be performed when applied to two of the most common flavors of legacy error handling. As sample code, I will use the examples already seen in Figures 6-1 and 6-2. If you follow the listings to come, you will be able to observe the gradual evolu-tion of the code.
Replacing the On Error Goto Label with the Try-Catch-Finally Construct
The first thing you need to do is to identify the error-protected and error-handling code. Go back toFigure 6-1 for a moment. You will see the arrow marking the normal flow. You can see that this code isplaced between the On Error GoTo ErrorHandler and Exit Sub lines. Now identify the error-handlingcode. This code is marked with the “Error handling flow” arrow: it starts below the ErrorHandler: labeland ends with End Sub. It is time to refactor this code.
Replacing On Error GoTo ErrorHandler with the Try KeywordReplace the Exit Sub and Error Handler: lines with a single Catch statement. Finally, place the EndTry statement before the End Sub statement. See Listing 6-2 for original code with comments indicatingplace for Try and End Try statements.
159
Chapter 6: Error Handling
79796c06.qxd:WroxPro 2/23/08 8:15 AM Page 159
Listing 6-2: Replace On Error GoTo ErrorHandler with Try-Catch
Imports System.IO
Public Class OnErrorDemo
Shared Sub Main()‘Replace with “Try”On Error GoTo ErrorHandler Dim writeTo As Stream = _New FileStream(“C:\SomeFile.txt”, _FileMode.Open)Dim fileWriter As TextWriter = _New StreamWriter(writeTo)fileWriter.WriteLine(“Hello!”)fileWriter.Close()‘Replace following two lines with “Catch”Exit Sub
ErrorHandler: Select Case Err.Number
Case 5MsgBox(“File access denied. “ & _“Check file and “ & _“directory write permissions.”)
Case 53MsgBox(“File SomeFile.txt not found.”)
End Select‘Place “End Try” here
End Sub
End Class
This is only an intermediate step, but if you try you will see that you can still compile the code withoutany problem, and it will behave in the same way it did before. Take a peek at Listing 6-3 to see the codewe ended up with. Next, it’s time to deal with error filtering.
Replacing Select Case Err.Number with Catch When Err.Number = ...You can see how error-handling code behaves differently depending on the type of error. In the code,this translates into the Select Case statement using different values for Err.Number as a condition.Now is the time to make use of the When keyword.
You eliminate the Select Case statement. The code for Case 5 is placed under a Catch When Err.Number= 5 block and the code for Case 53 is placed under a Catch When Err.Number = 53 block.
Listing 6-3: Replace Select Case Err.Number with Catch When Err.Number = ...
Imports System.IO
Public Class OnErrorDemo
Shared Sub Main()
160
Part II: Preliminary VB Refactorings
79796c06.qxd:WroxPro 2/23/08 8:15 AM Page 160
Listing 6-3: Replace Select Case Err.Number with Catch When Err.Number = ... (continued)
TryDim writeTo As Stream = _New FileStream(“C:\SomeFile.txt”, _FileMode.Open)Dim fileWriter As TextWriter = _New StreamWriter(writeTo)fileWriter.WriteLine(“Hello!”)fileWriter.Close()
‘DeleteCatch
‘Delete after adding Catch When conditionSelect Case Err.Number
‘Replace with “Catch When Err.Number = 5Case 5
MsgBox(“File access denied. “ & _“Check file and “ & _“directory write permissions.”)
‘Replace with “Catch When Err.Number = 53”>Case 53
MsgBox(“File SomeFile.txt not found.”)‘DeleteEnd Select
End TryEnd Sub
End Class
With this, you have finalized the transformation. Now you can compile and execute the code. It will per-form exactly the same as the legacy code you started off with.
Adding a Finally BlockIf you have code that is duplicated in such a way that it is present both in the main flow and in theerror-handling code, you can extract it and place it under a Finally block. In this example you do nothave duplicated code, but you might want to place cleanup code inside the Finally block. As I havealready mentioned, this step is optional, because it does change the behavior of your code. Extra atten-tion is advised.
In the sample you should close the Stream and Writer both after normal and after error-handling exe-cution. So you can add a Finally block and place the fileWriter.Close() and writeTo.Close()statements inside it. However, since Try–Catch–Finally blocks are scoped, you need to declare thewriteTo and fileWriter variables outside the Try block. Leave the creation code inside the Try block,because it is exactly the moment the error might be produced.
Because of block variable scope, as a precaution, before closing the Stream and Writer you need tomake sure that Stream and Writer objects were actually created. If you do not check this, you might get a NullReferenceException if the error was produced in the moment of object creation.
Listing 6-4 is what the code looks like after the Finally block is added.
161
Chapter 6: Error Handling
79796c06.qxd:WroxPro 2/23/08 8:15 AM Page 161
Listing 6-4: Optional Finally Block Added
Imports System.IO
Public Class OnErrorDemo
Shared Sub Main()‘Variable declaration outside Try blockDim fileWriter As TextWriter = Nothing Dim writeTo As Stream = Nothing Try
writeTo = _New FileStream(“C:\SomeFile.txt”, _FileMode.Open)fileWriter = _New StreamWriter(writeTo)fileWriter.WriteLine(“Hello!”)
‘Conditional “Catch” blockCatch When Err.Number = 5
MsgBox(“File access denied. “ & _“Check file and “ & _“directory write permissions.”)
‘Conditional “Catch” blockCatch When Err.Number = 53
MsgBox(“File SomeFile.txt not found.”)‘Finally block, fileWriter and writeTo variables accessed Finally
If fileWriter IsNot Nothing Then _ fileWriter.Close()If writeTo IsNot Nothing Then _writeTo.Close()
End TryEnd Sub
End Class
This wraps up the transformation of the On Error Goto ErrorHandler flavor of legacy error handling.The next section deals with the Next flavor.
Replacing On Error Resume Next with the Try-Catch-Finally Construct
Just as in the previous section, you start out with identifying the error-protected and error-handlingcode. If you go back to Figure 6-2 you can see that error-protected code starts with the On ErrorResume Next statement and is in operation until the end of the procedure, which ends with the EndSub statement. Error-handling code is present in an If block. The following sections take you throughthe refactoring steps.
Replacing On Error Goto Error Handler with Try KeywordAdd Catch and End Try statements before End Sub. Move the If block containing the error-handlingcode to the Catch block. After that, you can remove the redundant Exit Sub statements from the Ifblock. See Listing 6-5.
162
Part II: Preliminary VB Refactorings
79796c06.qxd:WroxPro 2/23/08 8:15 AM Page 162
Listing 6-5: Replace On Error Resume Next with a Try Block
Imports System.IO
Public Class OnErrorDemo
Shared Sub Main()‘Replace with “Try”On Error Resume Next Dim writeTo As Stream = _New FileStream(“C:\SomeFile.txt”, _FileMode.Open)‘1. Place following two lines: first “Catch” then “End Try”>‘2. Move “If” block inside “Catch” block and ‘ remove “Exit Sub” statements along the wayIf Err.Number = 5 Then
MsgBox(“File access denied. “ & _ “Check file and “ & _ “directory write permissions.”) Exit Sub
ElseIf Err.Number = 53 Then MsgBox(“File SomeFile.txt not found.”) Exit Sub
End If Dim fileWriter As TextWriter = _New StreamWriter(writeTo)fileWriter.WriteLine(“Hello!”)fileWriter.Close()
End SubEnd Class
Replacing If Err.Number = ... with Catch When Err.Number = ...This step is quite similar to Step 2 in the refactoring of the On Error GoTo ErrorHandler flavor of legacy error handling you did earlier. Again, you use two Catch When Err.Number = ... blocksto replace the If block. The resulting code is the same as that obtained after Step 2 in the previous flavor’s refactoring.
Adding a Finally BlockAt this point, the refactoring of the On Error Resume Next and On Error GoTo label flavors of legacyerror-handling code converge. So the actual step here is equal to the last step used in the conversion of the On Error GoTo label flavor of legacy error handling. The resulting code is the same as that shown in Listing 6-4. This means that both flavors of legacy error handling were transformed into the same finalversion of structured error-handling code.
With this, you have successfully eliminated On Error statements from your code. Now you are usingstructured Try–Catch–Finally blocks. However, one piece is still missing in the complete replacementof legacy error-handling code with structured code. You are still using the global Err object and needsome way to get rid of it.
163
Chapter 6: Error Handling
79796c06.qxd:WroxPro 2/23/08 8:15 AM Page 163
Replacing Error Code with Exception Type
Refactoring: Replace Error Code with Exception TypeMotivationUsing classes to represent exceptions instead of representing them through numbers isa far superior way to classify and organize exceptions. The code will be more readableand robust, will contain less duplication, and will be much more .NET-interoperable. Inaddition, exception classes can then be extended and organized in logical hierarchies,and their behavior customized if needed.
Related SmellsUse this refactoring to eliminate the Error Code smell.
MechanicsThis refactoring should follow the Replace On Error Construct with Try-Catch-Finally refactoring.
Dealing with System ExceptionsWhen raising exceptions, replace the code-raising exception by means of Err object, asin an Err.Raise(SomeErrorCode) statement with code throwing an exception by themeans of the Throw keyword, as in the Throw New SomeException statement.
When catching exceptions, replace Catch When Err.Number = SomeErrorCodewith Catch exception as SomeSystemException.
Dealing with Custom ExceptionsWhen raising exceptions, replace the custom error code with a new custom exceptionclass and use the Throw keyword instead of the Err object’s Raise method.
When catching exceptions, replace statements like Catch When Err.Number =SomeErrorCode with code such as Catch exception as SomeCustomException.
Code sample for system error codes:
BeforeImports System.IO
Public Class OnErrorDemo
Shared Sub Main()Dim fileWriter As TextWriter = NothingDim writeTo As Stream = NothingTry
writeTo = _New FileStream(“C:\SomeFile.txt”, _FileMode.Open)fileWriter = _New StreamWriter(writeTo)fileWriter.WriteLine(“Hello!”)
164
Part II: Preliminary VB Refactorings
79796c06.qxd:WroxPro 2/23/08 8:15 AM Page 164
Continued
Catch When Err.Number = 5MsgBox(“File access denied. “ & _“Check file and “ & _“directory write permissions.”)
Catch When Err.Number = 53MsgBox(“File SomeFile.txt not found.”)
FinallyIf fileWriter IsNot Nothing Then _fileWriter.Close()If writeTo IsNot Nothing Then _writeTo.Close()
End TryEnd Sub
End Class
AfterImports System.IO
Public Class OnErrorDemo
Shared Sub Main()Dim fileWriter As TextWriter = NothingDim writeTo As Stream = NothingTry
writeTo = _New FileStream(“C:\SomeFile.txt”, _FileMode.Open)fileWriter = _New StreamWriter(writeTo)fileWriter.WriteLine(“Hello!”)
Catch fileNotFound As FileNotFoundExceptionMsgBox(“File SomeFile.txt not found.”)
Catch unauthorizedAccess As UnauthorizedAccessExceptionMsgBox(“File access denied. “ & _
“Check file and “ & _“directory write permissions.”)
FinallyIf fileWriter IsNot Nothing Then _fileWriter.Close()If writeTo IsNot Nothing Then _writeTo.Close()
End TryEnd Sub
End Class
Code sample for custom error codes:
BeforePublic Const ERR_LOG_FILE_NOT_FOUND As Integer = vbObjectError +100‘...Public Sub SomeMethod()
165
Chapter 6: Error Handling
79796c06.qxd:WroxPro 2/23/08 8:15 AM Page 165
In other .NET languages like C# or C++ there is no equivalent for the Err object. Instead,errors are represented as exceptions, and each exception is a separate instance of someexception class. In VB .NET this is the recommended approach. It is based on the Catchexception as SomeException construct where exception represents an instance andSomeException represents an exception type.
In addition to catching exceptions, you need to deal with throwing exceptions. Here, instead of usingthe Raise method of the legacy Err object, you can use the Throw keyword in combination with anyexception type. This type might exist already, or you might need to implement your new custom excep-tion type. Either way, the exception type has to inherit System.Exception or some of its children. Thenext section takes a look at how you can deal with the simpler situation first, when you need to trans-form the raising of system errors by means of the Err object to the throwing of exceptions by means ofthe Throw keyword.
Replacing System Error Codes with Exception TypesThis refactoring is based on replacing the Err object and its Raise method with the Throw keyword. Inthe simplest form, you can replace the code
Err.Raise(53)
with this:
Throw New System.IO.FileNotFoundException
However, there is one question that you need to answer right away. How do you know that error code 53corresponds with the System.IO.FileNotFoundException class? Well, instead of some dull searchingthrough documentation in an attempt to find the real meaning of error code 53, I suggest you use the fol-lowing code to find the answer. Create a new Console Application project inside your Visual Studio IDEand write the code in Listing 6-6 inside the module.
‘...
Err.Raise(ERR_LOG_FILE_NOT_FOUND)End Sub
AfterOption Explicit OnOption Strict On
Public Class LogFileNotFoundExceptionInherits ApplicationException
End Class‘...Public Sub SomeMethod()‘...
Throw new LogFileNotFoundExceptionEnd Sub
166
Part II: Preliminary VB Refactorings
79796c06.qxd:WroxPro 2/23/08 8:15 AM Page 166
Listing 6-6: A Simple Application Used to Find the Relation Between an Error CodeUsed with Err.Raise and an Exception Type
Module ErrorCodeToExceptionTypeSub Main()
‘Replace with number you need to transform to exceptionDim code As Integer = 53On Error Resume NextErr.Raise(53)MsgBox(Err.GetException.GetType.ToString)
End SubEnd Module
After you execute this code, you see the message box showing you the fully qualified name of the excep-tion that corresponds to the error code used in the Err.Raise statement, thanks to the GetExceptionmethod of the Err object.
In some cases, some of the other optional parameters available in the Raise method can be used. This canprovide additional information on errors such as related description, source, and help files. Fortunately,the exception type provides a roughly similar set of properties, and some other exception types provideadditional sets of properties that can be used. Take this code for example:
Err.Raise(53, “Logger”, “Main.log is missing”, _“FileNotFound.html”)
This code can be transformed along the following lines:
‘One of FileNotFoundException constructors ‘accepts Message and file name parametersDim fileNotFound As New FileNotFoundException( _ “Main.log is missing”, “Main.log”) fileNotFound.Source = “Logger”fileNotFound.HelpLink = “FileNotFound.html”‘Throw exception instanceThrow fileNotFound
The main difference here is that the exception instance has to be created explicitly. You also have to writea bit more code and establish correspondence between the Raise method optional parameters and theFileNotFoundException properties. This is not so difficult. Take a look at Table 6-1.
Table 6-1: Correspondence Between the Err.Raise Method Parameters and GenericException Properties
Err.Raise Method Optional Parameter System.Exception Property
Description Message
HelpFile HelpLink
Source Source
HelpContext None
167
Chapter 6: Error Handling
79796c06.qxd:WroxPro 2/23/08 8:15 AM Page 167
In this specific example, the FileNotFoundException has a custom FileName property. You use informa-tion found in the legacy code (it says “Main.log is missing”) to set Main.log as a FileName propertyof the fileNotFound object. Since this property is read-only, it has to be provided as a constructor parame-ter; hence the second parameter in the FileNotFoundException constructor.
Now take a look at a bit more complex an issue and see what you can do with custom error codes.
Replacing Custom Error Codes with Exception TypesCustom errors in code are easily identified by the use of Visual Basic’s vbObjectError constant. Itmeans that the code is generally written this form: Err.Raise(vbObjectError + 100). Just in casethat constant is not present, it is worth mentioning that the range reserved for custom error codes isfrom 513 to 65535.
Now, to be fair, this refactoring is a bit difficult to complete as a preliminary refactoring, meaning as arefactoring when you have only superficial knowledge of the application problem domain. This is why,even though you are dealing with this refactoring now, this is by no means the end of the story. However,once you transform custom error codes to custom exception types, thanks to the object-oriented approachyou can treat your exception class just like any other class and improve its design as you would that ofany other class: by applying the same refactoring rules.
So imagine you need to deal with code written in this form:
Public Const ERR_LOG_FILE_NOT_FOUND As Integer = vbObjectError + 100‘...Err.Raise(ERR_LOG_FILE_NOT_FOUND)
You adopt the following approach: replace each custom error code with a new exception class. The resultingcode is as follows:
Option Explicit OnOption Strict On
Public Class LogFileNotFoundExceptionInherits ApplicationException
End Class‘...Public Sub SomeMethod()
‘...Throw new LogFileNotFoundException
End Sub
In case you need to deal with optional parameters of the Err.Raise method, the approach is similar to thatshown previously for dealing with system exception codes. It is important to note that the story of customexceptions does not end there. As you have seen, the exceptions are generally organized in the form ofinheritance hierarchies, but this additional work cannot be performed without deeper knowledge of theapplication problem domain and should be performed at some later stage. In the beginning you can stickwith the following formula:
1 error code = 1 new exception type
168
Part II: Preliminary VB Refactorings
79796c06.qxd:WroxPro 2/23/08 8:15 AM Page 168
Later on you can organize classes into inheritance hierarchies and even reduce their number.
Bear in mind that exception classes are in the end classes just like all others. This means that they cansuffer the same smells as the other classes. Fortunately, the same refactorings that can be applied toother classes will work with exception classes as well.
Error Handling in the Rent-a-WheelsApplication
Upon examining the Rent-a-Wheels code, you can see that only the most rudimentary legacy errorhandling is in place. Because this is a typical desktop executable, a direct message to users is shownand error information is cleared from the Err object, a typical error-handling routine.
As you go through the code, you realize that exactly the same approach is adapted throughout the appli-cation. It reacts the same way no matter the type of error. No code uses Err.Number information, nor areany errors raised by the means of the Err.Raise method. See Listing 6-7.
Listing 6-7: Typical Error Handling in the Rent-a-Wheels application
Sub Main()‘“On Error Goto Label” legacy error handling in useOn Error GoTo ErrorHandler ‘some code...
‘ Message to user is shown and error information clearedErrorHandler:
MsgBox(“A problem occurred “ + _“and the application can not recover! “ + _“Please contact the technical support.”)Err.Clear()
End Sub
This simplistic error-handling approach needs to go through numerous changes before a refactoredRent-a-Wheels application sees the light of the day. Also, all error-handling code implemented so fardoes the same thing. So instead of resorting to refactorings used for the transformation of legacy tostructured error handling, you can make use of little trick with VB 2008.
Instead of refactoring error-handling code, you can remove all scattered error handling and add a singleerror handler. Thanks to the fact that in VB 2008 all errors are channeled through the UnhandledExceptionevent, you can handle this event with some error-handling code and provide the same functionality that theoriginal error-handling code did.
Application-Level Events in VB 2008In VB 2008 it is possible to handle application-level events. Events available are Shutdown, Startup,StartupNextInstance, NetworkAvailabiltyChanged, and (especially interesting for this example)UnhandledException.
169
Chapter 6: Error Handling
79796c06.qxd:WroxPro 2/23/08 8:15 AM Page 169
In order to activate application-level error handling, do the following:
1. Open the Project Properties window. The Application tab will be displayed by default. PressView Application Events. This Visual Studio window is shown in Figure 6-4.
Figure 6-4
2. After this, the ApplicationEvents.vb file should be displayed and made visible in theSolution Explorer.
3. After that, open the ApplicationEvents.vb file in the code editor and add theUnhandledException event handler to the MyApplication class. Now you are ready to capture all unhandled errors produced by the application.
The Rent-a-Wheels code should perform the same as with original error handlers, so you can copy themessage box code to the UnhandledException handler and indicate that the application not exit by setting the ExitApplication property of parameter e to False. You can see the code you end up within Listing 6-8.
Listing 6-8: An Application-Level Unhandled Exception Treatment
Namespace My
Class MyApplicationPrivate Sub MyApplication_UnhandledException( _ByVal sender As Object, _ByVal e As Microsoft.VisualBasic.ApplicationServices. _UnhandledExceptionEventArgs) _Handles Me.UnhandledException
‘ Original message displayed to user in the case of exception
170
Part II: Preliminary VB Refactorings
79796c06.qxd:WroxPro 2/23/08 8:15 AM Page 170
Listing 6-8: An Application-Level Unhandled Exception Treatment (continued)
MsgBox(“A problem occurred “ + _ “and the application can not recover! “ + _ “Please contact the technical support.”) ‘ Replaces Err.Cleare.ExitApplication = False
End SubEnd Class
End Namespace
Admittedly, this is not refactoring in the strictest sense. It works here because the original error-handlingcode is so simple. However, after testing the application, you will no doubt be happy with the resultsand decide that at this point replacing error-handling code with a single application-level event will dothe job in this case.
SummaryIn this chapter you started out by taking a look at a traditional way to perform error handling in VisualBasic. The legacy, unstructured way of error handling consists of using On Error GoTo Error Handler orOn Error Resume Next statements and uses error codes to signal the type of error when they are raised.
The new, structured way of error handling in VB is performed by means of structured Try–Catch–Finallyblocks and uses classes to represent error types. The new style of error handling is more in the spirit of .NETand in various ways superior to traditional error handling:
❑ It is structured, meaning it is easier to read and comprehend.
❑ Exceptions are fully fledged, extendible types and not simple codes.
❑ Using exception types makes code more .NET-interoperable.
❑ Exception filtering is easily achieved.
Because of all these benefits, you can consider the use of unstructured error handling and error codes a smell. It is a good practice to transform legacy to structured error handling. You can do this with tworefactoring transformations:
❑ Replace On Error with Try-Catch-Finally blocks
❑ Replace Error Code with Exception Type
During the process of transforming legacy error-handling code to structured Try–Catch–Finally blocks,it is a good idea to make use of the When keyword and the Err object in combination with Catch blocks tohandle conditional code that depends on error type. This step is only intermediate, as in the next step filter-ing is performed by the means of a Catch As statement in which the exception type is explicitly given.
The second part of transforming legacy error handling deals with transforming custom error codes toerror types by implementing new classes that inherit the System.ApplicationException class to rep-resent different application exceptions.
171
Chapter 6: Error Handling
79796c06.qxd:WroxPro 2/23/08 8:15 AM Page 171
Finally, you took a look at the Rent-a-Wheels application. You used application-level events to implementa single error-handling routine for the whole application. While this is in no way a definitive solution forexception handling in Rent-a-Wheels, it can help postpone work on error handling for some more auspi-cious moment, when the whole design of the application starts to take its final form.
This completes the investigation of error handling in VB .NET. The story about error handling does notend here. Still, the good news is that from now on you can treat exceptions just as you would any otherclass in your code. Now it’s time to pursue further preliminary refactorings in the next chapter by lookingat some everyday code hygiene.
172
Part II: Preliminary VB Refactorings
79796c06.qxd:WroxPro 2/23/08 8:15 AM Page 172
Basic Hygiene: El iminatingDead Code, ReducingScope, Using Expl ic it
Impor ts, and RemovingUnused References
In this chapter you’ll continue to investigate preliminary VB refactorings. As I have already men-tioned, you can generally perform these refactorings without any deeper understanding of theapplication problem domain, and they are mostly performed on the syntactic level. You’ll usethem to prepare the code for more complex restructuring where application domain knowledge is indispensable.
❑ The first issue you’ll deal with in this chapter is dead code. Dead code comprises those sectionsof code that are left unused after some modification has been performed: a new feature isimplemented, a bug resolved, or some refactoring performed. Dead code can have very neg-ative effects on maintainability, and you will see the reasons why it should be eliminated.
❑ After that, I’ll remind you of the importance of well encapsulated code. Don’t forget,encapsulation is a pillar of well-constructed object-oriented code. You will take a look atelement scope and access level as mechanisms for hiding information and implementa-tion details in your code.
❑ The next topic I’ll deal with is that of explicit imports. You can reference elements from other namespaces by using fully qualified names in code or by using the Imports state-ment. I will compare both styles and will explain why I prefer using the Imports statement.
❑ The last issue I’ll deal with in this chapter is that of unused assembly references. It is agood practice to eliminate these unused references as they appear, and you will see howthe Visual Studio can help you do it.
79796c07.qxd:WroxPro 2/25/08 9:01 AM Page 173
Part II: Preliminary VB Refactorings
174
With this, you will finish the exploration of preliminary refactorings. I hope that after you finish readingthis chapter you will have a clear picture of code cleanness and hygiene, and of how to perform transfor-mations to bring into shape any code that does not respect the rules.
Eliminating Dead Code
In the first chapter I mentioned that code simplicity is one of most valuable qualities your code can possess.Each additional symbol in the source means an additional effort that has to be spent on code assimilation.Programmers often have the attitude that more cannot hurt, and they keep the code somewhere inside thecurrent code base just in case. However, code that that never gets executed can be the biggest riddle for aprogrammer who didn’t author that code. That programmer may reason: “code exists, so it must servesome purpose. Because no purpose is evident, full code comprehension has not been reached yet.” So theprogrammer ends up spending more effort on testing, debugging, and profiling in an attempt to discoverthe lost meaning of the code.
Definition: Dead code is redundant, inoperative code that is not executed under any circumstances.
Smell: Dead CodeDetecting the SmellUse a compiler to find unused code. You do this by commenting suspicious code andrebuilding the project. If no new errors are reported by the compiler, you have poten-tially discovered a new case of dead code. Now you need to inspect the use at runtimeof this possibly dead code.
Use a code coverage tool (we mentioned some of these in Chapter 3) to find potentiallydead code. Execute a comprehensive suite of tests with the coverage tool activated.Analyze coverage tool results and look for sections of code that were never executed. If the code that wasn’t executed is the same as the code that was not reported missingby the compiler, it is quite probable that you have discovered dead code. The accuracyof this methodology depends heavily on the level of coverage your tests provide.
Use a simple text search to find commented blocks of code. Commented code nevergets executed; the information it contains is generally historic and should not form partof the code base. Such information is best kept by your source-code repository.
Related RefactoringUse Eliminate Dead Code refactoring to eliminate this smell.
RationaleDead code increases the complexity of your code. It makes your code more difficult tounderstand and maintain; further, this code has no value. Since it is never used, it isnever executed. Dead code can result in unnecessary effort and can obscure the origi-nal intent and design of your code.
79796c07.qxd:WroxPro 2/25/08 9:01 AM Page 174
Chapter 7: Basic Hygiene
175
The most common sources of dead code are maintenance work and design changes. Maybe you haveerased a line or two, and before you know it, somewhere a whole method is left unused. Or you mayhave added a certain condition during debugging in order to test a specific block of code, and forgottento remove the condition before going into production. Maybe you copied the method and then com-mented a version of the method you changed, instead of removing the unused method. Whatever theorigin of dead code, keeping it inside your code base is counterproductive and such historic informationshould be left for the version-control system to keep.
Types of Dead CodeYou can distinguish the following types of dead code:
❑ Unreachable code: This is the code that you know with absolute certainty will never be executed.It is still considered by the compiler, which reports any syntax error or the like. Your first instinctis to resolve the compiler error, but in this case that’s a complete waste of time and effort.
❑ Commented code: These are ignored by the compiler, but can still cloud the programmer’s vision.If a bug is present in code related to the commented code, the programmer will inevitably start tosearch for a solution in the commented section, looking for the reason the code was commentedand not removed.
❑ Unused code: This code is present if you are dealing with some type of reusable library. Forexample, you have a method or a class that you suspect is never used. You cannot be sure of this because you do not have the complete code base inside your IDE. In such a case, the element should be phased out gradually. In the first version, make your clients aware that infuture versions this element will be eliminated by marking it with ObsoleteAttribute.
Listing 7-1 shows some examples of the different flavors of dead code.
Listing 7-1: Flavors of Dead Code
Option Explicit OnOption Strict On
‘XmlDocument never referencedImports System.Xml.XmlDocument
Public Class DeadCodeDemo Shared Sub Main()
Dim number As Integer = 5‘Never evaluates to True, following line is never reachedIf number < 4 Then
MsgBox(“Destined never to show, unreachable”)End If‘Redundant code, “number” variable value already 5number = 5 ‘ Exits the method, following line is never reachedExit Sub MsgBox(“Destined never to show, unreachable”)
End Sub
‘ Method is private and never used in the class it belongs to - unreachable
Continued
79796c07.qxd:WroxPro 2/25/08 9:01 AM Page 175
Part II: Preliminary VB Refactorings
176
Listing 7-1: Flavors of Dead Code (continued)
Private Sub NotUsedInClassItBelongsTo() MsgBox(“Destined never to show, unreachable”)
End Sub
‘ Public method is never usedPublic Sub NotUsed()
MsgBox(“Destined never to show, unused”)End Sub
‘Commented method is never used‘Public Sub Commented() ‘ MsgBox(“Destined never to show, commented”)‘End Sub
End Class
Common Sources of Dead Code
Refactoring: Eliminate Dead CodeMotivationMore code means more complexity. By eliminating unnecessary code from your codebase, you are making it easier to read, comprehend, and maintain.
Related SmellsUse this refactoring to eliminate the Dead Code smell.
MechanicsRefer to the Dead Code smell definition earlier in the chapter to learn how to locateoccurrences of dead code. After you have identified dead code, eliminate it. Commit the version to a version-control system so your latest action can be easily traced and a backup preserved.
In case you are working with reusable code like library or component code and you are suspicious that some code element is unused, start by marking this element withObsoleteAttribute. Remove the element completely in the version to follow.
BeforeOption Explicit OnOption Strict On
Imports System.Xml
Public Class Customer‘Import obsolete, since ToXml method commentedPrivate mFirstName As StringPrivate mLastName As StringPrivate mSSN As String
Public Property FirstName() As String
79796c07.qxd:WroxPro 2/25/08 9:01 AM Page 176
Chapter 7: Basic Hygiene
177Continued
GetFirstName = mFirstName
End GetSet(ByVal value As String)
mFirstName = valueEnd Set
End Property
Public Property LastName() As StringGet
LastName = mLastNameEnd GetSet(ByVal value As String)
mLastName = valueEnd Set
End Property
Public Property SSN() As StringGet
SSN = mSSNEnd GetSet(ByVal value As String)
mSSN = valueEnd Set
End Property
‘ Whole method commented‘Public Function ToXml() As XmlDocument ‘ Dim doc As XmlDocument = New XmlDocument‘ Dim xml As String‘ xml = “<Customer>” & _‘ “<FirstName>” & mFirstName & “</FirstName>” & _‘ “<LastName>” & mLastName & “</LastName>” & _‘ “<SSN>” & mSSN & “</SSN>” & _‘ “</Customer>”‘ doc.LoadXml(xml)‘ Return doc‘End Function
End Class
AfterOption Explicit OnOption Strict On
Public Class Customer
Private mFirstName As StringPrivate mLastName As StringPrivate mSSN As String
Public Property FirstName() As String
79796c07.qxd:WroxPro 2/25/08 9:01 AM Page 177
Part II: Preliminary VB Refactorings
178
There are a number of ways you can end up with some dead code inside your code base. I will mentionsome I find to be very common so you can prepare yourself and be ready to look for them.
Detached Event HandlerThe event is never fired, but the event code is still present. This happens very often if you decide toeliminate a control from the form that previously served some purpose. All related event-handlingcode is still preserved and needs to be removed manually.
Invisible Control Sometimes you place control on the form and it gets covered by another control, or it is resized in such away it is not visible anymore. Or maybe it is set invisible or disabled right from the beginning and nevergets used. Eliminate such controls and all related event-handling code.
Imports Section Importing Unused ElementsI come across obsolete elements in import sections very often. Once you eliminate a property or a method,or move some code between classes, it is only too easy to forget to remove import statements that weremade redundant by these changes. It may seem innocuous, but this code can have some far-reaching con-sequences. When you are performing some large-scale refactoring, it is very important to understand howdependencies work out in your code. When you are performing some fast code browsing, you often baseyour conclusions on the content of the Imports section. A few redundant Imports statements and you canbe completely misled.
GetFirstName = mFirstName
End GetSet(ByVal value As String)
mFirstName = valueEnd Set
End Property
Public Property LastName() As StringGet
LastName = mLastNameEnd GetSet(ByVal value As String)
mLastName = valueEnd Set
End Property
Public Property SSN() As StringGet
SSN = mSSNEnd GetSet(ByVal value As String)
mSSN = valueEnd Set
End PropertyEnd Class
79796c07.qxd:WroxPro 2/25/08 9:01 AM Page 178
Chapter 7: Basic Hygiene
179
Ignored Procedure Return ValueThis happens when the return value is ignored upon the function’s being called. It means that the clientsare not really interested in the information the return value provides. This function should be transformedto a sub, and code in charge of returning the value should be eliminated.
Ignored Procedure Return ParameterWhen the function is called, the return parameter value is ignored. Again, if this is the case with all functionclients, the ignored return parameter and related code should be eliminated.
Local Variable Is Not ReadThe variable can be assigned, but if it is not read, there is no use for it. In fact, it is just another case ofdead code. Eliminate the variable in question.
Read-Only or Write-Only Property Containing Both Get and Set Property Procedures
After you write down the property declaration, Visual Studio will automatically create both Get and Setproperty procedure stubs. Very often programmers, from force of habit, will leave and implement bothprocedures, even though the property is supposed to be read- or write-only.
Obsolete Elements With time, as more and more changes are applied, some bigger elements in your code can be left unreach-able or unused. Such elements can be classes, enumerators, interfaces, modules, and even whole name-spaces. These elements should be eliminated.
Don’t have any mercy on dead code. Be sure to use some versioning system as a backup and eliminatedead code without fear. You will soon feel as if you have left some burden from the past behind. Yourcode base will soon become much clearer, and easier to comprehend and maintain.
Reducing the Scope and Access Level of Unduly Exposed Elements
Continued
Smell: OverexposureDetecting the SmellIf you are suspicious of a certain element for having an unnecessarily broad access level,reduce the access level by one degree and then build the project. If no error is reported bythe compiler, you have found an overexposed element. The element can be any of the fol-lowing: an interface, module, class, structure, structure member, procedure, property,member variable, constant, enumeration, event, external declaration, or delegate.
79796c07.qxd:WroxPro 2/25/08 9:01 AM Page 179
Part II: Preliminary VB Refactorings
180
You have often heard that encapsulation is the first pillar of object orientation. Encapsulation is applied toconceal internal details of our code from the public eye. This is generally referred to as information andimplementation hiding. In order to reduce the complexity of systems, you often resort to decomposing thesystem into different units. When you are following the “divide and conquer” principle, it is very impor-tant to hide as much internal information and implementation detail of an individual module as possible.
In VB .NET, the largest organizational unit is the assembly. In order to use services that some assemblyprovides, you need only the compiled binary. You interact with the assembly through its interface — a setof publicly visible elements. Having this interface as simple and thin as possible greatly simplifies yourinteraction with the assembly. You need not have any notion of the internal workings of the assembly.
So how does this work out in practice? I will try to illustrate this with an example. Take a look atFigure 7-1. The difference in size between the ShippingCost and CartPersistence interfaces can be observed visually.
Imagine you have a system consisting of three assemblies: ShoppingCart, ShippingCost, and CartPersistence. You are working on the ShoppingCart assembly, and you are using the ShippingCost assembly services to calculate the cost of shipping for items in the cart. Using theShippingCost assembly proves to be quite simple: a single interface and a single operation are exposedto you as a client.
Now you need to write some test code in which you use CartPersistence services also. However, thisis proving to be much more complex. The CartPersistence interface exposes a myriad of implementa-tion details related to database communication, transaction management, logging, and so on — a numberof details a client using this interface should not have to worry about. As a matter of fact, there is no rea-son why you should have any notion of the underlying persistence mechanism. The persistence mecha-nism could just as well use a simple file, Excel spreadsheet, or whatever. There is no reason why anyCartPersistence client should be aware of its inner workings. In this case, while interacting withCartPersistence, you are unnecessarily bogged down with overexposure of assembly internals.
As a rule of thumb, keep the scope and access level of programming elements as restricted as possible.
You suspect that a certain element has too broad a scope, even when a minimum accesslevel is specified. Move that element to a more enclosed region and build the project. Ifno error is reported by the compiler, you have successfully identified an element withan unnecessarily broad scope.
Related RefactoringUse Reduce Access Level and Reduce Scope refactoring in order to eliminate this smell.
RationaleUnnecessary exposure of internal and implementation details goes against the basic prin-ciples of encapsulation and data and information hiding. It makes your code less modular,more complex, and difficult to use and maintain. Dependencies can freely spawn in such a system, making it in fact monolithic. An unnecessary level of detail is exposed in such away that it complicates use of the code units.
79796c07.qxd:WroxPro 2/25/08 9:01 AM Page 180
Chapter 7: Basic Hygiene
181
Figure 7-1
The benefits of encapsulation and information and data hiding can be easily appreciated. Even comparingthe size of the two interfaces shown in Figure 7-1 visually gives us a lot to talk about. Well-encapsulatedunits are easy to use and interact with, eliminating unnecessarily complexity and providing good modu-larity of the system.
In VB you can use the scope and access level to control the level of exposure of programming elements.By minimizing the exposure of programming elements, you will successfully encapsulate and hideimplementation details of your code.
Scope and Access LevelThe scope (or visibility) of a declared element is the region of code that can access that element withoutfully qualifying it. It means that for the element, this region is its closest neighborhood, where every-one is known by their first names. It also has one very practical consequence: such elements are read-ily displayed by IntelliSense. Elements out of scope have to be imported in order to be displayed byIntelliSense directly. If you do not import the element, IntelliSense will still help you find it, but only ifyou follow the whole path of the fully qualified name. In Figure 7-2 you can see how IntelliSense willhelp you reference an out-of-scope element if you use the element’s fully qualified name.
Figure 7-2
ShoppingCart ShippingCost<<interface>>ShippingCost::IShippingCost+CalculateCost()
<<interface>>CartPersistence::ICartPersistence+Save()+Update()+Delete()+Retrieve()+OpenConnection()+CloseConnection()+BeginTransaction()+RollbackTransaction()+ConnectionString()+ConnectionTimeout()+LogError()
CartPersistence
79796c07.qxd:WroxPro 2/25/08 9:01 AM Page 181
Part II: Preliminary VB Refactorings
182
ScopeIn VB, we differentiate four levels of scope:
❑ Block scope
❑ Procedure scope
❑ Module scope
❑ Namespace scope
The scope first and foremost depends on the enclosing region in which you declared the element —meaning the block, procedure, VB module, class, and structure. For example, if you declare a variableinside the method, it will be available throughout that same method, but it will not be available fromany other method in that or any other class.
In case that element is declared on the module level (VB module, class, structure), element scope is furtheraffected by access level. If a variable is declared as private, it will have module scope, while if the friend orpublic access level is used, the element will be visible throughout the namespace (namespace scope).
Access LevelAccess level controls which code can access (read or write) a declared element. In VB, you differentiatethe following access levels:
❑ Private
❑ Friend
❑ Protected
❑ Protected friend
❑ Public
Access level is controlled by corresponding access modifiers when the element is declared. In some cases,the access level of element also depends on the access level of the containing structure. For example, youwill not be able to access a method declared as public from another assembly if the method containing theclass is marked as friend.
Common Sources of Overexposure
Refactoring: Reduce Access LevelMotivationWell-encapsulated code keeps internal and implementation detail well hidden from outside view. This way, thanks to the modular approach, the complexity of the system asa whole is reduced. Encapsulation will also help control dependencies in the system.Other benefits of reduced access level are lower risk of name clashes and simplifiedsecurity management.
Related SmellsUse this refactoring to eliminate the Overexposure smell.
79796c07.qxd:WroxPro 2/25/08 9:01 AM Page 182
Chapter 7: Basic Hygiene
183
As is common with many smells, change is the easiest path to overexposure. After some reshuffling ofcode, you might get in a situation where the scope and access level of some elements should be reduced,because they are now referenced from a more enclosed area. Unfortunately, this step is frequently omitted.
On the other hand, overexposure can be the result of simple oversight. The benefits of encapsulation anddata and information hiding are not easy to see in the short term or on a small scale, and programmersoften don’t give them due attention.
MechanicsStart by reducing the access level by one degree and build the project. If the compile is successful, repeat this step until an error is reported by the compiler or the privateaccess level is reached. In case of error, return to the most reduced access level underwhich the project still compiles successfully.
BeforeOption Explicit OnOption Strict On
Public Class Customer‘This field need not be publicPublic mFirstName As String
Public Property FirstName() As StringGet
FirstName = mFirstNameEnd GetSet(ByVal value As String)
mFirstName = valueEnd Set
End Property‘...
End Class
AfterOption Explicit OnOption Strict On
Public Class Customer‘Access level reduced from public to privatePrivate mFirstName As String
Public Property FirstName() As StringGet
FirstName = mFirstNameEnd GetSet(ByVal value As String)
mFirstName = valueEnd Set
End Property‘...
End Class
79796c07.qxd:WroxPro 2/25/08 9:01 AM Page 183
Part II: Preliminary VB Refactorings
184
Decision and Control Structures in VB .NET Are ScopedIn VB .NET, there is a new type of variable scope. The block scope was not present in VB6 and priorversions of Visual Basic, so it is often overlooked by traditional VB programmers. In VB6 and earlier it was considered a good practice to declare all the variables at the beginning of the procedure. In VB.NET, however, you can keep your code even more tightly encapsulated.
While block-level variables have block-level scope and can be accessed only from within that same block,they have the lifetime of the procedure they belong to. This means that if the block is entered multipletimes during the procedure execution (For and While loops, for example), the variable will retain itsvalue between block executions unless it is initialized inside the block.
Take the code in Listing 7-2 for example.
Listing 7-2: Block-Level Scope
Public Shared Sub Main()For blockLevel As Integer = 1 To 10
MsgBox(“Iteration: “ & blockLevel.ToString)Next blockLevel
‘Compiler error is reported on following lineMsgBox(“Last Iteration: “ & blockLevel.ToString)
End Sub
If you try to compile this code, the compiler reports the following error: “Name ‘blockLevel’ is notdeclared.” The variable blockLevel is accessible only in the region between For and Next statements.
Visual Studio Adds Class with Public Access Level by DefaultIf you use the Add Item option and then select Class in Visual Studio, the IDE will add a file to the proj-ect. However, this file is not completely empty. The IDE will also generate a class declaration skeletonand add it to a file. As a matter of fact, you have already learned how Visual Studio templates work andhow they can be modified in Chapter 3. The Class template and templates for other types of items comewith a predefined public-access level. It is all too easy to leave the default access level unmodified, evenif this class is not meant to be exposed to the outside world.
Quick Fix for Design ErrorsIn some situations, a quick solution to some design-related problems can lead to intentional exposure ofsome private element. While this can give some fast short-term results, a hefty price will almost certainlybe paid later on. Take a look at the code in Listing 7-3.
Listing 7-3: Quick Fix Requires the Exposure of a Private Connection Object
Option Explicit OnOption Strict On
Imports System.Data.SqlClient
Public Class ProductDetailPersistence
Private connection As SqlConnection
79796c07.qxd:WroxPro 2/25/08 9:01 AM Page 184
Chapter 7: Basic Hygiene
185
Listing 7-3: Quick Fix Requires the Exposure of a Private Connection Object (continued)
‘...Public Sub Delete()‘...End Sub‘...
End Class
The class ProductDetailPersistence is, as the name suggests, responsible for the persistence ofproduct detail. At some point during development, the developers realize that a very important omis-sion was made. No provision was made for transactional behavior of the class. Some operations, forexample product elimination, have to be performed under the same transaction as product-detail elimi-nation. The quick fix is to expose the connection object by making it public and thus providing a com-mon transactional context for both operations.
Naturally this presents a problem, because low-level persistence implementation details are exposed to aclient. So if at some point the decision is made to change the underlying data store, the changes cannot beisolated to a persistence class only. Because all clients have knowledge of SqlConnection, any attempt toreplace the underlying data store with some other type of persistence mechanism or some other databasewould cause a chain reaction of changes in all client code.
Continued
Refactoring: Move Element to a More Enclosed RegionMotivationKeeping the scope of elements at a minimum helps make your programs better encap-sulated, more modular, more robust, and easier to maintain, read, and use. Other bene-fits are optimized memory management, simplified security, and so on.
Related SmellsUse this refactoring to eliminate the Overexposure smell.
MechanicsThis refactoring should be preceded by Reduce Access Level refactoring, whereapplicable. Select the element for scope reduction and move its declaration to a moreenclosed region.
BeforeOption Explicit OnOption Strict On
Public Class CustomerXmlWriter
‘Variable doc declared on class level, but used in ToXmlmethod only
Private customer as CustomerPrivate doc As System.Xml.XmlDocument _ = New System.Xml.XmlDocument
79796c07.qxd:WroxPro 2/25/08 9:01 AM Page 185
Part II: Preliminary VB Refactorings
186
Dealing with OverexposureOnce you have identified an overexposed element, dealing with overexposure is relatively simple. Thefirst step should be access-level reduction. Reduction should be performed gradually: you should rebuildthe project after each reduction until a compiler error is produced or you reach the private access level. If a compiler error is produced, the latest reduction should be undone. In Table 7-1, you can see the gradualpath of access-level reduction.
‘...Public Function ToXml() As System.Xml.XmlDocument
Dim xml As Stringxml = “<Customer>” & _“<FirstName>” & customer.mFirstName & “</FirstName>” & _“<LastName>” & customer.mLastName & “</LastName>” & _“<SSN>” & customer.mSSN & “</SSN>” & _“</Customer>”doc.LoadXml(xml)Return doc
End FunctionEnd Class
AfterOption Explicit OnOption Strict On
Imports System.Xml
Public Class CustomerXmlWriterPrivate customer as Customer‘...
Public Function ToXml() As XmlDocument ‘doc variable declaration moved to ToXml method Dim doc As XmlDocument = _ New XmlDocument Dim xml As Stringxml = “<Customer>” & _“<FirstName>” & customer.mFirstName & “</FirstName>” & _“<LastName>” & customer.mLastName & “</LastName>” & _“<SSN>” & customer.mSSN & “</SSN>” & _“</Customer>”doc.LoadXml(xml)Return doc
End FunctionEnd Class
79796c07.qxd:WroxPro 2/25/08 9:01 AM Page 186
Chapter 7: Basic Hygiene
187
Table 7-1: Gradual Access-Level Reduction
A similar pattern can be applied to scope reduction. This step should follow access-level reduction. Here,the location of the element declaration is changed in such way that scope is reduced to the minimal levelnecessary. Table 7-2 illustrates the scope-reduction path.
Table 7-2: Gradual Scope Reduction
When you are talking about access level and scope, less is truly more. If you are writing code, it pays tobe secretive. Make a habit of declaring all your elements under the minimum scope and access level.This can often result in better designed and better encapsulated code.
Using Explicit Impor ts
Continued
Smell: Using Fully Qualified Names Outside the Imports SectionDetecting the SmellUnfortunately, you need to resort to scanning the source visually in order to detectthis smell. There is, however, a step you can perform in order to speed up the search.Temporarily remove all project references. Also, comment the Imports section tem-porarily so it is not marked with an error sign by the compiler. The compiler will nowmark all the declarations in which types from external assemblies were used. All thatis left is to review each declaration in a search for fully qualified names in the body ofthe code.
Related RefactoringUse Explicit Imports refactoring in order to eliminate this smell.
Current Reduce to
No namespace Namespace
Namespace Module (class, structure)
Module (class, structure) Procedure
Procedure Block
Current Reduce to
Public Protected friend
Protected friend Friend
Friend Private
79796c07.qxd:WroxPro 2/25/08 9:01 AM Page 187
Part II: Preliminary VB Refactorings
188
The issue of explicit imports might not seem major at first. After all, imports are there only to help youwrite less, so you can reference an element (namespace, interface, module, class, structure, and so on)using its simple name instead of its fully qualified name. It will not absolve you from adding references toa project. So why attach any importance to it? If the effect is the same, why not let a programmer use fullyqualified names inside the body of an element? I will try to answer these questions in the next section.
Imports Section Depicts Dependencies in Your System
Refactoring: Replace Fully Qualified Names with Explicit ImportsMotivationBy adding an Imports statement you will reduce the quantity of the code you have towrite. Using imports is much more efficient than repeatedly writing fully qualifiednames in your code.
By consistently using an Imports section you will provide easy-to-find, reliable infor-mation related to dependencies in your code.
Related SmellsUse this refactoring to eliminate the Using Fully Qualified Names Outside ImportsSection smell.
Mechanics1. After identifying the fully qualified name in body of the module, add the
Imports statement for this particular name, if it is not already present inthe Imports section.
2. Replace the recently identified fully qualified name in the body of themodule with the simple name.
BeforeOption Explicit OnOption Strict On
Public Class CustomerXmlWriter
RationaleUsing fully qualified names in the body of the code module1 (Class, VB Module,Structure, Interface, and so on) will make your code more difficult to read and write.Fully qualified names can be very long and using them is plain tedious. It is much bet-ter to have them imported in a single place.
The Imports section is a very good place to look when trying to understand large-scale design and system dependencies. Using the Imports section inconsistently andresorting to fully qualified names in the body of the module can give a false impressionwhen you are inspecting the dependencies in your code.
1Here I use the broader meaning of the word “module,” meaning a form of code grouping, and itcan indicate interface, VB module, class, structure, and so on.
79796c07.qxd:WroxPro 2/25/08 9:01 AM Page 188
Chapter 7: Basic Hygiene
189
As projects grow in size and more people take part in development, large-scale issues like “dependencies”grow in importance and become more apparent. The natural way to deal with large-scale projects is toorganize software into separate pieces. This way, smaller groups of programmers can dedicate themselvesto constructing a single piece of a puzzle. Because you generally start out with the blueprint showing howthese pieces should fit in, construction of the final product is just a matter of putting them together. In.NET, these largest organizational units are identified as assemblies.
However, if special care is not taken, these pieces start to develop unpredicted and undesired dependenciesbetween each other. Growing new dependencies is as easy as adding one project reference and one Importsstatement in your code.
Private customer as Customer‘...Public Function ToXml() As System.Xml.XmlDocument
Dim doc As System.Xml.XmlDocument = _ New System.Xml.XmlDocument Dim xml As Stringxml = “<Customer>” & _“<FirstName>” & customer.mFirstName & “</FirstName>” & _“<LastName>” & customer.mLastName & “</LastName>” & _“<SSN>” & customer.mSSN & “</SSN>” & _“</Customer>”doc.LoadXml(xml)Return doc
End FunctionEnd Class
AfterOption Explicit OnOption Strict On
‘“Imports”statement addedImports System.Xml
Public Class CustomerXmlWriterPrivate customer as Customer‘...‘ XmlDocument simple name usedPublic Function ToXml() As XmlDocument
Dim doc As XmlDocument = _ New XmlDocument Dim xml As Stringxml = “<Customer>” & _“<FirstName>” & customer.mFirstName & “</FirstName>” & _“<LastName>” & customer.mLastName & “</LastName>” & _“<SSN>” & customer.mSSN & “</SSN>” & _“</Customer>”doc.LoadXml(xml)Return doc
End FunctionEnd Class
79796c07.qxd:WroxPro 2/25/08 9:01 AM Page 189
Part II: Preliminary VB Refactorings
190
Imagine you have finished working on your class. The morning after, you find out it’s not workinganymore because a class you depend upon has unexpectedly changed. The other programmer wasnot aware that your class depends on his or her class, and decided to apply changes as he or shedeemed necessary.
Imagine another scenario. Your assembly deals with a business domain but is also concerned with datapersistence. For that purpose, it uses different assemblies for XML or database-type persistence. However,in this specific application, you have reused your assembly for its business-domain functionality, eventhough you do not need any persistence functionality. Nevertheless, XML and database-persistence assem-blies still have to be deployed to go along with your assembly because your assembly depends on them.
This is just the tip of the iceberg of dependency-related problems. You will see more of them as youplunge into some more complex issues related to large-scale design later on in the book. Even with thisshort introduction, the significance of correct dependency management should be apparent.
The first and very natural attempt to comprehend the reality of dependencies in your project is to visu-ally scan the Imports sections of source-code files. In some large-scale projects scanning the whole codebase is not only impractical, but can be close to impossible. And available documentation and diagramscan often be out of sync with the real world — more showing good intentions than really representingthe current state of affairs.
In VB .NET you can import the namespace element or the whole namespace. If you import the wholenamespace, you will have less code to write, but import details will not be immediately visible. Youshould choose only one style and stick with it throughout the project.
Consider the following anecdote: An acquaintance of mine had to perform a lot of large-scale refactoringwork in order to reorganize the system and finally eliminate some dependencies from certain assemblies.After he thought he had finished the task, he triumphantly removed the external assembly reference fromthe project and pressed the build button. As a result, a list of compiler errors appeared in the IDE, andhe discovered a number of places in which external assembly types were referenced by means of fullyqualified names. He based his assumptions on the Imports section. He found that because of the freeuse of fully qualified names throughout the code, a number of places in which external types were refer-enced were not identified from the start.
While there are tools that can help you with the task of comprehending dependencies in your code, theyare not always available. That’s why maintaining the Imports section as current and avoiding the useof fully qualified names in the body of the modules is very important in the attempt to ensure your codeis readable and easy to maintain.
One last detail regarding the namespace imports. It is also possible to import a namespace globally on aproject level. For this, you need to go to the References tab on the Project Properties page. As you might havealready guessed, I am not a big fan of importing namespaces globally. While it might not hurt if you importnamespaces belonging to the .NET framework, you can always count on them to be available, so using thisoption on other namespaces can be the source of some unwarranted surprises in the same way as the usageof fully qualified names. In Figure 7-3 you can see how a namespace is globally referenced in a project.
Definition: One module is dependent on another if change in that other module canprovoke changes in the first.
79796c07.qxd:WroxPro 2/25/08 9:01 AM Page 190
Chapter 7: Basic Hygiene
191
Figure 7-3
Removing Unused Assembly References
Smell: Unused ReferencesDetecting the SmellYou should check for the presence of unused references after any major and large-scale refactoring. Open the References tab on the Project Properties page and press the Unused References... button. Visual Studio will search for and display the list ofunused references.
Related RefactoringUse Remove Unused References refactoring to eliminate this smell.
RationaleAn unused reference is a breeding ground for unnecessary dependencies. Dependencieshave a great influence on the design of your system from a large-scale “bird’s-eye” view.A reference is a signal to programmers that they can use services that a certain assemblyprovides, and thus an otherwise unnecessary dependency is established.
79796c07.qxd:WroxPro 2/25/08 9:01 AM Page 191
Part II: Preliminary VB Refactorings
192
While the VB compiler is intelligent enough not to include unused references in the assembly manifest,maintaining unused assembly references in your project still has a serious downside. Adding a referenceto an external assembly is a significant decision from a large-scale design viewpoint and should be madewith great care.
Having a dependency set in your project is a sort of a green light telling developers that they are free touse the services that the referenced assembly provides. In some cases, especially after some large-scalechanges to the code base, the reference is made obsolete, and dependencies on the referenced assemblyhave to go to some other place. The design has changed, and the project should not depend on the refer-enced assembly any more. However, if the reference from the project properties is not removed also,there is a good chance that this assembly will get used again at some point. In a way, it is only logical.“Why do we have a reference to an assembly if we are not meant to use it?” any developer might right-fully ask. In order to avoid this kind of confusion, avoid unused references and remove them as soon asthey appear.
Fortunately, in Visual Studio 2005 finding and removing unused references is very easy. Visual Studio 2005boasts a “Remove Unused References” feature, and you can even see the Unused References... button inFigure 7-3, which shows the References tab in the Project Properties window.
Basic Hygiene in the Rent-a-WheelsApplication
Thanks to the early age of the Rent-a-Wheels application, the smells described in this chapter were notrife. The changes we made amounted to removing a few commented methods and a few detached eventhandlers. Listing 7-4 shows one detached event handler that was eliminated after we performed basichygiene refactorings on the Rent-a-Wheels application.
Listing 7-4: Detached Event Handler in the Rent-a-Wheels Application Was Eliminated
Private Sub Button1_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles Button1.Click
Refactoring: Remove Unused ReferencesMotivationHaving strict policies on controlling references and removing unused referencesquickly will help keep dependencies at bay in your project.
Related SmellsUse this refactoring to eliminate the Unused References smell.
MechanicsSelect the References tab in the Project Properties window. Open the Unused Referencesdialog by pressing the Unused References... button. Visual Studio will search for unusedreferences and display them. Remove unused references by pressing the Remove button.
79796c07.qxd:WroxPro 2/25/08 9:01 AM Page 192
Chapter 7: Basic Hygiene
193
Listing 7-4: Detached Event Handler in the Rent-a-Wheels Application Was Eliminated (continued)
Dim oCn‘Test SqlConnectionoCn = New SqlConnection(“Data Source=TESLA-DAN;” + _“Initial Catalog=RENTAWHEELS;User ID=sa”)oCn.Open()
End Sub
SummaryThis chapter showed some very common cases of poorly maintained, unhygienic code. In such cases rotwill easily take hold, so you need effective methods to identify and eliminate it.
Dead code can often be a source of confusion and additional complexity. There are different ways fordead code to appear — as commented sections, unused event handlers, removed controls, ignoredmethod parameters or return values, and so on. Whatever the form, dead and redundant code shouldbe eliminated without exception.
Very often programmers unintentionally give unnecessarily broad scope and visibility to certain pro-gramming elements. To construct well-encapsulated code, it is important that you adhere to the princi-ples of information/implementation hiding. Exposing unnecessary internal detail goes against thisprinciple, and in such cases you can improve encapsulation by reducing the scope and access level ofoverexposed elements.
Often, when reading the code, you rely on the Imports section to understand dependencies in yourcode. Using fully qualified names instead of relying on the Imports section can make comprehendingthe dependencies in your code much more difficult. Performing imports explicitly should be favoredover using fully qualified names in the body of the code.
Each existing reference will tell the programmer that a referenced library is meant to be used from the current library. Problems occur when obsolete references are not removed, because such referencesencourage programmers to freely make use of the referenced libraries. It is a good practice to removeunused references, and Visual Studio has a feature that can help you do this.
The next chapter will deal with standard refactoring techniques. These refactorings cannot be performedwithout a good understanding of code and problem domain. They can have a profound impact on theway your code is designed; dealing with them means dealing with core issues related to code quality. I hope that the preliminary refactoring in this chapter served as a good warm-up for the more complexwork that lies before us.
79796c07.qxd:WroxPro 2/25/08 9:01 AM Page 193
79796c07.qxd:WroxPro 2/25/08 9:01 AM Page 194
Part III: Getting Started with Standard RefactoringTransformations
In Part III you will start out with some of the most basic and most common refactor-ings. These often do not span more than one class, and while they are very importanton their own, eliminating some unpleasant and very basic smells, they are also usedto prepare the code for some more complex refactorings.
In this part you will also witness how a direct link between the source code and theproblem domain is being established. That is why I will start out with a few words onobject-oriented analysis, a discipline that can help you understand well the problemyour application is meant to solve. You will learn why the right choice of names incode is so important.
Next you will read about an important distinction between public and published inter-faces and about this distinction in the context of Rename refactoring. This matterhas an important bearing on the rest of the refactorings and will be of consequencethroughout the book, because you are learning how to perform refactoring safely.
Next, this part turns to the basic structuring of code, and you will learn how to performan Extract Method refactoring, one of the most commonly used types, and see how ithelps you deal with duplication in your code and improves reuse. You will see howsome other related refactorings, like Replace Temp with Query or Inline Temp (DA),work, why using literal values liberally in your code is not such a good idea, and seesome other smells and refactorings.
By the end of this part, you should be able to see how the contours of the object-oriented design of the sample Rent-a-Wheels application are slowly but unques-tionably beginning to appear.
79796c08.qxd:WroxPro 2/25/08 9:02 AM Page 195
79796c08.qxd:WroxPro 2/25/08 9:02 AM Page 196
From Problem Domain toCode: Closing the Gap
Programmers have in one sense a unique profession. It’s their job to analyze and understand the busi-ness of others. Often they gloat over speed and algorithm effectiveness, forgetting that a program isonly as good as the extent to which it fulfills its requirements and the client’s needs. Unless you areone of those programmers who prefer counting processor cycles to making your software purposefuland useful to others, you will agree that understanding the problem domain of your application is thefirst and most critical step in the process of software development.
This process is by no means trivial. You have to guide your clients in discerning the essential require-ments from those that are superficial, in defining the rules of the business, and sometimes even ineducating your clients about proven solutions in software design. While you are at it, you have tolearn yourself and accumulate a compendium of knowledge on the problem domain you are dealingwith. Finally, you use this knowledge not only to make your application work properly and fulfilluser requirements, but also to make your code meaningful, establishing the links connecting theproblem domain, the design, and the source code.
In this chapter you will see how these links are established.
❑ The chapter touches upon some general subjects like object-oriented analysis and design.You will explore a few analysis methods like use cases and textual analysis and see howthese techniques can help you define the requirements your software must meet.
One caveat: These are all very important concepts on their own, and this chapter will only tellyou what you need to understand for our discussion of refactoring. So don’t take this chapter as acomprehensive guide on the subject.
❑ You will read about naming guidelines, why they are important, and how refactoring canhelp. (And you’ll even revisit a naming convention well known to veteran VB programmers,Hungarian notation.)
79796c08.qxd:WroxPro 2/25/08 9:02 AM Page 197
❑ You will see some refactorings that can help you in closing the gap between code and problemdomain, like the Rename and Safe Rename refactorings, which can help you eliminate smellslike Unrevealing Names and Long Method. You’ll even get to hear a few words of VB trivia.
❑ Then I will talk about the difference between published and public interfaces. This difference ispurely conceptual, meaning that you will not be able to use code to enforce it. But it has a greatimpact on the level and kind of refactorings you are allowed to perform on the code.
After this chapter is over, you will be much more aware of good practices that make code readable tohumans. Taking into account that other people will work on your code is not only altruistic, it is also animportant part of what being a good programmer is all about.
Understanding the Problem DomainThere are many approaches to creating software. Some developers are happy to dip into the code rightfrom the project’s outset, while others prefer to have everything laid down, described, and modeled indocuments and diagrams, thoroughly worked out before even the first line of code is written. Whereveryour style happens to be, and most likely it is somewhere in the middle, you without exception need tospend a significant amount of effort on understanding the requirements, the problem domain, and thegeneral context for your software.
Personally, I am not very keen on separating the roles of analyst and a programmer between two nar-rowly specialized individuals, something that is common practice in larger teams adhering to some of themore traditional and formal software-development methodologies. The problem with this approach is thatit introduces another “level of indirection” between the user and the developer. And if another step incommunication is established on the customer’s side, in the form of someone delegated to represent all theinterested parties, like final users and domain experts, a lot of disruptive noise is introduced during therequirements-gathering process.
You will generally start out by trying to get informed on the problem domain and trying to understandclient needs. These initial activities on the project can take the form of meetings, interviews, correspondence,and/or document exchange. Your interlocutor can be any interested party — the final user, manager,domain expert, or someone else. This process will typically go through certain phases of knowledge acquisi-tion and structuring that I describe in the next section. At the first step you will probably be confronted witha wealth of information that you understand only partially, and your reaction will be to try to understandand organize this information. Bear in mind that the steps I am about to describe are performed iterativelywith an evolutionary approach in mind. So take a look at this typical cycle of software analysis.
Step One: Gathering the InformationWhen you get started, unless you have previously made a system that caters to the same businessdomain, you’ll inevitably end up with that “first day at work” feeling. A lot of jargon will be used atthe first meetings, making it difficult for you to understand the complete meaning of the conversation.At this point it is important to gather as much information as possible, to start to organize it, and tocarefully take note of all the doubts you might have and all the facts you need to further clarify orhave explained to you.
Definition: A problem domain is a term that refers to all real-world concepts, entities, andprocesses that are part of the problem to be resolved by the system you are constructing.
198
Part III: Getting Started with Standard Refactoring Transformations
79796c08.qxd:WroxPro 2/25/08 9:02 AM Page 198
In order for this process to be effective, you should hear the opinions of as many interested parties aspossible. You might wish to speak to managers, to final users, domain experts, contractors, and so on.They all almost certainly have their own unique views of the project, the business, the requirements, andthe priorities of those requirements. It’s your task to serve as a sort of mediator, reconciling differentviews and making sure that all legitimate concerns are met.
On one project for a major bank that I participated in, an IT head manager was very keen on exploiting thebenefits of browser-based thin-client architecture: lower administration costs, ease of distribution, simplehardware upgrade path, and so on. A part of the application was to cater to cashiers, a typical teller frontend. They were still using a standard application made in Visual Basic 6, which had a DOS-like character-based interface. After spending some time with them, I realized that they needed an extremely responsiveuser interface that would operate without a glitch or a pause and in which all the options were easily acces-sible through different keyboard shortcuts. I decided to make a very simple browser-based prototype thatcashiers could try out. It was not long before the solution shortcomings in these particular circumstancesbecame apparent. The system’s lack of responsiveness, and the inability to use standard key combinations(the mouse was used for those instead), were described as “torture” by one of the employees. It was soonapparent that in this case that particular technology was not applicable, and the IT department wasswayed towards a compromise solution: a browser-distributed standalone .NET managed executable.
Still, unless you are able to communicate well, you have no use for the information that you havegathered during this first step. So the next step involves developing a common vocabulary.
Step Two: Agreeing on the VocabularyAfter the initiation, you need to make sure that all the information you have gathered is as precise aspossible. In order to do that, you first must make sure that everybody participating in the project speaksthe same language. As you can imagine, I am not referring to English, German, or some other naturallanguage. In every line of business, a specific language is developed with precise meanings that are notimmediately apparent to laymen (or developers in this case). This language can even have different flavors in different departments in the same organization.
It is crucial for enabling communication that common and precise meanings of terms are agreed upon in the early phases of the project. While this process sometimes occurs naturally, it is best if it is tackleddirectly and systematically and if all participants agree on consistent and precise meanings. As the projectprogresses, the vocabulary can be expanded and new terms can be added or even invented.
While developers need to carry the brunt of the process of vocabulary acquisition, other participantsmight need to cope with understanding some technical terms. For example, when you’re discussingusability with final users, those users should have a clear understanding of terms like shortcut key andaccess key and how they differ. These terms can also become part of the common vocabulary.
Something like a vocabulary document can prove to be quite useful for the purpose of formalizing and dis-seminating the common vocabulary. This document can also make the process of integration a lot easierfor newcomers.
The interviews recounted in Chapter 4 showed that the Rent-a-Wheels employees I spoke with haddeveloped their own vocabulary. While the conversations described there were probably easy to under-stand, there are some subtle differences between the meanings of the words as used by the employeesand the common meanings of those words. For example, the word renting for Rent-a-Wheels employees
199
Chapter 8: From Problem Domain to Code: Closing the Gap
79796c08.qxd:WroxPro 2/25/08 9:02 AM Page 199
describes the process of vehicle selection and the issuance of the receipt. A vehicle is given to a customeronly after it is handed over. The branch is an internal organizational unit that is defined not by the locationof the reception desk, but by the presence of its own parking lot.
Here is a small section of the vocabulary document developed for the purpose of the Rent-a-Wheelsapplication.
After you have analyzed the business and agreed on the vocabulary, it’s time to think about the require-ments that the system you are constructing should fulfill. Requirements are best described in the form ofusage scenarios, or use cases. You see more about that in the next step.
Step Three: Describing the InteractionsNow that you have an understanding of the problem domain and the purpose of the software you arecreating, it is a time to portray the requirements in a more structured way. You can do this by describingthe series of interactions between the user and the system that results in something useful for the user.Two of the most common ways to manage requirements are through use cases and user stories.
Use CasesA standard way to describe interactions between the system and the users is by means of use cases. Ause case is a description of a possible dialogue between the user and the system that results in the goaldesired by the user. While use cases may differ in format and level of detail, they focus on the systembehavior as perceived from the outside; they do not involve any assumptions about the way the sys-tem will be implemented.
Chapter 4 contains descriptions of the most important use cases for the Rent-a-Wheels application.
User StoriesA less formal way to manage requirements involves user stories. User stories are generally more condensedand less structured than use cases, and are written by customers in their own language. They do not con-vey enough information to implement the functionality required of the application, so if you use them youshould also have an expert involved in the project. This form of managing requirements is popular with theagile software development community.
Rent-a-Wheels VocabularyVehicle — Any vehicle, like a car, motorcycle, SUV, truck, et cetera, that is in a parkinglot and that a customer can rent. Each vehicle belongs to a single branch.
Renting a vehicle — A process whereby a customer selects a vehicle at the receptiondesk and the receptionist issues the receipt that is used to pick up the vehicle at theparking lot.
Handing over a vehicle — A process whereby the parking lot attendant hands over therented vehicle to a customer.
Branch — A Rent-a-Wheels rental company office that has its own parking lot. This isan internal organizational unit that can have multiple reception desks but always has asingle parking lot.
200
Part III: Getting Started with Standard Refactoring Transformations
79796c08.qxd:WroxPro 2/25/08 9:02 AM Page 200
Once you have structured the requirements, you can test your understanding by providing the clientwith the prototype of the software you are about to build.
Step Four: Building the PrototypeWith use cases at hand, you are ready to start the implementation. While prototypes can be throwaway, the best value is obtained by those that are evolutionary. They validate your understanding of the require-ments with the customers, both the functional requirements and ones that are not functional. There are anumber of other uses that prototypes can be employed for, such as testing some design and architecturaldecisions, providing the customer with proof of progress, checking interface design details, and so on.
In previous chapters of this book, you started to witness how the initial Rent-a-Wheels prototype isbeing transformed from the initial prototype into a fully functional application.
Once you start to code, the first thing you need to decide is the names you are going to use in your code.You need to name classes, methods, variables, namespaces, and so on in such way that they are bothunderstandable and consistent.
Naming GuidelinesIn order to write maintainable and readable code, you need to choose the names you employ very care-fully. Good naming is probably one of hardest skills you need to learn as a programmer. The names youchoose need to be concise and consistent and they need to communicate well with your fellow program-mers reading the code. This on the surface is a rather simple issue, but it can prove to be really daunting,even for the experienced programmer. It is important that you remember that perfection need not bereached in the first attempt, and that as you proceed you can simply rename the identifiers you need toimprove. Later on in the chapter you are going to see how this refactoring is performed. You can counton tools to help you with this task, so it is not too time-consuming.
Continued
Smell: Unrevealing NamesDetecting the SmellLook for identifiers that have no clear or immediately obvious meaning. This isapparent when you need to look inside the code in order to know what a certainelement’s purpose is. For example, if you need to look inside the method’s bodyin order to understand what the method is doing, it means that method’s nameis not conveying its intent and should be changed. Alternatively, comments maybe explaining a certain code element’s purpose better than its name, so that youhave to rely on them when reading the code.
Words that are not part of the project’s common vocabulary, do not relate well to theproblem domain, do not belong to the vocabulary of software development, and/or fitpoorly into the project’s general context are also poor choices.
201
Chapter 8: From Problem Domain to Code: Closing the Gap
79796c08.qxd:WroxPro 2/25/08 9:02 AM Page 201
So, because this is a rather tricky issue, it might be best to start by exploring some of the guidelines thatare easiest to follow, like capitalization styles.
Capitalization StylesWhile Visual Basic is a case-insensitive language, consistent capitalization style still has an effect on the read-ability and aesthetics of your code. Fortunately, Visual Studio enforces a single capitalization version for oneidentifier, so it will correct any difference in capitalization based on the first occurrence of the identifier. Forexample, if you try to write a variable as itemcount after you have declared it as Dim itemCount AsInteger, Visual Studio will change it automatically to itemCount. Another thing to bear in mind is thatother .NET languages are case sensitive, so if you change the capitalization of some public element in areusable .NET library written in VB, you break the compatibility between your VB library and client codewritten in some other language.
Visual Basic should respect capitalization style in general use in .NET framework libraries. Three stylesare in use, and each is applicable to a certain type of identifier.
❑ Camel case (also known as camelCase) is a capitalization style wherein the first word in theidentifier is written in lowercase and each subsequent word in compound identifiers has itsfirst letter capitalized. Words are joined without the use of spaces, underscores, or any otherspecial characters. For example, corporateAccount.
❑ Pascal case is a capitalization style wherein the first letter in the identifier and in each subse-quent word in compound identifiers are capitalized. Words are joined without the use of spaces,underscores, or any other special characters. For example, MonetaryTransaction.
❑ Upper case is a capitalization style wherein all letters in the identifier are capitalized. Forexample: System.IO or System.Web.UI. This capitalization style is less common and isused for two-letter acronyms.
Table 8-1 shows how these capitalization styles are used correctly in Visual Basic.
Other, more obvious signs of less-than-perfect names are abbreviations, unfamiliaracronyms, or capitalization styles that are not in line with the general .NET frameworkor VB capitalization guidelines.
Related RefactoringUse Rename or Symbolic Rename refactoring to eliminate this smell.
RationaleProgrammers use the names of methods, properties, classes, variables, parameters,namespaces, and so on in order to discern the purpose of some piece of source code.When these names are badly chosen, code fails in communicating its purpose to a programmer. Code that does not communicate is the essence of code that is difficult to maintain.
202
Part III: Getting Started with Standard Refactoring Transformations
79796c08.qxd:WroxPro 2/25/08 9:02 AM Page 202
Table 8-1: Identifiers and Capitalization Styles in Visual Basic
Simple Naming Guidelines Here are some fairly simple naming guidelines that are part of the .NET framework naming conventionsand should be followed by all .NET languages. They consist of some simple rules that can be enforced with-out a lot of intellectual effort, but should be respected for consistency, readability, and aesthetic purposes.
FxCop, a free analysis tool available for download at http://www.gotdotnet.com/Team/FxCop/,is capable of performing an intelligent spelling check on your code using capitalization to separatenames into constituent English words.
❑ Suffix any exception class with Exception, as in InsufficientFundsException.
❑ Suffix any attribute class with Attribute, as in SerializableAttribute.
❑ Suffix any event handler with EventHandler, as in AlarmEventHandler.
❑ Suffix any event arguments class with EventArgs, as in UnhandledExceptionEventArgs.
❑ Prefix any interface identifier with I, as in IDisposable or ISerializable.
❑ Avoid the ALL_UPPER_UNDERSCORE_DELIMITED style of declaring constants.
❑ Avoid misspelling words. As a matter of fact, you should be able to check your code with aspell-checking tool like Word after compound names are broken apart.
Identifier Capitalization Style
Class Pascal case
Method Pascal case
Property Pascal case
Parameter Camel case
Protected or private instance field Camel case
Local variable Camel case
Shared or public instance field Pascal case
Namespace Pascal case
Enum Pascal case
Interface Pascal case (starting with a capital I, as inIDisposable)
Event Pascal case
203
Chapter 8: From Problem Domain to Code: Closing the Gap
79796c08.qxd:WroxPro 2/25/08 9:02 AM Page 203
❑ Avoid using abbreviations and acronyms, unless they are widely used and understood. So naming a method ToXmlNode() is fine — no need to use the excessively longToExtensibleMarkupLanguageNode(). However, don’t use CalcFedTx() or anything of that sort; use CalculateFederalTax() instead.
❑ Finally, avoid Hungarian notation (see sidebar). Since this technology is made obsolete by moderntools, stick with easily pronounced and easily memorized names and rely on Visual Studio to pro-vide type safety and type information.
Good Communication: Choosing the Right WordsWhen choosing names, you have to always bear in mind one thing: the code you are writing is to be readby other people. It has to be easy to understand, informative, simple, and consistent, and it should followthe rule of least surprise. This way another person will easily be able to change the code, to understand it,and to become part of the team. And unless you have infinite memory, you will find that good naming
Hungarian NotationHungarian notation was invented by Charles Simonyi, a distinguished programmer of Hungarian origin at Microsoft. In Hungarian notation a variable name is used toindicate variable type, intended use, or even scope. The variable name is composed of a prefix containing additional type information and a variable name as such. Forexample: strName, meaning a string variable containing the name information.
An elaborate list of abbreviations had to be developed and documented for this pur-pose. These abbreviations were then combined to make identifier prefixes for names.So, for example, piszMessage is assembled from p-i-sz-Message and means a (p)pointer to an (i) index into an array of (sz) null-terminated strings (Message) contain-ing message information. In pre-.NET versions of Visual Basic, Hungarian notationwas a part of Microsoft’s official programming guidelines and was considered a goodpractice by the general VB programming community. You typically saw a lot of vari-ables named things like lblName or btnSave, where the lbl prefix stood for “label”and btn for “button.”
When Hungarian notation was invented, programmers had to worry about typesafety because operations performed on incompatible types were a major source ofbugs. (I talked about type safety in VB .NET in detail in Chapter 5.) Type informationcontained in the variable name helped programmers with type safety.
However, this approach also had a number of drawbacks. The list of abbreviations soongrew out of proportion, and the abbreviations were often difficult to memorize and pro-nounce. Often the logical letter combination was already taken, so some other combina-tion, poorly associated with the term it represented, had to be used. Furthermore, if theunderlying type changed, unless the name changed also, the type information conveyedwould be false.
Today, Hungarian notation is widely considered obsolete. Compilers check typecompatibility and ensure type safety, and IDEs with IntelliSense and other VisualStudio capabilities are much more powerful in conveying type information and providing type safety.
204
Part III: Getting Started with Standard Refactoring Transformations
79796c08.qxd:WroxPro 2/25/08 9:02 AM Page 204
practices pay off even with code that you wrote yourself. This section discusses some guidelines for making your source code informative.
Using the Problem Domain Vocabulary as a Source for Identifier NamesFirst of all, try to use the words from the problem domain that are already part of the common vocabularyon the project as a basis for naming the elements in your code. This way, a more direct link between thesource code and the requirements is established, and the purpose of certain elements in your code is easilyunderstood.
If you go back to Chapter 2 and the Calories Calculator sample application, you can see that words likePatient and compound words like WeightInPounds and DailyCaloriesRecommended are used.These are all terms that are easily related to the specific problem this application addresses: recommend-ing a daily caloric intake.
As your code evolves, more complex design is introduced and terms from the problem domain might notsuffice to explain your intent. However, there is one more domain that is easily understood by anyone whoneeds to read or change the source, and this is the solution domain.
Using the Solution Domain Vocabulary as a Source for Identifier NamesA typical programmer understands typical programming terms and paradigms from computer science andsoftware development. Therefore algorithms, patterns, mathematical terms, and common programmingidioms won’t raise any eyebrows when used in your code. There is no more than a slim chance that anyonebut professional programmers will ever look inside the code. Even so, it is quite possible that you will have more than one layer of design in your code, and internal implementation details can often be easilyexplained in solution domain terms.
Choosing One Word to Represent One Meaning and Sticking with ItAnother important characteristic of your source code is that names are applied consistently. While a richvocabulary can enhance the creations of a poet, in software you have different values. A single name shouldbe employed to indicate a single concept. Even if the same name can serve well for some other concept,choose another word, because any ambiguity in your code adversely affects its ability to communicate.When applying this guideline, you have to be much stricter than any natural language.
Choosing Words That You Can Easily Pronounce and MemorizeYou will often discuss your code with your teammates, so make life easy for everyone and choose namesthat are easy to pronounce. This guideline is related to one about avoiding Hungarian notation, with its theabbreviations and acronyms: those are naturally more difficult to pronounce. On the other hand, a problemwith excessively long names is that they are more difficult to memorize. You no longer have the problemsthat older programmers had when different environments limited the number of characters they could useto name a variable, or when long names had a visible impact on memory consumption. All the same, try toavoid excessively long names for simplicity’s sake.
Using Nouns and Verbs ConsistentlyAs a general rule, use nouns for object and class names, and use verbs to name methods. You can usenouns or adjectives to name properties.
205
Chapter 8: From Problem Domain to Code: Closing the Gap
79796c08.qxd:WroxPro 2/25/08 9:02 AM Page 205
Rename RefactoringIn a perfect world you would get these things right the first time. However, you often won’t choose thebest names for your code the first time around. Once you come up with a better name, there is no reasonnot to change it.
Since the advent of refactoring tools, the once-tedious task of searching for all the invocations of codeand replacing the old name with the new name has been reduced to only a few clicks of the mouse.
Refactoring: RenameMotivationPoorly chosen names prevent the code from communicating with the programmer.They make it difficult to understand the purpose of the code and to establish the linkbetween the problem domain and the code. This makes maintenance work a lot moredifficult, because it is hard to find the spot in the code that needs to be modified. If thepurpose of the existing code isn’t clear, poorly chosen names can even result in codeduplication and poor reuse. In order to avoid all this, we can use Rename refactoring.
Related SmellsUse this refactoring to eliminate the Unrevealing Names smell or to rename elements that do not comply with general naming guidelines.
MechanicsChange the name of the element that suffers from the Unrevealing Name smell. Thensearch for all the code that invokes the recently changed element and rename it as well.(When you use tools this step is automated.)
BeforePublic Class FemalePatient
Inherits Patient
‘ ...‘The name DailyCalRec uses abbreviations and does not‘communicate very wellPublic Overrides Function DailyCalRec() As Decimal
Return 655 + (4.3 * WeightInPounds) + _(4.7 * HeightInInches) - (4.7 * Age)
End FunctionEnd CLass
Imports NUnit.Framework
<TestFixture()> Public Class TestFemalePatient
‘ ...<Test()> Public Sub DailyCaloriesRecommended()
Dim femalePatient As FemalePatient _= New FemalePatient
206
Part III: Getting Started with Standard Refactoring Transformations
79796c08.qxd:WroxPro 2/25/08 9:02 AM Page 206
femalePatient.Height = 72femalePatient.Weight = 110femalePatient.Age = 30
Dim expectedResult As Decimal = 1015.2
Dim realResult As DecimalrealResult = _‘The method DailyCalRec is invoked herefemalePatient.DailyCalRec()
Assert.AreEqual(expectedResult, realResult)End Sub
End Class
AfterPublic Class FemalePatient
Inherits Patient
‘ ...‘The long form is much easier to understandPublic Overrides Function DailyCaloriesRecommended() As
DecimalReturn 655 + (4.3 * WeightInPounds) + _(4.7 * HeightInInches) - (4.7 * Age)
End FunctionEnd CLass
Imports NUnit.Framework
<TestFixture()> Public Class TestFemalePatient
‘ ...<Test()> Public Sub DailyCaloriesRecommended()
Dim femalePatient As FemalePatient _= New FemalePatientfemalePatient.Height = 72femalePatient.Weight = 110femalePatient.Age = 30
Dim expectedResult As Decimal = 1015.2
Dim realResult As DecimalrealResult = _‘Invocation code also has to be changedfemalePatient.DailyCaloriesRecommended()
Assert.AreEqual(expectedResult, realResult)End Sub
End Class
207
Chapter 8: From Problem Domain to Code: Closing the Gap
79796c08.qxd:WroxPro 2/25/08 9:02 AM Page 207
Rename Refactoring in Refactor!Rename refactoring in Refactor! is applicable to private fields, properties and methods, and local variables.Once you select your target, it turns all references to a variable to linked identifiers and they are all renamedsimultaneously with the target. Figure 8-1 shows how all occurrences of the field firstNameValue are high-lighted when the cursor is placed over the field and the Rename option in the Refactor! menu is invoked. Allreferences to the field are renamed simultaneously as you start to type.
Figure 8-1
For public elements, Refactor! provides Safe Rename refactoring, which I talk about in the next section.
Published and Public InterfacesOne of the reasons for the great popularity of Visual Basic is the component paradigm. Components arereusable binaries with well defined interfaces. Visual Basic programmers quickly learned how to makeuse of the ready-made COM components that provided an enormous productivity boost. The compo-nents provided new opportunities for reuse and encapsulation that led programming in VB to a newlevel of Rapid Application Development (RAD). Some of the great features of COM technology were the ability to create custom components in VB and to develop and use components across languages, so VB programmers could use components developed in C++ and vice versa.
While component technology in VB was a great advancement, programmers had to treat some issueswith care when using or creating components. For example, if the interface of the component waschanged with some new version of the component, the compatibility between the component and theclient would break, and replacing the old version with the new version of the component would resultin an application breaking down.
At the base of any component technology are the interfaces. Here I define the word interface in broadterms as anything exposed by the component to the outside world. Interfaces help you define the con-tract on how you can communicate with a certain component. Visual Basic also supports an interface asa language construct, wherein the contract is explicitly defined and separate from the implementation.While you can change the interface in a self-contained application with minimum consequences, thesame operation with reusable modules can have far-reaching effects.
208
Part III: Getting Started with Standard Refactoring Transformations
79796c08.qxd:WroxPro 2/25/08 9:02 AM Page 208
Self-Contained Applications Versus Reusable ModulesWhen programming, you can roughly categorize the project you’re working on into one of two groups,based on the reach your code has.
❑ Self-contained applications are those in which the code is used only in the context of the currentapplication and nowhere else. It also means that you have in reach all the client code of any class orother construct used in the code. In case you need to modify a signature of a method, for example,you are able to find all invocations of the method and to change them accordingly so the code doesnot break.
❑ Reusable assemblies are components and libraries that were created to be used by others inmany different projects. In this case you generally do not have any idea of the client code that isusing your component or library; neither is it possible for you to modify it. When releasing newversions of these components or libraries, you have to act with utmost care in order not to breakcompatibility of existing applications. When breaking compatibility with previous versions can-not be avoided, because in the end all projects have to evolve, a clear upgrade path and version-ing policy must be provided.
You can observe the difference between the two categories by comparing the direction of dependenciesbetween assemblies depicted in Figure 8-2.
Figure 8-2
When you deal with any private element, like a field or a method, you can be sure that any changes youperform are not far-reaching. You can easily find any invocation or reference to a private method or fieldand fix the problem that this change has caused.
Even when changing a public element in a self-contained application, you can be confident that in theend you’ll be able to fix any problem caused by the change. However, when changing public elements in reusable modules you have no idea of the cascading effect of change you might provoke. This meansyou have to develop a good versioning plan and a clear versioning policy for your product. If you donot, you can place clients in a very frustrating position.
Definition: A published interface consists of all public elements from your reusableassembly whose changes can provoke effects you are not able to control, becauseyou do not have access to the client code making use of the assembly.
Third-Party Data Library Other Third-Party Assembly
Client One Client N
Third-Party Widget
Self-Contained Application
Reusable Assembly
Client Two
209
Chapter 8: From Problem Domain to Code: Closing the Gap
79796c08.qxd:WroxPro 2/25/08 9:02 AM Page 209
Imagine you are shipping a new version that has fixed a few bugs, but also broken the backward compati-bility of your reusable component, in order to add some new functionality. A few clients will make use ofthe new functionality offered in a new version and will modify their code, and rebuild their projects inorder to do so. So far, so good. On the other hand, probably all the clients will try to make use of the newversion in order to resolve the bugs you have fixed and will do so without rebuilding the project, butrather by simply reconfiguring the application.
Check out the “Redirecting Assembly Versions” article in MSDN to see how applications can be recon-figured to make use of newer versions of assemblies. You can find the article here: http://msdn2.microsoft.com/en-us/library/7wd6ex19.aspx.
If the clients try to simply replace the old version with the new one without rebuilding client applica-tions after applying changes in the source code where needed, the upgrade will fail miserably.
It is worth noting that in .NET you can have multiple versions of assemblies registered side by side.When the client application that is making use of these assemblies is built, it is linked to a specific ver-sion. If you then install a new version, the original client application will continue to work with the orig-inal version of the assembly. In pre-.NET versions of VB, based on COM technology, the newer versionof the component replaced the old one. In cases where the compatibility between versions of the compo-nent was broken, a host of problems, also known as “dll hell,” ensued, resulting in broken applications.
Refactoring: Safe RenameMotivationThe basic motivation for this refactoring is the same as that for the simple Renamerefactoring: making code communicate its purpose and respect the naming guidelines.However, in this case you are dealing with an element that belongs to the publishedinterface, and you cannot reach all invocations of this element. You need to perform therenaming in a safe way.
Related SmellsUse this refactoring to eliminate the Unrevealing Names smell or to rename elementsthat do not comply with general naming guidelines.
MechanicsThis refactoring is generally performed on methods on properties. It is performed in afew consecutive steps.
1. Declare the method or property in your class with the same signature asthe targeted method and give it a new, improved name.
2. Cut the code from the original method or property and paste it into anewly created method with an improved name.
3. Make the original method or property delegate the call to the newly cre-ated method.
4. Mark the original method with ObsoleteAttribute and the System.ComponentModel.EditorBrowsable(EditorBrowsableState.Never)attribute. ObsoleteAttribute marks the invocation code of the originalmethod with a warning, while EditorBrowsable(EditorBrowsableState.Never) hides the original method from IntelliSense.
210
Part III: Getting Started with Standard Refactoring Transformations
79796c08.qxd:WroxPro 2/25/08 9:02 AM Page 210
Continued
BeforePublic Class FemalePatient
Inherits Patient
‘ ...‘The name DailyCalRec uses abbreviations and does not‘communicate very wellPublic Overrides Function DailyCalRec() As Decimal
Return 655 + (4.3 * WeightInPounds) + _(4.7 * HeightInInches) - (4.7 * Age)
End FunctionEnd CLass
Imports NUnit.Framework
<TestFixture()> Public Class TestFemalePatient
‘ ...<Test()> Public Sub DailyCaloriesRecommended()
Dim femalePatient As FemalePatient _= New FemalePatientfemalePatient.Height = 72femalePatient.Weight = 110femalePatient.Age = 30
Dim expectedResult As Decimal = 1015.2
Dim realResult As DecimalrealResult = _‘The method DailyCalRec is invoked herefemalePatient.DailyCalRec()
Assert.AreEqual(expectedResult, realResult)End Sub
End Class
AfterPublic Class FemalePatient
Inherits Patient
‘ ...<ObsoleteAttribute(“This method has been renamed.” + _“Use DailyCaloriesRecommended instead”), _
EditorBrowsable(EditorBrowsableState.Never)> _Public Overrides Function DailyCalRec() As Decimal
Return DailyCaloriesRecommended()End Function
Private Overrides Function DailyCaloriesRecommended() As DecimalReturn 655 + (4.3 * WeightInPounds) + _(4.7 * HeightInInches) - (4.7 * Age)
211
Chapter 8: From Problem Domain to Code: Closing the Gap
79796c08.qxd:WroxPro 2/25/08 9:02 AM Page 211
Modifying the Public InterfacesThere are certain tactics you can use to minimize the ripple effect of changes in reusable assemblies.Refactoring enables you to change code freely, but in the case of public interfaces you have to conductyourself in a more conservative manner. This means that public interfaces should be well thought out,built to last, and as extensible as possible.
Keeping Interfaces Stable and LeanIn this case it is necessary to do a little bit of planning and to try to think how the module might look in the future. This is a tricky thing to do, but making reusable components is not easy. Also, bear in mindthe Open-Closed principle: you should keep your assemblies open for extension and closed for change.
Publishing as Little and as Late as PossibleIn a way the open-closed principle is an extension of the principle discussed in Chapter 7: you shouldkeep the scope and access level of different elements to a bare minimum. Because every developmentprocess is susceptible to changes in requirements and design, postponing the publishing date as long as possible increases your probability of publishing a more mature and stable version of your software.Finally, avoid publishing inside the team that has access to the same code base, because it will slowdown your development and make it less agile.
Implementing Versioning Policies and Adhering to ThemIn .NET, you should use assembly version information to communicate version data. The important part of assembly version information is an assembly version number. This is the number used by .NET
End FunctionEnd CLass
Imports NUnit.Framework
<TestFixture()> Public Class TestFemalePatient
‘ ...<Test()> Public Sub DailyCaloriesRecommended()
Dim femalePatient As FemalePatient _= New FemalePatientfemalePatient.Height = 72femalePatient.Weight = 110femalePatient.Age = 30
Dim expectedResult As Decimal = 1015.2
Dim realResult As DecimalrealResult = _‘Compiler will mark DailyCalRec as obsoletefemalePatient.DailyCalRec()
Assert.AreEqual(expectedResult, realResult)End Sub
End Class
212
Part III: Getting Started with Standard Refactoring Transformations
79796c08.qxd:WroxPro 2/25/08 9:02 AM Page 212
runtime to bind assemblies together. Once the assembly is built referencing one specific version ofanother assembly, it uses this same version of the collaborating assembly in runtime. In .NET multipleversions of an assembly can exist in parallel (something not possible with COM dlls). If necessary,assemblies can be redirected to use other versions of collaborating assemblies by means of configurationfiles, machine configuration files, or publisher policy files.
The format of an assembly version number is as follows:
[major version].[minor version].[build number].[revision]
Your versioning policy should convey information about the amount of changes that each version brings.The client should always be able to conclude from version information whether the new version is com-patible with the previous one. Here is a sample policy that you might follow.
❑ Different build numbers — Compatible; no changes to published interfaces allowed
❑ Different minor versions — Backward compatible; additive; nonbreaking changes to publishedinterfaces and making elements obsolete allowed
❑ Different major versions — No compatibility guaranteed
Look at Table 8-2 to see some examples of compatibility between versions. “No” in the “Is Compatible”column means that the two versions are not guaranteed to be compatible; they may be compatible bycoincidence.
Table 8-2: Compatibility Between Different Versions of an Assembly
Using “Safe” Refactorings When Performing ChangesWhen you perform safe refactorings, the new version of the code is kept compatible with the previousversion, but the modified element is declared obsolete. All the work is delegated to a new element. Thismeans that all client code continues to operate as before, but all users are warned that certain elementsare to be discarded in the near future. For the mechanics of safe refactorings, take a look at the SafeRename refactoring discussion earlier in the chapter.
Original Version New Version Is Compatible
1.5.1254.0 1.5.2311.0 Yes
1.5.1254.0 1.5.0000.0 Yes
1.8.1114.0 1.9.1114.0 Yes
1.8.1114.0 1.7.1114.0 No
2.1.2325.0 3.0.1.2990.0 No
2.1.2325.0 1.5.8856.0 No
213
Chapter 8: From Problem Domain to Code: Closing the Gap
79796c08.qxd:WroxPro 2/25/08 9:02 AM Page 213
Announcing Your ChangesBy using ObsoleteAttribute, you can communicate to your users the decision to eliminate certainelements in the near future. If you do this in timely manner, you give them the opportunity to preparefor modifications and avoid the surprise and confusion produced by abrupt changes to publishedinterfaces. Never use exceptions for this purpose. Exceptions artificially maintain versions as compati-ble, but applications break unexpectedly at runtime. You can recognize this incorrect use of exceptionswhen you see a method that has only a single statement, one that is throwing an exception under anycircumstances in which that method might be called.
Thinking About Extensibility When you are making reusable components, because refactoring cannot be performed as freely as usualyou should give more thought to design than usual. The following sections discuss three examples oftypical choices you can make when designing your components and how they affect extensibility.
Fields Versus PropertiesIf you use public fields instead of properties, you cannot make your fields read-only if one day it’s neces-sary. Neither can you change the underlying representation of the data or add data-validation routines.You have to change a field to a property, and that is a change that breaks compatibility between versionsof published interfaces. This said, remember that the best option is to keep your field completely private.Not all the data an object needs has to be visible from the outside.
Interface Versus Abstract ClassThe main difference between the interface and the abstract class in Visual Basic is that the abstract (MustInherit) class can also hold implementation for a method that is not marked as abstract (MustOverride). This leads to an interesting afterthought: if you add a new method to an abstract class and provide it with the default implementation, you keep the code compatible.However, if you add a method to an interface, all classes implementing this interface have to bechanged, because the newly added method has to be implemented somewhere along the hierarchy.
Shadowing Versus OverridingRemember, in order to achieve polymorphic behavior you must override elements in the base class. Foryou to be able to do that, those elements have to be declared Overridable in the first place. Consequently,if you are not sure whether you or someone else might need to override some method or a property, youhad better declare it Overridable just in case, unless you have compelling reasons not to do so.
If you are not sure about the difference between shadowing and overriding, take a look at the article“Differences Between Shadowing and Overriding” on MSDN: http://msdn2.microsoft.com/en-us/library/ms172785(VS.80.aspx.
Safe Rename Refactoring in Refactor!After you perform Safe Rename refactoring a new public method or property is created, marked withlinked identifiers so you can write the new name for it. The body of the original method or propertycontains a single line of code, a delegation code to a new method or property. The original is alsomarked with the Obsolete and EditorBrowsable(EditorBrowsableState.Never) attributes.Your applying EditorBrowsable means that the original method or property will not show up inIntelliSense, minimizing its use in the future. The Obsolete attribute produces a warning message
214
Part III: Getting Started with Standard Refactoring Transformations
79796c08.qxd:WroxPro 2/25/08 9:02 AM Page 214
in the IDE in code that already uses this method or property, announcing to the user that it is depre-cated and is to be removed in future releases of the assembly. You can see the result of Refactor!’s SafeRename refactoring in Figure 8-3.
Refactor! makes it easy to perform Safe Rename. However, unless you are dealing with published inter-faces, this refactoring is overkill. Refactor! does not provide simple (symbolic) Rename for public ele-ments yet, but it is only logical to assume it will do so in the future.
Figure 8-3
Continued
An Object-Oriented Design Principle: Open-Closed You have already seen me mention “change” in this book quite a few times. It’s a preva-lent force in software development, and there is no use fighting it. New and changingrequirements are the order of the day for any typical software development shop. Theproblem with changes comes when they start producing an uncontrolled ripple effectinside your code, wreaking havoc and unexpectedly breaking code or introducing bugs.
If you are able to write code in such a way that new features can be written as addi-tions that do not require you to compile the original assembly again, you can be surethat no change has been introduced into the assembly.
DefinitionAccording to Bertrand Meyer, in Object-Oriented Software Construction (Prentice Hall,1988), “Software entities (classes, modules, functions, etc.) should be open for exten-sion, but closed for modification.”
I want to examine this definition.
First, I want you to understand what is meant by “closed for modification.” When youcreate a piece of software, you need to go through the significant effort of analyzing theproblem, gathering the requirements, designing the software, implementing the solu-tion, and testing the result. Each time you introduce a change to your code, you have togo again through more or less the same cycle. If not, you risk shipping software that isfraught with bugs. So once you ship your assembly, you should try not to modify itunless you need to fix a bug.
215
Chapter 8: From Problem Domain to Code: Closing the Gap
79796c08.qxd:WroxPro 2/25/08 9:02 AM Page 215
However, you need to be able to respond to new-feature requests. The solution is to beable to extend the behavior of your assembly. This is why entities should be “open forextension.”
The way to achieve this in object-oriented software is through the abstraction. The“client” class should always depend on the abstraction and not on the concrete imple-mentation. This way you can modify the client and add new functionality by creatingnew implementations of the abstraction. In Visual Basic you generally use interfacesand abstract classes to express these abstractions. Take a look at an example to see howthis works out in more concrete terms.
ExampleThink back to Chapter 2 once more. If you remember, at one point I had my Patient classwith a host of *Man and *Woman methods. So, if I wanted to calculate the recommendeddaily amount of calories for a male patient, I’d call the DailyCaloriesRecommendedManmethod. For a female patient, I’d call DailyCaloriesRecommendedWoman. I also createdthe PatientHistoryXMLStorage class to take care of the persistence functionality of theapplication. I generated XML to save patient data. Had I kept the *Man and *Womanmethods I would have to program an XML generation routine like this:
‘ ...“<dailyCaloriesRecommended>” + _IIf(patient.Gender = Gender.Male, _patient.DailyCaloriesRecommendedMan.ToString, _patient.DailyCaloriesRecommendedWoman.ToString + _“</dailyCaloriesRecommended>” + _‘ ...
The problem with this code is that it is not open for extension. Imagine that I wouldlike to add new functionality to my application so it can calculate a recommendeddaily amount of calories for child patients. The only solution would be to modifyPatientHistoryXMLStorage’s XML-generation routine by changing the IIf functionto a longer, If-Else statement:
‘ ...Dim dailyCaloriesRecommended As DecimalIf (patient.Gender = Gender.Male) Then
dailyCaloriesRecommended = _patient.DailyCaloriesRecommendedMan
ElseIf (patient.Gender = Gender.Female) ThendailyCaloriesRecommended = _patient.DailyCaloriesRecommendedWoman
ElseIf (patient.Gender = Gender.Child) ThendailyCaloriesRecommended = _patient.DailyCaloriesRecommendedChild
End If‘ ...“<dailyCaloriesRecommended>” + _ dailyCaloriesRecommended.ToString _“</dailyCaloriesRecommended>” + _‘ ...
216
Part III: Getting Started with Standard Refactoring Transformations
79796c08.qxd:WroxPro 2/25/08 9:02 AM Page 216
Rename and Safe Rename Refactoring in the Rent-a-Wheels Application
By just a very superficial visual analysis, you can see a number of obvious flaws in the naming practicesfor the Rent-a-Wheels application code. The mix of abbreviations and Hungarian notation used in thecode means you have to perform extensive code modifications. Table 8-3 shows some of the Renamerefactorings that need to be performed.
Table 8-3: Rename Refactoring and the Rent-a-Wheels Application
Continued
Original Name Renamed To
FrmRt VehicleRental
FrmCat VehicleCategoriesMaintenance
oCn connection
oCmd Command
I have implemented a new functionality, but it is more complicated than necessary anderror-prone. Finally, I had to modify and rebuild the original assembly, exactly the thingI should try to avoid according to the open-closed principle.
Fortunately, I spent some time refactoring my application so that by the end of Chapter 2,the XML-generation routine looked like this:
“<dailyCaloriesRecommended>” + _patientValue.DailyCaloriesRecommended.ToString + _“</dailyCaloriesRecommended>” + _
The patientValue variable references the instance of either MalePatient orFemalePatient. In both cases the code of the XML-generation routine is the same.This is possible because both the MalePatient and FemalePatient classes areextending the common parent class, Patient, and the routine was programmed to anabstraction, not to an implementation. Now say I need to calculate a recommendeddaily amount of calories for child patients. All I need to do is add another class to myproject: ChildPatient. This class, just like MalePatient and FemalePatient, needsto extend the common abstraction, the Patient class:
Public Class ChildPatientInherits Patient
The variable patientValue in this case points to an instance of ChildPatient, and the XML-generation routine is left untouched. This means that the code is open for extension.
217
Chapter 8: From Problem Domain to Code: Closing the Gap
79796c08.qxd:WroxPro 2/25/08 9:02 AM Page 217
Table 8-3: Rename Refactoring and the Rent-a-Wheels Application (continued)
Most of this refactoring work goes smoothly; however, a couple of things to have in mind are the following:
❑ When you rename a control or a form in Visual Studio, event-handling routines are not updated.You must rename event-handling routines manually. For example, you must manually changethe name FrmBranch_Load to BranchMaintenance_Load after the form FrmBranch has beenrenamed BranchMaintenance.
❑ Sometimes local variable names can clash with field names. In that case you can resolve the naming conflict by qualifying the field with Me. For example, Me.branches =branches.Tables.Item(0).
Since Rent-a-Wheels is a self-contained executable, there is no need to use Safe Rename refactoring.
SummaryYou have seen in this chapter how there is more to names than you might think at first. They help youestablish the link between the problem domain and the source. In order for this link to be effective, it isimportant to formalize the language you are using in the project through a common vocabulary. Andbecause you will rarely get things just right the first time around, you can use Rename refactoring to correct and improve the choice of words in your code.
You obtain further benefits in code comprehensibility and maintainability by consistent naming prac-tices. For these ends, always follow Visual Basic and .NET naming conventions and guidelines. A muchmore complex issue is the right words to employ when writing code. Try to base your word choice onproblem and solution domains, stick with a single meaning for each word, and use each word to repre-sent a single concept in your code. Also, try to create names that are easy to memorize and pronounce.
When dealing with reusable assemblies, you do not have the same freedom with your code as when youwork on self-contained projects. While you are still free to change the underlying implementation, anychange to the interface used by others can provoke numerous problems for clients. You cannot assume thatclients should rebuild and modify their applications with each release you perform. Instead, you need toclearly identify published interfaces, those that are freely exposed to users of your reusable assemblies. Youmust deal with such interfaces with utmost care, always announcing incoming changes before implement-ing them, respecting versioning policies, performing refactorings safely, and designing the code in such away that it can withstand the changes and is open for extensibility.
In the next chapter you’ll dive right into the world of object orientation, its basic principles, and somevery common refactorings that will let you efficiently combat complex and unstructured code on themethod level.
Original Name Renamed To
oAdapter adapter
txtName BranchName
FrmBranch_Load BranchMaintenance_Load
218
Part III: Getting Started with Standard Refactoring Transformations
79796c08.qxd:WroxPro 2/25/08 9:02 AM Page 218
The Method ExtractionRemedy for Duplicated Code
In this chapter we go back to the basics of object-oriented principles.
❑ You have certainly heard of encapsulation, but the benefits of encapsulation, like those of information and implementation hiding, are not automatically obtained. Nor can youguarantee them by using object-oriented capable language.
❑ Then you will investigate the simplest form of encapsulation: a function. The same principlesthat can be used to improve function organization can be used with methods. You will seewhy long methods are problematic and how they can be improved by method extraction.
❑ After that, you will face the dark side of programming in its worst form: duplicated code.You will see how duplicated code is potentially disastrous and why you should try toavoid it. You will see how even simple refactoring such as Extract Method can do a lot to avoid code duplication. Sometimes duplication is so misused it turns into a real anti-pattern, or so-called copy-paste style. In this chapter you will start to learn how to reusecode much more effectively.
❑ Finally, you are going to take a look at one of the oldest ailments from which your codecan suffer: magic literals. Fortunately, you are going to learn about the remedy and howmagic literals can be replaced with constants and enumerations.
Why Keep Code Encapsulated andDetails Hidden?
You often hear that encapsulation is the first pillar of object orientation. However, encapsulation in software did not start with object orientation. It was actually invented a long time before that.
79796c09.qxd:WroxPro 2/23/08 8:23 AM Page 219
In the very early days of programming, people noticed that certain patterns of instructions repeatedthemselves many times in code. In those days computer memory was a limited resource, so puttingthese instructions in only one place could result in significant memory savings. Soon they were puttingthose instructions together and calling them by a single name, instead of writing them each time theywere needed. This is how the subroutine was invented.
Very soon, an interesting side effect was noticed; subroutines made the code easier to comprehend andgrasp by programmers. Since a human brain is also limited in memory and in the number of ideas it canprocess, hiding certain details from the direct view had the very positive result of simplifying the codefor the programmer. A program could be composed of smaller units, thus making it easier to tackle com-plex problems, which could also be decomposed. This was much more convenient than using controlflow and numerous GOTO statements for the same purpose, which can produce very complex, tangledcode that is difficult to understand and follow, also known as spaghetti code.
Code like this inspired what is probably one of the most famous essays in software literature, “Go ToStatement Considered Harmful,” by Edsger Dijkstra.1
A few other benefits also resulted from use of the subroutine:
❑ Improved reuse resulted in better productivity, because the same solution for a certain problemcould be used over and over. Improved reuse meant less code and less duplication.
❑ Readability of the code was improved, because chunks of code could be given human-readablenames. This enabled programmers to establish a link between the code and the problem domain.(We discussed the benefits of establishing a link between the code and the problem domain in theprevious chapter.)
❑ Because you can reference a piece of code by its name, encapsulation means there’s no need to know any details of its implementation. This effect is generally referred to as information andimplementation hiding and you will learn more about it in the next section.
Information and Implementation HidingNow, if this chunk of code can receive parameters and return values, it means it can be observed from theoutside as a unit that receives input, does some processing, and produces the desired output. When view-ing the code from the outside, you are not concerned about implementation details; all you need to knowabout the function is its interface or the public part. You need not know any implementation details, suchas the operations or local variables that the method is using. Simply put, to use the function you need toknow the declaration, but you don’t need to know anything about the body of the function. For example,if you have a function that will return a sorted array of Account objects:
Public Function SortAccounts(ByVal accounts As Account()) As Account()
1Edsger Dijkstra (March 1968). “Go To Statement Considered Harmful.” Communications of the ACM 11 (3): 147–148.
Definition: Spaghetti code is code that has a complex and tangled control structureresulting from extensive use of GOTOs and other unstructured constructs. Such codeis very difficult to read, understand, debug, modify, test, and maintain.
220
Part III: Getting Started with Standard Refactoring Transformations
79796c09.qxd:WroxPro 2/23/08 8:23 AM Page 220
You need not know which sort of algorithm the programmer that implemented the method used. That pro-grammer could have used bubble sort, quicksort, heapsort, or any other algorithm. Thankfully, for you toget your sorted array of the Account object, you do not need to know any of these algorithms, nor evenwhich of them has been used. You don’t have to write this code, or even understand it; you can still use itand profit from it. (As an accomplished programmer you should be familiar with these sorting algorithms,mind you.)
What’s even more, if encapsulated code one day needs to be changed, as long as you do not changeany public parts (also known as the interface), you do not have to change any other parts of yourcode; the change is successfully localized. The possible ripple effect of changes in your code is thuseffectively minimized.
Encapsulation and information and implementation hiding have more sophisticated meanings in object-oriented programming. I will get back to the subjects of encapsulation and information and implementa-tion hiding in Chapter 11, but first take a look at how it all works out on a procedural level.
In order to organize your code in the form of well-structured, well-encapsulated methods that are easyto use, reuse, and understand, you should apply certain heuristics that are described in the next section.
Continued
Refactoring: Extract MethodMotivationSmall, well-named, granular methods will benefit your code in a number of ways:
❑ Better code comprehension: You can easily understand the purpose of thefew instructions in the code without getting bogged down with implemen-tation details.
❑ Increased reuse: There is a greater probability for more granular methodsto be reused.
❑ Minimized code duplication: With more effective reuse, code duplicationis minimized.
❑ Encapsulation of code: You don’t necessarily need to understand how amethod is programmed in order to use it.
❑ Changes can be performed in isolation: As long as the public part of amethod is not changed, the body of the method can be changed without a ripple effect on the rest of the code.
❑ Methods can be unit-tested: If you test large methods, the benefits of unittesting are reduced. You will be able to detect the error but will still have toput in an effort to discover where exactly the error was produced.
❑ Describe code better than comments: Comments can easily become obso-lete. Because the comments do not have any effect during execution, it isvery easy to change the code but forget to change the comments. Suchcomments are misleading and counterproductive.
This is one of the most basic and most common refactorings, and it is performed often. Itis essential for simplifying your code and making it easier to maintain and comprehend.
221
Chapter 9: The Method Extraction Remedy for Duplicated Code
79796c09.qxd:WroxPro 2/23/08 8:23 AM Page 221
Related SmellsUse this refactoring to eliminate Long Method, Duplicated Code, and Comments smells.
MechanicsThere are no strict definitions for a long method But as soon as a method is doing morethan one thing, the comments are used to explain what a section of the method is doing, or you can see that certain sections of code repeat themselves in more than one place, it istime to decompose the method by means of method extraction. Depending on local vari-ables, this process can be more or less complicated. In its complete form it can take the following six steps:
1. Identify the section of the method you will extract and think of a name thatexplains the intent well.
2. Declare a new method and copy the section of code you are extracting tothis method.
3. Search for local variables that are used only inside the new method anddeclare them inside the new method.
4. Search for local variables from the original method that are only read in thenew method. These variables should be declared as method parameters.
5. Search for a local variable that has been modified in the extracted section.This value should be returned by the method. If more than one variable ismodified in the extracted section, you will need to simplify the code furtherbefore performing the extraction. (See the Replace Temp with Query andSplit Temporary Variable refactorings in Chapter 10.)
6. Replace the extracted section of code in the original method with the call tothe newly created method. Delete the declaration of the local variable thatyou declared in Step 3 and that is now used only in the new method.
Note: Local variables can make method extraction much more difficult. Passing toomany parameters and returning too many values will result in an unnecessarily com-plicated method wherein the benefits of extraction are lost. In those cases you shouldtry to simplify the code before performing extraction. For example, you can eliminatesome local variables by performing Replace Temp with Query and Split TemporaryVariable refactoring (see Chapter 10).
BeforePublic Function RenderFundsTotalCell() As String
Dim total As DecimalFor Each account As Account In Me.accounts
total += account.BalanceNextDim formattedTotal As StringformattedTotal = “<P>Total:<B>” + _total.ToString(“C”) + “</B></P>”Return “<TD>” + formattedTotal + “</TD>”
End Function
222
Part III: Getting Started with Standard Refactoring Transformations
79796c09.qxd:WroxPro 2/23/08 8:23 AM Page 222
Decomposing MethodsAlmost every long method can be decomposed into a number of smaller, more granular methods thatare easier to manipulate and that will make your code more readable. This process of decomposition oflarger method into smaller ones is called method extraction. In order for you to perform this process effec-tively, extracted methods should contain code that fits well together, performing a meaningful singleoperation with clearly defined input and output, and that can be named in such a way that it can easilyestablish the link with the problem domain.
Circumference Calculation — Long Method ExampleThere is no clear-cut formula for performing method decomposition, so it is best if I start off with anexample. I have chosen the following problem: calculating the circumference of circle a given the centercoordinates and the coordinates of an arbitrary point on the circumference. You can visualize this by taking a look at Figure 9-1.
When you look at the drawing, the solution to the problem becomes pretty obvious. It comes down totwo geometrical formulae. This is clear if you decompose your problems into two calculations.
❑ Because you have the coordinates of point C — the circle’s center and point P — a point on the circumference — you can see that the distance between these two points represents a circleradius. If you calculate the radius, then you can use the following formula for calculating circum-ference length: C = 2rπ.
❑ You can see also see on the diagram that the a, b, and r lines form a right triangle, where r is the hypotenuse. This means that r can be calculated as r² = a² + b², according to thePythagorean theorem.
AfterPublic Function RenderFundsTotalCell() As String
Return “<TD>” + FormatTotal() + “</TD>”End Function
Private Function CalculateFundsTotal() As DecimalDim total As DecimalFor Each account As Account In Me.accounts
total += Me.account.BalanceNextReturn total
End Function
Private Function FormatTotal() As StringReturn “<P>Total:<B>” + _Me.CalculateFundsTotal.ToString(“C”) + _“</B></P>”
End Function
223
Chapter 9: The Method Extraction Remedy for Duplicated Code
79796c09.qxd:WroxPro 2/23/08 8:23 AM Page 223
Figure 9-1
Because methods are really quite similar to functions — the difference being that a method is associatedwith an object and a function is not (more on that in some of the following chapters) — I have programmedthe first version of the solution using the Sub Main function of the Module construct in a procedural style(Listing 9-1). You will see how you can decompose functions as you start to extract code from Sub Main.Later on in the book, in Chapter 11, you will see the solution programmed in an object-oriented way.
Listing 9-1: Calculate Circumference Length — Unstructured Example
Option Explicit OnOption Strict On
Namespace RefactoringInVb.Chapter9
Structure PointPublic X As DoublePublic Y As Double
End Structure
Module CircleCircumferenceLength
Sub Main()Dim center As PointDim pointOnCircumference As Point‘read center coordinatesConsole.WriteLine(“Enter X coordinate” + _“of circle center”)center.X = CDbl(Console.In.ReadLine())Console.WriteLine(“Enter Y coordinate “ + _“of circle center”)center.Y = CDbl(Console.In.ReadLine())‘read some point on circumference coordinates
y
x
P
C
rb
a
224
Part III: Getting Started with Standard Refactoring Transformations
79796c09.qxd:WroxPro 2/23/08 8:23 AM Page 224
Listing 9-1: Calculate Circumference Length — Unstructured Example (continued)
Console.WriteLine(“Enter X coordinate “ + _“of some point on circumference”)pointOnCircumference.X = CDbl(Console.In.ReadLine())Console.WriteLine(“Enter Y coordinate “ + _“of some point on circumference”)pointOnCircumference.Y = CDbl(Console.In.ReadLine())‘calculate and display the length of circumferenceConsole.WriteLine(“The length of circle “ + _“circumference is:”)Dim radius As Doubleradius = ((pointOnCircumference.X - center.X) ^ 2 + _(pointOnCircumference.Y - center.Y) ^ 2) ^ (1 / 2)Dim lengthOfCircumference As DoublelengthOfCircumference = 2 * 3.1415 * radiusConsole.WriteLine(lengthOfCircumference)
Console.Read()End Sub
End Module
End Namespace
You can test this code manually, and you will see that it performs the calculation correctly. However, thiscode is poorly structured, and a single method contains all the code used to solve the problem. You cansee that some comments are marking different sections in the code. If you analyze the code further, youcan see that some code is concerned with communicating with the user for the purposes of obtaining thedata for the calculation and displaying the result, and some other code is performing the actual calcula-tion. This is an important conclusion and can serve as the basis for the function extraction you are goingto perform in the next section.
Smell: Long MethodDetecting the SmellLong methods are easy to detect upon visual inspection. Sheer quantity of lines can bea good starting point. There is no strict rule for when the method is too long, but if it isdoing more than one thing it should be split into several methods. Another certain signthat the method is just too long is comments explaining the purpose of certain sectionsof the method.
Related RefactoringUse Extract Method refactoring to eliminate this smell.
RationaleLong methods are difficult to understand, debug, reuse, maintain, and unit-test. Theseproblems can be remedied by the extraction of sections of code from the original methodinto the new, more granular method.
225
Chapter 9: The Method Extraction Remedy for Duplicated Code
79796c09.qxd:WroxPro 2/23/08 8:23 AM Page 225
Extracting Circumference Length Calculation CodeYou will start out with your refactoring of the sample application by moving the code related to mathe-matical calculation into a separate function. Since this code calculates circumference length, you willdeclare a new Sub calculateCircumferenceLength and copy the code that performs the calculationinto the new function. You can even see that the comment on that section approximates well to the nameI have chosen for the new function. You can see the new sub’s code in Listing 9-2.
Listing 9-2: Calculating Circumference Length Function Extraction — Steps 1–3
Private Sub CalculateCircumferenceLength()Dim radius As Doubleradius = ((pointOnCircumference.X - center.X) ^ 2 + _
Smell: CommentsDetecting the SmellThe comments are easily detected by visual examination. When you see that the com-ments are carrying the information that should be communicated by code elementnames, or that this information is superfluous or obsolete, you have detected com-ments that should be refactored out of your code.
Related RefactoringUse Extract Method or Rename refactoring to eliminate this smell.
RationaleThe use of comments is traditionally considered a good programming practice for thesame reasons that you like to refactor your code — comments help you understandwhat the code is doing. Sometimes they can even group sections of code together orhelp you establish a connection between the code and the problem domain. However,comments have one big disadvantage: they don’t get executed, so they are very proneto becoming obsolete.
You can easily change a piece of code and test the results to change the behavior ofyour application, but if you forget to change the comments, no one will notice. No one,that is, until a trusting maintenance programmer tries to understand the code relyingon now-obsolete comments. There is probably nothing as frustrating as being led alongthe wrong path by obsolete comments.
Comments are sometimes used to help explain sections of poorly written code. In thosecases the valid solution lies not in comments but in refactoring the problematic code.
Instead of relying on comments to explain the purpose your code, use information provided by the names of elements in your code to communicate your intention. Thisinformation is always available and is less prone to becoming obsolete.
This does not mean that all forms of comments are bad. For example, XML comments areused to generate documentation and will provide information displayed by IntelliSenseand Object Browser.
226
Part III: Getting Started with Standard Refactoring Transformations
79796c09.qxd:WroxPro 2/23/08 8:23 AM Page 226
Listing 9-2: Calculating Circumference Length Function Extraction —Steps 1–3 (continued)
(pointOnCircumference.Y - center.Y) ^ 2) ^ (1 / 2)Dim lengthOfCircumference As DoublelengthOfCircumference = 2 * 3.1415 * radius
End Sub
As soon as you paste the code, two variables, pointOnCircumference and center, are marked bythe IDE as undeclared. This is because these variables are at the beginning of the Main() sub. Thesevariables are read inside the CalculateCircumferenceLength sub, so you need to pass them to theCalculateCircumferenceLength sub. You can do that by declaring these variables as parameters ofthe sub. Now the declaration of CalculateCircumferenceLength looks like this:
Private Sub CalculateCircumferenceLength(ByVal center As Point, _ByVal pointOnCircumference As Point)
With this, the IDE does not report errors any more.
Now the code compiles correctly, but CalculateCircumferenceLength is not really of much use. It calculates the circumference length and stores the value in the lengthOfCircumference variable, butthen does nothing with it. You can see that in the original sub, Main, the value of this variable is displayedto the user, and its calculation is actually the whole purpose of the application. What you need to do is topass this value as a return value of the CalculateCircumferenceLength sub, so that it is again availableto the code in the sub Main. This also means you need to convert the CalculateCircumferenceLengthsub into a function. The CalculateCircumferenceLength function is displayed in Listing 9-3.
Listing 9-3: Calculate Circumference Length Function Extraction — Steps 4 and 5
Private Function CalculateCircumferenceLength(ByVal center As Point, _ByVal pointOnCircumference As Point) As Double
Dim radius As Doubleradius = ((pointOnCircumference.X - center.X) ^ 2 + _(pointOnCircumference.Y - center.Y) ^ 2) ^ (1 / 2)Dim lengthOfCircumference As DoublelengthOfCircumference = 2 * 3.1415 * radiusReturn lengthOfCircumference
End Function
Now that you have successfully extracted the CalculateCircumferenceLength function, all that is left to do is to eliminate the extracted code section from the sub Main and make use of theCalculateCircumferenceLength function instead.
Before that, you should write a unit test for the extracted function, but I will skip this for the sake of space.Once you have transformed the sub Main code so it is calling the CalculateCircumferenceLengthfunction, you can eliminate the comment ‘calculate and display the length of circumferencebecause it is now superfluous and because the name of the new sub is eloquent. You can see what thiscode looks like in Listing 9-4.
227
Chapter 9: The Method Extraction Remedy for Duplicated Code
79796c09.qxd:WroxPro 2/23/08 8:23 AM Page 227
Listing 9-4: Complete code after Calculate Circumference Length Function Extraction
Option Explicit OnOption Strict On
Namespace RefactoringInVb.Chapter9
Structure PointPublic X As DoublePublic Y As Double
End Structure
Module CircleCircumferenceLength
Sub Main()Dim center As PointDim pointOnCircumference As Point‘read center coordinatesConsole.WriteLine(“Enter X coordinate “ + _“of circle center”)center.X = CDbl(Console.In.ReadLine())Console.WriteLine(“Enter Y coordinate “ + _“of circle center”)center.Y = CDbl(Console.In.ReadLine())‘read some point on circumference coordinatesConsole.WriteLine(“Enter X coordinate “ + _“of some point on circumference”)pointOnCircumference.X = CDbl(Console.In.ReadLine())Console.WriteLine(“Enter Y coordinate “ + _“of some point on circumference”)pointOnCircumference.Y = CDbl(Console.In.ReadLine())Console.WriteLine(“The length of circle “ + _“circumference is:”)Dim lengthOfCircumference As DoublelengthOfCircumference = CalculateCircumferenceLength(center, _pointOnCircumference)Console.WriteLine(lengthOfCircumference)
Console.Read()End Sub
Public Function CalculateCircumferenceLength(ByVal center As Point, _ByVal pointOnCircumference As Point) As Double
Dim radius As Doubleradius = ((pointOnCircumference.X - center.X) ^ 2 + _(pointOnCircumference.Y - center.Y) ^ 2) ^ (1 / 2)Dim lengthOfCircumference As DoublelengthOfCircumference = 2 * 3.1415 * radiusReturn lengthOfCircumference
End Function
End Module
End Namespace
228
Part III: Getting Started with Standard Refactoring Transformations
79796c09.qxd:WroxPro 2/23/08 8:23 AM Page 228
Extracting the Radius Calculation CodeYou have successfully performed your first method extraction. However, you should not stop just yet. Isee that the new function can also be simplified, by the separation of the calculation into the two steps I mentioned when we analyzed the problem initially. The first two lines of the function are concernedwith the radius calculation and therefore can be placed in a separate function. After these functions havebeen extracted you can also take the opportunity to eliminate the temporary (local) variables radiusand lengthOfCircumference because they do not improve code clarity. You can see how the codelooks after calculateRadius function extraction in Listing 9-5.
Listing 9-5: Calculate Radius Function Extraction
Option Explicit On
Public Function CalculateCircumferenceLength(ByVal center As Point, _ByVal pointOnCircumference As Point) As Double
Return 2 * 3.1415 * CalculateRadius(center, pointOnCircumference)End Function
Public Function CalculateRadius(ByVal center As Point, _ByVal pointOnCircumference As Point) As Double
Return ((pointOnCircumference.X - center.X) ^ 2 + _(pointOnCircumference.Y - center.Y) ^ 2) ^ (1 / 2)
End Function
Extracting the “Wait for User to Close” CodeIf you take a look at the end of Sub Main, you will see a somewhat curious line of code: Console.Read().Normally such code is used to read user input, but this code ignores the value the user entered.
You have probably guessed by now, but this line serves only to keep the console window open until theuser decides to close it. If you comment this line, you will see that the application still functions correctly,but the console window closes so rapidly that the user has no chance to read the result.
Someone reading it might be puzzled by this line of code whose purpose is not obvious. To make itspurpose clear, you can extract this single line of code into a separate method. You can name the methodWaitForUserToClose:
Private Sub WaitForUserToClose()Console.Read()
End Sub
So, you can see that even a single line whose purpose is not immediately obvious might be worthy ofmethod extraction, if by doing so you make the code more readable.
Extracting the Read Coordinates CodeNow you should go back to the Sub Main code. This sub is still too long. An interesting observation can bemade with regard to the coordinates’ input code; you can see that the segment for reading the center coor-dinates is quite similar to the code for reading the coordinates for the point on the circumference. The only
229
Chapter 9: The Method Extraction Remedy for Duplicated Code
79796c09.qxd:WroxPro 2/23/08 8:23 AM Page 229
difference is in literals containing the message for the user. You can do something about this. You can startby declaring a new sub for reading coordinates and copying the code for reading the center coordinateinto the body of the new sub. You should end up with the code shown in Listing 9-6.
Listing 9-6: Input Point Coordinates Extraction — Steps 1 and 2
Public Sub InputPoint()Console.WriteLine(“Enter X coordinate “ + _“of circle center”)center.X = CDbl(Console.In.ReadLine())Console.WriteLine(“Enter Y coordinate “ + _“of circle center”)center.Y = CDbl(Console.In.ReadLine())
End Sub
Since the code for reading both points is quite similar, you can try to make this method work for theinput of coordinates of both points. That is why I have named the function InputPoint without using a specific point name.
The IDE now marks the variable center as not declared. You can declare it at the beginning of the func-tion and rename it point at the same time. This way you can keep your method point-neutral. Then youshould make the sub return the point so that the original sub can make use of this function. The codefor this version of InputPoint is shown in Listing 9-7.
Listing 9-7: Input Point Coordinates Extraction — Steps 3 and 4
Public Function InputPoint() As PointDim point As PointConsole.WriteLine(“Enter X coordinate “ + _“of circle center”)point.X = CDbl(Console.In.ReadLine())Console.WriteLine(“Enter Y coordinate “ + _“of circle center”)point.Y = CDbl(Console.In.ReadLine())Return point
End Function
You can see that the only thing that keeps this code related to the specific circle center point is the two wordscircle center, part of the literals displayed to a user. You can parameterize this code by adding the newparameter pointName to the InputPoint function. Then you can construct the message to the user by con-catenating a string literal and pointName parameter. Take a look at the new version of the InputPointfunction in Listing 9-8.
Listing 9-8: Input Point Coordinates Extraction — Steps 3 and 4, Part 2
Public Function InputPoint(ByVal pointName As String) As PointDim point As PointConsole.WriteLine(“Enter X coordinate “ + _“of “ + pointName)point.X = CDbl(Console.In.ReadLine())Console.WriteLine(“Enter Y coordinate “ + _
230
Part III: Getting Started with Standard Refactoring Transformations
79796c09.qxd:WroxPro 2/23/08 8:23 AM Page 230
Listing 9-8: Input Point Coordinates Extraction — Steps 3 and 4, Part 2 (continued)
“of “ + pointName)point.Y = CDbl(Console.In.ReadLine())Return point
End Function
All that is left to do is to make use of this function in Sub Main. You will eliminate the original coordinate-reading code and replace it with a call to the InputPoint function. You will also eliminate an unnecessarytemporary variable in the process. (I will talk about Inline Temp refactoring in the next chapter.) The finalversion of the code is shown in Listing 9-9.
Listing 9-9: Final Method Extraction Version of the Calculate Circumference Length Function
Option Explicit OnOption Strict On
Namespace RefactoringInVb.Chapter9
Structure PointPublic X As DoublePublic Y As Double
End Structure
Module CircleCircumferenceLength
Sub Main()Dim center As PointDim pointOnCircumference As Pointcenter = InputPoint(“circle center”)pointOnCircumference = InputPoint(“point on circumference”)Console.WriteLine(“The length of circle “ + _“circumference is:”)Console.WriteLine(CalculateCircumferenceLength(center, _pointOnCircumference))WaitForUserToClose()
End Sub
Public Function CalculateCircumferenceLength(ByVal center As Point, _ByVal pointOnCircumference As Point) As Double
Return 2 * 3.1415 * CalculateRadius(center, pointOnCircumference)End Function
Public Function CalculateRadius(ByVal center As Point, _ByVal pointOnCircumference As Point) As Double
Return ((pointOnCircumference.X - center.X) ^ 2 + _(pointOnCircumference.Y - center.Y) ^ 2) ^ (1 / 2)
End Function
Public Function InputPoint(ByVal pointName As String) As PointDim point As Point
Continued
231
Chapter 9: The Method Extraction Remedy for Duplicated Code
79796c09.qxd:WroxPro 2/23/08 8:23 AM Page 231
Listing 9-9: Final Method Extraction Version of the Calculate Circumference Length Function (continued)
Console.WriteLine(“Enter X coordinate “ + _“of “ + pointName)point.X = CDbl(Console.In.ReadLine())Console.WriteLine(“Enter Y coordinate “ + _“of “ + pointName)point.Y = CDbl(Console.In.ReadLine())Return point
End Function
Private Sub WaitForUserToClose()Console.Read()
End Sub
End Module
End Namespace
This final version of the sample application looks a lot better. You now have a code that is a lot easier toread and to debug. If you, for example, have an error in calculation, you can easily test two functions incharge of mathematical computation and soon find in which of the two you made a mistake. You have alsomade a method shorter by extracting two very similar sections of code in a single method. This way, youhave eliminated duplicated code from the application. Duplicated code is a probably the worst smell thatyour code can suffer from and is actually so important I decided to dedicate a whole section to it. Before I end this section on method extraction I want to define another important smell very typical of VB code,and the cause of so many poorly written routines. While misuse of event-handling routines can be inter-preted as just another form of the Long Method smell, it is so prevalent and VB programmers fall into thistrap so easily that I decided to define it here as a smell on its own.
Smell: Event-Handling BlindnessDetecting the SmellGreat portions of code in the application are placed inside event-handling routines.
Related RefactoringUse Extract Method refactoring to eliminate this smell.
RationaleWhile this is only a special case of the Long Method smell, it is so common in Visual Basiccode that it deserves special mention. The typical scenario is as follows: you start out yourproject by placing components on the form and filling in the event-handling routineswhose declaration is generated by the IDE when you click the components on the form.As the project progresses, more and more code is simply added to existing event handlers.
The Rapid Application Development (RAD) paradigm grants an important productiv-ity boon, but if it is not coupled with good design and programming practices, all thebenefits are lost over the long term.
232
Part III: Getting Started with Standard Refactoring Transformations
79796c09.qxd:WroxPro 2/23/08 8:23 AM Page 232
I want to finish this section on method extraction with something that will make performing itimmensely easier. I want you to see how to perform Extract Method refactoring with Refactor!
Extract Method Refactoring in Refactor!This is one of my favorite refactorings that Refactor! can do. When you are dealing with complex methods,keeping track of all local variables can be complicated and time-consuming. Thanks to Refactor!’s visualaids, you can easily follow which variables are passed to a new method, which are returned by the method’sreturn value, and which are returned as output parameters. This can be a real time-saver. You can seeRefactor!’s Extract Method preview window in Figure 9-2.
Refactor! will correctly pass all variables read in the extracted method as parameters and all variablesassigned in the extracted segment as output parameters. If only one variable in the segment has been assigned, Refactor! will use this variable as the return value of the method. If no variables arebeing assigned in the new method, Refactor! will name this method Sub instead of Function.
Another interesting feature is the treatment that Refactor! gives to comments. When performing thisrefactoring, Refactor! will try to guess the name of the new method based on the comments above theextracted segment. I have already mentioned that comments are a good sign that you need to performmethod extraction, erase the comments, and use the method name to communicate the same informationthat the comments are currently communicating (see the sidebar on the Comments smell earlier in thechapter for more information). This can work out surprisingly well, and often the name that Refactor!comes up with by concatenating the words in comments can be used without alteration.
Figure 9-2
233
Chapter 9: The Method Extraction Remedy for Duplicated Code
79796c09.qxd:WroxPro 2/23/08 8:23 AM Page 233
When you are writing methods, strive to write them in such a way that they return only one value. Use ofoutput parameters is discouraged because it is probably a sign that the method is doing more than it should.Another sign that this refactoring has been carefully thought out is the color of the arrow displayed in thepreview window and in the Parameter Count section of the Action Hint window. If a method is returningmore than one value, the color of the arrow is red. If a method is returning a single value by means of thereturn value, the color of the arrow is green for the return value and blue for the input parameters. Thisaspect isn’t shown in Figure 9-3, but you can take my word for it that the arrow is red in the original win-dow, because the number of output parameters is greater than one.
Figure 9-3
Finally, I should mention that before displaying the Extract Method option in the menu, Refactor! willanalyze the code and display this option only if method extraction of the selected section is feasible.
I am sure you will promptly start to appreciate the benefits of Refactor!’s Extract Method. I am also sureyou will start to loathe code duplication after the next section.
The Duplicated Code SmellAt the last step in refactoring the original circumference calculation application, I identified two similarsections of code, extracted and parameterized a function, and replaced the two sections with a call to thenewly extracted function. While the two sections were not completely identical, I managed to recognizethe similarity, parameterize the difference, and effectively eliminate duplication from the code.
However, I still haven’t discussed the problems that duplicated code can lead to. The following listshows the problems duplicated code can create and discusses why it should be eliminated from yourcode base.
❑ Duplicated code adversely affects the maintainability of your code: If you need to modify apiece of code that has been duplicated, you’ll need to search for all occurrences of this code andmodify it more than once. This will obviously make you work much more than necessary, andwith a greater probability of error.
❑ Duplicated code is a powerful source of bugs: Imagine you need to modify your code. Becausekeeping track of code duplication is not very plausible, you are very likely to end up with anumber of occurrences of the same code that you missed and where you failed to implement thechange. Now you have a number of segments that do not behave as expected. This means thatyou have introduced a bug into your code.
❑ Duplicated code makes it difficult to establish a link between your code and the problemdomain: Because the same code is present in more than one place, it is difficult to understandhow your code relates to concepts from the problem domain. This means that the semanticvalue of your code is lost and the general design quality of your code is poor.
234
Part III: Getting Started with Standard Refactoring Transformations
79796c09.qxd:WroxPro 2/23/08 8:23 AM Page 234
❑ Duplicated code promotes longer methods: If you are simply copying the code, it means youare not preoccupied with structuring your code. This can often lead to the proliferation of long,non-granular methods. (See the sidebar on the Long Method smell earlier in the chapter tounderstand the adverse effect longer methods can have on your code.)
❑ Duplicated code means a bigger code base: Keeping the number of lines needed in your appli-cation at a minimum simply means that there’s less you need to deal with. In this sense, lesscode generally means less work. You will benefit from a smaller code base when optimizing,unit-testing, maintaining, comprehending, and generally dealing with your code.
Now that you understand why you should avoid code duplication at all costs, you can take a look at themost common sources of code duplication. This way you will be able to recognize duplication-producingbehavior when you program, and prevent code duplication right from the outset.
Sources of Duplicated CodeSeveral sources of duplicated code exist:
❑ The most common source of duplicated code is copy-paste programming. This style of program-ming is so important it deserves its own section, which you will read shortly.
❑ Another source of duplicated code can be simple unawareness that the code already exists inthe project you are working on. Maybe the project is too large, or maybe it is written in such away that it is not easy to understand what the code is doing, so you are not able to find the classthat is performing the action you need to implement.
Smell: Duplicated Code Detecting the SmellSometimes detecting duplicated code can be easy, because you can visually spot almostidentical sections of code in more than one place. This duplication can be the result of asimple copy-paste operation. At other times duplication is not at all obvious, and somelevel of pattern recognition is required. Once the similarity between sections in code hasbeen identified, further analysis is necessary to isolate duplication from the differentiatingcode. For example, a method can be parameterized so it can be used to replace similar sec-tions of code.
Some tools can also perform static analysis of code in search of duplication, but gener-ally this does not go beyond a simple textual comparison of code segments. However,this is still useful for detecting the copy-paste style of duplication.
Related RefactoringA number of refactorings can be employed to eliminate this smell: Extract Method,Extract Class, Extract Superclass, Pull Up Method, Pull Up Property, and so on.
RationaleDuplicated code results in a code base that is more difficult to maintain, because eachmodification has to be performed in more than one place. Such code is also more diffi-cult to comprehend, because it is hard to understand the purpose of the code sections.Code duplication is a sign of poorly designed code.
235
Chapter 9: The Method Extraction Remedy for Duplicated Code
79796c09.qxd:WroxPro 2/23/08 8:23 AM Page 235
❑ Sometimes the code can be so badly implemented that you prefer to write new code yourself.
❑ Today, open source has become ubiquitous, and a solution for numerous problems can be foundon the Internet. It is ever so easy to simply copy the code you need into your own project.
❑ Finally, literal values can become a source of duplication, as you will see later on in this chapter.
Whatever the reason, you should try to avoid the duplication. If you are not able to find your wayaround in the project you are working on, it is a good signal that the project code should be refactored.If you implemented some functionality on your own and consciously avoided using some unruly code,you should eliminate that unruly code from the code base altogether and use your improved versioninstead. When using open-source code, try to use a compiled binary: this way you will have easier timeupgrading the new versions of the component.
Copy-Paste ProgrammingThis style of programming is common with novice or inexperienced programmers. It is often the firstmechanism of reuse that a programmer learns and can lead to a significant productivity boost in the initialphase of learning the craft. Sometimes it is even used by more experienced programmers when they arefaced with ever closer deadlines. It is so simple to just copy and paste sections of code that you need.
Code snippets in Visual Studio: the Code Snippets feature in Visual Studio lets you paste predefined or cus-tom code snippets inside your code. Code snippets that come with Visual Studio will help you write somevery common lines of code by letting you paste skeleton code directly into the file you are working on. Thisfeature can improve productivity, especially for some novice programmers. However, if abused, this featurecan be a real hotbed of duplicated code. Beware the trap of copy-paste programming with this feature.
Even if you did duplicate code on purpose, the drawbacks are the same. This style of programmingshould be discouraged, and you should use better, object-oriented methods of reuse like delegation andinheritance. Refactoring techniques are very efficient in promoting reuse and eliminating duplicatedcode, and many refactorings have as a specific purpose the elimination of duplicated code. These tech-niques should be adopted as part of an overall style that promotes better design and reuse.
The next sidebar and section take a look at another dubious programming practice that often results induplicated code. Magic literals are one of the oldest programming ailments.
Smell: Magic LiteralsDetecting the SmellProbably the best way to start out is to analyze code visually or by performing a text search. Once you have spotted a literal, you can use IDE’s text search to look forother occurrences of the same value. Often you will come upon a magic literal valuewhile modifying the code: you should use that opportunity to replace the value witha constant.
Related RefactoringUse Replace Magic Literal with Constant refactoring to eliminate this smell.
236
Part III: Getting Started with Standard Refactoring Transformations
79796c09.qxd:WroxPro 2/23/08 8:23 AM Page 236
Magic LiteralsMagic literals are hard-coded values of numbers and strings that are disseminated liberally inside the code.These values have a special meaning with regard to program logic, but when they are placed as literals justanywhere in the code, the purpose of these values becomes less than obvious. In order to make the purposeof these variables explicit, you should give them names.
In Visual Basic you can declare constant values by means of the Const keyword. These values are guar-anteed not to change during program runtime. In some cases constant values can be logically groupedtogether; in these cases enumerations can be used instead of constants.
To resolve the problem with magic literals, you should replace them with constants or enumerations. Thisway your code will became easier to understand and easier to modify, because the values can be changedin a single location instead of your having to search the code for occurrences of the same literal values.
Refactor! makes this task easy through Introduce Constant refactoring, as you will see in the next section.
As with any other refactoring, you should use good judgment when you apply Replace Magic Literal withConstant refactoring. In some cases it just doesn’t make sense. Look at the code example in the “Refactoring:Replace Magic Literal with Constant” sidebar. In that example I have extracted pi, but have left the number 2inside the code. Both methods are implementations of mathematical formulae, but here the number 2 is justthat, a number inside a formula, and replacing it with a constant wouldn’t make sense.
Continued
Refactoring: Replace Magic Literal with ConstantMotivationMagic literals make your code more difficult to understand. The purpose of literal values has to be understood from the context. Because the use of magic literals leads to duplication, it makes the code more prone to bugs and more difficult to maintain.Instead of replacing a value in a single place, the programmer has to hunt for andreplace each occurrence of the magic literal in the code. Declaring literals as constantscan reduce typographical errors, because the compiler can check a constant or an enu-meration but cannot check a literal value. Finally, declaring literals as constants canlead to an increase in productivity and code less prone to bugs, because autocompletecan be used to search for the value instead of your having to memorize it.
Related SmellsUse this refactoring to eliminate the Magic Literals smell.
RationaleLiteral values disseminated liberally in your code can easily lead to duplication. Thepurpose of such values is not immediately obvious. That can lead to code that is diffi-cult to maintain and increase the likelihood of bugs.
237
Chapter 9: The Method Extraction Remedy for Duplicated Code
79796c09.qxd:WroxPro 2/23/08 8:23 AM Page 237
MechanicsAfter you have identified the magic literal, declare a constant of the same type as amagic literal, give it a meaningful name, and initialize it with the same value that themagic literal has. Now replace the literal with the constant. Search in your code forother occurrences of the same magic literal, and if they are used for the same purpose,replace them with the newly created constant as well.
If you come upon magic literal values that are logically related and can be grouped, anenumeration is a better choice for replacement than a constant. Use the Enum keywordto declare the enumeration and to enumerate these values. Now you can replace magicliterals with values from the enumeration.
BeforePublic Class Circle
Private radiusValue As Double
Public Property Radius() As DoubleGet
Return radiusValueEnd GetSet(ByVal value As Double)
radiusValue = valueEnd Set
End Property
Public Function CircumferenceLength() As DoubleReturn 2 * 3.1415 * Radius
End Function
Public Function Area() As DoubleReturn 3.1415 * Radius ^ 2
End Function
End Class After
Public Class CirclePrivate Const PI As Double = 3.1415
Private radiusValue As Double
Public Property Radius() As DoubleGet
Return radiusValueEnd GetSet(ByVal value As Double)
radiusValue = valueEnd Set
238
Part III: Getting Started with Standard Refactoring Transformations
79796c09.qxd:WroxPro 2/23/08 8:23 AM Page 238
Introduce Constant Refactoring in Refactor!You can use Introduce Constant refactoring for both numerical and string literal values. In order toinvoke this refactoring, you should place the cursor over the literal values you wish to replace with theconstant. After that, Refactor! will mark all the instances of the same literal in the file you are workingon, as you can see in Figure 9-4.
Figure 9-4
However, Refactor! will not go on to replace all of them with the constant after you click the IntroduceConstant option. Only the originally selected literal is replaced; then the progress indicator is displayed,letting you select which literal values you want to replace with the constant. This is only logical, becausethe same literal values can have very different purposes, and there is no way that Refactor! can guesswhich literals should be replaced and which should not. For example, the value 3.1415 can represent thenumber pi, but it can also represent an “agent commission.” Replacing instances of the literal 3.1415 thatrepresent “agent commission” with the constant pi would clearly be an error.
Now it’s time to see how all that you have seen in this chapter so far can be applied to the Rent-a-Wheelsapplication.
End Property
Public Function CircumferenceLength() As DoubleReturn 2 * PI * Radius
End Function
Public Function Area() As DoubleReturn PI * Radius ^ 2
End Function
End Class
239
Chapter 9: The Method Extraction Remedy for Duplicated Code
79796c09.qxd:WroxPro 2/23/08 8:23 AM Page 239
Extract Method and Replace Magic Literal Refactoring in the Rent-a-Wheels Application
After this chapter you will certainly look at the existing Rent-a-Wheels code with different eyes. Even uponsuperficial examination, you can see that the Rent-a-Wheels code suffers from the Event-Handling Blindnesssmell. In short, 100 percent of the code is placed in event-handling routines in different form classes.
Now examine the code further, in search of more unpleasant aromas. Here are the problems I am able toidentify so far:
❑ Event-handling blindness: As I have already mentioned, this special case of Long Methodsmell, so popular with Visual Basic programmers, is the most obvious smell in the Rent-a-Wheels code.
❑ Magic literals: A number of magic literals are present in the code: most notably, the database con-nection string is duplicated inside every routine that needs to make use of the persistence services.
❑ Duplicated code: Many methods contain very similar, sometimes even identical, code. Literalvalues are also duplicated in the code.
❑ Copy-paste programming: There is no doubt that the majority of the code in the Rent-a-Wheelsapplication was written using copy-paste style, resulting in numerous duplications and generalcode proliferation. For those with detective instincts, it will suffice to make a comparison betweenthe comments in some methods. Many were left in their original forms after being pasted, withoutbeing updated to a new context. This is a clear testimony to the copy-paste process.
❑ Comments: Because you performed a Rename refactoring in the previous chapter, the majority ofcomments have already become somewhat obsolete. However, they are still used to explain thepurpose of certain blocks of code inside the method, which is a positive signal that the methodsare too long and that method extraction should be performed.
I know that these smells say a lot about the great deficiencies of the existing code, but I will ask you to bepatient for the moment and not perform any refactorings right now. In the next chapter you are going tolearn a number of refactorings that can greatly simplify the work on method extraction. So leave Rent-a-Wheels as is for one more chapter and know that you’ll come back to it once you have a complete arsenalof refactorings for method extraction at your disposal.
SummaryThe chapter you have just read describes refactorings that can result in important and extensive “small-scale” changes to your code. Fortunately, these are the kind of changes that bring a fast return. The codewill immediately become easier to maintain and understand.
You have also seen some underlying object-oriented principles that can motivate these refactorings. Itouched upon encapsulation, and you have seen the ways it can help you deal with complexity in your code. I started with the simplest kind of encapsulation: a function. Functions are very similar to methods,and all refactorings that can be applied to a function work with methods as well. You have seen how Extract
240
Part III: Getting Started with Standard Refactoring Transformations
79796c09.qxd:WroxPro 2/23/08 8:23 AM Page 240
Method refactoring can greatly simplify methods by extracting blocks of code into new, separate methods. I have explained how method extraction can be employed to make methods more granular and reusable, toeliminate duplication, to replace comments, and to make code easier to read.
In this chapter I also dealt with duplication, the most destructive force in the development process.Duplicated code will lead you on the road to unmanageable, bloated code that can be a real nightmarefor any programmer who has to maintain it. Unfortunately, duplication is sometimes used deliberately,as a mechanism of reuse, a double-edged sword if ever there was one. Generally, programmers who usecopy-paste style are unaware of the perils that duplication brings even in the short term, or simply tradecode correctness for some very immediate gains in project schedule.
Magic literals are the last issue I talked about in this chapter. While magic literals are not very complex,the negative impact that they can have on code is very important.
In the next chapter you are going to see a few method-level refactorings that can greatly simplify methoddecomposition and are generally used to prepare the ground for method extraction. With that, you will beready to deal with even the most complex methods, and you will be able to take the first and most impor-tant step in bringing your code out of chaos and into the structured world.
241
Chapter 9: The Method Extraction Remedy for Duplicated Code
79796c09.qxd:WroxPro 2/23/08 8:23 AM Page 241
79796c09.qxd:WroxPro 2/23/08 8:23 AM Page 242
Method Consolidation andExtraction Techniques
In the previous chapter, you saw the benefits that can come from keeping the code granular andwell encapsulated on the most basic level, the method level. You also saw the importance of organ-izing the code in the form of small methods with well-chosen names.
When you come across long or poorly structured methods, the most common solution is to performmethod extraction. You have also seen the mechanics behind method extraction, different illustrationsof this refactoring, and how the whole process can be automated with the refactoring tool.
However, in the real world method extraction is seldom straightforward. If the method meritsextraction, that means it was not well written in the first place, so you cannot expect that it willlend itself to extraction easily. Very often you will have to perform a number of preparatory stepsto make method extraction meaningful. These steps may involve the following:
❑ Dealing with temporary variables that stand in the way of method extraction
❑ Regrouping statements so that blocks of code can be extracted in one step
❑ Dealing with temporary variable declaration and initialization statements
In this chapter you will see a number of refactorings that can be used as preliminary steps tomethod extraction. If you are able to apply these refactorings efficiently, you will be able to per-form method extraction on even the most complicated and tangled methods.
Dealing with Temporary VariablesTemporary variables are among the biggest impediments to efficient method extraction. Because theonly place you can reach the local variable is within the method that hosts it, the more temporaryvariables you have in the method, the longer the method has to be.
79796c10.qxd:WroxPro 2/23/08 8:25 AM Page 243
While you can perform method extraction by passing local variables as parameters, creating numerousmethods with long parameter lists will do much to improve code clarity. Such code goes against theidea of encapsulation, and you need some more efficient means at your disposal when performingmethod extraction.
In this section you are going to see how you can deal with temporary variables.
❑ The simplest case is that in which the variable reach is only inside the block you are going toextract. In that case the variable becomes local to the newly created method and does not standin the way of method extraction.
❑ In other cases you have to think about ways to avoid passing the variable as a parameter to a method.
❑ In some cases you can inline the variable.
❑ In other cases you can replace it with a query and make the result available to more than one method.
❑ Sometimes even temporary variables can be abused. This is the case when the temporary vari-able holds two or more unrelated values during its lifetime. Such a variable makes the codemuch more difficult to read, because its role depends on context. It also works against methodextraction, because it has a longer reach than necessary. The solution is to split this variable intotwo or more.
The next sections examine these valuable temporary-variable-management techniques in detail.
Move Declaration Near Reference RefactoringVB programmers are used to placing temporary variable declarations at the top of the method. Thisshould improve the readability of the method and make it tidier. However, I am not so sure about thisold piece of wisdom. Let me explain why.
First of all, as you saw in Chapter 7, decision and control structures in VB .NET are scoped. This meansthat by placing all variables you use in a method at the beginning of the method, you are declaring themwith greater visibility than necessary, and I have already discussed the benefits of well-encapsulated code.But such placement has another consequence as well. If the variable is initialized inside the decision orcontrol block and declared outside, you will not be able to move initialization to declaration. (I talk aboutmoving initialization to variable declaration in the next section.)
Second of all, placing variables at the beginning of the method instead of placing them in the part of the method where they are used makes Extract Method refactoring much more difficult to perform. As you saw in the previous chapter, when you extract methods, either manually or using a refactoring tool, you select a contiguous block of code and move it to the extracted method. If you are not able toreach the variable, you will have to declare it as a parameter of the newly extracted method. Had youdeclared the variable where it was used, you would be able to select the variable declaration as well,thus removing the necessity for parameter passing.
Definition: A temporary or local variable is a variable declared inside a method orproperty and visible only inside the containing method or property.
244
Part III: Getting Started with Standard Refactoring Transformations
79796c10.qxd:WroxPro 2/23/08 8:25 AM Page 244
I generally declare variables as I need them. I find such code easier to read this way. Because I do notneed to search for code that assigns to a variable, I can understand immediately what value the variableholds. Whenever I see that a method has grown a bit out of proportion, I try to consolidate it by refactor-ing. Finally, once you have finely-grained methods, you will probably end up declaring variables at thebeginning of the method anyway. Finely-grained methods are generally so short they don’t leave youwith any other option.
Continued
Refactoring: Move Declaration Near ReferenceMotivationBy placing the variable declaration far away from the line where it is first referenced,you are making code more difficult to read, because it is not immediately clear how thevariable was initialized.
In addition, declaring variables well ahead of time makes the method more difficult torefactor. If you try to extract a part of the method in which the variable was referenced,you will have to pass the variable as a parameter to a newly extracted method, eventhough the declaration could possibly have been placed inside the extracted block.
Related SmellsUse this refactoring to prepare methods for method extraction and to eliminate theLong Method and Overexposure smells.
MechanicsFind all variables in the method that are declared but not initialized at the same line.After that, look for the line where the variable is first referenced. Possibly the methodis long, and the variable can be enclosed in a more restricted scope, or the line wherethe variable is referenced for the first time belongs to a block that does something dif-ferent from the rest of the method and can be extracted. Cut the line with the variabledeclaration and paste it one line above the one in which the variable was referenced forthe first time.
BeforeSub Main()
‘Variable radius declared at the beginning of the methodDim radius As DoubleDim lengthOfCircumference As DoubleDim center As PointDim pointOnCircumference As Point
Console.WriteLine(“Enter X coordinate” + _“of circle center”)center.X = CDbl(Console.In.ReadLine())Console.WriteLine(“Enter Y coordinate “ + _“of circle center”)center.Y = CDbl(Console.In.ReadLine())Console.WriteLine(“Enter X coordinate “ + _“of some point on circumference”)
245
Chapter 10: Method Consolidation and Extraction Techniques
79796c10.qxd:WroxPro 2/23/08 8:25 AM Page 245
pointOnCircumference.X = CDbl(Console.In.ReadLine())Console.WriteLine(“Enter Y coordinate “ + _“of some point on circumference”)pointOnCircumference.Y = CDbl(Console.In.ReadLine())Console.WriteLine(“The length of circle “ + _“circumference is:”)
‘Next section is to be extracted when simplifying methodradius = ((pointOnCircumference.X - center.X) ^ 2 + _(pointOnCircumference.Y - center.Y) ^ 2) ^ (1 / 2)lengthOfCircumference = 2 * 3.1415 * radius
Console.WriteLine(lengthOfCircumference)Console.Read()
End Sub
AfterSub Main()
Dim lengthOfCircumference As DoubleDim center As PointDim pointOnCircumference As Point
Console.WriteLine(“Enter X coordinate” + _“of circle center”)center.X = CDbl(Console.In.ReadLine())Console.WriteLine(“Enter Y coordinate “ + _“of circle center”)center.Y = CDbl(Console.In.ReadLine())Console.WriteLine(“Enter X coordinate “ + _“of some point on circumference”)pointOnCircumference.X = CDbl(Console.In.ReadLine())Console.WriteLine(“Enter Y coordinate “ + _“of some point on circumference”)pointOnCircumference.Y = CDbl(Console.In.ReadLine())Console.WriteLine(“The length of circle “ + _“circumference is:”)
‘Now radius variable declaration can be extracted ‘ together with rest of circumference calculation codeDim radius As Doubleradius = ((pointOnCircumference.X - center.X) ^ 2 + _(pointOnCircumference.Y - center.Y) ^ 2) ^ (1 / 2)lengthOfCircumference = 2 * 3.1415 * radius
Console.WriteLine(lengthOfCircumference)Console.Read()
End Sub
246
Part III: Getting Started with Standard Refactoring Transformations
79796c10.qxd:WroxPro 2/23/08 8:25 AM Page 246
Move Declaration Near Reference in Refactor!In order to activate this refactoring, select or place the cursor over the variable whose declaration you wantto see moved near the reference. Once you do that, Refactor! will display an arrow helping you visualizethe destination of the variable declaration once the refactoring is performed. You can see Refactor!’s visualaids in Figure 10-1.
Figure 10-1
Move Declaration Near Reference refactoring leads directly to the next refactoring. In the cases in whichyou have the variable declaration outside and the variable initialization code inside the nested block, to beable to move the initialization to the declaration you have to perform Move Declaration Near Referencerefactoring beforehand. Take a look at the following code:
Public Function CalculateTaxReturn() As DoubleDim taxReturn As Double = 0‘variable “tax” can be declared inside If blockDim tax As DoubleIf Not Me.TaxWaivered Then
tax = 19 * Me.YearlyTotalEnd IftaxReturn = ApplyScale(tax)Return taxReturn
End Function
You are not able to invoke Move Initialization to Declaration refactoring on the variable tax unless youperform a Move Declaration Near Reference refactoring first. The next section takes a more detailed lookat Move Initialization to Declaration refactoring.
247
Chapter 10: Method Consolidation and Extraction Techniques
79796c10.qxd:WroxPro 2/23/08 8:25 AM Page 247
Move Initialization to Declaration RefactoringIn VB .NET you can initialize the variable on the same line on which you have declared it; this was notpossible in pre-.NET versions of VB. By placing initialization code as a continuation of the variable dec-laration, you are making the code more compact and easier to read. You do not have to scan the code inthe search of the line that initialized the variable.
More importantly, this refactoring will help you prepare the variable for Replace Temp with Query refac-toring. Replace Temp with Query refactoring can be invoked in Refactor! only if the variable has beeninitialized in the declaration statement.
Sometimes variable initialization might depend on certain conditions. In that case the variable cannot beinitialized on the same line on which it is declared. For example, take a look at variable connection inthe following code extract:
Dim connection As IDbConnectionIf Me.DatabaseProviderImplementation = _ProviderImplementation.MSSql Then
connection = New SqlConnectionElseIf Me.DatabaseProviderImplementation _= ProviderImplementation.Oracle Then
connection = New OracleConnection‘...
In this case it is not possible to initialize the variable connection on the same line on which it wasdeclared. However, if the code for some reason is changed in such a way that conditional initialization is not necessary anymore and the conditions have been eliminated, you can use Move Initialization to Declaration refactoring to consolidate your code. For example, if the code you just saw is changed tolook as follows, you can apply Move Initialization to Declaration:
Dim connection As IDbConnectionconnection = CreateConnection() ‘...
The next section shows how this refactoring is accomplished with Refactor! on Visual Basic code insideVisual Studio.
Refactoring: Move Initialization to DeclarationMotivationBy initializing the variable on the same line on which the variable is declared, you willimprove code readability.
Related SmellsUse this refactoring to prepare methods for method extraction and to eliminate theLong Method and Overexposure smells.
MechanicsFind all variables in the method that are declared but not initialized at the same line. Ifnecessary, perform Move Declaration Near Reference refactoring before consolidatinginitialization.
248
Part III: Getting Started with Standard Refactoring Transformations
79796c10.qxd:WroxPro 2/23/08 8:25 AM Page 248
Move Initialization to Declaration in Refactor!You can easily invoke this refactoring in Refactor! by selecting or placing the cursor over the variable inthe variable declaration or variable-initialization statement. You can see Refactor! ready to move initial-ization to declaration in Figure 10-2.
Figure 10-2
Split Temporary Variable RefactoringThe longer the temporary-variable reach in a method, the more intertwined the method is and the morecomplex any method extraction becomes. If the same variable is used inside the method for more thanone purpose, it will have a longer reach than necessary. Further, if you use a temporary variable to holdmore than one unrelated value, the code becomes less comprehensible for the reader.
Continued
Smell: Overburdened Temporary VariableDetecting the SmellA variable that holds more than one unrelated value is generally considered overbur-dened. It is a variable that has been assigned more than once, and it is neither a loopingnor a collecting variable. Sometimes these variables are given generic names likeresult or temp.
The mere existence of overburdened variables can be a good indicator that the methodcould benefit from method extraction.
Related RefactoringUse Split Temporary Variable refactoring to eliminate this smell.
BeforeDim connection As IDbConnectionconnection = CreateConnection() ‘...
AfterDim connection As IDbConnection = CreateConnection() ‘...
249
Chapter 10: Method Consolidation and Extraction Techniques
79796c10.qxd:WroxPro 2/23/08 8:25 AM Page 249
Those I’ve mentioned are already very good reasons why a temporary variable in a method should beused for a single purpose, but why are temporary variables used for more than one purpose to beginwith? Programmers sometimes use a single variable for more than one purpose in order to save memoryor to type less. Neither reason really holds water because any gains are surpassed by negative effects.Sometimes convoluted temporary usage is not really intentional. Whatever the origin of the multipurposetemporary variable, the variable should be split, and for each usage a new temporary variable should bedeclared. You can see this technique in the following example:
Public Sub PrintReport()Dim result As DoubleFor Each vehicle As Vehicle In Me.Vehicles
If vehicle.Rented Thenresult += 1
End IfNextPrintJob.PrintLine(“Total vehicles rented:” + result)result = 0For Each rental As Rental In Me.Rentals
result += rental.AmountDueNextPrintJob.PrintLine(“Total amount due:” + result)
End Sub
In this example the variable result first holds a value that represents the number of currently rentedvehicles; later on the same variable holds the total amount of pending payments.
Sometimes a good sign of an overburdened variable is the name chosen for it. If it is some genericterm like result, temp, or var, this may be because the variable is used for more than one purpose,so a more specific name would not fit well. After the variable has been split, newly created and origi-nal variables can be created, with names more in accordance with the single role each variable nowperforms.
Retaining Looping and Collecting VariablesHowever, if a variable has been assigned more than once, this is not automatically a sign that the vari-able should be split. Sometimes, the variable has to be assigned more than once, and that is fine. Typicalcases of such variables are looping variables, for example, an index when iterating an array or a collection.This variable has to be changed with each pass through the cycle. In VB .NET the For Each constructminimizes the necessity for declaring looping variables.
RationaleOverburdened variables are serving more than one role in a method. In a sense theyhave a longer “reach,” making method extraction more difficult and the method moreintertwined. Since the role of the variable is not clear, it is not possible to establish thelink between the name and the purpose of the variable, making the code more complexand more difficult to understand.
250
Part III: Getting Started with Standard Refactoring Transformations
79796c10.qxd:WroxPro 2/23/08 8:25 AM Page 250
Another case when assigning a variable more than once occurs is when you need to collect certain values,as when you are concatenating strings or calculating some totals. Take a look at the following code:
Private Function CalculateFundsTotal() As DecimalDim total As DecimalFor Each account As Account In Me.accounts
total += account.BalanceNextReturn total
End Function
In this case, the variable total is a collecting variable, and incrementing it by each cycle helps you calculatethe total balance for all accounts in the collection. In this case, splitting the variable does not make sense.
Continued
Refactoring: Split Temporary VariableMotivationEach temporary variable in a method should have a single role. Otherwise its role canbe understood only from the context, making the code less explicit and more difficultto read.
Such variables have a longer reach, and they make a method extraction much more dif-ficult to perform. By splitting the variable so the variable is really assigned only once(unless it is a collecting or looping variable), you make the purpose of the variablesmuch more explicit and greatly simplify method extraction.
Related SmellsUse this refactoring to eliminate the Overburdened Temporary Variable smell.
MechanicsFind all variables that are assigned more than once. Make sure these are neither collect-ing nor looping variables.
1. Start by renaming the variable at the declaration, until the second assign-ment, with a name that explains well its first usage. Rely on the compilerto discover all occurrences of the variable.
Public Sub PrintReport()Dim vehiclesRented As DoubleFor Each vehicle As Vehicle In Me.Vehicles
If vehicle.Rented ThenvehiclesRented += 1
End IfNextPrintJob.PrintLine(“Total vehicles rented:” + result)result = 0For Each rental As Rental In Me.Rentals
result += rental.AmountDueNextPrintJob.PrintLine(“Total amount due:” + result)
End Sub
251
Chapter 10: Method Consolidation and Extraction Techniques
79796c10.qxd:WroxPro 2/23/08 8:25 AM Page 251
2. Change the second assignment to a declaration. Check that the code com-piles correctly.
Public Sub PrintReport()Dim vehiclesRented As DoubleFor Each vehicle As Vehicle In Me.Vehicles
If vehicle.Rented ThenvehiclesRented += 1
End IfNextPrintJob.PrintLine(“Total vehicles rented:” + result)Dim result As Double = 0For Each rental As Rental In Me.Rentals
result += rental.AmountDueNextPrintJob.PrintLine(“Total amount due:” + result)
End Sub
3. Perform Rename refactoring on the newly declared variable so that thename of the second occurrence of the original variable corresponds to itspurpose.
4. Because the variable can be assigned more than twice, repeat this processuntil each usage of the variable is split into its own separately declaredvariable.
BeforePublic Sub PrintReport()
Dim result As DoubleFor Each vehicle As Vehicle In Me.Vehicles
If vehicle.Rented Thenresult += 1
End IfNextPrintJob.PrintLine(“Total vehicles rented:” + result)result = 0For Each rental As Rental In Me.Rentals
result += rental.AmountDueNextPrintJob.PrintLine(“Total amount due:” + result)
End Sub
AfterPublic Sub PrintReport()
‘original variable has been renamedDim vehiclesRented As DoubleFor Each vehicle As Vehicle In Me.Vehicles
If vehicle.Rented ThenvehiclesRented += 1
End If
252
Part III: Getting Started with Standard Refactoring Transformations
79796c10.qxd:WroxPro 2/23/08 8:25 AM Page 252
Now I want to look at how Refactor! can be used to split local variables. Refactor! simplifies the wholeprocess and makes it possible to perform this refactoring in a single step.
Split Temporary Variable in Refactor!In order to make this refactoring available in the Refactor! menu, you have to select or place the cursorover the name of the variable in a secondary assignment statement. You can see a variable selected andSplit Temporary Variable refactoring marked in the context menu in Figure 10-3.
Figure 10-3
After the refactoring is performed, the newly created variable name stays selected in the editor throughlinked identifiers, making the job of giving a name to the newly created variable immediately available.That completes your look at Split Temporary Variable refactoring; now you’ll see another refactoringthat can help you remove temporary variables altogether.
Inline Temp RefactoringOne way to deal with temporary variables is to eliminate them altogether. If the variable has beenassigned only once with a simple expression, you can eliminate the temporary variable and use theexpression instead.
NextPrintJob.PrintLine(“Total vehicles rented:” +
vehiclesRented)‘after splitting, new variable has been declaredDim amountDueTotal As Double = 0For Each rental As Rental In Me.Rentals
amountDueTotal += rental.AmountDueNextPrintJob.PrintLine(“Total amount due:” + amountDueTotal)
End Sub
253
Chapter 10: Method Consolidation and Extraction Techniques
79796c10.qxd:WroxPro 2/23/08 8:25 AM Page 253
As you might guess, the first motivation for this refactoring comes in the context of Extract Methodrefactoring. It can help you eliminate annoying temporary variables and facilitate method extraction.Another time to inline temporary variables is when they are plainly unnecessary, and the expression can be read as easily as the variable, as in the following example:
Public Function NumberOfVacantVehicles() As IntegerDim numberOfVehicles As IntegernumberOfVehicles = Me.NumberOfVehicles‘...
End Function
In this code the temporary variable numberOfVehicles is not adding any clarity to the code, because the property name Me.NumberOfVehicles reads just as well. You can also come upon a similar situationwhen a temporary variable is used to represent a return value of the method, but the method name is suffi-cient to identify its purpose. Such a method might benefit from temp inlining for the sake of simplicity.
Refactoring: Inline TempMotivationYou can reduce the number of temporary variables in the method by performing InlineTemp refactoring. This is especially useful if the local variable is standing in the way ofmethod extraction. Another good time to inline variables is when they add nothing tocode clarity, because the expression that initializes the variable is just as easily read.
Smell: Superfluous Temporary VariableDetecting the SmellA superfluous temporary variable is generally detected only on close inspection of the method. It is the variable that is assigned a value once by an expression and is usedonce, without any operation on the received value. If you can instead directly use theexpression that gives value to the variable, and the variable is contributing nothing tocode clarity, then you have identified a superfluous variable.
Related RefactoringUse Inline Temp refactoring to eliminate this smell.
RationaleWhen a temporary variable adds nothing to code clarity, it is best to eliminate it. Theway a variable can add to code clarity is through its name. If the expression that givesvalues to a variable is equally descriptive and the variable is not used for any addi-tional operation, then it has no use. Each superfluous symbol in the code means moreunnecessary complexity. Not only that, but a superfluous temporary variable will oftenstand in the way of Extract Method refactoring, so your best choice is to eliminate thevariable altogether.
254
Part III: Getting Started with Standard Refactoring Transformations
79796c10.qxd:WroxPro 2/23/08 8:25 AM Page 254
Sometimes a temporary variable is assigned more than once, and the initialization expression lends itselfto inlining. In this case, perform Split Temporary Variable before Inline Temp. Finally, if the variable hasbeen initialized by the result of multiple statements, you can perform Extract Method refactoring on thesestatements so that the variable is initialized by a simple expression and Inline Temp refactoring is thenmade available.
Inline Temp in Refactor!Refactor! will make Inline Temp refactoring available only if the variable has been assigned once and therefactoring can be safely performed. Just select or place the cursor over the variable you want to extract,and you will be able to invoke the refactoring from Refactor!’s context menu. You can see Refactor! readyto perform Inline Temp refactoring in Figure 10-4.
Figure 10-4
Related SmellsUse this refactoring to eliminate the Superfluous Temporary Variable smell (a temporaryvariable that adds nothing to code clarity) and as a step that precedes and facilitatesmethod extraction. (Take a look at Extract Method refactoring in Chapter 9 to see thesmells that can be remedied by Extract Method.)
MechanicsIf the variable has been assigned more than once, perform Split Temporary Variablerefactoring before embarking on Inline Temp refactoring.
BeforePublic Function GetVacancyPercentage() As Double
Dim vacancyPercentage As DoublevacancyPercentage = 100 * _(Me.NumberOfVehicles - Me.NumberOfRented) /
Me.NumberOfVehiclesReturn vacancyPercentage
End Function
AfterPublic Function GetVacancyPercentage() As Double
Return 100 * _(Me.NumberOfVehicles - Me.NumberOfRented) /
Me.NumberOfVehiclesEnd Function
255
Chapter 10: Method Consolidation and Extraction Techniques
79796c10.qxd:WroxPro 2/23/08 8:25 AM Page 255
Replace Temp with Query RefactoringSometimes the expression that initializes the temporary variable is not very simple, nor its meaning veryobvious, so inlining the variable would make the code in the end less legible. However, the temporaryvariable stands in the way of method extraction, so you need to find a way to deal with it. One thing thatinstance methods or properties can access easily is another instance method or property, and this is agood hint toward the solution to the problem: you can replace a temporary variable with a read-onlyproperty or a method.
The case for this refactoring is even stronger if the same temporary-variable-initialization code is presentin more than one method. If you replace the temporary variable with a read-only property or a method inmore than one encompassing method, not only will you make methods much more prone to extraction,but you will also reduce duplication in your code. This is a typical technique for a situation in which youwould like to have a temporary variable available to more than one property or a method.
Is It a Method or a Property?If a method returns a single value and has no output parameters, it can easily be changed to a read-onlyproperty and vice-versa. While the difference is mostly syntactical, it is nevertheless important to choosethe right form for the extracted block. So, when replacing the temporary variable with a query, shouldyou choose a property or a method?
In the most cases, the solution comes intuitively. Properties are used to represent data, for example toencapsulate access to fields of an object, while methods are used for sending messages between objects.For more borderline cases, take a look at Table 10-1 for guidance.
Table 10-1: Query As Property or Method
In some cases, though a variable might be assigned more than once, you would still like to replace itwith a query. Use Split Temporary Variable refactoring before Replace Temp with Query refactoring toeliminate any secondary assignment, and perform Replace Temp with Query refactoring afterwards.
If temporary variable-initialization code comprises multiple statements, you can use Extract Methodrefactoring to consolidate the initialization code and perform initialization in a single statement. Afterthat, if you move initialization to declaration, Replace Temp with Query refactoring will become avail-able through Refactor!’s menu. More about Refactor! and Replace Temp with Query refactoring is dis-cussed in the next section.
Use Property Use Method
Value returned is coherent data value Value is the result of conversion, for exampleToInt(...)
No observable side effects to calling the property Possible observable side effects
Successive calls will always return the same result
Successive calls do not necessarily return thesame result
Order of execution does not influence the result Order of execution may influence the result
Call is not computationally costly Call is costly
256
Part III: Getting Started with Standard Refactoring Transformations
79796c10.qxd:WroxPro 2/23/08 8:25 AM Page 256
Continued
Refactoring: Replace Temp with QueryMotivationSometimes you can see essentially the same value calculated and assigned to a temporaryvariable in more than one method. In order to reduce the duplication, you can extract thecode into a separate query method or a property. You can follow the same steps beforeperforming method extraction to avoid creating unnecessary parameters.
Related SmellsUse this refactoring to eliminate the Duplicated Code smell and to prepare a methodfor extraction.
MechanicsIf the variable has been assigned more than once, split it before replacing it with a query.
1. Perform Extract Method on the expression that assigns to the variable. Forthe newly created method, make the return type the same as the variable’sand make the method return the value of the expression.
2. Eliminate the variable by replacing it with a method call.
BeforePublic Sub PrintReport()
‘...Dim vacantVehicles As IntegervacantVehicles = Me.NumberOfVehicles - Me.NumberOfRentedPrintJob.PrintLine(“Total vehicles vacant:” + _CStr(vacantVehicles))
‘...End Sub
Public Function GetVacancyPercentage() As DoubleReturn 100 * _(Me.NumberOfVehicles - Me.NumberOfRented) /
Me.NumberOfVehiclesEnd Function
AfterPublic Sub PrintReport()
‘...PrintJob.PrintLine(“Total vehicles vacant:” + _CStr(Me.NumberOfVacantVehicles))‘...
End Sub
Public Function GetVacancyPercentage() As DoubleDim vacancyPercentage As DoublevacancyPercentage = 100 * _
257
Chapter 10: Method Consolidation and Extraction Techniques
79796c10.qxd:WroxPro 2/23/08 8:25 AM Page 257
Replace Temp with Query in Refactor!Replace Temp with Query will be available in Refactor! only if the variable is initialized in the declara-tion statement. So to make this refactoring available you might need to perform Move Initialization toDeclaration refactoring beforehand. You can see Replace Temp with Query refactoring selected in theRefactor! menu in Figure 10-5.
Figure 10-5
Method Reorganization HeuristicsIn this chapter you have so far seen a number of refactorings that can help you consolidate your meth-ods and make them prone to extraction. Method extraction is the most common remedy for long andamorphous methods. It is also the most common reason to apply the refactorings in this chapter. Whilethe refactorings I dealt with in this chapter have merits of their own, they are especially useful in thecontext of method extraction. As you become more experienced, you will be able to apply these refac-torings more naturally, and you will learn to recognize patterns that lead to extraction. However, at thispoint you might ask yourself how and where to start the whole process. Here are some hints that mighthelp you confront some more complex methods as you start out.
❑ Search for duplication: If you can get rid of duplication by extracting duplicated code as amethod, do it right away. You will rarely have to go back if you use this step to reduce duplicatecode. Doing this will make an original method a bit shorter.
❑ Analyze comments: Very often, comments are a good hint that certain sections of code belongtogether. However, the original author fell a step short of placing the blocks of code in separate
Me.NumberOfVacantVehicles / Me.NumberOfVehiclesReturn vacancyPercentage
End Function
Private ReadOnly Property NumberOfVacantVehicles() As IntegerGet
Return Me.NumberOfVehicles -Me.NumberOfRentedVehicles
End GetEnd Property
258
Part III: Getting Started with Standard Refactoring Transformations
79796c10.qxd:WroxPro 2/23/08 8:25 AM Page 258
methods. You can go one step further and use methods to mark the blocks instead of comments.Again, this step will make the original method a bit shorter.
❑ Search for sections of the method that stick well together and have a greater chance of reuse:When you are confronted with long methods, you will often find sections that can be extractedas separate methods. Make the original method thinner by extracting these sections into sepa-rate methods.
As you go through these steps, you will often have to shuffle the code in a method in order to deal withtemporary variables. Move initialization near the reference, split temporary variables, inline them, orreplace them with queries as the need arises. With practice you will soon become more confident andable to apply the techniques you have seen in this chapter. As you go along you will see how your codebecomes more expressive and leaner, and contains less duplication. When you finish with one cycle, goback for another iteration, and another, until you are finally satisfied.
These are more or less the heuristics I tried to apply to the Rent-a-Wheels application. In the next sectionI discuss the latest refactorings applied to this application.
Method Reorganization and Rent-a-WheelsI ended Chapter 9 with an analysis of the problems present in the Rent-a-Wheels application that can be identified based on the smells I had explained so far. I listed and explained the smells I identified inthe application. If you remember, the smells I identified in the Rent-a-Wheels code were:
❑ Event-handling blindness
❑ Magic literals
❑ Duplicated code
❑ Copy-paste programming
❑ Comments
Let me now illustrate these smells with examples in the code. I have found a single method from theRent-a-Wheels application (Listing 10-1) that neatly sums up all the smells I just mentioned. I have usedcomments in bold to mark these smells in the listing.
Listing 10-1: Button Save Click Event-Handling Code in Branch Maintenance Form
‘Event-handling blindness – event handler as a sole functionPrivate Sub Save_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Save.Click‘Declare variablesDim connection As IDbConnectionDim command As IDbCommand
‘Connection String as Magic Literal‘Create SqlConnectionconnection = New SqlConnection(“Data Source=teslateam;” + _“Initial Catalog=RENTAWHEELS;User ID=RENTAWHEELS_LOGIN;” + _
Continued
259
Chapter 10: Method Consolidation and Extraction Techniques
79796c10.qxd:WroxPro 2/23/08 8:25 AM Page 259
Listing 10-1: Button Save Click Event-Handling Code in Branch Maintenance Form (continued)
“Password=RENTAWHEELS_PASSWORD_123”)command = New SqlCommandDim insertBranchSql As StringIf (Me.Id.Text.Equals(“”)) Then
‘Create Sql String with parameter @SelectedLPinsertBranchSql = “Insert Into Branch (Name) “ + _“Values(@Name)“‘Adding command parameter in provider-neutral mannerDim name As IDbDataParameter = command.CreateParameter()name.ParameterName = “@Name”name.DbType = DbType.Stringname.Value = BranchName.Text.ToStringcommand.Parameters.Add(name)
Else‘Create Sql String with parameter @SelectedLPinsertBranchSql = “Update Branch Set Name = @Name “ + _“Where BranchId = @Id”‘Duplicated code – parameter adding statements are repeated‘Adding command parameter in provider-neutral mannerDim name As IDbDataParameter = command.CreateParameter()name.ParameterName = “@Name”name.DbType = DbType.Stringname.Value = BranchName.Text.ToStringcommand.Parameters.Add(name)‘Copy-Paste programming: look at duplicated comments below‘Adding command parameter in provider-neutral mannerDim id As IDbDataParameter = command.CreateParameter()id.ParameterName = “@Id”id.DbType = DbType.Int32id.Value = CInt(Me.Id.Text.ToString)command.Parameters.Add(id)
End If‘Redundant comments‘open connectionconnection.Open()‘Set connection to commandcommand.Connection = connection‘set Sql string to command objectcommand.CommandText = insertBranchSql‘execute commandcommand.ExecuteNonQuery()‘close connectionconnection.Close()BranchMaintenance_Load(Nothing, Nothing)
End Sub
Because the method selected in Listing 10-1 illustrates well a general pattern followed in the Rent-a-Wheelsimplementation, the rest of the code suffers from more or less similar problems. The first smell I attackedwas duplicated code.
260
Part III: Getting Started with Standard Refactoring Transformations
79796c10.qxd:WroxPro 2/23/08 8:25 AM Page 260
Removing Duplication in Rent-a-WheelsAfter my first visual inspection I was able to identify a number of event-handling routines containingcompletely identical blocks of code. The first of such blocks was control populating code found in all thenavigational buttons and in the Form_Load routine. Just in case you don’t remember what the BranchMaintenance form looks like, take a look at Figure 10-6.
Figure 10-6
Navigational buttons are small buttons at the bottom of the form that help you move between differentbranch records in the database. Event handlers for all of those buttons, including the Form_Load routine,have a few lines of identical code. In Listing 10-2 I have chosen two such routines for illustration purposes.
Listing 10-2: Navigational Button Event Handlers
Private Sub RightItem_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles RightItem.Click
If (branches.Rows.Count > currentRowIndex + 1) ThencurrentRowIndex += 1Dim row As DataRow = branches.Rows(currentRowIndex)Me.Id.Text = row.Item(“BranchId”).ToStringBranchName.Text = row.Item(“Name”).ToString
End IfEnd Sub
Private Sub LeftItem_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles LeftItem.Click
If (currentRowIndex - 1 >= 0 And branches.Rows.Count > 0) ThencurrentRowIndex -= 1Dim row As DataRow = branches.Rows(currentRowIndex)Me.Id.Text = row.Item(“BranchId”).ToStringBranchName.Text = row.Item(“Name”).ToString
End IfEnd Sub
I have marked the identical code in both routines in bold. I decided to extract repeated code in a separatemethod and call it DisplayCurrentRow; take a look at Listing 10-3.
Listing 10-3: The DisplayCurrentRow Extracted Method
Private Sub DisplayCurrentRow()Dim row As DataRow = branches.Rows(currentRowIndex)Me.Id.Text = row.Item(BranchTableIdColumnName).ToStringBranchName.Text = row.Item(BranchTableNameColumnName).ToString
End Sub261
Chapter 10: Method Consolidation and Extraction Techniques
79796c10.qxd:WroxPro 2/23/08 8:25 AM Page 261
You have already seen another episode of duplicated code in Listing 10-1. This code has to do with addinga parameter to a database command object. In this case extraction is not so straightforward. While the codeis very similar, there are differences in each section. In order to solve this problem I needed to parameterizethe extracted method. I called the method AddParameter, which receives four parameters. You can see it in Listing 10-4.
Listing 10-4: The AddParameter Extracted Method
Private Sub AddParameter(ByVal command As IDbCommand, _ByVal parameterName As String, ByVal parameterType As DbType, _ByVal paramaterValue As Object)
Dim parameter As IDbDataParameter = command.CreateParameter()parameter.ParameterName = parameterNameparameter.DbType = parameterTypeparameter.Value = paramaterValuecommand.Parameters.Add(parameter)
End Sub
Large portions of the code deal with ADO .Net objects. I extracted the part that creates the connection,sets the connection string to the connection, opens the connection, sets SQL code to the command, andadds a connection to the command to separate the PrepareDataObjects method. Take a look atListing 10-5.
Listing 10-5: The PrepareDataObjects Extracted Method
Private Function PrepareDataObjects(ByVal command As IDbCommand, _ByVal sql As String) As IDbConnection
Dim connection As IDbConnection = _New SqlConnection(ConnectionString)connection.Open()command.Connection = connectioncommand.CommandText = sqlReturn connection
End Function
The last interesting method I extracted is ExecuteNonQuery: its purpose is to execute a command. It isshown in Listing 10-6.
Listing 10-6: The ExecuteNonQuery Extracted Method
Public Sub ExecuteNonQuery(ByVal command As IDbCommand, _ByVal sql As String)
Dim connection As IDbConnection = _PrepareDataObjects(command, sql)command.ExecuteNonQuery()connection.Close()
End Sub
After method extraction, the code has become significantly easier to read. The code was now free of most of the duplication and could not be characterized as being of the copy-paste style anymore. However, somechanges to the code were still pending.
262
Part III: Getting Started with Standard Refactoring Transformations
79796c10.qxd:WroxPro 2/23/08 8:25 AM Page 262
Magic Literals, Comments, and Event-Handling Blindness in Rent-a-Wheels
One of the easiest things I’ve had to deal with were comments. After method extraction, most of those comments proved to be redundant, so they could simply be deleted.
Literal values were replaced with constants. For example, a literal string containing branch-deletion SQL code was replaced with a constant, DeleteBranchSql (see Listing 10-7).
Listing 10-7: Literal Value SQL String Replaced with a Constant
Private Const DeleteBranchSql As String = _“Delete Branch Where BranchId = @Id” ‘...Private Sub DeleteBranch()
Dim command As IDbCommand = New SqlCommandAddParameter(command, IdParameterName, DbType.Int32, _CInt(Me.Id.Text))ExecuteNonQuery(command, DeleteBranchSql)
End Sub
I also decided to separate code dealing with the GUI from code dealing with the database (Listing 10-8).As a result, event-handling blindness was done with.
Listing 10-8: Separation of the GUI Automation Code from the Database Code
Private Sub Delete_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles Delete.Click
DeleteBranch()BranchMaintenance_Load(Nothing, Nothing)
End Sub
Private Sub DeleteBranch()Dim command As IDbCommand = New SqlCommandAddParameter(command, IdParameterName, DbType.Int32, _CInt(Me.Id.Text))ExecuteNonQuery(command, DeleteBranchSql)
End Sub
As I performed these latest refactorings, I could see that the code was even visually much more pleasing.The longest method is now no more than ten lines long. It can easily be read and debugged. There is noduplication, and there are no unnecessary comments or scattered literals. You can judge the result foryourself. You can see the code for the complete BranchMaintenence form in Listing 10-9. The listingwill also help you understand how all the pieces I just discussed fit together.
Listing 10-9: BranchMaintenence Form Code
Option Explicit OnOption Strict On
Imports System.Data
Continued
263
Chapter 10: Method Consolidation and Extraction Techniques
79796c10.qxd:WroxPro 2/23/08 8:25 AM Page 263
Listing 10-9: BranchMaintenence Form Code (continued)
Imports System.Data.SqlClient
Public Class BranchMaintenancePublic Const ConnectionString As String = _“Data Source=teslateam;Initial Catalog=RENTAWHEELS;” + _“User ID=RENTAWHEELS_LOGIN;Password=RENTAWHEELS_PASSWORD_123”
Private Const BranchTableIdColumnName As String = “BranchId”Private Const BranchTableNameColumnName As String = “Name”
Private Const SelectAllFromBranchSql As String = _“Select * from Branch”Private Const DeleteBranchSql As String = _“Delete Branch Where BranchId = @Id”Private Const InsertBranchSql As String = _“Insert Into Branch (Name) Values(@Name)“Private Const UpdateBranchSql As String = _“Update Branch Set Name = @Name Where BranchId = @Id”
Private Const IdParameterName As String = “@Id”Private Const NameParameterName As String = “@Name”
Private branches As DataTablePrivate currentRowIndex As Integer
Private Sub BranchMaintenance_Load(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles MyBase.Load
LoadBranches()If (Me.branches.Rows.Count > 0) Then
currentRowIndex = 0DisplayCurrentRow()
End IfEnd Sub
Private Sub LoadBranches()Dim command As IDbCommand = New SqlCommandDim branches As DataSet = FillDataset(command, SelectAllFromBranchSql)Me.branches = branches.Tables.Item(0)
End Sub
Private Sub RightItem_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles RightItem.Click
If (branches.Rows.Count > currentRowIndex + 1) ThencurrentRowIndex += 1DisplayCurrentRow()
End IfEnd Sub
Private Sub LeftItem_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles LeftItem.Click
If (currentRowIndex - 1 >= 0 And branches.Rows.Count > 0) ThencurrentRowIndex -= 1
264
Part III: Getting Started with Standard Refactoring Transformations
79796c10.qxd:WroxPro 2/23/08 8:25 AM Page 264
Listing 10-9: BranchMaintenence Form Code (continued)
DisplayCurrentRow()End If
End Sub
Private Sub FirstItem_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles FirstItem.Click
If (branches.Rows.Count > 0) ThencurrentRowIndex = 0DisplayCurrentRow()
End IfEnd Sub
Private Sub LastItem_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles LastItem.Click
If (branches.Rows.Count > 0) ThencurrentRowIndex = branches.Rows.Count - 1DisplayCurrentRow()
End IfEnd Sub
Private Sub DisplayCurrentRow()Dim row As DataRow = branches.Rows(currentRowIndex)Me.Id.Text = row.Item(BranchTableIdColumnName).ToStringBranchName.Text = row.Item(BranchTableNameColumnName).ToString
End Sub
Private Sub NewItem_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles NewItem.Click
Me.Id.Text = “”BranchName.Text = “”
End Sub
Private Sub Save_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles Save.Click
SaveBranch()BranchMaintenance_Load(Nothing, Nothing)
End Sub
Private Sub SaveBranch()Dim command As IDbCommand = New SqlCommandIf (Me.Id.Text.Equals(“”)) Then
AddParameter(command, NameParameterName, DbType.String, _BranchName.Text.ToString)ExecuteNonQuery(command, InsertBranchSql)
ElseAddParameter(command, NameParameterName, DbType.String, _BranchName.Text.ToString)AddParameter(command, IdParameterName, DbType.Int32, _CInt(Me.Id.Text.ToString))ExecuteNonQuery(command, UpdateBranchSql)
Continued
265
Chapter 10: Method Consolidation and Extraction Techniques
79796c10.qxd:WroxPro 2/23/08 8:25 AM Page 265
Listing 10-9: BranchMaintenence Form Code (continued)
End IfEnd Sub
Private Sub Delete_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles Delete.Click
DeleteBranch()BranchMaintenance_Load(Nothing, Nothing)
End Sub
Private Sub DeleteBranch()Dim command As IDbCommand = New SqlCommandAddParameter(command, IdParameterName, DbType.Int32, _CInt(Me.Id.Text))ExecuteNonQuery(command, DeleteBranchSql)
End Sub
Private Sub Reload_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles Reload.Click
BranchMaintenance_Load(Nothing, Nothing)End Sub
Private Sub AddParameter(ByVal command As IDbCommand, _ByVal parameterName As String, ByVal parameterType As DbType, _ByVal paramaterValue As Object)
Dim parameter As IDbDataParameter = command.CreateParameter()parameter.ParameterName = parameterNameparameter.DbType = parameterTypeparameter.Value = paramaterValuecommand.Parameters.Add(parameter)
End Sub
Public Sub ExecuteNonQuery(ByVal command As IDbCommand, _ByVal sql As String)
Dim connection As IDbConnection = PrepareDataObjects(command, sql)command.ExecuteNonQuery()connection.Close()
End Sub
Private Function PrepareDataObjects(ByVal command As IDbCommand, _ByVal sql As String) As IDbConnection
Dim connection As IDbConnection = _New SqlConnection(ConnectionString)connection.Open()command.Connection = connectioncommand.CommandText = sqlReturn connection
End Function
Private Function FillDataset(ByVal command As IDbCommand, _ByVal sql As String) As DataSet
Dim connection As IDbConnection = PrepareDataObjects(command, sql)Dim adapter As IDbDataAdapter = New SqlDataAdapter
266
Part III: Getting Started with Standard Refactoring Transformations
79796c10.qxd:WroxPro 2/23/08 8:25 AM Page 266
Listing 10-9: BranchMaintenence Form Code (continued)
Dim branches As New DataSetadapter.SelectCommand = commandadapter.Fill(branches)connection.Close()Return branches
End FunctionEnd Class
If you take a look at the rest of the Rent-a-Wheels classes, you will find that a similar pattern was fol-lowed, resulting in much cleaner and more compact code. However, if you carefully inspect the com-plete code, you will probably notice that while there is no more duplication in a single class, there arestill many methods that are duplicated between classes. Just like any other duplicated code, this can’t beright. But don’t worry: the work on Rent-a-Wheels does not end here. In the following chapters you willsee ways to deal with this and other problems that can still be found in the code.
SummaryMethod extraction is arguably the most common refactoring you will perform. It is the step that carries thebrunt in transforming your code from a primordial mess to organized code structures. However, when youare dealing with long, complicated and poorly structured methods, method extraction is seldom straight-forward. To reap the benefits of extraction, you often have to perform a number of preparatory steps.
In this chapter you have seen some simple yet important refactorings that help prepare the code for methodextraction. These refactorings are mostly concerned with solving the problem of temporary variables, thebiggest impediment to effective method extraction.
You have seen how to internally reorganize methods by bringing the declaration near the variable initializa-tion. Because in VB .NET you can initialize the variable in the declaration statement, you have seen how todo this and how it can benefit code legibility.
Sometimes temporary variables have more than one role, making the code more tangled and more diffi-cult to understand and working directly against method extraction, because a long-reaching temporaryvariable has to be passed in and out of the method as a parameter. The solution for overburdened vari-ables is Split Temporary Variable refactoring.
In some cases it is beneficial to eliminate a temporary variable altogether. If the temporary variable hasbeen assigned only once with a simple expression, you can replace the variable with the expression itselfby inlining the temporary variable.
Another way to eliminate a temporary variable is to replace it with query method or property. Because amethod or property can be reached from other methods or properties, a newly created, extracted methodcan use the query instead of parameter-passing. Better yet, if the query code was present in more than onemethod, you can additionally reduce duplication by replacing all occurrences of the repeated expressionwith the query.
267
Chapter 10: Method Consolidation and Extraction Techniques
79796c10.qxd:WroxPro 2/23/08 8:25 AM Page 267
Finally, you saw the great impact that method organization had on the Rent-a-Wheels application. Bymy performing method extraction, the code was made much more granular, more legible, and morecompact. Code duplication was reduced to a great extent, and the general maintainability of the applica-tion was greatly improved.
With the refactorings mentioned in this chapter, you will be able to unleash method extraction and torestructure methods so that they are small, highly granular, and encapsulated units that do not containduplicated code. Now, to move forward, you need to bring your techniques to the next level. It is timeobjects came into play. In the next chapters we are going to see how refactorings work on that next levelby performing restructuring from an object-oriented perspective.
268
Part III: Getting Started with Standard Refactoring Transformations
79796c10.qxd:WroxPro 2/23/08 8:25 AM Page 268
Part IV: Advanced Refactorings
Now, in Part IV, you are at the heart of object-oriented design. Here I’ll talk aboutobject-oriented encapsulation, inheritance, polymorphism, genericity, and otherimportant characteristics of object orientation. You will see how to model objects,and the techniques you can employ to turn your analysis artifacts into classes thatwill compose the backbone of your application.
More importantly, you will see how to bring object orientation into the code. Veryoften in Visual Basic,you have to deal with code that mixes presentation, domain, andpersistence logic inside a single persistence class. Such a class can be a Window orWeb Form class. It is important to separate such code and make each of your classesdeal with a single responsibility.
To deal with duplication and other smells in your code, you will have to apply inheri-tance and organize your classes into hierarchies. You’ll see how you can move thefeatures between classes to make your code robust and reusable.
This part is the central one of the book. It deals with some of the most importantrefactorings, and if you are able to apply them successfully, you will be able to produce truly effective object-oriented code.
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 269
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 270
Discovering Objects
In theory, you could write programs in VB .NET without ever using a class or creating an object.Thanks to Visual Basic’s legacy Module construct, you could write your programs in a proceduralstyle without knowing the first thing about object orientation. In practice, such a style is rare, andto be honest I never come across it, except in some ad hoc and demo applications. However, mis-use and poor design of objects are much more common. At the root of such code is a poor under-standing of object orientation.
When designing an object-oriented system, you need to think of it as a system of communicatingand collaborating objects. However, the step of converting analysis artifacts to object-oriented codeis neither trivial nor straightforward and is often crucial for the project. While you have seen in thisbook so far that no change to code is impossible and no design decision is irrevocable, identifyingthe classes for the first time will establish the foundations of your design and will most probablylead the design in certain directions later on.
In this chapter, you’ll cover the following:
❑ I’ll start with a quick overview of object-oriented programming. This will help you under-stand the design techniques and refactorings I’ll talk about in this chapter in a deeper con-text. You will also see how there is more to encapsulation when you’re dealing with objectsthan when you’re dealing with functions.
❑ I will continue to discuss some topics I already touched on in Chapter 8 and Chapter 9. Youwill see how analysis artifacts like user stories or use cases serve as the basis for the design ofthe system. However, the gap between the code and the diagrams and text written in naturallanguage is not easy to bridge. You’ll see some techniques you can employ in this endeavor.
❑ You’ll also see what to do if you stray along the wrong path in designing your code. All isnot lost, and you can use diverse refactoring techniques to consolidate your design.
❑ You’ll see how to convert procedural or database-driven design to object-oriented design.
By the end of this chapter, you will have a better understanding of object-oriented design as adynamic, adaptable creation that can be modified to maximize code quality and to respond tochanging requirements.
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 271
A Quick Object-Oriented Programming Overview
As I indicated in the introduction to the chapter, this first section offers a quick overview of some key object-oriented programming concepts. This is not meant to be in any way comprehensive, and no doubt many of you will be familiar with much of the material. Rather, this section is meant as a quick refresher on somekey concepts/features of object-oriented programming that the refactorings in this chapter involve or takeadvantage of. I have found that such overviews can be useful even for those familiar with the subject inquestion for identifying possible gaps or blind spots in understanding. Once you have identified them, youcan fill in those gaps in order to complete your knowledge on the subject of object-oriented programming.
If you want to head straight to the refactorings discussed in this chapter, you can skip down to the“Designing Classes” section of the chapter and start reading there. However, if you keep reading, youwill refamiliarize yourself with some of the unique characteristics of object-oriented programming thatrefactoring (which is what this book is about) really uses to full advantage.
What Are Objects Anyway?In procedural programming, you write programs by invoking functions and asking them to perform someoperation on the data you generally supply as arguments. Sometimes, those functions return some data as aresult of an operation. In object-oriented programming, on the other hand, you construct systems by sendingmessages to objects and asking them to perform some operations. Objects work as small, independent, andencapsulated machines, each of which has its own piece of responsibility. I have already talked about encap-sulation in Chapter 9, but without talking explicitly about object-oriented programming. I want to start thischapter by taking a look at how encapsulation works with objects.
Encapsulation and ObjectsWith objects, encapsulation works on another level. Here information and operations go together. Objectsare constructed from information representing state and operations that permit access to the information.Thanks to encapsulation, you never access the information directly. The data is hidden, and accessibleonly through properties and methods that provide another internal level of control. These public proper-ties and methods that can be used by other objects to communicate with the object are called an interface.
In that sense, from the outside, objects look like black boxes that expose some functionality through awell-defined interface. However, you have no idea of what goes inside the box or how this functionalityis implemented internally. It also means that the data, implemented as object attributes, can be stored inone form internally and in another form visible to the public.
Take a look at the following Account class code. It illustrates techniques for hiding both information andimplementation.
Public Class Account‘Balance is internally kept as DoublePrivate balanceValue As Double
‘This attribute is not publicly visiblePrivate preferredCustomer As Boolean
‘Balance is publicly visible as String
272
Part IV: Advanced Refactorings
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 272
Public ReadOnly Property Balance() As StringGet
Return balanceValue.ToStringEnd Get
End Property‘...
End Class
In the class Account the field preferredCustomer is not visible from the outside, meaning that thispiece of information is hidden from the public but is still to be used by the object internally. The propertyBalance is visible to the public as a String; however, it is kept internally as a Double. Figure 11-1 illus-trates an object in the form of a machine, in this case a control panel, accessed through an interface. Justas an operator communicates with the machine through the control panel, having no idea as to whathappens inside the machine, so the programmer using only the class has no idea of the inner workingsof an object that programming is instantiating and using.
Figure 11-1
In Chapter 9 I talked about these welcome consequences of encapsulation known as information and implementation hiding. They bring more flexibility to the system, because some internal design andimplementation decisions can be changed without affecting anyone on the outside, thus localizing thechange and preventing its ripple effect. So in the Account example class, you could change the internalimplementation of the Balance property without affecting existing clients. For example, you couldmake use of the FormatCurrency function to take regional settings into account when making Balanceavailable to the public:
‘Total is publicly visible as StringPublic ReadOnly Property Balance() As String
GetReturn FormatCurrency(balanceValue)
End GetEnd Property
This makes the system much easier to maintain. However, the benefits do not end there. In this way representation and information content are separated, so the client is not coupled to a format used inter-nally to represent the information.
Finally, encapsulation is the basis of modularity, which can help you construct complex systems bycombining smaller components. This is crucial for taming complexity, something that object-orientedprogramming can be very good at.
Parts Finished Product
Machine
Control Panel Operator
273
Chapter 11: Discovering Objects
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 273
Before going further, I want take a look at how Refactor! can automate the process of encapsulatingfields by generating property setter and getter definitions. After that, I will return to the overview ofkey object-oriented concepts.
Encapsulate Field Refactoring in Refactor!You can invoke this refactoring by placing the cursor over the field name in the field declaration statement.Refactor! adds the Get and Set part of the property declaration. Get returns the value of the field, whileSet assigns a value to the field. The name that Refactor! chooses for a newly created property will be thesame as that of the field, in uppercase, while the field is renamed with an underscore [_] at the beginning ofits name. The property is declared as Public, while the visibility of the field is changed to Private, if thefield was not private to start with.
Refactor! features two additional refactorings that are essentially read- or write-only variants ofEncapsulate Field refactoring:
❑ Encapsulate Field (read-only) generates the getter method only, creating a read-only property.
❑ Create Setter Method creates a setter method for the field, but does not modify the visibility ofthe method. Write-only properties are much less common than read-only properties, soRefactor! generates an internal Set block with Friend visibility for the method.
You have probably observed that I chose to add the suffix Value to field names in order to differentiate a field name from the encapsulating property name. Since VB is not case-sensitive, field and propertynames have to differ by at least one character. One common convention is to add the underscore prefixto field names, as Refactor! does, but I prefer to use the Value suffix to denote a field encapsulated bythe property of the same name.
You can take a look at Encapsulate Field refactoring selected in the Refactor! menu shown in Figure 11-2.
Figure 11-2
Vehicle
Customer
+HandOver(in customer : Customer)
«enumeration»IdType
1 +owner
* +owned
IdDocument
+Number : String+Type : IdType
274
Part IV: Advanced Refactorings
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 274
Refactoring: Encapsulate FieldMotivationThe reasons for encapsulating a field can be numerous, but the basic one is informationhiding. A property can hide internal data representation from the public view. What’smore, a property can control the data access, validate the data, and inform the objectthat the data is being accessed or modified. A property is also the easiest means ofimplementing read-only data in an object.
Even when a property’s Set and Get blocks do nothing else other than read and write thefield value, you should encapsulate such a field if the class it belongs to is part of the pub-lished interface. Changing a field to a property later on will break binary compatibilitybetween the two versions of the class, so you should avoid such situations early, by usinga property instead of a field. While VB syntax for referencing fields and properties is thesame, CIL syntax for the two differs, resulting in binary incompatibility between versionsof the classes where one exposes a field and the other a property with the same name.
Related SmellsUse this refactoring to eliminate the Overexposure smell and to prepare a class forMove Field refactoring.
Mechanics1. Perform Rename refactoring on the field, adding the suffix Value to the
field name.
2. Create a property with the original name of the field that assigns to andreads from the field.
3. Change the field visibility to private and compile.
BeforePublic Class Account
‘...Public Balance As Decimal
End Class
AfterPublic Class Account‘...Private balanceValue As DecimalPublic Property Balance() As Decimal
GetReturn balanceValue
End GetSet(ByVal value As Decimal)
balanceValue = valueEnd Set
End PropertyEnd Class
275
Chapter 11: Discovering Objects
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 275
Object State RetentionOne very important characteristic of objects is that they retain state during their lifetime. For example, oneclient could call upon an object providing some information. Another client could call upon the same objectlater on, asking for the same information. The result it would receive would be the same information thatthe first client provided. This, of course, is only if the object was internally implemented to provide thesame information, because the clients generally don’t know what happens inside the object, as you learnedin the previous section. Thus the object is capable of preserving its state indefinitely — or at least for as longas it is alive.
This is not so in procedural programming. When the function is invoked, all data (except for global data)lives as long as the function invocation itself. Once the function is executed, the data is discarded, andthere is no trace of the execution left when the function is invoked again. Take a look at the followingcode snippet:
‘...Dim basket As ShoppingBasket = New ShoppingBasketbasket.AddItem(New Product)basket.AddItem(New Product)Console.WriteLine(basket.ItemCount)
Once this code is executed, the console will print 2 as a result. This is thanks to the capacity of the objectbasket to retain state between the calls.
ClassesWhen you program in VB .NET, you design and code classes. Classes have methods, properties, andevents. During runtime, object instances are created based on classes in a process similar to using a moldto create and give shape to some artifact. Once you create an instance from a certain class, you can knowfor sure that this particular object will have the same properties, methods, and events as any other objectever created from that same class.
There is another type of member that is not passed over to instances. Classes can have their own class-level members. These members are known as static in some other languages like C# or Java, but in VB aredenoted by the Shared keyword because all instances (of any class) share them. If one instance updatesthe shared property, all instances will see the updated value. These members behave like any memberdeclared inside the VB module. Members of the VB module are Shared by default, so you do not use theShared keyword when declaring VB-module members.
To create an instance of a class, you use Visual Basic’s New operator. Once the statement containing theNew keyword is executed, a special constructor procedure is executed next. This procedure in VB is knownas Sub New. This makes the Sub New procedure very convenient for some operations related to object creation, such as variable initialization.
Based on what I just said about constructors and shared members, you could easily write code that keepsa count of the instances of some class that are created by means of these two elements. Take a look at thefollowing MyClass code:
Public Class MyClass‘Holds number of created instances of MyClass classPrivate Shared NumberOfInstancesValue As Integer = 0
276
Part IV: Advanced Refactorings
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 276
Public Shared Property NumberOfInstances() As IntegerGet
Return NumberOfInstancesValueEnd GetSet(ByVal value As Integer)
NumberOfInstancesValue = valueEnd Set
End Property
Sub New()‘Increments NumberOfInstances property by one each time ‘an instance of MyClass is createdNumberOfInstances += 1
End Sub‘...
End Class
In this example, you can see how the value of a shared property is incremented by one each time a newinstance of MyClass is created, making this information available to all active objects in the program.
I mentioned earlier that all instances of the same class have the same members, including properties. Most of the time, the values that these properties hold are different from instance to instance. Sometimes,however, it happens that you have two instances that hold identical values for all the properties, making itdifficult for you to tell these objects apart. Nonetheless, Visual Basic’s runtime has no such problems. Thisis thanks to another important characteristic of objects: object identity.
Object IdentityEvery object, even if created from the same class as others and even if the values of all its properties arethe same as those of some other object, is unique in the system and can be told apart by the runtime. Thisis because every object is assigned its own memory space. That means that you can be sure that the handleto an object will point to the same unique object as long as you do not explicitly assign another object to it. You use variables to keep the handle to an object. In case you need two variables to point to the sameobject, you can use the Equals method. Take a look at the following code:
Dim object1 As New ObjectDim object2 As New ObjectConsole.WriteLine(object1.Equals(object2))‘Make object1 and object2 variable point to the same objectobject2 = object1Console.WriteLine(object1.Equals(object2))
If you execute that code, the output will be as follows:
FalseTrue
The default implementation of the Equals method uses reference semantics, meaning that it will returnTrue if the variables point to the same address in the memory.
277
Chapter 11: Discovering Objects
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 277
Sometimes the Equals method can be overridden so that it takes into account the value of properties in theobject. This is called value semantics. It can be useful for value type objects. For example, if you have a classthat represents a date and you have created two instances of that class that represent the same calendar date,say January first, 2008, it is logical that their comparison should yield the result True even if they occupydifferent memory spaces, such as in the following code using Visual Basic’s Date and the .NET Framework’sSystem.DateTime type:
Dim date1 As New DateTime(2008, 1, 1)Dim date2 As New DateTime(2008, 1, 1)Dim date3 As New Date(2008, 1, 1)Console.WriteLine(date1.Equals(date2))Console.WriteLine(date2.Equals(date3))
Execution of the previous code will result in True being displayed on the console window twice.
Objects as Basic Building BlocksIn pre-.NET versions of Visual Basic, you made a strict distinction between primitive types like Integer orString and objects like Collection, ComboBox, and so on. Primitive types were used to hold some datavalues and were often used as basic building blocks when you were writing your own custom classes.
In .NET even simple types like Integer can be treated as fully capable objects. Thanks to the .NET boxingand unboxing feature, the distinctions between simple types, value types, and reference types are almostcompletely blurred. So in .NET it is completely legal to write a code like the following:
Dim stringValue As String‘Initialize string by calling method on a number literalstringValue = 6.ToString
In VB 6, for example, calling a method on a literal representing a primitive was not possible. In .NET thisis possible because value types can be automatically boxed inside an instance of the object reference type.
You should be aware, however, that there is a performance penalty for treating values as reference types.
Root ObjectIn .NET, each time you write a class, it will implicitly inherit the .Net root object: System.Object. Youcan even inherit this object explicitly without any consequence. So whether you declare your class asClass MyClass or as Class MyClass inherits Object, the result is the same: any object you createwill already have a few methods implemented. The public methods are as follows:
❑ Equals
❑ GetHashCode
❑ GetType
❑ ToString
278
Part IV: Advanced Refactorings
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 278
The protected methods are:
❑ Finalize
❑ MemberwiseClone
This is also true for value types, because the base class for value type System.ValueType also inheritsSystem.Object. If you declare a variable as SomeType, the variable will be able to point to an instanceof that or any other subtype of SomeType, because of polymorphic behavior inherent in VB code. In con-sequence, if you declare a variable as Object, because all types in .NET inherit Object, the variable willbe able to point to any instance you can throw at it.
Because System is the default namespace, you are free to omit it and write either System.Object or,which is more common, just Object.
Object Lifetime and Garbage CollectionWhen I talked about object state, I said that objects retain state as long as they are alive. Objects can be aliveas long as the program lives, but this would not be very practical, so the majority are discarded during pro-gram execution once they are not needed anymore. Objects come to life when you call the New operator. Asyou have already seen, the constructor method is called right after object creation. This is straightforward.The next logical question is “When do objects die?” The reply this time is not as simple.
An object’s lifetime is determined by its scope. This means that when the object goes out of scope, theruntime is free to reclaim the memory that the object has been occupying. However, this does not happenimmediately. To understand what happens when an object goes out of scope, you need to understand afew facts about garbage collection.
Garbage collection is a mechanism for automated memory management. To run efficiently, the runtimeneeds to allocate memory for new objects and reclaim that memory once it has been left unused. Once an object has gone out of scope and become unreachable, the garbage collector is free to reclaim thememory space and use it for some other object. Failing to reclaim memory results in memory leakage, aperilous bug well known to COM programmers.
Reference Counting Garbage CollectorPre-.NET Visual Basic programmers could also count on the runtime to liberate memory once theobject was freed. In COM, garbage collection was based upon reference counting. Each time a new ref-erence to an object was created in COM, the reference count was incremented by one. When the refer-ence was removed, the count was decremented. When the count reached zero, the object was removedand the memory reclaimed. Most of the time this worked well. In some situations, as when circularreferences are produced, for example, the count would fail to reach zero, and the memory would neverbe reclaimed. This is partly why VB 6 programmers were accustomed to helping the garbage collectorby setting the variable value to nothing once they had no need for the object, basically manually decre-menting the reference counter. However, this does not work in .NET anymore, because in .NET thegarbage collector is radically different.
279
Chapter 11: Discovering Objects
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 279
Tracing Garbage CollectorIn .NET, Microsoft has implemented a tracing garbage collector. That means that the collector starts atroot objects (local variables and reference object parameters, global and static object pointers, and so on)and then creates the graph of objects that are reachable from the roots. Objects that are not in the graphare considered unreachable, and the collector is free to reclaim the memory.
To make garbage collection more efficient, you can use different collector-optimizing techniques such as dividing objects into generations. This is an optimizing technique used by the .NET garbage collec-tor. Garbage-collector performance is one of the most critical performance aspects of the .NET runtime.However, this is a bit out of the scope of this book, so I will not proceed further with the internals of.NET garbage collection. The important fact about the .NET garbage collector is that it is nondeterminis-tic, meaning that the programmer has no control over when the garbage collection is going to happen.To be precise, it is possible to invoke garbage collection explicitly, but this is not recommended, becauseit means meddling with complex garbage-collection algorithms, and any performance optimization isnot very likely, unless you are quite sure of the implications.
In conclusion, objects are removed from memory when the garbage collector decides it is the best momentto do so. This has one very practical consequence. If you are using some limited resources, like databaseconnections, files, or the like, it may not be practical to wait for the garbage collector to kick in. In thosecases you can use Visual Basic’s Using statement to mark the block after which the object should be dis-posed of. The Using block can be written like this:
Using connection As IDbConnection = _New SqlClient.SqlConnection
‘use connection...End Using
You are guaranteed that the connection object will be disposed of after the End Using statement. In caseyou need to create your own class that should release resources early by means of a Using block, yourclass should implement the System.IDisposable interface.
Garbage collection is a great feature of the .NET Framework. It relieves the programmer of one of themost tedious and bug-spawning tasks, manual memory management.
MessagesAs I mentioned earlier on, the dynamic of the object-oriented system comes from message interchangebetween objects. When you write the code, you send the message to an object, asking it to perform someoperation. This is radically different from calling functions while supplying them with data. As you willsee in the next chapter when I talk about inheritance and polymorphism, when you write the code, youmay not know until runtime the exact function that will be executed as a consequence.
In the previous sections you have seen some of the most important characteristics of object-oriented programming. It is very important to be aware of these characteristics to successfully design and writeobject-oriented code. Obviously, the story of object-oriented programming does not end here. In the nextchapter I’ll talk about inheritance and polymorphism, other crucial mechanisms that any object-orientedenvironment should support.
Now that I have reminded you of the basic building blocks of object-oriented programs, you are ready to embark on the fundamental step in software designing: discovering classes and partitioning your
280
Part IV: Advanced Refactorings
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 280
code in the form of types. You will see how new classes can be created starting from the “clean sheet,”based on your analysis artifacts, or how some existing code can be restructured so new classes are intro-duced in order to better organize the code. One such restructuring is known as Extract Class refactoring.I will start out by discussing the crucial step in the design process by which new classes are formedbased on analysis artifacts.
Designing ClassesIn Chapter 8 I talked about the process of writing software, and about techniques you can use to understandthe problem domain and gather the requirements. You have seen how in that phase of development, severaldiverse artifacts can be produced to help structure and organize knowledge about the requirements andproblem domain, artifacts such as an agreed-upon vocabulary, use cases, and user stories. All these artifactsuse some sort of natural language, diagrams, drawings, prototypes, user interface captures, and so forth tocapture the functionality that the system in question should perform. However, in that chapter I stoppedbefore embarking on the next crucial step — using all this gathered information and transforming it into thecode. Based on these artifacts, you can design the classes that are going to be the basic building blocks ofyour software.
In this section you are going to see some of the most popular approaches for the discovery of objects. Theseapproaches should help you breach a very difficult gap that lies between the natural language and otherartifacts made for human comprehension and the program code, which is written for computer execution.Keep in mind the complexity of the task; these approaches are hardly foolproof. To apply them efficiently,you need a significant amount of experience and practice.
First and foremost, you need to identify objects belonging to a problem domain. Implementation details will be refined as you go along. When you are designing classes, the most important decisions are concernedwith identifying the classes with their operations and attributes and with the relations that classes can have.Another aspect of the system you are designing that you must not overlook is its dynamics — that is, howobjects collaborate and exchange messages.
After a quick look at the first refactoring of this chapter, you will start bridging the gap from concept tocode with the most popular approach, linguistic analysis.
Continued
Refactoring: Extract ClassMotivationLarge classes are difficult to understand and maintain. They also make reuse muchmore difficult and can make you distribute unnecessary pieces of code. Large classesare also a breeding ground for uncontrolled dependencies. Extract Class provides themost direct manner for dealing with large classes.
You should start by looking at the features that are grouped and that go naturallytogether inside the class. Sometimes these features have a common prefix indicating thatthey belong together. For example, in the class Client you have properties prefixed bythe word Telephone: TelephoneNumber, TelephoneAreaCode, TelephoneType. Suchfeatures can be extracted into a separate class.
281
Chapter 11: Discovering Objects
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 281
Related SmellsUse this refactoring to eliminate the Large Class smell and to make a class comply withthe Single Responsibility Principle (SRP). (Note: the Large Class smell and the SRP areboth defined later in this chapter.)
Mechanics1. Create a class for features you would like to split into a separate class.
2. Make a link between an original and a new class. The new class canbecome a field of the originating class.
3. Use Move Field refactoring on the originating class and bring a field intothe new class.
4. Continue moving fields and methods (and rename them if needed aftermoving them) until you are done.
BeforePublic Class ShoppingCart
Private productsValue As IListPrivate customerNumberValue As IntegerPrivate customerNameValue As StringPrivate customerTypeValue As CustomerTypes
Public Function CustomerDiscount() As DecimalIf Me.CustomerType = CustomerTypes.Premium Then
Return 3Else
Return 0End If
End Function
Public Property CustomerType() As CustomerTypesGet
Return customerTypeValueEnd GetSet(ByVal value As CustomerTypes)
customerTypeValue = valueEnd Set
End Property
Public Function CalculateTotal() As DecimalDim total As DecimalFor Each product As Product In Me.Products
total += product.PriceNexttotal = total - ((total / 100) * Me.CustomerDiscount())Return total
End Function
Public Property Products() As IList
282
Part IV: Advanced Refactorings
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 282
Continued
GetReturn productsValue
End GetSet(ByVal value As IList)
productsValue = valueEnd Set
End Property‘...
End Class
AfterPublic Class ShoppingCart
Private productsValue As IListPrivate customerValue As Customer = New Customer()Public Function CalculateTotal() As Decimal
Dim total As DecimalFor Each product As Product In Me.Products
total += product.PriceNexttotal = total - ((total / 100) *
Me.customerValue.Discount())Return total
End FunctionPublic Property Products() As IList
GetReturn productsValue
End GetSet(ByVal value As IList)
productsValue = valueEnd Set
End Property‘...
End Class
Public Class CustomerPrivate NameValue As StringPrivate NumberValue As IntegerPrivate TypeValue As CustomerTypesPublic Property Name() As String
GetReturn NameValue
End GetSet(ByVal value As String)
NameValue = valueEnd Set
End PropertyPublic Property Number() As Integer
GetReturn NumberValue
End GetSet(ByVal value As Integer)
NumberValue = value
283
Chapter 11: Discovering Objects
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 283
Classes Are Nouns, Operations Are VerbsProbably the most popular approach in object-oriented circles for developing code from the artifacts youhave gathered is textual or linguistic analysis. During analysis you have surely produced some text thatdescribes the desired system behavior and sample user-system interactions. Often, this text takes theform of use cases. You can analyze such text and find classes “buried in the text,” along with their mem-bers, according to the following rules:
❑ Nouns become classes.
❑ Verbs become operations.
❑ Some nouns (or adjectives) become attributes of classes.
❑ An “is a” phrase indicates inheritance and a “has a” phrase indicates a strong association suchas composition or aggregation.
You can see how this works on a sample use case from the Rent-a-Wheels application. You have alreadyseen this use case in Chapter 4, and you have many more use cases available in that chapter to give this a try for yourself. This process generally goes through a few steps, and I will demonstrate them in theorder in which they are performed.
Step 1: Mark All the Nouns You start with a very simple operation, marking all nouns in the use case with bold. Here is the Rent aVehicle use case with all the nouns identified:
1. Receptionist selects the vehicle from the list of available vehicles.
2. Receptionist inputs the customer data: ID type and number.
3. Receptionist marks the vehicle as “hand over.”
End SetEnd PropertyPublic Property Type() As CustomerTypes
GetReturn TypeValue
End GetSet(ByVal value As CustomerTypes)
TypeValue = valueEnd Set
End PropertyPublic Function Discount() As Decimal
If Me.Type = CustomerTypes.Premium ThenReturn 3
ElseReturn 0
End IfEnd Function
End Class
284
Part IV: Advanced Refactorings
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 284
Step 2: Eliminate the Actor Generally you do not need to model the actor as a part of your system. The actor is an external entitythat interacts with the system. Actor elimination results in fewer selected nouns.
1. Receptionist selects the vehicle from the list of available vehicles.
2. Receptionist inputs the customer data: ID type and number.
3. Receptionist marks the vehicle as “hand over.”
Step 3: Eliminate Duplicated NounsIn a use case, the same noun can appear more than once. The same noun is usually transformed in thesame class, at least when use cases are well written. So you end up with the following list of nouns:
1. Vehicle
2. List of vehicles
3. Customer
4. ID type
5. ID number
Step 4: Separate System from Domain ClassesThe system classes are those supporting classes that do not belong to the problem domain. Very oftenyou can count on the .NET Framework or third-party libraries to provide you with these classes. If youhave some specific requirements that existing classes do not support, you might need to program theseclasses as well, or extend existing ones. In that case, you should consider them also. Very often, however,the decision to use existing classes or to program custom classes is made later on in the process, whenmore details about implementation are known.
In this example, you can see from the use-case content that “List of vehicles” is an object used for presen-tational purposes, to render a list of vehicles to a user and let that user select a single vehicle from the list.It is probable that some existing widget can be used to instantiate an object that fulfills this purpose, so I’llput aside for the moment the “List of vehicles” noun in this design.
Step 5: Separate Attributes from ClassesIf you read the use case carefully, you will find some compound nouns like ID type and ID number. If youinterpret the second statement in the use case, you will see that ID, in fact, refers to a form of identificationand that ID type and ID number refer to attributes of this class. Some attributes can be successfully repre-sented with system classes. For example, an identification document number can be represented with astring (some documents, like passports, can have characters in the identification number). You could alsorepresent the identification document type with a simple string, but a more elaborate design would limitthe choice of document types. So a better choice for the identification document type is an enumeration.
The rule about using adjectives to identify attributes will not work out well most of the time. Because ofthe style of writing that is common in use cases, you seldom find phrases like this: “The car is red.” Moreoften than not you will find a statement like this: “The color of the car is red.” This means that a noun(color, in this instance) is used in these cases to identify an attribute of a class.
285
Chapter 11: Discovering Objects
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 285
Going back to the example, after this step you have the following classes and enumerations identified:
1. Vehicle
2. Customer
3. ID with attributes number and type
a. Attribute number is of String type
b. Attribute type is of ID type enumeration
4. ID type (enumeration)
Step 6: Interpret “is a” and “has a” PhrasesAgain, you need to do some interpretation of the use case text. You have already seen how ID type andID number were hiding another class not directly mentioned, an ID document. This object is used toidentify the customer.
In fact, you can now say that “the customer has an ID document.” This means that there is some sort ofstronger association between the customer and the ID document. I have chosen to represent this associa-tion as an aggregation, since the customer can probably exist in the system even without an ID document.(The type of association will have no consequence on the code of these classes. It might, however, influ-ence how you program the system later on.) This means that the customer will have a property of thetype ID document. In a class diagram, this is represented with a line going from the IdDocument class to the Customer class with a diamond ending it (see Figure 11-3).
Step 7: Identify the Operations The last sentence in the use case talks about an operation that is performed in the system. You have alreadyidentified the classes in the system, and as you have seen in the introductory part of this chapter, in object-oriented programs you ask an object to perform an operation by sending it a message. Sentence number
Smell: Data ClassDetecting the SmellA class that has only properties and no methods is a data class. These classes and struc-tures are generally easily discovered because of a long list of fields and property blocksuninterrupted by method definitions.
Related RefactoringUse Convert Procedural Design to Objects and Move Method refactorings to eliminate the smell.
RationaleObject-oriented programming brings data and behavior together. A class consisting offields and properties only is lacking methods that give life to data. This behavior isthen implemented in some other place, maybe as a part of some other class or in theform of shared methods. Wherever the methods actually are, by not being in the rightplace they will stand in the way of encapsulation and will make inheritance, polymor-phism, and similar object-oriented techniques impossible to implement.
286
Part IV: Advanced Refactorings
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 286
three in the use case talks about handing over the vehicle. So, you can add a handOver operation to theVehicle class. You need to supply the Vehicle with a piece of information for it to be able to perform thisoperation: you need to supply the Customer. In the end, the operation will look something like this once itis written down:
Public Class Vehicle
Public Sub HandOver(ByVal customer As Customer)‘... to be implemented
End Sub‘...End Class
With this, the process of textual analysis is more or less complete. In real life, you will iterate the process forother use cases. During iteration, you will discover new classes and more members and relations betweenexisting classes. As you move on, more pieces of the picture are discovered, and you move one step closerto the first version of your application.
In the example you analyzed, you found no occurrences of “is a” relation. Such a relation would indicateinheritance between classes and might be written as follows: “If a vehicle is a truck, the hand-over opera-tion is performed as follows.” In that case, you would use another class, Truck, that inherits Vehicle, to represent this new entity. The Truck class would override the handOver operation to realize a differentset of steps that are performed when a truck is handed over to a customer.
Now, take a look at the classes you have identified so for in a diagram (see Figure 11-3).
Figure 11-3
As you can see, the class diagram contains all the classes, their operations and attributes, and the rela-tionships between classes identified in the sample use case by means of textual analysis. If you go backto the Rent-a-Wheels code, you will find no mention of the classes you just identified. From that you canconclude that some other modeling technique was used to design the code. If you remember, the Rent-a-Wheels Visual Basic application layer design was preceded by database design; I call this technique data-base-driven design and will talk about it in the context of anti-patterns. An anti-pattern is similar to a smell,only it generally works on a larger scale.
Customer
Vehicle
+HandOver(in customer : Customer)
«enumeration»IdType
1
+owner
*
+owned IdDocument
+Number : String
+rented by
+rents
+defines type
287
Chapter 11: Discovering Objects
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 287
The next section explores another technique you have at your disposal when you are trying to identifyclasses to comprise the system you are building: thinking about objects in terms of classes, their respon-sibilities, and their collaborators.
Classes, Responsibilities, and CollaboratorsClass-responsibility-collaborator cards (CRC cards) are a brainstorming and design tool that can help youidentify and design the classes for your system. The cards should help you think about classes in termsof their responsibilities and collaborators. This is a great tool for understanding the dynamics of the sys-tem and behavior of objects. The cards render naturally into an object-oriented design.
In a CRC-card design session, you start with a stack of blank 4˝-by-6˝ index cards. Each card shouldrepresent a single class. The cards have three compartments: top, left, and right. Each compartmentshould be filled by the developer.
❑ Top: Place the class name in this compartment. Optionally, add a section for listing super- and subclasses.
❑ Left: List all the responsibilities that belong to this class in this section.
❑ Right: List all the collaborators that the class communicates with to perform its responsibilities.
Definition: An anti-pattern is a repeated solution to a problem that presents a number of difficulties and design flows. A better solution is possible but is notapplied because of lack of knowledge, or simple malpractice. An anti-pattern can generally be refactored into a more optimal solution.
Smell: Database-Driven DesignDetecting the SmellA way to detect this smell is often indirect. Most of the design document is concernedwith database design. The team talks about database structure, data integrity, and nor-malization, but is not very concerned with identifying classes, designing objects, or con-sidering the interactions at work.
Such an approach often leads to a simplistic design wherein Visual Basic code fulfillsthe purpose of a simple data-flow conductor. Because there is no elaborate design,many more advanced object-oriented techniques like inheritance and polymorphismare not present. The design does not reflect the problem domain.
Related RefactoringUse Replace Row with Data Class, Convert Procedural Design to Objects, and ExtractClass refactorings to eliminate the smell.
RationaleCode whose only purpose is to store and retrieve data from the database does not com-ply with basic object-oriented principles and does not represent the problem domain orembody the business rules. Such VB code resigns all important logic to the SQL codelayer. It means that a lot of duplicated and redundant code is present, making theapplication difficult to maintain and modify.
288
Part IV: Advanced Refactorings
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 288
Continued
Refactoring: Move MethodMotivationSometimes a method just seems more interested in other class members than in membersof its own class. In that case, the method makes more sense in the other class.
Related SmellsUse this refactoring to eliminate Large Class, Data Class, and Procedural Design smellsand to simplify classes that do not obey the SRP.
Mechanics1. Start by copying the method into the new class. Rename it if you have a name
that fits better and makes it work with data available in the new class.
2. If the method needs originating class members, and there is no other way toget the necessary functionality, pass the originating object as a parameter toa method.
3. If possible, make the clients use the new method and erase the originalmethod. If not, keep the delegation code in the original method.
Sometimes moving a method might cause other members to be moved. If such a member is used only by the moved method, you can move the other member withoutconsequences.
BeforePublic Class Point
Public X As DoublePublic Y As DoublePublic Function CalculateCircumferenceLength( _ ByVal center As Point, _ByVal pointOnCircumference As Point) As DoubleReturn 2 * 3.1415 * CalculateRadius(center, _pointOnCircumference)End Function
Public Function CalculateRadius(ByVal center As Point, _ByVal pointOnCircumference As Point) As Double
Return ((pointOnCircumference.X - center.X) ^ 2 + _(pointOnCircumference.Y - center.Y) ^ 2) ^ (1 / 2)
End FunctionEnd Class
Public Class CirclePrivate centerValue As PointPrivate pointOnCircumferenceValue As PointPublic Property CenterValue1() As Point
GetReturn centerValue
End Get
289
Chapter 11: Discovering Objects
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 289
Set(ByVal value As Point) centerValue= value
End SetEnd PropertyPublic Property PointOnCircumferenceValue1() As Point
GetReturn pointOnCircumferenceValue
End GetSet(ByVal value As Point)
pointOnCircumferenceValue = valueEnd Set
End PropertyEnd Class
AfterPublic Class Point
Public X As DoublePublic Y As Double
End Class
Public Class CirclePrivate centerValue As PointPrivate pointOnCircumferenceValue As PointPublic Property Center() As Point
GetReturn centerValue
End GetSet(ByVal value As Point)
centerValue = valueEnd Set
End PropertyPublic Property PointOnCircumference() As Point
GetReturn pointOnCircumferenceValue
End GetSet(ByVal value As Point)
pointOnCircumferenceValue = valueEnd Set
End Property
Public Function CalculateCircumferenceLength() As DoubleReturn 2 * 3.1415 * CalculateRadius()
End Function
Public Function CalculateRadius() As DoubleReturn ((Me.PointOnCircumference.X - Me.Center.X) ^ 2
+ _(Me.PointOnCircumference.Y - Me.Center.Y) ^ 2) ^ (1 /
2)End Function
End Class
290
Part IV: Advanced Refactorings
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 290
Moving features between classes is very common in the refactoring process. The following Move Fieldrefactoring is similar to Move Method refactoring (shown in the preceding sidebar) and can be employedin its own right or as a part of some more complex refactorings like Extract Method.
Continued
Refactoring: Move FieldMotivationSometimes methods of some other class seem to be much more interested in the field inthe class than are the methods in the class containing the field. This results in high cou-pling between classes and weakened encapsulation and modularity. While this refactor-ing can be used on its own, this refactoring is an essential part of Extract Classrefactoring.
Related SmellsUse this refactoring to eliminate Large Class, Data Class, and Procedural Design smells.
Mechanics1. Start with Encapsulate Field refactoring, providing property access to a
field, if the field is public and accessed directly. In the originating class,replace all field access with property access.
2. Copy the field and the property that encapsulates it to a targeted class.
3. Make the target class accessible to the originating class. Sometimes you canobtain a target instance from existing fields or methods. If not, you mightneed to add a field with a target instance.
4. Make the property in the originating class use the property on the targetclass instead of on the field.
5. Eliminate the field in the originating class.
Alternatively, if there are only a small number of references to a field in the originatingclass, you can reference a property on the target class directly, eliminating the propertyin the originating class altogether.
BeforePublic Class Rental
‘...Private numberOfDaysValue As IntegerPrivate vehicleValue As VehiclePublic DailyPrice As DecimalPublic Property NumberOfDays() As Integer
GetReturn numberOfDaysValue
End GetSet(ByVal value As Integer)
numberOfDaysValue = valueEnd Set
291
Chapter 11: Discovering Objects
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 291
End PropertyPublic Property Vehicle() As Vehicle
GetReturn vehicleValue
End GetSet(ByVal value As Vehicle)
vehicleValue = valueEnd Set
End PropertyPublic Function Total() As Decimal
Return Me.NumberOfDays * Me.DailyPrice +Vehicle.Tank.Price
End FunctionEnd Class
Public Class VehicleType‘...Private nameValue As StringPublic Property Name() As String
GetReturn nameValue
End GetSet(ByVal value As String)
nameValue = valueEnd Set
End PropertyEnd Class
AfterPublic Class Rental
‘...Private vehicleValue As VehiclePrivate dailyPriceValue As DecimalPublic Property DailyPrice() As Decimal
GetReturn Me.Vehicle.Type.DailyPrice
End GetSet(ByVal value As Decimal)
Me.Vehicle.Type.DailyPrice = valueEnd Set
End PropertyPublic Property NumberOfDays() As Integer
GetReturn numberOfDaysValue
End GetSet(ByVal value As Integer)
numberOfDaysValue = valueEnd Set
End PropertyPublic Property Vehicle() As Vehicle
GetReturn vehicleValue
292
Part IV: Advanced Refactorings
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 292
Employing Cards in Brainstorming SessionsWhen starting a CRC-card design session, you should select a seemingly related set of use cases or user stories. You will first identify the classes in a process similar to textual or linguistic analysis by transformingnouns into classes. Then you will assign responsibilities to the classes. Again, you can use textual analysis toidentify verbs, indicating the problem the class should solve. Finally, you should look for other classes thatthe class needs to successfully fulfill its responsibilities, and you should list these classes in the Collaboratorscompartment on the card.
You generally work with cards in a team environment. As the session progresses, cards are refined, someare put aside, and new ones are created. The cards can be arranged on the table in such an order that differ-ent scenarios can be played out. During this role-playing, each card can represent an instance of a class. Asdifferent scenarios and use cases are played out, the design is further refined and reinforced. If the cardbecomes too crammed with features, it is a good indication that a new card should be introduced and someresponsibilities passed to that new card. Since new cards are easily created and old ones are easily put asideor thrown back into play, cards are a great interactive and dynamic aid. In Figure 11-4 you can see two sam-ple cards created based on the already familiar Rent a Vehicle use case, used for a textual analysis examplein the previous section.
End GetSet(ByVal value As Vehicle)
vehicleValue = valueEnd Set
End PropertyPublic Function Total() As Decimal
Return Me.NumberOfDays * Me.DailyPrice +Vehicle.Tank.Price
End FunctionEnd Class
Public Class VehicleType‘...Private nameValue As StringPrivate dailyPriceValue As DecimalPublic Property Name() As String
GetReturn nameValue
End GetSet(ByVal value As String)
nameValue = valueEnd Set
End PropertyPublic Property DailyPrice() As Decimal
GetReturn dailyPriceValue
End GetSet(ByVal value As Decimal)
dailyPriceValue = valueEnd Set
End PropertyEnd Class
293
Chapter 11: Discovering Objects
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 293
Figure 11-4
CRC cards are great tools for discovering classes that will comprise the design of your system. Theirfocus on responsibilities and object collaboration fits naturally into an object-oriented approach to soft-ware design and can be a great technique for bridging this always-present gap between the analysis artifacts and the code.
Object-Oriented Design Principle: Single ResponsibilityThe Single Responsibility Principle (or SRP) is a fundamental design principle that canhelp you produce a granular and reusable design that is resilient to change. SRP tellsyou that all members of the class should be similar and used to solve a single problem.This way, the motives for change will be much more homogenous and the impact ofchange will be localized.
DefinitionIn Agile Software Development: Principles, Patterns, and Practices (Prentice Hall, 2002),Robert C. Martin writes, “A class should have only one reason to change.”
Again, change is at the heart of this principle. While we cannot fight change in software,it is crucial that the process motivated by change is controlled and localized. The moreheterogeneous the members of the class, the more possible motives for change can befound. This can have huge consequences for application development. Once the respon-sibilities of a class become coupled, the effects of change become far-reaching and diffi-cult to control, resulting in an application that breaks unexpectedly in different places asa result of change.
Executable
ASP PageCOM+
COM+RelationalDatabase
DataLogicPresentation
294
Part IV: Advanced Refactorings
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 294
Continued
Imagine you have a Windows Form class that can calculate the length of a circle’s diame-ter. The class has both presentation and geometrical calculation code. Now imagine thatthe client needs to put this functionality on the web. For this, the presentation class youwill use will be some Active Server Page (ASP) class. Because the initial Windows Formclass has both presentation and calculation code, you’ll have to either completely rewriteyour Windows Form class or distribute unnecessary Windows Form code with an ASPapplication. Now, if your original application changes, you will also have to test the ASP application, distribute the Windows Form class to the ASP application that willnever make any use of this presentation code, version the class, and so on.
ExampleI’ll stick with the familiar subject for this example. Imagine you have programmed aVehicle class as follows:
Public Class VehiclePublic Sub HandOver(ByVal customer As Customer)
If Not Me.InMaintenance ThenMe.HandedOver = True
ElseThrow New InvalidVehicleOperationException( _“Cannot hand over vehicle. Vehicle in maintenance.”)
End IfEnd Sub
Public Sub Update()Dim connection As IDbConnection = _New SqlConnection(ConnectionString)connection.Open()Dim command As IDbCommand = _New SqlCommandCommand.Connection = connectionCommand.CommandText = Sql‘etc..
End Sub‘...
End Class
If you analyze this code, you will clearly see that the Vehicle class has more than one responsibility. It contains both rental business-related code, the HandOver method,and persistence-related code, the Update method. This has a number of possible consequences:
❑ If you change the persistence mechanism, the class has to change.
❑ If you change some business logic, the class has to change.
295
Chapter 11: Discovering Objects
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 295
In the next section, you are going to see another approach to modeling your system, this one more fit for modeling the data than for modeling your classes, but still practiced by many teams when they aredesigning Visual Basic applications.
❑ If one of the responsibilities of the class changes, you have no guaranteethat the other one still works correctly until you test that functionality also.
❑ If you would like to reuse the business-related code, you have to distributepersistence code in the same package and vice versa.
It is often an initial reflex to put persistence logic inside the domain class. After all, the data and the operations with the object go together and should not be separated.However, if you think about it carefully, the Update method is not really concernedwith Vehicle data. It needs this data to be able to persist vehicle instance, for sure,but it is really more concerned with the behavior of the database provider object.
It is clear now that this class should be split into two classes. One should contain only business-related logic and the other only persistence code. As a result, I’ll keepthe Vehicle class and leave the HandOver method inside it. I will create a newVehicleDataStore class and will move the Update method to this new class. The code that follows now obeys the SRP:
Public Class VehiclePublic Sub HandOver(ByVal customer As Customer)
If Not Me.InMaintenance ThenMe.HandedOver = True
ElseThrow New InvalidVehicleOperation( _“Cannot hand over vehicle. Vehicle in maintenance.”)
End IfEnd Sub‘...
End Class
Public Class VehicleDataStorePublic Sub Update(ByVal vehicle As Vehicle)
Dim connection As IDbConnection = _New SqlConnection(ConnectionString)connection.Open()Dim command As IDbCommand = _New SqlCommandcommand.Connection = connectioncommand.CommandText = Sql‘etc..
End Sub‘...
End Class
296
Part IV: Advanced Refactorings
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 296
Entities and RelationshipsThe entity-relationship model is a standard method for designing database structures. This model pres-ents a logical view of the data in which you think about the system in terms of of entities, their attributes,and the relationship between the two. Such a view is easily translated to a relational database design inwhich entities correspond to tables, attributes to columns, and relationships to relational constraints orrelation tables.
Database design is generally motivated by a desire for data retrieval and storage efficiency, data consis-tency, elimination of redundant data, and so on. Often the design is further refined through a normaliza-tion process. Such a process is guided by some mathematical rules that optimize data structuring, and itresembles the refactoring of object-oriented systems.
However, object-oriented design is a design guided by software design principles. You can often findredundant data in object-oriented systems, while this is undesirable from the database design viewpoint.You navigate through objects by following the relations; in databases, you join tables. You can groupobjects inside the collection, while in databases, you store rows inside the table. There are number of different principles that work in one technology, but are not applicable in the other.
This modeling technique can help you visualize some static relations, but the dynamics of the systemand object communication patterns will not be covered. Also, some complex relationships betweenobjects, like inheritance, will not be identified.
If in a project, general application design is driven by database design, you encounter the Database-Driven Design smell. In such cases, the design of classes and Visual Basic code is often neglected andsimply functions as database storage and retrieval. While this can be considered malpractice, it is oftenthe design approach taken by traditional Visual Basic development teams.
You have just seen what to do during the analysis phase. You have seen the techniques you have at yourdisposal to transform the analysis artifacts into the design of your classes. Very often, however, you willalready have at your disposal operational code with all the classes identified and structured already. Inthe next section, you’ll see how to deal with such code.
Object-Relational Impedance MismatchThis term refers to the problem of programs written in some object-oriented languageusing relational databases. The problem arises because the two technologies are notcompletely analogous. While at the first look, there is a correspondence betweenclasses and tables, databases are not capable of expressing more complex relationsbetween classes, like inheritance, that are present in object-oriented systems. As aresult, a lot of boilerplate code has to be produced in order for a database to be usedfor object storage. A number of products, ranging from object-oriented databases toobject-relation mapping tools, have been developed that can deal with this problem to a greater or lesser extent.
297
Chapter 11: Discovering Objects
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 297
Discovering Hidden ClassesIn the first section of this chapter, you saw some basic characteristics of object-oriented code. These char-acteristics should be taken into account when you design your system and write code for your classes. In the second section of this chapter, you saw how you can make a transition from analysis artifactstoward the design of your application.
At this point, you might be wondering what to do with already operational code that is not well structuredand that you sense has other classes that are not identified. In this section, I’ll deal with this very commonsituation: poorly structured code that has classes buried and hidden within it. A typical characteristic ofsuch code is that it does not comply with the SRP.
You will see how to identify overencumbered classes and learn what techniques you have at your dis-posal to refactor such code and distribute the responsibilities between classes in a more balanced way.You can start with a very common situation: code that communicates directly with a database withoutembodying the rules of the problem domain.
Smell: Large ClassDetecting the SmellA long list of properties and methods is the first sign of the Large Class smell, and theeasiest to detect. Another good indicator of a large class is a long and heterogeneousimport section. A large class is often accompanied by long methods.
Related RefactoringUse Extract Method, Extract Class, and Extract Superclass refactorings to eliminate this smell.
RationaleA large class is often a manifestation of some other, more specialized smell like Event-Handling Blindness, Database-Driven Design, or a similar problem caused by mixingpresentation and domain classes. Whatever the motive for its existence, it will suffer fromtoo many responsibilities. That means that the class can suffer change from many differentmotives and is a source of spiraling dependencies and cascading changes.
A large class is are difficult to read and understand. Sometimes only part of its attrib-utes have meaning, and some methods are used under specific circumstances, or itsexecution is conditioned.
Sometimes a large class appears and grows with time. As new functionalities arerequested, the easiest way to implement them is to add more code to existing classes.This is a sign of decaying design, in which not enough care is taken to refactor theapplication in time.
298
Part IV: Advanced Refactorings
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 298
Dealing with Database-Driven DesignA very common approach in database-driven development is to concentrate your design on the database.The data vocabulary is created, tables are designed with data integrity and normalization in mind, andindexes are created for optimal access.
After that, the rest of the application code is developed in sole function for the database. Often, in suchcases, applications are built in the form of a two-tiered architecture wherein the presentation layer is usedto communicate directly with the database, as a sort of data conductor, using the database provider APIdirectly in the presentation classes.
Continued
Refactoring: Replace Row with Data ClassMotivationAn application can be built in the form of a two-tiered architecture, wherein the pres-entation layer communicates directly with the database. Database access and someother APIs often use a row paradigm in the communication layer for retrieval of sets ofdata, such as the System.Data.DataRow class in the .NET Framework.
If such row objects are used, directly evading the use of a business (or domain) layer, thecode will most probably suffer from duplication and will be difficult to understand andmaintain. As a step toward more complete object-oriented design, you can replace rowswith a simple data class that has only properties. Each property should correspond to an item in a row. Such a class contains only data, a signal that the design is not completeand that you should complete it by moving related methods inside the class. You shouldlook at this only as a first step in the right direction: toward defining the domain layerfor your application.
Related SmellsUse this refactoring to eliminate Duplicated Code, Large Class, and Database-DrivenDesign smells and to make classes comply with the SRP.
MechanicsA need for defining the domain layer appears often in two-tiered applications thathave no domain layer. In such applications, presentation and other client layers com-municate directly with the database by means of a row paradigm.
1. Start by creating a class that will represent a row structure. One instance ofa class corresponds to one row.
2. For each item in the row, create a property in your class.
3. Instead of using the rows directly, make the code create instances of thedata class and return them to the client.
Before‘...Public Class AccountView
‘...
299
Chapter 11: Discovering Objects
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 299
Private Sub ViewAccountDetails_Click( _ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles ViewAccountDetails.Click
Dim connection As IDbConnection = _New SqlConnection(ConnectionString)Dim adapter As IDbDataAdapter = New SqlDataAdapterDim accountDataSet As New DataSetDim command As IDbCommand = New SqlCommandDim strSql As String = “Select * from Accounts “ + _“where Name = “ + Me.Number.Textconnection.Open()command.Connection = connectioncommand.CommandText = strSqladapter.SelectCommand = commandadapter.Fill(accountDataSet)connection.Close()accountTable = accountDataSet.Tables.Item(0)Dim accountRow As DataRow = _accountTable.Rows(0)‘Fill controls on the formMe.Name.Text = accountRow.Item(“Name”).ToStringMe.Type.Text = accountRow.Item(“Type”).ToStringIf Not CBool(accountRow.Item(“Blocked”)) Then
Me.Balance.Text = CDec(accountRow.Item(“Balance”))Else
Me.Balance.Text = “Blocked”End If
End SubEnd Class
AfterPublic Class AccountView
‘...Private Sub ViewAccountDetails_Click(ByVal sender AsSystem.Object, _
ByVal e As System.EventArgs) Handles ViewAccountDetails.ClickDim account As Account = GetAccount(Me.Number.Text)
Me.Name.Text = account.NameMe.Type.Text = account.Type
If Not account.Blocked ThenMe.Balance.Text = account.Balance
ElseMe.Balance.Text = “Blocked”
End IfEnd Sub
Private Function GetAccount(ByVal number As String) AsAccount
Dim connection As IDbConnection = _New SqlConnection(ConnectionString)Dim adapter As IDbDataAdapter = New SqlDataAdapterDim accountDataSet As New DataSet
300
Part IV: Advanced Refactorings
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 300
Continued
Dim command As IDbCommand = New SqlCommandDim strSql As String = “Select * from Accounts “ + _“where Name = “ + number
connection.Open()command.Connection = connectioncommand.CommandText = strSqladapter.SelectCommand = commandadapter.Fill(accountDataSet)connection.Close()accountTable = accountDataSet.Tables.Item(0)Dim accountRow As DataRow = _accountTable.Rows(selectedRow)Dim account As New Accountaccount.Number = accountRow.Item(“Number”).ToStringaccount.Name = accountRow.Item(“Name”).ToStringaccount.Type = accountRow.Item(“Type”).ToStringaccount.Balance = CDec(accountRow.Item(“Balance”))account.Blocked = CBool(accountRow.Item(“Blocked”))Return account
End Function‘...
End Class ‘New class defined to replace DataRowPublic Class Account
Private numberValue As StringPrivate nameValue As StringPrivate typeValue As AccountTypePrivate balanceValue As DecimalPublic Property Number() As String
GetReturn numberValue
End GetSet(ByVal value As String)
numberValue = valueEnd Set
End PropertyPublic Property Name() As String
GetReturn nameValue
End GetSet(ByVal value As String)
nameValue = valueEnd Set
End PropertyPublic Property Type() As AccountType
GetReturn typeValue
End GetSet(ByVal value As AccountType)
typeValue = valueEnd Set
301
Chapter 11: Discovering Objects
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 301
Another place to look for hidden objects is in code written in a procedural style. As you probably know,it has taken software science a few decades to move from procedural to object-oriented programming,but in the next section, you will do it in whirlwind fashion.
Moving From Procedural to Object-Oriented DesignAs I have already mentioned, it is possible to write Visual Basic .NET programs without ever coding a classor instantiating an object. The Module construct is a remnant of VB’s procedural origins. Within Moduleyou are free to write global functions and data. Another place for doing things procedurally is in shared(class) methods. You can invoke these methods without creating an instance of a class they belong to. Stillanother sign of procedural design is data without behavior. In VB, such data can be found inside a class orstructure that has only fields and simple properties declared that do nothing other than return the values ofprivate fields.
As I have already mentioned a few times, data can feel lonely without behavior, as can behavior withoutdata. It is only natural to try to bring them together. I will try to illustrate the mechanics with an example.Chapter 9 has an excellent example of procedural style, the CircleCircumferenceLength example. Inthis chapter, you can start just where you left off, at Listing 9-9 in that chapter.
Smell: Procedural DesignDetecting the SmellThis smell is easily detected because of the excessive presence of VB module–level meth-ods and fields. Shared type members have a similar effect to module-level members, sothe excessive use of shared members can also be a sign of procedural design.
Another sign of procedural design is classes or structures comprising properties andno methods (Data Class smell): a type having data but no behavior. Since behavior is not part of the type, it is most probably implemented in the form of shared or VB module functions.
Related RefactoringUse Convert Procedural Design to Objects refactoring to eliminate this smell.
RationaleProcedural design is often a remnant of old styles and practices. When using proceduraldesign, you cannot benefit from inheritance, polymorphism, higher-level encapsulation,or a host of other benefits object-oriented design can bring to your code.
End PropertyPublic Property Balance() As Decimal
GetReturn balanceValue
End GetSet(ByVal value As Decimal)
balanceValue = valueEnd Set
End PropertyEnd Class
302
Part IV: Advanced Refactorings
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 302
The first thing that you can observe is that two methods, CalculateRadius andCalculateCircumferenceLength, have the same parameter list: center and pointOnCircumferencepoints. In this example, these two points are used to define the circle. Because they define a single circle,they should be used as properties of that object. So you can add to your solution a class Circle with twoproperties of type Point: Center and PointOnCircumference. See Listing 11-1.
Listing 11-1: Circle Class Creation
Public Class CirclePrivate centerValue As PointPrivate pointOnCircumferenceValue As PointPublic Property Center() As Point
GetReturn centerValue
End GetSet(ByVal value As Point)
centerValue = valueEnd Set
End PropertyPublic Property PointOnCircumference() As Point
GetReturn pointOnCircumferenceValue
End GetSet(ByVal value As Point)
pointOnCircumferenceValue = valueEnd Set
End PropertyEnd Class
This still a simple data class, but don’t worry — this is just the beginning. Now you should make thecode use this class instead of passing circle points as unrelated objects. Take a look at the Module usingthe Circle class in Listing 11-2.
Listing 11-2: CircleCircumferenceLength After the Circle Class Is Introduced
Module CircleCircumferenceLength
Sub Main()‘Create circle instance and use it to hold circle pointsDim circle As Circle = New Circlecircle.Center = InputPoint(“circle center”)circle.PointOnCircumference = InputPoint(“point on circumference”)Console.WriteLine(“The length of circle “ + _“circumference is:”)Console.WriteLine(CalculateCircumferenceLength(circle))WaitForUserToClose()
End Sub
‘Change the method signature so it uses circle parameter instead of ‘two unrelated points Public Function CalculateCircumferenceLength( _ByVal circle As Circle) As Double
Return 2 * 3.1415 * CalculateRadius(circle)Continued
303
Chapter 11: Discovering Objects
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 303
Listing 11-2: CircleCircumferenceLength After the Circle Class Is Introduced (continued)
End Function
‘Change the method signature so it uses circle parameter Public Function CalculateRadius(ByVal circle As Circle) As Double
Return ((circle.PointOnCircumference.X - circle.Center.X) ^ 2 + _(circle.PointOnCircumference.Y - circle.Center.Y) ^ 2) ^ (1 / 2)
End Function
Public Function InputPoint(ByVal pointName As String) As PointDim point As PointConsole.WriteLine(“Enter X coordinate “ + _“of “ + pointName)point.X = CDbl(Console.In.ReadLine())Console.WriteLine(“Enter Y coordinate “ + _“of “ + pointName)point.Y = CDbl(Console.In.ReadLine())Return point
End Function
Private Sub WaitForUserToClose()Console.Read()
End Sub
End Module
All you have to do now is look for behavior that you can move inside the Circle class. There are twomethods in a module that receive all the data they use as a circle parameter. It would mean less writingif these methods could access the data in the form of instance data instead of as parameters. If you movethese methods inside the Circle class, they can do just that.
To accomplish this, just copy the methods into the Circle class. Then erase the parameter declarationand replace the circle parameter name with the Me keyword if some of the values from the circle are referenced. If the parameter is only referenced directly, simply erase it. The resulting code is shown inListing 11-3.
Listing 11-3: Circle After Move Method Refactoring
Public Class CirclePrivate centerValue As PointPrivate pointOnCircumferenceValue As PointPublic Property Center() As Point
GetReturn centerValue
End GetSet(ByVal value As Point)
centerValue = valueEnd Set
End PropertyPublic Property PointOnCircumference() As Point
Get
304
Part IV: Advanced Refactorings
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 304
Listing 11-3: Circle After Move Method Refactoring (continued)
Return pointOnCircumferenceValueEnd GetSet(ByVal value As Point)
pointOnCircumferenceValue = valueEnd Set
End Property
‘CalculateCircumferenceLength needs no parametersPublic Function CalculateCircumferenceLength() As Double
‘No need to pass circle to CalculateRadius, since ‘CalculateRadius can access all the circle’s propertiesReturn 2 * 3.1415 * CalculateRadius()
End Function
‘CalculateRadius needs no parametersPublic Function CalculateRadius() As Double
Return ((Me.PointOnCircumference.X - Me.Center.X) ^ 2 + _(Me.PointOnCircumference.Y - Me.Center.Y) ^ 2) ^ (1 / 2)
End FunctionEnd Class
All that is left is to make Module use Circle methods instead of its own. This is easily accomplished. AllModule has to do is call the method on the Circle instance. Listing 11-4 shows the final version of the code.
Listing 11-4: CircleCircumferenceLength Calculation Code Converted to Object Design
Option Explicit OnOption Strict On
Namespace RefactoringInVb.Chapter11
Public Structure PointPublic X As DoublePublic Y As Double
End Structure
Module CircleCircumferenceLength
Sub Main()Dim circle As Circle = New Circlecircle.Center = InputPoint(“circle center”)circle.PointOnCircumference = InputPoint(“point on circumference”)Console.WriteLine(“The length of circle “ + _“circumference is:”)Console.WriteLine(circle.CalculateCircumferenceLength())WaitForUserToClose()
End Sub
Public Function InputPoint(ByVal pointName As String) As PointDim point As PointConsole.WriteLine(“Enter X coordinate “ + _
Continued
305
Chapter 11: Discovering Objects
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 305
Listing 11-4: CircleCircumferenceLength Calculation Code Converted to Object Design (continued)
“of “ + pointName)point.X = CDbl(Console.In.ReadLine())Console.WriteLine(“Enter Y coordinate “ + _“of “ + pointName)point.Y = CDbl(Console.In.ReadLine())Return point
End Function
Private Sub WaitForUserToClose()Console.Read()
End Sub
End Module
Public Class CirclePrivate centerValue As PointPrivate pointOnCircumferenceValue As PointPublic Property Center() As Point
GetReturn centerValue
End GetSet(ByVal value As Point)
centerValue = valueEnd Set
End PropertyPublic Property PointOnCircumference() As Point
GetReturn pointOnCircumferenceValue
End GetSet(ByVal value As Point)
pointOnCircumferenceValue = valueEnd Set
End Property
Public Function CalculateCircumferenceLength() As DoubleReturn 2 * 3.1415 * CalculateRadius()
End Function
Private Function CalculateRadius() As DoubleReturn ((Me.PointOnCircumference.X - Me.Center.X) ^ 2 + _(Me.PointOnCircumference.Y - Me.Center.Y) ^ 2) ^ (1 / 2)
End FunctionEnd Class
End Namespace
Here are a couple more interesting items to note in this refactoring.
❑ Because there is no need to access the CalculateRadius method from the outside, you canhide the method by reducing its visibility to private. By doing this, you can witness how withobjects, encapsulation works on another level. The client doesn’t need to know that to calculatelength, you first need to calculate radius.
306
Part IV: Advanced Refactorings
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 306
❑ Also, if you take a look at the Circle class, you will see no data input code inside it. In the examplemodule, you used the Console interface to enable the user to communicate with the application. If you now decide you would prefer to implement a Windows Form interface, you can easily reusethe Circle class. This is possible because domain is separated from presentation, which is the sub-ject of our next section.
Continued
Refactoring: Convert Procedural Design to ObjectsMotivationThe motivation for this refactoring comes from advantages that the object-oriented para-digm provides compared with procedural programming. Object-oriented programmingis a superior, more robust, and more productive paradigm that supersedes proceduralprogramming.
Related SmellsUse this refactoring to eliminate Procedural Design and Data Class smells.
Mechanics1. If you do not have data structures in your code (you are using a DataRow,
for example), start by creating data classes from rows by means of ReplaceRow with Data Class refactoring. If you have data clumps (data that gener-ally goes together but is not structured — for example, repeatable parame-ter lists), start by grouping them in the form of data classes.
2. Move all your procedural code into the single module. Remember, all meth-ods in a module are shared by definition, so you don’t have to use the Sharedkeyword when declaring the methods in a module. This will be handy whenyou move methods to a class.
3. Perform Extract Method on all the long methods in a module to makethem more granular and easier to use.
4. For each method, look for a data class that methods operated on. Thisprobably means that the instance of the data class is passed to a method as a parameter. Move the method to a data class and make it use the dataclass property directly as an instance member, instead of passing it as a parameter.
5. Continue until you move all the methods into classes. Finally, delete themodule from the solution.
BeforePublic Structure ShoppingCart
Public Products As IListEnd Structure
Public Module ShoppingCartFunctionsPublic Function CalculateTotal( _ByVal cart As ShoppingCart) As Decimal
Dim total As Decimal
307
Chapter 11: Discovering Objects
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 307
Keeping Domain, Presentation, and Persistence ApartYou have certainly experienced how Visual Basic’s Rapid Application Development (RAD) features canentice you to confront the problems head-on, coding your way out right from the outset. It is great thatyou can fire up the VB IDE and just fill in the event handlers, but that can also often be a misleading,double-edged advantage that can lead you towards coupled, non-reusable code.
This situation is well known to veteran VB programmers. When the first versions of Visual Basic appeared,the prevailing architectural pattern was two-tiered, a pattern also known as client-server. Relational databasesprovided robust backends, and VB was a tool that permitted a very fast application development. Thencame the Web. Enterprises wished to put their businesses on the Internet. Unfortunately, domain logic cou-pled with presentation inside event-handling routines proved very difficult to reuse, spawning a new archi-tectural style known as three-tiered architecture. Figure 11-5 illustrates three-tier application separation.
Figure 11-5
Executable
ASP PageCOM+
COM+RelationalDatabase
DataLogicPresentation
For Each product As Product In cart.Productstotal += product.Price
NextReturn total
End FunctionEnd Module
AfterPublic Class ShoppingCart
Public Products As IList
Public Function CalculateTotal() As DecimalDim total As DecimalFor Each product As Product In Products
total += product.PriceNextReturn total
End FunctionEnd Class
308
Part IV: Advanced Refactorings
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 308
This separation is often logical and physical, meaning that different tiers reside on different machines.However, as long as the logical separation is correctly performed and the classes are well-structured andorganized, the physical distribution is a less complicated issue.
The tier separation does not end there. Inside the Logic tier, you can have further layering and separationbetween purely domain logic and between persistence code and presentation VB code.
When you have to deal with code in which tier separation is not correctly performed, the best way to startis by separating the domain from the presentation code.
Separating Domain from Presentation CodeThis large-scale refactoring is performed in a few steps. Let’s continue the refactoring example using theAccountView and Account classes from the “Refactoring: Replace Row with Data Class” sidebar earlierin the chapter.
Defining a Data Class for the Domain The first step in separating the domain from the presentation is often Replace Row with Data Classrefactoring. By creating data classes based on table rows, you can often identify the core of the classesthat will comprise your domain layer. This step was already performed in the example shown in the“Refactoring: Replace Row with Data Class” sidebar.
Moving Domain Logic Inside the Data ClassIf you analyze the original AccountView code, you will be able to identify one business rule codedinside an event handler:
If Not account.Blocked ThenMe.Balance.Text = account.Balance
ElseMe.Balance.Text = “Blocked”
End If
You can interpret this rule as follows: “If the account is blocked, hide the account balance.” You willmove this rule to the Account class. For that, you need to modify the Balance property as follows:
Public Property Balance() As DecimalGet
‘use getter to control the Balance accessIf Me.Blocked = True Then
Return 0Else
Return balanceValueEnd If
End GetSet(ByVal value As Decimal)
balanceValue = valueEnd Set
End Property
This way, the balance will stay hidden for anyone using the Account class.
309
Chapter 11: Discovering Objects
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 309
Moving Persistence Logic Inside the Data ClassYou need to keep the presentation layer free of all non-presentation code. For that purpose, you will moveall persistence code to the domain class.
In this example, the GetAccount method is used to read the account data from the database. This methodcan be moved into the Account class and converted into a parameterized constructor. The code for theconstructor looks like this:
Public Sub New(ByVal number As String)Me.Number = numberDim connection As IDbConnection = _New SqlConnection(ConnectionString)Dim adapter As IDbDataAdapter = New SqlDataAdapterDim accountDataSet As New DataSetDim command As IDbCommand = New SqlCommandDim strSql As String = “Select * from Accounts “ + _“where Name = “ + Me.Numberconnection.Open()command.Connection = connectioncommand.CommandText = strSqladapter.SelectCommand = commandadapter.Fill(accountDataSet)connection.Close()accountTable = accountDataSet.Tables.Item(0)Dim accountRow As DataRow = _accountTable.Rows(0)Dim account As New AccountMe.Name = accountRow.Item(“Name”).ToStringMe.Type = New AccountType(accountRow.Item(“Type”).ToString)Me.Balance = CDec(accountRow.Item(“Balance”))Me.Blocked = CBool(accountRow.Item(“Blocked”))
End Sub
Now all the code you have left inside the AccountView class is purely presentation-related. The originalevent handler now looks like this:
Private Sub ViewAccountDetails_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles ViewAccountDetails.Click
Dim account As Account = New Account(Me.Number.Text)Me.Name.Text = account.NameMe.Type.Text = account.TypeIf Not account.Blocked Then
Me.Balance.Text = account.BalanceElse
Me.Balance.Text = “Blocked”End If
End Sub
You have successfully separated persistence from domain code. However, your work is not over yet.Some persistence-related code exists inside your domain class, and such code does not comply with theSRP. You need to move the persistence-related code to a separate class.
310
Part IV: Advanced Refactorings
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 310
Separating Domain from Persistence CodeYou often find designs in which persistence classes contain database access code. In these cases if youneed to change the database, or the persistence method, the changes will affect your domain classes aswell. To avoid this situation, the solution is to extract database code to separate classes. You should startout by defining one data access class for each persisted domain object. Then you should extract methodsand move database access code to newly defined data access classes.
In this example, you will define a new AccountData class. You will move the persistence code from the con-structor to the newly created class. This method can be called GetAccount. Moving this method finishes thetransformation of this example. Take a look at the final version in Listing 11-5.
Listing 11-5: AccountView Example After Separation of Domain and Persistence Code
Option Explicit OnOption Strict On
Public Class ViewAccount‘...Private Sub ViewAccountDetails_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles ViewAccountDetails.Click
Dim account As Account = New Account(Me.Number.Text)Me.Name.Text = account.NameMe.Type.Text = account.TypeIf Not account.Blocked Then
Me.Balance.Text = account.BalanceElse
Me.Balance.Text = “Blocked”End If
End Sub‘...
End Class
Public Class AccountPrivate numberValue As StringPrivate nameValue As StringPrivate typeValue As AccountTypePrivate balanceValue As DecimalPrivate blockedValue As Boolean
Public Sub New(ByVal number As String, _ByVal name As String, ByVal type As AccountType, _ByVal balance As Decimal, ByVal blocked As Boolean)
Me.Number = numberMe.Name = nameMe.Type = typeMe.Balance = balanceMe.Blocked = blocked
End Sub
Public Property Blocked() As BooleanGet
Return blockedValueContinued
311
Chapter 11: Discovering Objects
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 311
Listing 11-5: AccountView Example After Separation of Domain and Persistence Code (continued)
End GetSet(ByVal value As Boolean)
blockedValue = valueEnd Set
End PropertyPublic Property Number() As String
GetReturn numberValue
End GetSet(ByVal value As String)
numberValue = valueEnd Set
End PropertyPublic Property Name() As String
GetReturn nameValue
End GetSet(ByVal value As String)
nameValue = valueEnd Set
End PropertyPublic Property Type() As AccountType
GetReturn typeValue
End GetSet(ByVal value As AccountType)
typeValue = valueEnd Set
End PropertyPublic Property Balance() As Decimal
GetIf Me.Blocked = True Then
Return 0Else
Return balanceValueEnd If
End GetSet(ByVal value As Decimal)
balanceValue = valueEnd Set
End PropertyEnd Class
Public Class AccountData‘...Public Function GetAccount(ByVal number As String) As Account
Dim connection As IDbConnection = _New SqlConnection(ConnectionString)Dim adapter As IDbDataAdapter = New SqlDataAdapterDim accountDataSet As New DataSet
312
Part IV: Advanced Refactorings
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 312
Listing 11-5: AccountView Example After Separation of Domain and Persistence Code (continued)
Dim command As IDbCommand = New SqlCommandDim strSql As String = “Select * from Accounts “ + _“where Name = “ + numberconnection.Open()command.Connection = connectioncommand.CommandText = strSqladapter.SelectCommand = commandadapter.Fill(accountDataSet)connection.Close()accountTable = accountDataSet.Tables.Item(0)Dim accountRow As DataRow = _accountTable.Rows(0)Dim account As New Accountaccount.Number = accountRow.Item(“Number”).ToStringaccount.Name = accountRow.Item(“Name”).ToStringaccount.Type = accountRow.Item(“Type”).ToStringaccount.Balance = CDec(accountRow.Item(“Balance”))account.Blocked = CBool(accountRow.Item(“Blocked”))Return account
End FunctionEnd Class
Sometimes, the code you deal with is so simple that you can extract the domain and persistence layer inparallel. However, your approach depends on your needs:
❑ If you need to free the domain layer from the presentation, start by moving domain and persistencecode out of the presentation.
❑ If you need to liberate the persistence layer, start by forming this layer first.
❑ If your code is simple enough, you can try to do refactorings in parallel.
You’ve covered many significant refactorings in this chapter so far. Now it’s time to turn back to theRent-a-Wheels application and apply that new knowledge to it.
Discovering Objects and the Rent-a-Wheels ApplicationThis content of this chapter promises to introduce some fundamental changes to the Rent-a-Wheelsapplication. On close inspection, you can see that Rent-a-Wheels suffers almost all the maladies youhave encountered in this chapter: large classes; mixed presentation, domain, and persistence code;encumbered classes that are not compliant with SRP; and so on. You need to take following transforma-tions one step at a time. Fortunately, you already know your first step.
Applying Replace Row with Data ClassYou start with the refactoring that is a good starting point in cases like this: Replace Row with DataClass. By looking at the BranchMaintenance form, you can see that you need to declare the Branchclass. Take a look at Listing 11-6 for the Branch class code.
313
Chapter 11: Discovering Objects
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 313
Listing 11-6: Newly Defined Branch Data Class
Option Explicit OnOption Strict On
Public Class Branch
Public Sub New(ByVal id As Integer, ByVal name As String)idValue = idnameValue = name
End Sub
Private idValue As IntegerPrivate nameValue As String
Public Property Id() As IntegerGet
Return idValueEnd GetSet(ByVal value As Integer)
idValue = valueEnd Set
End Property
Public Property Name() As StringGet
Return nameValueEnd GetSet(ByVal value As String)
nameValue = valueEnd Set
End PropertyEnd Class
The Branch class has two properties: a database-generated Id and Name. Also, a convenient parameter-ized constructor is defined so that the class can be instantiated and initialized with data on the same line.
Now, instead of operating directly with DataRow objects in the BranchMaintenance form, you use therow to initialize the instance of the Branch class. You then display the branch object data in the form.Take a look at an example of the use of the branch object in the DisplayCurrentObject method inListing 11-7.
Listing 11-7: Displaying the branch Object in GUI
‘Replaces DisplayCurrentRow method Private Sub DisplayCurrentObject()
Dim branch As Branch = _CType(branches.Item(currentRowIndex), RentAWheel.Branch)Me.Id.Text = branch.Id.ToStringMe.BranchName.Text = branch.Name
End Sub
314
Part IV: Advanced Refactorings
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 314
You need to take a similar approach with the rest of the methods that communicate with the database. Youwill make the rest of the methods use the Branch data class. After that, you need to take care of domainand persistence, and you can start by moving the domain logic into the data class.
Separating Domain from PresentationIn Chapter 4, which introduced the Rent-a-Wheels application, you saw a number of details of vehicleoperation and vehicle state: for example, how the vehicle is from time to time sent to maintenance andhow in that period the vehicle is, as you would expect, taken out of circulation.
In the existing Rent-a-Wheels application, you implement rules regarding vehicle states by manipulat-ing controls on the FleetView form. If the vehicle is in maintenance, then the Hand Over button is dis-abled. This rule belongs to the domain layer, and you need to move this logic inside the Vehicle class.In Listing 11-8 you can see how this is accomplished.
Listing 11-8: Vehicle Data Class Becomes Domain Class
Public Sub HandOver()If Me.Operational Then
Me.RentalState = VehicleRentalState.HandedOverElse
Throw New InvalidVehicleStateException( _“Vehicle in maintenance. Cannot be handed over”)
End IfEnd Sub
Implementation of the maintenance-related rule is simple — if someone tries to hand over a nonopera-tional vehicle, an exception is thrown.
Next, it is time to deal with persistence-related code.
Separating Persistence Code from PresentationTo extract the persistence code from the presentation classes, you define a data access class for each dataclass you have defined. So for the Branch class, you define a new BranchData class. This class holds allthe persistence-related code.
You should start by moving the simplest method, in this case the Delete method, to BranchData. As youmove this method, you have to move the rest of the members this method is referencing. For now, just copythe necessary methods into the BranchData class. As you progress, more and more persistence-related codewill find its way into the BranchData. Once everything is finished, you can erase all the unused persistence-related methods in the BranchMaintenance class. In Listing 11-9, you can see the final result for the branch-related code.
Listing 11-9: Branch-Related Classes: BranchMaintenance, Branch, and BranchData
Option Explicit OnOption Strict On
Imports System.DataImports System.Data.SqlClient
Continued
315
Chapter 11: Discovering Objects
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 315
Listing 11-9: Branch-Related Classes: BranchMaintenance, Branch, and BranchData (continued)
Imports System.CollectionsImports RentAWheel.Data.ColumnNames
Public Class BranchMaintenancePrivate branches As IList = New ArrayListPrivate currentRowIndex As Integer
Dim data As BranchData = New BranchData
Private Sub BranchMaintenance_Load(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles MyBase.Load
LoadBranches()If (Me.branches.Count > 0) Then
currentRowIndex = 0DisplayCurrentObject()
End IfEnd Sub
Private Sub LoadBranches()Me.branches = data.GetAll()
End Sub
Private Sub RightItem_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles RightItem.Click
If (branches.Count > currentRowIndex + 1) ThencurrentRowIndex += 1DisplayCurrentObject()
End IfEnd Sub
Private Sub LeftItem_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles LeftItem.Click
If (currentRowIndex - 1 >= 0 And branches.Count > 0) ThencurrentRowIndex -= 1DisplayCurrentObject()
End IfEnd Sub
Private Sub FirstItem_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles FirstItem.Click
If (branches.Count > 0) ThencurrentRowIndex = 0DisplayCurrentObject()
End IfEnd Sub
Private Sub LastItem_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles LastItem.Click
If (branches.Count > 0) ThencurrentRowIndex = branches.Count - 1
316
Part IV: Advanced Refactorings
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 316
Listing 11-9: Branch-Related Classes: BranchMaintenance, Branch, and BranchData (continued)
DisplayCurrentObject()End If
End Sub
Private Sub DisplayCurrentObject()Dim branch As Branch = _CType(branches.Item(currentRowIndex), RentAWheel.Branch)Me.Id.Text = branch.Id.ToStringMe.BranchName.Text = branch.Name
End Sub
Private Sub NewItem_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles NewItem.Click
Me.Id.Text = “”BranchName.Text = “”
End Sub
Private Sub Save_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles Save.Click
SaveBranch()BranchMaintenance_Load(Nothing, Nothing)
End Sub
Private Sub SaveBranch()If (Me.Id.Text.Equals(“”)) Then
Dim branch As Branch = New Branch(Nothing, _Me.BranchName.Text.ToString)data.Insert(branch)
ElseDim branch As Branch = New Branch(CInt(Me.Id.Text.ToString), _Me.BranchName.Text.ToString)data.Update(branch)
End IfEnd Sub
Private Sub Delete_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles Delete.Click
Dim branch As New Branch(CInt(Me.Id.Text.ToString), _Me.BranchName.Text.ToString)
data.Delete(branch)BranchMaintenance_Load(Nothing, Nothing)
End Sub
Private Sub Reload_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles Reload.Click
BranchMaintenance_Load(Nothing, Nothing)End Sub
End Class
Public Class BranchContinued
317
Chapter 11: Discovering Objects
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 317
Listing 11-9: Branch-Related Classes: BranchMaintenance, Branch, and BranchData (continued)
Public Sub New(ByVal id As Integer, ByVal name As String)idValue = idnameValue = name
End Sub
Private idValue As IntegerPrivate nameValue As String
Public Property Id() As IntegerGet
Return idValueEnd GetSet(ByVal value As Integer)
idValue = valueEnd Set
End Property
Public Property Name() As StringGet
Return nameValueEnd GetSet(ByVal value As String)
nameValue = valueEnd Set
End PropertyEnd Class
Public Class BranchData
Public Const ConnectionString As String = _“Data Source=R60;Initial Catalog=RENTAWHEELS;” + _“User ID=RENTAWHEELS_LOGIN;Password=RENTAWHEELS_PASSWORD_123”
Private Const SelectAllFromBranchSql As String = _“Select * from Branch”Private Const DeleteBranchSql As String = _“Delete Branch Where BranchId = @Id”Private Const InsertBranchSql As String = _“Insert Into Branch (BranchName) Values(@Name)“Private Const UpdateBranchSql As String = _“Update Branch Set BranchName = @Name Where BranchId = @Id”
Private Const IdParamterName As String = “@Id”Private Const NameParameterName As String = “@Name”
Public Sub Delete(ByVal branch As Branch)Dim command As IDbCommand = New SqlCommandAddParameter(command, IdParamterName, DbType.Int32, _
318
Part IV: Advanced Refactorings
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 318
Listing 11-9: Branch-Related Classes: BranchMaintenance, Branch, and BranchData (continued)
branch.Id)ExecuteNonQuery(command, DeleteBranchSql)
End Sub
Public Sub ExecuteNonQuery(ByVal command As IDbCommand, _ByVal sql As String)
Dim connection As IDbConnection = PrepareDataObjects(command, sql)command.ExecuteNonQuery()connection.Close()
End Sub
Private Function PrepareDataObjects(ByVal command As IDbCommand, _ByVal sql As String) As IDbConnection
Dim connection As IDbConnection = _New SqlConnection(ConnectionString)connection.Open()command.Connection = connectioncommand.CommandText = sqlReturn connection
End Function
Private Sub AddParameter(ByVal command As IDbCommand, _ByVal parameterName As String, ByVal parameterType As DbType, _ByVal paramaterValue As Object)
Dim parameter As IDbDataParameter = command.CreateParameter()parameter.ParameterName = parameterNameparameter.DbType = parameterTypeparameter.Value = paramaterValuecommand.Parameters.Add(parameter)
End Sub
Public Sub Insert(ByVal branch As Branch)Dim command As IDbCommand = New SqlCommandAddParameter(command, NameParameterName, DbType.String, _branch.Name)ExecuteNonQuery(command, InsertBranchSql)
End Sub
Public Sub Update(ByVal branch As Branch)Dim command As IDbCommand = New SqlCommandAddParameter(command, NameParameterName, DbType.String, _Branch.Name)AddParameter(command, IdParamterName, DbType.Int32, _Branch.Id)ExecuteNonQuery(command, UpdateBranchSql)
End Sub
Public Function GetAll() As IListDim command As IDbCommand = New SqlCommandDim branchesSet As DataSet = FillDataset(command, SelectAllFromBranchSql)
Continued
319
Chapter 11: Discovering Objects
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 319
Listing 11-9: Branch-Related Classes: BranchMaintenance, Branch, and BranchData (continued)
Dim table As DataTable = branchesSet.Tables(0)Dim branches As IList = New ArrayListFor Each row As DataRow In table.Rows
branches.Add(New Branch(CInt(row.Item(BranchTable.Id)), _row.Item(BranchTable.Name).ToString))
NextReturn branches
End Function
Private Function FillDataset(ByVal command As IDbCommand, _ByVal sql As String) As DataSet
Dim connection As IDbConnection = PrepareDataObjects(command, sql)Dim adapter As IDbDataAdapter = New SqlDataAdapterDim branches As New DataSetadapter.SelectCommand = commandadapter.Fill(branches)connection.Close()Return branches
End FunctionEnd Class
This code looks much better. The classes are not encumbered with more responsibilities than necessary,so they do not violate the SRP; presentation, domain, and persistence code are clearly separated; and thesize of the classes is inside the normal limits.
When you try to run the sample code for the chapter, be sure to execute the database modification scriptincluded in the download code. I have renamed a few table columns in order to make the SQL code morecompact. All the code for this book is available at the book’s web site at www.wrox.com.
A very similar pattern is applied to the rest of the Rent-a-Wheels classes. If you take a look at thoseclasses, you will see that many methods between the classes look exactly the same. You have alreadydone a lot of work on eliminating duplication from the code, but it seems that there is still some work to be done. The next chapter will deal with that, too. There you will see how code duplicated betweenclasses can be extracted and duplication eliminated.
SummaryYou covered a lot of ground in this chapter. If someone asked me to select the most important chapter in thebook, I might well name this one. You started by looking at an overview of object-oriented programmingconcepts like encapsulation, object state and identity, object lifetime, garbage collection, and messages.
After looking at objects from a theoretical point of view, you saw analysis techniques you can apply inorder to identify the classes that can be the pillars of your application design.
320
Part IV: Advanced Refactorings
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 320
When you program, you don’t always start out from zero. Very often, as a matter of fact, you need tomanipulate already existing code. In VB code, presentation, domain, and persistence code are mixedvery often. You have seen how you can extract the relevant classes and separate the code. You can oftenstart out by replacing rows with data classes.
Finally, you have once again applied your knowledge to the Rent-a-Wheels application, provoking pro-found changes to its code. Many new classes have been identified, and the code is more evenly spreadout, so that each class takes some responsibility.
In the next chapter, you will encounter some other important characteristics of object-oriented program-ming. You will see how techniques like inheritance, polymorphism, and genericity can be further used toconstruct robust and reusable code.
321
Chapter 11: Discovering Objects
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 321
79796c11.qxd:WroxPro 2/25/08 9:04 AM Page 322
Advanced Object-OrientedConcepts and Related
Refactorings
In the previous chapter, you explored some of the basic concepts in object-oriented programmingtheory. In this chapter, you will work with some more advanced, but by no means optional, charac-teristics of object-oriented programming environments. These are the characteristics that put VisualBasic in the same league as other modern programming languages like C# or Java.
This chapter progresses as follows:
❑ First, you will see how you can employ inheritance, polymorphism, and genericity to furtherimprove the design of your code, remove unnecessary duplication, and enhance encapsula-tion. These capabilities of Visual Basic are fundamental for implementing advanced reusabil-ity mechanisms and for the creation of frameworks and toolkit libraries.
❑ As you have seen many times in this book already, you won’t often reach optimal designduring the first iteration of code. So in the second part of this chapter, you will see howyou can refactor your code by extracting classes and interfaces, moving methods up anddown in the hierarchy, employing generic types, and so on. This way, you will make use of these advanced object-oriented capabilities and thus take the design of your code to thenext level.
❑ At the end of the chapter, you will get back to the Rent-a-Wheels application. You will seehow the number of classes in the application has increased substantially, but the overallcomplexity of the application and even the line count have been reduced.
❑ I will start this chapter by going back to some of the key concepts of object-oriented theory.
79796c12.qxd:WroxPro 2/23/08 8:38 AM Page 323
Inheritance, Polymorphism, and GenericityIt is fair to say that today, Visual Basic lacks no important object-oriented capabilities that other modernobject-oriented languages have. However, pre-.NET versions of Visual Basic contained only interface inheri-tance (I will explain the difference between interface and class inheritance soon enough), making VisualBasic fall into the category of object-based languages and fall short of reaching the object-oriented distinc-tion. This difference is by no means trivial, because implementation inheritance can help you remove impor-tant quantities of duplicated code and enforce desired behavior in inheritance hierarchies. Once VB .NETarrived, the issue was finally settled with the addition of the Inherits keyword to the language.
With the advent of Visual Basic 2005 (VB 8.0), other important features like generics and operator over-loading have been added to the language. This way, Visual Basic keeps evolving and keeping up withother languages as a first-level choice for programming in .NET.
This evolution continues with VB 2008, which adds to the language important features like LINQ, XMLLiterals, Lambda Expressions, Type Inference, Extension Methods, and others.
InheritanceYou have already seen on quite a few occasions refactorings that can be used to reduce code duplication. Youhave seen the devastating consequences that duplicated code can have on the maintainability of your codebase. Until now, though, you haven’t seen any technique that can solve duplication spread between differentclasses. How can you solve a situation in which the same members exist in more than one class?
Imagine you have programmed the class named Customer for a local opera house. This class has typicalproperties and methods like Name, Address, Telephone, PurchaseHistory, and so on. It also has aDiscountPercent method that takes into account the total number of purchases made.
Option Explicit OnOption Strict On
Public Class Customer‘...
Private purchaseHistoryValue As PurchaseHistoryPublic Property PurchaseHistory() As PurchaseHistory
GetReturn purchaseHistoryValue
End GetSet(ByVal value As PurchaseHistory)
purchaseHistoryValue = valueEnd Set
End PropertyPublic Function DiscountPercent() As Decimal
If Me.PurchaseHistory.TotalPurchases > 10 ThenReturn 3
ElseIf Me.PurchaseHistory.TotalPurchases > 100 ThenReturn 5
ElseReturn 0
End IfEnd Function
End Class
324
Part IV: Advanced Refactorings
79796c12.qxd:WroxPro 2/23/08 8:38 AM Page 324
As usual, very soon you have to add some new functionality. Opera management has decided that all senior citizens get a 50 percent discount for all their purchases. At this point, you remember the Open-Closed principle discussed in Chapter 8, and you decide you will not modify the existing Customer class.Instead, you decide to define a new SeniorCitizen class. And you obviously do not want to copy all thecontent of the Customer class into SeniorCitizen; you want to change only the single method.
One option is to declare the Customer class as a property of SeniorCitizen. Then you can delegate allcalls to the already existing Customer members to the Customer property. The only method that containssome new logic is DiscountPercent. Such code would look like this:
Option Explicit OnOption Strict On
Public Class SeniorCitizenPrivate customerValue As CustomerPublic Property Customer() As Customer
GetReturn customerValue
End GetSet(ByVal value As Customer)
customerValue = valueEnd Set
End PropertyPublic Property FirstName() As String
GetReturn Me.Customer.FirstName
End GetSet(ByVal value As String)
Me.Customer.FirstName = valueEnd Set
End Property‘... and so on for every member that Customer declares
Public Function DiscountPercent() As Decimal‘only method that does not delegate to CustomerReturn 50
End FunctionEnd Class
As you can see, this is quite cumbersome and requires a lot of coding. This style was actually used (to a greater or lesser extent) in VB 6, in combination with interface inheritance to simulate implementationinheritance. So here is where class inheritance comes into play. The better solution is simply to inheritthe Customer class and override the DiscountPercent method:
Option Explicit OnOption Strict On
Public Class SeniorCitizenInherits Customer
Public Overrides Function DiscountPercent() As DecimalReturn 50
End Function
325
Chapter 12: Advanced Object-Oriented Concepts
79796c12.qxd:WroxPro 2/23/08 8:38 AM Page 325
End Class
Public Class Customer‘...
Private purchaseHistoryValue As PurchaseHistoryPublic Property PurchaseHistory() As PurchaseHistory
GetReturn purchaseHistoryValue
End GetSet(ByVal value As PurchaseHistory)
purchaseHistoryValue = valueEnd Set
End Property‘The method has to be declared OverridablePublic Overridable Function DiscountPercent() As Decimal
If Me.PurchaseHistory.TotalPurchases > 10 ThenReturn 3
ElseIf Me.PurchaseHistory.TotalPurchases > 100 ThenReturn 5
ElseReturn 0
End IfEnd Function
End Class
However, the story does not end here. You are again faced with some new requirements. The opera househas also decided to provide walker aids for interested seniors. You can easily describe this requirement bydefining an additional property, RequiresAssistance, in the SeniorCitizen class:
Option Explicit OnOption Strict On
Public Class SeniorCitizenInherits Customer
Private requiresAssistanceValue As Boolean
Public Overrides Function DiscountPercent() As DecimalReturn 50
End Function
Public Property RequiresAssistance() As BooleanGet
Return requiresAssistanceValueEnd GetSet(ByVal value As Boolean)
requiresAssistanceValue = valueEnd Set
End PropertyEnd Class
In this case, the new property is accessible only if the variable is declared and created as SeniorCitizen,as in Dim customer as SeniorCitizen = new SeniorCitizen. If it is declared as Customer, as in
326
Part IV: Advanced Refactorings
79796c12.qxd:WroxPro 2/23/08 8:38 AM Page 326
Dim customer as SeniorCitizen = new Customer, the newly added RequiresAssistance prop-erty will not be accessible. This way the SeniorCitizen class can expose behavior that characterizes onlyitself and not the parent class.
One “overridable” caveat: in Visual Basic, class members are not overrideable by default. This means thatyou should program with extensibility on your mind and always add the overrideable keyword in a memberdeclaration if you suspect that this member might have to be overridden by some subclass in the future.
When the class is inherited, the following things are accomplished automatically:
❑ All the members that belong to the superclass, except private ones, are immediately available tosubclasses as if they were the subclasses’ own.
❑ A subclass automatically implements the interface of the superclass, making the child subclass avalid substitute in polymorphic behavior.
❑ A subclass can, by means of overriding, provide different implementation for certain membersmarked as overrideable in the superclass.
❑ A subclass can provide additional new behavior by defining new members.
As I already mentioned in Chapter 8, if you are not sure about the difference between shadowing andoverriding, take a look at the “Differences Between Shadowing and Overriding” article on MSDN:http://msdn2.microsoft.com/en-us/library/ms172785(VS.80).aspx.
Inheritance is not limited to classes. Interfaces can also be inherited by other interfaces or implementedin a class. This is generally referred to as interface inheritance.
Class Versus Interface InheritanceThe purpose of the Interface construct in Visual Basic is to explicitly define a class interface. You havealready seen how interfaces help encapsulate objects by making objects accessible through a single accesspoint only, and by hiding object internals. Every class has an interface that consists of all public membersof that class. The Interface construct helps you define a group of members that will be visible underthe name of the interface, thus reducing the visible footprint of a class if the class is accessed through an interface.
Sometimes you do not know what the implementation of some interface is going to look like. In thesecases, an interface can serve as a contract that communicating objects have to obey. The object that exposesthe interface guarantees that it is capable of receiving a certain message, and a client object has to send themessage in the exact form defined through its signature in the Interface.
Shadowing Versus OverridingIf you inherit the class and then declare a method or property with the same signature in the subclass, this method will be shadowed by default, as the compiler will warn you.Sometimes shadowing is the desired behavior. In order to override the member, you willhave to use the Overrides keyword. For further explanation on this subtle but crucialdistinction, consult Visual Basic help.
327
Chapter 12: Advanced Object-Oriented Concepts
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 327
An interface cannot be instantiated and has only abstract members. Abstract members have only a signatureand no implementation. A class implements an interface by means of the Implements keyword. In contrastto a class inheritance, a class can implement multiple interfaces. This is a form of multiple inheritance, apowerful object-oriented concept. When you are inheriting classes, however, only a single inheritance is permitted.
Now you can continue working on the opera example. Define an ICustomer interface and make theclass Customer implement the newly created interface. This way, the Customer class will be visiblethrough a newly created interface whenever a variable pointing to an instance of Customer has beendeclared as ICustomer. The same is true for SeniorCitizen. Because inheritance is transitory, all children implement or inherit all types that their parents implement or inherit. The code with theICustomer interface will, therefore, look like this:
Option Explicit OnOption Strict On
Public Interface ICustomer‘...a few more membersProperty FirstName() As StringProperty PurchaseHistory() As PurchaseHistoryFunction DiscountPercent() As Decimal
End Interface
Public Class CustomerImplements ICustomer‘...Private firstNameValue As StringPrivate purchaseHistoryValue As PurchaseHistory
Public Property FirstName() As String _Implements ICustomer.FirstName
GetReturn firstNameValue
End GetSet(ByVal value As String)
firstNameValue = valueEnd Set
End PropertyPublic Property PurchaseHistory() As PurchaseHistory _Implements ICustomer.PurchaseHistory
GetReturn purchaseHistoryValue
End GetSet(ByVal value As PurchaseHistory)
purchaseHistoryValue = valueEnd Set
End PropertyPublic Overridable Function DiscountPercent() As Decimal _Implements ICustomer.DiscountPercent
If Me.PurchaseHistory.TotalPurchases > 10 ThenReturn 3
ElseIf Me.PurchaseHistory.TotalPurchases > 100 ThenReturn 5
328
Part IV: Advanced Refactorings
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 328
ElseReturn 0
End IfEnd Function
End Class
Public Class SeniorCitizenInherits CustomerPrivate requiresAssistanceValue As BooleanPublic Overrides Function DiscountPercent() As Decimal
Return 50End FunctionPublic Property RequiresAssistance() As Boolean
GetReturn requiresAssistanceValue
End GetSet(ByVal value As Boolean)
requiresAssistanceValue = valueEnd Set
End PropertyEnd Class
Interfaces are closely related to polymorphic behavior. In order to be able to perform type substitution,an object has to implement an interface or inherit the class.
PolymorphismIn the example used so far in this chapter, you have seen how a class can inherit other classes and override some members and how it can implement an interface. But so far you have not seen what happens from the client’s point of view. For that purpose, you can add an additional class to the example,a PurchaseForm class that has a Customer property. You will also add another DisplayForm with twobuttons, InstantiateAsCustomer and InstantiateAsSeniorCitizen, whose sole purpose is to passan instance of Customer and then an instance of SeniorCitizen to PurchaseForm.
Note: The code I have displayed here is programmer-generated code of the PurchaseForm class. AnyWindows Form class is a partial class, and ID-generated code is placed in another file. I have omittedthe IDE-generated code for this example because of space considerations. The complete code for thisexample is available for download from www.wrox.com.
Option Explicit OnOption Strict On
Public Class PurchaseFormPrivate customerValue As ICustomerPublic Property Customer() As ICustomer
GetReturn customerValue
End GetSet(ByVal value As ICustomer)
customerValue = valueEnd Set
329
Chapter 12: Advanced Object-Oriented Concepts
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 329
End Property
Private Sub PurchaseForm_Activated( _ByVal sender As Object, ByVal e As System.EventArgs) _Handles Me.Activated
‘display discount percent on the formMe.Discount.Text = Me.Customer.DiscountPercent.ToString
End SubEnd Class
Public Class DisplayFormDim purchaseForm As PurchaseForm = New PurchaseFormPrivate Sub InstantiateAsCustomer_Click _(ByVal sender As System.Object, ByVal e As System.EventArgs) _Handles InstantiateAsCustomer.Click
‘create instance of CustomerpurchaseForm.Customer = New CustomerpurchaseForm.Show()purchaseForm.Activate()
End SubPrivate Sub InstantiateAsSeniorCitizen_Click( _ByVal sender As System.Object, ByVal e As System.EventArgs) _Handles InstantiateAsSeniorCitizen.Click
‘create instance of SeniorCitizenpurchaseForm.Customer = New SeniorCitizenpurchaseForm.Show()purchaseForm.Activate()
End SubEnd Class
If you run this example and then:
❑ Press the InstantiateAsCustomer button, the PurchaseForm will show 0 as the discountpercent. (Assume that purchase history is empty by default.)
❑ Press the InstantiateAsSeniorCitizen button, the PurchaseForm will show 50 as the discount percent.
In the first case, the DiscountPercent method implemented in Customer was executed, and in the second, the DiscountPercent method implemented in SeniorCitizen was executed.
This is an example of polymorphic behavior, wherein the same variable can point to different instances of different classes. The same operation was defined and implemented differently in different classes. An important thing to notice is that the exact type of the instance that the variable is pointing to is notknown until runtime.
In this example, this means that you can use the same form and the same code to perform purchasesfor both Customer and SeniorCitizen. The PurchaseForm is oblivious to the implementing type ofthe ICustomer interface. This helps improve the encapsulation of the system and makes reuse evenmore efficient.
330
Part IV: Advanced Refactorings
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 330
Interface Versus Abstract ClassSomewhere between classes and interfaces are abstract classes. These classes in Visual Basic are definedby means of the MustInherit keyword. Neither abstract class nor interface can be instantiated. Abstractclasses can contain both abstract and concrete methods. This means that abstract classes might be a goodchoice in situations where you know how to implement some methods but not others.
Object-Oriented Design Principle: Program to an AbstractionIn order to keep your code flexible and open for extension, you should always try todepend on the highest abstraction in the inheritance hierarchy. You can generally con-sider parent classes or interfaces to be more abstract, and child classes more concrete orspecialized. If you adhere to this principle, the client code will happily accept differentimplementations without needing modification or any type of intervention.
DefinitionAn interpretation of this principle from Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma et al. (Addison-Wesley Professional, 1995) isexpressed as follows: “Program to an abstraction, not an implementation.”
Remember, greater abstraction is always found higher up in the inheritance hierarchyand is often represented through interfaces or abstract classes.
ExampleImagine you have a PurchaseHistory class that holds the list of all the purchases acustomer has carried out. A possible implementation would be like this:
Public Class PurchaseHistory‘...Private purchases As ArrayList
End Class
In this case, PurchaseHistory is dependant on one specific implementation of a list,the System.Collections.ArrayList, and will not accept any other.
However, it is better to make PurchaseHistory dependant on list abstraction, definedby the System.Collections.IList interface:
Public Class PurchaseHistory‘...Private purchases As IList
End Class
This way, not only can ArrayList be used as a purchase list implementation, but anyother class that implements the IList interface. Such a class could be, for example,System.Collections.Generic.List(Of T).
331
Chapter 12: Advanced Object-Oriented Concepts
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 331
Some important things to remember when choosing an abstract class over an interface are the following:
❑ You can inherit only a single abstract class, and you can implement as many interfaces as youwant. Interfaces are a more flexible choice in this respect.
❑ Abstract classes can contain implemented members, so they are a good choice if you want toprovide or enforce some common behavior for all subclasses.
❑ Abstract classes can declare nonpublic members; interfaces can’t.
❑ If you declare a type as an interface and then add a member, you will force all the implementersto implement the new method and to be recompiled. If you choose an abstract class instead andadd a new method, you can ship a default implementation with the class itself. This way youwill avoid classes that inherit your abstract class being recompiled. This effect can be especiallyrelevant when you are programming frameworks.
❑ Abstract classes, just like any other class, can also implement interfaces. This is actually a com-mon pattern that makes it possible to provide a partial implementation for a certain interface, ifyou can make use of such implementation. If the implementation provided by an abstract classdoes not serve the purpose, you are free to implement an interface and write the class from zero.When an abstract class implements an interface, you are free to implement only members of theinterface you choose. Because the class is abstract, you are free to leave interface members ofyour choice unimplemented.
While abstract classes can contain both abstract and concrete members, there is no reason why you couldn’twrite an abstract class containing purely abstract members. Such an approach is beneficial for frameworkprogrammers, because it frees them to add new members to the abstract class in the next version of the class, as long as they provide default implementation for such members. This way binary compatibility betweenclasses will not be broken. In the case of interfaces, any new member in a new version of an interface meansthat all implementors have to be recompiled in order to implement newly defined members.
GenericityThis is another feature available in Visual Basic since the VB 2005 incarnation that can save you fromwriting a lot of boilerplate code and can also provide additional type safety. Generic types, also knownas parameterized types, enable you to define other types that a class uses at the point of usage, and thatare not in the class itself. This way the same class can work with many types that can be used to para-meterize the class.
The Typed Container ProblemGenerics are easiest to understand in the context of container types. Standard containers from theSystem.Collections namespace provide convenient placeholders for groups of objects and theirmanipulation, such as position or key-based retrieval. Because these containers are general-purpose,they let you add to and retrieve from the container any type of object. After the object is retrieved, ithas to be cast back into the expected type, if Option Strict is activated. (On a side note, in practicewe rarely need to hold objects of different types in the same container.) There are at least two problemswith the general-purpose container scheme:
❑ There is no guarantee that all the objects in the container will be of the type you expect. Thereis nothing to prevent the addition of any object of any type to the container. And if you comeacross an invalid type, a runtime System.InvalidCastException is thrown that objected is
332
Part IV: Advanced Refactorings
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 332
retrieved and cast into the expected type. There should be some way for the compiler to ensurethat only expected types are added to the container.
❑ After you retrieve the type, you have to write the code that casts the object into the expectedtype. Such code is repetitious and, therefore, should not be necessary.
There is a solution, even with standard containers, to the problems just listed. It consists in writing yourown typed container wrappers. Your container wrapper delegates all the work to a standard container ithides from the public. The sample code for this solution is shown in Listing 12-1.
Listing 12-1: Custom-Typed Container Implementation Using Standard Containers
Option Explicit OnOption Strict On
Public Class CustomerListPrivate listValue As IList‘... Other list-like membersPublic Function Add(ByVal customer As Customer) As Integer
‘delegate to internal listMe.listValue.Add(customer)
End FunctionDefault Public Property Item(ByVal index As Integer) As Customer
GetReturn CType(listValue.Item(index), Customer)
End GetSet(ByVal value As Customer)
Me.listValue.Item(index) = valueEnd Set
End PropertyEnd Class
The custom-container approach has two major drawbacks:
❑ It is repetitious and requires a lot of work and discipline. In practice, it is easier not to go to allthe trouble of writing a custom container and simply to use a general-purpose container instead.
❑ Because custom container wrappers do not implement standard non-type-safe container inter-faces (for instance, System.Collections.Ilist), they are not easily interchangeable with othercontainer implementations. For example, it is not easy to replace your CustomerList with astandard ArrayList.
The Generic Container Solution Generics are an elegant solution to the problem of typed containers. They let you specify the type of contained objects at the point of usage and in a single place. After such a container is instantiated, it will receive and return only objects of the specified type, resolving the problem of type safety andcasting code. You can easily define a type-safe container with the Of keyword:
Private listValue As IList(Of Customer)
No more do you need to cast an object to Customer after retrieval or worry about an object of some othertype cropping up from the container. It is worth mentioning that that all container implementations from
333
Chapter 12: Advanced Object-Oriented Concepts
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 333
the System.Container.Generic namespace also implement interfaces from the standard containernamespace, making them valid replacements for non-generic implementations. For example, System.Container.Generic.List implements the System.Container.IList interface, making it a validreplacement for System.Container.ArrayList as long as the ArrayList is accessed through aSystem.Container.IList interface (see the “Object-Oriented Design Principle: Program to anAbstraction” sidebar earlier in the chapter).
Generics are not container-only. They can be used for many other purposes as well. In VB, .NET, you arefree to write your own generic classes when you need them, and these can be used for various purposes.
Like any other powerful tool, these advanced object-oriented language capabilities that I just talkedabout are useful only if employed well and in the right circumstances. In the hands of an inexperienceduser, these advanced object-oriented features can be completely counterproductive, leading to poorlydesigned software. Therefore, the next step is to take a look at the most common errors that program-mers make when applying these features and how refactoring can help remedy those errors.
Inheritance Abuse and Refactoring Solutions
There are a few things to be aware of when applying inheritance. When you inherit a class, the linkbetween parent and child class (often referred to as superclass and subclass) is established at compilationtime. The link between parent and child class is strong in nature, because all non-private members in theparent are also visible in the child class. This has the following consequences:
❑ The behavior that the child class has inherited from the parent cannot be changed at runtime.
❑ Reusing a child class means also reusing the parent. If there is a need to change some behavior inthe parent, this will most probably provoke a change in a child class, because of tight couplingbetween the two.
You have just seen the arguments that inspired the writers of Design Patterns: Elements of Reusable Object-Oriented Software to coin the “favor object composition over class inheritance” design principle. This issometimes simplistically interpreted as meaning that class inheritance is not necessary at all. Dependingon the circumstances, either composition or inheritance can be beneficial for the design. Nonetheless,inheritance is often abused as a reuse technique because it is so easy to implement.
Object-Oriented Design Principle: Favor Object Composition over Class Inheritance
Class inheritance is a powerful and easily accessible reuse technique. A child class hasimmediate access to functionality provided by a parent class. A child class can overridesome of the parent’s functionality or provide new functionality. Inheritance should beused in cases where the child specializes or extends behavior provided by the parent. It means that there is an “is a kind of” relationship between the two.
334
Part IV: Advanced Refactorings
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 334
Continued
This link between the child and the parent is established at compile time and cannot bechanged at runtime. This link is strong in nature, exposing the child to the inner work-ings of the parent. The consequences are as follows:
❑ Inherited behavior cannot be changed at runtime.
❑ Changes in the parent will most probably provoke a change in all childclasses.
This makes class composition and reuse based on delegation a more flexible and betterchoice when the reused behavior should be changed in runtime. It can be a better choicefor more granular and more encapsulated design. Finally, it is better suited for express-ing a “has a” relationship instead of an “is a kind of” relationship between types.
DefinitionIn Design Patterns: Elements of Reusable Object-Oriented Software, the principle is formu-lated as follows:
“Favor object composition over class inheritance.”
ExampleIn a system that controls a reactor in an atomic plant, you will need to read reactor tem-perature and operate on control rods. Imagine a design in which the AtomicReactorclass inherits the Thermometer implementation:
Public Class BimetalThermometerPublic Function GetCurrentTemperature() As Decimal
‘...End Function
End Class
Public Class AtomicReactorInherits BimetalThermometer
Public Sub ControlTemperature()If GetCurrentTemperature() < _TemperatureLevels.DangerousLow Then
RaiseAlarm()EmergencyReactionIncrease()Exit Sub
ElseIf GetCurrentTemperature() > _TemperatureLevels.DangerousHigh Then
RaiseAlarm()EmergencyReactionDecrease()Exit Sub
ElseIf GetCurrentTemperature() < _TemperatureLevels.Low Then
IncreaseReaction()ElseIf GetCurrentTemperature() > _TemperatureLevels.High Then
335
Chapter 12: Advanced Object-Oriented Concepts
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 335
DecreaseReaction()End If
End Sub‘...
End Class
In such a design, thermometer functionality is deceptively easy to use. However, theproblems such a design imposes are various:
❑ The reactor is not “a kind of thermometer,” so from a problem domainpoint of view, inheritance is not employed correctly.
❑ The reactor is coupled with a specific implementation of the thermometer.If at one moment, another type of thermometer were used, the applicationwould have to be recompiled.
The relationship between the reactor and thermometer is much better expressed by acomposition relationship between the two:
Public Interface IThermometerFunction GetCurrentTemperature() As Decimal
End Interface
Public Class BimetalThermometerImplements IThermometerPublic Function GetCurrentTemperature() As Decimal _Implements IThermometer.GetCurrentTemperature
‘...End Function
End Class
Public Class InfraredThermometerImplements IThermometerPublic Function GetCurrentTemperature() As Decimal _Implements IThermometer.GetCurrentTemperature
‘...End Function
End Class
Public Class AtomicReactorPrivate thermometerValue As IThermometerPublic Property Thermometer() As IThermometer
GetReturn thermometerValue
End GetSet(ByVal value As IThermometer)
thermometerValue = valueEnd Set
End PropertyPublic Sub ControlTemperature()
If thermometer.GetCurrentTemperature() < _TemperatureLevels.DangerousLow Then
336
Part IV: Advanced Refactorings
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 336
You will now see some typical examples of inheritance misuse. We will start with composition that ismistaken for inheritance.
Composition Mistaken for Inheritance and Other MisusesYou are used to classifying things in your life. You know that a car is a kind of vehicle, that mammals areanimals, that an equilateral triangle is a kind of triangle. However, such relationships between differententities are not simple to establish and have been the subject of scientific and philosophical investigationfrom ancient times. When classifying, you try to establish a relationship between things. You look intocommon characteristics and then group things according to commonality. You rightfully expect anymember of a group to share all the common characteristics of that group.
Continued
Smell: Refused BequestDetecting the SmellA child class that refuses to implement certain parent members is a sign of a refusedbequest. The compiler will oblige a programmer to make a child class seemingly com-pliant with the parent, but a programmer is able to find a way around this obligationimposed by the compiler. The refusal can take a form of an empty overriding subim-plementation, an overriding function that returns a null object — nothing — or a mem-ber that does nothing but raise an exception.
RaiseAlarm()EmergencyReactionIncrease()Exit Sub
ElseIf thermometer.GetCurrentTemperature() > _TemperatureLevels.DangerousHigh Then
RaiseAlarm()EmergencyReactionDecrease()Exit Sub
ElseIf thermometer.GetCurrentTemperature() < _TemperatureLevels.Low Then
IncreaseReaction()ElseIf thermometer.GetCurrentTemperature() > _TemperatureLevels.High Then
DecreaseReaction()End If
End SubEnd Class
Now the reactor “uses” the Thermometer object to obtain temperature readings. The reactor does not depend on a specific implementation of Thermometer, and this implementation can be changed at runtime. Both BimetalThermometer andInfraredThermometer implement the IThermometer interface, and either can beused by the reactor to perform readings.
Finally, this example demonstrates how inheritance and composition are used togetherto make a more flexible and robust design.
337
Chapter 12: Advanced Object-Oriented Concepts
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 337
Inheritance should be used to express a relationship between more general and more specific cases, oftenexpressed as an “is a” or an “is a kind of” relationship. However, these relationships can often work outdifferently in science, in laymen’s understanding of the world, and, finally, in programming.
In programming, when designing inheritance, you base your thinking on generalization and an “is a”principle. However, everyday classifications do not always translate well into the programming world.When writing code, you have to be strict in the application of principles that govern software design.For example, if you ask any random person what is the most typical bird characteristic, he or she willmost probably reply, “Birds fly.” If you ask this same person what an ostrich is, they will most probablyserenely reply, “An ostrich is a bird.” While classifications you normally use are far from perfect, theygenerally do not interfere with your everyday life. However, similar imperfections in software can havemuch further-reaching consequences.
Refactoring: Replace Inheritance with DelegationMotivationUsing delegation instead of inheritance can often make for much more flexible, moreencapsulated, and more robust design. This is expressed in the “favor object composi-tion over class inheritance” design principle. In this case, delegation refers to havinganother object to do the work, another object to which the work is being delegated.Often the relationship between the two objects is one of composition.
Related RefactoringUse Replace Inheritance with Delegation refactoring to eliminate this smell.
RationaleA child class should support the parent’s interface in its totality. A child that is refusingsome of the parent’s members will result in clients having to know the child, and in theend, this will break encapsulation and polymorphic code. Here is an example of seem-ingly correct inheritance in which the child is refusing some of the parent’s behavior:
Public MustInherit Class Bird‘...Public MustOverride Sub Fly()
End Class
Public Class OstrichInherits Bird‘...Public Overrides Sub Fly()
Throw New InvalidOperationException( _“Ostrich cannot fly, silly!”)
End SubEnd Class
A more benign case of the smell is when the child is not making use of all members thatthe parent provides, but when the child is refusing the parent’s interface, refactoring hasto take place.
338
Part IV: Advanced Refactorings
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 338
Continued
In some cases, the inheritance relationship between two classes is unfounded, becausethe child class refuses some of the parent’s interface, which makes it nonconformant intype. Such a situation should be remedied by refactoring, and often the best solution isto replace inheritance with delegation.
When you program, it is often convenient to start by extending an already existingclass that provides ready behavior that you can reuse. In some cases, you may find thatyou do not want all the behavior exposed through inheritance to be visible. If you needto hide some of the parent’s members, it is best to replace inheritance with delegation.
Related SmellsUse this refactoring to eliminate the Refused Bequest smell and to make design moreflexible by making use of delegation instead of inheritance as a reuse mechanism.
MechanicsThe refactoring is performed in a few sequential steps. You can start by analyzing thesample code used in this refactoring. The PerishableContainer class discards itemsthat have passed in storage more times than is permitted. The class inherits the stan-dard ArrayList and extends its functionality with methods for adding and retrievingperishable items. The problem with the class is that it also exposes all the public mem-bers of ArrayList, making it easy for the client to circumvent time-restricted methods.The solution is to replace inheritance with delegation.
BeforeOption Explicit OnOption Strict On
Public Class PerishableContainerInherits ArrayListPrivate perishIntervalInSecondsValue As IntegerPublic Property PerishIntervalInSeconds() As Integer
GetReturn perishIntervalInSecondsValue
End GetSet(ByVal value As Integer)
perishIntervalInSecondsValue = valueEnd Set
End PropertyPublic Sub LeaveInStorage(ByVal item As PerishableItem)
MyBase.Add(item)End SubPrivate Function TakeOldestFromStorage() As PerishableItem
If MyBase.Count > 0 ThenDim item As PerishableItem = _CType(MyBase.Item(0), PerishableItem)MyBase.Remove(item)If Not HasPerished(item) Then
Return item
339
Chapter 12: Advanced Object-Oriented Concepts
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 339
End IfEnd IfReturn Nothing
End FunctionPublic Function TakeFromStorage() As PerishableItem
Dim item As PerishableItem = NothingWhile MyBase.Count > 0
item = TakeOldestFromStorage()If item IsNot Nothing Then
Exit WhileEnd If
End WhileReturn item
End FunctionPrivate Function HasPerished( _ByVal item As PerishableItem) As Boolean
If item.CreationTime.AddSeconds( _perishIntervalInSecondsValue) > Date.Now Then
Return FalseEnd IfReturn True
End FunctionEnd Class
Public Interface PerishableItemProperty CreationTime() As Date
End Interface
1. Create a parent type field in the child and initialize it with the object itself(Me):
Public Class PerishableContainerInherits ArrayListPrivate list As ArrayList = Me‘...
End Class
2. Make the code in the child use the newly defined field instead of MyBase:
‘...Private Function TakeOldestFromStorage() As PerishableItem
If list.Count > 0 ThenDim item As PerishableItem = _CType(list.Item(0), PerishableItem)list.Remove(item)If Not HasPerished(item) Then
Return itemEnd If
End IfReturn Nothing
End Function
340
Part IV: Advanced Refactorings
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 340
Continued
3. Remove the “Inherits” part from the class declaration and initialize thefield created in Step 1 with a new instance of the would-be parent:
Public Class PerishableContainerPrivate list As ArrayList = New ArrayList‘...
End Class
4. Expose all members from the parent used by the clients with simple dele-gation code:
Public Class PerishableContainerPublic ReadOnly Property Count() As Integer
GetReturn list.Count
End GetEnd Property‘...
End Class
AfterOption Explicit OnOption Strict On
Public Class PerishableContainerPrivate list As ArrayList = New ArrayListPrivate perishIntervalInSecondsValue As IntegerPublic Property PerishIntervalInSeconds() As Integer
GetReturn perishIntervalInSecondsValue
End GetSet(ByVal value As Integer)
perishIntervalInSecondsValue = valueEnd Set
End PropertyPublic Sub LeaveInStorage(ByVal item As PerishableItem)
list.Add(item)End Sub‘...Private Function TakeOldestFromStorage() As PerishableItem
If list.Count > 0 ThenDim item As PerishableItem = _CType(list.Item(0), PerishableItem)list.Remove(item)If Not HasPerished(item) Then
Return itemEnd If
End IfReturn Nothing
End FunctionPublic Function TakeFromStorage() As PerishableItem
Dim item As PerishableItem = Nothing
341
Chapter 12: Advanced Object-Oriented Concepts
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 341
It is best if you start inheritance misuse analysis with an example that will demonstrate possible pitfallswith inheritance application. You will then see how the pitfall can be resolved by means of differentrefactorings. The next section starts by looking at a peculiar print-system design.
Refactoring for Inheritance — Print-System IllustrationIn this particular print-system design, shown in Listing 12-2, the central class in the design is an abstractPrintService class. This class declares an abstract PrintJob method that is implemented by differentclasses that inherit PrintSystem. Child classes, like LexmarkX500 and HPLaserJet, implement physicaldevice-specific code that has to take care of communicating with the device and rendering the data in theform that the device understands.
In a system designed like this, each time you need to add another physical device to the group of supporteddevices, you need to add another sibling to the class hierarchy. It is up to the child class to implement alldevice-specific code.
Listing 12-2: Print System Using Inheritance to Incorporate Different Printing Devices
Option Explicit OnOption Strict On
Imports System.IO
Public Enum ServiceState
While list.Count > 0item = TakeOldestFromStorage()If item IsNot Nothing Then
Exit WhileEnd If
End WhileReturn item
End FunctionPrivate Function HasPerished( _ByVal item As PerishableItem) As Boolean
If item.CreationTime.AddSeconds( _perishIntervalInSecondsValue) > Date.Now Then
Return FalseEnd IfReturn True
End FunctionPublic ReadOnly Property Count() As Integer
GetReturn list.Count
End GetEnd Property
End Class
Public Interface PerishableItemProperty CreationTime() As Date
End Interface
342
Part IV: Advanced Refactorings
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 342
Listing 12-2: Print System Using Inheritance to Incorporate Different Printing Devices (continued)
IdleProcessingStopped
End Enum
Public MustInherit Class PrintServicePrivate jobsInQueueValue As IList(Of PrintJob)Private serviceStateValue As ServiceStatePublic Property JobsInQueue() As IList(Of PrintJob)
GetReturn jobsInQueueValue
End GetSet(ByVal value As IList(Of PrintJob))
jobsInQueueValue = valueEnd Set
End PropertyPublic Property ServiceState() As ServiceState
GetReturn serviceStateValue
End GetSet(ByVal value As ServiceState)
serviceStateValue = valueEnd Set
End PropertyPublic Function CreatePrintJob() As PrintJob
Return New PrintJobEnd FunctionPrivate Sub print()
While JobsInQueue.Count > 0PrintJob(JobsInQueue.Item(0))
End WhileEnd SubProtected MustOverride Sub PrintJob(ByVal job As PrintJob)
End Class
Public Class HPLaserJetInherits PrintServicePrivate initializedValue As BooleanPrivate Property Initialized() As Boolean
GetReturn initializedValue
End GetSet(ByVal value As Boolean)
initializedValue = valueEnd Set
End PropertyProtected Overrides Sub PrintJob(ByVal job As PrintJob)
If Not Me.Initialized ThenMe.Initialize()
End IfContinued
343
Chapter 12: Advanced Object-Oriented Concepts
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 343
Listing 12-2: Print System Using Inheritance to Incorporate Different Printing Devices (continued)
StartDocument()Dim renderedDocument As Stream = RenderDocument(job)WriteDocumentToDevice(renderedDocument)EndDocument()
End SubPrivate Function RenderDocument(ByVal job As PrintJob) As Stream
‘device specific codeEnd SubPrivate Sub WriteDocumentToDevice (ByVal data As Stream)
‘device specific codeEnd SubPrivate Sub Initialize()
‘device specific codeEnd SubPrivate Sub StartDocument()
‘device specific codeEnd SubPrivate Sub EndDocument()
‘device specific codeEnd Sub
End Class
Public Class LexmarkX500Inherits PrintServicePrivate initializedValue As BooleanPrivate Property Initialized() As Boolean
GetReturn initializedValue
End GetSet(ByVal value As Boolean)
initializedValue = valueEnd Set
End PropertyProtected Overrides Sub PrintJob(ByVal job As PrintJob)
If Not Me.Initialized ThenMe.Initialize()
End IfStartDocument()Dim renderedDocument As Stream = RenderDocument(job)WriteDocumentToDevice(renderedDocument)EndDocument()
End SubPrivate Function RenderDocument(ByVal job As PrintJob) As Stream
‘device specific codeEnd SubPrivate Sub WriteDocumentToDevice (ByVal data As Stream)
‘device specific codeEnd SubPrivate Sub Initialize()
‘device specific codeEnd Sub
344
Part IV: Advanced Refactorings
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 344
Listing 12-2: Print System Using Inheritance to Incorporate Different Printing Devices (continued)
Private Sub StartDocument()‘device specific code
End SubPrivate Sub EndDocument()
‘device specific codeEnd Sub
End Class
Public Class PrintJob‘...
End Class
There are a number of problems with this design. Take a look at some of the immediately obvious ones:
❑ Each time a different physical device is connected, the client application has to instantiate a dif-ferent child class. It can still use different device-specific print services in a polymorphic way,thanks to the inheritance relationship between the abstract parent PrintService class and thedevice-specific child classes.
❑ Each time a new physical device has to be supported, a new class has to be added to the hierarchy.The client has to be aware of the class name, and the client code has to be recompiled in order touse the new class (unless a more complex solution is implemented on the client side).
❑ In this design, one physical device corresponds to one printing system. In the future, this mightprove not to be flexible enough. What if you have more than one physical device? You then haveto instantiate a different print service for each device. The inheritance relationship between device-specific and general print functionality makes it complex for the service to support more than onephysical device.
Finally, thinking in problem, domain terms, you can see that the functionality betweenPrintService and the child classes is not particularly related. Child classes do not overrideany of the PrintService methods, except the device-specific PrintJob. However, they adda number of device-specific methods. This can make you wonder if the relationship betweenPrintService and the children is really of an “is a” type.
❑ If you would like one day to reuse device-specific code contained in the LexmarkX500 andHPLaserJet classes, you would be obliged to bring along PrintService code. This is notvery flexible.
From this analysis, it seems that the inheritance relationship between PrintService and the device-specific LexmarkX500 and HPLaserJet classes is not entirely justified. LexmarkX500 and HPLaserJetfit into the picture better as classes that PrintService can delegate to in order to communicate with aphysical device.
Extracting the Device InterfaceYou can start by noting that PrintService has a single abstract method. The abstract PrintJob methodis implemented by both the LexmarkX500 and HPLaserJet classes. This means that PrintService hasno knowledge of concrete child types like LexmarkX500 and HPLaserJet. This is actually a good thing,
345
Chapter 12: Advanced Object-Oriented Concepts
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 345
so when moving from inheritance to delegation in the design of this print system, you can try to useLexmarkX500 and HPLaserJet in a polymorphic way. In order to do so, you need to extract an IDeviceinterface that will have a single PrintJob method. You can perform interface extraction automaticallywith the help of Refactor! on a single class, but the second class you have to modify manually. First, youhave to make the method PrintJob public and to remove the Inherits keyword from the class declara-tion. The result of interface extraction is shown in Listing 12-3.
Listing 12-3: Extracted IPrintDevice Interface
Public Interface IPrintDeviceSub PrintJob(ByVal job As PrintJob)
End Interface
Public Class HPLaserJetImplements IPrintDevice
Public Sub PrintJob(ByVal job As PrintJob) _Implements IPrintDevice.PrintJob‘device specific codeEnd Sub
‘...End Class
This refactoring makes it possible to use all PrintDevice implementors in a polymorphic way. Thissimplifies things greatly when you are going from an inheritance to a composition relationship betweenthe PrintService and the LexmarkX500 and HPLaserJet classes.
Refactoring: Extract InterfaceMotivationSometimes you might want to expose only a part of the functionality offered by theclass. This is especially true if some clients use only a subset of the functionality pro-vided by the class.
Another case for this refactoring comes when some classes have a part of their interfacein common, but implementation of these members is different. In order for theseclasses to be used in a polymorphic way, they should share a common interface.
Related SmellsUse this refactoring to eliminate the Duplicated Code smell and to provide polymor-phic access to classes.
Mechanics1. Declare the interface with all the members that you wish to expose through
the interface.
2. Make the classes implement the interface by adding the Implements state-ment to both the class and member declarations.
3. Make the clients use the interface and not the concrete classes.
346
Part IV: Advanced Refactorings
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 346
Continued
BeforeOption Explicit OnOption Strict On
Public Class CustomerPrivate firstNameValue As StringPrivate purchaseHistoryValue As PurchaseHistoryPublic Property FirstName() As String
GetReturn firstNameValue
End GetSet(ByVal value As String)
firstNameValue = valueEnd Set
End PropertyPublic Property PurchaseHistory() As PurchaseHistory
GetReturn purchaseHistoryValue
End GetSet(ByVal value As PurchaseHistory)
purchaseHistoryValue = valueEnd Set
End PropertyPublic Function DiscountPercent() As Decimal
If Me.PurchaseHistory.TotalPurchases > 10 ThenReturn 3
ElseIf Me.PurchaseHistory.TotalPurchases > 100 ThenReturn 5 ElseReturn 0
End IfEnd Function
End ClassPublic Interface PerishableItem
Property CreationTime() As DateEnd Interface
AfterOption Explicit OnOption Strict On
Public Interface ICustomerProperty FirstName() As StringProperty PurchaseHistory() As PurchaseHistoryFunction DiscountPercent() As Decimal
End Interface
Public Class CustomerImplements ICustomerPrivate firstNameValue As StringPrivate purchaseHistoryValue As PurchaseHistory
347
Chapter 12: Advanced Object-Oriented Concepts
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 347
Extract Interface Refactoring in Refactor!Extract Interface refactoring in Refactor! becomes available when you select the class name in the editor.The tool will assume that you wish all the public members of your class to become a part of the newlyextracted interface. If you do not want this, you had better change to private the visibility of the membersyou do not wish to form a part of the extracted interface. After you execute the extraction, you can turnthe visibility back to public.
Refactor! will try to guess the name of the interface based on the originating class. It will add the I prefixto the class name. However, the name can be immediately changed thanks to linked identifiers.
After the extraction is performed, the code is placed inside the same file as the originating class. If you wishto move the interface to a separate file, as is most probably advisable, you can use the Move Type to Filerefactoring that Refactor! makes available. You can see Refactor! ready to extract the interface in Figure 12-1.
Delegation Instead of Inheritance Inside the Print SystemNow you are ready to modify PrintService so it delegates to IDevice instead on relying on a subclass to implement device-specific behavior. PrintService is no longer abstract, so the MustInherit keywordcan be removed from the declaration. The abstract PrintJob method now delegates to a device, and itshould be declared private. The code for PrintService after it has been refactored to use delegationinstead of inheritance is visible in Listing 12-4.
Public Property FirstName() As String _Implements ICustomer.FirstName
GetReturn firstNameValue
End GetSet(ByVal value As String)
firstNameValue = valueEnd Set
End PropertyPublic Property PurchaseHistory() As PurchaseHistory _Implements ICustomer.PurchaseHistory
GetReturn purchaseHistoryValue
End GetSet(ByVal value As PurchaseHistory)
purchaseHistoryValue = valueEnd Set
End PropertyPublic Function DiscountPercent() As Decimal _Implements ICustomer.DiscountPercent
If Me.PurchaseHistory.TotalPurchases > 10 ThenReturn 3
ElseIf Me.PurchaseHistory.TotalPurchases > 100 ThenReturn 5
ElseReturn 0
End IfEnd Function
End Class
348
Part IV: Advanced Refactorings
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 348
Figure 12-1
Listing 12-4: Delegation Instead of Inheritance for PrintService
Public Class PrintServicePrivate device As IPrintDevicePrivate jobsInQueueValue As IList(Of PrintJob)Private serviceStateValue As ServiceState
Public Sub New()Select Case My.Settings(“DeviceName”).ToString
Case “HPLaserJet”device = New HPLaserJet
Case “LexmarkX500”device = New LexmarkX500
End SelectEnd SubPublic Property Device1() As IPrintDevice
GetReturn device
End GetSet(ByVal value As IPrintDevice)
device = valueEnd Set
End PropertyPublic Property JobsInQueue() As IList(Of PrintJob)
GetReturn jobsInQueueValue
End GetSet(ByVal value As IList(Of PrintJob))
jobsInQueueValue = valueEnd Set
End PropertyPublic Property ServiceState() As ServiceState
GetReturn serviceStateValue
End GetSet(ByVal value As ServiceState)
Continued
349
Chapter 12: Advanced Object-Oriented Concepts
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 349
Listing 12-4: Delegation Instead of Inheritance for PrintService (continued)
serviceStateValue = valueEnd Set
End PropertyPublic Function CreatePrintJob() As PrintJob
Return New PrintJobEnd FunctionPrivate Sub print()
While JobsInQueue.Count > 0PrintJob(JobsInQueue.Item(0))
End WhileEnd SubPrivate Sub PrintJob(ByVal job As PrintJob)
Me.device.PrintJob(job)End Sub
End Class
Eliminating Duplication by Means of InheritanceIf you go back to Listing 12-2 for the moment and compare the implementation of the PrintJob method in LexmarkX500 and HPLaserJet, you can see that these methods are identical. The same goes for theproperty Initialized. One way that classes can share implementation is by means of inheritance. Thesolution in this case is to extract the superclass that both LexmarkX500 and HPLaserJet could inherit. Youcan name this class PrintDevice. Since the PrintJob method is calling other device-specific methods,you need to declare those methods in PrintDevice as well. However, because implementations of thesemethods differ from device to device, they have to be declared abstract, meaning that the extracted super-class has to be abstract as well. PrintDevice has to implement the IPrintDevice interface. You can seethe result of superclass extraction in Listing 12-5.
Listing 12-5: Extracted PrintDevice Abstract Superclass
Option Explicit OnOption Strict On
Imports System.IO
Public MustInherit Class PrintDeviceImplements IPrintDevicePublic Sub PrintJob(ByVal job As PrintJob) _Implements IPrintDevice.PrintJob
If Not Me.Initialized ThenMe.Initialize()
End IfStartDocument()Dim renderedDocument As Stream = RenderDocument(job)WriteDocumentToDevice(renderedDocument)EndDocument()
End SubProtected MustOverride Function RenderDocument( _ByVal job As PrintJob) As StreamProtected MustOverride Sub WriteDocumentToDevice( _ByVal data As Stream)
350
Part IV: Advanced Refactorings
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 350
Listing 12-5: Extracted PrintDevice Abstract Superclass (continued)
Protected MustOverride Sub Initialize()Protected MustOverride Sub StartDocument()Protected MustOverride Sub EndDocument()Public MustOverride Property Initialized() As Boolean
End Class
Public Class LexmarkX500Inherits PrintDevicePrivate initializedValue As BooleanPublic Overrides Property Initialized() As Boolean
GetReturn initializedValue
End GetSet(ByVal value As Boolean)
initializedValue = valueEnd Set
End PropertyProtected Overrides Function RenderDocument( _ByVal job As PrintJob) As Stream
‘device specific codeEnd FunctionProtected Overrides Sub WriteDocumentToDevice( _ByVal data As Stream)
‘device specific codeEnd SubProtected Overrides Sub Initialize()
‘device specific codeEnd SubProtected Overrides Sub StartDocument()
‘device specific codeEnd SubProtected Overrides Sub EndDocument()
‘device specific codeEnd Sub
End Class
Public Class HPLaserJetInherits PrintDevicePrivate initializedValue As BooleanPublic Overrides Property Initialized() As Boolean
GetReturn initializedValue
End GetSet(ByVal value As Boolean)
initializedValue = valueEnd Set
End PropertyProtected Overrides Function RenderDocument( _ByVal job As PrintJob) As Stream
‘device specific codeEnd FunctionProtected Overrides Sub WriteDocumentToDevice( _
Continued
351
Chapter 12: Advanced Object-Oriented Concepts
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 351
Listing 12-5: Extracted PrintDevice Abstract Superclass (continued)
ByVal data As Stream)‘device specific code
End SubProtected Overrides Sub Initialize()
‘device specific codeEnd SubProtected Overrides Sub StartDocument()
‘device specific codeEnd SubProtected Overrides Sub EndDocument()
‘device specific codeEnd Sub
End Class
Refactoring: Extract SuperclassMotivationOne way that different classes can share implementation is through inheritance.However, you cannot always identify duplicated implementations before you actuallycode the classes. Sometimes this duplication can be the result of newly added featuresor some other refactoring. Whatever the origin of duplication, you can eliminate it byextracting a superclass.
Sometimes the classes share implementation for some members and only declarationfor others. In those cases, the extracted class should be abstract, because that way itwill permit both abstract and concrete methods. If classes share only an interface, thenyou should use Extract Interface refactoring instead.
This refactoring is an alternative to Extract Class refactoring that promotes reuse by meansof delegation. In case you later on decide that delegation was a better choice, you canapply Replace Inheritance with Delegation refactoring in order to reverse the situation.
Related SmellsUse this refactoring to eliminate the Duplicated Code and Large Class smells.
Mechanics1. Create an empty abstract superclass and make the targeted class inherit
the superclass.
2. Start moving class members up to the superclass one by one. If only thedeclaration is common between future subclasses, pull only the declarationto the superclass and make the member abstract. If member implementa-tion is common as well, pull up the complete member.
3. Execute tests after each pull.
4. If one of the subclasses ends up having no members of its own because allhave been pulled up to superclass, you can eliminate it altogether.
5. Inspect the clients. If all they now use are superclass members, change thedeclared type to superclass.
352
Part IV: Advanced Refactorings
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 352
Continued
BeforePublic Class OrdinaryCustomer
Private firstNameValue As StringPrivate basketValue As ShoppingBasketPublic Property FirstName() As String
GetReturn firstNameValue
End GetSet(ByVal value As String)
firstNameValue = valueEnd Set
End PropertyPublic Property Basket() As ShoppingBasket
GetReturn basketValue
End GetSet(ByVal value As ShoppingBasket)
basketValue = valueEnd Set
End PropertyPublic Function CalculateDiscount() As Decimal
If Basket.Total > 3000 ThenReturn 2
End IfReturn 0
End FunctionEnd Class
Public Class RegisteredCustomerPrivate firstNameValue As StringPrivate historyValue As ShoppingHistoryPrivate basketValue As ShoppingBasketPublic Property FirstName() As String
GetReturn firstNameValue
End GetSet(ByVal value As String)
firstNameValue = valueEnd Set
End Property
Public Property Basket() As ShoppingBasketGet
Return basketValueEnd GetSet(ByVal value As ShoppingBasket)
basketValue = valueEnd Set
End PropertyPublic Function CalculateDiscount() As Decimal
If History.Total > 3000 ThenReturn 3
353
Chapter 12: Advanced Object-Oriented Concepts
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 353
ElseIf History.Total > 10000 ThenReturn 5
End IfReturn 0.5
End FunctionPublic Property History() As ShoppingHistory
GetReturn historyValue
End GetSet(ByVal value As ShoppingHistory)
historyValue = valueEnd Set
End PropertyEnd Class
AfterPublic Class Customer
Private firstNameValue As StringPrivate basketValue As ShoppingBasketPublic Property FirstName() As String
GetReturn firstNameValue
End GetSet(ByVal value As String)
firstNameValue = valueEnd Set
End PropertyPublic Property Basket() As ShoppingBasket
GetReturn basketValue
End GetSet(ByVal value As ShoppingBasket)
basketValue = valueEnd Set
End PropertyPublic MustOverride Function CalculateDiscount() As Decimal
End Class
Public Class OrdinaryCustomerInherits CustomerPublic Overrides Function CalculateDiscount() As Decimal
If Basket.Total > 3000 ThenReturn 2
End IfReturn 0
End FunctionEnd Class
Public Class RegisteredCustomerInherits CustomerPrivate historyValue As ShoppingHistoryPublic Property History() As ShoppingHistory
354
Part IV: Advanced Refactorings
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 354
You have now eliminated some code duplication that occurred between LexmarkX500 and HPLaserJet.As it happens, though, not all the code duplicated between the two classes has been identified. You need torefactor these classes further. You can see what you can do about leftover duplication in the next section.
Eliminating Duplication by Pulling up MembersVery often, code duplicated between the sibling classes can surface as a result of other refactorings, likemethod extraction and method parameterization. Another source of duplication is indiscriminate featureexpansion, when a member is first added to one class and later on added to another sibling without thecode in other siblings being checked.
The solution in cases wherein code is duplicated between all the siblings is to pull the member up to aparent class. This way, the duplication is eliminated and common features maintained in the parent class.
If you compare, LexmarkX500 and HPLaserJet classes from the print-system example, you will see that theproperty Initialized is identical in both classes. The solution is to pull up the Initialized property tothe PrintDevice class. The resulting code is shown in Listing 12-6.
Listing 12-6: Pulling up Initialized Property Common to LexmarkX500 and HPLaserJet
Option Explicit OnOption Strict On
Imports System.IO
Public MustInherit Class PrintDeviceImplements IPrintDevicePrivate initializedValue As BooleanPublic Property Initialized() As Boolean
GetReturn initializedValue
End GetSet(ByVal value As Boolean)
Continued
GetReturn historyValue
End GetSet(ByVal value As ShoppingHistory)
historyValue = valueEnd Set
End PropertyPublic Overrides Function CalculateDiscount() As Decimal
If History.Total > 3000 ThenReturn 3
ElseIf History.Total > 10000 ThenReturn 5
End IfReturn 0.5
End FunctionEnd Class
355
Chapter 12: Advanced Object-Oriented Concepts
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 355
Listing 12-6: Pulling up Initialized Property Common to LexmarkX500 and HPLaserJet (continued)
initializedValue = valueEnd Set
End PropertyPublic Sub PrintJob(ByVal job As PrintJob) _Implements IPrintDevice.PrintJob
If Not Me.Initialized ThenMe.Initialize()
End IfStartDocument()Dim renderedDocument As Stream = RenderDocument(job)WriteDocumentToDevice(renderedDocument)EndDocument()
End SubProtected MustOverride Function RenderDocument( _ByVal job As PrintJob) As StreamProtected MustOverride Sub WriteDocumentToDevice( _ByVal data As Stream)Protected MustOverride Sub Initialize()Protected MustOverride Sub StartDocument()Protected MustOverride Sub EndDocument()
End Class
Public Class LexmarkX500Inherits PrintDeviceProtected Overrides Function RenderDocument( _ByVal job As PrintJob) As Stream
‘device specific codeEnd FunctionProtected Overrides Sub WriteDocumentToDevice( _ByVal data As Stream)
‘device specific codeEnd SubProtected Overrides Sub Initialize()
‘device specific codeEnd SubProtected Overrides Sub StartDocument()
‘device specific codeEnd SubProtected Overrides Sub EndDocument()
‘device specific codeEnd Sub
End Class
Public Class HPLaserJetInherits PrintDeviceProtected Overrides Function RenderDocument( _ByVal job As PrintJob) As Stream
‘device specific codeEnd FunctionProtected Overrides Sub WriteDocumentToDevice( _
356
Part IV: Advanced Refactorings
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 356
Listing 12-6: Pulling up Initialized Property Common to LexmarkX500 and HPLaserJet (continued)
ByVal data As Stream)‘device specific code
End SubProtected Overrides Sub Initialize()
‘device specific codeEnd SubProtected Overrides Sub StartDocument()
‘device specific codeEnd SubProtected Overrides Sub EndDocument()
‘device specific codeEnd Sub
End Class
This concludes the redesign of the print system. In the first stage, inheritance was replaced with delegation,only to be introduced again later on. First, interface inheritance was introduced to provide polymorphicaccess to print devices, and then class inheritance to reduce duplication between different devices.
Such gradual changes to code are not uncommon, because some smells become visible only after moreimmediate ones are eliminated. The optimal design is hardly a tangible set of classes and their relations;it is a continuously changing set of decisions that corresponds best to current circumstances.
Continued
Refactoring: Pull up MethodMotivationA way to share implementation between classes is to have a common ancestor. Allmembers common to all the siblings should be contained in the parent class, in order to avoid unnecessary duplication.
When some member is completely identical in all of the child classes, in order toavoid duplication, the member should be moved from the child classes to a parentclass or interface.
Related SmellsUse this refactoring to eliminate the Duplicated Code and Large Class smells.
MechanicsThe simplest case in which to use this refactoring is when the methods in child classesare completely identical. Sometimes, however, this refactoring should be preceded byother refactorings, such as method extraction and method parameterization, until themethods are made equal.
The mechanics are similar but simpler when you are pulling up abstract method decla-rations to a parent abstract class or interface.
1. Cut and paste the method from child to base class.
357
Chapter 12: Advanced Object-Oriented Concepts
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 357
2. If the recently moved method is using some other methods in the childclasses, declare the method as abstract in the parent and make the childclasses override it.
If the method is using property available only in child classes, considermoving the property up as well.
3. Delete the method from rest of the siblings.
4. Compile and test.
BeforePublic Class Customer
Private firstNameValue As StringPrivate basketValue As ShoppingBasketPublic Property FirstName() As String
GetReturn firstNameValue
End GetSet(ByVal value As String)
firstNameValue = valueEnd Set
End PropertyPublic Property Basket() As ShoppingBasket
GetReturn basketValue
End GetSet(ByVal value As ShoppingBasket)
basketValue = valueEnd Set
End PropertyPublic MustOverride Function CalculateDiscount() As Decimal
End Class
Public Class OrdinaryCustomerInherits CustomerPublic Overrides Function CalculateDiscount() As Decimal
If Basket.Total > 3000 ThenReturn 2
End IfReturn 0
End FunctionPublic Sub SaveShoppingBasket()
shoppingData.Save(Me, Me.Basket)End Sub
End Class
Public Class RegisteredCustomerInherits CustomerPrivate historyValue As ShoppingHistoryPublic Property History() As ShoppingHistory
Get
358
Part IV: Advanced Refactorings
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 358
Continued
Return historyValueEnd GetSet(ByVal value As ShoppingHistory)
historyValue = valueEnd Set
End PropertyPublic Overrides Function CalculateDiscount() As Decimal
If History.Total > 3000 ThenReturn 3
ElseIf History.Total > 10000 ThenReturn 5
End IfReturn 0.5
End FunctionPublic Sub SaveShoppingBasket()
shoppingData.Save(Me, Me.Basket)End Sub
End Class
AfterPublic Class Customer
Private firstNameValue As StringPrivate basketValue As ShoppingBasketPublic Property FirstName() As String
GetReturn firstNameValue
End GetSet(ByVal value As String)
firstNameValue = valueEnd Set
End PropertyPublic Property Basket() As ShoppingBasket
GetReturn basketValue
End GetSet(ByVal value As ShoppingBasket)
basketValue = valueEnd Set
End PropertyPublic MustOverride Function CalculateDiscount() As DecimalPublic Sub SaveShoppingBasket()
shoppingData.Save(Me, Me.Basket)End Sub
End Class
Public Class OrdinaryCustomerInherits CustomerPublic Overrides Function CalculateDiscount() As Decimal
If Basket.Total > 3000 ThenReturn 2
End If
359
Chapter 12: Advanced Object-Oriented Concepts
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 359
Making Use of GenericsGenerics add additional type safety to your code and liberate you from writing tedious typecastingcode. Existing classes can often be further enhanced by the means of generics, which permit parametertype to be declared at the point of usage.
Very often, a good signal that generics can be employed is that the Object type is used in declarations.An additional signal of the possibility for upgrade to generic types is the use of container types from the System.Collections namespace. Generic containers are placed inside the System.Collections.Generic namespace and have close correspondence with containers from theSystem.Collections namespace.
In practice, you rarely operate in the same way on objects that are different in type. You can operate onobjects of different types, but through polymorphic mechanisms, meaning that those objects belong tothe same supertype. Even in those cases, a class can be made generic, and a parameter type can be a common supertype.
When introducing generics, you should make sure that all uses of Object correspond to the same type.Generic types can be parameterized with more than one parameter type, so for each implicit type, a newparameter type should be used.
Return 0End FunctionEnd Class
Public Class RegisteredCustomerInherits CustomerPrivate historyValue As ShoppingHistoryPublic Property History() As ShoppingHistory
GetReturn historyValue
End GetSet(ByVal value As ShoppingHistory)
historyValue = valueEnd Set
End PropertyPublic Overrides Function CalculateDiscount() As Decimal
If History.Total > 3000 ThenReturn 3
ElseIf History.Total > 10000 ThenReturn 5
End IfReturn 0.5
End FunctionEnd Class
360
Part IV: Advanced Refactorings
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 360
Continued
Refactoring: Replace General-Purpose Reference with Parameter Type
MotivationBefore generics were introduced to VB, the only way you could write code that dealtwith objects of any type was to declare a reference as an Object. Sometimes it is usefulto limit the type of manipulated objects at the point of usage for the following reasons:
❑ Additional type safety is provided.
❑ Duplicate typecasting code can be eliminated.
Related SmellsUse this refactoring to eliminate the duplicated code and to turn dynamically typedcode into strongly statically typed explicit code.
MechanicsGeneral-purpose reference can often be found when the type Object is employed asthe declaring type.
1. Identify how many different underlying types are declared as Object. Forexample, IList interface references only one type, one that is added andretrieved from the list, while IDictionary references two: one is used as akey, and the other is stored in the dictionary.
2. For each different underlying type discovered in Step 1, add one typeparameter in the class declaration.
3. Replace Object with the parameter type declared in Step 2.
BeforeOption Explicit OnOption Strict On
Public Class PerishableContainerPrivate list As ArrayList = New ArrayListPrivate perishIntervalInSecondsValue As IntegerPrivate timeInStorage As IDictionary = New Hashtable
Public Property PerishIntervalInSeconds() As IntegerGet
Return perishIntervalInSecondsValueEnd GetSet(ByVal value As Integer)
perishIntervalInSecondsValue = valueEnd Set
End PropertyPublic Sub LeaveInStorage(ByVal item As Object)
list.Add(item)timeInStorage.Add(item, DateAndTime.TimeOfDay)
End Sub‘...
361
Chapter 12: Advanced Object-Oriented Concepts
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 361
Private Function TakeOldestFromStorage() As ObjectIf list.Count > 0 Then
Dim item As Object = _CType(list.Item(0), Object)list.Remove(item)If Not HasPerished(item) Then
Return itemEnd If
End IfReturn Nothing
End FunctionPublic Function TakeFromStorage() As Object
Dim item As Object = NothingWhile list.Count > 0
item = TakeOldestFromStorage()If item IsNot Nothing Then
Exit WhileEnd If
End WhileReturn item
End FunctionPrivate Function HasPerished( _ByVal item As Object) As Boolean
If CType(timeInStorage.Item(item), DateTime).AddSeconds(_
perishIntervalInSecondsValue) > Date.Now ThenReturn False
End IfReturn True
End FunctionPublic ReadOnly Property Count() As Integer
GetReturn list.Count
End GetEnd Property
End Class
AfterOption Explicit OnOption Strict On
Imports System.Collections.Generic
Public Class PerishableContainer(Of Perishable)Private list As IList(Of Perishable) = New List(Of
Perishable)Private perishIntervalInSecondsValue As IntegerPrivate timeInStorage As IDictionary(Of Perishable, DateTime)
= _
362
Part IV: Advanced Refactorings
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 362
New Dictionary(Of Perishable, DateTime)
Public Property PerishIntervalInSeconds() As IntegerGet
Return perishIntervalInSecondsValueEnd GetSet(ByVal value As Integer)
perishIntervalInSecondsValue = valueEnd Set
End PropertyPublic Sub LeaveInStorage(ByVal item As Perishable)
list.Add(item)timeInStorage.Add(item, DateAndTime.TimeOfDay)
End Sub‘...Private Function TakeOldestFromStorage() As Perishable
If list.Count > 0 ThenDim item As Perishable = list.Item(0)list.Remove(item)If Not HasPerished(item) Then
Return itemEnd If
End IfReturn Nothing
End FunctionPublic Function TakeFromStorage() As Perishable
Dim item As Perishable = NothingWhile list.Count > 0
item = TakeOldestFromStorage()If item IsNot Nothing Then
Exit WhileEnd If
End WhileReturn item
End FunctionPrivate Function HasPerished( _ByVal item As Perishable) As Boolean
If CType(timeInStorage.Item(item), DateTime).AddSeconds(_
perishIntervalInSecondsValue) > Date.Now ThenReturn False
End IfReturn True
End FunctionPublic ReadOnly Property Count() As Integer
GetReturn list.Count
End GetEnd Property
End Class
363
Chapter 12: Advanced Object-Oriented Concepts
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 363
Inheritance and Generic Types in the Rent-a-Wheels Application
You have probably noticed by now in the Rent-a-Wheels application that various Data classes (BranchData,ModelData, and so on) have a number of identically declared and implemented methods. You have alsonoticed that these classes are used in a similar way, except for the domain objects they work upon. The pres-ence of identical methods means that code duplication is going on between the classes. From this, a few con-clusions can be reached:
❑ Data classes share some of their implementation, mainly private methods like AddParameteror ExecuteNonQuery.
❑ Interfaces are almost identical, except for the single object the classes operate on. For example, subDelete is in BranchData declared as Public Sub Delete(ByVal branch As Branch), whilein ModelData, the method is declared as Public Sub Delete(ByVal model As Model).
❑ Duplication can be avoided by superclass extraction.
Extracting SuperBecause some of the methods between different Data classes differ in implementation, the superclasshas to be abstract. You can call it AbstractData, and you can move all identical methods to it. Becausethe majority of these methods are private, visibility has to be changed to Protected so that subclassescan make use of these methods. These methods are AddParameter, ExecuteNonQuery, FillDataset,and so on.
After this extraction is performed, it becomes obvious that some of the methods, such asPrepareDataObjects, are used only by methods that found their way to the superclass, meaning thatthese methods can stay private.
Employing Generics Declaration of public methods differs only in the type of domain object upon which the Data classmanipulates. You can make AbstractData a parameterized class and let the domain object be definedat the point of usage. So AbstractData is declared as a generic class.
Public MustInherit Class AbstractData(Of PersistedObject)‘...
Public MustOverride Sub Delete(ByVal persisted As PersistedObject)End Class
The public methods now receive PersistedObject and can be overridden and implemented in each ofthe child data classes.
It is actually the child Data (BranchData, ModelData and so on) that parameterizes the abstract super-class AbstractData and defines the parameter type as Branch, Model, and so on. So BranchData isdeclared as follows:
Public Class BranchDataInherits AbstractData(Of Branch)
364
Part IV: Advanced Refactorings
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 364
‘...Public Overrides Sub Delete(ByVal branch As Branch)
Dim command As IDbCommand = New SqlCommandAddParameter(command, IdParamterName, DbType.Int32, _branch.Id)ExecuteNonQuery(command, DeleteBranchSql)
End Sub
This way, AbstractData can expose a common interface in the form of public methods like Deleteand Update that use a parameterized type. Only the parameterized GetAll method provokes change inform classes, because it uses a parameterized list to return a list of all domain objects. The declaration ofthe GetAll method in BranchData looks like this:
Public Overrides Function GetAll() As IList(Of Branch)
It seems that the Data class can be a very useful and reusable class that can serve as a base of futureconcrete Data class implementations. With this, you have managed to organize the Data classes into ahierarchy that both reduces the duplication and makes it possible for Data classes to be used in a poly-morphic manner.
You may still be concerned with one thing, however. AbstractData depends on a concrete ADO.NET provider, meaning that it would not be easy to change the system so that it uses a differentdatabase motor.
Extract Data Objects Provider Class To make AbstractData database-neutral, you can extract the data provider object-creation code to sep-arate the AbstractDataObjectsProvider class:
Option Explicit OnOption Strict On
Public MustInherit Class AbstractDataObjectsProviderPublic MustOverride Function InstantiateAdapter() _As System.Data.IDbDataAdapterPublic MustOverride Function InstantiateConnection() _As System.Data.IDbConnection
End Class
AbstractData can hold a field of AbstractDataObjectsProvider and delegate data-object creationto this class. You can also make the child classes supply the concrete DataObjectsProvider through aconstructor method, together with a connection string.
The BranchData constructor now looks like this:
Public Sub New()MyBase.New(BranchData.MSSQLConnectionString, _New MSSQLDataObjectsProvider)
End Sub
365
Chapter 12: Advanced Object-Oriented Concepts
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 365
The MsSqlDataObjectsProvider code contains simple creation code:
Option Strict OnOption Explicit On
Imports System.Data.SqlClient
Public Class MsSqlDataObjectsProviderInherits AbstractDataObjectsProviderPublic Overrides Function InstantiateAdapter() _As System.Data.IDbDataAdapter
Return New SqlDataAdapterEnd FunctionPublic Overrides Function InstantiateConnection() _As System.Data.IDbConnection
Return New SqlConnectionEnd Function
End Class
With this you have moved the dependency on a certain data provider down in the hierarchy. What iseven more important is that provider creation code is concentrated on a single line, so all it takes tochange the provider is to change a single line in the concrete data classes and to implement anotherDataObjectProvider. You can see the resulting design in Figure 12-2.
Figure 12-2
Now take a look at the most important new code for Rent-a-Wheels that resulted from the refactoringsdescribed in this chapter. It is shown in Listing 12-7.
Listing 12-7: Illustrative Rent-a-Wheels Classes After Advanced Refactorings
Option Strict OnOption Explicit On
Imports System.Data
Public MustInherit Class AbstractData(Of PersistedObject)Private connectionStringValue As StringPrivate providerValue As AbstractDataObjectsProviderPublic Sub New(ByVal connectionString As String, _ByVal provider As AbstractDataObjectsProvider)
Me.ConnectionString = connectionStringMe.Provider = provider
BranchData ModelData VehicleCategoryData VehicleData MsSqlDataObjectsProvider
AbstractData
PersistedObject AbstractDataObjectsProvider
366
Part IV: Advanced Refactorings
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 366
Listing 12-7: Illustrative Rent-a-Wheels Classes After Advanced Refactorings (continued)
End SubPublic Property ConnectionString() As String
GetReturn connectionStringValue
End GetSet(ByVal value As String)
connectionStringValue = valueEnd Set
End PropertyPublic Property Provider() As AbstractDataObjectsProvider
GetReturn providerValue
End GetSet(ByVal value As AbstractDataObjectsProvider)
providerValue = valueEnd Set
End PropertyPrivate Function CreateConnection() As IDbConnection
Dim connection As IDbConnection = Provider.InstantiateConnection()connection.ConnectionString = Me.ConnectionStringReturn connection
End FunctionPrivate Function PrepareDataObjects(ByVal command As IDbCommand, _ByVal sql As String) As IDbConnection
Dim connection As IDbConnection = CreateConnection()connection.Open()command.Connection = connectioncommand.CommandText = sqlReturn connection
End FunctionProtected Sub AddParameter(ByVal command As IDbCommand, _ByVal parameterName As String, ByVal parameterType As DbType, _ByVal paramaterValue As Object)
Dim parameter As IDbDataParameter = command.CreateParameter()parameter.ParameterName = parameterNameparameter.DbType = parameterTypeparameter.Value = paramaterValuecommand.Parameters.Add(parameter)
End SubProtected Function FillDataset(ByVal command As IDbCommand, _ByVal sql As String) As DataSet
Dim connection As IDbConnection = PrepareDataObjects(command, sql)Dim adapter As IDbDataAdapter = Provider.InstantiateAdapter()Dim dataSet As New DataSetadapter.SelectCommand = commandadapter.Fill(dataSet)connection.Close()Return dataSet
End FunctionProtected Sub ExecuteNonQuery(ByVal command As IDbCommand, _ByVal sql As String)
Continued
367
Chapter 12: Advanced Object-Oriented Concepts
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 367
Listing 12-7: Illustrative Rent-a-Wheels Classes After Advanced Refactorings (continued)
Dim connection As IDbConnection = PrepareDataObjects(command, sql)command.ExecuteNonQuery()connection.Close()
End SubPublic MustOverride Function GetAll() As IList(Of PersistedObject)Public MustOverride Sub Delete(ByVal persisted As PersistedObject)Public MustOverride Sub Update(ByVal persisted As PersistedObject)Public MustOverride Sub Insert(ByVal persisted As PersistedObject)
End Class
Imports System.DataImports System.Data.SqlClientImports RentAWheel.Data.ColumnNames
Public Class BranchDataInherits AbstractData(Of Branch)
Private Const MSSQLConnectionString As String = _“Data Source=LOCALHOST;Initial Catalog=RENTAWHEELS;” + _“User ID=RENTAWHEELS_LOGIN;Password=RENTAWHEELS_PASSWORD_123”
Private Const SelectAllFromBranchSql As String = _“Select * from Branch”Private Const DeleteBranchSql As String = _“Delete Branch Where BranchId = @Id”Private Const InsertBranchSql As String = _“Insert Into Branch (BranchName) Values(@Name)“Private Const UpdateBranchSql As String = _“Update Branch Set BranchName = @Name Where BranchId = @Id”
Private Const IdParamterName As String = “@Id”Private Const NameParameterName As String = “@Name”
Public Sub New()MyBase.New(BranchData.MSSQLConnectionString, _New MsSqlDataObjectsProvider)
End Sub
Public Overrides Sub Delete(ByVal branch As Branch)Dim command As IDbCommand = New SqlCommandAddParameter(command, IdParamterName, DbType.Int32, _branch.Id)ExecuteNonQuery(command, DeleteBranchSql)
End SubPublic Overrides Sub Insert(ByVal branch As Branch)
Dim command As IDbCommand = New SqlCommandAddParameter(command, NameParameterName, DbType.String, _branch.Name)ExecuteNonQuery(command, InsertBranchSql)
End SubPublic Overrides Sub Update(ByVal branch As Branch)
Dim command As IDbCommand = New SqlCommand
368
Part IV: Advanced Refactorings
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 368
Listing 12-7: Illustrative Rent-a-Wheels Classes After Advanced Refactorings (continued)
AddParameter(command, NameParameterName, DbType.String, _branch.Name)AddParameter(command, IdParamterName, DbType.Int32, _branch.Id)ExecuteNonQuery(command, UpdateBranchSql)
End SubPublic Overrides Function GetAll() As IList(Of Branch)
Dim command As IDbCommand = New SqlCommandDim branchesSet As DataSet = FillDataset(command, _SelectAllFromBranchSql)Dim table As DataTable = branchesSet.Tables(0)Dim branches As IList(Of Branch) = New List(Of Branch)For Each row As DataRow In table.Rows
branches.Add(New Branch(CInt(row.Item(BranchTable.Id)), _row.Item(BranchTable.Name).ToString))
NextReturn branches
End FunctionEnd Class
As was the case with other chapters, the complete code is available for download at the book’s website onwww.wrox.com, and I recommend you analyze it in more depth. The refactorings performed on Rent-a-Wheels in this chapter were quite complex and have significantly changed the application’s internals.
SummaryThis chapter dealt with some advanced and powerful object-oriented concepts. You have witnessed thepower and dangers of inheritance. With great power comes great responsibility. So you have to be care-ful not to misuse inheritance, or your losses will soon outweigh the benefits.
Even when inheritance is ill applied, there is still a solution. You can refactor your code so it uses delega-tion instead of inheritance. And in cases where inheritance is well applied, it still might need a bit oftweaking. You can do this by moving members up and down the hierarchy.
In cases where you can see that you could benefit from inheritance, you can perform Extract Interfaceand Extract Superclass refactoring. By extracting an interface, you open the door for polymorphic mech-anisms in your code, and by extracting a superclass, you reduce code duplication between the siblings.
In the Rent-a-Wheels example, you have seen these advanced refactorings in practice and how, whencombined, they can provide a solution to a number of problems, increasing the flexibility and reusevalue of your code.
The next chapter will wrap up the story on inheritance. I will also talk about large-scale code organiza-tion and give some insight into the concepts of namespace, assembly, and related refactorings.
369
Chapter 12: Advanced Object-Oriented Concepts
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 369
79796c12.qxd:WroxPro 2/23/08 8:39 AM Page 370
Code Organization on a Large Scale
Until now, I have hardly mentioned Visual Basic’s large-scale organizational mechanisms like name-spaces and assemblies. The organization of your project and the partitioning of types to namespacesand assemblies can have a profound effect on the maintainability of your project and the develop-ment process in general.
Many of these issues are not apparent with small projects. They become much more relevant asprojects grow and the number of types reaches hundreds or even thousands. Dependencies, on theother hand, are notorious to multiply with the age of the project.
This does not mean that these issues should be taken lightly with smaller projects. Projects have a tendency to grow and decisions that you make early on can result in important consequencesmuch later.
In this chapter, you will:
❑ See criteria that can be applied when using namespaces and assemblies to organize your project.
❑ Read about dependencies and see why it is important to keep dependencies between different organizational units at bay.
❑ Take a look at file-level organization of code, partial types, and how all this affects formsinheritance in Visual Basic.
NamespacesMany real-life projects can amass hundreds, if not thousands, of types. Without namespaces, it wouldbe difficult for programmers to find their way in projects that large. Namespaces let you group related
79796c13.qxd:WroxPro 2/22/08 6:30 PM Page 371
types and avoid name collisions. Each class (or any other type-like interface, structure, or enumeration, andso on) has a full name that consists of the type name itself plus the name of the namespace they belong to.For example, the ArrayList class belongs to the System.Collections namespace. While namespace isactually more than simply a label for a class, it is important that its name is chosen wisely.
Naming Guidelines and Namespace OrganizationOne of the most important things when creating namespaces is to choose appropriate and logical names.The guidelines for choosing namespace names do not differ that much from the guidelines for namingclasses. Names should be descriptive and clearly indicate the purpose of the namespace. For example, thesystem namespace System.IO clearly indicates that types in that namespace are used for reading and writing to files and data streams.
Therefore, placement of a type inside a certain namespace is by no means arbitrary. All types in a name-space should have a related purpose. This helps produce consistent library design and eases the use oftypes. Such organization is easily navigated with the help of IntelliSense in Visual Studio, as you canappreciate in Figure 13-1.
Figure 13-1
Nested NamespacesBecause namespaces can be nested, they can form logical hierarchies where more general types are placedfurther up in the hierarchy and more specific types are placed further down the hierarchy chain. Forexample, the System.Data namespace contains general data interface types such as IDbConnection,while System.Data.SqlClient contains implementation classes specific to the Microsoft SQL Serverdata provider such as System.Data.SqlClient.SqlConnection.
Changing the Root Namespace NameWhen you create a new project in Visual Studio, it is automatically assigned a root namespace, based on the project name. This name chosen by the IDE can be changed and need not have any relation withthe project or assembly name. The root namespace can be changed at the Project Properties page.
372
Part IV: Advanced Refactorings
79796c13.qxd:WroxPro 2/22/08 6:30 PM Page 372
Microsoft recommends using CompanyName.TechnologyName as a root namespace, so you can set thisto be a root namespace for your project. Remember that the root namespace is prepended to the nameyou write using a Namespace statement in your code. When you import the type to a class in anothernamespace, you must specify the full namespace, including the root namespace.
Using Import StatementsWhen you program, you should always use the full name of the class. The compiler will not find anyclass only by its short name unless the class belongs to the built-in System namespace, like String, orObject. To avoid typing long names, which would be rather tedious, you can use an Imports statementin an import section at the top of the file that enables you to specify the long name one time only orimport the containing namespace. You should avoid using full type names in the body of your type,because they make the code harder to read. Not only does this mean more clutter in the body of the type,it also means that the Imports section will fail to provide useful dependency information when youscan your code in browsing mode.
Because you import the whole namespace by the means of an Imports statement, you should strive to maintain the number of classes in the namespace on a moderate level. Even so, a name collision canoccur. Name collision happens when two or more imported namespaces contain the type with the samename. In those cases, you can use the alias part of an import statement to disambiguate collided names.For example:
Imports System.XmlImports ExtendedNode = ParsingCompany.ExtendedParser.XmlNode
Because System.Xml contains XmlNode class, it collides with ParsingCompany.ExtendedParser.XmlNode name. By introducing the ExtendedNode alias for ParsingCompany.ExtendedParser.XmlNode, you can use short names for both types in the same class.
Smell: Implicit ImportsDetecting the SmellThis smell is generally easily detected by simple visual inspection. Long Pascal-casedstatements such as System.Collections.Generic.IList should stand out from therest of the code in the body of the class.
Related RefactoringUse Explicit Imports refactoring to eliminate this smell.
RationaleUsing long type names in the body of your type means more clutter and more duplica-tion in your code. It will also render an Imports section uninformative, because there isno guarantee that it contains all the namespaces used by your class.
373
Chapter 13: Code Organization on a Large Scale
79796c13.qxd:WroxPro 2/22/08 6:30 PM Page 373
Your Visual Basic project can contain a number of namespaces. Each namespace can spawn a number ofsource files and even a number of projects, but the most common way to organize namespace is to placeit inside the single project. That means that when you compile your project, your namespace will be con-tained inside the assembly, and this will convert the assembly in the unit of release and deployment.
Refactoring: Explicit ImportsMotivationUsing long type names in your code adds more clutter to your code and makes it diffi-cult to read. It also means that you cannot count on the Imports section to give youthe full breakdown of your type’s dependencies. Finally, it means more duplicationthat can be avoided by the means of import statements.
Related SmellsUse this refactoring to eliminate Implicit Imports smell.
Mechanics1. Start by adding the Imports statement for the type that was used
implicitly.
2. Eliminate the full name part from the type that was used implicitly.
3. Perform textual search on the imported namespace in order to discoverother instances of implicit imports. If more instances are discovered, pro-ceed to eliminate the namespace part from the name of the type.
BeforeOption Explicit OnOption Strict On
Public Class PurchaseHistory
Private itemsValue As _System.Collections.Generic.IList(Of Item)‘...
End Class
AfterOption Explicit OnOption Strict On
Imports System.Collections.Generic
Public Class PurchaseHistory
Private itemsValue As IList(Of Item)‘...
End Class
374
Part IV: Advanced Refactorings
79796c13.qxd:WroxPro 2/22/08 6:30 PM Page 374
AssembliesAn assembly is the compiled product of your VB project. Assemblies can contain a number of namespacesthat can contain a number of types. Besides compiled code, each assembly contains a Win32 header, a CILheader, type metadata, an assembly manifest, and optionally, resources as images and string tables if addedto the assembly. Assemblies are large, physical building blocks. In .NET you construct your applications bycombining different assemblies.
If you choose Windows Application as the application type of your assembly, the compiler generates anexecutable file, and you can run your process assembly as an application under Windows. Double-clickingthe file initiates the execution by launching a startup object that can be Windows form or a shared SubMain method. If you choose Class Library as the assembly application type, then the compiler generates adynamically linked library, and you can invoke and use this assembly from some other assembly. This wayyou can reuse the compiled assembly without ever seeing the code that was used to produce it. This type ofreuse is known as binary reuse, and it has a number of benefits compared to reuse on the code level.
Binary ReuseImagine you have programmed a number of classes that resolve important functionality in your applica-tion. Then you start working on some other application, and you are faced with almost the same prob-lem. Obviously, because you have already programmed the code that solves this recurring problem, youwould not want to write the code all over again. One option is to add all the source files from originalproject to the new project and in this way reuse the code. All types, including ones that were developedfor the first project are compiled into the new assembly. This type of reuse is known as source-based reuse.
Another option is to add a reference to the original project’s compiled assembly and to reuse the func-tionality provided by the assembly. This type of reuse is known as binary reuse. Binary reuse is in manyways superior to source-based reuse. The next sections take a look at some of the benefits that binaryreuse brings to your development process.
Encapsulation and ModularityThe great thing about programming is that you can count on many ready-made building blocks whenyou construct your application. Because you can compose your application out of different pieces, youcan attack bigger problems by dividing them into smaller ones. Many times you can find assemblies thatsolve many of the problems you need to resolve in order to construct your application. In this way theassembly becomes the largest building block of your development process.
Because assembly is a binary, most of the time you do not have access to the code that was used to createit. Generally, you get the documentation, you can see types and signatures in an object browser, and youcan count on help from IntelliSense in order to make use of the assembly. However, code and internalimplementation details are hidden from your view. At first, this can be seen as a limitation, and youmight be tempted to look into the code of assembly that you use.
As a matter of fact, not having access to source code of an assembly is just another level of encapsulation. In this way an enormous amount of complexity is taken from your shoulders. Just imagine a project whereyou would have the code for System.IO and System.Data namespaces inside it. This would raise the linecount and the complexity of your project by several orders of magnitude. Even the simplest program mightturn unmanageable once you start to suspect that there is a bug somewhere inside the System namespaceand place the breakpoint inside the code in order to investigate it.
375
Chapter 13: Code Organization on a Large Scale
79796c13.qxd:WroxPro 2/22/08 6:30 PM Page 375
Versioning When you use someone else’s assembly, you expect it to be tried and tested, well documented, and with a well-designed API that is easy to use. Not only this, but you can rightfully expect the assembly’s creator to maintain the assembly. If there is a bug in the assembly, the creator should provide a new release thatresolves the bug. And because the creator is in the business of making software, it is only logical to expectthe assembly to evolve in time and that the creator will provide more and better functionality in the nextmajor release of the assembly. So, you need to be able to use assemblies that change in time, but you alsowish to avoid the problems that can result from the evolution of assemblies that you use:
❑ Because the assembly interface can change in time, you do not want your application to breakwhen a new version of the assembly you use is released.
❑ You do not want to be forced to recompile and redistribute your assembly just so you can use aminor new bug-fixing version of some assembly that you use.
❑ Finally, you want to be able to manage your own schedule of assembly upgrades.
Because in .NET, assemblies are versioned and assembly versioning mechanisms are built into the .NET run-time, you can avoid all of the aforementioned problems. You can declare that your assembly depends on a specific version of some other assembly, and you can choose if you want your assembly to use a newer version.Just imagine the complexity of these versioning procedures if you had to perform them on source code level.
See Chapter 8 for more information on assembly versioning schema and .NET version managementmechanisms.
Memory ResourcesBecause assemblies in .NET are dynamically linked and loaded, multiple application domains can refer-ence a single assembly that can be distributed in its own file. This reduces disk memory consumptionand can reduce time to download and start the application over the Internet. Each assembly is loadedonly once into the computer RAM. This means that multiple applications running and using the sameassembly at the same time will not provoke the assembly to be loaded multiple times, resulting in moreefficient use of system resources.
Strong SecurityAssemblies can be given strong names and can be digitally signed. This way, you can guarantee thatyour application is not using some other assembly that happened to have the same name as the oneused by your application and that could have been maliciously put in place of original assembly. Youcan also guarantee that the original assembly has not been tampered in any way and that when a newversion is provided, it is indeed provided by the same author you trust. You can also define permis-sions and security policies for your assemblies. The comprehensive set of security features makesassemblies safe for use and distribution even in the most demanding environments like the Internet.
Intellectual Property ProtectionAssemblies do not contain source code. When obfuscated, they can be extremely difficult to reverse-engineer. This makes an assembly an excellent platform for commercial software distribution. It alsoadds an additional level of security to your project, because no one can analyze the original source insearch for possible vulnerabilities. You can rest assured that generating the source code from the binaryand then using and modifying the code in order to compile malicious surrogate versions of the assemblywill be extremely difficult and complex.
376
Part IV: Advanced Refactorings
79796c13.qxd:WroxPro 2/22/08 6:30 PM Page 376
Multilanguage ReuseAn assembly programmed in any .NET language can be referenced from any .NET assembly no matterwhat language it is programmed in. This way it is possible for an assembly programmed in Visual Basicto reuse code programmed in C# or managed C++. This brings additional flexibility to your develop-ment process and opens up the field for reuse in .NET.
Now that you have seen some of the benefits of binary reuse, you are probably thinking about assem-bly organization. How do you decide on what namespaces the assembly should contain? And, whatcriteria should you apply when placing types inside the namespaces? The next section addresses those questions.
Namespace Organization GuidelinesAs projects grow in size, using classes as the sole organizational unit soon becomes insufficient. Thenamespace is a higher-level organizational unit that can group classes. The design of namespaces evolvesover time, as a result of changes in class organization that affect the way you build, maintain the system,and perform reuse. Sometimes, these forces can work against each other, or in time the balance canchange. So, it’s important to start by looking into how namespace organization affects the maintainabilityof your code.
MaintainabilityIn Chapter 11 you saw how the Single Responsibility Principle works out on classes. It states that a class should have only one reason to change. The same principle can be applied to the namespacelevel. If there are new requirements for changes, it is best if changes can be limited to a minimumnumber of namespaces. If classes are conceptually closely related or, because of the way they areimplemented, they belong inside the same namespace, then this principle applied to namespacesmeans that classes belonging to same namespace should have the same motive for change.
Smell: Large NamespaceDetecting the SmellUse class view to detect this smell. If you expand the namespace and it shows a largelist of classes, you should investigate further such namespace organization.
Related RefactoringUse Move Class to Namespace and Extract Namespace refactorings to eliminate this smell.
RationaleNamespaces are higher-order organizational units. By using them improperly, youreduce the maintainability and reusability of your code. A large namespace, especiallyif coupled with a non-coherent namespace, is poised to become a magnet and hotbedfor sprouting dependencies.
377
Chapter 13: Code Organization on a Large Scale
79796c13.qxd:WroxPro 2/22/08 6:30 PM Page 377
ReuseThe first principle regarding reuse and class distribution is the Reuse-Release Equivalence principle.Reusable classes are not just programmed and compiled; such classes have to be supported by an inte-gral development process. In order to be able to put versioning and release practices in place, youshould separate classes intended for reuse from classes not intended to be reused.
Object-Oriented Design Principle: Reuse-Release EquivalenceIn order to be able to successfully reuse classes, it is not enough that the class is wellwritten and well designed. Some development process considerations, besides puresoftware design arguments, have to be taken into account. When you reuse someclasses, you have to be sure that you will get support when you need it and that anybugs you might encounter will be resolved. But first of all, you need a mechanism thatwill make it possible to identify the exact version of the classes you are using. Reusableclasses have to be released with versioning policies in place and in such a manner sothat the programmer using the classes can decide what version to use and when to perform an upgrade.
DefinitionTo quote Robert C. Martin from his book Agile Software Development: Principles, Patterns,and Practices (Prentice Hall, 2002), “The granule of reuse is the granule of release.”
As a consequence, you should keep classes intended for reuse together and apart fromclasses not intended for reuse.
Smell: Non-Coherent NamespaceDetecting the SmellThis smell can often be discovered indirectly. If you take a look at the package diagram,this namespace can have a large number of incoming and outgoing dependencies.
Related RefactoringUse Move Class to Namespace and Extract Namespace refactorings to eliminate this smell.
RationaleA non-coherent namespace can come in many forms. For example, it can be a name-space that mixes:
❑ Classes intended for reuse with classes not intended for reuse
❑ Classes with different responsibilities
❑ Classes with different levels of abstractness and stability
378
Part IV: Advanced Refactorings
79796c13.qxd:WroxPro 2/22/08 6:30 PM Page 378
Distinguishing classes intended for reuse from classes programmed without such a purpose in mind isonly the first indicator of how to organize your classes through namespaces. Different groups of classesare reused for different purposes. For example, a ShoppingBasket class can be used while building anonline store application whereas an IDbConnection interface can be used for interacting with a data-base store in any application that needs to provide persistence features.
However, as it happens, some classes are reused together. It is highly probable that if you use aShoppingBasket class, you will also use a BasketItem class. On the other hand if you use anIDbConnection interface, you will also use an IDbCommand interface. This leads to the following con-clusion: certain related classes are reused together. When you place classes inside namespaces, youshould try to put classes that are reused together inside the same namespace.
ExampleWhile programming an online store purchase form, you create a new zip code validatorclass and you place it in the same namespace and assembly as the web GUI classes.
Namespace BookStore.Web.PurchasePublic Class PurchaseForm
‘...End Class
Public Class ZipValidator‘...
End ClassEnd Namespace
The problem with such partitioning is that a reusable ZipValidator class is placedinside the same namespace and assembly as other classes not intended to be reused. Ifyou reuse the ZipValidator, you then have to accept that versioning and releasecycle that is influenced by other non-reusable classes, if such mechanisms are at all putin place for the GUI classes.
Namespace BookStore.Web.PurchasePublic Class PurchaseForm
‘...End Class‘...
End Namespace
Namespace Web.Common.ValidatorsPublic Class ZipValidator
‘...End Class‘...
End Namespace
By placing the validator class together with other reusable classes, you are not suscep-tible to the effects that changes in GUI classes can produce. This way the validator classis part of the namespace intended for reuse and with the release cycle and versioningpolicies intended and planned for reuse. Needless to say, just placing the class insidethe right namespace cannot fix your development process; correct development prac-tices have to be put in place.
379
Chapter 13: Code Organization on a Large Scale
79796c13.qxd:WroxPro 2/22/08 6:30 PM Page 379
As an additional illustration, take a look at the existing .NET Framework classes in the System.Collections.Generic namespace. If you use the Dictionary class, you will most probably also use the KeyValuePair and the KeyNotFoundException classes.
Refactoring: Move Class to NamespaceMotivationSometimes, dependency problems and the Non-Coherent Namespace smell can beresolved by moving the class to proper namespace.
Related SmellsUse this refactoring to eliminate Non-Coherent Namespace and Cyclic Dependencies smell.
Mechanics1. If the originating and target namespaces are in different assemblies, start
by moving the source file to the target assembly. (In case there are multipletypes in the source file, you will need to perform Move Type to File refactor-ing before this step.)
2. Change the namespace declaration in the class to targeted namespace.Remember that assemblies often declare the root namespace.
3. Search for references to the type. Use the compiler to locate places wherethe type was used. It will display a “Type typename is not defined” mes-sage. Modify the imports section so it imports the namespace that the typenow belongs to.
BeforeNamespace Web.ShoppingBasket
Public Class PerishableContainer‘...
End ClassPublic Class Basket
‘...End Class‘...
End Namespace
AfterNamespace Web.ShoppingBasket
Public Class Basket‘...
End Class‘...
End Namespace
Namespace Common.ContainersPublic Class PerishableContainer
‘...End Class‘...
380
Part IV: Advanced Refactorings
79796c13.qxd:WroxPro 2/22/08 6:30 PM Page 380
So what is the benefit of placing classes reused together in the same package? The response to this ques-tion has to do with the issue of dependencies. The next section takes a look at what dependencies areand why they are important for your development process and application architecture.
Dependency ConsiderationsWhen you reference a certain type in your code, you are establishing the dependency between the typeyou referenced and the type that is making the reference. The effect of dependency is the following:
❑ If the type that your type depends upon changes, you most probably have to change and recom-pile the dependent type.
❑ Even when the change in the type that other class depends upon is implementation-only, meaningthat no signatures in the type have changed, you still have to test dependent type in order to makesure that the latest change didn’t result in a new bug.
❑ If the dependency related types belong to different namespaces that means that there is adependency relationship between the containing namespaces. And if the containing namespacesbelong to different assemblies, that means there is also a dependency relationship between thecontaining assemblies.
The effect of dependencies on the architecture of our application and development process in generalis profound. This makes dependency management one of the most important aspects of the large-scaledesign of your application. Take a look now at how dependencies influence some crucial aspects ofdevelopment process.
Build Process, Testability, and DistributionIn order to compile a dependent class, a compiler needs to have a compiled version of the class thatdependent class is dependent upon. If the dependency is circular, it turns into kind of a chicken oregg question for the compiler. Visual Studio will not even let you establish cyclic dependencybetween projects in the solution.
However, you can still establish cyclic dependencies by referencing the compiled assembly. The effect of cyclic dependencies is such that you are not able to normally control the versioning process in your solution.
Imagine that two projects, A and B, both depend on each other and a group of programmers is workingon these two projects in parallel. If you try to release new versions of these two projects, you will findthat you cannot release them independent of each other. The solution is in arduous integration work thatin practice converts these two projects into a single project physically divided into two assemblies. Thesetwo assemblies are still a single monolithic software product from a development process point of view.The software has to be compiled, tested, and distributed together.
When there are no circular dependencies in your solution, then each project can follow its own releasetimeline. Each dependent project can choose when to make use of the new version of the project itdepends upon.
381
Chapter 13: Code Organization on a Large Scale
79796c13.qxd:WroxPro 2/22/08 6:30 PM Page 381
Refactoring: Extract NamespaceMotivationNamespaces containing too many types increase the clutter and make it more difficultfor programmers to find and use classes. Large namespaces will negatively impactreuse and maintenance characteristics in your code. Such namespaces can be reducedin size by namespace extraction.
Sometimes, namespace extraction can resolve the problem of cyclic dependencies. Byextracting offending classes to a separate namespace, you can make both namespacesdepend on the extracted namespace, instead of having them depend on each other.
Namespace is a higher order organizational unit. By placing related classes in separatenamespaces you improve the organization and coherence of your code.
Related SmellsUse this refactoring to eliminate the Large Namespace, Non-Coherent Namespace, andCyclic Dependencies smells.
Mechanics1. Decide the name you want to give to the extracted namespace and create a
directory to store classes in this namespace.
2. If you have more than one class in a single file, perform Move Type to Filerefactoring where necessary.
3. For each class that belongs to the new extracted namespace, move thatclass’ file to the new directory created for the namespace.
4. For each class that belongs to the new extracted namespace, change thenamespace declaration so it corresponds to the newly extracted namespace.
BeforeNamespace RentAWheels.Business
Public Class Vehicle‘...
End Class
Public Class Branch‘...
End Class
Public Class VehicleCategory‘...
End Class
Public Class User‘...
End Class
Public Class Profile
382
Part IV: Advanced Refactorings
79796c13.qxd:WroxPro 2/22/08 6:30 PM Page 382
Breaking Dependency CyclesFortunately, there are methods to deal with dependencies in your projects. One such method can help youinvert dependency and in that way break the dependency cycle. Cycles often involve multiple projects.These complex relationships are best analyzed by viewing UML package diagrams that can show name-space structures and their relationships and that are being generated by some tool from the same codethat is being executed. In order to keep things simple, you should use only two namespaces belonging totwo different assemblies for this illustration.
‘...End Class
Public Class Permission‘...
End Class
End Namespace
AfterNamespace RentAWheels.Business
Public Class Vehicle‘...
End Class
Public Class Branch‘...
End Class
Public Class VehicleCategory‘...
End Class
End Namespace
Namespace RentAWheels.Users
Public Class User‘...
End Class
Public Class Profile‘...
End Class
Public Class Permission‘...
End Class
End Namespace
383
Chapter 13: Code Organization on a Large Scale
79796c13.qxd:WroxPro 2/22/08 6:30 PM Page 383
Imagine you have a typical GUI namespace whose classes are interacting with a faxing service inanother namespace. The mock-up code is shown in Listing 13-1.
Listing 13-1: Cyclic Dependency between GUI and Faxing Service Namespaces
Namespace FaxingPublic Class FaxService
Private faxDevice as FaxDevicePublic Sub SendFax(ByVal job As FaxJob)
TryfaxDevice.dial(job.number)faxDevice.transmit()faxDevice.endTransmission()
Catch ex As ExceptionDim errorForm As ErrorForm = New ErrorForm()errorForm.ShowErrorMessage(“Faxing failed:” + ex.Message)
End TryEnd Sub‘...
End ClassEnd Namespace
Namespace GUIPublic Class FaxingForm
Private Sub SendFax_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles SendFax.Click
Dim faxService As New FaxServicefaxService.SendFax(Me.Job)
End Sub‘...
End ClassEnd Namespace
Smell: Cyclic DependenciesDetecting the SmellThis smell is not easily detected on any but the smallest projects. Some code analysistools are capable of identifying dependency cycles. One such tool is NDepend(www.ndepend.com).
If the cycle spawns projects, Visual Studio refuses adding cyclical references betweenthe projects in the solution. However, you are still able to reference the compiledassembly in a cyclical manner.
Related RefactoringUse Move Class to Namespace and Extract Namespace refactorings and invert depend-encies in order to eliminate cyclical dependencies from your project.
RationaleCyclic dependencies work against the modularity of your code. Items joined through cyclic dependencies have to be tested, released, and reused together.
384
Part IV: Advanced Refactorings
79796c13.qxd:WroxPro 2/22/08 6:30 PM Page 384
If you were to present this in a diagram depicting dependencies between namespaces, it would looksomething like Figure 13-2.
Figure 13-2
Even from the simple look of the diagram, it is obvious that there is a problem with the design. The Faxingnamespace groups classes related to faxing functionality. The GUI namespace groups forms that renderuser interface and contains this particular application’s logic. Unfortunately, there is a circular dependencypresent between the two. Where does this problem come from?
If you were to design the faxing service as a reusable component, then depending on the particularapplication and its GUI is not such a good idea. If you analyze the faxing service code carefully, you cansee that it uses an ErrorForm to inform users that something went wrong with a faxing operation. Thisis the source of circular dependency and at the same time is the weakest link between the two. You canthen try to eliminate this dependency.
Inverting DependenciesYou need to eliminate the FaxingService dependency on ErrorForm, but at the same time you need tokeep the functionality that enables you to inform the user of any problem with the faxing operation. Thesolution is to invert the dependency between FaxingService and ErrorForm.
Consider extracting the error-reporting code to a separate method and naming it OnError:
Public Sub OnError(ByVal message As String)Dim errorForm As ErrorForm = New ErrorForm()errorForm.ShowErrorMessage(“Faxing failed:” + ex.Message)
End Sub
This method is now called from a Catch block. Now, you can extract interface with this method. Name it ErrorReport. But, instead of FaxService implementing this method, you can let clients interested inerror messages from FaxService implement this method.
Instead of using ErrorForm, FaxingService can use a newly defined interface that has the same purposeand that resides in the same namespace as FaxingService. Objects interested in getting information about faxing problems can implement this interface and subscribe to listening for errors messages fromFaxingService. The code for the solution is shown in Listing 13-2.
Listing 13-2: Cyclic Dependency between GUI and Faxing Service Namespaces Inverted
Namespace FaxingPublic Interface ErrorReport
Sub OnError(ByVal message As String)
Continued
FaxingGUI
385
Chapter 13: Code Organization on a Large Scale
79796c13.qxd:WroxPro 2/22/08 6:30 PM Page 385
Listing 13-2: Cyclic Dependency between GUI and Faxing Service Namespaces Inverted (continued)
End Interface
Public Class FaxServicePrivate faxDevice as FaxDevicePrivate errorReportValue As ErrorReportPublic Sub SendFax(ByVal job As FaxJob)
TryfaxDevice.dial(job.number)faxDevice.transmit()faxDevice.endTransmission()
Catch ex As ExceptionErrorReport.OnError(ex.Message)
End TryEnd Sub
Public Property ErrorReport() As ErrorReportGet
Return errorReportValueEnd GetSet(ByVal value As ErrorReport)
errorReportValue = valueEnd Set
End Property‘...
End ClassEnd Namespace
Namespace GUIPublic Class FaxingForm
Implements ErrorReportPrivate Sub SendFax_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles SendFax.Click
Dim faxService As New FaxService‘subscribe to error messagesfaxService.ErrorReport = MefaxService.SendFax(Me.Job)
End Sub
Public Sub OnMessage(ByVal message As String) _Implements ErrorReport.OnError
Dim errorForm As ErrorForm = New ErrorForm()errorForm.ShowErrorMessage(“Faxing failed:” + message)
End Sub‘...
End ClassEnd Namespace
The cycle between the two namespaces is eliminated. The new state of affairs is depicted graphically inFigure 13-3.
386
Part IV: Advanced Refactorings
79796c13.qxd:WroxPro 2/22/08 6:30 PM Page 386
Figure 13-3
When you are looking to break cycles, the best methodology is to search for the weakest link. If youbreak the cycle at one point, it means you have done away with the cycle. Dependency inversion is apowerful tool for controlling spiraling dependencies. It can be applied to break circular dependenciesand reduce dependencies in general. While dependencies have a negative impact on your design, mak-ing it more rigid and less modular, they cannot be avoided altogether.
Now it’s time to take a look at another dimension of source code organization, a more physical one —how you place the code inside a physical file and folder structure.
Visual Basic Project F ile Structure Organization
Visual Basic is very flexible in regards to organizing your project’s file structure. You can place yoursource code files inside an arbitrary directory structure, and you can place an arbitrary number of typesinside a single source code file. You can even distribute a single type over more than one source codefile, as you will soon see in the section dedicated to partial classes later in the chapter.
Object-Oriented Design Principle: Acyclic Dependencies PrincipleCyclic dependencies work directly against the modularity of your software. If you letsuch dependencies grow, you soon lose the benefits of modular design. You have to starttreating all the projects in your solution as a single project, meaning that you cannot test,release, and reuse each assembly in an independent manner. Your build process suffers,because all dependent namespaces and assemblies have to be released together.
DefinitionIn the words of Robert C. Martin, Agile Software Development: Principles, Patterns, andPractices (Prentice Hall, 2002), “Allow no cycles in the package-dependency graph.”
In VB, you can interpret a package as a namespace.
ExampleCyclic dependencies are best observed on package diagrams. Take a look at Figure 13-2for the simplest case of cyclic dependencies.
FaxingGUI
387
Chapter 13: Code Organization on a Large Scale
79796c13.qxd:WroxPro 2/22/08 6:30 PM Page 387
With all this flexibility at hand, you really need to choose a uniform schema for project file organization.One such schema that can help you avoid clutter in Solution Explorer and find your way around sourcecode files faster is the following:
❑ Mimic the namespace hierarchy with the directory hierarchy. For example, place the PurchaseForm class belonging to BookStore.Web.Purchase namespace inside the[Project Root Directory]\BookStore\Web\Purchase directory. You can ignore the rootnamespace for this purpose.
❑ Place a single type inside a single source code file. This lets you specify an Options and Importssection in such a way that it refers to single type. It also makes it easier to move the class toanother namespace or assembly, because you can simply move the file in Solution Explorer.
❑ Use the class name for the source file name. For example, if the file contains PurchaseForm,name it PurchaseForm.vb.
In case you have followed some different schema and now you wish to place a single type inside a singlefile, you can use the Refactor! to help you with this task. This refactoring is called Move Type to File.
Move Type to File Refactoring in Refactor!As you would expect, this refactoring becomes available only if the source code file that you work withcontains more than one type. Even then, the refactoring is available only for types whose name is differ-ent from the name of the source file. Refactoring can be invoked by right-clicking the class declaration,as shown in Figure 13-4. After the refactoring is invoked, it adds the new file to your project, uses theclass name for the source code file name, and moves the code of the class on the move to the new file. It moves the complete code of the class to the new file, including the namespace and complete importsection declaration.
Figure 13-4
388
Part IV: Advanced Refactorings
79796c13.qxd:WroxPro 2/22/08 6:30 PM Page 388
Refactoring: Move Type to FileMotivationMultiple types in a single source file can make your code more difficult to work with.Because a file is the smallest unit under source code control, by placing multiple typesin single file, you increase the probability that more than one developer works on thesame file and, thus, increase the probability of conflicting changes.
Multiple types in a single file make some large-scale organizational refactorings suchas Extract Namespace and Move Class to Namespace more difficult. While these refac-torings do not have to be performed on the file level, it is recommended. Physical proj-ect structures where directory and file hierarchy mimic namespace hierarchy makeyour project organization more logical and coherent.
Related SmellsUse this refactoring to place types in separate source files and to prepare your projectfor refactorings like Move Type to File and Extract Namespace.
Mechanics1. Start by creating new source file for the type you wish to move to a sepa-
rate file. Use the type name for the newly created file name.
2. Cut and paste type code to new file.
3. Optimize imports. Remove imports that are not used any more in the originalfile and be careful to copy to new file only those imports that are used by thetype you have just moved.
BeforeVehicle.vb file content:
Public Class Vehicle‘...
End Class
Public Class Branch‘...
End Class
AfterVehicle.vb file content:
Public Class Vehicle‘...
End Class
Branch.vb file content:
Public Class Branch‘...
End Class
389
Chapter 13: Code Organization on a Large Scale
79796c13.qxd:WroxPro 2/22/08 6:30 PM Page 389
After you perform this refactoring, it is advisable that you add Option Strict and Option Explicitdeclarations to the top of the newly added file. You should also inspect Imports section for any unusedimports, because the tool simply copies the import section from the original file to the new file. BecauseRefactor! is copying original source, some imports used only by the class that stayed put in the originalclass will find their way into the new file. Remove unused imports from the new file.
Next, you will turn to another peculiar schema for source code organization. Partial classes let youspread single type across multiple source code files.
Partial ClassesPartial classes are another interesting Visual Basic feature that appeared in the 2005 version of the lan-guage. This feature permits one class to span more than one source code file. It is primarily intended forcode generator tools like Windows Forms Designer. All tool-generated code can be placed in separatefile, out of the programmer’s sight, because it was never intended to be manipulated directly by pro-grammer anyway. In this sense, partial classes are similar to the Region directive of VB. (Region direc-tives let you mark the section of source code so it can be collapsed and hidden in the VB editor).
One caveat about partial types: You should not use partial types as a replacement for traditional object-oriented techniques. Remember that a partial type spread across several source code files is in the endstill a single type and will be treated by the compiler and client programmers as such. If you let such a type grow out of proportions, it will suffer all the problems that any other class suffering Large Classsmell would: it would be difficult to maintain and reuse, prone to change, and would not abide withSingle Responsibility Principle (SRP) or Open-Closed Principle (OCP).
Partial classes are related to another feature in VB. This feature has to do with Windows forms inheritance.
Inherited FormInherited Form is a built-in Visual Studio template that helps you inherit the form class. If you alreadyhave a form class in your solution and you decided to invoke Inherited Form template through AddNew Item dialog, Visual Studio displays the Inheritance Picker dialog and prompts you to select the par-ent form class. After you select the parent form, Visual Studio adds another form to your project thatinherits the selected parent. In design view, you can see all the controls that were placed on the parentform, but they are marked with special lock icon. You cannot modify these controls in the child form. Ifyou modify the controls placed on parent form, these changes are reflected in the child form only afteryou build the project containing the parent form.
For the Visual Basic compiler, the form class is the same as any other class. The same is true for an inher-ited form. However, if you open inherited form’s source code, you find an empty file. So, how is theinheritance relationship between the forms realized in source code?
To find out, create an ad hoc Windows Application project and perform the following steps:
1. Add an empty Form1 form to the project.
2. Build the project.
390
Part IV: Advanced Refactorings
79796c13.qxd:WroxPro 2/22/08 6:30 PM Page 390
3. Add a new inherited form item to the project and select Form1 as the component you wish toinherit from.
4. Check that the text property in the recently added child Form2 is set to Form2.
In order to see the source code that establishes inheritance relationship between the forms, you have toopen the designer-generated source part of the form partial class. You can display this file if you activatethe Show All Files option in the Solution Explorer in Visual Studio and expand the view of Form2. Thefile name is Form2.Designer.vb.You can see that the form’s declaration contains the Inherits state-ment that establishes an inheritance relationship between the two forms. Take a look at Listing 13-3.
Listing 13-3: Empty Inherited Form2 Designer-Generated Code
<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()> _Partial Class Form2
Inherits InheritedForms.Form1
‘Form overrides dispose to clean up the component list.<System.Diagnostics.DebuggerNonUserCode()> _Protected Overrides Sub Dispose(ByVal disposing As Boolean)
If disposing AndAlso components IsNot Nothing Thencomponents.Dispose()
End IfMyBase.Dispose(disposing)
End Sub
‘Required by the Windows Form DesignerPrivate components As System.ComponentModel.IContainer
‘NOTE: The following procedure is required by the Windows Form Designer‘It can be modified using the Windows Form Designer. ‘Do not modify it using the code editor.<System.Diagnostics.DebuggerStepThrough()> _Private Sub InitializeComponent()
Me.SuspendLayout()‘‘Form2‘Me.AutoScaleDimensions = New System.Drawing.SizeF(6.0!, 13.0!)Me.ClientSize = New System.Drawing.Size(292, 266)Me.Name = “Form2”Me.ResumeLayout(False)
End Sub
End Class
You can compare this code with designer-generated code for Form2. As you can see, this code does notdiffer that much from typical form declaration. The principal difference is that the parent Form1 form isinheriting System.Windows.Forms.Form class. I guess it is fair to say that with this form inheritancehas been largely demystified. However, form inheritance displays one abnormality inside the VisualStudio environment.
391
Chapter 13: Code Organization on a Large Scale
79796c13.qxd:WroxPro 2/22/08 6:30 PM Page 391
Abstract Form InheritanceBecause form classes can be considered normal Visual Basic classes, they should support all legal codemanipulation that is permitted by Visual Basic language. When using inheritance, you might well wishto make the form abstract. Using the example from the previous section, you can try to do so:
1. First, change the project’s startup object to Form2.
2. Modify the Form1.Designer.vb by adding a MustInherit statement to the form’s declaration.The code should now look like this:
Partial MustInherit Class Form1Inherits System.Windows.Forms.Form
3. Close all the editor windows in Visual Studio.
4. Build the project.
As you can see if you try this by yourself, everything works out fine. If you take a look at the Error Listwindow, it should be empty. And if you run the project, Form2 is displayed.
Now you can try to edit the Form2 in the Windows Form Designer. If you click Form2 in SolutionExplorer twice, instead of displaying an edit view of Form2, you get a window displaying the follow-ing error message:
The designer must create an instance of type ‘InheritedForms.Form1’ but it cannotbecause the type is declared as abstract.
If you have read this message carefully, you might think I made a mistake somewhere in the process ofdescribing the previous steps. Designer is saying it cannot create an instance of Form1 and you clickedthe Form2?
As a matter of fact, there was no error. The Windows Forms Designer is implemented in such a way that itinstantiates the parent form in the form hierarchy when displaying the Form Designer window. Becausein this example Form2 inherits Form1, the designer tried to instantiate Form1. Because the Form1 wasmarked as abstract, the designer could not create an instance of it, and the error was produced.
This is an unfortunate limitation of the Window Forms Designer tool and Visual Studio. There is no reasonwhy you shouldn’t be able to program abstract parent windows forms and then design nonabstract children.Fortunately, there is a workaround for this problem.
Delegating Abstract Form Work to a Form Helper ClassImagine you are programming reports in your application, and you detect that all forms that are used to display reports have a number of identical members. You decide to extract the AbstractReportFormsuperclass and make all concrete report forms inherit the abstract report form. Each concrete report formhas a different way of recollecting data for report generation, so you mark the AddReportData methodas abstract using MustOverride keyword and therefore you make AbstractReportForm abstract(MustInherit).
392
Part IV: Advanced Refactorings
79796c13.qxd:WroxPro 2/22/08 6:30 PM Page 392
Everything is working out fine, but the problem is with designing concrete report forms. A designer isreporting an error because AbstractReportForm is abstract. Is there a way that you could place com-mon code inside the single parent class and still design forms in Windows Designer?
The solution comes in the form of Replace Inheritance with Delegation refactoring. The parent reportform need not leave abstract methods that child forms have to implement. Instead, it can delegate allwork to an AbstractReportHelper class. While Windows forms cannot be abstract, they can use otherabstract classes without any problem. So, a concrete report form can create an instance of a concretehelper class that overrides all abstract members declared in AbstractReportHelper. The code inListing 13-4 illustrates this solution.
Listing 13-4: Delegating Work to AbstractHelper to Keep the Form Non-Abstract
Option Explicit OnOption Strict On
Public Class GeneralReportFormPrivate helperValue As AbstractReportHelperPrivate Sub ViewReport_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles ViewReport.Click
Helper.SelectReportType()Helper.AddReportData()Helper.ViewReport()
End SubProtected Property Helper() As AbstractReportHelper
GetReturn helperValue
End GetSet(ByVal value As AbstractReportHelper)
helperValue = valueEnd Set
End Property‘...
End Class
Public MustInherit Class AbstractReportHelperPrivate formValue As GeneralReportFormPublic Sub New(ByVal form As GeneralReportForm)
formValue = formEnd SubFriend Sub ViewReport()
‘... End SubFriend Sub SelectReportType()
‘...End SubFriend MustOverride Sub AddReportData()Public Property Form() As GeneralReportForm
GetReturn formValue
End GetSet(ByVal value As GeneralReportForm)
formValue = value
Continued
393
Chapter 13: Code Organization on a Large Scale
79796c13.qxd:WroxPro 2/22/08 6:30 PM Page 393
Listing 13-4: Delegating Work to AbstractHelper to Keep the Form Non-Abstract (continued)
End SetEnd Property‘...
End Class
Public Class AccountsReportFormPublic Sub New()
MyBase.New()‘ This call is required by the Windows Form Designer.InitializeComponent()‘ Add any initialization after the InitializeComponent() call.
‘Create concrete accounts helper and pass instance of this form (Me)‘so it can read data necessary for the reportMe.Helper = New AccountsHelper(Me)
End Sub‘...
End Class
Public Class AccountsHelperInherits AbstractReportHelperPublic Sub New(ByVal form As GeneralReportForm)
MyBase.New(form)End SubFriend Overrides Sub AddReportData()
‘ Add accounts report data End Sub‘...
End Class
With this finished, you are now ready to take a look at Rent-a-Wheels and see how the latest refactoringsapply to the sample application.
Namespace Organization and WindowsForms Inheritance in Rent-a-Wheels
I start work on Rent-a-Wheels by sorting out one issue that was pending from the previous chapter. Evenwhile making changes in the previous chapter, I noticed that all administration forms have a number ofidentical members. However, I didn’t cover forms inheritance in the last chapter, so it was left for later.Now is the right moment to deal with duplicate code in the administration forms.
Extracting Parent Administration Form through Abstract Form Helper Pattern Application
This chapter has discussed issues related to Windows Forms inheritance and Window Forms Designer.You have seen how Designer generates separate files for Designer-generated code. You have also seensome of the Designer’s limitations in regards to use of abstract forms.
394
Part IV: Advanced Refactorings
79796c13.qxd:WroxPro 2/22/08 6:30 PM Page 394
The first graphically obvious duplication between different administration forms in the Rent-a-Wheelsapplication involves two groups of controls. The first group of controls is a navigation strip at the bottom of the forms that lets you navigate between different records in the table being maintained by the form. Thesecond group comprises the New, Save, Delete, and Reload action buttons on the right side of the form.
Extract Super Administration FormStart by adding a new Windows form named GeneralAdministrationForm to the project. Next, copy the controls from the random administration form and paste it on GeneralAdministrationForm.Accommodate controls on the form so that the position of the action and navigation button strips fit all ofthe existing administration forms. Once all specific label and text controls are added, these should be visibleand should not overlap with controls on the parent class. You can see GeneralAdministrationForm beingdisplayed by the Designer in Figure 13-5.
Figure 13-5
Move up all members identical in all the forms to GeneralAdministrationForm. Some of these identicalmethods use methods that are different in each form. For example, the navigational button First event-handler routine is identical in all forms:
Private Sub FirstItem_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles FirstItem.ClickIf (models.Count > 0) Then
currentObjectIndex = 0DisplayCurrentObject()
End IfEnd Sub
However, the DisplayCurrentObject method is different in each of the forms. For example, take alook at the DisplayCurrentObject in the BranchMaintenance form:
Private Sub DisplayCurrentObject()Dim branch As Branch = _CType(branches.Item(currentObjectIndex), RentAWheel.Branch)Me.Id.Text = branch.Id.ToStringMe.BranchName.Text = branch.Name
End Sub
395
Chapter 13: Code Organization on a Large Scale
79796c13.qxd:WroxPro 2/22/08 6:30 PM Page 395
This method differs from the DisplayCurrentObject in the ModelMaintenance form:
Private Sub DisplayCurrentObject()Dim model As Model = _CType(models.Item(currentObjectIndex), RentAWheel.Model)Me.Id.Text = model.Id.ToStringMe.ModelName.Text = model.NameMe.ModelCategory.SelectedValue = model.Category.Id
End Sub
You need to have the DisplayCurrentObject method in the GeneralAdministrationForm so youcan compile the project. For the moment, declare an empty DisplayCurrentObject and change its dec-laration to Protected Overridable. This way, child forms can override this method with code specificfor the concrete administration form. The empty DisplayCurrentObject looks like this:
Protected Overridable Sub DisplayCurrentObject()
End Sub
Do the same with rest of the methods. In some cases you need to extract some event-handling code intoa separate method. For example, you can extract the NewItem_Click code to new CleanForm method.
Finally, it is time to make all administration forms inherit GeneralAdministrationForm. First, erase all thecode already existing in GeneralAdministrationForm, like all event-handling routines. Now, make eachadministration form inherit GeneralAdministrationForm. In order to do this, you have to change theDesigner-generated code. Because each form already has an Inherits declaration, all you need to do is tochange the Inherits System.Windows.Forms.Form line to Inherits GeneralAdministrationForm.Now run the application and make sure that everything works as it is supposed to.
This is already a great step forward. I have reduced a huge amount of duplicated code and streamlinedadministration form construction. However, I am not happy with leaving empty method declarations, suchas the empty DisplayCurrentObject method, in the extracted super GeneralAdministrationForm. Hadit not been for problems with the Windows Forms Designer, I’d mark this method with MustOverride andthe GeneralAdministrationForm with MustInherit.
Getting Help from Form HelperIn order to refactor the GUI layer of Rent-a-Wheels so it renders well in Windows Forms Designer, you needto introduce helper classes that contain the logic contained previously in the event-handler routines in theform classes. This way, you can extract a FormHelper class that contains all the code that is identical in all ofthe form classes and contains some abstract method declarations. The GeneralAdministrationForm endsup being an empty shell that delegates all the work to AdministrationFormAbstractHelper. Take a lookat GeneralAdministrationForm in Listing 13-5.
Listing 13-5: GeneralAdministrationForm Delegating toAdministrationFormAbstractHelper
Option Explicit OnOption Strict OnPublic Class GeneralAdministrationForm
Private helperValue As AdministrationFormAbstractHelper
396
Part IV: Advanced Refactorings
79796c13.qxd:WroxPro 2/22/08 6:30 PM Page 396
Listing 13-5: GeneralAdministrationForm Delegating toAdministrationFormAbstractHelper (continued)
Private Sub RightItem_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles RightItem.Click
Me.helper.RightItem_Click()End Sub
Private Sub LeftItem_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles LeftItem.Click
Me.helper.LeftItem_Click()End Sub
Private Sub FirstItem_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles FirstItem.Click
Me.helper.FirstItem_Click()End Sub
Private Sub LastItem_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles LastItem.Click
Me.helper.LastItem_Click()End Sub
Public Property Helper() As AdministrationFormAbstractHelperGet
Return helperValueEnd GetSet(ByVal value As AdministrationFormAbstractHelper)
helperValue = valueEnd Set
End Property
Private Sub GeneralAdministrationForm_Load(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles MyBase.Load
If Not IsNothing(Me.Helper) Then Me.Helper.Form_Load()End Sub
Private Sub NewItem_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles NewItem.Click
Me.Helper.NewItem_Click()End Sub
Private Sub Save_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles Save.Click
Me.Helper.Save_Click()End Sub
Private Sub Delete_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles Delete.Click
Me.Helper.Delete_Click()
Continued
397
Chapter 13: Code Organization on a Large Scale
79796c13.qxd:WroxPro 2/22/08 6:30 PM Page 397
Listing 13-5: GeneralAdministrationForm Delegating toAdministrationFormAbstractHelper (continued)
End Sub
Private Sub Reload_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles Reload.Click
Me.Helper.Reload_Click()End Sub
End Class
All the work has been moved to AdministrationFormAbstractHelper. Because the helper classdoes not contain any GUI elements and therefore won’t be manipulated in Windows Designer, it canbe marked as abstract (MustInherit) and can contain abstract members (MustOverride). The codefor AdministrationFormAbstractHelper is in Listing 13-6.
Listing 13-6: AdministrationFormAbstractHelper Code
Option Explicit OnOption Strict On
Public MustInherit Class AdministrationFormAbstractHelper
Private currentObjectIndexValue As IntegerPrivate objectsValue As IList
Public Sub New()CreateData()
End Sub
Public Sub Form_Load()LoadObjects()If (Me.Objects.Count > 0) Then
CurrentObjectIndex = 0DisplayCurrentObject()
End IfEnd Sub
Public Sub RightItem_Click()If (Objects.Count > CurrentObjectIndex + 1) Then
CurrentObjectIndex += 1DisplayCurrentObject()
End IfEnd Sub
Public Sub LeftItem_Click()If (CurrentObjectIndex - 1 >= 0 And Objects.Count > 0) Then
CurrentObjectIndex -= 1DisplayCurrentObject()
End If
398
Part IV: Advanced Refactorings
79796c13.qxd:WroxPro 2/22/08 6:30 PM Page 398
Listing 13-6: AdministrationFormAbstractHelper Code (continued)
End Sub
Public Sub FirstItem_Click()If (Objects.Count > 0) Then
CurrentObjectIndex = 0DisplayCurrentObject()
End IfEnd Sub
Public Sub LastItem_Click()If (Objects.Count > 0) Then
CurrentObjectIndex = Objects.Count - 1DisplayCurrentObject()
End IfEnd Sub
Public Sub NewItem_Click()CleanForm()
End Sub
Public Sub Save_Click()SaveObject()Form_Load()
End Sub
Public Sub Delete_Click()DeleteObject(Objects.Item(currentObjectIndexValue))Form_Load()
End Sub
Public Sub Reload_Click()Form_Load()
End Sub
Public Property Objects() As IListGet
Return objectsValueEnd GetSet(ByVal value As IList)
objectsValue = valueEnd Set
End Property
Public Property CurrentObjectIndex() As IntegerGet
Return currentObjectIndexValueEnd GetSet(ByVal value As Integer)
currentObjectIndexValue = valueEnd Set
Continued
399
Chapter 13: Code Organization on a Large Scale
79796c13.qxd:WroxPro 2/22/08 6:30 PM Page 399
Listing 13-6: AdministrationFormAbstractHelper Code (continued)
End Property
Protected MustOverride Sub LoadObjects()
Public MustOverride Sub DisplayCurrentObject()
Protected MustOverride Sub CreateData()
Protected MustOverride Sub CleanForm()
Protected MustOverride Sub SaveObject()
Protected MustOverride Sub DeleteObject(ByVal currentObject As Object)
End Class
Abstract methods are overridden in the concrete helper class. For example, you can seeBranchMaintenanceHelper in Listing 13-7.
Listing 13-7: BranchMaintenanceHelper Implementing Abstract Methods
Option Explicit OnOption Strict On
Public Class BranchMaintenanceHelperInherits AdministrationFormAbstractHelper
Private formValue As BranchMaintenance
Private branchData As BranchData
Public Property Form() As BranchMaintenanceGet
Return formValueEnd GetSet(ByVal value As BranchMaintenance)
formValue = valueEnd Set
End Property
Public Overrides Sub DisplayCurrentObject()Dim branch As Branch = _
CType(Objects.Item(CurrentObjectIndex), Branch)formValue.Id.Text = branch.Id.ToStringformValue.BranchName.Text = branch.Name
End Sub
Protected Overrides Sub LoadObjects()MyBase.Objects = CType(branchData.GetAll,
400
Part IV: Advanced Refactorings
79796c13.qxd:WroxPro 2/22/08 6:30 PM Page 400
Listing 13-7: BranchMaintenanceHelper Implementing Abstract Methods (continued)
System.Collections.Generic.List(Of Branch))End Sub
Protected Overrides Sub CreateData()Me.branchData = New BranchData
End Sub
Protected Overrides Sub CleanForm()Form.Id.Text = String.EmptyForm.BranchName.Text = String.Empty
End Sub
Protected Overrides Sub SaveObject()If (Form.Id.Text.Equals(String.Empty)) Then
Dim branch As Branch = New Branch(Nothing, _Form.BranchName.Text.ToString)branchData.Insert(branch)
ElseDim branch As Branch = CType(Objects.Item(CurrentObjectIndex), Branch)branch.Name = Form.BranchName.TextbranchData.Update(branch)
End IfEnd Sub
Protected Overrides Sub DeleteObject(ByVal currentObject As Object)branchData.Delete(CType(currentObject, Branch))
End SubEnd Class
All that is left for the concrete form is to instantiate helper. Take a look at the BranchMaintenance formin Listing 13-8.
Listing 13-8: BranchMaintenance Form
Option Explicit OnOption Strict On
Public Class BranchMaintenance
Public Sub New()
MyBase.New()Dim helper As BranchMaintenanceHelper = New BranchMaintenanceHelperMyBase.Helper = helper‘ This call is required by the Windows Form Designer.InitializeComponent()helper.Form = Me
End Sub
End Class
401
Chapter 13: Code Organization on a Large Scale
79796c13.qxd:WroxPro 2/22/08 6:30 PM Page 401
This way, you have surpassed the limitation that Windows Forms Designer has with abstract classes. You arenow able to display and design forms in Windows Forms Designer, and you are able to use abstract classes,an important feature of your code that will leave no loopholes and “apparently” implemented methods toimplementors, as is the case if you use parent Windows forms that contain abstract logic. With this, you havefinished refactoring GUI classes. Now it’s time to do some large-scale reorganization of the project.
Namespace and Assembly ReorganizationAt this point, you have a single project with a single namespace containing all the classes. (You rememberplacing classes containing column names to a separate directory and namespace at some point earlier on,but this is just an exception to the rule.)
If you analyze some of the rules discussed in this chapter, you will see that there are two primary forcesthat can be applied to Rent-a-Wheels:
❑ Single Responsibility
❑ Reuse-Release Equivalence
The first principle is telling you that you should keep GUI classes apart from business classes apart fromdata classes. The second principle is telling you that you should keep abstract classes apart from concreteclasses. You can interpret this as having horizontal and vertical axis of separation. Figure 13-6 presentsthis schematically.
Figure 13-6
The partitions depicted in the figure looks like a good way to partition Rent-a-Wheels into assemblies. Allprojects except for the concrete GUI will be class libraries. The concrete GUI assembly will be Windowsapplication. Each of the assemblies must be revised for imports and in some cases will reference anotherRent-a-Wheels assembly. Assembly dependencies are better depicted with the component diagram shownin Figure 13-7.
One important conclusion is immediately obvious from the diagram. The application has no circulardependencies. Secondly, less abstract namespaces depend on more abstract ones. Also, there is a clearseparation of concerns between the assemblies.
While organizing namespaces, you have also used an opportunity to organize class files into directories.This makes the project easier to browse in Solution Explorer. Take a look at the Rent-a-Wheels solutionexplorer after directory reorganization in Figure 13-8.
Abstract GUI Abstract Data
Concrete GUI Concrete Business Concrete Data
402
Part IV: Advanced Refactorings
79796c13.qxd:WroxPro 2/22/08 6:30 PM Page 402
Figure 13-7
Figure 13-8
This looks much more orderly and better organized. And I don’t mean only the files; the whole solutionis now much more coherent. You might even go so far as to say that there are no immediate problems inthe application needing your attention. However, this does not mean that you cannot take the designeven further. That will wait for the next chapter.
SummaryThis chapter dealt with some issues that are often related to large projects. You have seen how in VisualBasic you use some higher-level organizational structures to manage your projects. Organizing typesinside namespaces and partitioning solutions into assemblies has to be done with an understanding ofthe principles governing large-scale organization.
These principles are applied in order to improve maintainability and reuse and to minimize dependenciesbetween different elements in a project. Unmanaged dependencies can have direct repercussions on yourdevelopment process.
«executable»RentAWheel.GUI.Implementation
«library»RentAWheel.Data.Implementation
«library»RentAWheel.Data
«library»RentAWheel.Business
«library»RentAWheel.GUI
403
Chapter 13: Code Organization on a Large Scale
79796c13.qxd:WroxPro 2/22/08 6:30 PM Page 403
Luckily, you have the means to control and deal with dependencies. Minimizing dependencies betweenassemblies or namespaces can be as simple as moving a type to a proper namespace. In other cases, moreelaborate solutions, such as dependency inversion, are necessary.
The smaller scale effects of large-scale code organization are not immediate. However, problems that mightarise from improper organization can be crippling at later stages of a project. That is why it is recommend-able to think about project organization from its outset.
By finishing this chapter, you have covered most of traditional refactoring techniques. You have seen howcode can be reorganized from method to namespace level. You have seen a number of ailments that cantrouble your code and the ways they can be remedied. In the next chapter, you will go beyond the obvious.You will see how sometimes design can be problematic and “smelly” at first sight, but is actually an elegantsolution to a specific problem when analyzed in depth. The next chapter talks about design patterns.
404
Part IV: Advanced Refactorings
79796c13.qxd:WroxPro 2/22/08 6:30 PM Page 404
Part V: Refactoring Applied
You have come to the final part of the book. If you have adopted all refactoring skillsdescribed so far, you can consider yourself a programmer seasoned in refactoringtechniques. However, you do have a few more important things about refactoring thatyou can learn.
In Part V you will learn things that go beyond basic object-oriented skills. You willlearn about the Holy Grail of object-oriented design — design patterns. You will alsosee how you can refactor your code having patterns as a goal that you would like toreach with your design.
As you continue in this part, you will gain a complete perspective on the history ofdevelopment in Visual Basic. You will see what the future of Visual Basic brings inthe form of Language-Integrated Query (LINQ) and other language enhancementsscheduled for the 9.0 version of Visual Basic language specification (that ships withVisual Studio 2008 and is not yet released at the time of writing).
Finally, you will take a look at the past of Visual Basic. I’ll talk about .NET versions of Visual Basic, especially Visual Basic 6.0 that helped propel Visual Basic to becomeone of the most popular development tools around. You will see how all VB 6 code isnot completely lost for the future and how refactoring can help you give a new .NETlife to your VB 6 code.
79796c14.qxd:WroxPro 2/25/08 9:06 AM Page 405
79796c14.qxd:WroxPro 2/25/08 9:06 AM Page 406
Refactoring to Patterns
Until now, I have promoted a rather simple approach to refactoring. You inspect the code, find thesmells, and then refactor it so the smells are eliminated or at least reduced. This approach focusesessentially on negative aspects of your code in an attempt to eradicate them. Many times, followingsuch an approach, you can reach an overall improved and even sophisticated design of your code.
It is also possible to lead your design toward some thought-out design goal. Often, such a solutionis not immediately obvious nor the most simple. However, the benefits such premeditated designcan bring often justify the added complexity.
To be able to lead your code toward some well-known design, you need to study and understanddifferent design solutions. Design patterns were created with exactly that purpose, to share anddisseminate knowledge on successful object-oriented designs that go beyond common and thatcapture the brilliance of sophisticated object-oriented solutions.
In this chapter you will:
❑ Learn what design patterns are and how to make use of them.
❑ Examine an example of a design pattern.
❑ Learn about the very influential Dependency Injection pattern.
❑ Apply refactoring to patterns concepts to the Rent-a-Wheels application.
Refactoring to Patterns chapter caveat: Design patterns are a huge subject, and there is only somuch that can be accomplished in the space of one chapter. More than to give you any practicalknowledge on design patterns and refactoring combination, the purpose of this chapter is to insti-gate you in the right direction, to interest you in the subject, so you would learn more about designpatterns and their application. As you learn more about patterns, you will recognize situationswhere they can be applied, and you will devise refactorings that will help you incorporate theminto real-life, operational code.
79796c14.qxd:WroxPro 2/25/08 9:06 AM Page 407
Design Patterns: What’s All the Fuss About?Design patterns came into prominence in the software industry with the publication of Design Patterns:Elements of Reusable Software Design by Erich Gamma, Richard Helm, Ralph Johnson, and John M. Vlissides(Addison-Wesley, 2004), close to some 15 years ago. Since then, they have been the subject of numerousbooks, articles, conference speeches, and lectures and have become part of mainstream object-orientedcompendium.
Today, you will often hear developers discuss design not in terms of originality of the solution, but in termsof design patterns — what design patterns they used in their design and to which purpose. Patterns havebecome not only part of the common corpus of knowledge, but also part of the programmer’s commonvocabulary.
So just what are design patterns? Consider this example: a master chess player is capable of looking manymoves ahead. To some extent, this is because of his experience and his natural talent. In great part, however,this is thanks to the study of other players’ moves. Once notable moves are played out in a tournament, theyare analyzed and archived, and they become part of global chess-playing knowledge. By studying alreadyplayed out moves, learning openings and endgames, and studying common approaches in the middlegame, a master player is capable of taking advantage of other people’s inventiveness and hard work. Suchstudy gives a player a much broader horizon, meaning that player is a lot less likely to be surprised or facedwith a totally new situation. Study will liberate that player from solving some basic problems at each gameand with each move. Instead of thinking two or three moves in advance, a player can think of possibledevelopments five or six moves ahead. Even an extremely talented autodidactic player, when confrontedwith a learned player, will have little chance of success.
Design patterns will give you similar insight in the area of software development. By learning designpatterns, you will be able to meet the best developers in the world and learn from them. Programming is often viewed as a craft. In any craft, a master-apprentice relationship is very important for the learningprocess. Through design patterns you can record and learn a good portion of an expert programmer’smastery. Some crafts are not easily described or translated into the written form. Programming, on theother hand, is different because its final product is source code in a written form, and therefore can besuccessfully recorded in writing. If you‘re not able to learn from a teacher in flesh and blood, then thatteacher’s wisdom recorded in writing is the next best thing.
Patterns record moments of a programmer’s ingenuity, and they help share these moments with you.When you study patterns, you will see that there is always a nontrivial, inventive, and imaginativeturnaround view of the problem.
Defining Design PatternsPatterns capture and share the experience of others. They are a guide or template for a solution to a certainproblem in software design. They describe collaborating objects and classes that have to be adapted to dealwith certain problems, their structure, roles, and responsibilities. Patterns abstract and identify key aspectsof a design used to solve a single problem in a particular context.
While they provide you with an immediate insight on how to deal with a certain problem, you still haveto adapt the solution to your concrete situation, choose the correct variation, and often compare andweigh out different solutions for the same problem.
408
Part V: Refactoring Applied
79796c14.qxd:WroxPro 2/25/08 9:06 AM Page 408
Although patterns often include sample code, they are not intended to be reused directly, on code level, asfor example, components, frameworks, or reusable classes are. Instead of code reuse, they promote designreuse. By learning patterns, you learn to analyze, identify, and divide the problem; you learn different tech-niques to use, structure, and organize classes and objects. You compare patterns and their trade-offs andcome to understand how they relate to each other.
While some patterns can be turned into reusable components, insisting on code reuse with patterns goesin a certain sense against pattern philosophy. Learning patterns means that you should spend time ana-lyzing, understanding, assimilating, and applying the pattern. This way, you improve your design andprogramming skills. By using ready-made pattern components, you might miss important aspects ofobject-oriented design. In this case, exposure to detail is desired, and encapsulation will not help youhone your design skills.
Classifying PatternsHaving in mind the construction of a design patterns catalog that could be easily searched and browsed, the authors of Design Patterns: Elements of Reusable Software Design proposed the following classification for design patterns — all patterns should be categorized according to their purpose. “Gang of Four (GOF)”design patterns, named so because of the four authors of Design Patterns, are general-purpose patterns thatcan be further classified into three groups:
❑ Creational — These patterns solve different problems related to object creation or instantiation.
❑ Behavioral — These patterns deal with the ways that objects can communicate and shareresponsibilities in order to reach a certain goal.
❑ Structural — These patterns describe assembly and composition of objects and classes used tosolve certain problems.
Patterns can also describe designs related to some more specific field, so you can have real-time patterns,concurrency patterns, object-relational patterns, and so on. This chapter will stick with classic, mostlycreational patterns.
Pattern ElementsIn order for patterns to be efficiently consulted and used, they should be written in concise and consis-tent form. The authors of Design Patterns used a template where each pattern definition consisted of anumber of sections. This is today widely accepted form to describe a pattern. Whatever the form used to describe the pattern, it should explain the four following essential elements of each pattern:
❑ Name — A descriptive and original name is important for pattern recognition and memoriza-tion. As you study patterns, you will notice that name is associated with the solution that pat-tern provides.
Definition: Design patterns represent a proven solution to a recurring problem in agiven context.
409
Chapter 14: Refactoring to Patterns
79796c14.qxd:WroxPro 2/25/08 9:06 AM Page 409
❑ Problem — The problem section describes the situation where it makes sense to use the pattern.It describes context, conditions that need to be met in order to use the pattern, and the designproblem that the pattern solves. It should also mention some examples of poor design that thispattern resolves.
❑ Solution — In this section principal classes and objects representing the design are describedtogether with their responsibilities and collaboration. This section generally includes a UMLdiagram, most often the one depicting the static structure of the solution. It should also includesome sample code that illustrates the design.
❑ Consequences — Here different trade-offs that result from pattern usage are discussed. Often,this section mentions some real-life situations where this pattern can be applied. Different patterns are compared and confronted, and cases when some patterns can be used jointly are described.
Weighing the Benefits of Design PatternsWhen you design software, you have to weigh the trade-offs between different solutions. You generallystart the project by thinking about putting data and behavior together and encapsulating it by writingclasses. You have to consider granularity and work on eliminating duplication. As the project grows andprogresses, your focus changes, and you start thinking more about flexibility, performance, and modularity.With time, more importance is placed on aspects of growth, evolution, dependencies, and maintainability.Often, your focus is dedicated to only certain aspects of the design, those more prominent at certain stagesof the project. You are not always able to envisage the future consequences because this is complex, andyou lack the necessary experience.
You often hear that decisions you make early on in the project are the ones that have the greatest conse-quences and are hardest to change. This means that you should take into consideration all of these aspectsof your design from the beginning. This does not mean that you need to start your project with somesophisticated design up front, but you need to be aware of trade-offs that different designs bring into your design.
Design patterns help you understand these trade-offs and show you how to solve certain problems.They let you compare alternatives and understand different aspects right from the start; they give you the necessary perspective that is impossible to have without the experience and shared, incremen-tal knowledge.
Using PatternsWhen you learn patterns, you are not supposed to reproduce the solution you read in the exact form itwas described. You have to choose what is best for your concrete situation. Patterns can especially helpyou with the design of those classes that do not have direct correspondence in the real world, makingyour design more flexible and reusable. These classes are always present as your design gets moresophisticated. As you assimilate patterns, you learn how to incorporate them in your design, how tochoose between different alternatives, and how to combine and adapt the patterns until you reach asophisticated design that has its own aesthetics and style.
Now that you have some grounding in what design patterns are, it’s time to turn to an example.
410
Part V: Refactoring Applied
79796c14.qxd:WroxPro 2/25/08 9:06 AM Page 410
Example Design Pattern: Abstract FactoryAs this book demonstrates, I am a great proponent of evolutionary design. If you already have a lot ofexperience with a certain type of application, you may rely on your experience to start with some moresophisticated design up front. If not, it is best if you refine your design continuously. As you work on theproject, you are able to put your design to test in a real-life situation and to adapt it to resolve unforeseenproblems that might arise.
This example of a design pattern illustrates this approach. Imagine you have to construct a very simple data maintenance application. With time, new requirements are added, and the code is growing in quantity.In order to deal with increased complexity, the code has to be refactored and design patterns applied.
The merits of different refactorings including pattern incorporation are discussed as the example goesalong. This way, you can see when the circumstances to introduce a pattern in your code are indicatedand how the pattern introduction step is always motivated by a specific need in your design. You willhave the chance to discuss and evaluate the pattern not in some abstract circumstances but with a con-crete problem at hand.
NameAbstract Factory
ProblemIn this example, you will see how Abstract Factory is applied to resolve the problem of instantiation of related data provider objects such as connection, command, adapter, and others in a consistent poly-morphic manner:
❑ The client code should not depend on specific types belonging to a provider like Oracle orMicrosoft SQL Server. In case a new database needs to be added to the list of supported data-bases, a new provider should be incorporated without requiring code modification in the client.
❑ The provider object should be instantiated in a consistent manner. If a client is using an objectfrom one provider, then there should be some mechanism put to practice so that only objectsbelonging to a specific provider are instantiated. If a client is using System.Data.SqlClient.SqlConnection, then System.Data.SqlClient.SqlCommand should be created when the client asks for a command object, and not some other command like System.Data.OracleClient.OracleConnection or another.
Data Entry Sample ApplicationThe Initial version of the application is very simple. You have a few forms that let you display and mod-ify data in a database, and they have typical Load, New, Save, and Delete buttons. The module Globalsholds the global ConnectionString variable. The database in question is the Microsoft SQL database,so the data provider from System.Data.SqlClient namespace is used to query the database. The codebehind the Delete button click event is presented in Listing 14-1.
411
Chapter 14: Refactoring to Patterns
79796c14.qxd:WroxPro 2/25/08 9:06 AM Page 411
Listing 14-1: Simple Event-Handling Routine That Deletes a Record in the Database
Private Sub Delete_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles Delete.Click
Dim connection As New SqlConnection(ConnectionString)Dim command As New SqlCommand( _“Delete [User] where [Id] = @Id”, connection)command.CommandType = CommandType.Textconnection.Open()Dim idParameter As SqlParameter = command.CreateParameteridParameter.ParameterName = “@Id”idParameter.Value = CInt(Me.Id.Text)idParameter.DbType = DbType.Int16command.Parameters.Add(idParameter)command.ExecuteNonQuery()connection.Close()
End Sub
This code represents a very simple approach that works for the time being. As a matter of fact, other routinesare very similar to Delete_Click, but because variables are initialized at the same line that declares them,the code looks more compact and duplicated code does not look that worrisome at the moment.
Adding Multiple Database Engine Support by Means of PolymorphismA few finished forms later, you are informed by your client that some installations use the same databasestructure, but different database engine. This client would like to use the same application to access thedata residing on Oracle and in some cases even Microsoft Access databases.
Your initial idea is to add Select...Case statements that will deal with this situation. You have added aglobal flag DbProvider declared in the Globals module that can indicate the current provider. The sameDelete_Click routine programmed this way would then look something like the code in Listing 14-2.
Listing 14-2: Delete Routine with Different Case for Different Database Provider
Private Sub Delete_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles Delete.Click
Select Case Globals.DbProviderCase DbProviders.MsSql
Dim connection As New SqlConnection(ConnectionString)Dim command As New SqlCommand( _“Delete [User] where [Id] = @Id”, connection)command.CommandType = CommandType.Textconnection.Open()Dim idParameter As SqlParameter = command.CreateParameteridParameter.ParameterName = “@Id”idParameter.Value = CInt(Me.Id.Text)idParameter.DbType = DbType.Int16command.Parameters.Add(idParameter)command.ExecuteNonQuery()connection.Close()
412
Part V: Refactoring Applied
79796c14.qxd:WroxPro 2/25/08 9:06 AM Page 412
Listing 14-2: Delete Routine with Different Case for Different Database Provider (continued)
Case DbProviders.OracleDim connection As New OracleConnection(ConnectionString)Dim command As New OracleCommand( _“Delete [User] where [Id] = @Id”, connection)command.CommandType = CommandType.Textconnection.Open()Dim idParameter As OracleParameter = command.CreateParameteridParameter.ParameterName = “@Id”idParameter.Value = CInt(Me.Id.Text)idParameter.DbType = DbType.Int16command.Parameters.Add(idParameter)command.ExecuteNonQuery()connection.Close()‘...
End SelectEnd Sub
It is immediately obvious that this approach will not get you very far. The code will multiply for eachprovider you might add. So is there another approach you might use?
If you analyze the code a bit you see that the connection, command, and parameter declaration and thecreation code are the only pieces of code that differ between the two Case blocks. This is a good indica-tion for the direction in which you should start looking for a better solution.
If you take a look at SqlConnection and OracleConnection in ObjectBrowser, you notice that theyboth implement a common IDbConnection interface. They have a common parent interface, and if vari-able type is declared correctly, they can be treated as the same type of object.
The example routines employ primarily those members of SqlConnection and OracleConnection thatare defined in IDbConnection interface. The situation is similar with SqlCommand and OracleCommand;they share a common IDbCommand interface. And finally, OracleParameter and SqlParameter share acommon IDbParameter interface.
Now, if you treat your objects in polymorphic manner and declare connection as IDbConnection, command as IDbCommand, and parameter as IDbParameter, the only difference between the two caseblocks is the object creation code, namely the New statement.
Upcasting Object DeclarationUpcasting object declaration means that you change the declaration so the object is declared as some othertype higher in the type hierarchy. You change local variable declaration so that variables are declared as aninterface from the System.Data namespace. Connection is declared as System.Data.IDbConnection,command as System.Data.IDbCommand, and parameter as System.Data.IDbDataParameter. In otherroutines, where some other objects such as reader or adapter are used, a similar procedure is followed. Thecode resulting from upcasting variables can be seen in Listing 14-3.
413
Chapter 14: Refactoring to Patterns
79796c14.qxd:WroxPro 2/25/08 9:06 AM Page 413
Listing 14-3: Variables Upcast to an Interface Common to All Data Providers
Private Sub Delete_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles Delete.Click
Dim connection As IDbConnection = _New SqlConnection(ConnectionString)Dim command As IDbCommand = _New SqlCommand(“Delete [User] where [Id] = @Id”)command.Connection = connectioncommand.CommandType = CommandType.Textconnection.Open()Dim idParameter As IDataParameter = command.CreateParameteridParameter.ParameterName = “@Id”idParameter.Value = CInt(Me.Id.Text)idParameter.DbType = DbType.Int16command.Parameters.Add(idParameter)command.ExecuteNonQuery()connection.Close()
End Sub
Splitting Initialization from Declaration of Data Provider ObjectAt this point, you need to isolate the lines that differ depending on which data provider is used. Theonly code that differs in this example depending on the provider are the provider object instantiationstatements: New SqlConnection, New SqlCommand, and so on. Because the data provider objects are at the moment initialized on the same line that is used to declare them, in order to isolate this code, it isbest if you perform Split Initialization from Declaration refactoring on these variables. Take a look at theresulting code in Listing 14-4.
Listing 14-4: Initialization Split from Declaration for Data Provider Objects
Private Sub Delete_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles Delete.Click
Dim connection As IDbConnectionconnection = New SqlConnection(ConnectionString)Dim command As IDbCommandcommand = New SqlCommand(“Delete [User] where [Id] = @Id”)command.Connection = connectioncommand.CommandType = CommandType.Textconnection.Open()Dim idParameter As IDataParameter = command.CreateParameteridParameter.ParameterName = “@Id”idParameter.Value = CInt(Me.Id.Text)idParameter.DbType = DbType.Int16command.Parameters.Add(idParameter)command.ExecuteNonQuery()connection.Close()
End Sub
Extracting Data Provider Object Creation Code as MethodsIf you were to introduce conditional code that creates different provider objects depending on specificproviders, the Select-Case block would be significantly smaller than the first intent in Listing 14-2.
414
Part V: Refactoring Applied
79796c14.qxd:WroxPro 2/25/08 9:06 AM Page 414
However, as provider creation code is common to a majority of routines in the form, the better solutionis to extract these statements as separate methods.
You can extract connection and command object creation code to separate methods. Once that is done,all of the routines on the form can reference these extracted methods. The code resulting from dataprovider objects creation methods extraction is shown in Listing 14-5.
Listing 14-5: CreateConnection and CreateCommand Methods Extracted
Public Class UserMaintenancePrivate Const DeleteUserSql As String = _“Delete [User] where [Id] = @Id”
Private Function CreateConnection() As IDbConnectionDim connection As SqlConnectionconnection = New SqlConnection()Return connection
End Function
Private Function CreateCommand() As IDbCommandDim command As SqlCommandcommand = New SqlCommand()Return command
End Function
Private Sub Delete_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles Delete.Click
Dim connection As IDbConnectionconnection = CreateConnection()connection.ConnectionString = ConnectionStringDim command As IDbCommandcommand = CreateCommand()command.CommandText = DeleteUserSqlcommand.Connection = connectioncommand.CommandType = CommandType.Textconnection.Open()Dim idParameter As IDataParameter = command.CreateParameteridParameter.ParameterName = “@Id”idParameter.Value = CInt(Me.Id.Text)idParameter.DbType = DbType.Int16command.Parameters.Add(idParameter)command.ExecuteNonQuery()connection.Close()
End Sub ‘...End Class
Introducing Provider Objects Creation LogicNow that you have methods containing purely object creation code, you can finally introduce logicthat instantiates a specific provider’s objects depending on the currently configured data provider. Theprovider name is read from the application configuration file and then appropriate variables set in themodule Globals, declaring some global variables. The configuration section in app.config file lookslike the file shown in Listing 14-6.
415
Chapter 14: Refactoring to Patterns
79796c14.qxd:WroxPro 2/25/08 9:06 AM Page 415
Listing 14-6: ConnectionString Section in app.config Configuration File
<?xml version=”1.0” encoding=”utf-8” ?><configuration><!-- ... -->
<connectionStrings><add name=”DataEntry”
connectionString=”Data Source=localhost;Initial Catalog=Users;Integrated Security=SSPI;”providerName=”System.Data.SqlClient”/>
</connectionStrings></configuration>
Note that the connectionStrings section contains a providerName attribute, and the value of theattribute is the namespace of a specific provider.
You can use the Application Startup event to read these settings and populate variables in the Globalsmodule. The code for the Application Startup event is shown in Listing 14-7.
Listing 14-7: Application Startup Event Reading Provider Configuration Data
Namespace MyPartial Friend Class MyApplication
Private Sub MyApplication_Startup(ByVal sender As Object, _ByVal e As StartupEventArgs) Handles Me.Startup
Dim config As Configuration =ConfigurationManager.OpenExeConfiguration( _
ConfigurationUserLevel.None)Dim connectionStringsSection As ConnectionStringsSection = _config.ConnectionStringsDim connectionStringSettings As ConnectionStringSettings = _connectionStringsSection.ConnectionStrings(DbConfigurationSectionName)ConnectionString = connectionStringSettings.ConnectionStringGlobals.ProviderName = connectionStringSettings.ProviderName
End SubEnd Class
End Namespace
The code for the module Globals, used to hold the current data provider name and connection string, isshown in Listing 14-8.
Listing 14-8: Module Globals
Option Strict OnOption Explicit On
Module GlobalsPrivate ConnectionStringValue As StringPublic Const DbConfigurationSectionName As String = “DataEntry”
416
Part V: Refactoring Applied
79796c14.qxd:WroxPro 2/25/08 9:06 AM Page 416
Listing 14-8: Module Globals (continued)
Private ProviderNameValue As StringPublic Const MsSqlProviderName As String = “System.Data.SqlClient”Public Const OracleProviderName As String = “System.Data.OracleClient”Public Property ConnectionString() As String
GetReturn ConnectionStringValue
End GetSet(ByVal value As String)
ConnectionStringValue = valueEnd Set
End PropertyPublic Property ProviderName() As String
GetReturn ProviderNameValue
End GetSet(ByVal value As String)
ProviderNameValue = valueEnd Set
End PropertyEnd Module
Now that you have seen how the data provider configuration is read from the configuration file and main-tained in the module Globals, you can see the code for two data provider objects creation methods. It isshown in Listing 14-9.
Listing 14-9: CreateConnection and CreateCommand Methods Containing Conditional Logic
Private Function CreateConnection() As IDbConnectionDim connection As IDbConnection = NothingSelect Case Globals.ProviderName
Case Globals.MsSqlProviderNameconnection = New SqlConnection()
Case Globals.OracleProviderNameconnection = New OracleConnection()
End SelectReturn connection
End Function
Private Function CreateCommand() As IDbCommandDim command As IDbCommand = NothingSelect Case Globals.ProviderName
Case Globals.MsSqlProviderNamecommand = New SqlCommand()
Case Globals.OracleProviderNamecommand = New OracleCommand()
End SelectReturn command
End Function
417
Chapter 14: Refactoring to Patterns
79796c14.qxd:WroxPro 2/25/08 9:06 AM Page 417
Extracting Creation Methods to a Separate ClassAs it soon becomes apparent, the same data provider objects creation code is present in more than oneform class. This means that the same code would be duplicated across numerous classes. In order toavoid duplication, you can extract the connection and command objects creation code into a separateclass, as shown in Listing 14-10. You can call this class DataProviderFactory.
Listing 14-10: A First Take at the Factory Class
Option Explicit OnOption Strict On
Imports System.DataImports System.Data.SqlClientImports System.Data.OracleClient
Public Class DataProviderFactoryPublic Function CreateConnection() As IDbConnection
Dim connection As IDbConnection = NothingSelect Case Globals.ProviderName
Case Globals.MsSqlProviderNameconnection = New SqlConnection()
Case Globals.OracleProviderNameconnection = New OracleConnection()
End SelectReturn connection
End FunctionPublic Function CreateCommand() As IDbCommand
Dim command As IDbCommand = NothingSelect Case Globals.ProviderName
Case Globals.MsSqlProviderNamecommand = New SqlCommand()
Case Globals.OracleProviderNamecommand = New OracleCommand()
End SelectReturn command
End FunctionEnd Class
Now that you have a class dedicated to data provider objects creation, you can move global variablesfrom the Globals module to this class. Now that they reside in a class and not in a module, they have tobe marked as Shared and prefixed with the class name when referenced. This way you manage to findsome context for former global variables. By moving variables to DataProviderFactory, moduleGlobals is rendered empty, so it can be deleted from the project.
Global variables are one of the earliest recorded and the most notorious code smells.
The code for DataProviderFactory after receiving variables from the Globals module is shown inListing 14-11.
Listing 14-11: DataProviderFactory with Provider-Related Variables
Option Explicit OnOption Strict On
418
Part V: Refactoring Applied
79796c14.qxd:WroxPro 2/25/08 9:06 AM Page 418
Listing 14-11: DataProviderFactory with Provider-Related Variables (continued)
Imports System.DataImports System.Data.SqlClientImports System.Data.OracleClient
Public Class DataProviderFactory
Public Const DbConfigurationSectionName As String = “DataEntry”Public Const MsSqlProviderName As String = “System.Data.SqlClient”Public Const OracleProviderName As String = “System.Data.OracleClient”
Private Shared ConnectionStringValue As StringPrivate Shared ProviderNameValue As String
Public Shared Property ConnectionString() As StringGet
Return ConnectionStringValueEnd GetSet(ByVal value As String)
ConnectionStringValue = valueEnd Set
End Property
Public Shared Property ProviderName() As StringGet
Return ProviderNameValueEnd GetSet(ByVal value As String)
ProviderNameValue = valueEnd Set
End Property
Public Function CreateConnection() As IDbConnectionDim connection As IDbConnection = NothingSelect Case ProviderName
Case MsSqlProviderNameconnection = New SqlConnection()
Case OracleProviderNameconnection = New OracleConnection()
End SelectReturn connection
End FunctionPublic Function CreateCommand() As IDbCommand
Dim command As IDbCommand = NothingSelect Case ProviderName
Case MsSqlProviderNamecommand = New SqlCommand()
Case OracleProviderNamecommand = New OracleCommand()
End SelectReturn command
End Function
End Class
419
Chapter 14: Refactoring to Patterns
79796c14.qxd:WroxPro 2/25/08 9:06 AM Page 419
You now have a functional factory class. It provides methods that instantiate the correct connection orcommand object for a certain provider. However, the class still has a few major problems:
❑ If you change the ProviderName property, you will be able to instantiate objects for differentproviders from the same factory. What’s more, all instances of factory share the ProviderNameproperty, meaning that only one data provider can be current at a given time. You need to be ableto create objects in a consistent form. If the factory has returned an instance of System.Data.SqlClient.SqlConnection when calling the CreateConnection method, it is only logicalthat CreateCommand should return an instance of System.Data.SqlClient.SqlCommand andnot some other command object.
❑ As you add new providers, you have to modify the factory class, adding another Case block tothe Select-Case statement.
❑ The Select-Case statement is duplicated between different methods. While this duplication is not yet very harmful, as you add more object creation methods and more providers, it willbecome more annoying.
❑ Finally, you have single class referencing a number of providers. As providers are added, this will result in a growing Imports statement section and a class that centralizes dependencies on data providers. In order to compile the class, you must have at your disposal all the providersit references.
How should you go about addressing this? What is the next step you should take? Well, here is where theAbstract Factory pattern kicks in. It describes the solution that can be applied in the situation like this.
SolutionYou have refactored the code in incremental steps up to this point. You have managed to improve thedesign, but there are a number of issues that are still present in the code. However, the solution is notimmediately visible.
Refactoring Creational Code to Abstract Data Provider FactoryThe Abstract Factory pattern gives the solution to the problem at hand. It amounts to the following steps:
1. Create an abstract factory that defines interfaces for the creation of related objects.
2. Use concrete factories that inherit the abstract factory override and implement creational meth-ods that instantiate related concrete types defined by Abstract Factory’s abstract methods.
In this example, you will have a single factory for each data provider:
❑ For an Oracle data provider, you will create OracleProviderFactory.
❑ For a Microsoft data provider, you will create MsSqlProviderFactory.
❑ For an OLE DB provider, you will create OleDbProviderFactory.
You will add another concrete factory class for each provider you wish to support.
In this example, in order to refactor code to the Abstract Factory pattern, you perform ExtractSubclass refactoring on the DataProviderFactory class from Listing 14-11. The extracted methodsare CreateConnection and CreateCommand, and each subclass contains one Case block. Finally,
420
Part V: Refactoring Applied
79796c14.qxd:WroxPro 2/25/08 9:06 AM Page 420
you make the DataProviderFactory class abstract, and you mark CreateConnection andCreateCommand with MustOverride. These methods in DataProviderFactory contain no imple-mentation. The resulting code is presented in Listing 14-12.
Listing 14-12: DataProviderFactory as Abstract Factory
Option Explicit OnOption Strict On
Imports System.DataImports System.Data.SqlClientImports System.Data.OracleClient
Public MustInherit Class DataProviderFactory
Private Shared ConnectionStringValue As StringPublic Const DbConfigurationSectionName As String = “DataEntry”Private Shared ProviderNameValue As StringPublic Const MsSqlProviderName As String = “System.Data.SqlClient”Public Const OracleProviderName As String = “System.Data.OracleClient”Public Const OleDbProviderName As String = “System.Data.OleDb”
Public Shared Property ConnectionString() As StringGet
Return ConnectionStringValueEnd GetSet(ByVal value As String)
ConnectionStringValue = valueEnd Set
End Property
Public Shared Property ProviderName() As StringGet
Return ProviderNameValueEnd GetSet(ByVal value As String)
ProviderNameValue = valueEnd Set
End Property
Public Shared Function CreateDataProviderFactory( _ByVal provideName As String) As DataProviderFactory
Select Case provideNameCase MsSqlProviderName
Return New MsSqlProviderFactoryCase OracleProviderName
Return New OracleProviderFactoryCase OleDbProviderName
Return New OleDbProviderFactoryEnd SelectReturn Nothing
End Function
Public Shared Function CreateDataProviderFactory() As DataProviderFactory
Continued
421
Chapter 14: Refactoring to Patterns
79796c14.qxd:WroxPro 2/25/08 9:06 AM Page 421
Listing 14-12: DataProviderFactory as Abstract Factory (continued)
Return CreateDataProviderFactory(ProviderName)End Function
Public MustOverride Function CreateConnection() As IDbConnection
Public MustOverride Function CreateCommand() As IDbCommand
End Class
Note how both CreateConnection and CreateCommand return objects of interface type from the rootdata provider namespace System.Data. The class does not reference any concrete providers. The sharedCreateDataProviderFactory method is used to return concrete factory and contains a single SelectCase statement. Listings 14-13 to 14-15 present the code of each concrete factory.
Listing 14-13: MsSqlProviderFactory Factory
Option Strict OnOption Explicit On
Imports System.Data.SqlClient
Public Class MsSqlProviderFactoryInherits DataProviderFactory
Public Overrides Function CreateCommand() _As System.Data.IDbCommand
Return New SqlCommandEnd Function
Public Overrides Function CreateConnection() _As System.Data.IDbConnection
Return New SqlConnectionEnd Function
End Class
Note how MsSqlProviderFactory imports only the System.Data.SqlClient namespace.
Listing 14-14: OleDbProviderFactory Factory
Option Strict OnOption Explicit On
Imports System.Data.OleDb
Public Class OleDbProviderFactoryInherits DataProviderFactory
Public Overrides Function CreateCommand() _As System.Data.IDbCommand
Return New OleDbCommand
422
Part V: Refactoring Applied
79796c14.qxd:WroxPro 2/25/08 9:06 AM Page 422
Listing 14-14: OleDbProviderFactory Factory (continued)
End Function
Public Overrides Function CreateConnection() _As System.Data.IDbConnection
Return New OleDbConnectionEnd Function
End Class
OleDbProviderFactory imports only the System.Data.OleDb namespace.
Listing 14-15: OracleProviderFactory Factory
Option Strict OnOption Explicit On
Imports System.Data.OracleClient
Public Class OracleProviderFactoryInherits DataProviderFactory
Public Overrides Function CreateCommand() _As System.Data.IDbCommand
Return New OracleCommandEnd Function
Public Overrides Function CreateConnection() _As System.Data.IDbConnection
Return New OracleConnectionEnd Function
End Class
Finally, OracleProviderFactory imports only the System.Data.OracleClient namespace. You cansee this code represented graphically in Figure 14-1.
Figure 14-1
MsSqlProviderFactory OleDbProviderFactory OracleProviderFactory
+CreateConnection()+CreateCommand()
+CreateConnection()+CreateCommand()
+CreateConnection()+CreateCommand()
+ConnectionString+ProviderName+CreateConnection()+CreateCommand()+CreateDataProviderFactory()
DataProviderFactory
423
Chapter 14: Refactoring to Patterns
79796c14.qxd:WroxPro 2/25/08 9:06 AM Page 423
The future implementation and modification is easy to follow. For each new provider, a new concretefactory inheriting DataProviderFactory is added. For each new object, for example IDbDataAdapter,a new creation method (CreateDataAdapter) is added.
ConsequencesNow that you have implemented the solution, you should understand the consequences of this patternin detail. First, consider the benefits that this pattern brings to your design:
❑ Clients can use data provider in a polymorphic manner. They never need to reference any con-crete data provider class. All they need to know are interfaces from the system data namespace.This leads to less code and less duplication in your code and all other benefits that polymorphicbehavior can bring.
❑ Polymorphic use of providers makes exchanging different providers easy. Clients are neveraware of the concrete type of object that factory creates. This makes it easy to add configurationcapability to the application and to externalize to a configuration file a string that indicates thecurrent provider. This way, there is no need to change the code or to recompile the applicationin order to use different providers. This makes your code less dependent on a specific databaseand helps evade vendor lock-in.
❑ The rule where only related objects are created is enforced. Once you create a factory, itreturns provider objects of a specific provider only. MsSqlProviderFactory always createsobjects from System.Data.SqlClient, SqlConnection, and SqlCommand and none other.This makes creational code behave in a consistent and predictive manner.
❑ From a testing perspective, the pattern makes client code easier to unit test. All you need to do when executing tests is use MockDataProviderFactory, a factory that creates mocked dataprovider objects.
There are also some negative sides to this pattern. You need to understand those as well.
❑ Creating another type that needs to be created by factory is not easy. For example, if you wishto add a creational method for Data Adapter, you need to add a new method to the abstractDataProviderFactory class. This, as a consequence, obliges you to implement this method in all concrete factory classes inheriting the DataProviderFactory class.
❑ Adding a new family of objects (new data provider in this example) is relatively costly,because it requires you to implement new a concrete factory class for each family you add.
When you need to use the pattern, it does not mean that the design is cast in stone and that you have to follow the code from the pattern example line by line. You can modify and adapt the pattern so it suitsyour own needs. As a matter of fact, that is the only correct way to use patterns. To illustrate that, take alook at some variations that can be applied to the Abstract Factory pattern.
VariationsThere are different ways to refine and implement this pattern. The following sections explore some ofthe variations you might use when implementing the pattern yourself.
424
Part V: Refactoring Applied
79796c14.qxd:WroxPro 2/25/08 9:06 AM Page 424
Instantiating the Factory ItselfMaybe the biggest problem with the solution is the concrete factory creation code from Listing 14-12. If you take a look at the shared CreateDataProviderFactory method, you can see that you need tomodify the code of Abstract Factory by adding a new Case block for each new concrete factory thatneeds to be incorporated. One solution is to use the concrete factory’s name as a provider parameter(ProviderName property) and then instantiate the factory by means of reflection. This way, each time anew concrete factory is added, it is enough to give the correct provider name parameter to instantiate thecorrect provider, and no modification to DataProviderFactory is required. If you wish, you can give ita try and implement CreateDataProviderFactory in this fashion.
Default ImplementationIn some cases, it might be applicable to have some default implementation for the factory. In that case,you could instantiate the AbstractFactory itself that would provide some meaningful default imple-mentation of the factory. Needless to say, this would convert the abstract factory into a concrete factoryitself. It means you would have to erase the MustInherit keyword from the abstract factory’s declara-tion and change the MustOverride keyword to Overridable in the creation methods declaration andprovide default implementation for them. In this case, the OLE DB provider makes the most sense as adefault implementation because of its versatility in connecting to different data sources (provided thatnecessary dll libraries are available).
Concrete Factories as SingletonsA singleton is another design pattern. It helps you enforce only a single instance of a certain type ifyour application is created during application lifetime. It makes sense to implement concrete factoriesas singletons, because only a single instance of each concrete factory will be needed anyway.
Factory Inheritance HierarchiesIn a similar way you create hierarchies of other objects, you can have hierarchies of factories. Imagine you have programmed an enhanced SQL Command object, capable of caching data parameters. TheEnhancedSqlCommand class works with the same System.Data.SqlClient.SqlConnection object asSystem.Data.SqlClient.SqlCommand does. You can program your EnhancedMsSqlProviderFactoryinheriting MsSqlProviderFactory and override a single CreateCommand method that will return aninstance of EnhancedSqlCommand instead of the standard SqlCommand.
Data Provider Factory in .NET Framework 2.0If you are using Visual Studio 2005 or later, you do not need to create your own data provider facto-ries. Creators of the .NET Framework have realized the benefits of the Abstract Factory pattern andprovided the classes that implement it. Take a look at System.Data.Common.DbProviderFactoryand System.Data.Common.DbProviderFactories classes and compare their design to one used inthis chapter.
The existence of these classes by no means renders our refactor to the abstract factory exercise futile. Thepurpose of this example was not to solve a specific problem but to use a well-known problem to exposea possible solution to this and similar problems; to demonstrate the effectiveness of the refactoringprocess in combination with design patterns. Now you have tasted the power of design patterns andrefactoring combination. The next section continues this discussion with a few points about a relatedand very influential pattern.
425
Chapter 14: Refactoring to Patterns
79796c14.qxd:WroxPro 2/25/08 9:06 AM Page 425
Dependency Injection PatternWhile not one of classic GOF patterns, the Dependency Injection (DI) pattern has become hugely influ-ential as of late. It has given rise to numerous lightweight containers and unobtrusive DI frameworks. It has changed the way many developers approach application design. However, I must warn you rightaway that improvements that this approach brings to your design and especially to your developmentprocess are not easily observable on a small scale. Nevertheless, it is important to learn more about thispattern and see what benefits it brings.
ProblemWhen you write your typical industrial-strength application, you are faced with great complexity thattypical real-life software projects are characterized by. The common way to deal with the complexity inyour project is modularity. Instead of making your application as a single monolithic construct, youassemble your application out of existing or newly created components and services. Each componentand service has to solve a piece of the puzzle that your application represents.
Software Components and ServicesTo put it in simple terms, components are reusable and deployable binaries created to bereused as a part of some (often unknown) application. Component-based applicationsare created by composing or assembling different components. As a VB programmer,you are most probably familiar with COM and COM + components. (Software compo-nents are discussed in Chapter 8.)
Services are similar to components and they are also used by other applications. Serviceshave the capacity to be accessed remotely, meaning that they can be hosted by someremote machine and generally execute in some other process. A typical example of serviceis a web service that is accessed through SOAP protocol.
Providing components with remote access can often be technically straightforward, sothe difference between the two can be rather blurred. It may depend only on how youdeploy the component. Finally, in a .NET environment, you access and use servicesand components in the same way you would use and access any other object.
Dependency Injection and Inversion of ControlDI is also often referred to by another name: Inversion of Control (IoC). These terms areused more or less interchangeably throughout the chapter. However, Inversion of Controlis a more general technique often used in programming when part of control is resignedin favor of some other entity. For example, when you code the Windows Form class, youimplement different event handlers that react to user actions. In this case, you have nocontrol over the flow of the program; it is the user that can choose to push any enabledbutton at any point in time. On the other hand, had you programmed this as a consoleapplication, asking the user to enter one data at a time, you would interact with the userin a strictly defined order of execution that is under your complete control.
426
Part V: Refactoring Applied
79796c14.qxd:WroxPro 2/25/08 9:06 AM Page 426
This modular approach is not without its own problems. These problems become more relevant with thescale and overall complexity of the application. Some of the problems you have to deal with are:
❑ How to create components and locate services
❑ How to deal with dependencies between your components and services
❑ How to avoid wedding yourself into single implementation of the component or the service
❑ How to test such an application easily
❑ How to configure the application in a uniform and consistent manner
❑ How to provide the application with additional services such as transaction support or objectpooling without obliging components to implement specific interfaces or inherit specific classes
In any assembled application, you have two principal collaborators:
❑ Client
❑ Service
A Client collaborator (often a service or component in its own right) asks a Service collaborator to providesome service. You can see this represented graphically on Figure 14-2.
Figure 14-2
In order to make this more illustrative, imagine that the client is an Online Purchase component and theservice is a Credit Card Verification service.
The first problem the client has to resolve is to get a hold of service. In a normal object-oriented scenario,the Online Purchase component would simply create a new instance of the Credit Card Verification serviceobject. This is as simple as this:
Dim verificationService as CreditCardVerificationService _= New CreditCardVerificationService())
The problems with this approach are numerous. Because you cannot easily switch to another a CreditCard Verification service implementation:
❑ You depend on a specific service implementation and service provider. What happens if thereis another verification service that charges less?
❑ Your application is not easy to test. Such services generally charge for use, so you wouldn’t liketo use a real service just so you could test your application.
❑ You are not isolated from changes to the verification service, because you depend upon itsimplementation.
perform serviceClient Service
427
Chapter 14: Refactoring to Patterns
79796c14.qxd:WroxPro 2/25/08 9:06 AM Page 427
You can generally distinguish between a service implementation and a service interface. So you couldreduce the dependency on the service by viewing the service through its interface, like this:
Dim verificationService as CreditCardVerificationService _= New PremiumCreditCardVerificationService ())
However, while dependency is reduced to a single service creation line, the Online Purchase componentstill depends on a specific Credit Card Verification Service implementation.
Earlier in this chapter, you saw how you can use factory classes to decouple the client from a specificclass implementation. In the case of services, instead of factories, you rely on Service Locator classes toprovide you with services you need to use. Online Purchase could use a Service Locator to obtain aninstance of Credit Card Verification Service:
Dim locator As ServiceLocator = New ServiceLocatorDim verificationService As CreditCardVerificationService _= locator.find(“premum”)
Or, to present it graphically in more general terms, using Service and Client generic names for collabora-tors, take a look at Figure 14-3.
Figure 14-3
So, how does this fare compared to previous solutions? Obviously, this is much better, because you donot depend on a specific service implementation. There is one problem, however; the client still dependson the Service Locator. You have to ask (send message to) the locator that you need to find a certainobject (service).
Depending on the Service Locator, this can be a limitation, but is not necessarily excessively damaging.There is a way, however, to do even without Service Locator. The solution is to have someone else injecta service inside the client. I will call such an entity an assembler. When creating clients, an assemblerinjects the client with specific service implementation.
SolutionWhen you program your code following the Dependency Injection pattern, you pursue the followingparadigm shift, expressed here from the client’s point of view: “Why should I create or locate compo-nents or services or even ask someone else to create or locate them for me? I’ll just declare that I needcomponents or services, and I’ll let someone else provide them for me.”
perform serviceClient
ServiceLocator ServiceImplementation
«interface»IService
locate
428
Part V: Refactoring Applied
79796c14.qxd:WroxPro 2/25/08 9:06 AM Page 428
Here is where inversion of control comes from: the client is letting someone else create or locate compo-nents or services the client itself needs. Here is what this would look like presented graphically (take alook at Figure 14-4).
Figure 14-4
You can observe one very important effect. The client now depends only on the IService interface. Itmeans that you have successfully resolved the service implementation location problem in such a waythat your client does not depend even on a Service Locator anymore.
You will note that the Assembler, however, depends both on client and service implementation. You mightthink at first that all you have accomplished is to move dependencies from one place to another. You willprobably have numerous components, services, and clients in each application, so having a single place toreconfigure dependencies is already an important accomplishment. Also, this way you are managing tocreate reusable components that do not depend on specific context. If you consider that client can also be acomponent, you have managed to reduce its dependencies on specific service implementations, factories,or service locators. Finally, Assembler dependencies can be often removed by programmatic methods likereflection. This is often the case with lightweight containers.
Constructor-Based vs. Property-Based InjectionIn order for DI to work, the client needs a mechanism that can be used to inject dependencies. For this,we can count on standard object-oriented mechanisms, namely constructors and properties.
❑ If the client declares a service as a property, all an assembler has to do is set the property to refer-ence a service implementation instance. For example, an assembler can contain the following code:
Dim purchase As OnlinePurchase = New OnlinePurchasepurchase.VerificationService = New PremiumCreditCardVerificationService
❑ If the client declares a service as a constructor parameter, all the assembler has to do is create theclient invoking this parameterized constructor and passing a service implementation instance asa parameter.
Dim purchase As OnlinePurchase = _New OnlinePurchase(New PremiumCreditCardVerificationService)
Which way is better? There have been a lot of discussions on the subject. The advantage of constructor-basedinjection is that it instantiates objects in a legal state, with all dependencies provided in the constructor itself.The advantage of property-based injection is that it allows clients to have a default (nonparameterized)
perform serviceClient
Assembler ServiceImplementation
«interface»IService
create or locate
inject service
429
Chapter 14: Refactoring to Patterns
79796c14.qxd:WroxPro 2/25/08 9:06 AM Page 429
constructor, a requirement of some technologies. For example, if you wish to program COM components inVB, you must provide public default, nonparameterized constructors. Finally, there are other forms of injec-tion, for example, interface-based. The choice is more or less the question of specific context and maybe even personal preferences, so I will not dwell on the subject. Now that you can provide the client with thedependency, the question is how the assembler knows what concrete service implementation it is supposedto inject?
What Service Implementation to InjectVery often, you have at your disposal more than one implementation of a service or component. So, howdoes the assembler know which concrete implementation should be injected into the client?
Coded AssemblerIn its most simple form, an assembler can contain literal code that creates and injects concrete dependenciesinto the client. Go back to the Online Purchase component and Credit Card Verification Service example. In Listing 14-16 you can see an example of assembler-encoded injection.
Listing 14-16: Assembler-Encoded Injection
Option Strict OnOption Explicit On
Public Class AssemblerPublic Sub IntializeApplication()
Dim purchase As OnlinePurchase = _New OnlinePurchase(New PremiumCreditCardVerificationService)‘...set purchase client with instance of purchase
End Sub‘...
End Class
What happens is that when you construct applications, you generally end up with large object graphs. In this example, this means that the instance of Online Purchase, the client to the Credit Card VerificationService, will probably be a component that is required by its own client, and so on.
Autowiring AssemblerSuch an assembler is used in combination with other assembling approaches. The most basic way for acomponent to declare a certain dependency and that it should be injected with a service is on the codelevel. If a client has declared a writable property or constructor parameter of a certain type, it means itdepends on this type and needs to have an instance of that type injected. In case there is only single classavailable at runtime that implements a certain service interface, it is obvious that this class is exactly theimplementation that client needs because, simply put, there is no other.
In such a case, many DI-based frameworks are capable of discovering and providing the client with theonly available implementation because they are capable of inspecting components through introspection.This way, some dependencies can be resolved with no additional effort. This is an important characteristicof full-blown assemblers because it keeps applications simple. At the same time it permits programmingto an abstraction principle to be followed, without incurring additional configuration cost.
430
Part V: Refactoring Applied
79796c14.qxd:WroxPro 2/25/08 9:06 AM Page 430
Metadata AssemblerThis form of dependency resolution is the most common in full-blown DI frameworks. They let you exter-nalize to a configuration file (often XML) the dependency information. Such an assembler is capable ofresolving complex dependency graphs and lets you mix standard configuration data with dependencyinformation. Take a look at sample configuration file used by the Spring Framework for .NET:
<object id=”onlinePurchase” type=”OnlinePurchase, OnlinePurchaseAssembly “><property name=” verificationService” ref=”premiumVerification”/>
</object>
<object id=”premiumVerification” type=”PremiumCreditCardVerificationService,CreditCardVerificationAssembly”/>
You can see how each object is defined with a unique ID and type that consists of a full class name andassembly name. In this case, onlinePurchase is property-injected with the premiumVerificationobject, an instance of the PremiumCreditCardVerificationService class.
An alternative to the configuration files is attributes. This is a bit more an intrusive technique (you haveto recompile your code in order to change the way the application is assembled) and is supported byNano Container for .NET. For example:
<RegisterWithContainer()> _Public Class PremiumCreditCardVerificationService‘...
The RegisterWithContainer attribute registers PremiumCreditCardVerificationServicewith the assembler under a class name. The assembler is capable of discovering thatPremiumCreditCardVerificationService is a type of CreditCardVerificationService.
ConsequencesYou can obtain numerous benefits by following Dependency Injection pattern, and the recent popularityof DI frameworks and lightweight containers confirms it. For example:
❑ Solution to component creation and service location code — You are liberated from coding different factories or programming to different service locators. This is not the concern of yourcomponent or service any more. (Remember, a client of some service or component is most oftena service or component itself).
❑ Decoupling of client from specific implementation — By not depending on a specific imple-mentation, you are harvesting all the benefits of the polymorphic approach, and you are creatinghighly reusable, configurable components. Such components do not depend on a specific contextto be reused.
❑ Ease of configuration — Your application is glued from loosely coupled elements. You canexchange different implementations with ease, often just by changing the configuration file. The radically different approach to configuration, completely in the spirit of object-oriented programming, saves you from programming configuration plumbing code. Through DI, config-uration elements are just objects that you assign to other objects.
431
Chapter 14: Refactoring to Patterns
79796c14.qxd:WroxPro 2/25/08 9:06 AM Page 431
❑ Ease of testing — You can easily exchange real implementation with stubs or mocked imple-mentation of services and components for testing purposes, facilitating different forms of testingwithout incurring any programming cost.
❑ DI-based lightweight containers make it easy to add support for other, crosscutting services —An example of this is transaction or pooling that can be added declaratively, without imposingany limitation on the component itself.
❑ DI containers are unobtrusive — This means you do not have to extend a class or implement aspecific interface in order to use services they provide. Often, your class is not aware of the con-tainer’s existence. This is not the case with heavyweight containers.
Some of the negative sides of DI pattern are that you create code that at the first sight is not easy to follow (Inversion of Control). Some programmers are put off because they have to write additional con-figuration files. Often, these files are larger than necessary, because programmers do not use auto-wiringcapacities present in all major DI containers.
Lightweight vs. Heavyweight ContainersDI has given rise to numerous unobtrusive frameworks and lightweight containers. Probably the mostpopular lightweights on the .NET platform are Spring .NET and the Pico/Nano .NET container. Theselightweight containers have certain advantages compared to traditional application server heavyweightcontainers like Microsoft Transaction Server (MTS).
In this case, the choice of names is probably not the best one, because the difference between two typesof containers consists of much more than simple size in bytes. The following list explores the differencesbetween the two in greater detail.
What Is a Component Container?In the simplest form, component containers are capable of hosting components and tak-ing care of their lifetime. From the client point of view, when a component is hostedinside a container, the client does not need to take care of creating and initializing thecomponent, because the container does this for the client. From a hosted componentpoint of view, the component can count on the container to provide it with some infor-mation on component lifetime. The container can notify component of some importantevents in the lifetime of component, such as creation, that can be used by the compo-nent to perform necessary initialization or destruction and can be used by componentto perform the necessary cleanup. In addition, containers can provide a host of addi-tional services to components, such as assembly and dependency resolution, configura-tion, manageability, and so on.
Definition: POCO is an acronym for Plain Ol’ CLR Object. It refers to objects thatare not forced to implement any specific interface or inherit any specific class inorder to make use of services certain frameworks or containers provide.
432
Part V: Refactoring Applied
79796c14.qxd:WroxPro 2/25/08 9:06 AM Page 432
❑ DI support — Both lightweight and heavyweight containers support DI. However, MTS supportsonly the injection of services that are part of MTS and of some simple configuration data throughthe “Object Construction” feature. Lightweight containers can host and inject any .NET object.
❑ POCO programming model — In order to host your component inside MTS, the implementingclass must inherit the System.EnterpriseServices.ServicedComponent class. BecauseVisual Basic (like the majority of .NET languages) supports only single inheritance, this imposesan important limitation to the programming model you need to follow. Also, you won’t be ableto reuse other classes through inheritance when programming your component, nor will otherclasses be able to inherit your class, unless they get tied up with MTS libraries. Lightweight con-tainers impose no such limitation. You can program your classes as simple POCOs, not tyingthem up to a specific hosting environment. Crosscutting services such as transaction support areimplemented in the form of interceptors and can be added declaratively.
❑ Modular architecture — Lightweight containers are generally programmed in modular manner,so you can use only the services you need. This means that you need to reference and distributeonly the libraries you really use. And if you really need to, you can program your own. MTS ismonolithic application and cannot be installed or distributed partially.
❑ Embedding — MTS is an application server that runs in a separate process from your ownapplication. It is registered as a Windows service and has to be installed and administered sepa-rately from your application. This can be an advantage in certain circumstances, for example, inlarge enterprises that use components produced by different software companies. However, thisalso dictates its size and complexity. Lightweight containers can be embedded and distributedas a part of your application. Core libraries are often so small that they can be embedded insidethick clients (as in a client-server model) and distributed over the Internet.
❑ Ease of testing — Thanks to DI, with lightweight containers, implementation of different servicescan be easily replaced with stubs or mocks. This facilitates the testing process. With heavyweightcontainers, this is not possible.
❑ Centralization — Centralization is characteristic of heavyweight application servers. Becausecomponents are hosted in a centralized server, this can facilitate administration, monitoring,configuration, and assembly of applications in one central location. Lightweight containers arenot installed separately and are bundled together with the application.
The differences between the two types of containers are listed in Table 14-1.
Table 14-1: Lightweight vs. Heavyweight Containers
Characteristics Lightweight Containers Heavyweight Containers
DI Yes Very Limited
POCO programming model Yes No
Modular architecture Yes No
Embedding Yes No
Ease of testing Yes No
Centralization No Yes
433
Chapter 14: Refactoring to Patterns
79796c14.qxd:WroxPro 2/25/08 9:06 AM Page 433
Now that you have seen the differences between the two types of containers, it seems that lightweightwins straight out. Does this means that this is the end of the road for heavyweight containers? Well, notlikely. They are still proven technology and very useful for highly scalable, high traffic, large enterprise-level applications that need to support redundancy and high availability.
Up until now, however, you could often witness application servers used even for an application thatbenefited minimally from services they provide and where using heavyweight containers is clearlyoverkill. The advent of lightweight containers should help developers program more productively andshould help place the heavyweight containers in their rightful niche.
Refactoring to DIYou have already seen how code can be refactored to use DI in the example used to discuss DI. You haveseen how the assembler is used to inject dependency into the client instead of having the client find thedependency.
To summarize: instead of instantiating and using a factory to create the component or instead of using aservice locator to locate the service, you invert the control and let someone else inject the dependenciesyour component needs. The following section puts this into practice with the Rent-a-Wheels application.
Refactoring to Patterns and Rent-a-Wheels Application
If you consider the refactorings performed in Chapter 12, you might recall a first attempt at AbstractFactory for data provider objects in the form of AbstractDataObjectsProvider class with concreteMSSQLDataObjectsProvider factory class.
Eliminating Code That Duplicates Functionality Available in .NET Framework
Because the same functionality is provided by the DbProviderFactories and DbProviderFactoryclasses in the .NET Framework System.Data.Common namespace, you can eliminate theAbstractDataObjectsProvider and MSSQLDataObjectsProvider and use classes provided by the .NET Framework instead. The classes aren’t adding any additional value, so the less code youhave to maintain, the better. Because the DbProviderFactory class provides a comprehensive list of methods for data provider object creation, you can decouple the rest of the code Data classes(BranchData, ModelData, and so on) code from the concrete data providers.
For example the BranchData Delete method does not reference SqlCommand type anymore. Take alook at the original code:
Public Overrides Sub Delete(ByVal branch As Branch)Dim command As IDbCommand = New SqlCommandAddParameter(command, IdParamterName, DbType.Int32, _branch.Id)ExecuteNonQuery(command, DeleteBranchSql)
End Sub
434
Part V: Refactoring Applied
79796c14.qxd:WroxPro 2/25/08 9:06 AM Page 434
This code is now refactored to following form:
Public Overrides Sub Delete(ByVal branch As Branch)Dim command As IDbCommand = MyBase.DbProviderFactory.CreateCommandAddParameter(command, IdParamterName, DbType.Int32, _branch.Id)ExecuteNonQuery(command, DeleteBranchSql)
End Sub
Injecting Data Classes to GUI Classes via Dependency Injection
In order to provide less coupling between the GUI and Data layers and make the RentAWheel.Dataassembly nondependent on Rent-a-Wheels configuration specifics, you can do the following:
❑ Inject GUI classes with Data implementations, instead of having GUI classes creating instancesof Data.
❑ Inject Data classes with connection string and a concrete DbProviderFactory. This will decoupleData classes from the specific Rent-a-Wheels implementation and make especially AbstractDatahighly reusable.
Instead of incorporating third-party assemblers, you implement your own coded assembler. ApplicationStartup even provides a convenient trigger for DI, so you can code your assembler in the form of aMyApplication class. The code for the MyApplication class is available in Listing 14-17.
Listing 14-17: MyApplication Class as DI Assembler
Option Strict OnOption Explicit On
Imports System.DataImports System.Data.CommonImports System.Configuration
Imports RentAWheel.Data.Implementation
Imports Microsoft.VisualBasic.ApplicationServices
Namespace My
‘ The following events are available for MyApplication:‘...Partial Friend Class MyApplication
Private connectionStringSettings As ConnectionStringSettingsPrivate Const DbStringConfigurationSectionName As String = “RentAWheels”
Private branchData As BranchDataPrivate vehicleData As VehicleDataPrivate modelData As ModelData
Continued
435
Chapter 14: Refactoring to Patterns
79796c14.qxd:WroxPro 2/25/08 9:06 AM Page 435
Listing 14-17: MyApplication Class as DI Assembler (continued)
Private categoryData As VehicleCategoryData
Private Sub MyApplication_Startup(ByVal sender As Object, _ByVal e As StartupEventArgs) Handles Me.Startup
CreateDatas()ReadConnectionSettings()AssignConnectionStringSettingsToDatas()SetUpBranchMaintenanceForm()SetUpChangeBranchForm()SetupFleetMaintenanceForm()SetUpFleetViewForm()SetupModelMaintenanceForm()SetUpRentVehicleForm()SetUpVehicleCategoriesMaintenanceForm()SetUpVehicleReceptionForm()
End SubPrivate Sub AssignConnectionStringSettingsToDatas()
branchData.ConnectionStringSettings = Me.connectionStringSettingsvehicleData.ConnectionStringSettings = Me.connectionStringSettingsmodelData.ConnectionStringSettings = Me.connectionStringSettingscategoryData.ConnectionStringSettings = Me.connectionStringSettings
End SubPrivate Sub CreateDatas()
branchData = New BranchDatavehicleData = New VehicleDatamodelData = New ModelDatacategoryData = New VehicleCategoryData
End SubPrivate Sub ReadConnectionSettings()
Dim config As Configuration = _ConfigurationManager.OpenExeConfiguration( _ConfigurationUserLevel.None)Dim connectionStringsSection As ConnectionStringsSection = _config.ConnectionStringsconnectionStringSettings = _connectionStringsSection.ConnectionStrings( _DbStringConfigurationSectionName)
End SubPrivate Sub SetupModelMaintenanceForm()
ModelMaintenance.Helper.ModelData = modelDataModelMaintenance.Helper.CategoryData = categoryData
End SubPrivate Sub SetUpFleetViewForm()
FleetView.BranchData = branchDataFleetView.CategoryData = categoryDataFleetView.VehicleData = vehicleData
End SubPrivate Sub SetupFleetMaintenanceForm()
FleetMaintenance.Helper.BranchData = branchDataFleetMaintenance.Helper.ModelData = modelData
436
Part V: Refactoring Applied
79796c14.qxd:WroxPro 2/25/08 9:06 AM Page 436
Listing 14-17: MyApplication Class as DI Assembler (continued)
FleetMaintenance.Helper.VehicleData = vehicleDataEnd SubPrivate Sub SetUpChangeBranchForm()
ChangeBranch.BranchData = branchDataChangeBranch.VehicleData = vehicleData
End SubPrivate Sub SetUpBranchMaintenanceForm()
BranchMaintenance.Helper.BranchData = branchDataEnd SubPrivate Sub SetUpVehicleReceptionForm()
VehicleReception.VehicleData = vehicleDataEnd SubPrivate Sub SetUpVehicleCategoriesMaintenanceForm()
VehicleCategoriesMaintenance.Helper.VehicleCategoryData = _categoryData
End SubPrivate Sub SetUpRentVehicleForm()
RentVehicle.VehicleData = vehicleDataEnd Sub
End Class
End Namespace
This is not yet all I have to say about design patterns and Rent-a-Wheels. It seems that I have incorpo-rated certain design patterns a lot before this chapter.
CRUD Persistence PatternIf you take a look at different Data classes, you will see that they all inherit AbstractData and imple-ment certain abstract methods that AbstractData declares:
Public MustOverride Function GetAll() As IList(Of PersistedObject)
Public MustOverride Sub Delete(ByVal persisted As PersistedObject)
Public MustOverride Sub Update(ByVal persisted As PersistedObject)
Public MustOverride Sub Insert(ByVal persisted As PersistedObject)
You can see that for each domain object, the data implements a few persistence methods. This pattern isactually not so new. It is often referred to by the acronym CRUD, meaning Create, Retrieve, Update, andDelete. The pattern is popular today and often goes in similar form and under different names such asDAO for Data Access Object and others. Mind you, this implementation here is very basic. It doesn’thave methods for parameterized querying, not to mention more advanced problems like concurrency,stale data, transactions, lazy loading, and so on. Object persistence is a very complex field and is dis-cussed in the next chapter.
437
Chapter 14: Refactoring to Patterns
79796c14.qxd:WroxPro 2/25/08 9:06 AM Page 437
SummarySoftware design patterns provide a wealth of programming wisdom and ingenuity. Learning patterns willgreatly hone your designer skills but will not be enough for real-life pattern application. Because the onlypractical way to approach design is evolutionary, you will have to learn techniques on how to refactor yourcode so it incorporates solutions described by design patterns.
This chapter gave you a first taste of design patterns and refactoring to patterns process. When refactoringto patterns, you are not guided only by the immediate goal of eliminating undesired qualities from yourcode or smells; you are led forward by a clear vision on desired design that you wish to implement.
You have seen how the problem of creation of related objects can be resolved by an Abstract Factorypattern, decoupling the client from the knowledge of the concrete family it will use. You have seen howtypical creational code can be refactored along the lines of the Abstract Factory pattern.
This chapter also discussed Dependency Injection pattern, a design pattern that is increasingly influentialand that is changing the way typical applications are programmed and assembled. You have seen that DI can be applied in very simple form, or can be implemented with the help of DI-based containers. Youhave seen the benefit of the pattern and how containers based on DI compare to traditional heavyweightcontainers and application servers.
I hope that after this chapter you can appreciate the importance and benefits that design patterns canbring to your development process. I hope you will be inspired to learn more about patterns and todevise ways you can refactor your code along solutions proposed by certain patterns.
The next chapter takes a look at what the future, albeit the very near future, has for you. LINQ and othernew features that ship with Visual Studio 2008 are discussed.
438
Part V: Refactoring Applied
79796c14.qxd:WroxPro 2/25/08 9:06 AM Page 438
LINQ and Other VB 2008 Enhancements
Visual Studio 2008 brings probably the most important version of Visual Basic since the first versionof Visual Basic .NET. This is not because of the novelties in the integrated development environment(IDE) itself; more important are the new features that ship with version 3.5 .NET Framework andespecially interesting for those involved in refactoring, Visual Basic version 9.0.
This chapter discusses the following features:
❑ LINQ technology brings additional power to working with data in Visual Basic. Differentdata sources can now be queried in a uniform and language-native manner. LINQ pavesthe way for implementation of different technologies like object-relational mapping frame-works and others.
❑ In the 2008 version, Visual Basic is additionally equipped for working with XML. Featuressuch as XML axis properties and XML literals with embedded expressions make VisualBasic one of the best environments for working with XML.
Type Inference for Local VariablesChapter 5 discussed the VB type system in some detail. You have seen how in a statically typed lan-guage a type of variable is declared explicitly by the programmer as a part of variable declaration.You have also seen how you can defer type resolution until runtime when Option Strict is deacti-vated and variable declaration does not contain the “As” part. Finally, you have seen how you caninfer variable type based on the initial value assigned to a variable. This way, you can refactor per-missive code in order to enforce variable type declaration and explicit type conversion.
In Visual Basic 2008, Microsoft has equipped the compiler with type inference for local variables.This means that you do not have to declare a local variable explicitly; the variable will still be stati-cally typed. Take a look at Figure 15-1, for example. You can see how the IDE correctly recognizesvariable name as string.
79796c15.qxd:WroxPro 2/25/08 9:08 AM Page 439
Figure 15-1
By omitting the “As” part of variable declaration, you can write more concise code and save yourself afew keystrokes. Nevertheless, if Option Infer is activated, the compiler will correctly infer variabletype based on the initial value, and you will be able to take advantage of all the benefits that VisualStudio IntelliSense and Visual Basic compiler can provide. Your code will be statically checked and anautocomplete and autolist members feature will be available.
Remember that you can apply type inference for local variables only. These are the variables declaredinside the method or a property. If you try to infer type for class members with Option Explicitand Option Strict deactivated, like function parameters or return values or property types, thesetypes will not be inferred. They will be late-bound instead.
While local variable type inference is a nice feature on its own, it was actually a necessary addition to the Visual Basic 2008 in order for it to support Language Integrated Query (LINQ). LINQ is probably themost prominent feature of Visual Basic 2008, and is discussed in greater detail later on in this chapter.
XML Productivity EnhancementsWhen you work with XML, you often need to construct an XML document or document fragment pro-grammatically. You can use the DOM API for this purpose, but because of its verbose nature, you willhave to write a lot of boilerplate code. This can be quite tedious.
Your other option is to implement this by writing XML literals that you use to initialize DOM objects.The problem with this approach is that you have to construct XML by concatenating strings. You don’tget any support from Visual Studio for this task, and if you need to edit the XML in some XML-awareeditor, you have to clean up all string concatenation characters and vice versa. Actually, this was the situ-ation until Visual Basic 2008 was released. In Visual Basic 2008 you can embed your XML directly insideyour Visual Basic code.
XML LiteralsWhen using XML literals, you can treat XML inside your Visual Basic code in the same form as if youwere dealing with a standalone XML document. What’s more, Visual Studio will provide typical produc-tivity enhancements such as syntax coloring and syntax checking, helping you to edit your XML effi-ciently inside your Visual Basic editor. Take a look at the embedded XML in Figure 15-2.
You probably noticed that the variable customer in Figure 15-2 is declared as an XElement. XElementbelongs to a new set of classes that Microsoft has provided for working with XML and that ship with the.NET Framework 3.5. They belong to a System.Xml.Linq namespace, and they are similar to the docu-ment object model (DOM) classes.
440
Part V: Refactoring Applied
79796c15.qxd:WroxPro 2/25/08 9:08 AM Page 440
Figure 15-2
With embedded XML you can use a template mechanism to populate a document with data. The syntaxfor templating XML is similar to Active Server Pages syntax. You use <%= as opening tag for VB codeinside XML and %> as closing tag. Take a look at the item element in the following XML fragment:
Dim item As Item = Purchase.GetItem(0)
Dim customer As XElement = <customer name=”John Doe” id=”1323223”><history>
<purchase date=”23.03.2007”><item id=<%= item.Id %>/>
</purchase></history>
</customer>
You can see how the item element id attribute value is obtained from common VB item variable. Withall these XML literal capabilities, there is no need to construct XML through string concatenation. In caseyou have some XML constructed by string concatenation, you will be better off replacing such code withan XML literal. This leads to the first refactoring in this chapter.
Smell: XML String LiteralsDetecting the SmellUse visual inspection to detect this smell. XML strings are easily spotted because of anXML distinctive syntax.
Related RefactoringUse Replace XML String Literals with XML Literal refactoring to eliminate this smell.
RationaleIn VB 2008 XML can be embedded inside your Visual Basic code in the form of XML literals. This makes using simple string literals to represent XML obsolete,tedious, and error prone. When using strings instead of XML literals, you will not getIntelliSense support for XML or syntax checking. XML literals can be easily extractedto a separate file or copied to an external editor. This is not straightforward whenusing strings; you will first have to get rid of all double quotes and string concatena-tion symbols.
441
Chapter 15: LINQ and Other VB 2008 Enhancements
79796c15.qxd:WroxPro 2/25/08 9:08 AM Page 441
Refactoring: Replace XML String Literals with XML LiteralMotivationXML literals let you effectively mix VB code with XML. When you use XML literals,you do not have to concatenate line breaks or enclose XML segments inside doublequotes. This means that you can paste XML inside your VB code without having tomodify it, or you can copy XML from your VB code and paste it directly inside XMLeditor, as is. Also, when using XML literals you can count on full IntelliSense support.This is a superior way of writing XML inside your code compared to simple XMLstring concatenation. In order to take advantage of full XML support that the IDE pro-vides, and in order to be able to write clean XML inside your code, replace XML stringliterals with XML literals.
Related SmellsUse this refactoring to eliminate the XML String Literals smell.
MechanicsIn order to perform this refactoring efficiently, you should understand the rough correspondence between classes in System.Xml and the new XML API classes inSystem.Xml.Linq namespace. You will see that for some classes the name in the two namespaces differs only by prefix. In System.Xml, names begin with Xml, for example, XmlDocument, and in the System.Xml.Linq names begin with X, forexample, XDocument. Take a look at Table 15-1 for a list of corresponding classes inthe two namespaces.
The refactoring is performed in a few sequential steps.
1. Start by replacing XmlDocument, XmlNode, and other DOM objects fromthe System.Xml namespace with objects from the System.Xml.Linqnamespace. Replace XmlDocument with XDocument and XmlNode withXElement. Use the shared Parse method of XDocument to loadXDocument with an XML string representation in place of the LoadXmlmethod of XmlDocument.
Dim document As XDocument = New XDocumentdocument = XDocument.Parse( _“<customer name=”“John Smith”“ id=”“335434”“/>”)
2. Now remove all string delimiters, double quote escape, and concatenationsymbols and turn the string into an XML literal. Remove the call to theXDocument.Parse shared method and initialize XDocument directly fromthe XML literal.
3. Add XML document declaration at the beginning of XML literal, for example:<?xml version=”1.0”?>.
BeforeDim document As XmlDocument = New XmlDocumentdocument.LoadXml(“<customer name=”“John Smith”“ id=”“335434”“/>”)
442
Part V: Refactoring Applied
79796c15.qxd:WroxPro 2/25/08 9:08 AM Page 442
Table 15-1: Correspondence between System.Xml and System.Xml.Linq Namespace
Replacing XML Strings with XML LiteralsIn order to demonstrate how you can replace XML strings with XML literals I will need a suitable example.In Chapter 2 you will find one such example in the Calories Calculator application. If you take a look atListing 2-11 you will see how XML strings are used to inflate DOM objects, and in Listing 2-12 you will seethe final version of the PatientHistoryXMLStorage class.
You can apply Replace an XML strings with XML Literals refactoring on the CreateXmlDocumentFirstTimemethod. Remember, this is a bit different from its final form at the end of Chapter 2 (Listing 2-11). You cantake a look at the resulting method, once XML strings are replaced with XML literals, in Listing 15-1.
Listing 15-1: CreateXmlDocumentFirstTime Method after XML Strings Are Replacedwith XML Literals
Private Sub CreateXmlDocumentFirstTime()documentValue = _<?xml version=”1.0”?><patientsHistory>
<patient ssn=<%= patientValue.SSN %>firstName=<%= patientValue.FirstName %>lastName=<%= patientValue.LastName %>><measurement date=<%= DateTime.Today %>>
<height><%= patientValue.HeightInInches.ToString %></height><weight><%= patientValue.WeightInPounds.ToString %></weight><age><%= patientValue.Age.ToString %></age><dailyCaloriesRecommended>
Continued
System.Xml Class System.Xml.Linq Class
XmlDocument XDocument
XmlElement XElement
XmlAttribute XAttribute
XmlNode XNode
XmlText XText
XmlComment XComment
AfterDim document As XDocumentdocument = <?xml version=”1.0”?>
<customer name=”John Smith” id=”335434”/>
443
Chapter 15: LINQ and Other VB 2008 Enhancements
79796c15.qxd:WroxPro 2/25/08 9:08 AM Page 443
Listing 15-1: CreateXmlDocumentFirstTime Method after XML Strings Are Replacedwith XML Literals (continued)
<%= patientValue.DailyCaloriesRecommended.ToString() %></dailyCaloriesRecommended><idealBodyWeight>
<%= patientValue.IdealBodyWeight.ToString() %></idealBodyWeight><distanceFromIdealWeight>
<%= patientValue.DistanceFromIdealWeight.ToString() %></distanceFromIdealWeight>
</measurement></patient>
</patientsHistory>End Sub
You can see how the XML declaration is added at the beginning of the XML literal. Without the XMLdocument declaration line, the compiler would infer the literal as an XElement, not an XDocument. Youcan also see how an ASP-like template syntax is used to embed values inside the XML. You’ll come backto the Calories Calculator example later on in this chapter, but now it’s time to take a look at anotherimportant novelty feature in Visual Basic 2008.
As an interesting side note, XML literals are not part of the C# 3.0 version published with VisualStudio 2008. This means that Visual Basic has a certain advantage over C# when you’re working withembedded XML. This advantage comes with the price of additional complexity in language syntax.
Navigating XML with XML Axis PropertiesBecause XML objects are now first-class Visual Basic citizens, you can use XML syntax to navigate theXML tree. You can query XML documents using a very compact flavor of LINQ syntax based on XMLand in a way similar to XPath. LINQ is discussed later on in this chapter, but in case you are wonderingwhat this syntax looks like, here’s an example. Take a look at Listing 15-1. Imagine now you want tonavigate the XML and obtain the Social Security number of the first patient in the patient history file.The statement will look like this:
Dim ssn = documentValue.<patientsHistory>(0).<patient>(0).@ssn
As you can see, it is similar to a property syntax in Visual Basic, where child elements in XML are refer-enced by a period. You can use the index syntax (0) to obtain a child element at a specific position, andfinally you can use the “At” sign (@) to reference a certain attribute of an element. Such code is muchmore compact than the same code written using DOM or System.Xml.Linq objects.
Extract XML Literal to Resource in Refactor!In some circumstances, it is more convenient to have an XML document declaration in a separate fileand not embedded inside your Visual Basic source. If your XML is embedded inside your Visual Basiccode, the only way to change the XML is to change the source and to recompile the project. And if youneed to pass your XML to somebody who is not a Visual Basic programmer, you will probably preferpassing a simple XML file to passing Visual Basic source code.
In case your literal contains some Visual Basic embedded expression, through <%= ... %> tag syntax,Refactor! will not display the Extract XML Literal to Resource option in the Refactor! menu, because such
444
Part V: Refactoring Applied
79796c15.qxd:WroxPro 2/25/08 9:08 AM Page 444
a literal cannot be extracted automatically. If you have a lot of XML and only a few embedded values, it stillmight be worth extracting such literal. In that case, remove all embedded expressions from XML and usestandard VB code to assign values in continuation. Once you have cleansed your XML literal from embed-ded expressions, you can use Refactor! to extract such a literal to a resource file.
The Extract XML Literal to Resource refactoring is invoked by the pressing the right mouse button any-where over the XML literal. Take a look at Figure 15-3 to see the option selected in the IDE.
Figure 15-3
Once you have performed the refactoring, the resulting code will read XML from the resource and loadthe XDocument or XElement object with the extracted XML. The resulting code looks like this:
Dim customer As XElement = XElement.Parse(My.Resources.XMLFile)
If you take a look at the Resources.resx file, you will find the entry containing extracted XML.
Querying the Objects with LINQLINQ stands for Language Integrated Query. LINQ adds native querying capabilities to Visual Basicthrough syntax similar to SQL. With LINQ, you can enumerate, filter, and create projections on a numberof collection types, arrays, XML objects, database sources, and other objects.
Take a look at some of the benefits that LINQ brings to the VB programming language:
❑ Querying syntax is now native to Visual Basic. This effectively means that a number of new key-words are added to the language. Instead of complex Visual Basic code often based on iteratingobjects, now you can write a single query statement to locate the object that you’re searching for.
❑ No matter the object of your query, the query syntax is always the same. You can query a collection or an array, an XML document or a SQL database, and the code you write willalways be the same. This approach is inspired by the Microsoft vision of unified data accessand should simplify the way Visual Basic programmers view data.
❑ Now that the LINQ is an integral part of the Visual Basic language, you can count on thesame support of the IDE as your normal Visual Basic code has, with features such as syntaxchecking and IntelliSense working with LINQ queries as well. Remember writing embeddedSQL statements inside Visual Basic code as strings and copying them over to SQL debugger inorder to test them? No more of that. This should significantly boost your productivity.
❑ LINQ paves the way for implementing an object-relational mapping layer in .NET in a mannerthat is tightly coupled with the language itself.
Before going any further, take a look at a simple LINQ query example, so you have a better idea of howit works. Listing 15-2 shows LINQ in action.
445
Chapter 15: LINQ and Other VB 2008 Enhancements
79796c15.qxd:WroxPro 2/25/08 9:08 AM Page 445
Listing 15-2: Basic LINQ Query Example
Option Explicit OnOption Strict OnOption Infer On
Imports System.Collections.Generic
Public Class AuthorPublic FirstName As StringPublic LastName As StringPublic Books As Book()
End Class
Public Class BookPublic Name As String
End Class
Module LinqExample
Sub Main()Dim authors As Author() = { _
New Author With { _.FirstName = “Leo”, .LastName = “Tolstoy”, _.Books = New Book() { _
New Book With {.Name = “War and Peace”}, _New Book With {.Name = “Anna Karenina”}, _New Book With {.Name = “Resurrection”} _
} _}, _
New Author With { _.FirstName = “Homer”, .LastName = “”, _.Books = New Book() { _
New Book With {.Name = “The Iliad”}, _New Book With {.Name = “Odyssey”} _
} _}, _
New Author With { _.FirstName = “Miguel”, .LastName = “Cervantes”, _.Books = New Book() { _
New Book With {.Name = “Don Quixote”} _} _
} _}
Dim authorWithMostBooks = (From author In authors _Order By author.Books.Count Descending _Select author).First
Console.WriteLine(“Author with most books: “ + _authorWithMostBooks.FirstName + _“ “ + _authorWithMostBooks.LastName)
Console.ReadKey()End Sub
End Module446
Part V: Refactoring Applied
79796c15.qxd:WroxPro 2/25/08 9:08 AM Page 446
In this example you have the two classes: Author and Book. Each Author has an array of Book. In sub-routine Main I first created and initialized an array of authors, so I could have some data to query upon.I used the new array initializer syntax to create a ready-to-use array of Author objects. Finally, I queriedthe array by first ordering descending authors by the number of books and second selecting the firstauthor from the ordered list by the means of the operator First. This way I’m able to find the authorwith the most books among authors in the array.
Writing this query with LINQ was rather simple. Accomplishing the same in standard Visual Basic with-out using the LINQ means iterating over elements in the array and then finding the one with the biggestbook count. There are a number of implementation details that would have to be resolved. It would bedefinitely more verbose than the simple LINQ query. As queries become more complex, the power ofLINQ becomes more evident.
LINQ is a world on its own; there are a number of aggregate, groping, restriction, partitioning, and otheroperators that add to the expressiveness of LINQ language. Still, LINQ can preserve the feel of SQL sothat you will be able to write your first LINQ queries fairly quickly. Start by writing simple queries andwork your way up to more advanced LINQ features and soon you will appreciate the power of LINQdata query capabilities.
Now that you have a fairly good understanding of various features that the new version of Visual Basic brings, such as LINQ and XML literals, you can take a look at an example that brings all these features together.
Refactoring: Replace Complex VB Queries with LINQMotivationLINQ adds powerful data querying capabilities to the Visual Basic language. Theexpressiveness of LINQ means that complex Visual Basic query code can be replacedwith compact, easy-to-understand LINQ syntax.
Related SmellsUse this refactoring to replace complex query code with the more expressive LINQ syntax.
MechanicsIn case the query code is long and complex and inside a very long method, you shouldstart by extracting query code to a separate method. This way, you separate query codecompletely from the rest of the code in the method, and you can crystallize the inten-tion behind the query. It is important to project the desired result of the query, becauseexpressing it through LINQ will be probably easier than understanding the underpin-nings of Visual Basic code used for the same purpose.
BeforeDim authorWithMostBooks As Author = Nothing
For Each author In authorsIf authorWithMostBooks Is Nothing OrElse _author.Books.Count > authorWithMostBooks.Books.Count
Descending _Select author).First
447
Chapter 15: LINQ and Other VB 2008 Enhancements
79796c15.qxd:WroxPro 2/25/08 9:08 AM Page 447
Old Example in New RobesThe Calories Calculator example from Chapter 2, written in VB 2005, has all the elements of Visual Basicsyntax that can be upgraded by the refactorings discussed in this chapter.
XML Literals in Calories CalculatorPreviously in this chapter, in Listing 15-1, you could already witness how XML strings are replaced withXML literals and embedded expressions. Because I needed to use parts of the same XML structure onmore than one occasion, I have performed method extraction on CreateXmlDocumentFirstTime andobtained two new methods: CreatePatientElement and CreateMeasurementElement. This helpedeliminate two methods: AddNewPatient and SetMeasurementValues, because new methods were per-forming the same function.
Querying Patient History with LINQIn FindPatientNode a nested loop is used to iterate over elements in a document and then over attrib-utes of an element. Take another look at the method:
Private Function FindPatientNode() As XmlNodeDim patientNode As XmlNode = NothingFor Each node As XmlNode In _ documentValue.FirstChild.ChildNodes
For Each attrib As XmlAttribute In node.Attributes‘We will use SSN to uniquely identify patientIf (attrib.Name = “ssn” And _attrib.Value = patientValue.SSN) Then
patientNode = nodeEnd If
NextNextReturn patientNode
End Function
Now, this is not that easy to understand. You have to know the underlying XML very well to understandan expression like this: documentValue.FirstChild.ChildNodes. This code is a good candidate forreplacement with a LINQ query. Code written in LINQ syntax looks like this:
Private Function FindPatientElement() As XElementDim resultingPatients = _
From patients In documentValue...<patient> _Select patients Where patients.@ssn = patientValue.SSN
Return resultingPatients(0)End Function
Not only is the method now much more compact, but it also reads almost naturally. The method alsodemonstrates the combination of LINQ and XML axis properties syntax.
In case you’re curious to see what the PatientHistoryXMLStorage class finally ended up looking like,and so you could understand better the refactorings performed, you can find the code for the entire classin Listing 15-3.
448
Part V: Refactoring Applied
79796c15.qxd:WroxPro 2/25/08 9:08 AM Page 448
Listing 15-3: PatientHistoryXMLStorage Refactored to LINQ and XML Literals
Option Infer OnOption Strict OnOption Explicit On
Imports System.XmlImports System.IOImports System.Xml.LinqImports System.Linq
Public Class PatientHistoryXMLStoragePrivate Const HistoryXmlFileName As String = “patientHistory.xml”Private Const ExeFileName As String = “CaloriesCalculator.exe”
Private patientValue As PatientPrivate documentValue As XDocumentPublic Shared patientHistoryXmlFile As String = _CalculateXmlHistoryFilePathNextToExe()
Private Shared Function CalculateXmlHistoryFilePathNextToExe() _As String
Return System.Reflection.Assembly. _GetExecutingAssembly.Location.Replace( _ExeFileName, HistoryXmlFileName)
End Function
Private Sub LoadPatientHistoryFile()documentValue = XDocument.Load(patientHistoryXmlFile)
End Sub
Private Sub CreateXmlDocumentFirstTime()documentValue = <?xml version=”1.0”?>
<patientsHistory></patientsHistory>
documentValue.Root.Add(CreatePatientElement)Dim patient = documentValue...<patient>(0)patient.Add(CreateMeasurementElement())
End Sub
Private Function CreatePatientElement() As XElementDim patient = <patient ssn=<%= patientValue.SSN %>
firstName=<%= patientValue.FirstName %>lastName=<%= patientValue.LastName %>>
</patient>Return patient
End Function
Private Function CreateMeasurementElement() As XElementDim measurement = <measurement date=<%= DateTime.Today %>>
<height><%= patientValue.HeightInInches.ToString %>
</height>Continued
449
Chapter 15: LINQ and Other VB 2008 Enhancements
79796c15.qxd:WroxPro 2/25/08 9:08 AM Page 449
Listing 15-3: PatientHistoryXMLStorage Refactored to LINQ and XML Literals (continued)
<weight><%= patientValue.WeightInPounds.ToString %>
</weight><age>
<%= patientValue.Age.ToString %></age><dailyCaloriesRecommended>
<%= patientValue.DailyCaloriesRecommended.ToString %></dailyCaloriesRecommended><idealBodyWeight>
<%= patientValue.IdealBodyWeight.ToString %></idealBodyWeight><distanceFromIdealWeight>
<%= patientValue.DistanceFromIdealWeight.ToString %></distanceFromIdealWeight>
</measurement>Return measurement
End Function
Private Function FindPatientElement() As XElementDim resultingPatients = _
From patients In documentValue...<patient> _Select patients Where patients.@ssn = patientValue.SSN
Return resultingPatients(0)End Function
Private Sub AddNewPatient()Dim patient = CreatePatientElement()patient.Add(CreateMeasurementElement())Me.documentValue.Root.Add(patient)
End Sub
Private Function FileCreated() As BooleanDim created As Boolean = TrueTry
LoadPatientHistoryFile()Catch noFile As IO.FileNotFoundException
created = FalseEnd TryReturn created
End Function
Public Sub Save(ByVal patient As Patient)patientValue = patientIf Not FileCreated() Then
CreateXmlDocumentFirstTime()Else
Dim patientElement As XElement = FindPatientElement()If patientElement Is Nothing Then
AddNewPatient()Else
patientElement.Add(CreateMeasurementElement())End If
450
Part V: Refactoring Applied
79796c15.qxd:WroxPro 2/25/08 9:08 AM Page 450
Listing 15-3: PatientHistoryXMLStorage Refactored to LINQ and XML Literals (continued)
End IfdocumentValue.Save(patientHistoryXmlFile)
End SubEnd Class
Now you can take a look at how LINQ can be applied in yet another area. LINQ is revolutionizing theway VB programmers interact with databases.
Object-Relational Mapping with LINQ to SQLLINQ to SQL is Microsoft’s first attempt at object-relational mapping (ORM). ORM frameworks resolveproblematic relational persistence for your Plain Ol’ CLR Objects (POCOs) and are aimed at minimizingor eliminating effects of object-relational impedance mismatch. (Object-relational impedance mismatchis discussed in Chapter 11).
Simply put, when using an ORM framework, all you need to do is to declare, in a file or through othertypes of metadata like .NET attributes, the way your classes are mapped to tables in the database, andyou let ORM do its magic, just as depicted in Figure 15-4. But how do ORM frameworks resolve the mismatch?
❑ ORM frameworks are capable of generating SQL statements themselves, thus eliminating theneed for programming the SQL code manually.
❑ ORM frameworks are capable of generating data store schemas, relieving programmers fromthe task of designing and creating data store structures.
❑ ORM frameworks know how to interact with a data store and how to map results of queries toPOCOs and back, removing any data store–related code from the view of the programmer.
❑ Some ORM frameworks know how to generate .NET classes based on the data store schema.This feature can be useful as a staring point when creating applications that use already existingdata stores.
❑ Because mappings are generally declared (not programmed) in mapping files or applied in theform of mapping attributes, objects are isolated from structural changes in the data store. Whenthe data store structure changes, it is often enough to modify the mapping and not the code ofthe mapped class in order to keep the application working.
❑ Because ORM frameworks know how to generate SQL for each specific data store they support,changing from one data store product to another can be as easy as changing the ORM configura-tion data. This way, your code is not married to a specific data store product, thus eliminatingvendor lock-in.
Figure 15-4
Customer+Id : Integer+FirstName : String+LastName : String+Email : String
ORM FirstNameLastNameEmail
CustomerPK Id
451
Chapter 15: LINQ and Other VB 2008 Enhancements
79796c15.qxd:WroxPro 2/25/08 9:08 AM Page 451
ORM frameworks are not without their problems. These are first and foremost related to performanceissues, as many ORM skeptics will tell you. In my experience, as is generally the case when performanceissues are concerned, these problems are overrated. In order to get the maximum out of your ORMframework, you might need to tweak the mappings, generate a data store schema, and experiment withlazy loading and cache strategies. For some expensive queries that need to retrieve and work upon hugequantities of data, you’d be better off if you write your own SQL and execute it directly against the data-base because most ORM frameworks provide a back door for direct communication with your database.
LINQ to SQL at a GlanceWith LINQ, Microsoft has greatly improved traditional object-relational mapping. LINQ integrates dataquery and makes it an integral part of VB .NET (and C#) programming language. Querying a database isnow achieved through the same syntax as when querying arrays or other “queryable” objects. The syn-tax is part of the .NET programming language, so there is no need to learn SQL or a SQL dialect specificto a certain database in order to communicate with any database supported by LINQ to SQL. (Currently,only Microsoft SQL server can be used with a LINQ-to-SQL database, but other LINQ-to-SQL providerimplementations will certainly be incoming in the near future). This also means that LINQ queries cancount on the full support of a compiler and IDE-like syntax checking, coloring, and IntelliSense.
DataContext ClassThe base class in LINQ to SQL is the DataContext found in System.Data.Linq namespace. TheDataContext instance functions use an underlying Connection object. You can instantiate DataContextby supplying a connection string or IDbConnection instance to a DataContext constructor. In additionto methods typical of any connection object such as ExecuteQuery or ExecuteCommand, DataContextboasts CreateDatabase and DeleteDatabase methods that can be used for database schema creationand disposal.
The DataContext class contains a crucial SubmitChanges method that commits all changes made onentity classes to the database, synchronizing the two worlds.
Table ClassBy calling a generic version of the GetTable method of DataContext, you obtain an instance of theSystem.Data.Linq.Table class, a central class for querying the database. This class is generic, andwhen using it, you should parameterize it with the entity class you wish to map to the database. TheTable class implements a System.Linq.IQueryable interface, meaning that you can write LINQqueries against this class.
System.Data.Linq.Mapping Namespace Attribute ClassesThe world of relational data is represented through the DataContext and Table classes. The world of .NETentities is represented by domain classes. Now you need some way to relate the two worlds. You need to tell
Definition: The class that is being mapped to a table or tables in a database isreferred to by the term entity class.
452
Part V: Refactoring Applied
79796c15.qxd:WroxPro 2/25/08 9:08 AM Page 452
the LINQ-to-SQL framework which property of which class maps to which column of which table in thedatabase. The mapping information can be passed through to LINQ to SQL by the following means:
❑ Mapping configuration file — These files have a dbml extension and are written in XML for-mat. Here is the sample excerpt:
<Database Name=”northwnd” Class=”Northwnd”xmlns=”http://schemas.microsoft.com/dsltools/DLinqML”>
<Table Name=”Customers”><Type Name=”Customer”>
<Column Name=”CustomerID” Type=”System.String”DbType=”NChar(5) NOT NULL” IsPrimaryKey=”True” CanBeNull=”False” />
❑ Attribute based mapping — This style of mapping is achieved by applying attribute metadataover entity classes. For example:
<Table(Name:=”Customers”)> _Public Class Customer
Private lastNameValue As String‘ ...<Column(Name:=”LastName”, DBType:=”NVarChar(20) NOT NULL”, _
CanBeNull:=False), IsPrimaryKey:=False> _Public Property LastName() As String
GetReturn Me.lastNameValue
End GetSet
Me.lastNameValue = valueEnd Set
End PropertyEnd Class
Using attributes means that you will have less configuration files to worry about and might sim-plify your development. On the other hand, using attributes means that any change in mappingresults in recompilation of your application.
Continued
Refactoring: Replace Programmatic Data Layer with LINQ to SQLMotivationLINQ to SQL abstracts you from programming in database-specific language and makesyour code database neutral. You can keep your objects POCOs, without loading themwith persistence-specific code. You can switch the underlying database with minimumeffort and often can change the way entity classes are mapped to database schema with-out any intervention in VB code.
Related SmellsUse this refactoring to simplify the persistence layer.
453
Chapter 15: LINQ and Other VB 2008 Enhancements
79796c15.qxd:WroxPro 2/25/08 9:08 AM Page 453
LINQ and the Rent-a-Wheels ApplicationI have managed to separate the data layer in the Rent-a-Wheels application and abstract the rest of theapplication from relational persistence code. However, current implementation is far from being onthe industrial level. The truth is that implementing a successful object-relational layer is not a simplefeat. While the data layer will perform satisfactorily in a single-user environment, this would not bethe case if the assembly were placed server-side.
If placed in a highly demanding server environment with lots of concurrent access, concurrency-relatedproblems would certainly start to show up. Here are some problems the current version of the data per-sistence layer fails to address:
❑ Object identity — There is no relationship between object and database identity. At any givenmoment you can have a number of different objects in application memory that have the samedatabase ID. This means that if one object has been changed in one place in memory, other objectswith the same database ID somewhere else in the memory will not be affected, ending up hold-ing obsolete data.
❑ Phantom updates and optimistic locking — I have adopted an optimistic locking schema in theapplication, but there is no verification that the underlying data was not changed (by some otheruser, for example) from the moment it was read when committing updates.
❑ Rudimentary query capabilities — Data classes provide a few query methods that are notcapable of expressing more complex query logic. As a consequence, each time a new query isnecessary, the only solution is to implement a new ad hoc SQL statement.
❑ Performance related issues: lazy loading, caching, and so on — These are some of the typicalperformance features that quality ORM frameworks provide. The data layer in the Rent-a-Wheels application does not address any of these issues.
The next part of this section shows you new data persistence layer implementation based on LINQ toSQL. However, you will not eliminate the existing layer. Because LINQ to SQL does not support anydatabase other than Microsoft SQL, you still might have a use for it.
Extracting Persistence Layer Interfaces and Restructuring Class Hierarchy
Start by extracting interfaces for BranchData, ModelData, VehicleData, and VehicleCategoryData.First, move extracted interfaces to a separate file and then to a new assembly named RentAWheels.Data.Base.
MechanicsIf you already have a well-defined, strongly typed data access layer, this refactoringshould be fairly simple to perform. Start by extracting interfaces for the data accesslayer classes and make your new LINQ-to-SQL data access classes implement theseinterfaces. Once you have the new layer ready, inject the new LINQ-to-SQL data accessclasses instead of the traditional data access classes that communicate directly with the database.
454
Part V: Refactoring Applied
79796c15.qxd:WroxPro 2/25/08 9:08 AM Page 454
Make your new LINQ-to-SQL persistence classes implement these interfaces. Use the same names for theseclasses as for the existing ones: BranchData, ModelData, VehicleData, and VehicleCategoryData, butyou will be able to differentiate them because they belong to a different namespace. This way, it is enoughto change a single Imports statement in the Application Assembler (ApplicationEvents class) in orderto inject a different persistence layer provider. Take a look at the extracted interface shown in Listing 15-4.
Listing 15-4: Extracted IBranchData Interface Provides Type Safe Data Operations
Option Explicit OnOption Strict On
Imports RentAWheel.Business
Public Interface IBranchDataSub Delete(ByVal branch As Branch)Sub Insert(ByVal branch As Branch)Sub Update(ByVal branch As Branch)Function GetAll() As IList(Of Branch)
End Interface
This interface now implements the existing BranchData class and will be also implemented by the newBranchData based on LINQ-to-SQL technology. You can take a look at the original BranchData inListing 15-5 now that the IBranchData interface has been extracted.
Listing 15-5: BranchData after Interface Extraction
Option Strict OnOption Explicit On
Imports System.Data.SqlClientImports RentAWheel.Data.Implementation.ColumnNamesImports RentAWheel.DataImports RentAWheel.BusinessImports RentAWheel.Data.BaseImports System.Configuration
Public Class BranchDataInherits AbstractAdoData(Of Branch)Implements IBranchData
Private Const SelectAllFromBranchSql As String = _“Select * from Branch”Private Const DeleteBranchSql As String = _“Delete Branch Where BranchId = @Id”Private Const InsertBranchSql As String = _“Insert Into Branch (BranchName) Values(@Name)“Private Const UpdateBranchSql As String = _“Update Branch Set BranchName = @Name Where BranchId = @Id”
Private Const IdParamterName As String = “@Id”Private Const NameParameterName As String = “@Name”
Public Sub New(ByVal settings As ConnectionStringSettings)Continued
455
Chapter 15: LINQ and Other VB 2008 Enhancements
79796c15.qxd:WroxPro 2/25/08 9:08 AM Page 455
Listing 15-5: BranchData after Interface Extraction (continued)
MyBase.New(settings)End Sub
Public Overrides Sub Delete(ByVal branch As Branch) _Implements IBranchData.Delete
Dim command As IDbCommand = MyBase.DbProviderFactory.CreateCommandAddParameter(command, IdParamterName, DbType.Int32, _branch.Id)ExecuteNonQuery(command, DeleteBranchSql)
End Sub
Public Overrides Sub Insert(ByVal branch As Branch) _Implements IBranchData.Insert
Dim command As IDbCommand = MyBase.DbProviderFactory.CreateCommandAddParameter(command, NameParameterName, DbType.String, _branch.Name)ExecuteNonQuery(command, InsertBranchSql)
End Sub
Public Overrides Sub Update(ByVal branch As Branch) _Implements IBranchData.Update
Dim command As IDbCommand = MyBase.DbProviderFactory.CreateCommandAddParameter(command, NameParameterName, DbType.String, _branch.Name)AddParameter(command, IdParamterName, DbType.Int32, _branch.Id)ExecuteNonQuery(command, UpdateBranchSql)
End Sub
Public Overrides Function GetAll() As IList(Of Branch) _Implements IBranchData.GetAll
Dim command As IDbCommand = MyBase.DbProviderFactory.CreateCommandDim branchesSet As DataSet = FillDataset(command, _SelectAllFromBranchSql)Dim table As DataTable = branchesSet.Tables(0)Dim branches As IList(Of Branch) = New List(Of Branch)For Each row As DataRow In table.Rows
branches.Add(New Branch(CInt(row.Item(BranchTable.Id)), _row.Item(BranchTable.Name).ToString))
NextReturn branches
End FunctionEnd Class
You can observe that this class inherits AbstractAdoData. It used to be AbstractData, but I had toreorganize the hierarchy a bit now that SQL to LINQ has to be implemented. Here is what the top of thedata classes hierarchy now looks like — take a look at Listings 15-6 through 15-8.
Listing 15-6: AbstractData
Option Strict OnOption Explicit On
456
Part V: Refactoring Applied
79796c15.qxd:WroxPro 2/25/08 9:08 AM Page 456
Listing 15-6: AbstractData (continued)
Imports System.Configuration
Public Class AbstractDataPrivate connectionStringSettingsValue _As ConnectionStringSettings
Public Sub New(ByVal settings As ConnectionStringSettings)Me.ConnectionStringSettings = settings
End Sub
Public Overridable Property ConnectionStringSettings() _As ConnectionStringSettings
GetReturn connectionStringSettingsValue
End GetSet(ByVal value As ConnectionStringSettings)
connectionStringSettingsValue = valueEnd Set
End PropertyEnd Class
AbstractData does not contain a lot of functionality; still this will be enough to have all data classesinjected with configuration settings (connection string and data provider information) in the same way.
Listing 15-7: AbstractAdoData
Option Strict OnOption Explicit On
Imports System.DataImports System.Data.CommonImports System.Configuration
Public MustInherit Class AbstractAdoData(Of PersistedObject)Inherits AbstractData
Private dbProviderFactoryValue As DbProviderFactoryPrivate connectionStringSettingsValue As ConnectionStringSettings
Public Sub New(ByVal settings As ConnectionStringSettings)MyBase.New(settings)
End Sub
Public Overrides Property ConnectionStringSettings() _As ConnectionStringSettings
GetReturn connectionStringSettingsValue
End GetSet(ByVal value As ConnectionStringSettings)
MyBase.ConnectionStringSettings = valueContinued
457
Chapter 15: LINQ and Other VB 2008 Enhancements
79796c15.qxd:WroxPro 2/25/08 9:08 AM Page 457
Listing 15-7: AbstractAdoData (continued)
Me.DbProviderFactory = DbProviderFactories.GetFactory( _MyBase.ConnectionStringSettings.ProviderName)
End SetEnd Property
Protected Property DbProviderFactory() As DbProviderFactoryGet
Return dbProviderFactoryValueEnd GetSet(ByVal value As DbProviderFactory)
dbProviderFactoryValue = valueEnd Set
End Property
Private Function CreateConnection() As IDbConnectionDim connection As IDbConnection = _DbProviderFactory.CreateConnection()connection.ConnectionString = _MyBase.ConnectionStringSettings.ConnectionStringReturn connection
End Function
Private Function PrepareDataObjects(ByVal command As IDbCommand, _ByVal sql As String) As IDbConnection
Dim connection As IDbConnection = CreateConnection()connection.Open()command.Connection = connectioncommand.CommandText = sqlReturn connection
End Function
Protected Sub AddParameter(ByVal command As IDbCommand, _ByVal parameterName As String, ByVal parameterType As DbType, _ByVal paramaterValue As Object)
Dim parameter As IDbDataParameter = command.CreateParameter()parameter.ParameterName = parameterNameparameter.DbType = parameterTypeparameter.Value = paramaterValuecommand.Parameters.Add(parameter)
End Sub
Protected Function FillDataset(ByVal command As IDbCommand, _ByVal sql As String) As DataSet
Dim connection As IDbConnection = PrepareDataObjects(command, sql)Dim adapter As IDbDataAdapter = DbProviderFactory.CreateDataAdapterDim dataSet As New DataSetadapter.SelectCommand = commandadapter.Fill(dataSet)connection.Close()Return dataSet
End Function
Protected Sub ExecuteNonQuery(ByVal command As IDbCommand, _
458
Part V: Refactoring Applied
79796c15.qxd:WroxPro 2/25/08 9:08 AM Page 458
Listing 15-7: AbstractAdoData (continued)
ByVal sql As String)Dim connection As IDbConnection = PrepareDataObjects(command, sql)command.ExecuteNonQuery()connection.Close()
End Sub
Public MustOverride Function GetAll() As IList(Of PersistedObject)
Public MustOverride Sub Delete(ByVal persisted As PersistedObject)
Public MustOverride Sub Update(ByVal persisted As PersistedObject)
Public MustOverride Sub Insert(ByVal persisted As PersistedObject)
End Class
AbstractAdoData contains the code you are already familiar with from the previous chapters. The newclass is AbstractLinqData, shown in Listing 15-8.
Listing 15-8: AbstractLinqData Is the Base Class for Data Classes Based on LINQ to SQL
Option Explicit OnOption Strict On
Imports System.Data.LinqImports System.ConfigurationImports System.Data.Common
Public Class AbstractLinqDataInherits AbstractData
Private contextValue As DataContextPrivate connectionStringSettingsValue As ConnectionStringSettings
Public Sub New(ByVal settings As ConnectionStringSettings)MyBase.New(settings)
End Sub
Public Overrides Property ConnectionStringSettings() _As ConnectionStringSettings
GetReturn connectionStringSettingsValue
End GetSet(ByVal value As ConnectionStringSettings)
MyBase.ConnectionStringSettings = valueMe.Context = New DataContext(value.ConnectionString)
End SetEnd Property
Public Property Context() As DataContextGet
Continued
459
Chapter 15: LINQ and Other VB 2008 Enhancements
79796c15.qxd:WroxPro 2/25/08 9:08 AM Page 459
Listing 15-8: AbstractLinqData Is the Base Class for Data Classes Based on LINQ to SQL (continued)
Return contextValueEnd GetSet(ByVal value As DataContext)
contextValue = valueEnd Set
End PropertyEnd Class
Now you are ready to start with the LINQ-to-SQL data classes implementation. Start by adjusting entity classes.
Applying Attributes to Entity ClassesI have decided to use attribute-based mapping for LINQ to SQL. This means that classes and propertiesattributes have to be marked by appropriate TableAttribute and ColumnAttribute classes. The goodthing about metadata (attributes) is that if you do not need them, they do not stand in the way. This meansthat the entity classes from the RentAWheel.Business namespace work with standard data classes evenwith attributes applied.
Some additional tweaking must be performed on entity classes:
❑ Adding default constructor — Entity classes mapped with LINQ to SQL have to possess adefault (parameter-less) constructor method.
❑ Using EntityRef for property in one-to-one relationship — LINQ is capable of resolving therelationship between classes, but in order to do that, the private property value has to bechanged to EntityRef(Of RelatedClass) type.
❑ Adding to classes the property to represent the Id field of related classes — For example, theModel class has a property of type VehicleCategory. VehicleCategoryId has to be added tothe Model class.
❑ Changing database type of columns mapping to enums from tinyint to integer.
❑ Changing enumerators to base 1.
The Model class represents well the changes performed on the entity classes. You can see the Model classcode in Listing 15-9.
Listing 15-9: Entity Class Example after LINQ-to-SQL Adaptation
Option Explicit OnOption Strict On
Imports System.Data.Linq.MappingImports System.Data.Linq
<Table()> _Public Class Model
Private idValue As Integer
460
Part V: Refactoring Applied
79796c15.qxd:WroxPro 2/25/08 9:08 AM Page 460
Listing 15-9: Entity Class Example after LINQ-to-SQL Adaptation (continued)
Private nameValue As String<Column()> _Public CategoryId As IntegerPrivate categoryValue As EntityRef(Of VehicleCategory)
Public Sub New()‘needed by Linq to Sql implementation
End Sub
Public Sub New(ByVal id As Integer, _ByVal name As String, ByVal category As VehicleCategory)
Me.Id = idMe.Name = nameMe.Category = category
End Sub
<Column(IsPrimaryKey:=True, IsDbGenerated:=True, Name:=”ModelId”)> _Public Property Id() As Integer
GetReturn idValue
End GetSet(ByVal value As Integer)
idValue = valueEnd Set
End Property
<Column(Name:=”ModelName”)> _Public Property Name() As String
GetReturn nameValue
End GetSet(ByVal value As String)
nameValue = valueEnd Set
End Property
<Association(Storage:=”categoryValue”, ThisKey:=”CategoryId”)> _Public Property Category() As VehicleCategory
GetReturn categoryValue.Entity
End GetSet(ByVal value As VehicleCategory)
categoryValue.Entity = valueEnd Set
End Property
End Class
You can see in this class an example of the use of another attribute from the System.Data.Linq.Mappingnamespace. This is the Association attribute. It is used to map the property that holds the value of anotherrelated mapped entity class.
461
Chapter 15: LINQ and Other VB 2008 Enhancements
79796c15.qxd:WroxPro 2/25/08 9:08 AM Page 461
You can now see the implementation of the LINQ-to-SQL data class. Take a look at VehicleData inListing 15-10.
Listing 15-10: Example of LINQ-to-SQL Data Class — VehicleData
Option Explicit OnOption Strict On
Imports System.Collections.GenericImports System.Data.LinqImports RentAWheel.BusinessImports RentAWheel.Data.ImplementationImports System.ConfigurationImports RentAWheel.Data.Base
Public Class VehicleDataInherits AbstractLinqDataImplements IVehicleData
Private vehicles As Table(Of Vehicle)
Public ReadOnly Property Queryable() As IQueryable(Of Vehicle)Get
Return vehiclesEnd Get
End Property
Public Sub New(ByVal settings As ConnectionStringSettings)MyBase.New(settings)vehicles = Context.GetTable(Of Vehicle)()
End Sub
Public Sub Delete(ByVal vehicle As Business.Vehicle) _Implements IVehicleData.Delete
vehicles.DeleteOnSubmit(vehicle)Context.SubmitChanges()
End Sub
Public Function GetAll() As IList(Of Business.Vehicle) _Implements IVehicleData.GetAll
Dim allVehicles = _From queried In vehicles _Select queriedReturn allVehicles.ToList
End Function
<Obsolete(“Use Queriable instead”)> _Public Function GetByCriteria(ByVal operational As Business.Operational, _
ByVal rentalState As Business.RentalState, _ByVal branchId As Integer, _ByVal categoryId As Integer) _As IList(Of Business.Vehicle) _Implements IVehicleData.GetByCriteria
If (operational = Nothing And _rentalState = Nothing _
462
Part V: Refactoring Applied
79796c15.qxd:WroxPro 2/25/08 9:08 AM Page 462
Listing 15-10: Example of LINQ-to-SQL Data Class — VehicleData (continued)
And branchId = Nothing _And categoryId = Nothing) Then
Return Me.GetAll()End If
Dim allVehicles = _From queried In vehicles _Where (queried.Operational = operational Or operational = Nothing) And _(queried.RentalState = rentalState Or rentalState = Nothing) And _(queried.BranchId = branchId Or branchId = Nothing) And _(queried.Model.CategoryId = categoryId Or categoryId = Nothing) _Select queriedReturn allVehicles.ToList
End Function
Public Sub Insert(ByVal vehicle As Business.Vehicle) _Implements IVehicleData.Insert
vehicles.DeleteOnSubmit(vehicle)Context.SubmitChanges()
End Sub
Public Sub Update(ByVal vehicle As Business.Vehicle) _Implements IVehicleData.Update
Context.SubmitChanges()End Sub
End Class
One interesting detail is the read-only property Queryable. It permits writing LINQ queries againstVehicleData, resolving the problem of complex query logic. Take note, however, that this propertydoes not belong to the IVehicleData interface, because it cannot be supported by a standard versionof VehicleData. In order to use this property, the client has to downcast the VehicleData instancefrom IVehicleData to VehicleData.
One curious effect of XML literal support in Visual Basic is discussed next.
Using XML Literals Side Effect to Embed Formatted SQLWith XML literals you can embed text values inside XML tags, and these values preserve multiline for-matting and spacing. Placing SQL inside these tags can be a smart way to preserve SQL formatting thatdoes not contain string delimiters and concatenation signs. This way, SQL code can be pasted directly toand from a SQL and Visual Basic editor. Take a look at an example of the XML literals for the purpose ofembedding SQL in the standard BranchData class in Listing 15-11.
Listing 15-11: XML Literals Help Embed SQL Statements and Preserve Formatting
Private Shared Function SelectModelJoinCategorySql() As StringReturn <sql>
Select Model.ModelId As ModelId, Model.ModelName as ModelName, Category.CategoryName, Category.CategoryId,
Continued
463
Chapter 15: LINQ and Other VB 2008 Enhancements
79796c15.qxd:WroxPro 2/25/08 9:08 AM Page 463
Listing 15-11: XML Literals Help Embed SQL Statements and Preserve Formatting (continued)
Category.DailyPrice, Category.WeeklyPrice, Category.MonthlyPrice from Model Inner Join Category On Model.CategoryId = Category.CategoryId
</sql>.Value.TrimEnd Function
When you download this chapter version of Rent-a-Wheels from www.wrox.com, don’t forget tochange the type of columns in the Vehicle table from tinyint to integer.
It is enough to compare two versions of the data classes to witness the advancement that LINQ brings toVisual Basic: efficient, database-agnostic persistence layer implementation with no SQL code and withquery capabilities as strong as the SQL. Once more providers become available, LINQ to SQL is poisedto become a preferred technology for implementing persistence in .NET.
SummaryThe future holds great promise for Visual Basic. With features such as LINQ and XML literals, 2008 can bethe most productive version of Visual Basic ever. Compared to previous versions, code written in VB 2008is more expressive, compact, and powerful. Many new applications of these features, such as LINQ-to-SQLsupport for different database providers, are yet to be expected.
In case you are working with the code written in some previous version of Visual Basic, such code cannottake advantage of these new Visual Basic 2008 features. In those cases you can retrofit the code with thenew features, making it more compact and expressive.
If your code is creating XML structure through simple string literals, you can replace XML strings withXML literals. If that same XML structure is being populated with data through the DOM API, you canuse embedded expressions in XML literals instead.
When writing code that searches for any type of data in Visual Basic, you will most probably write thecode that iterates some structure and has a lot of complicated implementation details. Such code, writtenin imperative style, can be much better expressed through declarative syntax. Replace your imperativestyle query code with LINQ.
LINQ brings object-relational mapping to .NET in full swing. ORM frameworks can help you create anindustrial-strength persistence layer for your objects with minimum effort. Solutions based on ORM will be more database-independent, will insulate better your Visual Basic code from database structure changes,and will help you write less code than classic solutions based on SQL. And you can replace your classicpersistence layer with a new one, based on LINQ.
The space of one chapter is too small for all the novelties that the 2008 version of VB is bringing. Staytuned with new versions of Refactor! and experiment yourself to discover new refactorings that can beperformed with this latest version of VB.
464
Part V: Refactoring Applied
79796c15.qxd:WroxPro 2/25/08 9:08 AM Page 464
The Future of Legacy VB Code
You have seen the current state of the art of Visual Basic programming and refactoring so far inthis book. And in the previous chapter you have seen what the future holds for Visual Basic. It istime to make the circle full. In this chapter you will look back at the history of Visual Basic. Youwill see how refactoring can be relevant to dealing with the significant and still existing code baseof legacy Visual Basic code. In this chapter you will not see many new refactorings; however, youwill see how you can apply already known refactorings to upgrade legacy VB code.
Pre-.NET versions of Visual Basic made VB during the 1990s the most popular programming toolon the planet. The simplicity and power of Visual Basic, promoted under the Rapid ApplicationDevelopment (RAD) concept, won the sympathies of a multitude of developers who needed a no-frills tool for instant results.
The success of classic Visual Basic (VB 6 and previous) and its loyal user base proved to be oneof the biggest hurdles to adoption of Visual Basic .NET. Many VB 6 programmers were contentwith their tool and decided to keep using it. This decision was further fueled by two characteris-tics of VB .NET:
❑ VB .NET brings a significant rupture in syntax and other language characteristics (object-oriented versus object-based, and so on) compared to classic VB.
❑ Classic VB code is not easily migrated to VB .NET.
I will not take part in the Visual Basic .NET versus classic Visual Basic debate, because I considerit to be in the area of each programmer’s personal preferences. There is no doubt, however, thatVB .NET is now a better choice in a commercial and technological sense, and more and more pro-grammers and organizations are switching from classic VB to VB .NET or some other languagelike C# or Java.
79796c16.qxd:WroxPro 2/25/08 9:09 AM Page 465
Unfortunately, the migration process from VB 6 to VB .NET is by no means smooth. In case you stillhaven’t started with migration, you are certainly considering it. This chapter starts by dealing with thefollowing question: should you migrate from VB 6 code to VB .NET?
This chapter deals mostly with VB 6 legacy code, because the VB 6 code base is probably the largest ofall pre-.NET Visual Basic versions. If your code is pre-VB 6, then you will have to put additional effortinto migration, more so the older your code is. In addition, you will not be able to count on the help ofthe Upgrade Wizard.
To Migrate or Not To MigrateThis decision will be mostly influenced by the value of your existing Visual Basic 6 code base. If youplan to use and evolve the code further, then you should consider migrating. On the other hand, if youneed the functionality the code provides, but the code itself is difficult to maintain and has reached theend of useful life, instead of migration, you should consider a complete rewrite in .NET. Most com-monly, you will migrate certain parts of your solution, phasing out VB 6 code gradually. You shouldchoose the path that suits your specific situation. Whatever line of action you choose, there are someaspects of the migration process that you should be aware of before embarking on it.
Migration versus UpgradeWith the term migration, I refer to the basic process of transforming Visual Basic 6 codeto Visual Basic .NET syntax so it can run in the .NET run time correctly. The process ofmigration is focused on obtaining the functional equivalent of VB 6 code in .NET. Thisprocess can be partially automated through Microsoft’s Visual Basic Upgrade Wizard.
A VB 6 upgrade to .NET is geared toward obtaining code that is benefiting from newlanguage capabilities, idioms, and patterns characteristic of Visual Basic .NET code.This process has to be preceded by Visual Basic 6 to .NET migration and is based ondifferent refactorings that are applied to raw migrated code.
Are Classic VB and VB .NET the Same Programming Language?VB .NET is an evolution of VB 6 programming language, and this is reflected by theversioning policy that Microsoft applies to the Visual Basic language. The first versionof VB .NET is also known as VB 7 while the version that ships with Visual Studio 2008is known as VB 9.
However, changes introduced in the first version of .NET when compared to VB 6 meanthat there is no backward compatibility between the two. If you compare this to the rela-tionship between the C and C++ programming languages, you will see that almost any Ccode can be compiled with a C++ compiler, making C and C++ in that aspect more closelyrelated than VB and VB .NET. New language capabilities like full object orientation andmetadata in the forms of attributes, generics, and so on, require a significant paradigmshift from the programmer’s point of view. That is why when embarking on Visual Basic.NET and coming from classic Visual Basic, you are better off viewing VB .NET as a com-pletely new programming language that deserves thorough study, practice, and signifi-cant assimilation time.
466
Part V: Refactoring Applied
79796c16.qxd:WroxPro 2/25/08 9:09 AM Page 466
Much information is available on the subject of migration, ready tool support, and so on. However, onceyou have performed the migration, and your legacy code can execute in .NET, you have only one halfdone. In order to benefit from .NET, you need to upgrade your code through a refactoring process so itbecomes written in VB .NET spirit. This chapter deals with the upgrade process in much greater detail.
Migration Cannot Be 100 Percent AutomatedMicrosoft ships the Visual Basic Upgrade Wizard as a part of Visual Studio. The tool can be invoked byselecting a File ➪ Open ➪ Convert option in the Visual Studio menu. This tool is capable of taking yourVisual Basic 6 code as an input and creating a new Visual Basic .NET project containing migrated code.Unfortunately, the tool will rarely migrate 100 percent of your code. Real-life production code will bemigrated with varying success rate, depending on numerous factors:
❑ The use of syntax, functions, and keywords like GoSub/Return, and so on.
❑ The use of conditional compilation
❑ The use of controls, methods, properties, and events
If you use any of these that have no direct equivalent in .NET, the Upgrade Wizard cannot migrate thecode, and you will end up with numerous ‘UPGRADE_ISSUE: Feature X is not supported com-ments inside your code and blocks of code that do not compile in .NET. You will have to resolve suchissues manually.
VB 6 and VB .NET Code Can Interoperate.NET comes with a built-in COM interoperability solution. The interoperability goes both ways. COMcomponents can consume services provided by .NET components, and .NET components can invokeCOM components.
You can reference Visual Basic 6 and other COM libraries from your .NET project, instantiate COMobjects, and invoke their methods. This is accomplished through a runtime callable wrapper (RCW)that makes a COM component look like a .NET component. This option is very useful for a gradualmigration approach. This way, you can start your migration by upgrading your presentation layer to.NET first, then your domain, and finally the persistence layer.
The COM callable wrapper (CCW) lets you expose your .NET classes to the COM world. You will beable to reference .NET libraries from your VB 6 integrated development environment (IDE). VisualStudio .NET lets you choose the COM component template when you add items to your project. Onceyou build your .NET project, you will see a new item in the list of available references in VB 6 IDE. TheCOM name of the class will be in the form of [Project Name].[Class Name]. This opens another setof possibilities for gradual migration. You can start with the migration of the persistence layer and thenmove gradually toward the presentation layer. This will be possible on the condition that your applica-tion is not monolithic and is partitioned along three-tiered architecture. Take a look at how CCW andRCW are enabling .NET–COM interoperability in Figure 16-1.
The decision on how to deal with your legacy code is a complex one. However, the market often dictatescertain conclusions. .NET has been widely accepted, and it’s perceived as a cutting-edge technology forbusiness applications development. In that sense, VB 6 is becoming less and less competitive.
467
Chapter 16: The Future of Legacy VB Code
79796c16.qxd:WroxPro 2/25/08 9:09 AM Page 467
Figure 16-1
The best choice for you depends on specific circumstances in your case. You might decide to stick withVB 6 code, upgrade it, or simply discard this code and perform a complete rewrite. A comparison of dif-ferent options is shown in Table 16-1.
Table 16-1: Legacy Code Upgrade Options
I assume that the majority of you have probably already chosen the gradual upgrade path to .NET. Inthat case there are some tools that you might find useful in the process. The next section of the chapterdiscusses these tools.
Decision Factors No Upgrade Upgrade Rewrite
Initial cost None Moderate High
Effort None Medium High
Risk None Medium Medium
Competitiveness Low Medium High
Different technologies used simultaneously
No Yes No
Skill upgrade cost None High High
Automation Not relevant Medium None
Third-party support (components,tools, training, consulting, and so on)
Low (Declining) Medium High
Positive image and marketing Low High High
COM Client
.NET Client
CCW
.NETClass
RCW
COMClass
468
Part V: Refactoring Applied
79796c16.qxd:WroxPro 2/25/08 9:09 AM Page 468
Migration Tools and LibrariesMicrosoft provides a set of free tools and libraries that can help you with the migration process and easethe burden to a large degree. There are also third-party commercial tools that go beyond basic functionalityprovided by Upgrade Wizard.
Code Advisor for Visual Basic 6This tool installs as a Visual Basic 6 add-in. Its purpose is to analyze your existing Visual Basic 6 codeand to mark migration-problematic code. It will help you identify code you should modify before evenattempting migration. It is a lot easier to modify a fully functional Visual Basic 6 code than to try to fixbroken Visual Basic .NET code that resulted from a poorly executed migration process. Using code advi-sory before attempting automated migration with the help of Visual Basic Upgrade Wizard will result in a much higher upgrade success rate. You can find the download link for Code Advisor at this URL:http://msdn2.microsoft.com/en-us/vbasic/ms789135.aspx.
Visual Basic 6 Upgrade Assessment ToolThe purpose of this tool is to help you estimate the cost and effort needed for migration of a certain project. The Assessment tool gives you a detailed breakdown of manual adjustments that have to be performed, estimating necessary effort in terms of hours and even giving you an estimated amount indollars. You can configure different calculation parameters such as the cost of programmer per hour sothey fit your specific situation. You can find the download link for the Upgrade Assessment Tool at thisURL: http://msdn2.microsoft.com/en-us/library/aa480541.aspx.
The Microsoft.VisualBasic.Compatibility AssemblyThis assembly contains classes that help you migrate VB 6 code that is using types that have no directequivalent in the .NET Framework. You should strive toward phasing out the use of this assembly,because it is provided for compatibility purposes only.
Visual Basic Upgrade WizardPart of the Visual Studio distribution, this wizard will perform the migration work by creating a new .NETproject with corresponding classes based on an input VB 6 project. Migration success largely depends ontweaking your code beforehand, with the help of Code Advisor, or running the wizard for the first time foranalysis purposes only.
ArtinSoft Visual Basic Upgrade CompanionArtinSoft Upgrade Companion goes beyond the Upgrade Wizard. It can perform type inference and error-handling upgrades and supports migration of a number of additional ActiveX controls to .NET equivalents.You can learn more about Upgrade Companion at www.artinsoft.com/pr_vbcompanion.aspx.
Most of the modifications to your Visual Basic 6 code that precede the migration process will be basedon the advice obtained from VB 6 Code Advisor. In addition, you should perform some refactorings inorder to simplify the migration process. The next section of this chapter mentions a few.
469
Chapter 16: The Future of Legacy VB Code
79796c16.qxd:WroxPro 2/25/08 9:09 AM Page 469
Preliminary VB 6 RefactoringsEven before embarking on migration, you can perform some refactorings on Visual Basic 6 code that willease the burden of the migration process to follow. You can start out by taking care of basic hygiene: youcan eliminate dead code and unused references.
Breaking the MonolithThroughout the book, you have seen the value of the divide and conquer principle. If you are dealing with a large code base, it is best if you can approach migration in a gradual manner, with the help of.NET–COM interop or by simply migrating and testing different pieces and then putting everythingtogether. Attempting migration on large projects in one go will result in a huge broken code base, andyou will not know where to start in order to make things work. This is especially true for applicationsthat are not divided into components and consist of a single executable or library. This is the you shouldperform Break Monolith refactoring beforehand when migrating such projects.
Smell: Monolithic ApplicationDetecting the SmellA good indication of a monolithic application is a single binary file. However, just the physical view of an application will not tell you a lot about internal application design.
If classes are well designed, but only placed into a single binary, then separating classesin the form of dynamically linked libraries is often a simple affair. If classes are poorlydesigned, for example all classes in your project are forms, then breaking the physicalstructure into smaller pieces (see Break Monolith refactoring) has to be preceded byclass-level (logical model) redesign of your application.
Client-server applications that use a database as a simple persistence and transactionalmechanism and internally embed SQL with business logic do not differ significantly intheir substance from classical monolithic applications. In those cases, breaking such anapplication into smaller physical parts is applicable just as well.
Related RefactoringUse Break the Monolith refactoring to eliminate this smell.
RationaleNonmodular, single-tiered applications are much more difficult to maintain, test, and dis-tribute. They do not promote reuse. Modular design is the most important tool for com-bating complexity in software design. This means that monolithic applications are oftenmuch more complex, and in order to make these applications maintainable, breakingthem into smaller parts such as libraries and components is often the only solution fortheir longevity.
470
Part V: Refactoring Applied
79796c16.qxd:WroxPro 2/25/08 9:09 AM Page 470
Figure 16-2
Figure 16-3
Refactoring: Break MonolithMotivationMonolithic applications are single-tiered, self-contained applications, nonmodular con-structs. Such applications are difficult to maintain, because the effects of changes arenot isolated and they do not promote encapsulation on a large scale. They do not pro-mote reuse, except reuse that can be performed internally, adding to tight coupling andinterdependency of the code.
From a migration point of view, such applications are much more difficult to transform to.NET because you have to perform the migration process in an “all or nothing” manner.This means that once you perform the automated step of migration process, you will endup with huge quantities of nonfunctional, broken code. Nor will you be able to test smallparts of an application in isolation. Therefore, you should break your monolithic applica-tion into smaller parts that are easier to migrate and test in a gradual manner.
Related SmellsUse this refactoring to eliminate the Monolithic Application smell and to break theapplication into smaller parts like components or services.
MechanicsThis is large scale refactoring that is comprised of many smaller refactorings. In orderto break the monolith, you have to extract assemblies, move and rename classes andnamespaces, and organize your project around typical architectural tiers. Take a look at Chapter 13 for more details on the related refactorings.
BeforeBefore Break Monolith refactoring is performed, you are generally dealing with a singleexecutable, as shown in Figure 16-2.
AfterAfter the extraction, your application comprises more components, often assembledalong architectural tiers, as shown in Figure 16-3.
«executable»Monolith
«executable»Presentation Layer
«library»Persistence Layer
«library»Domain Layer
471
Chapter 16: The Future of Legacy VB Code
79796c16.qxd:WroxPro 2/25/08 9:09 AM Page 471
Performing Break Monolith refactoring before migration is a valid option if you can count on a battery of functional tests that will help you perform this restructuring on your VB 6 code effectively and with-out introducing any new bugs. Unfortunately, you do not often have functional, or even less common,unit tests for your legacy code at your disposal. In these cases, it makes more sense to migrate the codeand implement the tests only after the migration. This leads to the section of the chapter coming shortlythat talks about techniques you can use to put your freshly migrated code under a testing harness.
Dealing with Conditional CompilationThe Upgrade Wizard does not react gracefully when confronted with conditional compilation blocks.You will most probably end up with VB 6 code pasted “as is” inside the block in your migrated class. In order to deal with this issue, you can employ a very simple methodology.
Comment out all your conditional compilation statements before invoking the Upgrade Wizard. TheUpgrade Wizard will not erase the comments. Once you have finished working with the wizard, erasethe comment sign and turn your conditional compilation statements back on. This way, you will success-fully migrate code that would be otherwise ignored by Upgrade Wizard.
Putting Your Migrated Code under a Testing Harness
Back in Chapter 3 of this book, you can find a short introduction to unit testing and the NUnit frame-work. The NUnit framework was written in order to facilitate automated unit testing. Unit testing meanstesting the smallest testable part of your application, generally a method of a property, in isolation. Witha test-driven approach, meaning test first, implementing unit tests is feasible. Implementing unit tests aposteriori, on already finished code, is quite a different matter altogether. Putting your freshly migratedcode under unit tests is generally highly complex and has to be done gradually, on par with furtherrefactorings of your migrated code.
This leaves you in a quandary. In order to refactor migrated code, you need a testing backup, an assur-ance that your refactoring is not introducing bugs to the migrated code base. And in order to put yourcode under unit tests, you need to refactor your code for dependency injection. This looks like thevicious circle. So, what is the solution?
Introducing a Functional Testing HarnessInstead of starting off with unit tests, you can create a functional testing harness that will help you startwith refactoring. Functional testing means performing tests from the user’s point of view. Whenever auser interacts with the system, he or she does so in order to achieve a certain goal. If you can simulatethe user action and then verify that the goal is achieved, you have the basic insurance that the system isperforming as it should. As you progress toward better designed, better encapsulated code, extract andconsolidate classes, you can add new unit tests to already existing functional test base.
Definition: Functional testing verifies that the system behavior is correct from theuser’s point of view and according to requirements.
472
Part V: Refactoring Applied
79796c16.qxd:WroxPro 2/25/08 9:09 AM Page 472
The basic difference between unit and functional testing is that functional tests result in execution of thecomplete execution flow. They do not test single method or property. This makes them less reliable anduseful as refactoring guardian than unit tests from two aspects:
❑ It is difficult to achieve high code coverage with functional tests. Because you are testingthe complete execution flow, the code will inevitably branch in different directions some-where along the line. You can try changing initial parameters and test for limit values and boundary conditions; nevertheless, you will most probably never achieve 100 percent code coverage.
❑ In case of error, it is more difficult to identify and pin down the exact source of error. Theerror has been produced somewhere along in the execution stack. You can catch the error, butfinding the exact offending line requires a lot of work such as debugging and the like.
While there are some downsides to functional testing harnesses, it is probably the best we can hope forwhen starting to work on legacy code. Let’s see how you can implement a functional testing harnessover your legacy code.
Implementing a Functional Testing HarnessWhile not originally created for this purpose, NUnit can serve as a good functional testing framework.You can use NUnit to invoke your tests automatically and to verify the results of test execution.
You construct tests using NUnit by implementing test classes with test methods that exercise the classunder scrutiny. You mark such methods with the Test attribute. When writing functional tests, youwrite the following code in the body of test methods:
❑ Code that simulates user action on the system
❑ Code that verifies the expected result of user action on the system
I have prepared a very basic example to illustrate an implementation of functional tests using NUnit.Take a look at Figure 16-4. It shows a very simple form that can be used to create users in some imagi-nary system.
Figure 16-4
When dealing with real-life applications, you will probably deal with much more complex code, but theunderlying approach will be the same. You can take a look at the form’s code in Listing 16-1.
473
Chapter 16: The Future of Legacy VB Code
79796c16.qxd:WroxPro 2/25/08 9:09 AM Page 473
Listing 16-1: Sample Legacy Form — UserMaintenance
Option Explicit OnOption Strict On
Imports System.Data.SqlClient
Public Class UserMaintenance‘Inherits System.Windows.Forms.Form (in partial)
Private Const connectionString As String = _“Data Source=localhost;Initial Catalog=CH16;Trusted_Connection=yes”
Private Sub CreateUser_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles CreateUser.Click
Dim insertUserSql As String = _“Insert Into Users (FirstName, LastName, Email) “ + _“Values(@FirstName, @LastName, @Email)“Dim connection As SqlConnection = _New SqlConnection(connectionString)Dim command As SqlCommand = New SqlCommand(insertUserSql)command.Connection = connectioncommand.Parameters.AddWithValue(“@FirstName”, Me.FirstName.Text)command.Parameters.AddWithValue(“@LastName”, Me.LastName.Text)command.Parameters.AddWithValue(“@Email”, Me.Email.Text)connection.Open()command.ExecuteNonQuery()connection.Close()
End SubEnd Class
In order to stimulate user action, in this case the Create User button click, you need to invoke theCreateUser_Click routine. Because this routine is private, you can extract code in the body of this routine into a new public routine called CreateNewUser. This way you have separated the code for event handling from the code that communicates with the database. In a more realistic situation, your event-handling routine would probably contain some other code, such as data validation or something similar,besides database interaction code. You can take a look at the resulting UserMaintenance form code in theListing 16-2.
Listing 16-2: UserMaintenance Form after Routine Extraction
Imports System.Data.SqlClient
Public Class UserMaintenance‘Inherits System.Windows.Forms.Form (in partial)
Public Const ConnectionString As String = _“Data Source=localhost;Initial Catalog=CH16;Trusted_Connection=yes”
Private Sub CreateUser_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles CreateUser.Click
474
Part V: Refactoring Applied
79796c16.qxd:WroxPro 2/25/08 9:09 AM Page 474
Listing 16-2: UserMaintenance Form after Routine Extraction (continued)
CreateNewUser()End Sub
Public Sub CreateNewUser()Dim insertUserSql As String = _
“Insert Into Users (FirstName, LastName, Email) “ + _“Values(@FirstName, @LastName, @Email)“
Dim connection As SqlConnection = _New SqlConnection(connectionString)Dim command As SqlCommand = New SqlCommand(insertUserSql)command.Connection = connectioncommand.Parameters.AddWithValue(“@FirstName”, Me.FirstName.Text)command.Parameters.AddWithValue(“@LastName”, Me.LastName.Text)command.Parameters.AddWithValue(“@Email”, Me.Email.Text)connection.Open()command.ExecuteNonQuery()connection.Close()
End SubEnd Class
Now you are ready to write a functional NUnit test class. You can call the class UserMaintenanceTest.You can use the SetUp method to instantiate UserMaintenance form and open database connection inUserMaintenanceTest. Inside the CreateUser test method you perform the following:
1. Fill controls in the UserMaintenance form with some random generated data for user first andlast name and e-mail.
2. Invoke the CreateNewUser method on the UserMaintenance form.
3. Check in the database that the user was correctly inserted.
Take a look at the test fixture code in Listing 16-3.
Listing 16-3: UserMaintenance Test Class
Imports NUnit.FrameworkImports System.Data.SqlClient
<TestFixture()> _Public Class UserMaintenanceTest
Private userForm As UserMaintenancePrivate connection As IDbConnection
<SetUp()> _Public Sub Prepare()
userForm = New UserMaintenanceconnection = New SqlConnectionconnection.ConnectionString = UserMaintenance.ConnectionStringconnection.Open()
Continued
475
Chapter 16: The Future of Legacy VB Code
79796c16.qxd:WroxPro 2/25/08 9:09 AM Page 475
Listing 16-3: UserMaintenance Test Class (continued)
End Sub
Private Shared Function RandomString() As StringRandomize()Return CStr(Hex(CLng((10000000000 * Rnd()) + 1)))
End Function
<Test()> _Public Sub CreateUser()
FillFormWithNewUserData()userForm.CreateNewUser()Dim reader As SqlDataReader = FindCreatedUserInDB()Assert.IsTrue(reader.HasRows)
End Sub
Private Function FindCreatedUserInDB() _As SqlDataReader
Dim command As SqlCommand = New SqlCommandDim findInsertedSql As String = _
“Select * from Users Where “ + _“FirstName = @FirstName and “ + _“LastName = @LastName and “ + _“Email = @Email”
command.Connection = CType(Me.connection, SqlConnection)command.CommandText = findInsertedSqlcommand.Parameters.AddWithValue(“@FirstName”, userForm.FirstName.Text)command.Parameters.AddWithValue(“@LastName”, userForm.LastName.Text)command.Parameters.AddWithValue(“@Email”, userForm.Email.Text)Dim reader As SqlDataReader = command.ExecuteReaderreader.Read()Return reader
End Function
Private Sub FillFormWithNewUserData()userForm.FirstName.Text = RandomString()userForm.LastName.Text = RandomString()userForm.Email.Text = RandomString() + “@“ + RandomString() + “.com”
End Sub
<TearDown()> _Public Sub EndTests()
connection.Close()End Sub
End Class
Now that you have tests in place, you can move further. As you progress with refactoring and youextract new classes, you can add new unit tests for them. Functional tests you can keep. They will stillserve you to test your system from a functional point of view, and as you layer your application, theyfulfill the role of integration tests.
476
Part V: Refactoring Applied
79796c16.qxd:WroxPro 2/25/08 9:09 AM Page 476
Upgrading Your Legacy CodeYou have performed the migration; your code executes correctly on .NET. You have created the testing har-ness, and you will be able to recognize if your code stops working. Your code is now ready for the upgradeprocess. Through the numerous refactorings that make up the upgrade process, you transform your legacycode to natural .NET code that is making use of typical VB .NET language capabilities and idioms.
Strict Static TypingYour migrated code is about to go through a number of transformations. The changes that the upgradeprocess will provoke on your code are by no means minor. This is why you need to enlist all the helpyou can get.
If your code is written in strict static type style, the compiler can warn you during compile time of manyerrors even before you commit them. VB 6 code is sometimes statically typed, if the Option Explicitstatement was placed at the beginning of the file. Option Strict is new to VB .NET, so it will never beactivated in freshly migrated code.
Start your upgrade process by executing Set Option Explicit On and Set Option Strict On refactorings onyour code base. This way, you will be able to identify a number of type related errors while performingthe refactorings to follow.
Check out Chapter 5 to see how these refactorings are performed.
Moving Design from Procedural toward an Object-Oriented Paradigm
A good sign of excessive procedural code in a migrated project is a large number of functions insidemodules and the use of user-defined types. As you can see in Chapter 12, this means that data andbehavior are split one from another, and you need to bring those two together in the form of a singletype. Take a look at Listing 16-4, which shows code inside a VB 6 module. Here you can see a simplifiedexample of procedural design in VB 6 code.
Listing 16-4: Procedural VB 6 Code
Option Explicit
Public Type CustomerFirstName As StringLastName As StringIsPremier As Boolean
End Type
Public Function CalculateDiscount(customer As Customer) As DoubleIf customer.IsPremier Then
CalculateDiscount = 3Else
CalculateDiscount = 0End If
End Function
477
Chapter 16: The Future of Legacy VB Code
79796c16.qxd:WroxPro 2/25/08 9:09 AM Page 477
Once migrated, a VB 6 type construct is replaced with a Visual Basic .NET structure. A structure in VB .NETcan have method and property members, but cannot be inherited. You can start by moving free functions to a structure. Once you place a function inside a structure, it becomes a method and no longer needsparameters to operate. Instead, it can access structure data members directly. Take a look at the exampleafter moving from procedural to object-oriented design in Listing 16-5.
Listing 16-5: Procedural to Object-Oriented Code: Type to Structure and Functioninside Structure
Option Strict OnOption Explicit On
Module MyModule
Public Structure CustomerDim FirstName As StringDim LastName As StringDim IsPremier As BooleanPublic Function CalculateDiscount() As Double
If Me.IsPremier ThenCalculateDiscount = 3
ElseCalculateDiscount = 0
End IfEnd Function
End StructureEnd Module
Because the CalculateDiscount method can access the IsPremier property directly, it no longerneeds the customer parameter. Instead of calling the CalculateDiscount directly, clients call theCalculateDiscount method on the Customer type.
The most important difference between structure and class constructs in Visual Basic .NET is that youcan inherit class while with structure you cannot. If necessary later on, you can move the structure to a separate file and replace the declaration with the keyword Class, transforming structure into class.Now you can inherit your object-oriented type.
For more background on converting from procedural to object-oriented design, consult Chapter 12.
Introducing InheritanceOne of the most hailed features introduced in VB .NET was implementation inheritance. In VB 6, youhad to resort to interface inheritance and simple delegation in order to accomplish code reuse that in VB .NET can be expressed through simple implementation inheritance.
Through inheritance you can eliminate a lot of the duplication in your code or you can spare yourselffrom writing a lot of unnecessary delegation code. In Chapter 12, you have seen how to amend such asituation through Extract Superclass refactoring.
A good place to start looking for duplication caused by lack of implementation inheritance in VB 6 is inclass-interface hierarchies. If you have a few classes that implement the same interface, it is highly probablethat you will find duplicated code in members inherited from the interface.
478
Part V: Refactoring Applied
79796c16.qxd:WroxPro 2/25/08 9:09 AM Page 478
Take a look at Figure 16-5. The code for members inherited from the IDataAccess interface —OpenConnection, CloseConnection, StartTransaction, CommitTransaction, andRollbackTransaction in classes UserDataAccess and AccountDataAccess — is identical.
Figure 16-5
The solution is to extract the common code for both classes into the common parent class and make thisclass implement IDataInterface. This way, you insert a new abstract class in the middle of the existinghierarchy. Take a look at Figure 16-6 to see the result.
Making Use of Parameterized ConstructorIn VB 6 you had the Class_Initialize() routine at your disposal for any object initialization tasks.However, this routine could not be parameterized. In VB .NET, you can write parameterized constructors.You can use parameterized constructors to impose certain rules on object instantiation. For example, youcan oblige the client to provide some objects that you might need in order to instantiate your object. And,as a more specific application of parameterized constructor, you can use parameterized constructors toimplement a constructor-based dependency injection, as discussed in Chapter 14.
Using Generic Containers for Additional Type SafetyIn VB 6, you had a small number of containers at your disposal. There was a VB 6 language built-inCollection, a list-like container for index-based access. Then later on came the Scripting.Dictionaryobject distributed with the Windows Scripting host, a dictionary-like container for key-based access.
«interface»IDataAccess
+OpenConnection()+CloseConnection()+StartTransaction()+CommitTransaction()+RollbackTransaction()
UserDataAccess
+Insert()+Update()+Delete()+Find()+OpenConnection()+CloseConnection()+StartTransaction()+CommitTransaction()+RollbackTransaction()
AccountDataAccess
+Insert()+Update()+Delete()+Find()+OpenConnection()+CloseConnection()+StartTransaction()+CommitTransaction()+RollbackTransaction()
479
Chapter 16: The Future of Legacy VB Code
79796c16.qxd:WroxPro 2/25/08 9:09 AM Page 479
Figure 16-6
While the choice of containers was quite limited, it was sufficient for most situations. In VB 6, you coulduse containers in one of the following manners:
❑ Bare-bones Collection and Dictionary
❑ Custom type-safe wrapper over Collection and Dictionary
The second option provided additional security for your code, meaning that only objects of the sametype could be added into the container. However, it also meant writing a lot of repetitive code. Take alook at an example of such a wrapper class in Listing 16-6.
In VB .NET you have a much greater number of container classes at your disposal. Ones available since the first edition of the .NET framework are found inside the System.Collections namespace,and newer type-safe generic collections that provide additional type safety can be found in theSystem.Collections.Generic namespace.
Listing 16-6: Example of Type-Safe Container Wrapper in VB 6
Option Explicit
Private coll As New collection
Public Sub Add(Item As Customer, Optional key As String, _Optional before As Integer, Optional after As Integer)
coll.Add Item, key, before, after
«interface»IDataAccess
+OpenConnection()+CloseConnection()+StartTransaction()+CommitTransaction()+RollbackTransaction()
DataAccess
+OpenConnection()+CloseConnection()+StartTransaction()+CommitTransaction()+RollbackTransaction()
UserDataAccess
+Insert()+Update()+Delete()+Find()
AccountDataAccess
+Insert()+Update()+Delete()+Find()
480
Part V: Refactoring Applied
79796c16.qxd:WroxPro 2/25/08 9:09 AM Page 480
Listing 16-6: Example of Type-Safe Container Wrapper in VB 6 (continued)
End Sub
Public Function Count() As IntegerCount = coll.Count
End Function
Public Sub Remove(index As Integer)coll.Remove (index)
End Sub
Public Function Item(index As Integer) as ObjectItem = coll.Item(index)
End Sub
When upgrading VB 6 code, you should strive to replace unsafe containers with their generic counter-parts, provided the two following conditions are met:
❑ Only objects of same type are placed inside the container.
❑ If the custom type-safe wrapper is used, the wrapper is not performing any additional process-ing besides basic container function.
Take a look at the upgrade path for containers in Table 16-2.
Table 16-2: Upgrade Path for Container Types
Upgrading Exception HandlingI have devoted a whole chapter (Chapter 6) to exception handling in this book. It deals exactly with thesubject relevant to this section: upgrade of legacy to structured error handling.
Bear in mind one fact: it will be much easier for you to organize your exceptions in the form of well-designed hierarchies if you have structured your classes well. Therefore, leave this refactoring for last.Once you have extracted classes and namespaces and formed inheritance hierarchies, it will be a wholelot easier to transform VB 6 error code to full-blown exception classes.
Implementing XML CommentsXML comments in .NET are not your typical VB 6 comments. The purpose of these comments is todocument the assembly to be used in Object Browser and by IntelliSense by the client programmerthat should not be exposed to source code of your assembly. XML comments are not meant to giveadditional information when browsing the code.
VB 6 Container VB .NET Migrated Container VB .NET Upgraded Container
Collection Microsoft.VisualBasic.Collection System.Collections.Generic.List
Scripting.Dictionary Scripting.Dictionary System.Collections.Generic.Dictionary
481
Chapter 16: The Future of Legacy VB Code
79796c16.qxd:WroxPro 2/25/08 9:09 AM Page 481
If you already have comments written with a similar purpose in mind in your VB 6 code, you can transformthose to XML format. If not, write new ones. These comments will greatly facilitate the work for programmersusing your assemblies.
Releasing Resources in .NETIn VB 6, it was recommended that you destroy resources explicitly. You would perform this by settingthe variable to Nothing. In COM, there is a reference-counting garbage collector; this has the effect ofdecrementing an instance counter and in this way helps the garbage collector clean the instance.
In .NET, the garbage collector is fundamentally different. (.NET garbage collection is discussed inChapter 11.) For the .NET tracing garbage collector, setting the variable to Nothing will not have anyeffect on resource collection dynamics. Make use of a Using–End Using block instead to guarantee thatthe runtime disposes of a resource when your code exits the block.
SummaryThe migration process of VB 6 code to VB .NET is far from straightforward. Even after basic migrationhas been performed, you will find resulting code lacking many of the good design features that can beapplied in VB .NET. After a simple migration, such code will have a definite legacy feel about it.
No need to despair. If your code is really worth it, you can upgrade it completely to VB .NET. Refactoringtechniques can be of great help in the process. Start by preparing your code still in VB 6 for the migration.Make use of tools such as the Code Advisor and Upgrade Wizard to get your code to VB .NET at the highest possible operational rate.
Once your code can be executed in .NET, be sure to create tests that will help you validate the refactoringsyou are about to perform. Putting legacy code under a testing harness is by no means simple, so start offby testing your code from a functional point of view. As you progress in redesigning your code, add moreunit tests to your test battery.
VB 6 has no implementation inheritance, generics, constructors, or strict typing. Start by making yourcode explicit and strict. This way, the compiler can give you further assurance that you are not breakinganything when modifying your code.
Look for duplicate code between sibling classes in interface implementation hierarchies, and eliminate anyduplication by extracting a superclass. Use parameterized constructors to add additional safety to yourcode. Use generic containers to reduce code base and to provide type safety to your container classes.
In this chapter, you have seen some typical situations and you have received some hints on where tostart when dealing with legacy code. After all, once your code executes in .NET, it should leave its legacyorigins behind. Treat your migrated code as any other VB .NET code and make sure you are using all theobject-oriented language features at your disposal to make such code a worthy citizen of the .NET world.
482
Part V: Refactoring Applied
79796c16.qxd:WroxPro 2/25/08 9:09 AM Page 482
Unleash Refactor!
Thanks to its partnership with Microsoft, Developer Express has released Refactor! for VB, a freeand scaled, down version of their Refactor! Pro product. The free version has several limitations:
❑ It supports only the Visual Basic programming language.
❑ It works only with Visual Studio 2005 and 2008, while the Pro version works with VisualStudio 2002 and 2003 as well.
The Pro version lets you extend the capability of the tool and define your own refactorings; it hascode-analysis capabilities, more refactoring options, and so on.
There is one more limitation. Configuration options are visible only in the Pro version of the toolby default. However, with a little bit of tweaking, you can make a DevExpress menu item appearand obtain access to the Refactor! Options window even with the VB version!
Configuration options let you customize certain behaviors of Refactor! so they better suit your per-sonal programming style. For example, you can change the keyboard shortcut keys used for one-keyrefactoring, access different visual options, like color or contrast, and more importantly, customizetool behavior when performing different refactorings. Feel free to explore other options and set themto your liking.
So, to activate this hidden feature, you need to change a certain setting in the Windows Registry.
Depending on the selection you made during the installation process, this setting is located in oneof two places listed in Table A-1.
79796bapp01.qxd:WroxPro 2/22/08 5:01 PM Page 483
Table A-1: Registry Setting Location for Configuration Options Window andDevExpress Menu Activation
1. Use regedit to locate this entry.
2. Then find the HideMenu key. This key has the DWORD type, and the value should be set to 1when you first find the key. Change this value to 0.
3. Once you apply the new setting, close Visual Studio and open it again so that the changes cantake effect.
4. Then open any VB source file for editing in order to load the Refactor! plug-in inside VisualStudio. The DevExpress menu item should appear now in the Visual Studio menu.
Note that the exact location of the setting might be slightly different, depending on the exact version ofRefactor! you have installed.
You can open the Options window and experiment with different settings that let you further customizethe tool. You can see the DevExpress top menu item inside the Visual Studio menu and the DevExpressOptions window in Figure A-1.
Changes in the configuration you apply in the Options window are persisted in different .ini files.Refactor! places these files inside your User Profile folder. As another option to tweak Refactor!, thesefiles can be edited manually. You can find these files in the following folder:
C:\Documents and Settings\YourProfile\Application Data\CodeRush for VS.NET\2.5\Settings\
where C: is the drive on your computer where the Windows operating system is installed, and whereYourProfile corresponds to the username under which you are logged in.
This way you will be able to use many options that are available only in the Pro version of the tool and customize Refactor! in accordance to your personal preferences.
Refactor! Installation Option Windows Registry Location
Refactor! is available to any user. HKEY_LOCAL_MACHINE\SOFTWARE\DeveloperExpress\CodeRush for VS\2.5
Refactor! is available only to thecurrent user.
HKEY_CURRENT_USER \SOFTWARE\DeveloperExpress\CodeRush for VS\2.5
484
Appendix A: Unleash Refactor!
79796bapp01.qxd:WroxPro 2/22/08 5:01 PM Page 484
Figure A-1
485
Appendix A: Unleash Refactor!
79796bapp01.qxd:WroxPro 2/22/08 5:01 PM Page 485
79796bapp01.qxd:WroxPro 2/22/08 5:01 PM Page 486
Rent-a-Wheels PrototypeInternals and Intricacies
This appendix extends the scenario from Chapter 4 by investigating the implementation of theRent-a-Wheels application prototype in more detail. It continues recounting the conversationbetween Tim, the novice VB programmer who worked up the first version of the prototype, andme, focusing on two main areas of the prototype application:
❑ Relevant event-handling code
❑ Relevant form class code
Remember, this code is not meant to illustrate good design principles, but quite the opposite. It is hereto illustrate a non–object-oriented, legacy VB approach to Rapid Application Development (RAD).Try to think about better ways to implement this application. Compare your ideas with the solutionsthat I offer throughout the book while refactoring and improving the Rent-a-Wheels prototype.
Hand Over Button Click Event-Handling Code
When I asked Tim about the Hand Over button click event, he said, “In this case, there is no inter-mediate form, since no additional data is necessary for this operation. Essentially, a user can justmark a vehicle as handed over to customer. This amounts to changing the value of the columnAvailable for that vehicle to the number 2 in the database. Essentially, the code is quite similar totwo routines we examined when looking into the Rent operation code. The only difference is thatsince no other form was necessary, the whole operation is performed at once, and all code is con-tained in the BtnHandOver_Click routine in the FrmFleetView class.”
79796bapp02.qxd:WroxPro 2/22/08 5:01 PM Page 487
Appendix B: Rent-a-Wheels Prototype Internals and Intricacies
488
To remind yourself what the code for the application’s Rent operation looked like, you can turn back toChapter 4, or you can download the code for the whole sample application at www.wrox.com.
We turned our attention to the Receive button next.
Receive Button Click Event-Handling CodeWhen discussing the Receive button, Tim said, “Not too much is a surprise here. This code is quite simi-lar to that hiding under the Rent button, too. The only difference is that the FrmRcv is displayed and notthe FrmRt.”
Tim showed me FrmRt, the Receive a Vehicle form, which is shown in Figure B-1.
Figure B-1
“This form is used to enter Mileage and Tank level data after vehicle reception. The code for the Receivebutton click event is very similar to code for the Rent button click event: this update sets Available to 3,and Mileage and Tank level data are saved also.”
I then said to Tim that it seemed he had used the same approach throughout the application.
Tim said, “The similarity in code is not a pure coincidence. I was able to finish this application so quicklybecause I used a lot of copy-paste with the code.”
Charge Button Click Event-Handling CodeTim continued his thought by turning to the Charge button: “As a matter of fact, there is no need to goover Charge button click event-handling code in detail. Again, similar to button Hand Over, the updateis executed right away without displaying an intermediate form.”
I said, “I guess that the SQL query code is a bit different, isn’t it?”
Tim said, “Yes, the only important difference, compared to BtnHandOver_Click routine, is in the SQLcode.” He showed me the following code:
strSql = “Update Vehicle “ + _“Set Available = 0, “ + _“Mileage = 0,” + _“Tank = 0, “ + _
79796bapp02.qxd:WroxPro 2/22/08 5:01 PM Page 488
Appendix B: Rent-a-Wheels Prototype Internals and Intricacies
489
“CustomerFirstName = ‘’,” + _“CustomerLastName = ‘’,” + _“CustomerDocNumber = ‘’,” + _“CustomerDocType = ‘’ “ + _“WHERE LicensePlate = @SelectedLP”
“Column Available is set to 0, so that the vehicle can again be considered available when the receptionistsearches for vehicle a to rent. The rest of the data related to a previous rental is simply erased. In essence,a single update over the Vehicle table is executed.”
I said, “By the way, StringBuilder is more efficient at string concatenation than simple String.”
Next, we turned our attention to the Change Branch button.
Change Branch Button Click Event-Handling Code
Tim said, “The code here is similar to the rest of the buttons where an intermediate form is displayedfirst. The difference is that we need one more piece of data transferred from the grid in the main form to the intermediate form — the current branch. After that, the FrmChangeBranch is shown.”
The Change Branch click event code is shown in Listing B-1.
Listing B-1: BtnChangeBranch_Click from Receive Form Event-Handling Routine
Private Sub BtnChangeBranch_Click( _ByVal sender As System.Object, ByVal e As System.EventArgs) _Handles BtnChangeBranch.Click
‘Check that user has made a selectionIf DGridFleetView.SelectedRows.Count > 0 Then
‘Read value from first cell as Vehicle Id and set it‘as a value of TxtLP in FrmRentFrmChangeBranch.TxtLP.Text = _ DGridFleetView.SelectedRows.Item(0).Cells.Item(0).Value ‘Read value from fifth cell as Vehicle Id and set it‘as a value of TxtCurrentBranch Text prop in FrmRentFrmChangeBranch.TxtCurrentBranch.Text = _ DGridFleetView.SelectedRows.Item(0).Cells.Item(5).Value ‘Show change branch formFrmChangeBranch.Show()
Else‘Warn user if no selection made in table and exitMsgBox(“Please select vehicle first!”)
End IfEnd Sub
After we examined the event-handling code, Tim said, “The real code is in FrmChangeBranch. First,take a look at the form’s appearance.” He then showed me the Change Branch form, which is shown inFigure B-2.
79796bapp02.qxd:WroxPro 2/22/08 5:01 PM Page 489
Appendix B: Rent-a-Wheels Prototype Internals and Intricacies
490
Figure B-2
“The form opens with the Vehicle License Plate and Current Branch values set. A new branch can beselected from the combo box in the form. So, users can change the branch or close the form by pressingthe Cancel button.
“In this case, I think it’s best if I take you through the complete code for this class. We can start off by examining the class declaration and then look at the methods in detail.” Tim then showed me theFrmChangeBranch class code shown in Listing B-2.
Listing B-2: Class FrmChangeBranch
‘ Importing data provider namespacesImports System.Data Imports System.Data.SqlClient
Public Class FrmChangeBranch
‘Maintain BranchId - BranchName relation in HashtablePrivate branchIdTable
‘Close form when button cancel pressedPrivate Sub BtnCancel_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles BtnCancel.Click
‘close form Me.Close()
End Sub
‘Form Load event handling routinePrivate Sub FrmChangeBranch_Load( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load
‘method body (see next code listing)End Sub
‘Button Change Branch Click event, event handling routinePrivate Sub BtnChangeBranch_Click( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles BtnChangeBranch.Click
‘method body … End Sub
End Class End Class
79796bapp02.qxd:WroxPro 2/22/08 5:01 PM Page 490
Appendix B: Rent-a-Wheels Prototype Internals and Intricacies
491
Tim noted, “Just like the rest of the classes in the project, this class imports the data provider namespaces,as observed in import section of the class. There are three event-handling routines. BtnCancel_Clickcloses the form. The Form Load event handler is used to retrieve the data and fill the Branch combo but-ton from the Branch table. We can examine this code, and the code for the BtnChangeBranch_Clickroutine, after we’re done looking at the class code.”
I asked what the purpose of the branchIdTable hashtable field in the form was.
Tim said, “This is necessary in order to relate the name of the branch, visible to the user in the Branchcombo box text, with the branch ID, necessary for the database update statement, as can be observed inthe BtnChangeBranch_Click code. Take a look at the FrmChangeBranch_Load code, where thecombo box and the branchIdTable are filled.”
In VB 6, the combo box control had a data item property you could use for this purpose, but it was madeobsolete with VB .NET. ComboBox in the .NET Framework has a ValueMember property that fulfillsa similar purpose.
The Load event in the Change Branch code is shown in Listing B-3.
Listing B-3: FrmChangeBranch_Load Event-Handling Routine
Private Sub FrmChangeBranch_Load( _ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles MyBase.Load
‘Declare variablesDim oCnDim oCmdDim oReaderOn Error GoTo ErrorHandlerbranchIdTable = New Hashtable‘Create SqlConnectionoCn = New SqlConnection(“Data Source=TESLA-DAN;” + _“Initial Catalog=RENTAWHEELS;User ID=sa”)oCmd = New SqlCommand‘Create Sql String with parameter @SelectedLPstrSql = “Select * from Branch”‘open connectionoCn.Open()‘Set connection to commandoCmd.Connection = oCn‘set Sql string to command objectoCmd.CommandText = strSql‘SqlDataReader retrieves the data to fill branch combo ‘and branch id-name hashtable‘Execute command:oReader = oCmd.ExecuteReader() While oReader.Read
CboBranch.Items.Add(oReader(1)) ‘Add Id object to table with name as key branchIdTable.Add(oReader(1), oReader(0))
End While CboBranch.SelectedIndex = 0
Continued
79796bapp02.qxd:WroxPro 2/22/08 5:01 PM Page 491
Appendix B: Rent-a-Wheels Prototype Internals and Intricacies
492
Listing B-3: FrmChangeBranch_Load Event-Handling Routine (continued)
‘close readeroReader.Close()‘close connectionoCn.Close()‘destroy objectsoCmd = NothingoCn = NothingoReader = NothingExit Sub
ErrorHandler:MsgBox(“A problem occurred “ + _“and the application can not recover! “ + _“Please contact the technical support.”)Err.Clear()
End Sub
Tim said, “I’m sure this FrmChangeBranch_Load routine code looks very familiar. It connects to data-base and executes the query that retrieves the data from the Branch table, and then this data is used tofill combo box Branch and the table that maintains the branch name-ID relationship. We’ll need the IDwhen executing an update in the BtnChangeBranch_Click routine, which we can take a look at now.”
I said, “In .NET you have another, more object-oriented form of exception handling, called structuredexception handling, at your disposal. It consists in writing Try-Catch-Finally blocks around the codeyou want to be under managed error handling.”
Listing B-4 shows the Change Branch button click event-handling code.
Listing B-4: BtnChangeBranch_Click from FrmChangeBranch Form Event-Handling Routine
Private Sub BtnChangeBranch_Click( _ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles BtnChangeBranch.Click
‘Declare variablesDim oCnDim oCmd‘double-check with userIf MsgBox(“Are you sure?”, MsgBoxStyle.OKCancel) Then
On Error GoTo ErrorHandler‘Create SqlConnectionoCn = New SqlConnection(“Data Source=TESLA-DAN;” + _“Initial Catalog=RENTAWHEELS;User ID=sa”)oCmd = New SqlCommand‘Create Sql String with parameter @SelectedLPstrSql = “Update Vehicle “ + _
“Set BranchId = @BranchId “ + _“WHERE LicensePlate = @SelectedLP”
‘open connectionoCn.Open()‘Set connection to command
79796bapp02.qxd:WroxPro 2/22/08 5:01 PM Page 492
Appendix B: Rent-a-Wheels Prototype Internals and Intricacies
493
Listing B-4: BtnChangeBranch_Click from FrmChangeBranch Form Event-Handling Routine (continued)
oCmd.Connection = oCn‘set Sql string to command objectoCmd.CommandText = strSql‘Branch name is used to obtain branch id for database update‘Add parameter to commandoCmd.Parameters.AddWithValue( _ “@BranchId”, branchIdTable.Item(CboBranch.Text)) oCmd.Parameters.AddWithValue( _“@SelectedLP”, TxtLP.Text)‘execute commandoCmd.ExecuteNonQuery()‘close connectionoCn.Close()‘destroy objectsoCmd = NothingoCn = NothingExit Sub
ErrorHandler:MsgBox(“A problem occurred “ + _“and the application can not recover! “ + _“Please contact the technical support.”)Err.Clear()
End IfEnd Sub
Tim said, “As you can see, BtnChangeBranch_Click contains the usual database-related code. A connec-tion is created and opened, an SQL statement is constructed, a command and parameters are created, andthe command is executed. There is some cleanup code and usual error-handling code. The only curiosityhere is a private field, a hashtable used to maintain relation between the branch ID and branch name. Thisway a branch ID is retrieved by using the branch name that a user selected in the Branch combo box, andthe database table update can be executed.”
To Maintenance and From MaintenanceButton Click Event Code
The next logical place for us to turn was to the two buttons left on the right-hand side, To Maintenanceand From Maintenance. Tim said, “When they are pressed, the update is executed over the databasedirectly, very much as in the Hand Over and Charge click events we already discussed. The only piece of code worth examining is the SQL command construction code.” He then showed me the code forBtnToMaintenance_Click.
‘Create Sql String with parameter @SelectedLPstrSql = “Update Vehicle “ + _
“Set Operational = 1 “ + _“WHERE LicensePlate = @SelectedLP”
79796bapp02.qxd:WroxPro 2/22/08 5:01 PM Page 493
Appendix B: Rent-a-Wheels Prototype Internals and Intricacies
494
“Basically, for the selected vehicle, the Operational column is set to value 1. The query forBtnFromMaintenance_Click does exactly the opposite — it sets the value of column Operational to 0.”
‘Create Sql String with parameter @SelectedLPstrSql = “Update Vehicle “ + _
“Set Operational = 0 “ + _“WHERE LicensePlate = @SelectedLP”
With the examination of code related to buttons on the right-hand side of the Fleet View form complete,we decided to next take a look at the Administration menu Tim added to the application.
Administer Fleet FormTim said, “The only purpose behind this menu is to display different forms used for data administration.Let’s start off by taking a look at the Vehicle Fleet Administration form.” The Vehicle Fleet Administrationform is shown in Figure B-3.
Figure B-3
“As you can probably guess, the button strip at the bottom is used to navigate through the set of vehi-cles. The button with < displays the previous record, the button > the next record, and the buttons <<and >> the first and the last vehicle entry. Pressing buttons on the right-hand side triggers databasequery execution, except when the button New is pressed. The New button’s only role is to erase contentof controls on the form. However, pressing Delete provokes elimination of the current record in thedatabase. The Save button first eliminates and then saves the current record, thus performing a kind ofupdate or insert, depending on if this particular license plate number already exists in the database ornot. If the license plate number already exists in the database, eliminating the vehicle first and theninserting the vehicle equals an update. If vehicle is new, it means it does not exist in the database, sodeleting it in a first step has no effect on the database and does not raise any exception, so this equals asimple insert. That way, I was able to simplify the user interface.”
I said, “So, basically, you hold all the records from the Vehicle table in memory, and you navigatethrough the records with navigation buttons on the button strip. When you press the Delete or Save button, you issue a statement that synchronizes the local data in memory with the database.”
Tim said, “Yes, that about sums it up. We can take a look at the code for this form in detail, starting withBtnDelete_Click, since it is really straightforward.”
79796bapp02.qxd:WroxPro 2/22/08 5:01 PM Page 494
Appendix B: Rent-a-Wheels Prototype Internals and Intricacies
495
Delete Button Click Event-Handling RoutineI indicated to Tim that I thought this must be another simple SQL delete query.
Tim said, “Yes, not much surprise here. This code looks very similar to a large portion of code we haveseen so far. A simple delete SQL query is executed against the Vehicle table in the database, using thevehicle license plate as a parameter.”
The SQL code for the delete query is shown in Listing B-5.
Listing B-5: SQL Query Inside BtnDelete_Click Event-Handling Routine
strSqlDelete = “Delete From Vehicle “ + _“Where LicensePlate = @LicensePlate”
New Button Click Event-Handling RoutineAfter completing our look at the Delete button code, we moved on to the New button. Tim said, “Thisbutton is rather obvious also. Components are cleared of previous values.” The code for the New buttonclick event is shown in Listing B-6.
Listing B-6: BtnNew_Click from FrmFlt Form Event-Handling Routine
Private Sub BtnNew_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles BtnNew.Click
TxtLP.Text = “”CboBranch.Text = “”CboModel.Text = “”
End Sub
Reload Button Click Event-Handling RoutineTim indicated that another very simple routine was the one for the Reload button event handling. “Allit does is call the FrmFleet_Load routine.” The Reload Button event-handling routine is illustrated inListing B-7.
Listing B-7: BtnReload_Click from FrmFlt Form Event-Handling Routine
Private Sub BtnReload_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles BtnReload.Click
FrmFleet_Load(Nothing, Nothing)End Sub
Then Tim said, “This all leads us to the Form Load routine. It is significantly longer when compared toroutines we have inspected so far. Let’s take a look at it.”
Form Load Event-Handling RoutineTim went on, “The code in this method is again a database-related code. The only real difference here isthat there are three queries executed instead of one. The first two queries are concerned with filling two
79796bapp02.qxd:WroxPro 2/22/08 5:01 PM Page 495
Appendix B: Rent-a-Wheels Prototype Internals and Intricacies
496
combo boxes, Branch and Model, with data from Branch and Model tables. Two fields of type Hashtableare used to maintain relation between the text that goes into the combo boxes and is presented to the user(Branch and Model names) and the Branch and Model IDs needed for update and delete.
“The last query is used to retrieve data from the Vehicle table joined with other, lookup tables. This data isdisplayed to users as they navigate through the Vehicle table records. The resulting data table is assignedto a private field dtVehicles.”
You can see a rather large Fleet Form Load event in Listing B-8.
Listing B-8: FrmFleet_Load Event-Handling Routine
Private Sub FrmFleet_Load(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles MyBase.Load
‘Declare variablesDim oCnOn Error GoTo ErrorHandler‘Create SqlConnectionoCn = New SqlConnection(“Data Source=TESLA-DAN;” + _“Initial Catalog=RENTAWHEELS;User ID=sa”)‘open connectionoCn.Open()
‘LOAD BRANCH COMBO->Dim oCmdBranchDim oBranchReaderbranchIdTable = New HashtableoCmdBranch = New SqlCommand‘Create Sql String with parameter @SelectedLPstrSqlBranch = “Select * from Branch”‘Set connection to commandoCmdBranch.Connection = oCn‘set Sql string to command objectoCmdBranch.CommandText = strSqlBranch‘execute commandoBranchReader = oCmdBranch.ExecuteReader()‘Loop over records from Branch tableWhile oBranchReader.Read
‘Add Branch Name to Branch comboCboBranch.Items.Add(oBranchReader(1)) ‘Add Id object to hashtable with name as keybranchIdTable.Add(oBranchReader(1), oBranchReader(0))
End While‘close readeroBranchReader.Close()‘destroy objectsoCmdBranch = NothingoBranchReader = Nothing‘END LOAD BRANCH COMBO->
‘LOAD MODEL COMBO->Dim oCmdModelDim oModelReader
79796bapp02.qxd:WroxPro 2/22/08 5:01 PM Page 496
Appendix B: Rent-a-Wheels Prototype Internals and Intricacies
497
Listing B-8: FrmFleet_Load Event-Handling Routine (continued)
modelIdTable = New HashtableoCmdModel = New SqlCommand‘Create Sql String with parameter @SelectedLPstrSqlModel = “Select * from Model”‘Set connection to commandoCmdModel.Connection = oCn‘set Sql string to command objectoCmdModel.CommandText = strSqlModel‘execute commandoModelReader = oCmdModel.ExecuteReader()‘Loop over records from Model tableWhile oModelReader.Read
CboModel.Items.Add(oModelReader(1)) ‘Add Id object to table with name as keymodelIdTable.Add(oModelReader(1), oModelReader(0))
End While‘close readeroModelReader.Close()‘destroy objectsoCmdBranch = NothingoModelReader = Nothing‘END LOAD MODEL COMBO->
‘LOAD VEHICLES TABLE->Dim oAdapter‘create data setdsVehicles = New DataSet‘create adapteroAdapter = New SqlDataAdapter‘Create SqlConnectionoCn = New SqlConnection(“Data Source=TESLA-DAN;” + _“Initial Catalog=RENTAWHEELS;User ID=sa”)oCmd = New SqlCommand‘Build SQL query that joins vehicle with rest of tablesstrSql = “Select Vehicle.LicensePlate AS LicensePlate, “ + _ “Branch.Name as BranchName, “ + _ “Model.Name as ModelName “ + _ “from Vehicle “ + _ “Inner Join Branch On “ + _ “Vehicle.BranchId = Branch.BranchId “ + _ “Inner Join Model On “ + _ “Vehicle.ModelId = Model.ModelId” ‘open connectionoCn.Open()‘Set connection to commandoCmd.Connection = oCn‘set Sql string to command objectoCmd.CommandText = strSql‘execute commandoAdapter.SelectCommand = oCmd‘fill DataSetoAdapter.Fill(dsVehicles)
Continued
79796bapp02.qxd:WroxPro 2/22/08 5:01 PM Page 497
Appendix B: Rent-a-Wheels Prototype Internals and Intricacies
498
Listing B-8: FrmFleet_Load Event-Handling Routine (continued)
‘destroy objectsoCmd = NothingoAdapter = Nothing‘Assign Vehicles Sql Table object to fielddtVehicles = dsVehicles.Tables.Item(0) If (dtVehicles.Rows.Count > 0) Then
drRow = dtVehicles.Rows(0)TxtLP.Text = drRow.Item(“LicensePlate”)CboBranch.Text = drRow.Item(“BranchName”)CboModel.Text = drRow.Item(“ModelName”)currentRowIndex = 0
End If‘close connectionoCn.Close()oCn = NothingExit Sub
ErrorHandler:MsgBox(“A problem occurred “ + _“and the application can not recover! “ + _“Please contact the technical support.”)Err.Clear()
End Sub
Administer Fleet Form Class Code: FieldsTim moved on to discuss the fields and class declaration next. “Again, this class imports SQL Data Providernamespaces. There are two hashtables, branchIdTable and modelIdTable, related to two combo boxes,Branch and Model. As you have already seen in the Change Branch form, the purpose of these two tables is to maintain relation between the name displayed in the combo box and the ID of the branch or model.
“A new element here is a DataTable named dtVehicles. It represents the Vehicle table from theRENTAWHEELS database. As the user navigates between vehicles on the form, a single row from the table is displayed. The integer currentRowIndex serves to conserve an index of the currently displayed row.”
Then Tim showed me the Fleet Form class code in Listing B-9.
Listing B-9: Fleet Form Class Fields
‘Importing data provider namespacesImports System.Data Imports System.Data.SqlClient
Public Class FrmFlt
‘Maintain BranchId - BranchName relation in Hashtable Private branchIdTable ‘Maintain ModelId - ModelName relation in Hashtable Private modelIdTable
79796bapp02.qxd:WroxPro 2/22/08 5:01 PM Page 498
Appendix B: Rent-a-Wheels Prototype Internals and Intricacies
499
Listing B-9: Fleet Form Class Fields (continued)
‘table VehiclesPrivate dtVehicles ‘Index of displayed rowPrivate currentRowIndex
‘rest of class code – methodsEnd Class
When we had finished looking at this code, Tim then said, “A look at the Left button event-handlingcode will show how this currentRowIndex number is used when users navigate through the Vehicletable records.”
Left Button Click Event-Handling RoutineTim said, “When someone presses BtnLeft (the one with the < symbol), the current row index getsdecremented and then used to obtain a row from the Vehicle table. This row is then used to fill controlson the form with data. In this way, the effect of moving through the records in the table using the navi-gation button on the bottom of the form is achieved.” The Left button click event-handling code isshown in Listing B-10.
Listing B-10: BtnLeft_Click Event-Handling Routine
Private Sub BtnLeft_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles BtnLeft.Click
‘Check actual row not first already ‘and table vehicle contains dataIf (currentRowIndex >= 1 And _ dtVehicles.Rows.Count > 0) Then
‘Decrement currentRowIndexcurrentRowIndex -= 1 ‘Obtain actual row from table using currentRowIndexdrRow = dtVehicles.Rows(currentRowIndex) ‘Fill controls with data from current rowTxtLP.Text = drRow.Item(“LicensePlate”) CboBranch.Text = drRow.Item(“BranchName”) CboModel.Text = drRow.Item(“ModelName”)
End IfEnd Sub
Tim then showed that similar code is present in the rest of the navigation buttons. “The Right buttonincrements the current row index, the First button sets the current row index to zero, and the Last button sets the current row index to row count minus one.”
Private Sub BtnFirst_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles BtnFirst.Click
If (dtVehicles.Rows.Count > 0) Then‘Current row index set to zerocurrentRowIndex = 0 drRow = dtVehicles.Rows(currentRowIndex)TxtLP.Text = drRow.Item(“LicensePlate”)
79796bapp02.qxd:WroxPro 2/22/08 5:01 PM Page 499
Appendix B: Rent-a-Wheels Prototype Internals and Intricacies
500
CboBranch.Text = drRow.Item(“BranchName”)CboModel.Text = drRow.Item(“ModelName”)
End IfEnd Sub
Private Sub BtnRight_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles BtnRight.Click
If (dtVehicles.Rows.Count > currentRowIndex + 1) Then‘Current row index incrementedcurrentRowIndex += 1 drRow = dtVehicles.Rows(currentRowIndex)TxtLP.Text = drRow.Item(“LicensePlate”)CboBranch.Text = drRow.Item(“BranchName”)CboModel.Text = drRow.Item(“ModelName”)
End IfEnd Sub
Private Sub BtnLast_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles BtnLast.Click
If (dtVehicles.Rows.Count > 0) Then‘Current row index set to row count -1currentRowIndex = dtVehicles.Rows.Count – 1 drRow = dtVehicles.Rows(currentRowIndex)TxtLP.Text = drRow.Item(“LicensePlate”)CboBranch.Text = drRow.Item(“BranchName”)CboModel.Text = drRow.Item(“ModelName”)
End IfEnd Sub
Then Tim said, “The only button left to inspect is the Save button. The code executed when this button isactivated is probably the most complicated in the application so far.”
Save Button Click Event-Handling RoutineTim continued, “There is a single button to perform both insert and update. However, I am not able todistinguish between the update when a vehicle already exists in database and the insert when you aredealing with a completely new vehicle. To resolve this, two steps are always performed:
1. A record is deleted based on a primary key, license plate number.
2. A record with the same primary key (license plate number) is inserted.”
Tim then showed me the Save button click event-handling routine in Listing B-11.
Listing B-11: BtnSave_Click Event-Handling Routine
Private Sub BtnSave_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles BtnSave.Click
‘Declare variablesDim oCnDim oCmdDeleteDim oCmdInsertDim oTrxOn Error GoTo ErrorHandler
79796bapp02.qxd:WroxPro 2/22/08 5:01 PM Page 500
Appendix B: Rent-a-Wheels Prototype Internals and Intricacies
501
Listing B-11: BtnSave_Click Event-Handling Routine (continued)
‘Create SqlConnectionoCn = New SqlConnection(“Data Source=TESLA-DAN;” + _“Initial Catalog=RENTAWHEELS;User ID=sa”)‘Create two commands, one for delete and one for insertoCmdDelete = New SqlCommand oCmdInsert = New SqlCommand ‘Create Sql String for deletestrSqlDelete = “Delete From Vehicle “ + _ “Where LicensePlate = @LicensePlate” ‘Create Sql String for insertstrSqlInsert = “Insert Into Vehicle “ + _ “(LicensePlate, ModelId,BranchId) “ + _ “Values(@LicensePlate, @ModelId, @BranchId)“ ‘add parameter for delete oCmdDelete.Parameters.AddWithValue( _ “@LicensePlate”, TxtLP.Text) ‘add parameters for insertoCmdInsert.Parameters.AddWithValue( _ “@LicensePlate”, TxtLP.Text) oCmdInsert.Parameters.AddWithValue( _ “@ModelId”, modelIdTable(CboModel.Text)) oCmdInsert.Parameters.AddWithValue( _ “@BranchId”, branchIdTable(CboBranch.Text)) ‘open connectionoCn.Open()‘Set connection to commandoCmdDelete.Connection = oCnoCmdInsert.Connection = oCn‘set Sql string to command objectoCmdDelete.CommandText = strSqlDeleteoCmdInsert.CommandText = strSqlInsert‘start transactionoTrx = oCn.BeginTransaction() ‘enlist commands with transactionoCmdDelete.Transaction = oTrx oCmdInsert.Transaction = oTrx ‘execute command: first delete and then insert recordoCmdDelete.ExecuteNonQuery() oCmdInsert.ExecuteNonQuery() oTrx.Commit() ‘close connectionoCn.Close()‘destroy objectsoCmdDelete = NothingoCmdInsert = NothingoCn = NothingFrmFleet_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
79796bapp02.qxd:WroxPro 2/22/08 5:01 PM Page 501
Appendix B: Rent-a-Wheels Prototype Internals and Intricacies
502
“This in the end has same effect as performing SQL update. In the case of an insert, erasing non-existingrecords will not provoke any problems, since I am not checking for the number of records that wereaffected with a delete command. And just in case some problem occurs between the executions of thesetwo separate queries, they are executed under the same transaction, thus making sure that the partialexecution will not happen. That way, we can be at ease that the record will not be deleted unless theinsert is executed also.
“In the Vehicle table, the license plate number serves as the primary key. In other tables, the primary keyis autogenerated by the database itself upon each insert. This makes it possible to use SQL insert andupdate in the rest of the administration forms, because the ID is placed in a read-only text box. If the IDis present, this means the record already exists, and we need to execute update. If the ID is not present,the user has already pressed the New button that cleared the form and is trying to insert a new record,meaning that an insert command should be issued to the database. The other administration forms likeModel, Category, and Branch are based on the same model.”
Display Button Click Event-HandlingRoutine
Having finished our discussion about the administration forms, we returned to the last piece of func-tionality we needed to discuss on the application’s main form, the Display button and filtering capabili-ties it made available to users.
Tim said, “At the bottom of the main form, the user can set different search options and in that way filter the vehicles that are going to be displayed. The table is refreshed by pressing the Display button.Different combo boxes in the vehicle display filter group box are converted as part of a Where clause in anSQL Select query that is used to retrieve data from the RENTAWHEELS database. Additional complexitycomes from the fact that certain filters can be applied or not, depending on the user’s decision. For exam-ple, if the Category combo box is left with the value All, this means that Category should not form part ofthe Where clause in an SQL statement. Since there are four combo boxes, and each can form part of a Whereclause or not, there is a need to dynamically construct this query. Take a look at the code now.” With that,Tim showed me the event-handling code for the Display button that you can see in Listing B-12.
Listing B-12: BtnDisplay_Click Event-Handling Routine
Private Sub BtnDisplay_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles BtnDisplay.Click
‘Declare variablesDim oCnDim oCmdDim oAdapterDim oRdDim strSql As StringOn Error GoTo ErrorHandler‘clear grid of previous resultMe.DGridFleetView.Rows.Clear() ‘create adapteroAdapter = New SqlDataAdapter‘Create SqlConnection
79796bapp02.qxd:WroxPro 2/22/08 5:01 PM Page 502
Appendix B: Rent-a-Wheels Prototype Internals and Intricacies
503
Listing B-12: BtnDisplay_Click Event-Handling Routine (continued)
oCn = New SqlConnection(“Data Source=TESLA-DAN;” + _“Initial Catalog=RENTAWHEELS;User ID=sa”)oCmd = New SqlCommand‘Construct SELECT part of SQL Query strSql = “Select Vehicle.LicensePlate as LicensePlate,” + _ “Category.Name as CategoryName,” + _ “Vehicle.Available as Available,” + _ “Vehicle.Operational as Operational,” + _ “Model.Name as ModelName,” + _ “Branch.Name as BranchName,” + _ “Category.DailyPrice as DailyPrice,” + _ “Category.WeeklyPrice as WeeklyPrice,” + _ “Category.MonthlyPrice as MonthlyPrice,” + _ “Vehicle.Mileage as Mileage,” + _ “Vehicle.Tank as Tank,” + _ “Vehicle.CustomerFirstName as FirstName,” + _ “Vehicle.CustomerLastName as LastName,” + _ “Vehicle.CustomerDocNumber as DocNumber,” + _ “Vehicle.CustomerDocType as DocType “ + _ “from Vehicle “ + _ ‘Construct INNER JOIN part of SQL Query “Inner Join Model ON “ + _ “Vehicle.ModelId = Model.ModelId “ + _ “Inner Join Branch ON “ + _ “Vehicle.BranchId = Branch.BranchId “ + _ “Inner Join Category ON “ + _ “Model.CategoryId = Category.CategoryId” ‘Check that at least one filter applied If (CboAvailable.Text <> “All” Or _
CboBranch.Text <> “All” Or _ CboCategory.Text <> “All” Or _ CboOperational.Text <> “All”) Then strSql += “ Where “ ‘Concatenate “Available” conditionIf (CboAvailable.Text <> “All”) Then
strSql += “Vehicle.Available = @Available And “ ‘Convert “Available” textual value to integerSelect Case CboAvailable.Text
Case “Available” available = 0
Case “Hand Over” available = 1
Case “Rented” available = 2
Case “Charge” available = 3
End Select ‘Add “Available” command parameteroCmd.Parameters.AddWithValue( _ “@Available”, available)
End If‘Concatenate “Branch” condition
Continued
79796bapp02.qxd:WroxPro 2/22/08 5:01 PM Page 503
Appendix B: Rent-a-Wheels Prototype Internals and Intricacies
504
Listing B-12: BtnDisplay_Click Event-Handling Routine (continued)
If CboBranch.Text <> “All” Then strSql += “Vehicle.BranchId = @BranchId And “ oCmd.Parameters.AddWithValue( _ “@BranchId”, Me.branchIdTable(CboBranch.Text))
End If‘Concatenate “Model” conditionIf CboCategory.Text <> “All” Then
strSql += “Model.CategoryId = @CategoryId And “ oCmd.Parameters.AddWithValue(“@CategoryId”, _ Me.categoryIdTable(CboCategory.Text))
End If‘Concatenate “Operational” conditionIf CboOperational.Text <> “All” Then
strSql += “Vehicle.Operational = @Operational And “ ‘Convert “Operational” textual value to integerSelect Case CboOperational.Text
Case “In Operation” operational = 0
Case “In Maintenance” operational = 1
End Select‘ Add “Operational” command parameteroCmd.Parameters.AddWithValue( _ “@Operational”, operational)
End If‘Remove trailing [ AND ] from SQL statementstrSql = strSql.Substring(0, strSql.Length - 5)
End If‘open connectionoCn.Open()‘Set connection to commandoCmd.Connection = oCn‘set Sql string to command objectoCmd.CommandText = strSql‘execute commandoRd = oCmd.ExecuteReader() ‘Fill Combo Categories - construct string array ‘to fill single row in gridWhile oRd.Read
Dim row As String() = {oRd(“LicensePlate”), _ oRd(“CategoryName”), _ ‘Convert “Available” integer value to stringAvailableText(oRd(“Available”)), _ ‘Convert “Operational” integer value to stringIIf(oRd(“Operational”) = 0, _ “In Operation”, “In Maintenance”), _ ‘Add rest of values to string arrayoRd(“ModelName”), _ oRd(“BranchName”), _ oRd(“DailyPrice”), _ oRd(“WeeklyPrice”), _ oRd(“MonthlyPrice”), _
79796bapp02.qxd:WroxPro 2/22/08 5:01 PM Page 504
Appendix B: Rent-a-Wheels Prototype Internals and Intricacies
505
Listing B-12: BtnDisplay_Click Event-Handling Routine (continued)
oRd(“Mileage”), _ oRd(“Tank”), _ ‘Add rest of values to string array ‘while checking for database null valuesIIf(IsDBNull(oRd(“FirstName”)), “ “, oRd(“FirstName”)), _ IIf(IsDBNull(oRd(“LastName”)), “ “, oRd(“LastName”)), _ IIf(IsDBNull(oRd(“DocNumber”)), “ “, oRd(“DocNumber”)), _ IIf(IsDBNull(oRd(“DocType”)), “ “, oRd(“DocType”)) _ }‘Add string array to grid as single row valuesMe.DGridFleetView.Rows.Add(row)
End While‘close readeroRd.Close()oCn.Close()‘destroy objectsoRd = NothingoCmd = NothingoCn = NothingExit Sub
ErrorHandler:MsgBox(“A problem occurred “ + _“and the application can not recover! “ + _“Please contact the technical support.”)Err.Clear()
End Sub
“The user can choose not to apply filters, to apply all filters, or to apply just any number of filters. Eachfilter is transformed into a single condition in the Where part of an SQL query. Because of this, we needto construct the query dynamically. If no filter is applied, we do not need to add the Where keywordafter the Select part of the query. If there is a single condition, we need to add Where to the SQL com-mand. Filters are exclusive, and it means they need to be concatenated using the AND keyword. Since wecannot be sure which filter is the last and so as not to add any more ANDs after constructing the query,the last AND keyword is simply removed from the SQL string.”
I said, “So, while you build the Where part of the query, each time you add a condition, for example,Vehicle.Available = @Available, you always add an AND keyword to the end of the condition.This way you always end up with one AND keyword too many.”
Tim said, “Exactly. String AND plus two blank spaces amounts to a length of five; this can be seen in the line
strSql = strSql.Substring(0, strSql.Length - 5)
“This way the trailing AND is removed from the SQL query code.
“Now, since we use integers to represent the Available and Operational state in the database, we need totranslate these codes to the user. In the case of Operational, this is done directly, by using the IIf function:
IIf(oRd(“Operational”) = 0, “In Operation”, “In Maintenance”)
79796bapp02.qxd:WroxPro 2/22/08 5:01 PM Page 505
Appendix B: Rent-a-Wheels Prototype Internals and Intricacies
506
“Here, 0 means In Operation and any other value means In Maintenance. But the only other value thatthis field can have in the database is 1. So, this corresponds to the values of the Maintenance column weexamined earlier on.
“With the available column, the situation is more complicated, because there are more than two valuesthat this column can have. Therefore, I used a separate method called AvailableText to translate theinteger value to a descriptive string value. The method is called using this code:
AvailableText(oRd(“Available”))
“Now, take a look at the method itself.”
Tim presented the AvailableText method shown in Listing B-13.
Listing B-13: AvailableText Method Used to Translate Integer Values of AvailableColumn to Descriptive String Values
Private Function AvailableText(ByVal available As Integer) _As String
Select Case availableCase 0
strAvailable = “Available”Case 1
strAvailable = “Hand Over”Case 2
strAvailable = “Rented”Case 3
strAvailable = “Charge”End SelectReturn strAvailable
End Function
“That is how the dynamic search SQL query is constructed and used to help the user display only thevehicles he is interested in. That way we have given an efficient tool to a user and optimized the timeused to perform each operation.”
SummaryThis more or less wraps up all the interesting details of the VB code in the prototype Rent-a-Wheelsapplication constructed for example purposes for this book. I didn’t go into the detail a of each andevery method, but you’ve had a chance to inspect all that’s of major interest, and certainly all you needto see of this application so it can serve as a test case for the refactoring demonstrated in the book.
Once again, please remember this code is presented as an illustration of legacy VB approach to program-ming. It is poorly designed and it is not using any of the VB .NET object-oriented features, but this hasbeen done on purpose. Through the refactoring process, and throughout this book, you improve thiscode and in the process discover new, better ways to design your applications.
The full code for the application is available for download at the book’s web site at www.wrox.com, and I definitely encourage you to download it (if you haven’t already) and use it to follow along as you workthrough the book.
79796bapp02.qxd:WroxPro 2/22/08 5:01 PM Page 506
Inde
x
Index
Aabstract classes, 331–332Abstract Factory design pattern, 411–425abstract form inheritance, 390–391
form helper classes, 392–394access level, 181, 182
gradual reduction, 187public, 184reduce, 182–183
applicationsCalories Calculator, 19–22
BtnCalculate_Click method, 42–43BtnSave_Click method, 45–47calculating by gender, 28–29calculating ideal weight, 22–24conditional logic, 34–37DailyCaloriesRecommended, 35DistanceFromIdealWeight, 35frmCaloriesCalculator, 36–37gender-specific methods, 39–40IdealBodyWeight, 35LINQ, 448–451Patient class hierarchy, 37–38Patient class interface, narrowing, 32–34patient classes hierarchy, 40–42Patient Data class, 30–31patient history display, 52–55Patient-History XML File Format, 45PatientHistory XML Storage class, 53–55persistent data, 24–26, 44XML literals, 448
GUI-based application, 103Patient class, Gender property, 33Rent-a-Wheels
actors, 91assemblies, 402–403basic hygiene, 192–193BtnRent_Click Fleet View, 99–100
BtnRent_Click Rent Form, 100–101database model, 96–98desk receptionist interview, 89detached event handler eliminated, 192–193error handling, 169–171extract method, 240generic types, 364–369inheritance, 364–369introduction, 87LINQ, 454–464Magic Literals, 240maintenance personnel interviews, 90–91manager interview, 88–89method reorganization, 259–267namespaces, 402–403objects, 313–320Option Explicit and, 114–115Option Strict and, 132–135parking lot attendant interview, 90patterns, 434–437rename refactoring, 217–218Safe Rename refactoring, 217–218team, 96vehicle states, 93–94
reusable modules and, 209–212self-contained, 209–212
ArtinSoft Visual Basic Upgrade Companion, 469assemblies
binary reuseencapsulation, 375intellectual property, 376memory resources, 376modularity, 375multilanguage reuse, 377security, 376versioning, 376
compatibility between versions, 213namespaces, 377–381Rent-a-Wheels, 402–403
79796bindex.qxd:WroxPro 2/22/08 5:01 PM Page 507
508
assembly references
assembly references, unused, removing, 191–192Assert class, 78–79Assessment tool, 469automated refactoring tools
Refactor! (Developer Express), 60smart tags, 62
Refactor! Pro (Developer Express), 61ReSharper (JetBrains), 60–61Visual Assist X (Whole Tomato), 61
automated unit testing, 71
Bbinary reuse
encapsulation, 375modularity, 375
block scope, 184blocks
Catch, 158Finally, 161–162finally block, 155
BtnCalculate_Click, 21event-handling routine, 25patient classes hierarchy, 42–43
BtnCalculate_Click method, 24after method extraction, 29Calculate and Display Distance from Ideal Weight
segment, 28Clear Old Results segment, 27Validate User Input segment, 27
BtnRent_ClickFleet View, 99–100Rent Form, 100–101
BtnSave_Click, decomposing, 48–50
Ccalculating circumference, 223–225calculating ideal weight, 22–24calculating radius, extracting code, 229Calories Calculator application
BtnSave_Click method, 45–47DailyCaloriesRecommended, 35DistanceFromIdealWeight, 35first try, 21–22frmCaloriesCalculator, 36–37
IdealBodyWeight, 35introduction, 19–20LINQ, 448–451Patient class, 37–38
conditional logic, 34–37gender-specific methods, 39–40
Patient class interface, 32–33patient classes hierarchy, 40–42
BtnCalculate_Click method, 42–43Patient Data class, 30–31
moving methods to, 31–32patient history display, 52–55Patient-History XML File Format, 45PatientHistory XML Storage class, 53–55persistence, patient personal data, 44XML literals, 448
Calories Calculator with Ideal-Body-Weightcalculation, 23–24
Camel case, 202Catch block, 158CCW (COM callable wrapper), 467child class, 334circumference calculation, 223–225circumference length calculation code,
extracting, 226–228classes
abstract, 331–332child, 334extract, 281–284hidden, 298–320inheritance, 327–329large, 298object-oriented programming, 276–277
actor, 285attributes, 285designing, 281domain classes, 285has a phrases, 286is a phrases, 286nouns, 284duplicated, 285operations, 286–288system classes, 285
parent, 334partial, 390
client-server, 308
79796bindex.qxd:WroxPro 2/22/08 5:01 PM Page 508
509
DI (Dependency Injection) pattern
Inde
x
codeduplicated, 234–234readability, 10–11simplicity, 9–10
Code Advisor, 469code listings
BtnCalculate_Click, 21Calories Calculator first try, 21–22Calories Calculator with Ideal-Body-Weight
calculation, 23–24code smells, 7–8color, NUnit framework, 74commented code, 175comments, smell, 226compiler
conditional compilation, 472defaults, 142
component containers, 432composition, 334–337conditional compilation, 472constant refactoring, 239constructors, 276containers
component containers, 432generic, 333–334typed, 332–333
control structures, 184controls, forms, invisible, 178conversion function, inferring variable type,
123–124copy-paste as code reuse mechanism, 104copy-paste programming, 236–237CRC (class-responsibility-collaborator) cards,
288–297brainstorming sessions and, 293–297
CType function, 126–127cyclic dependencies, 384, 385–386
DDailyCaloriesRecommended method, testing, 79data class
defining for domain, 309domain logic, 309persistence logic, 310
data entry, unit testing, manual entry, 70–71
database-driven design, 102–103, 288, 299–302dead code, 173
commented code, 175definition, 174eliminating, 174–179, 176–177flavors of, 175–176smell, 174sources of, 176–179types of, 175–176unreachable code, 175unused code, 175
decision structures, 184decomposing methods, 223–234delegation, 348–350dependencies, 381
breaking dependency cycles, 383cyclic, 384, 385–386direction, 209inverting, 385
design, 7database-driven, 102–103, 299–302design rot, 7errors, quick fix, 184–185
design patternsAbstract Factory, 411–425behavioral, 409benefits of, 410consequences, 410creational, 409introduction, 408–409names, 409problem, 410solution, 410structural, 409using, 410
detached event handlers, 178Developer Express
Refactor!, 60Refactor! Pro, 61
DI (Dependency Injection) pattern, 426–434autowiring assembler, 430centralization, 433coded assembler, 430constructor-based injection, 429–430embedding, 433
79796bindex.qxd:WroxPro 2/22/08 5:01 PM Page 509
DI (Dependency Injection) pattern (continued)IoC (Inversion of Control), 426metadata assembler, 431modular architecture, 433POCO programming model, 433property-based injection, 429–430Rent-a-Wheels, 434–437support, 433testing, 433
DirectCast, 127–132domain
data class, defining, 309persistence code, 311–313
double type variables, 120downcast, 127downloads, NUnit, 72duck typing, 136–137duplicated code, 234–234
sources, 235–236duplication, inheritance and, 350–352dynamic behavior, resetting, 138dynamic typing, static typing and, 135dynamically typed languages, 109
Eelements
obsolete, 179unused, importing, 178
encapsulation, 173, 180field (Refactor!), 274objects and, 272–274
entities, relationships and, 297–298entity classes, attributes, 460Err object, 166error code
replacing with exception type, 164–169smell, 154
error filtering, 154–155error handling
introduction, 147legacy, 148–150
smell, 153.NET and, 155–156Rent-a-Wheels application, 169–171structured, 148, 150–153
unstructured and, 153
Err.Raise method, 168event-driven programming, 103–104event handlers, detached, 178event-handling blindness smell, 232exception handling, 147
hierarchies, 151upgrading, 481
exceptions, as types, 154excplicit imports, 173ExpectedException attribute, 81–82explicit imports, 187–181, 374explicit variable-type conversion, 126–127external assemblies, inferring variable type,
124–125extract class, 281–284extract method, 221–223extract namespace, 382–383extract superclass, 352–355
Ffield, move field, 291–293field refactoring, 274filtering, error filtering, 154–155Finally block, 161–162finally block, 155forms
abstract form inheritance, 390–391controls, invisible, 178helper classes, abstract form inheritance,
392–394inheritance, abstract, 392–394Inherited Form, 390–391
functional testing, 472–473harness, implementing, 473–476
functionsIsNumeric, 22type-conversion, 125–132
Ggarbage collection, 279
reference counting garbage collector, 279tracing garbage collector, 280
generic containers, 333–334, 479–481generic types, 332
Rent-a-Wheels, 364–369
510
DI (Dependency Injection) pattern (continued)
79796bindex.qxd:WroxPro 2/22/08 5:01 PM Page 510
generics, 360GetException method, 167GUI-based application, 103
Hhidden classes, 298–320Hungarian notation, 204
IIdealBodyWeight method, testing, 77–78implementation hiding, 220–223implicit narrowing conversions, 125implicit type declaration, 118implicit variable declaration, 113importing
dependencies in system, 188–181explicit imports, 187–181, 374unused elements, 178
Imports statement, 373–374inferring initial type of variable, 118–119
conversion function, 123–124inferring variable type, 122–125
external assemblies and, 124–125information hiding, 220–223inheritance, 324–327, 478–479
class inheritance, 327–329delegation, 338–342, 348–350duplication and, 350–352forms, abstract, 390–391, 392–394interface, 327–329Rent-a-Wheels, 364–369
Inherited Form, 390–391inline temp refactoring, 253–255interface inheritance, 327–329interfaces, 208
objects and, 272published, 209
inverting dependencies, 385invisible controls on forms, 178IsNumeric function, 22item templates, 143–144
JJetBrains ReSharper, 60–61JUnit framework, 72
Kkeywords
MustInherit, 331New, 276Shared, 276When, 158
Llanguages
dynamically typed, 109statically typed, 109
large class, 298late binding, 136late bound calls, 117–118legacy code upgrades, 468
static typing, 477legacy error handling, 148–150
smell, 153LINQ (Language Integrated Query),
440, 445Calories Calculator, 448–451queries, example, 446Rent-a-Wheels, 454–464
LINQ to SQL, 451–454literals
inferring variable type, 122–123magic literals, 236, 237–239
local variables, 179method extraction, 244type inference, 439–440
long method, 225Long type, 119–120looping, retaining, 250–253looping variables, 250
Mmagic literals, 236, 237–239
replacing with constant, 237–239memory, garbage collection, 279
reference counting garbage collector, 279tracing garbage collector, 280
messages, object-oriented programming,280–281
method extraction, 243local variables, 244
511
method extraction
Inde
x
79796bindex.qxd:WroxPro 2/22/08 5:01 PM Page 511
methodsambiguous parameter types, 128BtnCalculate_Click, 24decomposing, 223–234Err.Raise, 168extract, 221–223GetException, 167long, 225move, 289–290obsolete, 129overloading, 128pull up, 357–360queries as, 256–258reorganization, 258–259
Rent-a-Wheels, 259–267migration
ArtinSoft Visual Basic Upgrade Companion, 469Assessment tool, 469automation, 467Code Advisor, 469Microsoft.VisualBasic.Compatibility
assembly, 469testing harness, 472upgrade and, 466Visual Basic Upgrade Wizard, 469
misconceptions of refactoring, 12–13monolithic application, 470move field, 291–293move method, 289–290move type to file refactoring, 388–390MustInherit keyword, 331
Nnamespaces, 371–372
extract, 382–383large, 377naming guidelines and, 372nested, 372non-coherent, 378Rent-a-Wheels, 402–403root namespace name, changing, 372–373
naming guidelines, 201–202Camel case, 202capitalization styles, 202–203consistency in nouns and verbs, 205
easily pronouncable and memorizable, 205Hungarian notation, 204namespaces and, 372one word to represent one meaning, 205Pascal case, 202simple, 203–204Upper case, 202vocabulary as source for identifier names, 205
narrowing conversions, 125NCover, 84nested namespaces, 372.NET
error handling and, 155–156Option Explicit and, 110–111Option Strict and, 110–111
New keyword, 276NMock, 83non-coherent namespace, 378NUnit framework
Assert class, 78–79color, 74Console Error, 74Console Out, 74DailyCaloriesRecommended method, 79Errors and Failures, 73exceptions, 81–82ExpectedException attribute, 81–82IdealBodyWeight method, 77–78installing, 72–74introduction, 72JUnit framework, 72Log, 74Setup attribute, 79–80TearDown attribute, 79–80test project, creating, 74–76test runners, 72TestFixture, 76–77Tests Not Run, 73Trace, 74writing tests, 77–78
NUnitForms, 83
Oobject identity, 277–278object-mocking framework, 83–84
512
methods
79796bindex.qxd:WroxPro 2/22/08 5:01 PM Page 512
object-oriented programmingclasses, 276–277
abstract, 331–332actor, 285attributes, 285designing, 281domain classes, 285has a phrases, 286is a phrases, 286nouns, 284duplicated, 285operations, 286–288system classes, 285
composition, 334–337CRC (class-responsibility-collaborator) cards,
288–297database-driven design, 299–302design principles
acyclic dependencies principle, 387favor object composition over class inheritance,
331open-closed, 215–217program to an abstraction, 331reuse-release equivalence, 378–379single responsibility, 294–297
entities, relationships and, 297–298garbage collection, 279
reference counting garbage collector, 279tracing garbage collector, 280
generic types, 332inheritance, 324–327introduction, 272messages, 280–281moving from procedural, 477–478object identity, 277–278object lifetime, 279polymorphism, 329–331procedural design and, 302–308reuse-release equivalence, 378–379root object, 278–279single responsibility, 294–297
objects, 272encapsulation and, 272–274interfaces, 272lifetime, 279
Rent-a-Wheels, 313–320root object, 278–279state retention, 276upcasting object declaration, 413
obsolete elements, 179On Error Goto label, 148, 159–162
replacing with Try-Catch-Finally, 159–162On Error Resume Next statement, 149–150
replacing with Try-Catch-Finally, 162–163On Error statement, 147
replacing with Try-Catch-Finally, 158–159Option Explicit, 110
activating, 141–144.NET and, 110–111refactoring and, 112–114relaxed code and, 111–115Rent-a-Wheels and, 114–115variable declaration and, 111–112
Option Strict option, 110activating, 141–144late bound calls, 117–118.NET and, 110–111off setting, 116–119relaxed code, 115Rent-a-Wheels and, 132–135
ORM (object-relational mappings), 451overexposure, 179–180, 186–187
sources of, 182–186overriding, 327
Pparameterized constructor, 479parent class, 334partial classes, 390Pascal case, 202permissive code, 116–119persistence
calories calculator, validating patient personaldata, 44
implementing, 43–52POCO (Plain Ol’ CLR Object), 432polymorphism, 329–331
Abstract Factory design pattern, 412primitive types, 278print system, 342–360
513
print system
Inde
x
79796bindex.qxd:WroxPro 2/22/08 5:01 PM Page 513
problem domaininformation gathering, 198–199interactions
use cases, 200user stories, 200–201
prototype, 201vocabulary, 199–200
procedural design, moving to object-oriented,302–308, 477–478
Project Properties, options, 141–142properties, queries as, 256–258public access level, 184public interfaces, 208
modifying, 212–214published interfaces, 209pull up method, 357–360
Qqueries. See also LINQ (Language
Integrated Query)as methods, 256–258as properties, 256–258syntax, 445
RRAD (Rapid Application Development), 104,
208, 308radius, calculating, extracting code, 229read coordinates code, extracting, 229–233Refactor! (Developer Express), 60
constant refactoring, 239Ctrl key, 63cut and paste, 63encapsulate field, 274extract method refactoring, 233–234inline temp, 255move declaration near reference, 247move initialization to declaration, 248–249move type to file, 388–390progress indicator, 67refactorings supported, 67–69replace temp with query, 258right-clicking mouse, 62Safe Rename refactoring, 214–217
smart tags, 62split temporary variable, 251–253VB user interface
action hints, 64big hints, 64linked identifiers, 66markers, 64shortcut hints, 64target pickers, 66
XML literals, 444–445Refactor! Pro (Developer Express), 61refactoring
benefits of, 9–11constant, 239inheritance and, 342–345legacy issues, 16misconceptions of, 12–13Option Explicit and, 112–114process overview, 4–5Replace Temp with Query, 247
refactorsBreak Monolith, 471convert procedural design to objects, 307–308eliminate dead code, 176–177encapsulate field, 275explicit imports, 374extract class, 281–284extract interface, 346–348extract method, 221–223extract namespace, 382–383extract superclass, 352–355inline temp, 254–255move class to namespace, 380move declaration near reference, 245–246move element to more enclosed region,
185–186move field, 291–293move initialization to declaration, 248–249move method, 289–290move type to file, 388–390Option Explicit, set on, 113–114Option Strict on, 131–132pull up method, 357–360reduce access level, 182–183remove unused references, 192
514
problem domain
79796bindex.qxd:WroxPro 2/22/08 5:01 PM Page 514
renaming, 206–207replace complex VB queries with LINQ, 447replace error code with exception type, 164–166replace fully qualified names with explicit imports
motivation, 188–189replace general-purpose reference with
parameter type, 361–363replace inheritance with delegation, 338–342replace magic literal with constant, 237–239replace on error construct with Try-Catch-Finally
motivation, 156–157replace programmatic data layer with LINQ to
SQL motivation, 453–454replace row with data class, 299–302replace temp with query, 257–258replace XML string literals with XML literal,
442–443replacing row with data class, 299–302safe rename, 210–212set option explicit on (enforce variable
declaration), 113–114set option explicit on (enforce variable type
declaration and explicit type con), 131–132split temporary variable, 251–253
reference counting garbage collector, 279reference refactoring, variable declaration,
244–246relationships, entities and, 297–298renaming, 206–207
Rent-a-Wheels, 217–218safe, 210–212
Rent-a-Wheelsabstract forms, 394–396actors, 91
steps, 92assemblies, 402–403basic hygiene, 192–193BtnRent_Click
Fleet View, 99–100Rent Form, 100–101
database, model, 96–98desk receptionist interview, 89detached event handler eliminated, 192–193error handling, 169–171extract method, 240
forms, abstract, 394–396generic types, 364–369inheritance, 364–369introduction, 87LINQ, 454–464Magic Literals, 240maintenance personnel interviews, 90–91manager interview, 88–89method reorganization, 259–267namespaces, 402–403objects, 313–320Option Explicit and, 114–115Option Strict and, 132–135parking lot attendant interview, 90patterns, 434–437rename refactoring, 217–218Safe Rename refactoring, 217–218team, 96vehicle states, 93–94
Replace Temp with Query refactor, 247replacing row with data class, 299–302ReSharper (JetBrains), 60–61retaining looping, 250–253return parameter, ignored procedure return
parameter, 179return values, ignored procedure return
value, 179reusable assemblies, 209–212right-clicking mouse, Refactor!, 62root namespace name, changing, 372–373root object, 278–279
Ssafe rename refactor, 210–212Safe Rename refactoring (Refactor!), 214–217scope, 181
block scope, 184gradual reduction, 187levels of, 182
Setup attribute, 79–80shadowing, 327Shared keyword, 276smells
comments, 226cyclic dependencies, 384
515
smells
Inde
x
79796bindex.qxd:WroxPro 2/22/08 5:01 PM Page 515
smells (continued)data class, 286database-driven design, 288dead code, 174duplicated code, 235error code, 154event-handling blindness, 232implicit imports, 372implicit narrowing conversions, 125implicit type declaration, 118implicit variable declaration, 113large class, 298large namespace, 377legacy error handling, 153long method, 225magic literals, 236monolithic application, 470non-coherent namespace, 378overburdened temporary variable, 249–250overexposure, 179–180procedural design, 302refused bequest, 337–338superfluous temporary variable, 254unrevealing names, 201–202unused references, 191using fully qualified names outside the imports
section detecting the smell, 187–188XML string literals, 441
software, 5–6change and, 6–7design, 6design rot, 7
source files, options, 143spaghetti code, 220split temporary variable, 251–253
Refactor!, 251–253SQL (Structured Query Language), LINQ to SQL,
451–454state retention, objects, 276static behavior, resetting, 138static typing, 477statically typed languages, 109statically typed wrappers, 138–140statically typing, dynamic typing and, 135string variables, 121
subroutines, 220superclass, extract, 352–355System.Exception class, 151
TTearDown attribute, 79–80temporary variables, 243–244
split refactoring, 249–253superfluous, 254
test runners, NUnit, 72TestDriven.NET, 83TestFixture, 76–77testing. See also unit testing
functional testing, 472–473harness, 473–476
testing harness, migrated code, 472three-tiered architecture, 308tools
Refactor! (Developer Express), 61–67Refactor! Pro (Developer Express), 61ReSharper (JetBrains), 60–61Visual Assist X (Whole Tomato), 61
tracing garbage collector, 280transformations, 8
automating, 8–9Try-Catch-Finally blocks, 147
replacing On Error Goto label, 159–162replacing On Error Resume Next, 162–163replacing On Error statement, 158–159
TryCast, 127–132type-conversion functions, 125–132type declaration, implicit, 118type inference, local variables, 439–440typed containers, 332–333
Uunit testing
ad hoc, 71Assert class, 78–79automated, 71DailyCaloriesRecommended method, 79data entry, manual, 70–71exceptions, 81–82ExpectedException attribute, 81–82
516
smells (continued)
79796bindex.qxd:WroxPro 2/22/08 5:01 PM Page 516
framework, 70–72IdealBodyWeight method, 77–78introduction, 69–70NCover, 84NUnit framework, 72NUnitForms, 83Setup attribute, 79–80TearDown attribute, 79–80test project, creating, 74–76TestDriven.NET, 83TestFixture, 76–77writing tests, 77–78
unreachable code, 175unused assembly references, 173
removing, 191–192unused code, 175unused elements, importing, 178upcasting object declaration, 413upgrading
legacy code upgrades, 468migration and, 466
Upper case, 202
Vvariables
collecting, 250–253declaration, reference refactoring, 244–246declaring
implicit, 113Option Explicit and, 111–112
double, 120dynamically changing type, 117inferring type, 122–125
conversion function, 123–124external assemblies, 124–125
initial type, inferring, 118–119late bound calls, 117–118long type, 119–120looping, 250string, 121temporary, 243–244
split refactoring, 249–253type conversion, explicit, 126–127
version control, 84–85Visual Assist X (Whole Tomato), 61Visual Basic
history, 14legacy code, 15legacy issues, 14legacy language elements, 15–16legacy programming, 15–16VB 6 and VB .NET, 467–468
Visual Basic Upgrade Wizard, 469
Wwait for user to close code,
extracting, 229When keyword, 158Whole Tomato’s Visual Assist X, 61
XXML (eXtensible Markup Language)
axis properties, 444comments, 481–482literals, 440–444
Calories Calculator, 448extract to resource, 444–445string literals, 441
strings, replace with literals, 443–444
517
XML (eXtensible Markup Language)
Inde
x
79796bindex.qxd:WroxPro 2/22/08 5:01 PM Page 517
79796bindex.qxd:WroxPro 2/22/08 5:01 PM Page 518
Get more fromWrox.
Available wherever books are sold or visit wrox.com
978-0-470-18757-9 978-0-470-19137-8 978-0-470-19136-1
Gorroffr
teetGmoom
ommorWWr
erreoorxooxrroorroffr moom rWWr .xooxrro
9-75781-074-0-879 9 -879 879 8-73191-074-0- 8731910740 1-63191-074-0-879
obreverehwelbaliavAAv oc.xorwtisivrodloseraskoo mo
79796badvert.qxd:WroxPro 2/22/08 5:01 PM Page 519
Take your library wherever you go.Now you can access more than 200 complete Wrox booksonline, wherever you happen to be! Every diagram, description,screen capture, and code sample is available with your subscription to the Wrox Reference Library. For answers when and where you need them, go to wrox.books24x7.com andsubscribe today!
Programmer to ProgrammerTM
• ASP.NET • C#/C++ • Database • General • Java• Mac• Microsoft Office
• .NET • Open Source • PHP/MySQL • SQL Server • Visual Basic • Web• XML
Find books on
www.wrox.com
79796badvert.qxd:WroxPro 2/22/08 5:01 PM Page 520