Tomando tus ejemplos de PDF como punto de partida, veamos esto.
enlace
El principio de responsabilidad única sugiere que un objeto debe tener uno y solo un objetivo. Ten esto en cuenta.
enlace
El principio de separación de inquietudes nos dice que las clases no deben tener funciones superpuestas.
Cuando observas estos dos, sugieren que la lógica debe ir en una clase solo si tiene sentido, solo si esa clase es responsable de hacerlo.
Ahora, en su ejemplo de PDF, la pregunta es, ¿quién es el responsable de imprimir? ¿Qué tiene sentido?
Primer fragmento de código:
Pdf pdf = new Pdf();
pdf.Print();
Esto no es bueno. Un documento PDF no se imprime solo. Se imprime por ... ta da! .. una impresora. Así que tu segundo fragmento de código es mucho mejor:
Pdf pdf = new Pdf();
PdfPrinter printer = new PdfPrinter();
printer.Print(pdf);
Esto tiene sentido. Una impresora Pdf imprime un documento pdf. Mejor aún, una impresora no debería ser una impresora PDF o una impresora fotográfica. Simplemente debe ser una impresora capaz de imprimir todo lo que se le envíe lo mejor que pueda.
Pdf pdf = new Pdf();
Printer printer = new Printer();
printer.Print(pdf);
Así que eso es simple. Poner métodos donde tengan sentido. Obviamente, no siempre es así de simple. Tome las estadísticas de su país, por ejemplo:
Country m = new Country("Mexico");
double ratio = m.GetDebtToGDPRatio();
Su preocupación es que podría haber n número de estadísticas, y que no deberían estar en una clase de País. Eso es verdad. Sin embargo, si su modelo solo solicita estadísticas particulares, este ejemplo de modelado podría estar bien.
En este caso, podría decir lógicamente que un país debería poder calcular sus propias estadísticas, específicas para su modelo y los requisitos disponibles.
Y ahí está la cosa: ¿cuáles son sus requisitos? Sus requisitos impulsarán la forma en que modela el mundo, el contexto en el que se deben cumplir estos requisitos.
Si efectivamente tiene un número de estadísticas de multitudes / variables, entonces su segundo ejemplo tiene más sentido:
Country m = new Country("Mexico");
DebtStatistics ds = new DebtStatistics();
double usRatio = ds.GetDebtToGDPRatio(m);
Mejor aún, tenga una superclase o interfaz abstracta llamada Estadísticas que toma a un país como parámetro:
interface StatisticsCalculator // or a pure abstract class if doing C++
{
double getStatistics(Country country); // or a pure virtual function if in C++
}
la clase DebtToGDPRatioStatisticsCalculator implementa StatisticsCalculator ....
la clase InfantMortalityStatisticsCalculator implementa StatisticsCalculator ...
Y así sucesivamente y así sucesivamente. Lo que lleva a lo siguiente: generalización, delegación, abstracción. La recopilación de estadísticas obtiene delegado a instancias específicas que generalizan una abstracción específica (una API de recopilación de estadísticas).
No sé si esto responde a tu pregunta al 100%. Después de todo, no tenemos modelos infalibles basados en leyes inviolables (como las de EE). Todo lo que puedes hacer es poner las cosas donde tienen sentido. Y esa es una decisión de ingeniería que debes tomar. Lo mejor que puedes hacer es familiarizarte con los principios de OO (y los buenos principios de modelado de software en general).