Overloading abstract methods

C

Chris Smith

Rhino said:
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
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?

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.
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.

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.

--
www.designacourse.com
The Easiest Way To Train Anyone... Anywhere.

Chris Smith - Lead Software Developer/Technical Trainer
MindIQ Corporation
 
C

Chris Uppal

Chris said:
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.

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
 
C

Chris Uppal

Rhino wrote:

[me:]
It wouldn't make sense to put the two different writeEmploymentHistory()
methods in ResumeUtilities, right?
Agreed.


You're basically saying that it's okay
that ResumeWriterAscii and ResumeWriterPdf use the two parameter version
of writeEmploymentHistory() but have different implementations and that
ResumeWriterHtml uses the six-parameter version of
writeEmploymentHistory() and has yet another concrete implementation,
right?

Well, not really. If I were doing this (and were using an inheritance-based
approach) then I'd leave the concrete subclass to choose which data it needed
for creating an employment history. So the superclass (driver) would /not/
make that decision. The writeEmploymentHistory() would be parameterless (as
would all the other writeThisThatOrTheOther() methods) since it would be up to
the subclass to choose, find, and format the appropriate data. I would almost
certainly add utility methods to minimise avoidable code duplication, but I
wouldn't sweat if the HTML and PDF versions of writeEmploymentHistory() looked
pretty similar and even started off by using identical code to extract
identical data from the resource bundle.

Frankly, I'm strongly inclined to leave it alone. I just don't have any
more time to diddle around with this. I _have_ to start my job search
this week.

Nothing wrong with doing that. There's no law of God or Man that states that
software has to be perfect. Even less that it has to be perfect /now/.

Good luck !

-- chris
 
R

Rhino

Oliver Wong said:
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>
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
 
O

Oliver Wong

[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) {

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());
}

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*/
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.

I don't think the constructor should call write(). A constructor should
generally do very little other than actually construct or initialize the
object, and make it ready for use. I think the constructor should save the
ResumeData instance passed in into a field, and then the writeXXX() methods
become a parameterless, and will read their own field to get the data they
wants.

<example usage>
ResumeWriterAscii2 rwa2 = new ResumeWriterAscii2(resumeData,
"C:\myResume.txt");
rwa2.write();
</example usage>

Or if you don't intend to use rwa2 for anything else after it's done
writing:

<example usage>
new ResumeWriterAscii2(resumeData, "C:\myResume.txt").write();
</example usage>

[more snipping]
So, have I understood you correctly? Is that basically what you intended?

Yes, and yes.
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.
[code snipped]

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?

Probably worse. Inheritance should be used to represent an IS-A
relationship. A "Car" has an IS-A relationship with a "Vehicle" (that is, a
car IS A vehicle), so it probably makes sense for Car to extend Vehicle.
However, a ResumeWriter doesn't have an IS-A relationship with a ResumeData.
They are two distinct and seperate concepts.

The reason why this is important is that in most programming languages
(Java included), when you need to use an instance of class Foo, you could
just as well use an instance of a subclass of Foo.

Here's an example of where using inheritance the way you've suggested
may lead to problems in your program. Let's say you want to write an applet
to display the contents of ResumeData on the web. So your
ResumeDisplayingApplet class defines a constructor which expects a
ResumeData. But since ResumeWriterAscii2 inherits from ResumeData, that
means you could pass ResumeWriterAscii2 into the constructor of
ResumeDisplayingApplet! Probably not something you want to allow...
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 :)

I usually refer to the approach I suggested as "Model View Controller"
(and in fact, I think I did mention MVC in another one of your threads a
while ago, the one about batch processing), but others have (rightfully)
pointed out that maybe this isn't the best name for the pattern I've
recommended for you to use. One point of contention is that there is no
"Controller" involved here, so I started calling it "MV" or "Model View" in
that batch-processing thread.

The idea is simply to seperate the data you want to display, from the
actual behaviour of displaying that data. MVC solves two problems, one of
which is not relevant to your situation. The first problem it solves is
keeping all the views in sync, and is only of concern when your data
dynamically changes during the runtime of your application (which is not
your case, I think).

The second problem it solves, is it allows the developper to add new
views with minimal modification to existing code, which sounds exactly like
your situation.

MVC is known as a "design pattern"; a concept largely invented by four
people (referred to as the Gang of Four). They wrote a book, also called
"Design Patterns" which lists a few other problems that come up relatively
frequently in software engineering, and tried-and-tested solutions to those
problems.

