Работа с функциями в классе, которые должны быть разбиты на функции для ясности? - PullRequest
4 голосов
/ 02 ноября 2011

Как обычно решается эта ситуация? Например, объект может нуждаться в очень специфических вещах:

class Human
{
   public:
   void eat(Food food);
   void drink(Liquid liquid);
   String talkTo(Human human);
}

Скажите, что это то, что должен делать этот класс, но на самом деле это может привести к тому, что функции будут занимать более 10 000 строк. Таким образом, вы сломаете их. Проблема в том, что многие из этих вспомогательных функций не должны вызываться ничем, кроме функции, которую они обслуживают. Это делает код в некотором роде запутанным. Например, жевать (Food food); будет вызываться методом eat (), но не должен вызываться пользователем класса и, вероятно, не должен вызываться где-либо еще.

Как рассматриваются эти случаи в целом. Я смотрел некоторые классы из реальной видеоигры, которая выглядела так:

class CHeli (7 variables, 19 functions)
Variables list

    CatalinaHasBeenShotDown
    CatalinaHeliOn
    NumScriptHelis
    NumRandomHelis
    TestForNewRandomHelisTimer
    ScriptHeliOn
    pHelis

Functions list

    FindPointerToCatalinasHeli (void)
    GenerateHeli (b)
    CatalinaTakeOff (void)
    ActivateHeli (b)
    MakeCatalinaHeliFlyAway (void)
    HasCatalinaBeenShotDown (void)
    InitHelis (void)
    UpdateHelis (void)
    TestRocketCollision (P7CVector)
    TestBulletCollision (P7CVectorP7CVectorP7CVector)
    SpecialHeliPreRender (void)
    SpawnFlyingComponent (i)
    StartCatalinaFlyBy (void)
    RemoveCatalinaHeli (void)
    Render (void)
    SetModelIndex (Ui)
    PreRenderAlways (void)
    ProcessControl (void)
    PreRender (void)

Все они выглядят как функции довольно высокого уровня, что означает, что их исходный код должен быть довольно длинным. Что хорошо в этом, так это то, что с первого взгляда становится ясно, что может делать этот класс, и этот класс выглядит простым в использовании. Тем не менее, код для этих функций может быть довольно большим.

Что должен делать программист в этих случаях; что является правильной практикой для подобных ситуаций.

Ответы [ 4 ]

11 голосов
/ 02 ноября 2011

Например, жевать (Food food); будет вызываться методом eat (), но не должен вызываться пользователем класса и, вероятно, не должен вызываться где-либо еще.

Затем создайте chew private или protected функцию-член или автономную функцию в анонимном пространстве имен в модуле реализации eat:

// eat.cc

// details of digestion
namespace {
    void chew(Human &subject, Food &food)
    {
        while (!food.mushy())
            subject.move_jaws();
    }
}

void Human::eat(Food &food)
{
    chew(*this, food);
    swallow(*this, food);
}

Преимущества этого подхода по сравнению с private функциями-членами в том, что реализация eat может быть изменена без изменения заголовка (что требует перекомпиляции клиентского кода). Недостатком является то, что функция не может быть вызвана какой-либо функцией за пределами ее модуля, поэтому она не может быть совместно использована несколькими функциями-членами, если они не совместно используют файл реализации, и что она не может напрямую получить доступ к private частям класса. .

Недостатком по сравнению с protected функциями-членами является то, что производные классы не могут напрямую вызывать chew.

1 голос
/ 02 ноября 2011

Реализация одной функции-члена может быть разделена любым способом.

Популярным вариантом является использование закрытых функций-членов:

struct Human
{
    void eat();

private:
    void chew(...);
    void eat_spinach();
    ...
};

или использование Pimpl.идиома:

struct Human
{
    void eat();
private:
    struct impl;
    std::unique_ptr<impl> p_impl;
};

struct Human::impl { ... };

Однако, как только сложность eat возрастет, вы, безусловно, не захотите накапливать коллекцию частных методов (будь то внутри класса Pimpl или внутри частного раздела).

Итак, вы хотите сломать поведение.Вы можете использовать классы:

struct SpinachEater
{
    void eat_spinach();
private:
    // Helpers for eating spinach
};

...

void Human::eat(Aliment* e)
{
    if (e->isSpinach()) // Use your favorite dispatch method here
                        // Factories, or some sort of polymorphism
                        // are possible ideas.
    {
        SpinachEater eater;
        eater.eat_spinach();
    }

    ...
}

с основными принципами:

  • Упрощайте
  • Один класс, одна ответственность
  • Никогда не дублируйте код

Редактировать: немного лучшая иллюстрация, показывающая возможное разделение на классы:

struct Aliment;

struct Human
{
    void eat(Aliment* e);

private:
    void process(Aliment* e); 
    void chew();
    void swallow();
    void throw_up();
};

// Everything below is in an implementation file
// As the code grows, it can of course be split into several
// implementation files.
struct AlimentProcessor
{
    virtual ~AlimentProcessor() {}
    virtual process() {}
};

struct VegetableProcessor : AlimentProcessor
{
private:
    virtual process() { std::cout << "Eeek\n"; }
};

struct MeatProcessor
{
private:
    virtual process() { std::cout << "Hmmm\n"; }
};

// Use your favorite dispatch method here.
// There are many ways to escape the use of dynamic_cast,
// especially if the number of aliments is expected to grow.
std::unique_ptr<AlimentProcessor> Factory(Aliment* e)
{
    typedef std::unique_ptr<AlimentProcessor> Handle;

    if (dynamic_cast<Vegetable*>(e)) 
        return Handle(new VegetableProcessor);
    else if (dynamic_cast<Meat*>(e))
        return Handle(new MeatProcessor); 
    else
        return Handle(new AlimentProcessor);
};

void Human::eat(Aliment* e)
{
    this->process(e);
    this->chew();

    if (e->isGood()) this->swallow();
    else this->throw_up();
}

void Human::process(Aliment* e)
{
    Factory(e)->process();
}
0 голосов
/ 02 ноября 2011

Декомпозиция функций - это то, что изучается из опыта, и обычно подразумевает декомпозицию типов одновременно. Если ваши функции становятся слишком большими, есть разные вещи, которые можно сделать, что лучше всего подходит для конкретного случая, зависит от рассматриваемой проблемы.

  • разделение функциональности на частные функции

Это имеет больше смысла, когда функции должны получить доступ к большому количеству состояния из объекта, и если они могут использоваться в качестве строительных блоков для одной или нескольких открытых функций

  • разбить класс на разные подклассы с разными обязанностями

В некоторых случаях часть работы естественным образом попадает в ее собственную небольшую подзадачу, тогда функции более высокого уровня могут быть реализованы в виде вызовов внутренних подобъектов (обычно членов типа).

Поскольку область, которую вы пытаетесь смоделировать, может интерпретироваться множеством различных способов, я боюсь, пытаясь обеспечить разумную разбивку, но вы можете представить, что у вас есть mouth подобъект в Human, который вы могли бы используйте для ingest еды или питья. Внутри подобъекта mouth вы можете иметь функции open, chew, swallow ...

0 голосов
/ 02 ноября 2011

Одна возможность состоит (возможно, private ly) в составлении Human более мелких объектов, каждый из которых выполняет меньшую часть работы.Итак, у вас может быть объект Stomach.Human::eat(Food food) делегировал бы this->stomach.digest(food), возвращая объект DigestedFood, который функция Human::eat(Food food) обработала далее.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...