You may start from a different point of view to apply "Single Responsibility Principle" here. What you have shown to us is (more or less) only the data model of your application. SRP here means: make sure your data model is responsible only for keeping data - no less, no more.
So when you are going to read your XML file, create a data model from it and write SQL, what you should not do is implement anything into your Table
class which is XML or SQL specific. Your want your data flow look like this:
[XML] -> ("Read XML") -> [Data model of DB definition] -> ("Write SQL") -> [SQL]
So the only place where XML specific code should be placed is a class named, for instance, Read_XML
. The only place for SQL specific code should be a class like Write_SQL
. Of course, maybe you are going to split those 2 tasks into more sub-tasks (and split your classes into multiple manager classes), but your "data model" should not take any responsibility from that layer. So don't add a createStatement
to any of your data model classes, since this gives your data model responsibility for the SQL.
I don't see any problem when you are describing that a Table is responsible for holding all it's parts, (name, columns, comments, constraints ...), that is the idea behind a data model. But you described "Table" is also responsible for the memory management of some of its parts. That's a C++ specific issue, which you would not face so easily in languages like Java or C#. The C++ way of getting rid of those responsibility is using smart pointers, delegating ownership to a different layer (for example, the boost library or to your own "smart" pointer layer). But beware, your cyclic dependencies may "irritate" some smart pointer implementations.
Something more about SOLID: here is nice article
http://cre8ivethought.com/blog/2011/08/23/software-development-is-not-a-jenga-game
explaining SOLID by a small example. Let's try to apply that to your case:
you will need not only classes Read_XML
and Write_SQL
, but also a third class which manages the interaction of those 2 classes. Lets call it a ConversionManager
.
Applying DI principle could mean here: ConversionManager should not create instances of Read_XML
and Write_SQL
by itself. Instead, those objects can be injected through the constructor. And the constructor should have a signature like this
ConversionManager(IDataModelReader reader, IDataModelWriter writer)
where IDataModelReader
is an interface from which Read_XML
inherits, and IDataModelWriter
the same for Write_SQL
. This makes a ConversionManager
open for extensions (you very easily provide different readers or writers) without having to change it - so we have an example for the Open/Closed principle. Think about it what you will have to change when you want to support another database vendor -ideally, you don't have to change anything in your datamodel, just provide another SQL-Writer instead.
First, lets understand what was the issue:
The Staple job knows about many more methods which it doesn't use today, (e.g., the print
method) but which are available for it to use, should it want to.
However, the Job class "thinks" that the Staple class will be "a good citizen" and never use the print method at all.
There are many potential big issues here -
For some reason, the Staple job may start using the print method - by accident or intentionally.
Then down the road, either any changes to the print method may go untested,
OR, any changes to the print method will trigger a regression test in the Staple job also,
AND, any impact analysis for changes to the print job will necessarily involve impact analysis of the staple job too.
This is just the issue of Staple knowing about the print functions. Then there's the case of the Print job knowing all about stapling functions. Same problems.
Very soon, this system would reach a point where any change will require a full blown impact analysis of each module and a full blown regression test.
Another problem is that today, all jobs which can be printed can be stapled, and vice versa on this particular printer.
However, tomorrow, there could be a need to install the same firmware on a device that only prints or only staples. What then? The code already assumes that all Jobs are printable and stapleable. So any further granular breakdown / simplification of responsibilities is impossible.
In more recent terms, imagine a class called "AppleDevice" which has functions for MakePhoneCall as well as PlayMusic. Now your problem is while you can easily use this on an iPhone, you cannot use it for an iPod since the iPod cannot make phone calls.
So, the issue is not that the Job class is all-powerful. In fact, that's how it should be, so that it can act as a common link in the entire "workflow" where someone may scan a job, then print it, then staple it etc.
The problem is that the usage of all its methods is not restricted. Anyone and everyone could use and abuse any method whenever they want to, thus making the maintenance difficult.
Hence, the Dependency Injection approach of only telling users "exactly what they need to know, and nothing more" ensures that calling modules only use the code that they are meant to.
A sample implementation would look like:
interface IStapleableJob { void stapleYourself(); }
interface IPrintableJob { void printYourself(); }
class Job implements IStapleableJob, IPrintableJob {
....
}
class Staple {
public static void stapleAllJobs(ArrayList<IStapleableJob> jobs) {
for(IStapleableJob job : jobs) job.stapleYourself();
}
}
class Print {
public static void stapleAllJobs(ArrayList<IPrintableJob> jobs) {
for(IPrintableJob job : jobs) job.printYourself();
}
}
Here, even if you pass a Job object to the Staple and Print methods, they dont know that its a Job, so they cannot use any methods that they are not supposed to. Thus, when you make any changes to a module, your scope of impact analysis and regression testing is restricted. That's the problem that ISP solves.
Best Answer
Using an API should not lead to breaking any principles unless the API is severely broken. If some API has more features than needed in some application, which is quite normal, it's always possible to hide the API behind a restricted custom interface in the consumer end. Problem solved, if there ever was one.
However, if your API has a lot of consumers and those consumers could have conflicting interests, then it's probably wise to consider splitting the interface so that interfaces are only shared between consumers with similar concerns. It's best to avoid situations where a change to the API needed by some consumer leads to unwanted changes in dozens of consumers. This is the rationale behind ISP.
Consumers that need nothing but read and find operations will typically have different interests than consumers that need to alter the data. Having a consumer that only creates and reads entities is probably not the most common scenario.