Once you become more familiar with object oriented *programming* (as
opposed to OO *design*), and it sounds like you will soon become familiar
enough, I strongly recommend you look into design patterns, as they are a
fairly useful set of tools to know about.

- Oliver
 
R

Rhino

Oliver Wong said:
[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) {

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());
}

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.
Okay, I see your point and have revised the classes so that each of the
methods invoked in write() simply use ResumeData as the sole input
parameter. The individual writeXXX methods then use whichever getters they
need via resumeData.getXXX() calls. I see that this gives the writeXXX
methods the latititude to get any data they want at any time without the
write() method needing to know anything about it.
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>
My data isn't really organized very hierarchically; since it's all about one
person, me, it's relatively flat. Still, I'll mull your suggestion over and
see if there's a reasonable way to group the data better.
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.

I don't think the constructor should call write(). A constructor should
generally do very little other than actually construct or initialize the
object, and make it ready for use. I think the constructor should save the
ResumeData instance passed in into a field, and then the writeXXX()
methods become a parameterless, and will read their own field to get the
data they wants.

<example usage>
ResumeWriterAscii2 rwa2 = new ResumeWriterAscii2(resumeData,
"C:\myResume.txt");
rwa2.write();
</example usage>

Or if you don't intend to use rwa2 for anything else after it's done
writing:

<example usage>
new ResumeWriterAscii2(resumeData, "C:\myResume.txt").write();
</example usage>

[more snipping]
I'm already using the latter approach in the class that writes all of the
files; none of the classes that do the writing are ever needed again.

I'm not quite clear on what will remain in the constructor if I remove the
write() method from there. Pretty much everything that happens in the
constructor is either doing the writing or some supporting task, like
deleting the old version of the file. Should I perhaps just make the
constructors empty then and move the work currently in the constructors to
the write() method? I'd have to change the write() method to pass in two
additional parameters, the ResourceBundle and the name of the output file,
but that's dead easy....
Yes, and yes.
Oh good! I was pretty sure I was getting it but I didn't want to jump to any
conclusions.
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.
[code snipped]

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?

Probably worse. Inheritance should be used to represent an IS-A
relationship. A "Car" has an IS-A relationship with a "Vehicle" (that is,
a car IS A vehicle), so it probably makes sense for Car to extend Vehicle.
However, a ResumeWriter doesn't have an IS-A relationship with a
ResumeData. They are two distinct and seperate concepts.

The reason why this is important is that in most programming languages
(Java included), when you need to use an instance of class Foo, you could
just as well use an instance of a subclass of Foo.

Here's an example of where using inheritance the way you've suggested
may lead to problems in your program. Let's say you want to write an
applet to display the contents of ResumeData on the web. So your
ResumeDisplayingApplet class defines a constructor which expects a
ResumeData. But since ResumeWriterAscii2 inherits from ResumeData, that
means you could pass ResumeWriterAscii2 into the constructor of
ResumeDisplayingApplet! Probably not something you want to allow...
I had a feeling that making the ResumeWriterXXX classes subclass ResumeData
was a bad idea but I couldn't quite put my finger on why. Thanks for giving
me a sensible explanation.
I usually refer to the approach I suggested as "Model View Controller"
(and in fact, I think I did mention MVC in another one of your threads a
while ago, the one about batch processing), but others have (rightfully)
pointed out that maybe this isn't the best name for the pattern I've
recommended for you to use. One point of contention is that there is no
"Controller" involved here, so I started calling it "MV" or "Model View"
in that batch-processing thread.

The idea is simply to seperate the data you want to display, from the
actual behaviour of displaying that data. MVC solves two problems, one of
which is not relevant to your situation. The first problem it solves is
keeping all the views in sync, and is only of concern when your data
dynamically changes during the runtime of your application (which is not
your case, I think).

The second problem it solves, is it allows the developper to add new
views with minimal modification to existing code, which sounds exactly
like your situation.

MVC is known as a "design pattern"; a concept largely invented by four
people (referred to as the Gang of Four). They wrote a book, also called
"Design Patterns" which lists a few other problems that come up relatively
frequently in software engineering, and tried-and-tested solutions to
those problems.

