Jak najlepiej używać konstruktora w większym projekcie i czy upychać w nim logikę?

0

Joł,
Od pewnego czasu zacząłem robić ciut większe projekty, aby poćwiczyć oop. Przy najbliższej okazji zacząłem się zastanawiać jak wygląda realne użycie konstruktorów klas - czy tylko są używane do inicjalizowania składowych czy też mają inną rolę. Przykład:

Class::Class() {
(...) //inicjalizacja składowych na paręset linijek z listą czy bez
}
Class::Class() {
(...) //wywoływanie metod inicjalizujących składowe
}

Czy właśnie tak powinno wyglądać wywoływanie konstruktorów czy jest to niewydajne/brzydkie/bzdurne użycie? Może w ogóle lepiej jest nie męczyć aż tak konstruktora i zostawić sprawę metodom, które zrobią za niego robotę? Jak Wy używacie konstruktorów? Czy jest odgórnie ustalone ile powinien mniej więcej mieć loc taki konstruktor czy to bez znaczenia dopóki wszystko jest czytelne?
Pozdro

1

Konstruktory wykorzystuje się nie tylko do inicjalizowania składowych ale też do pozyskiwania zasobów, poczytaj o idiomie RAII (https://en.cppreference.com/w/cpp/language/raii). Co do wywoływania metod w konstruktorach to często taki wzorzec widuję, jednak nie jestem fanem tego podejścia. Z jednej strony kod staje się nieco bardziej czytelny ale to też może oznaczać, że klasa jest zbyt duża i powinna być przemyślana jeszcze raz (np. rozdzielona). Dodatkowo C++ 11 wprowadził coś takiego jak delegating constructors, czyli w skrócie jeden konstruktor może wywoływać inny z tej samej klasy - nie trzeba klepać niemal tego samego kodu w konstruktorach (właśnie wtedy osobne metody miały sens). Należy również pamiętać, że mechanizm wywołań wirtualnych nie działa w konstruktorach i destruktorach.
Nie zapominaj też o liście inicjalizacyjnej.
I odpowiadając na Twoje ostatnie pytanie - przejrzystość kodu to podstawowa zasada.

1

Nie ma odgórnych ustaleń, poza dedykowaną rolą - konstruktor powinien niejako przygotować obiekt do użytku, tak aby zaraz po jego utworzeniu dało się z niego korzystać. Raczej należy unikać sytuacji, jeśli tylko oczywiście możemy, że najpierw obiekt trzeba utworzyć, potem musieć robić na nim jakieś obiekt.init1(), obiekt.init2() (a już Swarogu broń w określonej kolejności) bo inaczej nie będzie zachowywał się poprawnie.
Czy wykorzystywać wewnątrz niego jakieś metody, też nie ma na to reguły, zależy od kontekstu. Ot przykład konstruktora z jednej z moich klas:
(@Marooned @Adam Boduch - Panowie, a gdzie znacznik spoiler/spoiler/?)

WeaponsDatabase& WeaponsDatabase::getInstance()
{
    static WeaponsDatabase instance;
    return instance;
}

WeaponsDatabase::WeaponsDatabase()
{
    QVector<QString> weaponFilesToExclude = DatabasesManager::getInstance().getFilesToExclude("WeaponsDatabase");

    QDir weaponsDir(DatabasesManager::getInstance().getJsonDataPath() + "/weapon", "*.json", QDir::Name|QDir::IgnoreCase, QDir::Files);

    QStringList weaponsFiles = weaponsDir.entryList();
    foreach (const QString &weaponFile, weaponsFiles)
    {
        if (weaponFilesToExclude.contains(weaponFile))
        {
            continue;
        }

        QFile file(weaponsDir.absolutePath() + '/' + weaponFile);
        if (file.open(QFile::ReadOnly))
        {
            //get json object for a weapon
            QJsonObject json = QJsonDocument::fromJson(file.readAll()).object();
            file.close();

            //the weapon does not exist yet, so it must be built and added to map of WeaponData
            WeaponData *weapon = new WeaponData;
            weapon->slotSize = json.value("InventorySize").toInt();
            weapon->tonnage = json.value("Tonnage").toInt();
            weapon->heat = json.value("HeatGenerated").toInt();
            weapon->damage = json.value("Damage").toInt();
            weapon->stabilityDamage = json.value("Instability").toInt();
            weapon->heatDamage = json.value("HeatDamage").toInt();
            weapon->startingAmmoCapacity = json.value("StartingAmmoCapacity").toInt();
            weapon->ammoCategory = json.value("AmmoCategory").toString();
            weapon->type = json.value("ComponentType").toString();
            weapon->subType = json.value("WeaponSubType").toString();
            weapon->bonusValueA = json.value("BonusValueA").toString();
            weapon->bonusValueB = json.value("BonusValueB").toString();
            weapon->name = json.value("Description").toObject().value("UIName").toString();

            QString hardpoint = json.value("Category").toString();
            SettingsContainer &settings = SettingsContainer::getInstance();
            if (hardpoint == "Ballistic")
            {
                weapon->hardpoint = HardpointTypes::Ballistic;                
                weapon->color = settings.getColorForEquipment(HardpointTypes::Ballistic);
            }
            else if (hardpoint == "Energy")
            {
                weapon->hardpoint = HardpointTypes::Energy;
                weapon->color = settings.getColorForEquipment(HardpointTypes::Energy);
            }
            else if (hardpoint == "Missile")
            {
                weapon->hardpoint = HardpointTypes::Missile;
                weapon->color = settings.getColorForEquipment(HardpointTypes::Missile);
            }
            else//the only possible hardpoint type left is support/antipersonnel
            {
                weapon->hardpoint = HardpointTypes::Antipersonnel;
                weapon->color = settings.getColorForEquipment(HardpointTypes::Antipersonnel);
            }

            //for now all weapons can be put anywhere where they fit.In the future however there should be added more conditions after the "All" check
            QString allowedLocations = json.value("AllowedLocations").toString();
            if (allowedLocations == "All")
            {
                weapon->allowedLocations = static_cast<MechLocations::Location>(weapon->allowedLocations | MechLocations::All);
            }

            //store the weapon in a map with same QString ID as it is in .JSON
            QString componentDefID(weaponFile);
            componentDefID.chop(5);//get rid of .json from the end

            weapons.insert(componentDefID, weapon);
        }
    }
}

Z kolei konstruktor bardzo podobnej klasy wygląda tak:

EquipmentDatabase &EquipmentDatabase::getInstance()
{
    static EquipmentDatabase instance;
    return  instance;
}

EquipmentDatabase::EquipmentDatabase()
{
    createEquipmentMap("AmmunitionBoxDatabase", "/ammunitionBox", Categories::ammo, true);
    createEquipmentMap("JumpJetsDatabase", "/jumpjets", Categories::jumpJets);
    createEquipmentMap("HeatsinksDatabase", "/heatsinks", Categories::heatsinks);
    createEquipmentMap("ActuatorsDatabase", "/upgrades/actuators", Categories::actuators);
    createEquipmentMap("CockpitModsDatabase", "/upgrades/cockpitMods", Categories::cockpitMods);
    createEquipmentMap("GyrosDatabase", "/upgrades/gyros", Categories::gyros);
    createEquipmentMap("TargetingTrackingDatabase", "/upgrades/targetTrackingSystem", Categories::computers);
}

void EquipmentDatabase::createEquipmentMap(const QString &exclusionsDatabaseName, const QString &equipmentDirectoryName, const QString &categoryName, bool isAmmo)
{
//robi w sumie to samo co klasa wyżej - wczytywanie danych z .json-ów i tworzenie na ich podstawie mapy
}

w tym createEquipmentMap z grubsza jest to samo, co w konstruktorze WeaponsDatabase, ale ponieważ każda z kategorii różniła się tylko niuansami takimi jak położenie folderu zawierającego .json-y z danymi to zamiast 7x powtarzać niemal identyczny kod wydzieliłem to do osobnej funkcji. Problem był z amunicją, bo ona ma inny schemat kolorystyczny - z tym poradziłem sobie wprowadzając dodatkowy parametr, domyślnie false.

1
Sunnydev napisał(a):

(...) //inicjalizacja składowych na paręset linijek z listą czy bez

Raczej długość funkcji, metod, konstruktorów, destruktorów nie powinna przekraczać 10 linijek. To pozwala uniknąć błędów i poprawia czytelność.
O zasadach dot. konstruktorów możesz poczytać tutaj -> link

0

Co twoje pytanie ma wspólnego z OOP? Bez polimorfizmu (statyczny lub dynamiczny) nie ma OOP, a twój problem nie ma nic wspólnego z polimorfizmem.

Zwykle to standardy kodowania danego projektu, określają preferowane rozwiązanie.

Jaka jest różnica dla kompilatora? W większości wypadków dosłownie żadna. W C++ obowiązuje zasada "AS IF", wiec sposób zapisu kodu ma drugorzędne znaczenie.
Ma być czytelne dla developerów.

1 użytkowników online, w tym zalogowanych: 0, gości: 1