Java Forum / General / January 2006
Overloading abstract methods
Rhino - 07 Jan 2006 00:56 GMT I have a question about abstract classes but I'm going to need to give you some of the background before the question makes much sense. Please bear with me.
I've decided that my project, which writes my resume in a variety of different formats - HTML, ASCII, PDF for starters - would benefit from an abstract class. I'm calling this abstract class ResumeWriter. There will be one class for each each of the resume formats and each of these will extend ResumeWriter so I'm calling them ResumeWriterHtml, ResumeWriterAscii and ResumeWriterPdf. In other words, ResumeWriterHtml extends the abstract ResumeWriter and writes the HTML version of the resume and so on.
The ResumeWriter abstract class includes: - a number of concrete getters, one for each of the things that the ResumeWriterHtml, ResumeWriterAscii, and ResumeWriterPdf classes will get from the ResourceBundle that supplies the data to all of the classes whose names start with 'ResumeWriter' - several abstract write methods, one for each portion of the resume, e.g. writeCandidateInformation(), writeEmploymentHistory(), writeSoftwareSkills(). In the subclasses, ResumeWriterHtml et. al., each of the abstract methods will be implemented to write that portion of the resume in the appropriate format, e.g. the writeSoftwareSkills() method will write software skills information in HTML format in ResumeWriterHtml and in PDF format in ResumeWriterPdf and so on.
I'm reasonably confident that this is a sensible way to do this from a design point of view and the code already works so I know that this is a workable approach.
Now, here's where I run into problems. In some of the ResumeWriter subclasses, the write() methods display slightly different data than in others. For instance, the ResumeWriterAscii class displays only the heading "EmploymentHistory" and then an array called "EmploymentHistoryData" but the ResumeWriterHtml displays additional information, specifically some text that serve as column titles for the different aspects of each job, namely "From/To", "Employer", "Role", and "Main Tools Used" for those columns of the HTML table that displays the employment history.
This suggests to me that I need _two_ abstract methods called writeEmploymentHistory(); one has only two arguments, EmploymentHistoryText and EmploymentHistoryData, while the other version also has the same two arguments plus four additional arguments, FromToText, EmployerText, RoleText, and MainToolsText. Now, I know that this is simply overloading the writeEmploymentHistory() method and that this is perfectly fine in an OO program.
The problem for me is that I apparently have to implement _both_ versions of writeEmploymentHistory() in each class that subclasses ResumeWriter, even though only one of the two methods will be used in any given subclass of ResumeWriter. And that makes me uneasy: I find myself wondering if I am doing the right thing when I implement multiple versions of the same method in a given subclass even though I will only use one of them. Implementing methods that I'm not going to use doesn't seem right somehow.
Can someone who is stronger in OO Analysis and Design help me figure out if I am doing the right thing here? If not, what should I be doing differently?
One other small issue related to the main problem. In the version of the overloaded method that I am NOT using, it seems to me that the easiest thing to do is just leave the method body empty with a comment that it is not being implemented because it is not appropriate for this subclass. However, if I do that, I get compiler warnings that the parameters for that method are never used. I know I could disable those warnings in the compiler but I'd rather avoid that and just put some sort of do-nothing code in the method body to satisfy the compiler that the parameters are used, even if the use is trivial. What is the most trivial thing I can do with each parameter to satisfy the compiler that the parameter is being used? I'm looking for something that would be harmless if it were executed and that would be more-or-less harmless looking to anyone who sees the code, i.e. something that seems to be obvious do-nothing code.
Rhino
Chris Smith - 07 Jan 2006 01:47 GMT > I've decided that my project, which writes my resume in a variety of > different formats - HTML, ASCII, PDF for starters - would benefit from an > abstract class. I'm calling this abstract class ResumeWriter. There will be > one class for each each of the resume formats and each of these will extend > ResumeWriter so I'm calling them ResumeWriterHtml, ResumeWriterAscii and > ResumeWriterPdf. Okay, you're only the seventeen gazillionth person to do this, but I'm picking on you.
Why?!? Would it have killed you to call them HtmlResumeWriter, AsciiResumeWriter, and PdfResumeWriter? Have you ever asked a neighbor to borrow her mower-lawn?
Anyway, just ignore me. Had to get that off my chest.
> This suggests to me that I need _two_ abstract methods called > writeEmploymentHistory(); one has only two arguments, EmploymentHistoryText [quoted text clipped - 8 lines] > though only one of the two methods will be used in any given subclass of > ResumeWriter. There's no problem with giving an object more than it needs. What you want is, most fundamentally, one method called writeEmploymentHistory, which takes as parameters all of the information that might be useful for writing an employment history. Which information you choose to make use of in each subclass is up to you.
> One other small issue related to the main problem. Actually, it turns out I think this is more related to the problem than you may think.
> In the version of the > overloaded method that I am NOT using, it seems to me that the easiest thing > to do is just leave the method body empty with a comment that it is not > being implemented because it is not appropriate for this subclass. If you decide on this design against my recommendations, the better thing to do would be to throw an exception... an unchecked one, since this is a programming error.
> However, if I do that, I get compiler warnings that the parameters for > that method are never used. I know I could disable those warnings in > the compiler but I'd rather avoid that Why? Disable that warning. That warning is counterproductive to good programming technique. It will cause you to do a worse job than if you leave it enabled.
If the warning only warned about unused parameters for an entire hierarchy of types in unpublished APIs, then I could see its uses. What it's doing, though -- warning you for not using a parameter in one method when you ARE using it within that method signature in the type hierarchy -- is counterproductive, and encourages poor code... like this throw away "trick the compiler" nonsense you're asking for.
I know it sounds like rehashing the same thing, but you're really providing a perfect example now of why you should NOT turn on random warnings just because some compiler vendor saw fit to give you the option (and leave it OFF by default). I imagine I could really make your life miserable by patching your copy of Eclipse to add an optional warning if you use more than 16 characters in an identifier.
> and just put some sort of do-nothing code in the > method body to satisfy the compiler that the parameters are used, even if > the use is trivial. What is the most trivial thing I can do with each > parameter to satisfy the compiler that the parameter is being used? Is this Java 1.5? If so, @SuppressWarnings("unused").
If it's not Java 1.5, and if you can't be dissuaded from doing this, then I guess you could declare a temporary variable of the same type, assign the value of the parameter to the parameter to that temporary variable, and then assign the temporary variable back to the parameter. Like this:
public void myMethod(int unusued) { int tmp = unused; unused = tmp; }
(The second bit is to prevent another warning about an unread local variable.)
Then again, with any luck Eclipse will soon gain a warning for assigning to a parameter (which WOULD be a good one to leave on) and you'll trigger that one. Or its control flow will get smarter and you'll have to get more complex to trick it. Eventually, someone might get wise enough to use escape analysis for that warning, and then you'll be really snookered.
Do you care about the standard output and error streams? I guess you could print the values there, and that would sure take care of it... but you'd get weird output if you ran the app from the command line.
 Signature www.designacourse.com The Easiest Way To Train Anyone... Anywhere.
Chris Smith - Lead Software Developer/Technical Trainer MindIQ Corporation
VisionSet - 07 Jan 2006 02:38 GMT > > ResumeWriter so I'm calling them ResumeWriterHtml, ResumeWriterAscii and > > ResumeWriterPdf. [quoted text clipped - 7 lines] > > Anyway, just ignore me. Had to get that off my chest. LOL, I felt that from this side of the pond! They would at least sort nicely in (my) IDEs list's of classes.
-- Mike W
Roedy Green - 07 Jan 2006 04:15 GMT >Okay, you're only the seventeen gazillionth person to do this, but I'm >picking on you. [quoted text clipped - 4 lines] > >Anyway, just ignore me. Had to get that off my chest. the advantage of doing that weird way is javadoc listings will put the three classes side by side.
 Signature Canadian Mind Products, Roedy Green. http://mindprod.com Java custom programming, consulting and coaching.
Rhino - 07 Jan 2006 04:57 GMT >>Okay, you're only the seventeen gazillionth person to do this, but I'm >>picking on you. [quoted text clipped - 7 lines] > the advantage of doing that weird way is javadoc listings will put the > three classes side by side. And that was my basic motivation for naming things that way, as it turns out.
One other reason is something I heard in school many years ago: a teacher described the ISO standards as putting things like dates, which comprised several parts, into order on the basis of largest things to smallest things, e.g. the ISO standard is to put years first, then months, then days. To me, the abstract class, ResumeWriter is the "larger" thing and the specific type of document being produced is the "smaller" think; therefore, ResumeWriter goes first, followed by the type of document, thus giving us things like ResumeWriterHtml or ResumeWriterAscii.
Then again, I could persuade myself that I've really got three parts in the name, not two, and by that reasoning, the names should be WriterResumeHtml and WriterResumeAscii. Something tells me that would cause a lot of raised eyebrows too :-)
Well, I'm not going to please everyone so I'm going to concentrate on pleasing myself. :-)
Rhino
Chris Smith - 07 Jan 2006 05:29 GMT > Well, I'm not going to please everyone so I'm going to concentrate on > pleasing myself. :-) As I said, you can ignore that.
Hope the rest of the response was helpful.
 Signature www.designacourse.com The Easiest Way To Train Anyone... Anywhere.
