Clean OOP programming question

Started by
6 comments, last by Joeydenhaag 9 years, 2 months ago

Hi,

I have a class like:


class CApplication {

private:

CProjectWizardWindow* _pProjectWizardWindow;
CMainWindow* _pMainWindow;

public:

CApplication();
~CApplication();

bool initialize();

};

In the CProjectWizardWindow window class I receive user events for example if the user selects a project and presses 'ok'. In that case I want to send a message to _pMainWindow that this event has occured.

I know there is no 'best' way to do this because there are more options available and might be personal preferences but what is your opinion on the best way to achieve this?

I could use a pointer to _pMainWindow in _pProjectWizardWindow but that just doesn't feel right.. And a pointer to the CApplication instance in _pProjectWizardWindow doesnt feel right either..

Advertisement
This is not clean code. You are violating the Rule of 0/3/5 in a class where you are using 2 raw pointers (try using unique_ptr for automatic cleanup without handwriting copy constructors/operators and destructors).
I would also stop using hungarian name-mangling and not start names with _ .
If the main window contains the other windows you probably should also have your code that way; and not work around having everything directly inside some application god-class by routing everything through it.

PS: It would be helpful if you provided more information about what exactly your problems are.

A lot of programmers has its different ways of formating code. Tell which code layout is correct or not may be difficult or not correct at all, but if you're talking about OOP design things could be better.


I would also stop using hungarian name-mangling and not start names with _ .

Hungarian notation uses m_ for member variables, switch to that (to the OP).

Actually variables starting with _ tends to be standard includes or function parameters. I don't see any problem on using it as long you know what you're doing when using as function parameters. As I said, there is some stuff that should be avoided completely in code layout and other that can be done if the programmer knows what he is doing.

Generally code tends to get bigger, and the probability of having a variable with the same name as another in the middle of the thing IT'S OVER 9000!

CApplication is a vague term. What is your application? A game? A event based application? If is a event based application actually you can call anything. If is a game call it a game and nothing more.

Backing to OOP:

Looks like your application is a event-based one. There are tons of ways of doing it (MVC, MVP, etc.).

If you're doing a game you're doing it wrong because is not event-based at any moment.

Apply the SRP (Single Responsability Principle) whenever you want and make sure you have a well working functionality without being afraid of increasing the complexity of your program—work on the single functionality of your classes.

A well written code will make things more organized and should be done, but it won't turn better its speed or logic. If you're working alone I'd suggest that just do whetever makes sense for the program.

When working with a team at some company there are some things that you must follow in order to organize... not be fired laugh.png . When working alone some things can be tolerated (at least 50%).

(try using unique_ptr for automatic cleanup without handwriting copy constructors/operators and destructors)


Or just don't use pointers for ownership when it can be avoided. I suspect that this may be one of those cases.

My version of OP's class would probably look something like this:
class App {
public: //people reading headers usually care more about the public section, so I usually put it on top
  App(); //ctors are for initialization - exceptions are for error handling
  ~App();
  //There should probably be other methods here, or else something is very wrong.
private:
  ProjectWizardWindow wizWnd; //avoid using pointers when you don't need to
  MainWindow mainWnd; //ctor args for these objects can be passed in the initializer list of App::App()
};
Elaborating on the use of initializer lists, if ProjectWizardWindow::ProjectWizardWindow() takes (int x, int y) as parameters, then...

App::App() : wizWnd(640, 480) {
  //ctor body
}
And wizWnd will construct with 640 and 480 as its arguments. You can also use args passed into the App() ctor.

App::App(int wizwinx, int wizwiny) : wizWnd(wizwinx, wizwiny) {}
Multiple items to be initialized are separated with commas:

App::App() : wizWnd(arg, arg), mainWnd(arg, arg, arg) {
  //stuff (or - more often - nothing, since you can often do everything in the list ^)
}

