Discussion (13)4
Log in or sign up to continue

I think you might want to track these numbers instead:

  • Number of properties. $lb is list, so the larger the number of elements in a list the longer the time to get to the last element. Also larger larger lists exhaust the global buffer faster, so they might be less optimal. 
  • Method size in sloc. Ideally, your class shouldn't have methods spanning hundreds (thousands) of lines.

I wouldn't worry about total class length, but rather the number of individual elements and method sizes.

There's also class limits docs, but if you hit them, you probably have an issue already.

Hi Evgeny!

First, kudos on developing such complex logic; no easy feat! As far as the question itself, it's a good question, but leading down the wrong line of thought.

The question shouldn't be "how big can I make this one class," but rather, "how is this logic and code going to be maintained." Having a class with so many lines of code, while possible, will be a nightmare to extend and maintain.

For example, say you're working on adding functionality and discover a bug in the code already in production. As a monolithic design, are you going to remove your not-ready-for-production feature, fix the bug, promote to live, and add the feature code back? What if you bring on another developer? Depending on server side/client side change control, you may end up limiting to one developer being able to work on the code at any given time. And what about multi threading and bottlenecks identified during testing and live processing? A single class will already likely need refactoring to deal with these sorts of things.

There is a reason ISC is implementing IPM and "micro services" as a strategy moving forward. The less monolithic, the better in the long run (within reason, as you don't want code scattered everywhere for no reason).

Look at options to break things up a bit. You'll thank yourself later!

Thank you, @Scott Weithman, for sharing the real-life cases! This all makes sense!

It'd be ideal to have guidelines on how to better distribute logic among a set of classes for complex solutions.

And I agree IPM can be super-helful not only for distribution and deployment but also for distributing the shared logic over the large solution.

My particular case is rather simpler: I have a full-stack app, and the /app/api spec file is getting bigger and bigger, and the impl class is already more than 2K lines. So I'm inclining to split it but the issue is that the current design of REST-API we have in  InterSystems will demand me to introduce another web app endpoint, so i will have /app/functionality1/api and /app/functionality2/api and so on. 

 Ah, I see. I think you could still apply the same logic as follows:

Create the implementation class such that it only has the stub methods and calls into other classes for the processing logic. Like creating a service to receive the messages and pass it on to the process for business logic, so too can you leave your stub classes rather simple and pass to another class (set of classes) for the processing logic.

If you've gotten up in the thousands of lines simply from bare bones stub classes, exposing another endpoint is likely the way to go, where there will be a decent separation point somewhere in your API. Same issues apply as before; if you have so many stub methods that you're hitting this many lines in your class, you've got so many stub methods in a single endpoint that future dev work will be a pain.

From the ISC documentation Intro to Creating REST Services, they have the following example stub class and show placing your processing logic in the stub method as below:

/// A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification<br/>
/// Business logic class defined by RESTSpec in petstore.spec<br/>
Class petstore.impl Extends %REST.Impl [ ProcedureBlock ]
{

/// If ExposeServerExceptions is true, then details of internal errors will be exposed.
Parameter ExposeServerExceptions = 0;

/// Returns all pets from the system that the user has access to<br/>
/// The method arguments hold values for:<br/>
///     tags, tags to filter by<br/>
///     limit, maximum number of results to return<br/>
ClassMethod findPets(tags As %ListOfDataTypes(ELEMENTTYPE="%String"), limit As %Integer) As %Stream.Object
{
    //(Place business logic here)
    //Do ..%SetStatusCode(<HTTP_status_code>)
    //Do ..%SetHeader(<name>,<value>)
    //Quit (Place response here) ; response may be a string, stream or dynamic object
}

...
}

Rather than placing all of the logic here in the implementation class, which works well for APIs with a typical number of resources, you could instead pass off the business logic to other function calls outside of the implementation class. That way you only need to check out and modify the implementation class if you're adding URI updates, and have the processing classes that can be updated without impacting the implementation class (i.e. separate your front end vs back end dev work into separate classes).

It's a bit difficult to standardize how one should create these class separations, as it's highly dependent on user/customer preference, change control methodology, dev teams, and API. If you wanted to share more about the number of resources and stub method count, even in a private message, I'd be happy to try and provide a more specific set of thoughts.

Hi Evgeny,

Thank you for sharing the details, now I see why you have so much lines in a single class 😊

As an example of what Scott and Robert are explaining in their excellent answers, when dealing with a REST API spec defining (mostly) CRUD operation on many resources, I think one way to divide and, hopefully, conquer is to delegate the implementation of operations related to each resource to a separate class.

Taking as example petstore, for which, of course, this is an overkill, this would result in five classes :

  1. Class dc.petstore.impl Extends %REST.Impl : (generated), all endpoints class methods, with most bodies being single line call to another class method
  2. Class dc.petstore.Commons : logic common to all the classes below
  3. Class dc.petstore.PetOperations Extends Commons : class methods dealing with 'pet' resource (CRUD + other)
  4. Class dc.petstore.StoreOperations Extends Commons : same for 'store' resource
  5. Class dc.petstore.UserOperation Extends Commons : same for 'user' resource

dc.petstore.impl class methods simply delegate the call to resource class, e.g.

ClassMethod findPetsByStatus(status As %String) As %DynamicArray
{
    return ##class(PetOperations).FindByStatus(status)
}

Hope this helps

Thank you @Robert Barbiaux ! Makes sense! I'll take a look if I can implement it vs what I'm doing.

I was thinking of introducing several IRIS web-apps serving one frontend that addresses different parts of logic as a spec file and implementation far beyond 2K of lines already. But not sure if it is a good/bad practice to have several web apps, and if it is a common approach. Of course, it brings a burden of accesses/roles/security whoops, etc.