Once you become more familiar with object oriented *programming* (as
opposed to OO *design*), and it sounds like you will soon become familiar
enough, I strongly recommend you look into design patterns, as they are a
fairly useful set of tools to know about.
I've had a link to an online Java Design Patterns resource for a while but
when I've looked at it in the past, it tended to largely go over my head.
Now, with working on this redesign and learning the reasoning behind the
decisions, I think Design Patterns will make more sense to me.

Thanks again for your help - and the help of everyone else who responded to
this thread! - for the very valuable suggestions that have improved my code
_and_ my understanding of OO design!

Rhino
 
C

Chris Uppal

Rhino said:
[... all snipped... ]

Some very quick observations:

Your choice of name "ResumeWriterInterface" may only have been for this one
posting. If not then I advise against giving interfaces names that /say/ they
are interfaces.

You are still doing too much work in the objects' constructors. An object is
created then used, and it is very nearly always better to keep the two phases
separate in the code.

I would make the ResumeData part of the ResumeWriter's state. I would /not/
make the ResumeWriter responsible for creating the ResumeData from the
ResourceBundle, in fact the ResumeWriter should not know anything at all about
ResourceBundles.

Personally, I would make the various flavours of ResumeWriter inherit from a
common abstract class. The abstract class would include code that was used by
all ResumeWriters -- such as opening and closing files. In fact I would
probably move all of the ResumeUtilities code into the common superclass.

ResumeData would be better named just Resume -- 'cos that's what it is.

-- chris
 
J

Juhan Kundla

Rhino wrote:

[...]
Now I need to redesign because I've got excessive parameters in some
methods....

Well, it's hardly surprising and I'm not trying to shoot the messenger; I
get uneasy when I have methods that have large numbers of parameters. One of
my methods, writeSoftwareSkills(), has 13 parameters! But I could break this
method up into 7 smaller methods, one that has a single parameter and six
that have two parameters each, e.g. writeProgrammingLanguages(String text,
String data), writeDatabases(String text, String data), writeIDEs(String
text, String data), etc.

Does not seem like a good idea to me. Why don't you extract the
parameter object and use polymorphism instead? The example below is not
probably very relevant to your situation, but HTH.

public interface WriterFactory {
public Writer createTableWriter(List<List<String>> data);
public Writer createListWriter(List<String> data);
public Writer createParagraphWriter(String data);
//etc
}


public class PDFWriterFactory implements WriterFactory {...}
public class TextWriterFactory implements WriterFactory {...}


public interface SoftwareSkill {
public void write(WriterFactory factory);
}

public class ProgrammingSkill implements SoftwareSkill {
public List<String> getProgrammingLanguages() {...}
public void write(WriterFactory factory) {
factory.createParagraphWriter("My programming skills are:").write();
factory.createListWriter(getProgrammingLanguages()).write();
}
}

public class DatabaseSkill implements SoftwareSkill {...}

// etc


And finally you ca use the class hierarchies like in the example below.

public class CV {
//...
writeSoftwareSkills(List<SoftwareSkill> skills) {
for (SoftwareSkill skill: skills) {
skill.write(someWriterFactory)
}
}
}


Juhan
 
O

Oliver Wong

[snip]
I'm not quite clear on what will remain in the constructor if I remove the
write() method from there. Pretty much everything that happens in the
constructor is either doing the writing or some supporting task, like
deleting the old version of the file. Should I perhaps just make the
constructors empty then and move the work currently in the constructors to
the write() method? I'd have to change the write() method to pass in two
additional parameters, the ResourceBundle and the name of the output file,
but that's dead easy....

Yes, assuming "ResourceBundle" here are purely for localization purposes
(e.g. the bundle does NOT vary from one person's resume to another's, if
both people are interested in producing a resume in the same language).

Probably your constructor will end up being not much longer than this:

public ResumeWriterAscii2(ResumeData rd) {
this.resumeData = rd;
}

- Oliver
 
O

Oliver Wong

Chris Uppal said:
Rhino said:
[... all snipped... ]

Some very quick observations:

Your choice of name "ResumeWriterInterface" may only have been for this
one
posting. If not then I advise against giving interfaces names that /say/
they
are interfaces.

This might be a matter of opinion though, as a very common practice is
to start interface names with the character 'I'. E.g. IResumeWriter.
Personally, I don't have much of a preference either way, and I've worked
both on projects which did clearly flag interfaces and those that didn't. I
just followed whatever the conventions of that particular project was.