Chris Smith - Lead Software Developer/Technical Trainer MindIQ Corporation
Rhino - 07 Jan 2006 16:24 GMT >> Well, I'm not going to please everyone so I'm going to concentrate on >> pleasing myself. :-) > > As I said, you can ignore that. I wasn't trying to be defensive, I just wanted to make the case for the approach I was using :-)
> Hope the rest of the response was helpful. It certainly was!
Rhino
P.Hill - 12 Jan 2006 04:01 GMT Roedy Green wrote about putting modifiers on the end of a class name.
> the advantage of doing that weird way is javadoc listings will put the > three classes side by side. VisionSet wrote:
> They would at least sort nicely in (my) IDEs list's of classes. Just to pick this slightly old thread. Let's see, something that can group things together. How about a package! Now that was simple enough.
If you didn't want them all in their own package, I'd wonder what the list of other things were in the package that is making it so cluttered that you can't see the three classes.
Also IDEs with inheritance hierarchies are cool tools also.
Yeah, I'm with Chris, adjectives in English go _before_ the noun!
"Scoping" modifiers in names are for programming in assembly no Java or C++
-Paul
Rhino - 07 Jan 2006 05:11 GMT >> I've decided that my project, which writes my resume in a variety of >> different formats - HTML, ASCII, PDF for starters - would benefit from an [quoted text clipped - 13 lines] > > Anyway, just ignore me. Had to get that off my chest. See my explanation in a followup to Roedy's first reply. It probably won't persuade you but, then again, maybe it will :-)
>> This suggests to me that I need _two_ abstract methods called >> writeEmploymentHistory(); one has only two arguments, [quoted text clipped - 17 lines] > for writing an employment history. Which information you choose to make > use of in each subclass is up to you. Okay, I guess I can buy that as a valid approach. But is it good design? If I applied that same argument to other classes in the J2SE, then I'd probably never overload _any_ method: I'd just combine all overloaded methods into a single big method that had all possible parameters. I'd expect to get criticized on the grounds that I'm discarding the benefits of overloaded methods by doing that. Isn't that what you're proposing here? Or is this a "special case" where that is okay?
>> One other small issue related to the main problem. > [quoted text clipped - 39 lines] > > Is this Java 1.5? If so, @SuppressWarnings("unused"). Yes, it is Java 1.5.
> If it's not Java 1.5, and if you can't be dissuaded from doing this, > then I guess you could declare a temporary variable of the same type, [quoted text clipped - 17 lines] > enough to use escape analysis for that warning, and then you'll be > really snookered. I'm actually not as determined to write do nothing code as you may think. I was hoping someone could suggest a way to avoid that, first and foremost. But I wanted a suggestion for the least offensive way of doing it if I had to do it.
You make a good case for using the compiler switch to simply ignore unused variables, at least in this case. I suppose I was looking for some bragging rights in saying that I am not ignoring any warnings or errors and have an absolutely warning-free compile. But I agree that this is just going to lead to poor code in this case. And, as you say, the next version of the compiler will probably find ways to detect any little tricks that I play on the current version of the compiler and make me change the code eventually.
> Do you care about the standard output and error streams? I guess you > could print the values there, and that would sure take care of it... but > you'd get weird output if you ran the app from the command line. Interesting. That's exactly what I came up with on my own: I just did a System.out.println() of each parameter. I'm not even worried about the code executing because the unused method simply isn't called anywhere within its class. Of course, if I subclassed that class, there'd be the risk that the subclass could execute it.
Rhino
Chris Smith - 07 Jan 2006 05:40 GMT > > There's no problem with giving an object more than it needs. What you > > want is, most fundamentally, one method called writeEmploymentHistory, > > which takes as parameters all of the information that might be useful > > for writing an employment history. Which information you choose to make > > use of in each subclass is up to you.
> Okay, I guess I can buy that as a valid approach. But is it good design? If > I applied that same argument to other classes in the J2SE, then I'd probably [quoted text clipped - 3 lines] > methods by doing that. Isn't that what you're proposing here? Or is this a > "special case" where that is okay? Actually, you can overload or not... it doesn't matter all that much. The change in design is that you provide one point of implementation for subclasses to customize the behavior of the superclass. And yes, it is good design. For example, here's something with overloads.
public abstract class ResumeWriter { public abstract void writeEmploymentHistory( String employmentHistoryText, String employmentHistoryData, String fromToText, String employerText, String roleText, String mainToolsText);
public final void writeEmploymentHistory( String employmentHistoryText, String employmentHistoryData) { writeEmploymentHistory( employmentHistoryText, employmentHistoryData, null, null, null, null); } }
You still need only override the method that receives all the data, and just use what you need.
I'm not exactly clear on how you call this method, though. I would think that from the calling context, you'd just forget about what kind of resume you're writing, and pass all the information you've got. Let the subclass sort out what it uses and what it doesn't use. In that case, you'd never use the overload anyway. The change here isn't just to reuse the same method from two contexts... it's to design your code as much as possible to stop caring about the context in the first place.
> > Is this Java 1.5? If so, @SuppressWarnings("unused").
> Yes, it is Java 1.5. So that's the solution to the unused parameter warning, then, eve if you do leave it on. You'll have to deal with that regardless of which design decision you've made.
 Signature www.designacourse.com The Easiest Way To Train Anyone... Anywhere.
Chris Smith - Lead Software Developer/Technical Trainer MindIQ Corporation
Rhino - 07 Jan 2006 16:31 GMT >> > There's no problem with giving an object more than it needs. What you >> > want is, most fundamentally, one method called writeEmploymentHistory, [quoted text clipped - 50 lines] > to reuse the same method from two contexts... it's to design your code > as much as possible to stop caring about the context in the first place. For what it's worth, I've decided not to overload the method after all; I'll just have a single version of writeEmploymentHistory() and pass it all the parameters it will ever need, even if some parameters don't get used in some subclasses of ResumeWriter.
>> > Is this Java 1.5? If so, @SuppressWarnings("unused"). > [quoted text clipped - 3 lines] > do leave it on. You'll have to deal with that regardless of which > design decision you've made. You sold me on using the @SuppressWarnings annotation. I'd been meaning to start using annotations in the near future anyway and your note gave me the impetus to read up about the basics. I really like that I can apply annotations in a very narrow scope, such as a single method of a given class, rather than having to turn off the unused variable detection for all classes throughout my IDE.
Rhino
Chris Smith - 07 Jan 2006 16:55 GMT > You sold me on using the @SuppressWarnings annotation. I'd been meaning to > start using annotations in the near future anyway and your note gave me the > impetus to read up about the basics. I really like that I can apply > annotations in a very narrow scope, such as a single method of a given > class, rather than having to turn off the unused variable detection for all > classes throughout my IDE. Yep. I'd like still narrower scope, though... especially for @SuppressWarnings("unchecked"). This particular annotation ought to be able to be applied to a statement, rather than just a method or other declaration. Except, of course, that this isn't what annotations are really designed for.
 Signature www.designacourse.com The Easiest Way To Train Anyone... Anywhere.
