2

I have a scenario wherein I need to do certain specific steps in a specific order. For clarity lets take a simple example.

Let's say I have a Student and some Student Properties which I encapsulate in a class StudentDetails.

A simplistic StudentDetails class:

class StudentDetails {
  private String name;
  private Integer standard;
  private String grade;
  private List<StudentSubjectDetails> subjectDetails;
  // other attributes, if needed
}

I need to apply some Business Logic on this Student and process it in a specific way. For simplicity consider following steps:

  1. Filter Students which are in standard 10 or above.

  2. Filter students with Grade "A" or above.

  3. Process these students. Apply some logic or store their data in a database.

This is quite a simple scenario, but the idea is to segregate the logical steps or commands in separate class so that they can be reused in other similar Business Processes.

The concern I see is with the StudentDetails object which is being used by the different steps.

Typically a class should have all logically related attributes together & those attributes should be used together. But in this case different processes might be interested in different attributes. So they are using only specific attributes of the class at a time.

Is this okay? or is there a better way available?

2

Please ignore "patterns" and "anti-patterns" for now. Implement whatever you need according to your requirements.

In particular, if you have a need to instantiate a boolean filter expression that acts on StudentDetails at runtime (i.e. the composition of the rules aren't hard-coded) and then to use the filtered records somehow, then your approach is correct and you shouldn't worry about "anti-patterns" for now.

Just as a reminder, this approach has a high code maintenance cost, in terms of the number of classes you need. And these aren't generic - they are specifically designed for filtering StudentDetails, and can't be used to filter other types of information. You may need many dozens of classes in order to implement this runtime-instantiable boolean filter expression feature.

Before implementing, check whether the framework(s) and library(s) you are using provides something similar. Not reusing existing facility is perhaps the worst anti-pattern of all. In particular, make you you have read the following:

If you have a million records in SQL, you should do everything in SQL, to the extent possible. Otherwise, if someone performs a query, expecting at most 10 records (or just 1), a filter that is implemented inside the application will end up having to download the one million records. If this is the case, there are several facilities that help you convert your boolean expression into SQL statements.

To begin with, you will have something similar to this:

interface StudentBoolFilter
{
    bool Check(StudentDetails student);
}

interface StudentProcessor
{
    void Process(StudentDetails student);
}

Then, you will need lots of mechanics classes:

class StudentAndFilter : StudentBoolFilter
{
    List<StudentBoolFilter> filters;
    void Add(StudentBoolFilter childFilter);
    bool Check(StudentDetails student)
    {
         if (filters.Count == 0)
         {
             throw new Exception("typically wrong for business software to encounter an AND node without any children.");
         }
         foreach (StudentBoolFilter childFilter in filters)
         {
             if (!childFilter.check(student)) return false;
         }
         return true;
    }
}

Similarly you need StudentOrFilter (N-ary boolean reduction), StudentNotFilter (unary), and so on.

On the processing side, you will need one prepackaged processor (sink), and allow the application programmer to implement additional processor classes (i.e. allow client classes to implement StudentProcessor).

This one processor (sink) writes the received StudentDetails into a list.

class StudentListAppender : StudentProcessor
{
    IList<StudentDetails> targetList;
    StudentListAppender(List<StudentDetails> targetList)
    {
        if (targetList.IsReadOnly) throw new Exception("Cannot write to read-only list!");
        this.targetList = targetList;
    }
    void Process(StudentDetails student)
    {
        targetList.Add(student);
    }
}

Finally, you need one "driver" (a class that will invoke everything you have implemented so far), and I assume that this "driver" will take its input from a list. Note that this processor doesn't implement an interface.

class StudentListProcessor
{
    StudentListProcessor(IList<StudentDetails> students, StudentBoolFilter filter, StudentProcessor processor)
    {
        // Store the three; do nothing.
        // This class only stores one filter, but because
        // a filter can be composite, and a tree of filters
        // can be constructed at runtime, this gives unlimited 
        // possibility.
    }
    void Run()
    {
        foreach (StudentDetails student in students)
        {
            if (!filter.Check(student))
            {
                continue;
            }
            processor.Process(student);
        }
    }
}

Keep in mind this is just one way of implementing this functionality. In particular, because of the presence of the StudentListProcessor class, this implementation belongs to the "push" style of list filtering. (The StudentListProcessor is the class that "pushes" items through the boolean filter tree and finally to the sink/processor.)

There is another implementation that uses "pull" style. A pull-style implementation will act as IEnumerator (an interface that has Current (gets the current item), MoveNext, and Reset).

Implementing in this pull-style alongside filtering is slightly more involved.

In C#, each filter has to query the the previous filter for an item in the implementation of MoveNext. If the received item doesn't evaluate to true in the current filter, the current filter has to keep calling the previous filter's MoveNext and Current to get the next item.

In Java, there is a bigger issue, because calling hasNext also requires the same operations as the MoveNext in C# - it cannot answer the hasNext query without actually receiving an item that has satisfied all boolean conditions in previous stages.

1

I'm of the opinion that the command pattern does not apply here. The intent of the command pattern is to collect lots of pieces of data, then supply them to some sort of processor class Wikipedea. This can easily become an anti-pattern with the command object becoming little more than a container for (almost) global variables.

In my world, what you have is a StudentDetails class. This should hold all attributes that apply to the student, and to the student alone. In your example, name and subjectDetails do this nicely. The class should exhibit behavoural methods that depend only on the class attributes. grade and standard appear to be computed fields that need data from an external database. Strictly speaking they don't belong to a student, but in practice they may be expensive to compute, so caching them in the StudentDetails object may provide a big performance win if they are used multiple times.

Filtering student lists on any criteria is not a job for your StudentDetails class. You need a higher-level class (CourseProcessor?, StudentRanker?) that can iterate across a list of students, compute the selection criteria and select the subset of StudentDetails it wants.This class should be reusable in any part of your code.

Finally, you probably need to carefully consider your database persistence strategy. Fields like grade and standard are poor candidates for database persistence, since they may vary over time as student performance improves or degrades.

This was a good question. I enjoyed it.

  • 1
    The scenario I have at hand is kind of a pipeline. It has distinct stages. Each stage has some specific responsibility - like talking to a dependency & doing something. Each pipeline step has the capability to terminate the pipeline. I was thinking of representing each pipeline stage as a Class (for easy re-use) and was looking for a good strategy to apply them in code instead of having a lot of if-else in my calling method. – AgentX Apr 29 '16 at 9:00
  • 1
    @AgentX A pipeline would possibly lend itself to the state pattern - each state would do some business processing, then continue to the next state (or abort). As you suggest, states are often implemented as classes, but I would cleanly separate the pipeline implementation from the business actions performed by each step. – kiwiron Apr 29 '16 at 9:04
0

From a Domain Driven Design perspective, I would argue that each Command should capture user intent, and only carry the kinds of information that goes with that particular user-action. So the guiding question is "how do your domain-experts mentally categorize this"?

If it's something like "Generate the list of Honor-Roll students", that's probably a single command with a few pieces of information like "time frame" and "year-cohort".

Then, when somebody needs a new one, like "Generate the list of failing students", that's a separate Command that resembles the first but shouldn't be sharing much code. Instead, seek code-reuse inside whatever receives and handles the Command, or better yet within your domain-entities themselves.

The idea of putting Commands inside other Commands makes me nervous, especially because it complicates the question of things like transaction boundaries or audit-logging.

  • In the domain I am working on, there would be requests like - 1. generate list of grade A students & send congratulation email, 2. generate list of grade D students and enroll them in PIP program and send email. So the Business Requests would contain a couple of commands that are exactly the same maybe in different order. So I was planning to represent these Business Steps as commands. Each logical step would be in its own class. – AgentX Apr 29 '16 at 5:41

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.