Using initializer lists will help you get away from pointer issues with member objects, which can save you a ton of grief. It also lets you have things like const members, which may cause you a little more grief if you're not up to speed on copy and move constructors.
void hurrrrrrrr() {__asm sub [ebp+4],5;}

There are ten kinds of people in this world: those who understand binary and those who don't.

Pointer debate aside, "Clean OOP" is elusive. To OP, I would suggest that you shouldn't sweat too much about doing things the right way. Pick one, do it, and assess it yourself whether it has helped you steer in the right direction, or make your code worse.

Your app is event driven. If user interacts with CProjectWizardWindow, and CMainWindow should receive a message when such event occurred, then you have several options. You could create some "listener" classes for CProjectWizardWindow. How many listener classes? Depends on how many events you are emitting, and how many of them you want to differentiate into separate classes, or you could just lump them into one class. Up to you.

For the sake of an example, let's have one and call this listener class: CProjectWizardEventListener. This class would define certain methods that need to be implemented by the subclasses. Depending on the events, CProjectWizardWindow will invoke different methods.

CMainWindow should inherit from CProjectWizardEventListener, and implement these methods. CProjectWizardWindow will then maintain a list of these listeners, then, when the ok is pressed, it iterates through the list and call the method. Something like this:


class CProjectWizardEventListener {
    virtual void OnOkPressed(CProjectWizardWindow*) = 0;
    virtual void OnCancelPressed(CProjectWizardWindow*) = 0;
    // add more events here
};

// notify the listeners when ok is pressed
std::vector<CProjectWizardEventListener*> listeners;
for_each(listener.begin(); listener.end(); InvokeOnOkPressed);

class CMainWindow : public CProjectEventListener {
    virtual void OnOkPressed(CProjectWizardWindow* cpww) {
        // handle the ok event here
    }
};

Now, this is not the silver bullet. You can design this in however you want, but this is a direction you can decide to choose. Sometimes people don't like this approach because your CProjectWizardWindow has to maintain the list of the listeners, inserts them, removes them, etc. Sometimes people rather choose to emit the event to some event manager that handles all events and all the listeners. Pick one, and decide for yourself.

I'll offer my own meandering, unfocused critique / best-practices-grab-bag -- but before I do that, I'll make a caveat that if you're doing this for a class and your instructor expects a certain way of doing things, then its best to simply do things in the format she or he expects. What they want may not always be important or good practice, but your time is ultimately better spent by learning rather than fighting 'the man' -- so without further ado...

As others have pointed out, you seem to be doggedly following some notion of Hungarian notation that was popularized in the Windows Programming books of the 90s. There's no good reason to do this. In fact, the form of Hungarian notation that was popularized where the lower-case prefixes denoted type information was wrong from the start: proper Hungarian notation didn't encode type information, but was meant to encode how the data was to be interpreted. For example, you wouldn't have 'fAngle' to mean that this variable was an angle stored in a float, but you might have something like 'radAngle' to denote that this variable stored an angle in radians, rather than degrees. I would argue that neither is a particularly good name, though; 'radians' alone would suffice, and something more descriptive like 'radianChangeToTarget' would be even better.

It's also worth noting that Hungarian notation really was a C-ism. C++ has the concept of classes, and that allows you to encode both variable type and variable usage formally. Hungarian notation was a way to informally encode this kind of information into the variable name, in the hope of helping the programmer not make mistakes. In C++, the better tools that classes provide are there so that the compiler can prevent the programmer from making silly type mistakes -- you can imagine that the compiler is much better at this than we meatbags are.

