Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Proper Architecture for Application-Level Collections

Given an application-wide collection of objects, and many unrelated classes that need frequent access to these objects, what is the best way to provide said access?

Example:

// Object A, stored in collections, used to do useful things
class A
{
  ...
public:
  QString property(const QString& propertyName) {return m_properties.value(propertyName);}

protected:
  QHash<QString,QString> m_properties;
}

// Collection class, contains methods to:
// - Access members of collections
// - Add/Remove members from collection
class GlobalCollection
{
public:
  // Accessors to collection/collection members
  static A* getAs() {return aHash;}
  static QHash<QString,A*> getAByKey(const QString& key) {return aHash.value(key);}
  static QList<A*> getAsMatchingCriteria(const QString& property, const QString& value)
  {
    QHash<A*> subsetOfA;

    foreach(A* pA, aHash.values())
    {
      if (pA->property(property) == value)
        subsetOfA << pA;
    }

    return subsetOfA;
  }

protected:
  QHash<QString,A*> aHash;
}

// Example client class that uses A's to do its job
class Client
{
public:
  // This is tied to a button click, and is executed during run-time at the user's whim
  void doSomethingNonTrivialWithAs()
  {
    // Get A* list based on criteria, e.g. "color" == "green"
    QList<A*> asWeCareAbout = ???;

    // Draw all the "green" A's in a circle holding hands
    foreach(A* pA, asWeCareAbout)
    {
      // Draw a graphical representation of pA
      // If pA has "shape" == "square", get a list of all the non-"green" "square" A's and draw them looking on jealously from the shadows
      // Else if pA has "shape" == "circle", draw the non-"green" "circles" cheering it on
    }
  }
}

Assumptions:

  • Preference has been given to small, lightweight classes, so client objects are legion
  • A client object could be several layers deep inside a "peer" of GlobalCollection, and intermediate layers have no dependency on A* or GlobalCollection
  • This is currently implemented as a singleton

Design Requirements and Problems with Other Solutions:

  • Dependency injection looks like an unreasonable burden on calling code (given the layering,) and sacrifices too much clarity for my liking
  • I'm not opposed to a static class instead of a singleton, but that doesn't feel much better than a singleton
  • Code that modifies the collection is isolated, so I'm not worried about that at this time
  • The solution needs to promote thread-safety in GlobalCollection and within A's (given that multiple clients could end up working on the same A*.) This is currently being achieved with one mutex and overzealous locking, in large part because it is so difficult to manage access to the A's.
  • I'm trying to iterate towards testability, and the current design makes nearly every test of a client require properly setting up the GlobalCollection first.
  • In production code we have multiple GlobalCollections (for A, B, C, etc.,) so template solutions are welcome.

While I'm refactoring legacy code to do this, my main concern is designing the right architecture in the first place. This seems like a very common logical concept, but all the solutions I see fail to address some important aspect of using it for production or have a glaring flaw/tradeoff. Maybe I'm being too picky, but in my experience the right tool for the job has zero drawbacks in that context.

like image 854
John Neuhaus Avatar asked Nov 29 '15 20:11

John Neuhaus


People also ask

What is the architecture of an application?

An application architecture describes the patterns and techniques used to design and build an application. The architecture gives you a roadmap and best practices to follow when building an application, so that you end up with a well-structured app. Software design patterns can help you to build an application.

What are the three main components of application architecture?

The three-tier architecture includes: Browser (client-side) Presentation layer (server-side) Business/application layer (server-side)

Which architecture is best suited for starting out with an application?

For smaller applications, monolithic architecture is often the best solution.


1 Answers

There is one clean, maintainable and testable solution, but you reject it in your requirements:

Dependency injection looks like an unreasonable burden on calling code (given the layering,) and sacrifices too much clarity for my liking

I'm going to ignore that requirement for now. See the end of my answer if you really really want to avoid dependency injection (which I don't recommend).

Designing the collection objects

  • Creating a wrapper around the actual collection (as you already do) is a good idea. It gives you full control over the interaction of clients with the collection (e.g. with regards to locking).
  • Don't make it static. Design it in a way that you can instantiate a collection, use it, and finally delete it. All the collections of the standard library and Qt work like that too, after all.
  • Introduce an interface for the collection object.

Designing the collection access mechanism

The solution needs to promote thread-safety

This screams for a factory-like intermediary: Create a factory that provides access to the collection. The factory can then decide when to return a new collection or an existing one. Make sure clients give the collection back when they are finished, so that you know how many clients are using it.

Now all clients access the collection through the factory. They only see the interface, never the real implementation.

Getting a reference to the factory

Now that we introduced the factory, clients don't need to know access the collection directly (statically) anymore. However, they still need to get hold on the factory itself.

Make the factory a dependency by injecting it to the constructors of the clients. This design clearly communicates that the clients depend on the factory. It also enables you to switch out the factory during testing, e.g. to replace it with a mock.

Note that using dependency injection does not mean you need to use a DI framework. The important thing it to have a clean, well-defined composition root.

Avoiding DI

As I already said, this is not recommended. DI is the basis for clean, decoupled designs that emphasize testability.

If you still want to avoid DI, then modify the above design as follows:

  • Create a singleton that provides access to the factory.
  • Access the factory through that singleton from all clients.
  • Keep the collections and factories as they are, i.e. non-static and not aware of any singletons.

Further notes

Your collections and usage thereof sound a lot like the repository pattern. My design suggestions above are in line with this similarity (e.g. access collections in a narrow scoped way and "give them back"). I think reading about the repository pattern will help you to get your design right.

like image 153
theDmi Avatar answered Sep 27 '22 17:09

theDmi