I'm not sure there is any "best way", but I can speak to personal practice (using Java, which is inherently object oriented). I will typically have one class that represents the "problem" (including data). If the data for the problem instance is drawn from an XML file, a text file, a database connection or whatever, I'll use a separate class for the data import. Next, I'll have a model class, whose constructor receives the problem class as an argument. The model class has the various CPLEX constructs (CPLEX model, variables, constraints, objective, ...) as fields, along with any structures that facilitate accessing them. (For instance, I might have two maps, one mapping model objects to CPLEX variables and the other doing the reverse mapping.) If the model involves callbacks, I'll usually make them subclasses of the model class.
If the algorithm just consists of calling the solve() method for the CPLEX object in the model class, I'll make solving a method in the model class. If it is more complicated (initial heuristics, looping through modifications of the model, ...), I'll create a separate solver class that accesses the model class and does all that stuff.
If the problem is sufficiently complicated, or if I want to be able to seed the algorithm with starting solutions from other runs, I'll create a separate solution class, which holds the solution (typically in terms understandable by the problem class, so storing "number of vehicles = ..." rather than "IloNumVar nveh = ..."). If solutions need to be read from and/or written to files of a particular type (XML, JSON, CSV) or a database, I'll use a separate class for that I/O.
If the user gets to set algorithm parameters (including CPLEX parameters), I typically put those in a class of their own, along with a mechanism to import and export them. That helps with reproducibility and cuts user time if the user wants to reuse parameter settings repeatedly.
If there is a GUI, that will, of course, be a class of its own.
Finally, I adhere to a couple of general practices. If anything (problem data, solutions, parameters) is read from or written to external sources, I try to make the relevant field names (column headings in a CSV file, database field names, ...) text data, either static string values in the relevant class or text resources read from a text file that is part of the source code. That way, if someone mucks with one of the names, I can tweak one line of source code or one line in a text resource file, and not run around looking for every place in the code where that name is used (inevitably missing one).
Also, when specifying the APIs for each class, I try to be as minimalist/vague/general as possible. So the problem class provides accessors to give the model class what it needs to know, but only what it really needs, and at more or less the most general level possible. As one example of that last point, if the model class needs to know the arcs in a network model, the problem class (which is where the network details are stored) will ideally return a Collection of arcs, not a HashSet or ArrayList. (In Java, both HashSet and ArrayList are subclasses of Collection.) That way, if something changes and I find myself needing to alter how the arcs are stored in the problem class, I don't have to worry about breaking any code in the model class.