3

The task

The piece of software I'm writing operates on the following object types:

  • Agents
  • Calls
  • CallQueues

These objects can be linked together, and each of them contains some additional information. For example, a Call contains the caller number, an Agent contains it's name, and so on.

This model should react to external events delivered through a message queue. Most of the events are like the following:

  • an Agent has joined a CallQueue;
  • an Agent has left a CallQueue;
  • a Call was added to a CallQueue;
  • a Call has left a CallQueue;
  • an Agent has answered a Call;
  • an Agent has completed a Call;
  • etc.

And so on. Basically, most of the events trigger creation and removal of links between the objects - and also contain additional information. Some events trigger additional business logic, which is outside of the scope of this question.

Updates of Agents, Calls and CallQueues should be observed from outside for scenarios like these:

  • delivering application state updates to clients through event streams (e.g. over WebSockets);
  • persisting updates to a database;
  • etc.

Un update is an event which is raised when either the object's data has changed, or a link to another object was created or removed.

The question

What are the best practices to organize business logic? Options that I have:

  1. Make a separate component for handling each type of object, e. g. AgentsRegistry, CallsRegistry and CallQueuesRegistry. When, for example, an agent-linked-to-call-queue event happens, catch it in the CallQueuesRegistry, create a link and notify the AgentsRegistry, so that both of them execute their business logic. Expose update event streams from each component to the outside world, so that persistance and WebSockets handlers can bind to it and react.

  2. Make a single ApplicationState component for handling all kinds of events. Process links updates internally. Try to separate business logic into private methods. Expose a single update event stream from the component to the outside world.

  3. A mix of the two above: hide separate components behind an ApplicationState object, which should react to the events and act as a conductor for type-specific business logic.

  4. Put business logic into Agents, Calls and CallQueues - I can hardly envision this kind of approach when Dependency Injection comes into play, and additional components should be used. Also, it is unclear to me which object should react to an agent-has-answered-a-call event: the Agent or the Call.


Which option is preffered and why? What other options exist?


EDIT: split the question in two, renamed Queues to CallQueues.

EDIT 2: Clarification: a CallQueue is generally a list of calls waiting to be processed by an Agent who is bound to that particular CallQueue. It is a virtual representation of a real queue in an external application, and it has nothing to do with a queue data structure, because the FIFO logic is managed in that external app. My app only gets notifications about a certain call entering the queue, leaving it (which can happen randomly) or making all the way to a free Agent and getting connected with it. Multiple CallQueues exist.

There is no such thing as a queue of Agents waiting for a Call, at least in the scope of this app. As far as we're concerned, a pseudo-random free Agent connects to a pseudo-random Call form the CallQueue.

  • 1
    Is this about a call-center application? Because in that case it seems like you need to identify more "nouns" in the spec. E.g. Queues is a bit confusing. Logically queues of waiting customers and queues of waiting agents would be two different things, and there would probably also be a list of "running" calls. I'd also expect a distinction between an incoming, waiting call (which would e.g. have the the customers number) and a running call, which would have the agent who answered it and could just have a reference to an incoming call for that data. – R. Schmitz May 31 '18 at 14:12
  • 1
    You'll probably also have to divide this question up, because at the moment it looks like "what would be the best architecture for a call-center app", which would be considered too broad of a question here. – R. Schmitz May 31 '18 at 14:19
  • 1
    @R.Schmitz thank you for the reply! I have split the question in two. The business logic organisation part is left in this question, and the data relations representation part is moved to a different question. The link is in the post. – G. Kashtanov May 31 '18 at 18:32
  • 1
    @R.Schmitz I plan to distinguish between incoming, waiting and running calls by their internal state. E.g. fun getState(): SomeEnum. The Queues are queues of calls. I have renamed the entity in the post, made it CallsQueue. Thank you for suggestions! – G. Kashtanov May 31 '18 at 18:34
  • 1
    Well first off, good job, dividing does make it easier to understand. However, I still have some trouble with those queues. The word "queue" in programming has a solidly defined meaning: a FIFO list. Your programming language probably has an own type for that. A queue makes sense for incoming calls - among those waiting, the one who called first should be served first. FIFO can also make sense for waiting agents (when it's a slow day). However, it doesn't make sense for running calls - when a customer is helped, they are done; they don't need to wait for other, earlier calls to end. – R. Schmitz Jun 1 '18 at 8:58
2

Disclaimer

I know several ways to do this, but only one way which lead to code that felt "clean" and didn't make me lose my overview and turn at least partially into spaghetti. I certainly don't want to imply that this is "the correct way". This is heavily influenced by Robert C. Martin's book "Clean Code", among others.

"True" OOP

As far as I'm aware, there's 2 schools of thought regarding OOP. For most people, it boils down to "an object is a class".

However, I found the other school of thought more helpful: "an object is something that does something". If you're working with classes, each class would be one of two things: a "data-holder" or an object. Data-holders hold data (duh), objects do stuff. That does not mean that a data-holder can't have methods! Think of a list: A list doesn't really do anything, but it has methods, to enforce the way it holds the data. For the rest of this answer, we'll assume this definition of OOP.

Data holders

If I'm not overlooking something, all the things you mentioned are data holders. Agents, Calls, CallQueues, AgentsRegistries - each of them is a collection of facts. Sure, outside of your program an agent is somebody who does something, but inside your program, an Agent is e.g. a string EmployeeId, a string Name and an int NumberOfCalls.

That data holders don't do anything also doesn't mean that they don't contain business logic! If your Agent class has a constructor requiring EmployeeId, Name and NumberOfCalls, you've successfully encapsulated the business rule that every agent has those. If you can not change the EmployeeId after an Agent class has been created, that's another business rule encapsulated. These are very basic things, but that just means that's it's very important to not get them wrong - and this way, you ensure that it's impossible to get that wrong.

Objects

Objects do something. If you want clean objects, we can change that to "Objects do one thing".

What you mentioned under your business logic option 1 would be the job of an object. For example, you could call this object "CallRegisterer", with a public method RegisterCall(Call, Agent). That is all we need to know from the outside, this object registers calls and to do so, it needs a Call and an Agent.

On the inside, this object would have references to the AgentsRegistry and CallQueuesRegistry so it could update those two accordingly. If the logic for updating the AgentRegistry becomes too complex, CallRegisterer can just have another object AgentsRegistryUpdater which is responsible for that part. If later on, those calls need to be logged to a file, you can add a CallToFileLogger object which does that.

PS I already mentioned that the things you mention are mostly data holders. For things to do, I only see very abstract verbs like "execute their business logic" of "Expose update event streams", so this example might not be perfect.

Data holders vs objects - summary

Now, why is this division so good? Because now changing that class changes only one thing (the way a call is registered in the example). If you're changing an object, you're only changing the way a certain action is done; if you change a data holder, you only change what data is held and/or in which way it is held.

Changing data might mean the way something is done has to change, too, but that change won't happen until you make it happen yourself by changing the responsible object.

Dependency injection

It's good that you mentioned DI, because it makes this considerably easier. If several objects need to work on the AgentQueue, it might be a hassle to get the same instance to all of them.

If you implement the observer patterns on your XYRegistry classes, making this whole thing work is just a matter of connecting your acting objects to those observable events. That is an action, so to keep it clean there should be another object doing that; I like to go with classes named XYInitializer. Your DI framework might have a "built-in" way to call such initializer classes.

  • 1
    Thank you for such detailed answer! :) It explains many important concepts and also leaves some space for thinking and consideration. – G. Kashtanov Jun 1 '18 at 15:56

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.