Sunday, February 04, 2007

How to process dependant tasks


Математиците казват, че ако можеш да си дефинираш поставената задачата, то тя наполовина е решена. Програмистите се шегуват, че всяко задание е решено тогава и само тогава, когато решението удоволетворява и всички бъдещи изменения на заданието. Вярно е, че непрекъснато се променят условията, но всички го знаят. Затова може би се приема за решение всяко лесно изменимо приложение. Искам да споделя процеса на решение на една задача, с която си играх тези дни. Много малко книги са написани така, че да покажат еволюцията на идеата в главата на автора. Дадени са cool решения и те карат да се подтискаш. Всяко решение си има исторя. Ето и първоначалното "поръчение", което получих:



Получаваш два обеката SuperTask и Task, които са в определено състояние (state). Можеш да изпълниш SuperTask само при условие, че можеш да доведеш Task до състояние "Complated". Състоянията са две - Completed, NotStarted.


Тъй като тази структура не е нищо особенно я аз мога да се справя мнооо лесно като напиша нещо от рода на (примерно):


.....
Task task = superTask.getChildTask();
if (Task.COMPLATED_STATE.equals(task.getStatus())) {
superTask.process();
} else {
log.debug("Child task is not ready..");
// do something else
}
.....


Е, какво пък толкова - върши работа и е добре четимо. Много се чудя на колеги, които гадаят какво може да се случи и залитат в една посока. Не знам дали ви се е случвало такова нещо. И после какво, идва новото задание и наистина е интересно да се гледа реакцията. Ами когато сочи в друга посока? За да се докаже човек прави и "задни салта" за промяна на кода и нито крачка назад. Понякога тази крачка е и невъзможна ако "новата посока" прекалено късно и върху "гадаенето" са се понатрупали досна нови добавки. Затова аз съм си за принципа KISS - Keep it Simple Stupid. Така си винаги в посоката, в която духне вятъра.
Не закъснява и промяната:

SuperTask може да има и повече от един Child. За да може да се обработи всички Child трябва да са в Completed. Няма промяна на броя на състоянията.

Та като говорим за вятъра, каква може да е посоката. Ами ако се увеличава броя на Task? О, не е "big deal" си казвам нали има цикъл for. Слагам for и си свиркам, вместо за ден аз се оправям за час. Па и шефа доволен. Обаче мамка му ако се променят броя на състоянията - закивам. Тогава ще стане едно if ... else няма оправяне. Може да се достигне до нещо от рода на:

if(STATUS_COMPLETED.equals(this.status)) return false;
...
if(taskFromSf.getStatus()!= null && !taskFromSf.getStatus().equals(status)){
...
}
if(STATUS_VERIFICATION.equals(status)){
....
}


Аз никак не мога да се оправям с такъв "хоп-хоп" код. Все му губя нишката. А и тестването става нещо ужасно. При това условие ще влезе в този if, а иначе в онзи. Ами нали уж Java, нали уж обекти джиджи-биджи. Не искам блок-схеми решение. И в един момент уж последното задание направо настръхнах:

Child могат да имат повече от две състояния - цели 4 (NotStarted, Verivication, Activating, Completed)


Казах си стига! И започнах да мисля как да пооправя нещата. Тъй като всичко е на английски в началото залитнах като се хванах за думичката "state". Не е ли очевидно - "State Pattarn". Цял ден експлоатирах идеята. Да ама не. Получи се нещо ама беше извратено. Аз се занимавам с Таsk процедирам с него, по определен начин. Няма ли да е добре да се съберат всияки операции върху този обект на едно място. Да кажа:

task.process();

и в зависимост от състоянието на Task, process да реаргира по различен начин. Леле и на това ако не му се вика polymorphism. Един обект да има много(poly-) форми (-morphism). После ми изникна примерчето от една книжка - много видове криптиране и всеки в отделен клас. И тогава:

file.crypt();

и в зависимост от това какъв ТИП (= STATE) e file, crypt() реаргира по различен начин. Главата в книгата беше озаглавена "Strategy Pattern". Добре де, ама защо да избера него. Един пасаж ме убеди:

To alter the behavior of the context (file.crypt()), a client object needs to
configure the context with the selected strategy instance. This type of
arrangement completely separates the implementation of an algorithm from the context that uses it.

As a result, when an existing algorithm implementation is changed or a new algorithm
is added to the group, both the context and the client object (that uses the context) remain unaffected.

Хехе ами да ми дават колкото си състояния тогава аз ще си ги отделям и ще им имплементирам специфичен process() метод (..и други ако трябва). Важно е да четеш "правилините" книги, ей.