Continuing on the naming conventions, it is good to have and to follow one. But the particular one is a matter of individual preference (or group preference, when working together). In Java and C# the general conventions are laid out in language style guides, and are mostly followed by the community at large, so its best to stick to those. In C++ (and C) many more 'canonical' styles evolved such that there is no credible candidate for which is "the one true style". In the C and C++ world, many companies and projects have their own styleguides that borrow liberally from generally-agreed-upon practices, but adhere also to a particular mix of the less-agreed-upon-practices, so you see lots of style guidance 'families' that overlap 80-90%. My observation is that there are two predominant families -- you have the classic family that encompasses things like K&R-style C, Stroustrup, and the C++ standard library (e.g. identifiers_with_underscores), and then you also have a younger family who's style is inspired by Java and C# (e.g. CClassName, variablesInCamelCase). My own maxim is "do as the standard library does" (although you should not use things like leading underscores that it prohibits for good reason) -- I find that not only is this style familiar to everyone, but also that the standard library has already encountered every strange corner case and provides examples of how to handle all of them, so I don't have to expend brain-cells coming up with my own way. For yourself, pick one and be consistent -- I see you mixing styles here in a way I'd characterize as inconsistent (e.g. your use of casing).

Drop the leading underscores -- use of single leading underscore is prohibited by the standard for external identifiers, some 'clever' individual started using it for their member functions and member variables since those things are not (unqualified) externals, but its still confusing while reading code, and I imagine can still cause overload resolution conflicts within a member function body (or if not conflicting for the compiler, certainly confusing for you, humble programmer). The standard also prohibits all identifiers that use a double leading underscore, or a single leading underscore followed by a capital letter, for all uses internal and external. All of this is so that the library and header files can define functions, variables, and macros for internal purposes without risk of conflict with names in user programs.

Do use std::unique_ptr when you have a variable that singularly-owns a resource. It makes your code shorter and more readable when you don't have a bunch of extra resource-freeing code in your destructors (and in many cases, unique_ptr makes it so you don't have to write a custom destructor at all), and is also much safer in the face of an exception, where your custom resource-handling code might be skipped and resources leaked.

But when you can, prefer to avoid pointers whenever you can. Prefer values first, references second, and an appropriate pointer type third. When you must resort to a pointer, use std::shared_ptr for objects who are (or potentially are) co-owned by more than one entity, use std::unique_ptr for objects who are only owned by one entity at a time (ownership can be transferred to another entity, but never co-owned), and use a raw pointer for referencing an object that you are taking no ownership of (in other words, use raw pointers when someone else aready owns the object, and you know that the owner will keep it around for longer than you need it here. Also, don't use unique_ptr or shared_ptr types as parameters just to pass what's pointed to, only use them as parameters when you mean to (potentially) change or add to its owner(s).

'initialize' member functions are a code-smell, and usually a sign that your design is not properly adhering to RAII, and/or that you have some weird inversion of relationships that's causing you to not have all the information you need to initialize an object at once -- sometimes the latter case also indicates that the object lives in the wrong scope, or that you might have a kind of cyclical relationship between it and another object. Sometimes having an initialize method that's not the constructor is justifiable, but I'd say that about half the time I see it used its employed as a kludge and indicates the kind of problems I've written about here.

To end on a positive note, I'll say that one thing I see you doing *right* is that you haven't utilized any unnecessary inheritance. It's not uncommon to see a green programmer doing something like having their "Application" class derive from their "MainWindow" class (or vice versa).

throw table_exception("(? ???)? ? ???");

OP, aside from the code formatting issues, what you're looking for is an event listener system. You could create your own (ala alnites suggestion) or you could use a "signals and slots" library such as Boost.Signals .

The idea behind these is to decouple your event handler from the event source.

if you think programming is like sex, you probably haven't done much of either.-------------- - capn_midnight

Thank you everyone for the very useful answers!

I agree with the Hungarian notation being a bad thing. I was using it very often until recently because I just had too many variable prefixes to deal with at some point. The project this question was about was a hobbyist game engine and I have rewritten the UI several times because I just didnt like the way how variables were so globally accessible in different classes. I will try to use the information provided by you guys to achieve the desired results.

This topic is closed to new replies.

Advertisement