Chris Smith - Lead Software Developer/Technical Trainer MindIQ Corporation
Chris Uppal - 08 Jan 2006 12:16 GMT > For what it's worth, I've decided not to overload the method after all; > I'll just have a single version of writeEmploymentHistory() and pass it > all the parameters it will ever need, even if some parameters don't get > used in some subclasses of ResumeWriter. That's not too bad, but it would be better still not to try to work out what "all the parameters it will ever need" are. Just pass[*] it the source of /all/ the data and let the individual implementations pull what they require out of it.
(by "pass" I don't necessarily mean pass as a parameter -- since your raw data is held as part of the object's state, the writeEmploymentHistory() method is entitled to use whatever bits of that it happens to need)
-- chris
Rhino - 08 Jan 2006 18:50 GMT >> For what it's worth, I've decided not to overload the method after all; >> I'll just have a single version of writeEmploymentHistory() and pass it [quoted text clipped - 13 lines] > is > entitled to use whatever bits of that it happens to need) This is an intriguing idea but I think it would be counterproductive in my case.
I've gone ahead and reworked the resume classes that we discussed in a thread just before Christmas. I was persuaded by several of the people who responded, including you, that I should take advantage of the fact that each version of the resume would have several common elements and create an abstract class that recognized that; then use subclasses of the abstract class to generate each specific format of the resume, e.g. HTML, PDF, ASCII.
All of the information for the resume is coming from a ResourceBundle. (Currently, I am only creating an English-language resume but I thought I'd use ResourceBundles in case I decided to generate other languages as well; that would be dead easy since I'd only have to copy my English ResourceBundle and then translate the relevant bits of in the second ResourceBundle.)
I realized that each of the resumes executed _exactly_ the same code to get the desired information, such as my name, from the ResourceBundle, regardless of the format of the resume being generated. I also realized that each resume _wrote_ that information in its respective file _differently_ from the other files. For example, the HTML version of the resume would write this to display my name:
<h1>RHINO</h1>
while the ASCII version of the resume would write this left-justified:
RHINO
and the PDF version of the resume would write this centered:
RHINO
As a result I created the abstract class, ResumeWriter, with _concrete_ methods to _get_ the contents of the resume from the ResourceBundle and _abstract_ methods to _write_ those contents in the desired format. Therefore, ResumeWriter looks like this (I'm including only the code needed for the first bit of the resume; that's all I need for illustrating my point):
------------------------------------------------------------------------------------------------------------------- public abstract class ResumeWriter{
public String getCandidateNameData(ResourceBundle locList) {
String candidateNameData = locList.getString("CandidateNameData"); return candidateNameData; }
public String[] getCandidateAddressData(ResourceBundle locList) {
String[] candidateAddressData = (String[]) locList.getObject("CandidateAddressData"); return candidateAddressData; }
public String getJobTargetText(ResourceBundle locList) {
String jobTargetText = locList.getString("JobTargetText"); return jobTargetText; }
public String getJobTargetData(ResourceBundle locList) { String jobTargetData = locList.getString("JobTargetData"); return jobTargetData; }
public abstract void writeCandidateAndTarget(String candidateNameData, String[] candidateAddressData, String jobTargetText, String jobTargetData);
-------------------------------------------------------------------------------------------------------------------
The ResumeWriterHtml class looks like this (with most of the irrelevancies omitted):
------------------------------------------------------------------------------------------------------------------- public class ResumeWriterHtml extends ResumeWriter implements ResumeConstants, ResumeGeneratorConstants {
private final String CLASS_NAME = getClass().getName();
private PrintWriter pw = null;
/** * Creates an HTML version of a resume. * * @param strOutfile the name of the file which will contain the HTML version of the resume * @param locList the ResourceBundle which contains the data that will be written on the resume */ public ResumeWriterHtml(String strOutfile, ResourceBundle locList) {
/* Delete the old version of the output file. */ ResumeUtilities.deleteFile(strOutfile);
/* Open the output file. */ this.pw = ResumeUtilities.openOutputFile(strOutfile);
/* Write the head of the page. */ writeHead();
/* Get and write the candidate name, address, and job target information. */ String candidateNameData = getCandidateNameData(locList); String[] candidateAddressData = getCandidateAddressData(locList); String jobTargetText = getJobTargetText(locList); String jobTargetData = getJobTargetData(locList); writeCandidateAndTarget(candidateNameData, candidateAddressData, jobTargetText, jobTargetData);
}
/** * Writes the resume lines that show the candidate name, address, and job target in HTML format. * * @param candidateNameData the name of the candidate * @param candidateAddressData the address of the candidate * @param jobTargetText the job target text, typically "Job Target" * @param jobTargetData the job target data */ @Override public void writeCandidateAndTarget(String candidateNameData, String[] candidateAddressData, String jobTargetText, String jobTargetData) {
this.pw.println("<BODY>"); this.pw.println("<table width=\"580\">"); this.pw.println("<tr><td>"); this.pw.println("<h1>" + candidateNameData.toUpperCase() + "</h1>"); this.pw.println("<p>"); //$NON-NLS-1$ for (String candidateAddressLine : candidateAddressData) { if (candidateAddressLine.indexOf("@") > -1) { this.pw.println("<a href=\"mailto:" + candidateAddressLine + "\">" + candidateAddressLine + "</a><br>"); } else { this.pw.println(candidateAddressLine + "<br>"); //$NON-NLS-1$ } } this.pw.println("</p>"); this.pw.println("<h4>" + jobTargetText + ": " + jobTargetData + "</h4>"); this.pw.println("<br>"); this.pw.println("</td></tr>"); }
-------------------------------------------------------------------------------------------------------------------
See? I use the concrete methods from the abstract ResumeWriter class to get the data from the ResourceBundle and don't need to implement them within my subclasses at all, then I implement the abstract writeCandidateAndTarget() method within the subclass so that it writes the information in HTML. Class ResumeWriterPdf looks identical to ResumeWriterHtml except that its implementation of method writeCandidateAndTarget() writes PDF format; however, it is using the exact same concrete methods to get the data from the ResourceBundle and those methods don't need to be overridden in the subclasses of ResumeWriter.
If I followed your suggestion, I'd basically be eliminating those concrete getters from the abstract class and having to implement that code in each of the subclasses of ResumeWriter, which would mean duplicating the getter code in each and every one of the subclasses. That would be a definite step backwards from the current design.
The only quibble is that some implementations of some of my abstract write methods don't need all of the data that the method wants. For instance, in some cases, the implementations of writeEmploymentHistory() need only EmploymentHistoryText and EmploymentHistoryData but in other implementations, they need FromToText, EmployerText, RoleText, and MainToolsText. That's what prompted this thread in the first place.
Chris's remarks got me thinking that I was better to define all the parameters that writeEmploymentHistory() would ever need in a single abstract definition of the method rather than making several overloaded abstract methods, one for each valid combination of parameters that the overloaded method might need. It seems easier to simply ignore superfluous parameters than to implement each of the overloaded methods, even if all but one of the overloaded versions of the method contains only an empty implementation.
Going through this thought process again has made me change my mind! As I wrote the preceding paragraph, I realized that it still bothered me to pass parameters that were being ignored and that this bothered me more than having overloaded versions of the writeEmploymentHistory() method. I created an abstract two-parameter version of writeEmploymentHistory() in ResumeWriter to complement the six-parameter version and implemented it in each of my three subclasses of ResumeWriter. In each of the subclasses, I did a proper implementation of the version I was actually using and an empty implementation of the version I wasn't using. The empty implementation got an @SuppressWarnings("unused") annotation and a "//not used in this subclass" comment; the proper implementation got neither. Then, I made sure that I adjusted the invocation of writeEmploymentHistory() to pass only the appropriate number of parameters, two or six, none of which were null. Frankly, I like this approach better overall, although I could certainly live with the no-overloaded methods approach in a pinch.
Raymond DeCampo - 08 Jan 2006 21:32 GMT > Going through this thought process again has made me change my mind! As I > wrote the preceding paragraph, I realized that it still bothered me to pass [quoted text clipped - 11 lines] > Frankly, I like this approach better overall, although I could certainly > live with the no-overloaded methods approach in a pinch. Why do your writeEmploymentHistory() methods require any parameters, if all the data is available via the superclass?
HTH, Ray
 Signature This signature intentionally left blank.
Rhino - 09 Jan 2006 05:09 GMT >> Going through this thought process again has made me change my mind! As I >> wrote the preceding paragraph, I realized that it still bothered me to [quoted text clipped - 15 lines] > Why do your writeEmploymentHistory() methods require any parameters, if > all the data is available via the superclass? Good point!
I'm invoking all of the getters from the constructor of each ResumeWriter subclass, e.g. ResumeWriterAscii. Here's a short fragment from the actual ResumeWriterAscii:
--------------------------------------------------------------------------------------------------------- public ResumeWriter(ResourceBundle locList) {
... String employmentHistoryText = getEmploymentHistoryText(locList); String[][] employmentHistoryData = getEmploymentHistoryData(locList); writeEmploymentHistory(employmentHistoryText, employmentHistoryData);
... -----------------------------------------------------------------------------------------------------------
I suppose I could very easily change the class so that employmentHistoryText and employmentHistoryData are class variables, then simply remove the parameters from the invocation of writeEmploymentHistory().
But isn't it generally bad form to make something a class variable unless it really needs to be a class variable because it is used all over the class? In my case, employmentHistoryText and employmentHistoryData are used only in the constructor and in writeEmploymentHistory().
Wouldn't that suggest the two variables really ought to be local variables? That's my gut hunch anyway. But maybe that's just me overcompensating for my first several years of programming! I was writing mainframe COBOL and COBOL doesn't have local variables: everything is a global variable. Once I discovered that local variables existed in some languages, I've tended to try to use a local variable whenever possible, perhaps to excess.
Do you think I should be making the variables that hold my getter results class variables rather than local variables?
Rhino
Chris Smith - 09 Jan 2006 06:37 GMT > public ResumeWriter(ResourceBundle locList) { > [quoted text clipped - 4 lines] > writeEmploymentHistory(employmentHistoryText, > employmentHistoryData); Danger, Will Robinson! You're calling a non-final and non-private method from a constructor, here. That's a very dangerous thing to do, and best avoided. It means that any fields of the subclass will be uninitialized -- even if they are compile-time constants! -- when writeEmploymentHistory is called. For example, consider a subclass:
public class HTMLResumeWriter extends ResumeWriter { private String docType;
public HTMLResumeWriter(String docType, ResourceBundle locList) { super(locList); this.docType = docType; }
public void writeEmploymentHistory(String text, String[][] data) { ... } }
docType will be null inside of writeEmploymentHistory.
> But isn't it generally bad form to make something a class variable unless it > really needs to be a class variable because it is used all over the class? > In my case, employmentHistoryText and employmentHistoryData are used only in > the constructor and in writeEmploymentHistory(). It's good form to make something an instance field if it is logically a property of any object of that class. Rules of thumb that rely on things like how or where something is used are necessarily approximate, and prone to give wrong suggestions sometimes. In this case, I see two possible designs, and you have your choice between them:
Option 1: ResumeWriter is conceived of as a tool. You could create one without giving it any specific information about any particular person's resume. It has a method to write a resume, and all necessary information is passed in there. Much like a printer, the ResumeWriter is a general-purpose tool that may be used to create any number of resumes, and the details of the resumes it writes depend on how it's used, not how it was built. In that case, the resume information should be passed around as a parameter, because it's not logically a property of the writer.
Option 2: A ResumeWriter is a one-shot object that's used to write a specific resume. It's created to write one resume and then discarded. It's more like a print job than a printer, in that it is a transient thing that is created for and works with a specific set of data, and a new one is created for the next data set. In this case, the resume that's being written definitely is a property of the ResumeWriter, and it should be a field.
Both options are fine. Each gives you different tools for handling additional requirements if the application becomes more advanced in the future. So far, you seem to have chosen option 2, and that's fine. In that case, you should store the information about the resume in fields, and provide protected APIs for subclasses to access it.
 Signature www.designacourse.com The Easiest Way To Train Anyone... Anywhere.
Chris Smith - Lead Software Developer/Technical Trainer MindIQ Corporation
Rhino - 09 Jan 2006 14:32 GMT >> public ResumeWriter(ResourceBundle locList) { >> [quoted text clipped - 28 lines] > > docType will be null inside of writeEmploymentHistory. Okay, so what are you saying I should do to prevent these problems? I know the compiler will not let me make writeEmploymentHistory() (within ResumeWriterAscii) a private method because it is public in the abstract class and the implementation can't have less access than the abstract version. I know the compiler will only let me make the method public or protected in the abstract class. I _can_ make writeEmploymentHistory() final within ResumeWriterAscii; is that what you are suggesting? I don't plan on subclassing ResumeWriterAscii or its siblings any further so I don't have a problem with making the writeXXX() implementations final.
With regards to your docType example, I wasn't aware that docType would be uninitialized in that case. Frankly, it rather surprises me that docType would be uninitialized in this case, given that docType is a class variable in the class. Why exactly does Java behave that way? Would I be safe in assuming that a compiler warning would bring this to my attention?
Am I correct in thinking that you are warning me against setting a class variable based on a value passed in the parameter of a constructor?
My actual ResumeWriterAscii class, and its siblings, pass only two values to their constructors: a String containing the name of the file that is to be written and a ResourceBundle that contains the data used by the class. Both of these values are not assigned to any class variables, are used only in the constructor and are not passed explicitly to any other method of the class. I also have several class variables in ResumeWriterAscii: all but two of them are constants used throughout the class for formatting. The other two class variables are:
private StringUtils stringUtils = null; private PrintWriter pw = null;
They get set to useful values in the constructor and are then used throughout the class. I'm not going to have any problems with any of that, am I? The compiler is not giving me any errors or warnings anywhere in my project.
Hmmm, just out of curiousity, I made a class variable called strOutfile which was defined as a private String and initialized to null. Then, within my constructor, I added this line:
this.strOutfile = strOutfile;
I also added this line to writeEmploymentHistory():
System.out.println("strOutfile: " + this.strOutfile); //$NON-NLS-1$
The compiler flags 'strOutfile' within my constructor signature ['public ResumeWriterAscii(String strOutfile, ResourceBundle locList)'] and gives me this warning: "The parameter strOutfile is hiding a field from type ResumeWriterAscii." That's fair enough since the strOutfile value passed in via the constructor will inevitably clobber the value of the strOutfile class variable. But I get no warnings or errors about strOutfile being uninitialized in writeEmploymentHistory() even though it now parallels the docType example you gave. I assume that's why you brought that to my attention: since the compiler gives no warning, I can easily fail to notice this problem.
>> But isn't it generally bad form to make something a class variable unless >> it [quoted text clipped - 33 lines] > that case, you should store the information about the resume in fields, > and provide protected APIs for subclasses to access it. You're right; I do think of my ResumeWriter subclasses as Option 2.
Could you elaborate on what you mean by your last sentence? Are you saying that I am doing the right thing in storing the values obtained by the getters in local variables and then passing them to their respective writers? Do the implementations of the writeXXX methods which appear in my ResumeWriter subclasses constitute the protected APIs that you are recommending? Or are you proposing something else?
Rhino
Chris Smith - 09 Jan 2006 17:18 GMT > Okay, so what are you saying I should do to prevent these problems? I know > the compiler will not let me make writeEmploymentHistory() (within [quoted text clipped - 5 lines] > subclassing ResumeWriterAscii or its siblings any further so I don't have a > problem with making the writeXXX() implementations final. I mean that any method you call from a constructor should be either private or final in the class where you call it. Since you need to override writeEmploymentHistory, that precludes the possibility of calling writeEmploymentHistory from the constructor. An alternative design is:
public class ResumeWriter { private ResourceBundle locList;
public ResumeWriter(ResourceBundle locList) { this.locList = locList; }
public void writeResume() { String employmentHistoryText = ...; String[][] employmentHistoryData = ...;
writeEmploymentHistory( employmentHistoryText, employmentHistoryData); } }
There are now two steps to writing a resume: first, create a ResumeWriter, and then call its writeResume method. That's better, anyway. It's surprising when writing the resume -- which is the main purpose of a ResumeWriter -- is done as a side-effect of creating the object. If other people were using this API, it would take them time to get accustomed to that little quirk, and it prevents the code from reading well. There is no mental model of the ResumeWriter class that makes it logical for it to write the resume in the constructor.
Note that this paves the way for the changes suggested elsewhere. What needs to happen now is:
- your getEmploymentHistoryText and getEmploymentHistoryData methods made protected, so that they can be accessed by the subclass, and modified so that they get locList from the instance field rather than a parameter.
- All parameters removed from writeEmploymentHistory. Implementations of writeEmploymentHistory in the subclass should call getEmploymentHistoryText and getEmploymentHistoryData directly if they need that information.
> With regards to your docType example, I wasn't aware that docType would be > uninitialized in that case. Frankly, it rather surprises me that docType > would be uninitialized in this case, given that docType is a class variable > in the class. Why exactly does Java behave that way? It's a quirk of the implementation. The superclass constructor runs before the subclass constructor... and a polymorphic call to a method (like writeEmploymentHistory) from the superclass constructor can therefore reach the subclass before its constructor is called. The typical way to avoid this is to make sure that all methods called from a constructor are private or final, so that they can't be overridden.
> Would I be safe in > assuming that a compiler warning would bring this to my attention? In Eclipse, I assume? I don't believe that warning exists.
> Am I correct in thinking that you are warning me against setting a class > variable based on a value passed in the parameter of a constructor? I'm confused by your use of the term "class variable". When most people say "class variable" or "class field", they mean a static field. My example didn't use any static fields (although it would probably be incorrect to set them from a constructor, if you did have one).
My warning was against calling polymorphic (that is, non-final and non- private) methods from a constructor.
> I also have several class variables in ResumeWriterAscii: all but two > of them are constants used throughout the class for formatting. The other [quoted text clipped - 6 lines] > throughout the class. I'm not going to have any problems with any of that, > am I? Well, in the code you posted, writeEmploymentHistory is going to be called BEFORE the constructor for ResumeWriterAscii... so you very well might have problems. Both variables will be null -- and not because you set them to null above, but because all reference variables will be null.
> The compiler is not giving me any errors or warnings anywhere in my > project. As I said, I don't think Eclipse has a warning for this one.
> Hmmm, just out of curiousity, I made a class variable called strOutfile > which was defined as a private String and initialized to null. Then, within [quoted text clipped - 12 lines] > via the constructor will inevitably clobber the value of the strOutfile > class variable. The warning you're getting is not related to this problem. It's just saying that you're hding a field with a parameter. To get rid of it, just rename the parameter.
> I assume that's why you brought that to my > attention: since the compiler gives no warning, I can easily fail to notice > this problem. Yep. I'm starting to wonder how hard it would be to write code to detect this, and contribute the code to Eclipse. I'll look into that today.
> Could you elaborate on what you mean by your last sentence? Are you saying > that I am doing the right thing in storing the values obtained by the > getters in local variables and then passing them to their respective > writers? No, I'm saying that all the information should be stored in instance fields of the object. I'm given more detailed suggestions about that may be more helpful.
 Signature www.designacourse.com The Easiest Way To Train Anyone... Anywhere.
Chris Smith - Lead Software Developer/Technical Trainer MindIQ Corporation
Rhino - 11 Jan 2006 17:34 GMT >> Okay, so what are you saying I should do to prevent these problems? I >> know [quoted text clipped - 34 lines] > } > } Okay, I can see some virtues to making ResumeWriter a concrete class rather than an abstract one. For example, locList becomes an instance variable so I don't need to pass it to each method in the concrete ResumeWriter. But I'm not clear from your fragment about a few things: - Am I correct in assuming that my getters will still exist as separate methods in the concrete versio of ResumeWriter? You're not proposing that I should take the work done by the getters and move them into the writeResume() method are you? - Are you proposing that I write a single version of writeEmploymentHistory(), presumably the six-parameter version, or two versions, one that has two parameters and one that has six parameters? - Am I correct in understanding that the implementation(s) of writeEmploymentHistory need to be complete? Since ResumeWriter is not an abstract class any more, I believe I have to make each implementation concrete. But what format will my implemenation write: HTML or ASCII or PDF?
If you can answer these last two questions, it will go a long way to helping me understand exactly what you're proposing.
> There are now two steps to writing a resume: first, create a > ResumeWriter, and then call its writeResume method. That's better, [quoted text clipped - 12 lines] > modified so that they get locList from the instance field rather than a > parameter. Okay, so you do mean for me to leave the getters as standalone concrete methods. Good, that makes sense to me.
> - All parameters removed from writeEmploymentHistory. Implementations > of writeEmploymentHistory in the subclass should call > getEmploymentHistoryText and getEmploymentHistoryData directly if they > need that information. So all the values obtained from my getters become instance variables. Okay, that's fine. Should these instance variables be public, protected or private?
>> With regards to your docType example, I wasn't aware that docType would >> be [quoted text clipped - 22 lines] > example didn't use any static fields (although it would probably be > incorrect to set them from a constructor, if you did have one). Sorry, that was my fault for misusing the terminology. When I used the term 'class variable', I should have said 'instance variable'; I got the two terms muddled.
> My warning was against calling polymorphic (that is, non-final and non- > private) methods from a constructor. Okay, I'll watch for that from now on.
>> I also have several class variables in ResumeWriterAscii: all but two >> of them are constants used throughout the class for formatting. The other >> two class variables are: >> >> private StringUtils stringUtils = null; >> private PrintWriter pw = null; Again, I've mischaracterized these as class variables when they are actually instance variables. Sorry!
>> They get set to useful values in the constructor and are then used >> throughout the class. I'm not going to have any problems with any of [quoted text clipped - 35 lines] > saying that you're hding a field with a parameter. To get rid of it, > just rename the parameter. Sorry, that was a red herring.
>> I assume that's why you brought that to my >> attention: since the compiler gives no warning, I can easily fail to [quoted text clipped - 14 lines] > fields of the object. I'm given more detailed suggestions about that > may be more helpful. Chris Uppal - 10 Jan 2006 12:02 GMT > Option 1: ResumeWriter is conceived of as a tool. You could create one > without giving it any specific information about any particular person's [quoted text clipped - 6 lines] > > Both options are fine. Though I'd argue that (1) is in general inferior to (2). It depends on cases, of course, but usually (2) is simpler to use and think about, easier to implement, and more flexible. So I'd recommend (2) as the norm, with (1) as an alternative to use in the (relatively rare) cases where it turns out to be more appropriate.
-- chris
Chris Smith - 11 Jan 2006 05:30 GMT > Though I'd argue that (1) is in general inferior to (2). It depends on cases, > of course, but usually (2) is simpler to use and think about, easier to > implement, and more flexible. So I'd recommend (2) as the norm, with (1) as an > alternative to use in the (relatively rare) cases where it turns out to be more > appropriate. Funny. I was thinking exactly the opposite; i.e., that (1) is the more normal and the easier to think about... hence my numbering is "1".
 Signature www.designacourse.com The Easiest Way To Train Anyone... Anywhere.
Chris Smith - Lead Software Developer/Technical Trainer MindIQ Corporation
Chris Uppal - 11 Jan 2006 11:18 GMT [me:]
> > Though I'd argue that (1) is in general inferior to (2). [..] > > Funny. I was thinking exactly the opposite; i.e., that (1) is the more > normal and the easier to think about... hence my numbering is "1". You astonish me!
-- chris
Rhino - 11 Jan 2006 17:36 GMT > [me:] >> > Though I'd argue that (1) is in general inferior to (2). [..] [quoted text clipped - 3 lines] > > You astonish me! I suppose one man's normal is another man's bizarre - and vice versa :-)
Rhino
Chris Uppal - 11 Jan 2006 18:43 GMT [I have to leave the mess of attributions in place here -- I've trimmed a few inessentials]
> > > Funny. I was thinking exactly the opposite; i.e., that (1) is the > > > more normal and the easier to think about... hence my numbering is > > > "1". > > You astonish me! > I suppose one man's normal is another man's bizarre - and vice versa :-) Indeed. What surprised me was not that /someone/ would prefer to (1) to (2) -- which is inevitable -- but that /Chris Smith/ would. I'm still puzzled...
-- chris
Chris Smith - 11 Jan 2006 20:10 GMT > Indeed. What surprised me was not that /someone/ would prefer to (1) to (2) -- > which is inevitable -- but that /Chris Smith/ would. I'm still puzzled... I don't know what to say. I suppose I'm just weird.
I almost included a warning against "option 2", saying that if you do it that way, it would be better to call the object Resume instead of ResumeWriter, since one of its two purposes is to organize the data that belongs to a resume. Then I'd suggest collapsing the data into a single concrete Resume class and factoring out the format-specific writing parts into a different class hierarchy. Once that's done, we're back at option 1.
 Signature www.designacourse.com The Easiest Way To Train Anyone... Anywhere.
Chris Smith - Lead Software Developer/Technical Trainer MindIQ Corporation
Chris Uppal - 12 Jan 2006 13:11 GMT > I almost included a warning against "option 2", saying that if you do it > that way, it would be better to call the object Resume instead of [quoted text clipped - 3 lines] > parts into a different class hierarchy. Once that's done, we're back at > option 1. And the format-specific stuff would be stateless. Or rather, it would be stateless with respect to the Resume -- it might still have state for things like the output stream, page numbers/header/footers, a stack for keeping track of any nested constructs, etc.
That's pretty much the design I suggested to Rhino before Christmas, so I'm not going to claim there's anything very much wrong with it ;-)
-- chris
Chris Uppal - 10 Jan 2006 11:50 GMT > Going through this thought process again has made me change my mind! As I > wrote the preceding paragraph, I realized that it still bothered me to > pass > parameters that were being ignored and that this bothered me more than > having overloaded versions of the writeEmploymentHistory() method. I think that the underlying problem here is that you are making the same decision in two different places, and that is mucking-up the structure of your code.
Basically, what you have is a framework for generating CVs. Your framework code -- in the abstract ResumeWriter superclass "drives" the process, and the "pluggable" code (implemented as concrete subclasses of ResumeWriter) implements the actual formatting. That's fair enough as far as it goes (though I'll mention that it has a distinctly procedural flavour to my mind [*]). But the problem comes when you try to decide /what/ data to print. You are attempting to make that decision in the driver (which is not unreasonable), but then you hit the problem that that decision can only be made with knowledge of the capabilities of the concrete document format (HTML has tables, ASCII text does not). So although you /want/ your driver to abstract away from the details of the concrete representation, in this case it cannot do so. So you end up with the "knowledge" about how the employment history is represented in two different places. In one place (in the driver) you select which of the two methods to invoke (the simple or complete versions). In another place (in the various subclasses) you duplicate that knowledge by the choice of which of the two method to provide a real implementation for.
([*] I can talk more about that if you are interested.)
In this case, that's (IMO) because you are trying to make the "driver" do too much. If it did not know about the details of /what/ data was shown in an employment history then there would be no problem. More specifically still, if it did not /choose/ what data to display and pass that to the implementation method, then there would be no problem. I understand that the reason you have been lead to this position is to avoid the code duplication that would otherwise occur, but that is leading you astray (I think). Code duplication is not -- in itself -- bad. It's only bad when code is duplicated that is /necessarily/ the same in both places. That's to say if one version changed then the other /must/ change too. And in this case, the code duplication is accidental rather than necessary -- it's only an accident that (say) the PDF generator will display the same data as the HTML generator. So the fact that the PDF generator and HTML generator use the same accessors to find the employment history is not in itself an indicator of a problem. (Although there is probably scope for factoring out some utility code which can be shared between them).
Just because there is common code in (some of) the concrete subclasses does /not/ mean that you must move that code into the driver. Certainly you should consider whether it is appropriate to do so, but that decision should be based on whether that code should reasonably be expected to live in the driver -- whether it is part of the design of the driver-- the question is not whether it /can/ but whether it /must/.
Oh, by the way, I would take the large number of parameters (six) to this method to be another hint that the design is ill-balanced. If the driver code were not trying to do too much, then it would not be extracting all that data from the raw input, and so would not need to pass it to the writing methods.
-- chris
Andrew McDonagh - 10 Jan 2006 19:38 GMT >>Going through this thought process again has made me change my mind! As I >>wrote the preceding paragraph, I realized that it still bothered me to [quoted text clipped - 55 lines] > > -- chris I'd say the problems comes from trying to use inheritence to solve a problem that delegation is better suited to.
From whats been posted, I'd say have the concrete drivers classes as strategy objects and the abstract base class being a normal concrete driving class, would be a better design.
Each strategy class then gets the specific data it needs from the single 'model'? concrete class.
as in...
driver::createResume() { for each Strategy strategy.createResume(this); }
my 2 cents
Andrew
Oliver Wong - 10 Jan 2006 22:19 GMT > I'd say the problems comes from trying to use inheritence to solve a > problem that delegation is better suited to. Yes, I agree. It seems like the only thing Rhino's abstract super class does is provides a unified way of reading the data. That class should be renamed into "ResumeData" or something like that, and then passed into the resume-writing objects. The resume-writing objects query the DataReader to get the information they want, and write whatever they want. A quick sketch of the design might be something like:
<pseudocode> class ResumeData { public ResumeData(File resumeFile) { /* * Read in the data, populate the fields of this class. */ }
public String getName() { return this.name; }
public EmploymentHistory getEmploymentHistory() { return this.employmentHistory; } }
interface ResumeWriter { public void write(ResumeData data); }
class PDFResumeWriter implements ResumeWriter { public PDFResumeWriter(File outputFile) { this.outFile = outputFile; }
public void write(ResumeData data) { openReadersOn(this.outFile) getDataFrom(data); writeDataTo(this.outFile); } } </pseudocode>
- Oliver
Chris Smith - 11 Jan 2006 05:37 GMT > Yes, I agree. It seems like the only thing Rhino's abstract super class > does is provides a unified way of reading the data. The other thing (the more important thing, in fact) that the abstract superclass is doing is to call methods to write the pieces of the resume. A method like "writeResume" would exist in the parent, and it calls overridden methods to write each section in turn.
 Signature www.designacourse.com The Easiest Way To Train Anyone... Anywhere.
Chris Smith - Lead Software Developer/Technical Trainer MindIQ Corporation
Oliver Wong - 11 Jan 2006 15:41 GMT >> Yes, I agree. It seems like the only thing Rhino's abstract super >> class [quoted text clipped - 4 lines] > resume. A method like "writeResume" would exist in the parent, and it > calls overridden methods to write each section in turn. But presumably each type of resume (the PDF, HTML, etc.) might have a different set of sections (I believe Rhino mentioned he doesn't want the employment history in the ASCII version or something like that), so the generic writeResume() wouldn't know what set of methods to call anyway, and would thus probably just be abstract and body-less. In which case, again, I think it makes more sense to define an interface with a single writeResume() method.
- Oliver
Chris Smith - 11 Jan 2006 15:56 GMT > But presumably each type of resume (the PDF, HTML, etc.) might have a > different set of sections (I believe Rhino mentioned he doesn't want the [quoted text clipped - 3 lines] > think it makes more sense to define an interface with a single writeResume() > method. If that's the case, then it changes things of course. What was really said, though, is that the employment history should look different depending on the type of resume. Different formats would need different information fields... but the list of sections doesn't change.
 Signature www.designacourse.com The Easiest Way To Train Anyone... Anywhere.
Chris Smith - Lead Software Developer/Technical Trainer MindIQ Corporation
Rhino - 11 Jan 2006 18:22 GMT >> But presumably each type of resume (the PDF, HTML, etc.) might have a >> different set of sections (I believe Rhino mentioned he doesn't want the [quoted text clipped - 11 lines] > depending on the type of resume. Different formats would need different > information fields... but the list of sections doesn't change. You're right Chris; Oliver has misunderstood: each "section" of the resume (name, job target, employment history, etc. ) appears in every format of the resume. The exact appearance of a given section will vary slightly in some resumes though, as you (Chris) has understood.
For instance, the HTML version will look (roughly!!) like this (picture a table to contain the data):
EMPLOYMENT HISTORY
From/To Employer Role Main Tools 2003/2005 IBM Programmer Java, DB2 Developed programs for XYZ department
2001/2003 Foo Corp Programmer COBOL, IMS Developed programs for ABC department
The ASCII version will be very similar but omit the "From/To Employer Role Main Tools" heading and, of course, it won't be drawn in a table.
The PDF version will also omit the "From/To..." heading but each job will be a separate box.
So, the content of the Employment History is essentially identical in each case; only the presence or absence of that one heading distinguishes the resumes. But the layout of the data is quite different in each case.
Rhino
Chris Smith - 11 Jan 2006 18:46 GMT > For instance, the HTML version will look (roughly!!) like this (picture a > table to contain the data): [quoted text clipped - 10 lines] > The ASCII version will be very similar but omit the "From/To Employer Role > Main Tools" heading and, of course, it won't be drawn in a table. I had misunderstood slightly, then. As far as I can tell (correct me if I'm wrong), the table headers are not really resume-dependent data. You don't need to pass them into anything. Hard-coding English strings for the table headers is still perhaps a bad idea, but just have your HTML subclass get them from a ResourceBundle instead of trying to have the ResumeWriter superclass know about them. The only data you should make available via the superclass is that which is specific to a single resume.
 Signature www.designacourse.com The Easiest Way To Train Anyone... Anywhere.
Chris Smith - Lead Software Developer/Technical Trainer MindIQ Corporation
Rhino - 11 Jan 2006 23:58 GMT >> For instance, the HTML version will look (roughly!!) like this (picture a >> table to contain the data): [quoted text clipped - 13 lines] > > I had misunderstood slightly, then. Not as much as Oliver though :-)
> As far as I can tell (correct me if > I'm wrong), the table headers are not really resume-dependent data. You > don't need to pass them into anything. Hard-coding English strings for > the table headers is still perhaps a bad idea, but just have your HTML > subclass get them from a ResourceBundle instead of trying to have the > ResumeWriter superclass know about them. I'm not sure what you mean by resume-dependent data. The table headers, specificially the text strings "From/To", "Employer", "Role", and "Main Tools", already come from a ResourceBundle since I may want to change their precise wording; for example, I may want to change "Main Tools" to "Main Tools Used" and I'd like to only have to change the ResourceBundle, not the coding in the ResumeWriter class or ResumeWriter subclass. At present, while ResumeWriter is an abstract class - you haven't yet sold me on the virtues of making it a concrete class - I have a getter for each of those 4 Strings. Those then are the 4 "extra" Strings that distinguish the two versions of writeEmploymentHistory().
I _could_ modify the ResumeWriterHtml class so that its writeEmploymentHistory() method gets the 4 extra Strings directly; then the abstract ResumeWriter would only need the two-parameter writeEmploymentHistory() method and the subclasses would only need to implement that one version of the method. Then the writeEmploymentHistory() method would be self-sufficient in ResumeWriterAscii and ResumeWriterPdf, i.e. it wouldn't need anything but what was passed in via its parameters, plus any existing instance variables in the class. However, the writeEmploymentHistory() method in ResumeWriterHtml would need to get those 4 supplemental fields directly. That's not necessarily bad but I liked the fact that all of the information was being obtained in the concrete getters of ResumeWriter; that consistency would be gone if ResumeWriterHtml starts doing its own gets against the ResourceBundle. Is is bad design to do some of the getting at the subclass level and some at the abstract class level?
> The only data you should make > available via the superclass is that which is specific to a single > resume. Sorry? I don't follow. That seems to be exact opposite of what I'm doing now; _all_ of the data I'm writing in the ResumeWriter subclasses was supplied by getters in the ResumeWriter class. The only data that is specific to one resume is the 4 extra Strings and you just finished telling me to get them directly in the subclass, not the superclass.
Also, I'm really eager to see your reply to my remarks of 11/01/2006 12:34 PM; I had some key followup questions but it looks like you've missed them in this increasingly packed thread. Those answers will help me understand why ResumeWriter should be concrete instead of abstract and make it clear whether I should have one or two versions of getEmploymentHistory().
Rhino
Chris Smith - 12 Jan 2006 06:57 GMT > I'm not sure what you mean by resume-dependent data. I mean data that might conceivably change between two resumes that are both printed with your program.
> The table headers, > specificially the text strings "From/To", "Employer", "Role", and "Main > Tools", already come from a ResourceBundle since I may want to change their > precise wording; for example, I may want to change "Main Tools" to "Main > Tools Used" and I'd like to only have to change the ResourceBundle, not the > coding in the ResumeWriter class or ResumeWriter subclass. Great. I'm saying you don't need to provide that information to your ResumeWriter subclass via the ResumeWriter interface. Instead, have the subclass that needs that information look in a common location for the ResourceBundle, just as you'd handle an average internationalization problem. You don't want accessors in ResumeWriter to get the headers, or parameters to writeEmploymentHistory(), or anything like that.
> At present, while > ResumeWriter is an abstract class - you haven't yet sold me on the virtues > of making it a concrete class If I said to make it concrete, I mistyped. You should not make ResumeWriter concrete. (If you're referring to my comment to Chris Uppal... ignore it; Chris and I disagree anyway, and that's a good enough reason to not worry about making more changes than you need to. Even if you took my advice, you'd have to do more than JUST make ResumeWriter concrete.)
> I _could_ modify the ResumeWriterHtml class so that its > writeEmploymentHistory() method gets the 4 extra Strings directly; then the [quoted text clipped - 10 lines] > doing its own gets against the ResourceBundle. Is is bad design to do some > of the getting at the subclass level and some at the abstract class level? I see the two as fundamentally different. One of them is getting information that needs to go into this resume. The other is getting some parameter regarding the FORMAT of the resume. The latter belongs in the format-specific subclass.
> > The only data you should make > > available via the superclass is that which is specific to a single [quoted text clipped - 5 lines] > specific to one resume is the 4 extra Strings and you just finished telling > me to get them directly in the subclass, not the superclass. I mean one resume... not one type of resume. That is, ResumeWriter should provide data that's specific to John Smith's resume from London.
> Also, I'm really eager to see your reply to my remarks of 11/01/2006 12:34 > PM; I had some key followup questions but it looks like you've missed them > in this increasingly packed thread. Those answers will help me understand > why ResumeWriter should be concrete instead of abstract and make it clear > whether I should have one or two versions of getEmploymentHistory(). Okay, it looks like the confusion came because I forgot the word "abstract" in the sample code I posted in the parent of that article. I never intended for ResumeWriter to be concrete.
 Signature www.designacourse.com The Easiest Way To Train Anyone... Anywhere.
Chris Smith - Lead Software Developer/Technical Trainer MindIQ Corporation
Rhino - 12 Jan 2006 20:07 GMT >> I'd say the problems comes from trying to use inheritence to solve a >> problem that delegation is better suited to. [quoted text clipped - 39 lines] > } > </pseudocode> Forgive me for taking so long to respond to this suggestion as I have; I've been trying to understand and use the suggestions of Chris Smith and Chris Uppal and have only now gotten around to your remarks.
Your main observation makes a lot of sense: "It seems like the only thing Rhino's abstract super class does is provides a unified way of reading the data." That is exactly the case: I _am_ using my abstract superclass to get data from my ResourceBundle via a bunch of getters; the write methods are all abstract and each one is implemented differently in the classes that write the Resumes.
Your suggestions for a redesign follow logically from that observation. I think it makes perfect sense to make the abstract ResumeWriter into a class called ResumeData that contains only the getters and handle the writing via a different mechanism, an interface. I looked at your pseudocode and found myself somewhat unclear over exactly what you meant in some places; at first glance, it seemed you'd left a fair bit to my imagination and I have an (over?) active imagination :-)
But I knocked together the code you described and got it to work after a bit of trial and error. I even think I understand it. So now I'm going to try to describe it back to you (and anyone else who is still watching this thread) and make sure that I have written the code as you intended and that I understand how it works correctly.
Okay, let's start with ResumeData. I simply copied abstract class ResumeWriter to a new concrete class called ResumeData and deleted all of the abstract writeXXX methods.
Here is a brief excerpt of the class with the package statement and all Java and Javadoc comments removed for brevity:
------------------------------------------------------------------------------------------------------------ import java.util.ResourceBundle;
public class ResumeData {
private ResourceBundle resourceBundle;
public ResumeData(ResourceBundle resumeResourceBundle) { this.resourceBundle = resumeResourceBundle; }
public String getCandidateNameData() { String candidateNameData = this.resourceBundle.getString("CandidateNameData"); return candidateNameData; }
public String[] getCandidateAddressData() { String[] candidateAddressData = (String[]) this.resourceBundle.getObject("CandidateAddressData"); return candidateAddressData; } } ------------------------------------------------------------------------------------------------------------
Now, I'm afraid that the only interfaces I've written for myself contain only constants; I've never written any methods in my interfaces before. But I dug into some reference material to find out the rules for methods in interfaces and found that they are always public and abstract, just like the writeXXX methods had been in the abstract ResumeWriter class. Now, to prevent any confusion with other parts of this thread, I decided to name the interface that you call ResumeWriter in your pseudocode "ResumeWriterInterface"; that should make it crystal clear that I'm _not_ referring to the abstract ResumeWriter class.
So, here's the code for that interface, again with the package statement and all comments removed:
------------------------------------------------------------------------------------------------------------ public interface ResumeWriterInterface {
public void write(ResumeData resumeData); } ------------------------------------------------------------------------------------------------------------
Okay, so now I only need to implement ResumeWriterInterface in each class that writes a resume. That class will be free to write whatever data it wants, as long as it comes from ResumeData, in whatever format it likes. It will be able to choose any subset of that data and therefore write as much or as little of the information as it wants.
I created a new version of ResumeWriterAscii called ResumeWriterAscii2 to write the resume this new way.
Here is an excerpt of the code, again omitting the package statement, comments, for brevity. I've also omitted most of the writeXXX methods since the ones I've shown are enough to demonstrate what I'm doing so that you can tell me if I'm doing the right things: ------------------------------------------------------------------------------------------------------------
import java.io.PrintWriter; import java.util.ResourceBundle;
public class ResumeWriterAscii2 implements ResumeWriterInterface {
private PrintWriter printWriter = null;
public ResumeWriterAscii2(String outfile, ResourceBundle resourceBundle) {
ResumeUtilities.deleteFile(outfile); this.printWriter = ResumeUtilities.openOutputFile(outfile); ResumeData resumeData = new ResumeData(resourceBundle); write(resumeData); ResumeUtilities.closeOutputFile(this.printWriter); }
public final void write(ResumeData resumeData) {
writeCandidateAndTarget(resumeData.getCandidateNameData(), resumeData.getCandidateAddressData(), resumeData.getJobTargetText(), resumeData.getJobTargetData()); writeEmploymentHistory( resumeData.getEmploymentHistoryText(), resumeData.getEmploymentHistoryData()); writeEducationAndEmployability( resumeData.getFormalEducationText(), resumeData.getFormalEducationData(), resumeData.getRecentEducationText(), resumeData.getRecentEducationData(), resumeData.getEmployabilityText(), resumeData.getEmployabilityData()); writeSoftwareSkills(resumeData.getAddendumText(), resumeData.getOsText(), resumeData.getOsData(), resumeData.getComputerLanguagesText(), resumeData.getComputerLanguagesData(), resumeData.getDatabasesText(), resumeData.getDatabasesData(), resumeData.getWordProcText(), resumeData.getWordProcData(), resumeData.getEditorsText(), resumeData.getEditorsData(), resumeData.getOtherToolsText(), resumeData.getOtherToolsData()); writeOtherExperience(resumeData.getExperienceText(), resumeData.getExperienceData()); writeImportantNote(resumeData.getNoteData()); }
public final void writeCandidateAndTarget(String candidateNameData, String[] candidateAddressData, String jobTargetText, String jobTargetData) {
this.printWriter.println(this.stringUtils.wrap(candidateNameData.toUpperCase(), ResumeWriterAscii2.DELIMS, ResumeWriterAscii2.LINE_WIDTH, ResumeWriterAscii2.INDENT1, ResumeWriterAscii2.INDENT2_NORMAL, ResumeWriterAscii2.BLANK_LINES_BEFORE_0, ResumeWriterAscii2.BLANK_LINES_AFTER_1)); for (String candidateAddressLine : candidateAddressData) { this.printWriter.println(candidateAddressLine); } this.printWriter.println(this.stringUtils.wrap(jobTargetText + ": " + jobTargetData, ResumeWriterAscii2.DELIMS, ResumeWriterAscii2.LINE_WIDTH, ResumeWriterAscii2.INDENT1, ResumeWriterAscii2.INDENT2_NORMAL, ResumeWriterAscii2.BLANK_LINES_BEFORE_1, ResumeWriterAscii2.BLANK_LINES_AFTER_2)); }
}
------------------------------------------------------------------------------------------------------------
ResumeWriterAscii2 implements ResumeWriterInterface but doesn't extend anything. It gets an instance of ResumeData in its constructor, then passes it to the implemenation of the write() method which was defined in the interface. The write() method implementation uses the getters in the ResumeData class to obtain the information that is being written to the resume. The writeXXX methods in ResumeWriterAscii2 simply write the desired information to the file in the desired format, ASCII in this case.
The other file writer classes, ResumeWriterPdf2 and ResumeWriterHtml2, would have identical constructors to ResumeWriterAscii2. Their write() methods might vary somewhat; for example, ResumeWriterHtml2 will obtain a little bit more information from ResumeData, namely the "From/To", "Employer", "Role", and "Main Tools" strings. The writeXXX methods would take whatever data was obtained in the write() method and write it to the file in whatever format was desired.
So, have I understood you correctly? Is that basically what you intended?
Assuming it is, I have one followup question.
As I was writing ResumeWriterAscii2, it occurred to me that it could be defined a little differently and still work just fine; in fact, the code would actually get just a little more concise. The problem is that I'm not sure if this alternate approach would be better or worse from a design point of view. The alternate method is quite simple: make ResumeWriterAscii2 extend ResumeData _and_ implement ResumeWriterInterface.
------------------------------------------------------------------------------------------------------------
import java.io.PrintWriter; import java.util.ResourceBundle;
public class ResumeWriterAscii2 extends implements ResumeWriterInterface {
private PrintWriter printWriter = null;
public ResumeWriterAscii2(String outfile, ResourceBundle resourceBundle) {
super(resourceBundle); //new line ResumeUtilities.deleteFile(outfile); this.printWriter = ResumeUtilities.openOutputFile(outfile); // ResumeData resumeData = new ResumeData(resourceBundle); //no longer needed // write(resumeData); //replace with following line write(); //new version of the previous line ResumeUtilities.closeOutputFile(this.printWriter); }
// public final void write(ResumeData resumeData) { //replace with the following line public final void write() { //new version of the previous line
/* no longer need 'resumeData.' in front of each getter */
writeCandidateAndTarget(getCandidateNameData(), getCandidateAddressData(), getJobTargetText(), getJobTargetData()); writeEmploymentHistory( getEmploymentHistoryText(), getEmploymentHistoryData()); writeEducationAndEmployability( getFormalEducationText(), getFormalEducationData(), getRecentEducationText(), getRecentEducationData(), getEmployabilityText(), getEmployabilityData()); writeSoftwareSkills(getAddendumText(), getOsText(), getOsData(), getComputerLanguagesText(), getComputerLanguagesData(), getDatabasesText(), getDatabasesData(), getWordProcText(), getWordProcData(), getEditorsText(), getEditorsData(), getOtherToolsText(), getOtherToolsData()); writeOtherExperience(getExperienceText(), getExperienceData()); writeImportantNote(getNoteData()); }
public final void writeCandidateAndTarget(String candidateNameData, String[] candidateAddressData, String jobTargetText, String jobTargetData) {
this.printWriter.println(this.stringUtils.wrap(candidateNameData.toUpperCase(), ResumeWriterAscii2.DELIMS, ResumeWriterAscii2.LINE_WIDTH, ResumeWriterAscii2.INDENT1, ResumeWriterAscii2.INDENT2_NORMAL, ResumeWriterAscii2.BLANK_LINES_BEFORE_0, ResumeWriterAscii2.BLANK_LINES_AFTER_1)); for (String candidateAddressLine : candidateAddressData) { this.printWriter.println(candidateAddressLine); } this.printWriter.println(this.stringUtils.wrap(jobTargetText + ": " + jobTargetData, ResumeWriterAscii2.DELIMS, ResumeWriterAscii2.LINE_WIDTH, ResumeWriterAscii2.INDENT1, ResumeWriterAscii2.INDENT2_NORMAL, ResumeWriterAscii2.BLANK_LINES_BEFORE_1, ResumeWriterAscii2.BLANK_LINES_AFTER_2)); }
}
------------------------------------------------------------------------------------------------------------
Naturally, you also need to change the definition of the write() method in the ResumeWriterInterface() so that it doesn't expect any parameters.
So, is that variation any better or worse than what you originally proposed? If so why?
By the way, the approach you have suggested inadvertently solves another problem that I've been struggling with for the last while! I also have an applet that displays my resume but I was having a heck of a time trying to figure out how it could take advantage of the original abstract ResumeWriter class. That's because it doesn't write to a file: it creates GUI components and displays them in an applet; therefore the writerXXX methods don't apply to the applet. Since an applet has to subclass JApplet, I was at a loss in how to use ResumeWriter with the applet. Now that I have a concrete ResumeData class, I can simply use it to gather the data that I need, then use the existing methods within the applet to turn that data into GUI components without having to access the ResourceBundle directly in the ResumeApplet class.
Therefore, you've killed two birds with one stone, which is pretty good considering you didn't even know that the second bird even existed :-)
P.S. I'm copying you via email, Oliver, in case you've wandered away from this thread. I just want to make sure you get my thanks for pointing out an even better solution to my problem!
Rhino
Oliver Wong - 12 Jan 2006 21:12 GMT [snip]
> I knocked together the code you described and got it to work after a bit > of trial and error. I even think I understand it. So now I'm going to try > to describe it back to you (and anyone else who is still watching this > thread) and make sure that I have written the code as you intended and > that I understand how it works correctly. [code and explanation snipped]
Looks mostly good to me. However, I'd like to comment on your ResumeWriterAscii2 class, and in particular, the write() method. Here's the code you provided:
> public final void write(ResumeData resumeData) { > [quoted text clipped - 26 lines] > writeImportantNote(resumeData.getNoteData()); > } Personally, I don't like how writeCandidateAndTarget() and the others take about a million parameters. I would just pass in resumeData, and let them extract the data they want, than to pre-extract the data for them, and then pass all of them in. Otherwise, you're sort of placing knowledge of what writeCandidateAndTarget() is and isn't interested inside of write().
Maybe later on, your writeXXX() methods will become a lot "smarter" or more complex. For example, in your writeEmploymentHistory(), you write a full description of your responsibilities for your previous 3 jobs (and thus would need to get these "previous job responsibility" data items from the ResumeData), but not for any of the ones older than 3. Or perhaps writeEmploymentHistory() will query other parts of your ResumeWriter, to find out how much space is left on the page it's writing on, and vary the detail level it wants to get into accordingly.
What you could do is make ResumeData become hierarchical. It looks like you've done a bit of that, because you named some of the getters "getFooData()" instead of "getFooText()", but it looks like you haven't got a clear organizational system. For example, why is getExperienceText() not a part of getExperienceData()?
<example> int numberOfYears = resumeData.getExperienceData().getJobNumber(4).getYearsEmployedThere();
/*Use better method names than the above, they were named this way just for clarity*/ </example>
> ResumeWriterAscii2 implements ResumeWriterInterface but doesn't extend > anything. It gets an instance of ResumeData in its constructor, then [quoted text clipped - 3 lines] > resume. The |
|