6

Imagine a production line - say, building a car. Starting from the beginning, a long, defined sequence of operations takes place, and at the end a car comes out. At various points along the way, production stops and waits for input from a human being; other parts of the process can be carried out automatically.

Taking this as a metaphor (I'm not coding a literal production line), how would you structure something like this in object-oriented code? My specific situation is that I have an existing large method calling a lot of other large methods, all of which are peppered with UI interaction, which I want to refactor so I can call it from within a web service. I feel like my route to success is to work out how I could better code it if I were starting from scratch and then aim to evolve the architecture in that direction.

I could break it up into a sequence of method calls that represent steps along the way, but I want to encode the ordering too, since, e.g. calling A then B then C is correct but B then A then C is not. Also those methods would serve no other purpose than being parts of the whole, so it seems like I'm almost arbitrarily chopping the main method into pieces just to satisfy the desire for short methods.

I think a completely different approach is needed and I'd be grateful for some inspiration. It feels like maybe it's a state machine? But then again the ordering must be encoded somewhere.

Any ideas?

3
  • One advice: Clean Code does not always lead to Clean UI (or User Experience). For any user interactions that involve a lot of steps, screens / dialogs, and/or pieces of information, it is extremely important that they are designed so that (1) to maximize the likelihood that a typical user can provide all needed information correctly; (2) that a typical user will not feel frustrated or fail to complete. This often requires a professional UX designer. Consider this to be your "software requirement" - you can implement your clean code solution later on.
    – rwong
    Commented Oct 30, 2020 at 14:06
  • @rwong: this question does not seem to be about UI design.
    – Doc Brown
    Commented Oct 30, 2020 at 15:12
  • @DocBrown // ... all of which are peppered with UI interaction ... //
    – rwong
    Commented Oct 30, 2020 at 15:24

6 Answers 6

6

Sounds like you want to design your system in a more functional manner, using a pipes & filters architecture.

  • decompose your large functions into smaller ones or smaller components which are responsible for processing one step

  • each component should have a well-defined input set and a well defined output sets, with ideally no side-effects except things like logging

  • "UI interactions" should be strictly separated from those components and injected into the processing components using interfaces or callbacks (so they can be exchanged by different technologies, replaced by mocks or automated non-UI reactions)

  • the order of steps is determined in some outer "assembling" method which instantiates the individual components and connects their input and output channels

  • especially for huge datasets, it may be a good idea to use streams or "lazy sequences" (like IEnumerable / yield in C#, or iterators in Python) for the input and output data of the components of the "production line"

This approach leads to components which can be reused very easily within a desktop UI, within a web services or run within some unit tests.

5
  • 1
    This is a nice way to think about it, and a feasible target for refactoring the code I have already - thanks. Commented Oct 30, 2020 at 17:00
  • @DanParsonson Functional FTW. Especially if you want a parallel pipeline, I think this is the way to go. To slightly build on this answer, I'd suggest code like, immutable a = pass1(...); immutable b = pass2(a); immutable c = pass3(b); with each pass being a pure function that inputs something and outputs something new. No mutations occur. That eliminates all temporal coupling and allows you to make the pipeline parallel without worrying about thread-safety. That said, IMO this is moving a bit away from OO, because encapsulation ceases to become so important to maintain invariants [...]
    – user377672
    Commented Oct 31, 2020 at 21:17
  • [...] if your data is immutable and can enforce any relevant invariants whenever we invoke a function to produce a new version of it or something else. DTOs tend to be far more acceptable if they're immutable. Then you end up with something in code that translates to the efficiency of the parallel assembly line in the real world where the machine can be working on a previous stage of the production process in parallel with the next given a lot of items to produce.
    – user377672
    Commented Oct 31, 2020 at 21:19
  • 1
    @DemonCode: "IMO this is moving a bit away from OO," that is probably true, but I don't see this as a disadvantage, quite the opposite.
    – Doc Brown
    Commented Nov 1, 2020 at 7:27
  • @DocBrown I tend to think the same. But tricky to share such opinions here without meeting some pitchforks.
    – user377672
    Commented Nov 1, 2020 at 7:42
2

Long running operations, especially those requiring user interaction, are nicely represented by state, state that is persisted perhaps rather than as objects in memory.

You might consider a message broker, or, a database, to establish persistent queues of things waiting for each specific human interaction required (e.g. approval) or awaiting resources, inventory, or other external events.

I would use OOP within the handlers, but not for persistence when waiting for events like UI.

Modeling the entirety within OOP seems like it could promote a monolithic design, using threads for long running concurrent jobs, and issues during outages, restarts, and upgrades — all of which are pitfalls.  I would consider persistence (stepping out of OOP at certain points) up front.

1
  • While the specific system I'm working on is not quite big enough to go to this level of decoupling, the mental model is useful - thanks. Commented Oct 30, 2020 at 17:02
1

The production line metaphor itself may lead you a less object-oriented design.

Object-orientation is not about deconstructing a problem into steps needed to make some output happen. It is about deconstructing the problem into cooperating, but individual objects. This (i.e. OO) may be the completely new approach you are looking for.

State and ordering are technical things, that can ultimately be encoded in many different forms. For example if you create a good vocabulary (public classes and methods) of what is happening, it might encode the ordering in the type system. I.e. you can only call a method after you somehow create/generate/get another object.

Try to start with things that this process involves. Reports, Accounts, Amounts, you know... business things. Try to imagine methods on these things then and a chain of calls that would solve your problem.

HTH

2
  • @DocBrown You're right, "less OO" is what I meant. I corrected my statement. Commented Oct 30, 2020 at 15:39
  • 2
    "State and ordering are technical things, that can ultimately be encoded in many different forms..." - this paragraph in particular helped jog me out of the procedural mindset I had become stuck in. Very useful, thanks. Commented Oct 30, 2020 at 16:59
0

Let's set aside the parallelisation of operations for now and keep it as a "chain" of operation like a manufacturing chain.

I would advise to read about the factory and builder pattern first.

Then my advise is to decouple the operations as much as possible.

Let's take the point of view of the user. The user/manufacturer only car to have the car build so let's start with buildCar(Model)

buildCar can then ask the different part of the line to do their job. We can have:

buildCar(Model) {
var frame = FrameFactory.BuildFrame(Model)
EngineFactory.BuildEngine(Model, frame)
WheelFactory.BuildEngine(Model, frame)
...
color = PromptUserForColor()
PaintFactory.Paint(frame, color)
return FinalTouch(frame)
}

Here I use the hypothesis that each Factory object can retrieve the proper information from just an ID. This can be achieved with something like a BluePrintDB injected to each factory that contains the parameters of each component for a specific models Example

EngineFactory.BuildEngine(Model, frame) {
 var parameters = BluePrintDB.GetEngineParameters(Model)
 return new Engine(parameters)
}

Each object/class as limited responsibility (SRP principle) even the object holding the buildCar method has only one responsibility (assembling the car) it is delegating details to other object.

0

You are looking for an Orchestrator pattern. The purpose of this pattern is to manage the order that things need to happen in. So in sticking with the car example you would call something like CarOrchestrator.Build() that would handle calling AddFrame then AddAxels and so on.

All the logic to do things would be in other objects, so that the business logic can be isolated and tested separately from managing the order, which can also be easily tested.

-3

Looking just at your requirements alone, from an OO perspective, this is what I came up with (assuming "car" is actually an object that does things):

let car = Car()

Take care!

Edit: Okay. In order to avoid more downvotes, i'll expand on another possible answer.

assuming "production line" is an actual object which does things, another possible approach can be similar to the following:

let productionLine = LeanProductionLine()
productionLine.beginAssemblingCar()
productionLine.stop()

let progress = productionLine.reportProgress()
progress.displayIndicator()
progress.sendEmail()

Hope that helps!

6
  • I noticed someone downvoted my response, but not the others. Can I ask why? Again, looking at the requirements, why can't the car make itself? Why do we need a "CarOrchestrator", "FrameFactory", "lazy sequences", "model", "PaintFactory", etc? Commented Oct 31, 2020 at 1:22
  • 2
    The question was about structuring a sequence of actions, and your answer is just "call a constructor", with no regard to structure at all. It's meaningless. (Note: I didn't donwvote, just explaining what is wrong with your answer.)
    – Frax
    Commented Oct 31, 2020 at 2:23
  • The question that was asked is "how would you structure something like this in object-oriented code?" This is exactly how I would structure it in OOP, thus, it does answer the question. Commented Oct 31, 2020 at 2:38
  • 2
    The OP says that "I have an existing large method calling a lot of other large methods, all of which are peppered with UI interaction, which I want to refactor so I can call it from within a web service.", and your answer is basically "call your function". I'm sorry, it's not just useless, it's ridiculous.
    – Frax
    Commented Nov 1, 2020 at 0:44
  • @AboveTheGods I don't want a constructor or indeed any other method that's 1000+ lines long, and I certainly don't want UI code (or indeed file access code, of which there is also a lot in my case) in any constructor ever. Also I'd like to be able to share my solution between projects that operate with a UI and those that don't. Commented Nov 1, 2020 at 13:47

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