[...]
I would make the ResumeData part of the ResumeWriter's state. I would
/not/
make the ResumeWriter responsible for creating the ResumeData from the
ResourceBundle, in fact the ResumeWriter should not know anything at all
about
ResourceBundles.

"ResourceBundle" has a specific meaning from Sun, I think, and I'm not
sure if Rhino is using the same meaning as they are. There's the file that
the ResumeData class reads to find out information about the person applying
for the job (e.g. their name, address, employment history, etc." and then
there are the files which contain locale-specific translations of terms that
appear in a resume (e.g. the English, French, Spanish, German, etc.
translation for the term "Employment History")

The ResumeWriter should definitely not directly interact with the file
that the ResumeData reads from, but it may make sense for the ResumeWriter
to interact with the locale-specific translation files.
Personally, I would make the various flavours of ResumeWriter inherit from
a
common abstract class. The abstract class would include code that was
used by
all ResumeWriters -- such as opening and closing files. In fact I would
probably move all of the ResumeUtilities code into the common superclass.

ResumeData would be better named just Resume -- 'cos that's what it is.

I agree with the above two paragraphs.

- Oliver
 
C

Chris Uppal

Oliver Wong wrote:

[me:]
This might be a matter of opinion though, as a very common practice is
to start interface names with the character 'I'.

It may be common, but it is nonetheless completely wrong-headed.

I would make the ResumeData part of the ResumeWriter's state. I would
/not/
make the ResumeWriter responsible for creating the ResumeData from the
ResourceBundle, in fact the ResumeWriter should not know anything at all
about
ResourceBundles.
[...]
The ResumeWriter should definitely not directly interact with the file
that the ResumeData reads from, but it may make sense for the ResumeWriter
to interact with the locale-specific translation files.

Good point. I hadn't thought of that possibility.


-- chris
 
R

Rhino

Chris Uppal said:
Rhino said:
[... all snipped... ]

Some very quick observations:

Your choice of name "ResumeWriterInterface" may only have been for this
one
posting. If not then I advise against giving interfaces names that /say/
they
are interfaces.
Agreed! I only chose that name to make it very clear to everyone still
watching this thread that I was talking about an interface that imitated the
ResumeWriter _interface_ that Oliver proposed in his pseudocode, _not_ the
abstract ResumeWriter _class_ that I'd been using earlier.
You are still doing too much work in the objects' constructors. An object
is
created then used, and it is very nearly always better to keep the two
phases
separate in the code.
Okay....

I would make the ResumeData part of the ResumeWriter's state. I would
/not/
make the ResumeWriter responsible for creating the ResumeData from the
ResourceBundle, in fact the ResumeWriter should not know anything at all
about
ResourceBundles.

Personally, I would make the various flavours of ResumeWriter inherit from
a
common abstract class. The abstract class would include code that was
used by
all ResumeWriters -- such as opening and closing files. In fact I would
probably move all of the ResumeUtilities code into the common superclass.

ResumeData would be better named just Resume -- 'cos that's what it is.

Sorry, I think I need you to sketch out how your proposal would actually
look in code. I've been through so many iterations now that I'm not sure
just what you mean. Obviously, you don't need to write all of the code but
some fragments showing just what classes and interfaces you are proposing
and what each class extends and implements would be very helpful right now.

Rhino
 
R

Rhino

Oliver Wong said:
Chris Uppal said:
Rhino said:
[... all snipped... ]

Some very quick observations:

Your choice of name "ResumeWriterInterface" may only have been for this
one
posting. If not then I advise against giving interfaces names that /say/
they
are interfaces.

This might be a matter of opinion though, as a very common practice is
to start interface names with the character 'I'. E.g. IResumeWriter.
Personally, I don't have much of a preference either way, and I've worked
both on projects which did clearly flag interfaces and those that didn't.
I just followed whatever the conventions of that particular project was.
I haven't seen the convention of starting interface names with an 'I' but I
suppose I could live with it although I'm inclined not to do that. I
certainly didn't plan to create an interface named ResumeWriterInterface;
that name was simply chosen to clarify that I wasn't talking about the
former ResumeWriter abstract class.
[...]
I would make the ResumeData part of the ResumeWriter's state. I would
/not/
make the ResumeWriter responsible for creating the ResumeData from the
ResourceBundle, in fact the ResumeWriter should not know anything at all
about
ResourceBundles.

"ResourceBundle" has a specific meaning from Sun, I think, and I'm not
sure if Rhino is using the same meaning as they are. There's the file that
the ResumeData class reads to find out information about the person
applying for the job (e.g. their name, address, employment history, etc."
and then there are the files which contain locale-specific translations of
terms that appear in a resume (e.g. the English, French, Spanish, German,
etc. translation for the term "Employment History")
I'm using ResourceBundle in _both_ senses. I have actually created a file
called ResumeList.java which contains locale-specific translations of my
resume data in my default locale, English/Canada. Although I don't actually
expect to translate the resume to English/French for example, I wanted to
leave my options open in case that became necessary; if it does, I'll copy
ResumeList.java to ResumeList_fr_CA and translate the contents of the file.
Ditto for any additional locales should that become necessary.
The ResumeWriter should definitely not directly interact with the file
that the ResumeData reads from, but it may make sense for the ResumeWriter
to interact with the locale-specific translation files.
Define 'directly interact'. ResumeWriterAscii and its siblings instantiate
ResumeData and use it to invoke the getters that read the ResourceBundle.
Does that constitute "direct interaction"?
I agree with the above two paragraphs.

- Oliver
I'm not quite clear what Chris is envisioning here. Are we talking about
going back to the ResumeWriter abstract class that I had before but with
differences:
- the ResumeUtilities methods would get moved to the abstract class
- the writeXXX methods would be restored to - or left out of? - the abstract
class

What are _you_ envisioning, Oliver? I've already asked Chris to elaborate.

Rhino
 
O

Oliver Wong

Rhino said:
Define 'directly interact'. ResumeWriterAscii and its siblings instantiate
ResumeData and use it to invoke the getters that read the ResourceBundle.
Does that constitute "direct interaction"?

I'll put it this way: If later on, you decide to change ResumeData so
that it reads from a SQL Database instead of a file, you will need to make
zero changes to ResumeWriterAscii. Similarly, if you decide to change the
fileformat for the file you're reading from, or if you have ResumeData query
the user for their employment history in a fancy GUI, like some sort of
"automatic resume generating wizard".

In all cases, ResumeWriterFoo doesn't know or care where the data is
coming from.
I'm not quite clear what Chris is envisioning here. Are we talking about
going back to the ResumeWriter abstract class that I had before but with
differences:
- the ResumeUtilities methods would get moved to the abstract class
- the writeXXX methods would be restored to - or left out of? - the
abstract class

What are _you_ envisioning, Oliver? I've already asked Chris to elaborate.

I'd have to see ResumeUtilities (or parts of it), to be sure, but
usually giving a class the name "Utilities" implies a not-so-OO design. It
usually means you have a bunch of functions floating around, not associated
with any one particular object, so you decided to gather them together and
call them a set of utilities.

If the only class using the methods in ResumeUtilities are the
ResumeWriters, then it probably makes sense to move those methods into
ResumeWriter (and possibly making them private or protected).

As for the writeXXX, I'd leave them where they are (i.e. NOT in the
abstract super class). To do otherwise would force any future subclass of
ResumeWriter to implement those methods, without bringing much gain (since
the writeXXX are only called internally anyway).

Since you say the sections never change, then perhaps your write()
method is the same for all ResumeWriterFoo? If so, then go ahead and move
that into the super class ResumeWriter. If they differ, then leave them
where they are.

- Oliver
 
R

Rhino

Oliver Wong said:
I'll put it this way: If later on, you decide to change ResumeData so
that it reads from a SQL Database instead of a file, you will need to make
zero changes to ResumeWriterAscii. Similarly, if you decide to change the
fileformat for the file you're reading from, or if you have ResumeData
query the user for their employment history in a fancy GUI, like some sort
of "automatic resume generating wizard".

In all cases, ResumeWriterFoo doesn't know or care where the data is
coming from.
Okay, that makes good sense to me since it means that I can get the data
from some other source entirely, like a database, without having to change
any of the other classes. By that standard my code is already avoiding
direct interaction since the getters are in ResumeData; ResumeWriterFoo
invokes those getters via an instance variable but nothing in
ResumeWriterFoo would change if the code in ResumeData changed to get the
data from a different place.
I'd have to see ResumeUtilities (or parts of it), to be sure, but
usually giving a class the name "Utilities" implies a not-so-OO design. It
usually means you have a bunch of functions floating around, not
associated with any one particular object, so you decided to gather them
together and call them a set of utilities.

That's pretty much exactly what happened :)

Here is the code, with package statement, imports, and comments omitted:
--------------------------------------------------------------------------------------------------------------
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io_OutputStreamWriter;
import java.io.PrintWriter;

import ca.maximal.common.utilities.DateTimeUtils;

public class ResumeUtilities {

public static final String CLASS_NAME = "ResumeUtilities";

public static void deleteFile(String fileName) {

/* Delete the old version of the output file. */
File outfile = new File(fileName);
outfile.delete();
}

public static PrintWriter openOutputFile(String fileName) {

String METHOD_NAME = "openOutputFile()";

FileOutputStream fileOutputStream = null;
OutputStreamWriter outputStreamWriter = null;
PrintWriter printWriter = null;

try {
fileOutputStream = new FileOutputStream(fileName, true);
} catch (FileNotFoundException excp) {
System.err.println(CLASS_NAME + "." + METHOD_NAME + " - Tried to
open file " + fileName + ". Error: " + excp);
excp.printStackTrace();
return printWriter;
}

outputStreamWriter = new OutputStreamWriter(fileOutputStream);
printWriter = new PrintWriter(new
BufferedWriter(outputStreamWriter), true);

return printWriter;
}

public static void closeOutputFile(PrintWriter printWriter) {

printWriter.flush();
printWriter.close();
}

public static String getGeneratedBy(String className) {

String currentDate = DateTimeUtils.getCurrentDateIso();
String currentTime = DateTimeUtils.getCurrentTime();

String generatedBy = "This file was generated by " + className + "
on " + currentDate + " at " + currentTime + ".";

return generatedBy;
}


}

--------------------------------------------------------------------------------------------------------------
Basically, these methods occurred in _almost_ every one of my file
generating classes. (Remember, _some_ of the files being generated are
resumes but some are not; the files which aren't resumes include a Javadoc
overview, an HTML page that has links to the different resume files, an HTML
page that displays the resume version of the applet, files containing CSS,
and a PDF that contains professional references who can vouch for me.)

Hmmm.....

I've wondered several times if I should have an abstract class with a name
like FileWriter that contains methods shared by _all_ of the classes which
write files, then have ResumeWriterAscii and its siblings be subclasses of
FileWriter. If I did that, FileWriter would be a logical place to put the
methods in ResumeUtilties so that they would be available to all of the
classes that write files, whether they are resumes or other files. This
version of the design also seems like a more complete design since it
recognizes _all_ of the files I'm writing, not just the resumes.

In fact, I've just made a lengthy exploration of that exact design. (I made
one wrong turn that messed me up for a while but then I reasoned my way out
of it.) I was able to make this new design work.

So, here's what I came up with.

I created a new file called FileWriter. It is essentially just the
FileUtilities class I showed you earlier in this note but I've made the
formerly static methods non-static and added an empty constructor. At the
moment, FileWriter is abstract but I don't quite see the point in leaving it
that way since it has no abstract methods in it; unless you can suggest a
good reason, I'm going to make it concrete.

Now, all of the classes that write files, regardless of whether they are
resume files or other supporting files, subclass FileWriter. Since
ResumeApplet does _not_ WRITE a resume - it displays it in the applet GUI,
ResumeApplet does _not_ subclass FileWriter.

ResumeGenerator got a bit of a facelift and now looks like this (package,
constants, and comments omitted):
--------------------------------------------------------------------------------------------------------------
public class ResumeGenerator3 implements ResumeConstants,
ResumeGeneratorConstants {

public static void main(String[] args) {

new ResumeGenerator3();
}

public ResumeGenerator3() {

ResourceBundle resumeResources =
LocalizationUtils.getResources(Locale.getDefault(), LIST_BASE);

ResumeData resumeData = new ResumeData(resumeResources);

new ResumeWriterHtml3().writeResume(OUTPUT_FILE_PATH +
RESUME_HTML_FILE, resumeData);
new ResumeWriterAscii3().writeResume(OUTPUT_FILE_PATH +
RESUME_ASCII_FILE, resumeData);
new ResumeWriterPdf3().writeResume(OUTPUT_FILE_PATH +
RESUME_PDF_FILE, resumeData);
new ResumeMainHtml3().writeFile(OUTPUT_FILE_PATH +
RESUME_MAIN_HTML_FILE);
new ResumeExampleHtml3().writeFile(OUTPUT_FILE_PATH +
RESUME_EXAMPLE_HTML_FILE);
new ResumeCssDisplay3().writeFile(OUTPUT_FILE_PATH +
CSS_DISPLAY_FILE);
new ResumeCssPrint3().writeFile(OUTPUT_FILE_PATH + CSS_PRINT_FILE);
new ResumeJavadocOverview3().writeFile(OUTPUT_FILE_PATH +
RESUME_JAVADOC_OVERVIEW_FILE);
new ResumeReferencesPdf3().writeFile(OUTPUT_FILE_PATH +
RESUME_REFERENCES_PDF_FILE);

return;
}

}
--------------------------------------------------------------------------------------------------------------

I realized that it made no sense to instantiate ResumeData more than once
since I wanted all of the resume writing classes to use the exact same data;
why pay for creation of ResumeData three times? (I will have to pay for it
one additional time, when I run ResumeApplet.) So, I simply do the work once
in ResumeGenerator: I get the ResourceBundle, then use that to get the
ResumeData. Then, I instantiate each file writing class and invoke the
method that writes that particular file, passing it the name of the output
file and the resume data if it is needed by that class.

[Side note: am I correct in understanding that
new Foo().writeFile();
instantiates class Foo, performs its no-parameter constructor, then executes
method writeFile()? I've never seen that approach before but it seems like
that is what it should do.]

The various file writing classes got modified so that they have a trivial
constructor, then do the real work in the write() method.

Here is an example of one of classes that creates a non-resume file:

--------------------------------------------------------------------------------------------------------------
public class ResumeCssPrint3 extends FileWriter implements ResumeConstants,
ResumeGeneratorConstants {

final String MY_CLASS_NAME = getClass().getName();

public ResumeCssPrint3() {

super();
}

public void writeFile(String outfile) {

deleteFile(outfile);

PrintWriter printWriter = openOutputFile(outfile);

String whiteHex = ColorConversionUtils.getHexColor(Color.WHITE);
String blackHex = ColorConversionUtils.getHexColor(Color.BLACK);

/* Write the CSS. */
printWriter.println("/* " +
ResumeUtilities.getGeneratedBy(this.MY_CLASS_NAME) + " */");
printWriter.println("/* Link in HTML to stylesheet */");
printWriter.println("/* <LINK rel=\"stylesheet\" TYPE=\"text/css\"
MEDIA=\"print\" HREF=\"" + CSS_PRINT_FILE + "\"> */");
printWriter.println("");
printWriter.println("/* " + RESUME_UNICODE_CAMELCASE + " Style Sheet
*/");
printWriter.println("");

...

closeOutputFile(printWriter);

System.out.println("The print version of the CSS for the " +
RESUME_UNICODE_LOWERCASE + " pages was written to file " + outfile + ".");

return;
}

}
--------------------------------------------------------------------------------------------------------------


And here is an example of one of the classes that creates a resume:
--------------------------------------------------------------------------------------------------------------
public class ResumeWriterAscii3 extends FileWriter implements ResumeWriter3,
ResumeConstants {

public ResumeWriterAscii3() {

super();
}

public final void writeResume(String outfile, ResumeData resumeData) {

deleteFile(outfile);

PrintWriter printWriter = openOutputFile(outfile);

StringUtils stringUtils = new StringUtils();

/* Write the candidate name and address data. */
printWriter.println(stringUtils.wrap(resumeData.getCandidateNameData().toUpperCase(),
ResumeWriterAscii3.DELIMS, ResumeWriterAscii3.LINE_WIDTH,
ResumeWriterAscii3.INDENT1, ResumeWriterAscii3.INDENT2_NORMAL,
ResumeWriterAscii3.BLANK_LINES_BEFORE_0,
ResumeWriterAscii3.BLANK_LINES_AFTER_1));
for (String candidateAddressLine :
resumeData.getCandidateAddressData()) {
printWriter.println(candidateAddressLine);
}

/* Write the job target. */
printWriter.println(stringUtils.wrap(resumeData.getJobTargetText() +
": " + resumeData.getJobTargetData(), ResumeWriterAscii3.DELIMS,
ResumeWriterAscii3.LINE_WIDTH, ResumeWriterAscii3.INDENT1,
ResumeWriterAscii3.INDENT2_NORMAL, ResumeWriterAscii3.BLANK_LINES_BEFORE_1,
ResumeWriterAscii3.BLANK_LINES_AFTER_2)); //$NON-NLS-1$

...

/* Close the output file. */
closeOutputFile(printWriter);

/* Tell the user where to find the report. */
System.out.println("The ASCII version of the " +
RESUME_UNICODE_LOWERCASE + " was written to file " + outfile + ".");

}
}
--------------------------------------------------------------------------------------------------------------

As you can see, I've gone ahead and moved all the code that used to be in
the small writeXXX methods into writeResume(). The whole class is less than
200 lines, even including the constants and comments. See below for why I
did this....
If the only class using the methods in ResumeUtilities are the
ResumeWriters, then it probably makes sense to move those methods into
ResumeWriter (and possibly making them private or protected).

As for the writeXXX, I'd leave them where they are (i.e. NOT in the
abstract super class). To do otherwise would force any future subclass of
ResumeWriter to implement those methods, without bringing much gain (since
the writeXXX are only called internally anyway).

Since you say the sections never change, then perhaps your write()
method is the same for all ResumeWriterFoo? If so, then go ahead and move
that into the super class ResumeWriter. If they differ, then leave them
where they are.
The write() method in ResumeWriterAscii and its siblings does _not_
currently have the same implementation in each class that implements it -
but it _could_ easily enough. After you expressed your discomfort with my
original formulation of the write() method in ResumeWriterAscii, I changed
the write method as follows:

-----------------------------------------------------------------------------------------------------
public final void writeResume(ResumeData resumeData) {

writeCandidateAndTarget(resumeData);
writeEmploymentHistory(resumeData);
writeFormalEducation(resumeData);
writeRecentEducation(resumeData);
writeEmployability(resumeData);
writeAddendum(resumeData);
writeOses(resumeData);
writeComputerLanguages(resumeData);
writeDatabases(resumeData);
writeWordProcessors(resumeData);
writeEditors(resumeData);
writeOtherTools(resumeData);
writeOtherExperience(resumeData);
writeImportantNote(resumeData);
}

-----------------------------------------------------------------------------------------------------

The write() method for the other ResumeWriterXXX classes are very similar
except that some have some extra methods.

But it would be very easy to simply take the code from each of the writeXXX
methods and simply move it into the write() method; then it wouldn't invoke
any other methods at all and simply write the entire resume in one long but
very simple method; there are a couple of simple loops but not a single if
or case. Frankly, I'm not sure I see any point in splitting the writing up
amongst all these little writeXXX methods aside from the fact that each
writeXXX method is very short, just a few lines long. That makes the
functionality of each writeXXX method very simple to understand.

I know there are people who instantly loathe methods that are longer than a
handful of lines no matter what code is contained within the method and who
ruthlessly break code up so that no method contains more than, say, 7 lines.
Frankly, I've never quite understood that reasoning; to me, there's nothing
wrong with a long method, even hundreds of lines long, as long as it is very
linear. But if it contains more than very small number of ifs or branches, I
am inclined to break it into smaller pieces. With regards to my write()
methods though, they would be longish but very very linear so I don't see
much harm in doing all the writing directly within the write() method of
each ResumeWriterXXX class.

Since I was making the other changes that I mentioned earlier in the note
anyway, I went ahead and moved the writeXXX code into the write() method in
each of the ResumeWriterXXX classes _except_ ResumeWriterPdf; that one had
its writeXXX methods left alone because there was somewhat more code
involved in writing the PDF so it made more sense to retain the existing
small writeXXX methods.

Now, I would be curious to hear your reaction to my new design, particularly
the new class FileWriter and how it is used. Is it a step forward or
backward? Is it a reasonable design?

Frankly, I'm a bit uneasy about FileWriter since I'm not quite sure what it
_is_. I can't really say that it is a class that can create and write a file
since there are no write methods in it, just ones for deleting, opening, and
closing files. All it can really do is create an _empty_ file. So maybe it
needs some kind of (abstract?) write methods in it that can be implemented
in its subclasses - or _not_ implemented in the ResumeWriterXXX classes
since they already implement write() (actually, I've renamed it
writeResume() as you may have noticed in my code excerpts). I'm also not
sure that the ResumeWriterXXX classes subclass it. Then again, it all
_works_ very well so maybe it is not so bad. And I can't think of anything
else that particularly bothers me about the code now.

I'd be interested in your opinion - as well as anyone else who is still
watching this thread.

Rhino
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,777
Messages
2,569,604
Members
45,234
Latest member
SkyeWeems

Latest Threads

Top