+--------------+ +----------------+
| State | | TaskWorkflow |
| | <--------------| (Contex) |
+--------------+ +----------------+
/ \
/ ... \
/ \
+----------------+ +----------------+
|VerificationTask| | ActivationTask |
| | | |
+----------------+ +----------------+


Мога да отделя всичко не е късно. Радвам се. Внимателно си подбирам интерфейсите и систематизирам нещата:

interface SystemInterface {

// our specific operations
void escalate();
void persist();
void reverse();
}

interface State extends SystemInterface {

// Possible states
String NOTSTARTED_STATE = "Not Started";
String ACTIVATION_STATE = "Activation";
String VERIFICATION_STATE = "Verification";
String COMPLATE_STATE = "Complate";

// workflow operaion
void process();
}

public class NotStartedState implements State ...
public class ActivationState implements State ...
public class ValidationState implements State ...
public class ComplatedState implements State ...


Пфуу усетих се навреме. Готов съм да посрешна следващата буря, нещата се подредени и най-важното всеки след мен ще ги разбере. Знам къде какво ме чака и знам къде да го намеря. За десерт са ми оставили нещо специално:


Всеки Task може да има също наследници и те наследници...демек до безкрай. Task отива в Completed т.и.с.т.к всички негови наследници са достигнали Complete. И най-потресаващото, че една задача може да е преди друга.

Много ме обърка последното изискване - признавам.
Както са ме е учил другаря Цанков по математика, бързам да онагледя нещата:

+--------------+
| Super Task |
| | ROOT
+--------------+
/ \
/ ... \
/ \
* *
+--------------+ +--------------+
| Child Task 1 | | Child Task 2 |
|(Verification)| | (Activating) | LEVEL 1
+--------------+ +--------------+
/ \
/ ... \
/ \
* *
+--------------+ +--------------+
| Child Task 3 | | Child Task 4 |
| (NotStarted) | | (Completed) | LEVEL 2
+--------------+ +--------------+


Така такаааа, почват разсъжденията по картинка. Имам ги типовите операции значи сега трябва да се средоточа по обхождането на дървото, което в крайна сметка се получи. След като има предусловие трябва да почна от долу-нагоре. Рекурсия ще играе тука момче, но след Lisp закалката само се усмихвам под (заченка на) мустак. Важно е да се уточни тука и как се изгражда дървото и как работя с него. Като дадено имам SuperTask и после започвам да закачам един вид "условията" - Task. Така че, в Task имам:

public void addChildTask(TaskObj child) {

assert child != null;

child.setParentTask(this);
childTasks.add(child);
}


Искам да кажа, че аз сам си изграждам дървото. Добре де, аз не моооо ли да кажа getAllChildTask(). Мога, ето как:

public List<TaskObj> getAllChildTasks() {

List<TaskObj> taskStack = new ArrayList<TaskObj>();

Iterator<TaskObj> subTasks = childTasks.iterator();
while ( subTasks.hasNext() ) {
taskStack.addAll(subTasks.next().getAllChildTasks());
}

taskStack.add(this);

return taskStack;
}

Докато си пусках тестовете видях, че греша и добавям и смия SuperTask. Обаче ме осени следната идея: това не е ли ми дава и реда на изпълнение на task.process()? Не ми ли дава свобода да се "движа" навсякъде с SuperTask и спокоино да си вземам обектите, към които има referece? Ами да!

Call stack
+--------------------------+
| 3 | 4 | 1 | 2| SuperTask |
+--------------------------+

Е, не са в идеален ред, но за мен е важно, че е спазен реда на извикване по нива LEVEL2, LEVEL1, ROOT. За реда(кой преди кой) на обработването на Task се получи някак естествено, след като аз си изграждам дървото, който е по-напред е по-дълбоко в йерархията и точка. Ами май май това е. За сега няма ново поръчение.


Малко изводи да си направим сега. Всеки програмист се влияе от кода написан преди него. Ако аз бях видял нещо от рода на:


while(salesOrders.hasNext()){
SalesOrder salesOrder = salesOrders.next();
Iterator services = salesOrder.getProduct().getServices().iterator();
while(services.hasNext()){
Service service = services.next();
Iterator resources = service.getResources().iterator();
while(resources.hasNext()){
Resource resource = resources.next();
if(resource.getValue()!=null) continue;

boolean automated = lookup.isResourceAutomatic(resource.getName());
String hashKey = automated+"/"+ service.getRfsName()+"/"+ resource.getRoleQueue();
TaskObj workTaskObj = tasksInMotion.get(hashKey);
if(workTaskObj==null || workTaskObj.getResources().size()==9){
TaskObj newWorkTaskObj = new TaskObj(
....

от мен какъв код се очаква за бога? Добър програмист не е този, който може да го чете, а този който не го допуска. Хората по природа са мързеливи и тръгват по пътя с най-малко съпротивление. Когато се поставят и срокове ставата гадни хакове. Прихващаме бързо лошите неща. Когато един софтуер започне да се "крепи" в крайна сметка се получава по-зле и по-зле и по-... Това, че рефакторираш използвайки Extract Method и Rename е като да си подменяш плочките на банята в къща без основи. Това е началото на края. Далече от това! Важното е да направиш промяната навреме, да евоюираш заедно с посоката на заданията. И другото е да атакауваш проблема в неговата основа, а не да се търсят заобикони пътища. Ako да речем ми трябва и точен ред на извикване няма да се опитвам да променям целия workflow, а ще потърся начин да изградя самата структура по-добре. Task се взема/слага в базата през Hibernate. Тогава сменям част от кода на Hibernate и имам:


<!-- Relation Foreign key -->
<many-to-one name="parentTask" class="TaskObj"
foreign-key="FK_TASK_PARENT_ID" lazy="false">
<column name="PARENT_TASK_ID" not-null="false" />
</many-to-one>

<bag name="childTasks" cascade="all" lazy="false">
<key column="PARENT_TASK_ID" />
<one-to-many class="TaskObj" />
</bag>

вместо:
.............
<set name="childTasks" cascade="all" lazy="false">
<key column="PARENT_TASK_ID" />
<one-to-many class="TaskObj" />
</set>


и вече разполагам с List интерфейс, вместо с Set и мога лесно да променя "Call Stack"-a.

public Stack<TaskObj> getDependenciesCallStack() {

Stack<TaskObj> taskStack = new Stack<TaskObj>();

for ( TaskObj child : getChildTasks() ) {
taskStack.push(child);
taskStack.addAll(child.getDependenciesCallStack());
}

return taskStack;
}


Винаги съм подозрителен и затова решавам да си направя визоализация на дървото от задачи:

private int currentPosition = 0;
private final StringBuffer listTask = new StringBuffer(100);

private static final int INDENT_STEP = 5;

/**
* Display(first) TaskObj taskId and subject in a tree structure
*/
public void dumpTaskTree() {

// display title
listTask.append("\n============================\n");
listTask.append("(ROOT) ").append(this.getSubject()).append('\n');

final Set subtasks = this.getChildTasks();
final Iterator tIter = subtasks.iterator();
while ( tIter.hasNext() ) {
final TaskObj metaTask = (TaskObj) tIter.next();

// Task properties that I need
listTask.append(" !-- ");
listTask.append(metaTask.getSubject());
listTask.append('(');
listTask.append(metaTask.getId());
listTask.append(", ");
listTask.append(metaTask.getStatus());
listTask.append(")\n");

subTasksDraw(metaTask, INDENT_STEP);
}

log.debug(listTask.toString());
}


/**
* Draw childes task(rest) of a meta task if they exist
*
* @param metaTask
* @param shift
* indentation step
*/
private void subTasksDraw(final TaskObj metaTask, final int shift) {
final Set subtasks = metaTask.getChildTasks();
final Iterator tIter = subtasks.iterator();

while ( tIter.hasNext() ) {
final TaskObj aTask = (TaskObj) tIter.next();

currentPosition = currentPosition + shift;
for ( int i = 0; i < currentPosition; i++ ) {
listTask.append(" ");
}

listTask.append("\\+--- ");
listTask.append(aTask.getSubject());
listTask.append('(');
listTask.append(aTask.getId());
listTask.append(", ");
listTask.append(aTask.getStatus());
listTask.append(")\n");

subTasksDraw(aTask, shift);
}
currentPosition = 0;
}


И последно: "Premature optimization is the root of all evil."


ПП.
Той: "Добро утро, ще можите ли да добавите и друг тип обекти към SuperTak, освен Task?"
Аз: "Да. Чували ли сте за Visitor Pattern?"
Той: "А може ли ..." (внезапно изгубва съзнание и диалога прекъсва)

1 comment:

Unknown said...

Браво, Злато :))
Много ми хареса това решение :)))
Супер просто! :)

algorithms (1) cpp (3) cv (1) daily (4) emacs (2) freebsd (4) java (3) javascript (1) JSON (1) linux (2) Lisp (7) misc (8) programming (16) Python (4) SICP (1) source control (4) sql (1) думи (8)