Perhaps, this isn't the answer you expect, but I believe strongly, that you should put more effort in your design decisions, than making bad decisions and invest most of the time in thinking about how to implement this bad design.
Now I have two different ways I might want to store these songs in a database:
1) just two columns; one for ID, one for the whole text of the song
2) three columns; one for ID, one for line number, and one for that line
Yes. These are the possibilities. But two questions coming to my mind:
1) Why do you want 2 different formats (at the same time)?
2) Does the second design even make sense?
What would be the advantage of storing the line number and the content of that line? The only case, when it might eventually somehow would make sense is, when you oftentimes have queries
get me exactly line n from the song x and even then I see no justification to store text line by line as column rows. Put the text as
TEXT in the DB and do whatever magic you want to do on that in-memory after you retrieved it from the DB
So I need a way of swapping out how songs are written/read from/to the database. That might even mean making a runtime decision on which schema I use. What is the proper OO way of doing this? I feel like the Strategy pattern would work here, but I can't connect it all together. Some questions I've been struggling with over this
The strategy would be a good fit for that; so you could change the
strategy how to store or retrieve data at will.
Should there be a separate object for each read and each write? Or should one class implement both a read and a write?
The typical pattern for persistence is the
repository. The job of the repository is to abstract away details of persitence. And it would be the repository, which would use the
How do I avoid duplicated logic? For example, method (2) means the write method splits the song on newlines, while the read method joins them. Is there a way to combine the similar logic here?
It depends on your implementation. Sure, there are ways to prevent duplication.
What if Song didn't have public setters? How does the design change?
You have to implement some way, how your song represents state.
Since you need it to set at least at object creation, you would write a
constructor such, that your object is initialized in the desired way.
Letting out (public)
setters, you make the object immutable which could be what you wanted. The advantage of immutable objects is simple: they are astoundingly boring; they have always the same state - which is nice in multithreaded context. There are no surprises, that state was altered unintentionally.