-1

I'm attempting to develop an open-source Python module for modeling task networks for discrete event simulation. The most fundamental component is the task object, which includes various data such as a distribution of possible task duration times, a release condition (criteria for initiating the task), a beginning effect (statements executed upon initiation of the task), an ending effect (statements executed upon conclusion of the task, such as beginning a different task), and so forth. While each task must include all of the above elements, the logic that they contain is unique to each task instance.

An entire task network could conceivably contain any number of tasks, so I would like to make it as easy as possible to create one. Therefore I was thinking of following an object-oriented paradigm and simply having a task class that users could instantiate, but I'm not sure how to implement the fact that there are different behaviors associated with each task instance. The obvious solution would be to make each task a subclass that overrides the main task class (or possibly just supplies a different class implementation file), but that seems silly because it would result in a ton of pointless singletons.

I also considered wrapping these methods in a dictionary that gets passed to the default constructor, but I don't want the process of defining a task to become unintuitive/require too many steps for the end user. I'm also trying to figure out how this task network data should be stored- raw Python, JSON (but as far as I'm aware this just runs into the same problem since JSON cannot represent the behavior associated with a task)? I'm sure this is probably a lot simpler than I'm making it out to be, but can anybody point me toward a good design pattern for this kind of situation? Thanks.

mcman
  • 1
  • 2
  • *the logic that they contain is unique to each task instance* Are you expecting the user to provide the logic for each task instance? If you are intending to provide the logic for the instances, the instances of the task is fixed. Your user can only pick and choose to use one of the existing instances. Is that what you had in mind? – R Sahu May 17 '17 at 18:45
  • The idea is for the user to provide the logic separately for each task instance (and otherwise these methods would default to empty), ideally during the process of instantiating the task object. – mcman May 17 '17 at 19:48
  • Does a Task need to be represented by a class? Have you considered representing tasks using lambdas (or even closures) instead? – Ben Cottrell May 17 '17 at 19:56

1 Answers1

2

I've implemented a simulation framework for continuous time simulations (in Python) that allowed you to define custom behavior within an overall simulation framework, so I understand what you are getting at with the need for easy extensibility of your simulation objects.

One of the things about OO design is that inheritance gets a lot of play, but really it's composition and dependency injection that I've found to be more useful. From a design persepctive, your situation calls for something like a Strategy Pattern.

Here's some suggestions for making your Tasks user-friendly:

  1. Implement a "Task" as a class that takes other objects as arguments in its constructor. For example, based on your descirption, you'd feed it a task duration timer, a release condition checker (criteria for initiating the task), a beginning effect implementer (statements executed upon initiation of the task), an ending effect implementer (statements executed upon conclusion of the task, such as beginning a different task), and so forth.
  2. Implement each of the classes used in the Task constructor arguments as abstract classes - these are what you'll need to extend when you create new behavior. Note: If you are using a functional programming language (or where functions can be passed back and forth like objects), then you can skip the need for creating objects that encapsulate this behavior and just feed in functions directly (that's why I like Python and its ilk)
  3. The purpose of the Task class is to define the common interface that your simulation engine will expect to have available for working with all Tasks.
  4. Now, program your simulation engine so that you only allow yourself to use the interface defined in (1).
  5. Sub-class your behavior objects defined in (1) to represent the different behaviors you want (and use good, descriptive names for each sub-class). Try to think about how you can break up the behavior such that it is reusable (i.e., you don't have to constantly write new sub-classes, but just compose existing ones).

I hope this helps you organize your code. The key is to program to an interface not a concrete class. Then, you can hook in specific sub-classes (with the same interface) to change behavior as opposed to creating tons of Task sub-classes. You may even be able to use these components multiple times (bonus!)

  • Thank you- I think your suggestions will prove to be very useful as I continue my efforts to tackle this project! If it's not too much trouble, could you possibly elaborate any further on #4? I feel like I'm starting to obtain some clarity re: how I can represent the necessary information, but I'm still trying to wrap my head around how that might plug in to the simulation engine. I suppose that the implementation should be highly abstracted, using placeholders that get dynamically substituted for the various components of the user-generated task network? – mcman May 22 '17 at 15:33
  • @mcman Sure thing. Basically, pretend that Task is a concrete object. I'm assuming you have a class or some code that actually simulates the task network, correct? This class/code needs to use the methods provided by Task to accomplish its goals. If you find you are needing to "peek into" a method provided by Task, this means you need to re-think how you are abstracting Task. The simulation should just be able to call Task.runBeginningEffect() and expect that it will work (its the job of the person subclassing behaviors to make sure this is true). –  May 22 '17 at 15:36
  • @mcman in other words, your simulation should not care HOW Task gets its work done, only that is DOES, and does so CORRECTLY. You are basically putting the burden of correct operation on the person subclassing the behavior objects fed to Task. What your Task interface does is provide a "contract" that says "I promise to provide method X, which accepts the agreed upon arguments and returns agreed upon values". The person writing each of Tasks behavior objects needs to respect this contract so that nothing breaks. –  May 22 '17 at 15:42
  • @mcman as you continue to pursue OOP, you may want to refer to some time-tested design principles that I've found come in handy in deciding how to organize your software (see especially "L" and "D"): https://en.wikipedia.org/wiki/SOLID_(object-oriented_design) –  May 22 '17 at 15:45
  • Great, I think that makes it quite clear. Thanks again! – mcman May 22 '17 at 15:46