4

SORRY FOR THE LENGTH OF THIS POST; IT IS MEANT TO DOCUMENT MY JOURNEY WITH THIS PROBLEM.

I have a question about a shared object in a Cocoa app that needs to change from time to time and how best to store it so that it's accessible from a few different places. Bear with me.

Class Implementation

The shared object is implemented as a Class Cluster (i.e., https://stackoverflow.com/a/2459385/327179) that looks like the following (note that Document is merely a class name; it is not necessarily indicative of what my actual class does):

In Document.h:

typedef enum {
    DocumentTypeA,
    DocumentTypeB
} DocumentType;

@interface Document : NSObject {}
- (Document *) initWithDocumentType:(NSUInteger)documentType;
- (void) methodA;
- (void) methodB;
@end

In Document.m:

@interface DocumentA : Document

- (void) methodA;
- (void) methodB;

@end

@interface DocumentB : Document

- (void) methodA;
- (void) methodB;

@end

@implementation Document

- (Document *)initWithDocumentType:(NSUInteger)documentType;
{
    id instance = nil;
    switch (documentType) {
        case DocumentTypeA:
            instance = [[DocumentA alloc] init];
            break;
        case DocumentTypeB:
            instance = [[DocumentB alloc] init];
            break;
        default:
            break;
    }

    return instance;
}

- (void) methodA
{
    return nil;
}

- (void) methodB
{
    return nil;
}

@end

@implementation DocumentA

- (void) methodA
{
    // ...
}

- (void) methodB
{
    // ...
}

@end

@implementation DocumentB

- (void) methodA
{
    // ...
}

- (void) methodB
{
    // ...
}

@end

How The User Interacts with a Document

Via a menu item, the user can switch between DocumentA and DocumentB at will.

What Happens When A "Switch" Occurs

When the user switches from, say, DocumentA to DocumentB, I need two things to happen:

  1. My primary NSViewController (MainViewController) needs to be able to use the new object.
  2. My AppDelegate needs to update an NSTextField that happens to be located in the content border of the main window. (FWIW, I can only seem to assign an outlet for the NSTextField in the AppDelegate)

The Question(s)

I've seen singletons mentioned quite a bit as a way to have a global reference without cluttering up one's AppDelegate (primarily here and here). That said, I've not seen much info on overwriting such a singleton (in our case, when a user switches from DocumentA to DocumentB [or vice versa], this global reference would need to hold the new object). I'm not an expert on design patterns, but I do remember hearing that singletons are not meant to be destroyed and recreated...

So, given all this, here are my questions:

  1. How would you store my Class Cluster (such that MainViewController and AppDelegate can access it appropriately)?
  2. Am I mixing concerns by having both MainViewController (who uses Document heavily) and AppDelegate (who manages the primary window [and thus, my NSTextField]) have knowledge of Document?

Feel free to let me know if I'm thinking about this problem incorrectly; I want this implementation to be as orthogonal and correct as possible.

Thanks!


Status Update #1

Thanks to advice from @JackyBoy, here's the route I've taken:

  • Document is the one that, upon "switching", "notifies" AppDelegate and MainViewController by passing them the newly created instance.
  • Both AppDelegate and MainViewController can update the Document object via the Singleton instance as necessary.

Here are my new files (dumbed down so that y'all can see the crux of the matter):

In Document.h:

#import <Foundation/Foundation.h>
@class AppDelegate;
@class MainViewController;

typedef enum {
    DocumentTypeA,
    DocumentTypeB
} DocumentType;

@interface Document : NSObject

@property (weak, nonatomic) MainViewController *mainViewControllerRef;
@property (weak, nonatomic) AppDelegate *appDelegateRef;

+ (Document *)sharedInstance;
- (id)initWithParser:(NSUInteger)parserType;

@end

In Document.m:

#import "AppDelegate.h"
#import "Document.h"
#import "MainViewController.h"

@interface DocumentA : Document

// ...

@end

@interface DocumentB : Document

// ...

@end

@implementation Document

@synthesize appDelegateRef;
@synthesize mainViewControllerRef;

+ (Document *)sharedInstance
{
    static XParser *globalInstance;
    static dispatch_once_t predicate;
    dispatch_once(&predicate, ^{
        // By default, I return a DocumentA object (for no particular reason).
        globalInstance = [[self alloc] initWithDocumentType:DocumentA];
    });

    return globalInstance;
}

- (id)initWithDocumentType:(NSUInteger)documentType
{
    Document *instance = nil;
    switch (parserType) {
        case DocumentTypeA:
            instance = [[DocumentA alloc] init];
            break;
        case DocumentTypeB:
            instance = [[DocumentB alloc] init];
            break;
        default:
            break;
    }

    // QUESTION: Is this right? Do I have to store these references
    // every time a new document type is initialized?    
    self.appDelegateRef = (AppDelegate *)[NSApp delegate];
    self.mainViewControllerRef = self.appDelegateRef.mainViewController;

    [self.appDelegateRef parserSwitchedWithParser:instance];
    [self.mainViewControllerRef parserSwitchedWithParser:instance];

    return instance;
}

@end

@implementation Xparser_NSXML

// ...

@end

@implementation DocumentA

// ...

@end

Should I be bothered by the fact that Document has knowledge of the existence of AppDelegate and MainViewController? Additionally, should I be bothered by the fact that when the Document object updates, it re-notifies both AppDelegate and MainViewController (even though one of those initiated the update)?

As always, I appreciate everyone's eyeballs on this as my quest for the ideal implementation continues. :)


Status Update #2

A comment from @Caleb helped me understand that an NSNotification-based setup would be a lot less unwieldy for this particular problem.

Thanks, all!

Community
  • 1
  • 1
ABach
  • 3,743
  • 5
  • 25
  • 33

2 Answers2

1

I do remember hearing that singletons are not meant to be destroyed and recreated...

Well, you can have references inside of it, so you are not actually "destroying" the singleton, but the objects he points to. I tend to leave the App Delegate without application logic, so I normally put it somewhere else. In your case, since you need to access something from different places, it makes sense to have one. About the cluster, you can still have it, you just ask the singleton to access it and return the appropriate object like so:

Document *myDocument = [[MySingleton defaultManager] createObjectWithType:aType];

You gain some things out of this:

  1. you can access your cluster from any place in your app

  2. you decouple things, only one entity knows about your cluster.

  3. Inside the Singleton you can have a reference to you AppDelegate and interact with it.

  4. Inside the Singleton you can have a reference to the objects that are being used (Document A, Document B)

One more thing, I would advise putting the cluster access method as a class method (instead of an instance one).

vikingosegundo
  • 52,040
  • 14
  • 137
  • 178
Rui Peres
  • 25,741
  • 9
  • 87
  • 137
  • Thanks for your input. One thing I'm still having trouble with: it seems that with this setup, when the user switches from `DocumentA` to `DocumentB`, `AppDelegate` and `MainViewController` need to *both* initialize a new version of the Cluster.Of course, I can control that, but it seems a little unwieldy that both classes would need to be notified of a change from `DocumentA` to `DocumentB` (or vice versa). I suppose AppDelegate could pass its instance of `DocumentA`/`DocumentB` to `MainViewController`? – ABach Apr 20 '13 at 18:41
  • Got it. Thanks so much. If you don't mind, I'm going to give it a whirl and provide a status update on my question; I'd really appreciate your eyes on whether I'm thinking about things the right way. – ABach Apr 20 '13 at 23:44
  • 2 things ABAch: 1) you should only make the necessary initialization in the `- (id)initWithDocumentType:(NSUInteger)documentType`like `- (id)initWithDocumentType:(NSUInteger)documentType withMainViewController:(MainViewController*)mainViewController withAppDelegate:(AppDelegate*)appDelegate. After that you could use a method like: `- (void)switchDocumentWith:(Document *)aDocument` and there: ` [self.appDelegateRef parserSwitchedWithParser:instance]; [self.mainViewControllerRef parserSwitchedWithParser:instance];` – Rui Peres Apr 21 '13 at 09:07
  • Got it. Thanks for your continued input. At this time, the answer from @Caleb makes a bit more sense in my application. That said, I do appreciate all your assistance. – ABach Apr 21 '13 at 16:36
  • @Caleb approach is a perfectly valid option as well. – Rui Peres Apr 21 '13 at 16:40
1

I don't see he need for a shared object here, much less a singleton. Do you really need to find the current Document at arbitrary times from many different objects? Seems more like you just have two objects (app delegate and view controller) that both need to know about the current Document. Notifications provide an easy way to manage that: whenever a switch happens, you can post a NSNotification that includes the new Document. Any objects that need to know about the current Document will have registered for the "document switch" notification, and when the notification arrives they can stash a pointer to the Document in an instance variable or property.

Caleb
  • 124,013
  • 19
  • 183
  • 272