TLDR: Все зависит от ситуации. Набивайте свои шишки и наберетесь опыта.
Самый главный вопрос - зачем вы вообще хотите использовать ООП, насколько сложный и "правильный", какие цели хотите достичь.
Когда разрабатываете библиотеку, то есть несколько важных целей (независимо от парадигмы):
1. Апи должнен быть простым, понятным и самодокументируемым
2. Вы должны иметь возможность развивать библиотеку и не нарушать при этом уже используемый пользователями API
Когда пишете что-то - всегда помните два этих правила. Ведь пользователи всегда на первом месте и их значительно меньше интересует, что там внутри и насколько сложно вам с этим работать. Для примера можете посмотреть код Редакса - там отвратительный процедурный код, но многие хавают.
Еще одна важная причина - это обучение. Вы хотите попробовать и набить своих шишек. Тогда пользуйтесь и вставляйте все самые стремные подходы, о которых только сможете прочитать.
Не слушайте евангелистов ФП - сейчас в JS функциональностью модно прикрывать говнокод.
Так же не слушайте тех, которые на каждом углу кричат "Composition over inheritance", "Banana monkey problem" другие страшные термины. В своем религиозном угаре они забывают, что все эти принципы рекомендации для определенных ситуаций. Да, иногда (в большинстве случаев на самом деле) композиция значительно лучше наследования, очень часто стыкаешься с ситуацией, что наследование применено не в тему и из-за этого усложнено развитие. Но иногда наследование - правильный путь и пока сами не набъете шишек - будет крайне сложно понять разницу. Помогает
принцип is-a/has-a. И я видел код отличных программистов, где наследование было применено так, что значительно разгрузило API и упростило код. Не зацикливайтесь из-за религиозных фанатиков.
Множество паттернов ООП придумано, чтобы изолировать функциональность и облегчить её тестирование, но каждый паттерн - лишь рекомендация.
Получился класс, в котором есть эти 4 метода. Все эти методы я вызываю в конструкторе. То есть получается, я процедурный стиль просто запихнул в класс.
Да, вы правы, но это не обязательно плохо, идеальный код никто и никогда не пишет. Но посмотрите какая проблема - у вас и парсятся входные данные и делается запрос и делается обработатка. Можно ли изменить парсинг входных данных, оставив другие шаги? Можно ли не отправлять запрос? Как вы будете тестировать эту функцию, если она обязательно выполняет запрос и нету никакого способа не дать ей отправить этот запрос?
С другой стороны, будут ли довольны пользователи вашей библиотеки, если им придется инджектить все зависимости? Даже если вы сделаете вызов по-умолчанию, то чтобы изменить одну зависимость - придется изменять все. Я приведу пример, где использование наследования даст необходимую вам гибкость и API для пользователя не разбухнет, зато он сможет изменить каждый шаг. Вот:
interface IPipeline {
ResultView Render (RawData rawData);
}
class Pipeline : IPipeline {
protected virtual ParsedRawData ParseRawData(RawData rawData) {
return new DataParser(rawData).GetParsedData();
}
protected virtual ActualData GetActualData (ParsedRawData rawData) {
return new HttpRequest(rawData.src);
}
protected virtual ResultState GetResultState (RawData rawData, ActualData actualData) {
return new StateCounter().Count(rawData, actualData);
}
public virtual ResultView Render (RawData rawData) {
var parsed = ParseRawData(rawData);
if (parsed.IsValid) {
var actual = GetActualData(rawData);
var state = GetResultState(rawData, response);
return new Renderer().Render(state);
} else {
throw new Exception("Raw data is invalid");
}
}
}
Для пользователя это выглядит довольно симпатично:
new Pipeline().Render(rawData);
Если же ему необходимо брать из другого источника данные - он легко это исправит:
class MyPipeline : Pipeline {
protected virtual ActualData GetActualData (ParsedRawData rawData) {
return new LocalStorageRequest(rawData.src);
}
}
new MyPipeline().Render(rawData);
Конечно, это же можно сделать при помощи композиции. Ну давайте поиграем на основе паттерна Билдер:
interface IPipeline {
ResultView Render (RawData rawData);
}
class Pipeline : IPipeline {
private IRawDataParser rawDataParser;
private IActualDataReciever actualDataReciever
private IResultStateCounter resultStateCounter;
private IRenderer renderer;
public GetRawDataParser () {
return rawDataParser ?? new RawDataParser();
}
public GetActualDataReciever () {
return rawDataParser ?? new ActualDataReciever();
}
public Pipeline GetActualDataReciever (actualDataReciever) {
actualDataReciever = actualDataReciever;
return this;
}
// ...
private GetRenderer () {
return renderer ?? new Renderer();
}
public virtual ResultView Render (RawData rawData) {
var parsed = GetRawDataParser().Parse(rawData);
if (parsed.IsValid) {
var actual = GetActualDataReciever().Get(rawData);
// ...
return GetRenderer().Render(state);
} else {
throw new Exception("Raw data is invalid");
}
}
}
Использование по-умолчанию всё то же:
new Pipeline().Render(rawData);
А изменить пользователь их может так:
new Pipeline()
.SetActualDataReciever(new MyDataReciever())
.Render(rawData);
Тут впору уже и о DI Container'ах почитать.
Обратите внимание, что все варианты оставляют простое, но гибкое API для пользователя и кучу возможностей для расширения и поддержки вами. Вы можете протестировать каждый кусочек, каждую отправку, мокнуть всё, что угодно
В качестве вывода повторю самое главное - просто тренируйтесь, пишите и набивайте шишки, читайте о паттернах и приемах, но помните, что это рекомендации, а не законы и старайтесь думать и анализировать, что пишете и читаете.