📜 ⬆️ ⬇️

Programmer's notes: OOP, And and Or

OOP Philosophy



Encapsulation, inheritance, polymorphism ... Methods, class members, demarcation of privacy, abstraction ... How often do I see articles on OOP and how often I do not see OOP itself in these articles. I do not see the real, live object-oriented programming. The authors are familiar with the terminology, they can give a thousand definitions of the notorious OOP, they will recall a couple of classic examples with a simple inheritance, everywhere they are stuffed with assertions that encapsulation is good ...


')
Yes, encapsulation is good, but it is not the essence of OOP. How not to be all the other terms. Not even the fact that in the PLO it is customary to operate with classes. In fact, the word "accepted" is wrong. In OOP, you can operate with classes, and it is convenient to work with classes, however, there are approaches when there are no classes, but OOP is. How so, why? Is the class the most important part of the PLO, can it be thrown away, will the PLO remain, do we get rid of the classes? Will remain. What is a “class”? Tool, complex multi-tool. And what is the "OOP"? Philosophy. The philosophy of interaction, the philosophy of programmer thinking, a philosophy in which there are laws and rules, methods and techniques, tools and material. You can kick out a musician from behind the piano - he will take a flute. Take the flute, - the musician will pull the string and pretend melody. Tie his hands, and he will just begin to hum: the music is in him.

OOP is like music. It can be studied in theory: extensive scores of the OOP code, a proud violin abstract class, notes-classes in bundles, flat heirs, a refrain, a going overload of methods, and a couple of clever transitions from size to size to achieve a certain effect ... With practice, too OK. Fold a simple melody using a metallophone, tapping a hammer on foreign classes, learn guitar chords, and make a simple program with standard solutions, standard algorithms, standard approaches. Want something non-standard? Then you - in a music school. In it, you will be taught how to play different instruments and correctly combine them, you will learn about design patterns, sample classes, abstraction from algorithms, information hiding, and much more. You suddenly realize that composing a melody is also an art. Past your crafts will seem rude, amorphous, ugly, and you will be surprised that they used to write so awkwardly. It would be better to create a class there and transfer some functions to it; here it is better to use the standard solution than to invent your own; but this unobvious roundabout maneuver is decided only by the abstract class and the three simplest heirs ... Know: you are on the right track. You already understand that OOP is not notes, but music, and you learn harmony. Now, in order to conduct the PLO orchestra or to be a good composer, there are very few, just to finish the conservatory. There you not only study music, you learn to feel it, to breathe it, to think it.

And and Or problem



In my work on the 0.4 version of the ORM for Qt, I encountered an interesting programming problem and was able to solve it beautifully. I wanted to share my thoughts with someone else, and I began to write them down in the form of fragmentary phrases. Soon I took up more meaningful suggestions, - so these programmer notes appeared.

In my QST ORM library, about the 0.3 version of which I already wrote on Habré, there is no generation of complex queries, and this is exactly what I want to implement now. But where there are complex requests, there are also big conditional constructions that you can try to make out by priority. All these nested AND and OR conditions, and even if without parentheses, you can go crazy, until you figure out how to do it. I realized that I first need a class for one small simple condition. Let it be QstCondition, - and nothing that long title. At the preliminary design stage, it will come down well

At first, I wrote on paper what I would like to see in C ++ code if I already had a QstCondition class.

To begin with, let's “program” simple conditions. F1 and F2 are fields in the SQL query.

F1 = F2
F1 = 5
5 = 5

I would like to instantiate QstCondition in a variety of ways.

QstCondition ( "F1" , FunctorEqual, "F2" )
QstCondition ( "F1" , FunctorEqual, QVariant ( 5 ) )
QstCondition ( "F1 = F2" )
QstCondition ( QVariant ( 5 ) , FunctorEqual, QVariant ( 5 ) )
QstCondition ( "F1 =" , QVariant ( 5 ) )


In my library there is a class QstField, - an abstraction over a field in SQL-understanding, QstValue - an abstraction over a constant. The QstCondition class should understand them.

QstCondition ( QstField ( "F1" ) , FunctorEqual, "F2" )

// Here the nested doll: QstField is passed to QstCondition, to which QstValue is sent, to which QVariant is transferred. Recording can be shortened slightly, leaving 5 instead of QVariant (5).
QstCondition ( QstField ( "F1" , QstValue ( QVariant ( 5 ) , FunctorEqual ) ) )

QstCondition ( QstField ( "F1" ) , FunctorEqual, QstField ( "F2" ) )
QstCondition ( QstValue ( QVariant ( 5 ) ) , FunctorEqual, QVariant ( 5 ) )
QstCondition ( QstValue ( QVariant ( 5 ) , FunctorEqual ) , QVariant ( 5 ) )


What should be the designers for all these cases? In the first approximation, it turned out five pieces:

( 1 ) QstCondition ( QString field1, CompareFunctor functor, QString field2 )
( 2 ) QstCondition ( QString field1, CompareFunctor functor, QVariant value )
( 3 ) QstCondition ( QVariant value1, CompareFunctor functor, QVariant value2 )
( 4 ) QstCondition ( QString stringCondition )
( 5 ) QstCondition ( QstField qstField1, CompareFunctor functor, QstField qstField2 )


I already understand that further I have two options for development. Somewhere inside the QstCondition class there will be field names, a functor, values. One would put the field names in two lines, something like:

QstCondition {
QString _fieldName1, _fieldName2 ;
QVariant _value1, _value2 ;
CompareFunctor _functor ;
}


What then to do with the fifth designer? In QstField there is a “field name” - name (), I could extract it and put it in a string:

QstCondition ( QstField field1, CompareFunctor functor, QstField field2 )
: _fieldName1 ( field1. name ( ) ) , // extract and put in a string.
_fieldName2 ( field2. name ( ) ) ,
_value1 ( QVariant ( ) ) ,
_value2 ( QVariant ( ) ) ,
_functor ( functor )
{ }


I predict: I will have to take these very text _fieldName1, _fieldName2 and transfer them many times to QstField to generate SQL. Wouldn't it be better to store the field names right away in the QstField ready class? And more logical, and semantically correct. How is it at the cost of creating / storing / copying QstField class objects? I don’t know, it concerns me last. Especially at the design stage.

Then I remembered that one of the QstField constructors looks like this:

QstField ( const QString & name,
const FieldVisibility & visibility = FieldVisible,
const char * columnTitle = "" ,
const int & columnWidth = 0 ,
const Qt :: Orientation & titleOrientation = Qt :: Horizontal ) ;


I keep the data all the same in QstField, which means that the constructor (1) can be deleted. The constructor (5) will be called instead, even if we pass just two strings as field names. Leave me the constructor (1), and the compiler would curse me: hey, prog, you have ambiguous constructors! To make sure of this, I moved from theory to practice: I wrote the QstCondition class, put everything in it that I did above and did a few tests. That's right, the compiler gives an error, that's just not where I expected:

main.cpp: 67: error: call of overloaded 'QstCondition (const char [3], Qst :: CompareFunctor, const char [3])' is ambiguous
note: candidates are: Qst :: QstCondition :: QstCondition (QVariant, Qst :: CompareFunctor, QVariant)
note: Qst :: QstCondition :: QstCondition (QString, Qst :: CompareFunctor, QVariant)
note: Qst :: QstCondition :: QstCondition (QString, Qst :: CompareFunctor, QString)

That's it. The constructors (1), (2) and (3) conflict. After some deliberation, I rewrote the class, changing the order of parameters at the same time. Now designers look like this:

QstCondition ( QString stringCondition ) ;
QstCondition ( QstField field1, QstField field2, CompareFunctor functor ) ;
QstCondition ( QString fieldName1, QString fieldName2, CompareFunctor functor ) ;
QstCondition ( QstValue value1, QstValue value2, CompareFunctor functor ) ;
QstCondition ( QstField field, QVariant value, CompareFunctor functor ) ;
QstCondition ( QstField field, QstValue value ) ;
QstCondition ( QString fieldName, QstValue value, CompareFunctor functor ) ;


Here, dealt with the simplest cases. It's time to move on to the monsters. I remember about the priority of Boolean operations in the “A AND B OR C” constructions and I understand that I don’t want to tinker with the priority. So, it is necessary to somehow prohibit the creation of such SQL queries. Parentheses? Yes. And where are the brackets, there are nested conditions: "A AND (B OR C)". Recorded target condition in SQL:

F1 = F2 AND (F3 <5 OR F3 IS NULL)

Some hypothetical classes QstAnd and QstOr would be useful.

QstAnd ( QstCondition ( "F1 = F2" ))
QstOr ( "F3 <5" , QstCondition ( "F3 IS NULL" ) )
)


The design is monstrous, but so far, at the preliminary design stage, nothing can be done. Although, one could do something like this:
QstCondition ( "F1 = F2" ) . and (
QstCondition ( "F3" , FunctorLess, 5 ) . or ( "F3 IS NULL" ) . or ( ... ) . or ( ... )
) . and ( ... ) . and ( ... )


Let's take note of this “stringing”, but for now I ask you to pay attention: AND and OR are equal, except for priority, of course. That is, both of the following options should work:

QstAnd ( QstCondition ( "F1 = F2" ))
QstOr ( "F3 <5" , QstCondition ( "F3 IS NULL" ) )
)

QstOr ( QstCondition ( "F1 = F2" ) ,
QstAnd ( "F3 <5" , QstCondition ( "F3 IS NULL" ) )
)


And here's the challenge: how to make these two classes QstAnd and QstOr? Both "know" each other. Theoretically, when this happens in C ++, you can try predestination.

class B ;

class A
{
// uses class B
} ;

class B
{
// use class A
} ;


I rejected this option immediately, without even understanding whether it would have turned out or not. The QstAnd and QstOr classes are equal, and if you think about it, they are the same thing, only the first one gives “AND” and the second one “OR”. They should know about each other as little as possible, should not depend on each other. In a different situation, where there were not two options (and / or), but three, five, ten, or more equal classes, predestination would not work. I do not like the very kind of predestination, something is broken here: not that logical harmony, not the principle of hiding information. I began to think further and came to the conclusion that using inheritance QstAnd and QstOr would be elementary. We set an abstract ancestor class QstBool, prescribe a virtual method for it, and inherit it two times:

class QstBool
{
public :
QstBool ( ) ;
virtual ~ QstBool ( ) = 0 ;
virtual QString operatorName ( ) const ;
} ;

class QstAnd : public QstBool
{
public :
QstAnd ( ) ;
virtual QString operatorName ( ) const { return "AND" ; } ;
} ;

class QstOr : public QstBool
{
public :
QstOr ( ) ;
virtual QString operatorName ( ) const { return "OR" ; } ;
} ;


Everything would work fine, what designers should be called, but ... Let's look again at what we want to get:

QstAnd ( QstCondition ( "F1 = F2" ))
QstOr ( "F3 <5" , QstCondition ( "F3 IS NULL" ) )
)


Let's write a constructor:

QstAnd ( QstCondition condition1, QstOr orCondition2 ) ;


QstAnd should receive a QstOr object in the constructor, about which it knows nothing. I could make this standard for OOP tricks:

QstBool ( QstCondition condition1, QstBool * boolCondition2 ) ;


and transmit not the objects of the QstAnd and QstOr classes, but pointers to these objects. Everything would work, but bad luck: we would have to either pass a pointer to the previously described object, or create this object using the new operator and see that it was deleted.

1 ) .
QstOr myOr1 ( QstCondition ( "F3 <5" ) , QstCondition ( "F3 IS NULL" ) ) ;

QstAnd ( QstCondition ( "F1 = F2" ))
& myOr1 ) ;

2 ) .
QstAnd ( QstCondition ( "F1 = F2" ))
new QstOr ( QstCondition ( "F3 <5" ) , QstCondition ( "F3 IS NULL" ) )
) ;


Neither one nor the other corresponds to what I wanted to receive. Inheritance makes you worry about superfluous things, weighs down the already rather big code.

Is there a way out? .. Templates were spinning in my mind Somewhere I have already met with a similar task. I started thinking about the QstOr, QstAnd and QstBool classes as if they were template. At the same time, neither QstAnd nor QstOr should bring with it such a bulky thing as the template definition, that is, options
  QstAnd <AndStrategy> 
and
  QstOr <OrStrategy> 
fell away because they were forced to write a lot. But the very idea of ​​strategies seems to be good. I wrote two simple classes:

class QstAndOperatorStrategy
{
public :

QString operatorName ( ) const
{
return "AND" ;
}
} ;

class QstOrOperatorStrategy
{
public :

QString operatorName ( ) const
{
return "OR" ;
}
} ;


Now they need to use. How? I couldn’t think of anything, this QstBool made me nervous, and I decided that for now I’ll score on it. Wrote just a class using the “Strategy” template.

template < class T > class BoolTemplate
{
public :

QString operatorName ( ) const
{
T t ;
return t. operatorName ( ) ;
} ;
} ;


Let us suppose. How now to withdraw from this QstAnd and QstOr? At first I tried this:

typedef BoolTemplate < QstAndOperatorStrategy > QstAnd ;
typedef BoolTemplate < QstOrOperatorStrategy > QstOr ;


Everything is good, but again it is impossible to stuff in each of these classes the implicit knowledge of the other. Something is missing. I am again at an impasse. Decided to return to QstBool, experimented. Suppose that what I wrote is not compiled, but it reflects what I want:

template < class Operator > class QstBool
{
public :
QstBool ( ) { } ; // Default constructor

QstBool ( const QstCondition & cond, const QstAnd & op ) { } ;
QstBool ( const QstCondition & cond, const QstOr & op ) { } ;

QstBool ( const QstCondition & cond1, const QstCondition & cond2 ) { } ;
} ;

typedef BoolTemplate < QstAndOperatorStrategy > QstAndTemplate ;
typedef BoolTemplate < QstOrOperatorStrategy > QstOrTemplate ;

typedef QstBool < QstAndTemplate > QstAnd ;
typedef QstBool < QstOrTemplate > QstOr ;


And again, how to transfer to the QstBool class what it is all about, well, does not know anything at all? And do not transmit! Let him get himself in each of the constructors - and after all QstAnd and QstOr - this is QstBool, only pre-defined. The main thing that I had to understand: all three classes are one and the same. QstAnd and QstOr are two sides of the same coin, and QstBool is the coin itself. I wonder what happens if you pass both strategies to QstBool? I add the second strategy, rewrite typedefs:

template < class Operator1, class Operator2 > class QstBool
{
public :
QstBool ( ) { } ;

QstBool ( const QstCondition & cond, const QstAnd & op ) { } ;
QstBool ( const QstCondition & cond, const QstOr & op ) { } ;

QstBool ( const QstCondition & cond1, const QstCondition & cond2 ) { } ;
} ;

typedef QstBool < QstAndTemplate, QstOrTemplate > QstAnd ; // I put the second strategy, different from the first.
typedef QstBool < QstOrTemplate, QstAndTemplate > QstOr ;


So ... I feel that the mystery has almost fallen. Replace QstAnd and QstOr with what I have in typedef and get beauty:

template < class Operator1, class Operator2 > class QstBool
{
public :
QstBool ( ) { } ;

QstBool ( const QstCondition & cond, const QstBool < QstAndTemplate, QstOrTemplate > & op ) { } ;
QstBool ( const QstCondition & cond, const QstBool < QstOrTemplate, QstAndTemplate > & op ) { } ;

QstBool ( const QstCondition & cond1, const QstCondition & cond2 ) { } ;
} ;


And again: QstBool does not know anything about the QstAndTemplate, QstOrTemplate classes, but after all I give them as strategies! I replace QstAndTemplate with Operator1, QstOrTemplate with Operator2. As a result, the code looks like this:

template < class Operator1, class Operator2 >
class QstBool
{
public :
QstBool ( ) { } ;

QstBool ( const QstCondition & cond, const QstBool < Operator1, Operator2 > & op ) { } ;
QstBool ( const QstCondition & cond, const QstBool < Operator2, Operator1 > & op ) { } ;

QstBool ( const QstCondition & cond1, const QstCondition & cond2 ) { } ;
} ;


I quickly write examples:

QstAnd andCond ( QstCondition ( "F1 = F2" ) ,
QstOr ( QstCondition ( "F3 = F4" ) , QstCondition ( "F3 IS NULL" ) ) ) ;

QstOr orCond ( QstCondition ( "F1 = F2" ) ,
QstAnd ( QstCondition ( "F3 = F4" ) , QstCondition ( "F3 IS NULL" ) ) ) ;


Both work! There are very few. I want the objects to return the string of their operator. I add a function to QstBool:

QString operatorName ( ) const
{
Operator1 t ;
return t. operatorName ( ) ;
} ;


The trick is that for QstAnd QstAndTemplate comes as Operator1, and QstOr comes as QstOrTemplate, and each time the function returns exactly what you need. Make sure the result is simple:

QstAnd andCond2 ;
QstOr orCond2 ;

qDebug ( ) << andCond2. operatorName ( ) ; // Displays AND
qDebug ( ) << orCond2. operatorName ( ) ; // Displays OR


Wonderful! So, do not relax. I will give all the code that turned out:

class QstAndOperatorStrategy
{
public :

QString operatorName ( ) const
{
return "AND" ;
}
} ;

class QstOrOperatorStrategy
{
public :

QString operatorName ( ) const
{
return "OR" ;
}
} ;

template < class T > class BoolTemplate
{
public :

QString operatorName ( ) const
{
T t ;
return t. operatorName ( ) ;
} ;
} ;

template < class Operator1, class Operator2 >
class QstBool
{
public :

QstBool ( ) { } ;

QstBool ( const QstCondition & cond, const QstBool < Operator1, Operator2 > & op ) { } ;
QstBool ( const QstCondition & cond, const QstBool < Operator2, Operator1 > & op ) { } ;

QstBool ( const QstCondition & cond1, const QstCondition & cond2 ) { } ;

QString operatorName ( ) const
{
Operator1 t ;
return t. operatorName ( ) ;
} ;
} ;

typedef BoolTemplate < QstAndOperatorStrategy > QstAndTemplate ;
typedef BoolTemplate < QstOrOperatorStrategy > QstOrTemplate ;

typedef QstBool < QstAndTemplate, QstOrTemplate > QstAnd ; // I put the second strategy, different from the first.
typedef QstBool < QstOrTemplate, QstAndTemplate > QstOr ;


Take a closer look: the BoolTemplate, QstAndTemplate and QstOrTemplate classes are redundant. I remove this unnecessary layer. Now let's think about two constructors:

QstBool ( const QstCondition & cond, const QstBool < Operator1, Operator2 > & op ) { } ;
QstBool ( const QstCondition & cond, const QstBool < Operator2, Operator1 > & op ) { } ;


Because of the first, constructions of the form QstAnd (QstCondition, QstAnd) and QstOr (QstCondition, QstOr) are possible. They, in principle, do not hinder much, if you need to build such a SQL code: F1 AND (F2 AND F3), I don’t see much point in this. I remove the first constructor - I prohibit using it inside the QstAnd class. The same for QstOr.

template < class Operator1, class Operator2 >
class QstBool
{
public :
QstBool ( ) { } ;

QstBool ( const QstCondition & cond, const QstBool < Operator2, Operator1 > & op ) { } ;

QstBool ( const QstCondition & cond1, const QstCondition & cond2 ) { } ;
QstBool ( const QString & stringCond1,
const QString & stringCond2 ) { } ;

QString operatorName ( ) const
{
Operator1 op1 ;
return op1. operatorName ( ) ;
} ;
} ;

typedef QstBool < QstAndOperatorStrategy, QstOrOperatorStrategy > QstAnd ;
typedef QstBool < QstOrOperatorStrategy, QstAndOperatorStrategy > QstOr ;


So, the main task is solved - and solved beautifully. Lastly, I will give another twist. Recall that a little earlier, I argued that it would be possible to organize "stringing":

QstCondition ( "F1 = F2" ) . and (
QstCondition ( "F3" , FunctorLess, 5 ) . or ( "F3 IS NULL" ) . or ( ... ) . or ( ... )
) . and ( ... ) . and ( ... )

For the QstAnd and QstOr classes, these functions would be useful. Only I have to remember that if AND'y are strung, OR is already unacceptable and vice versa. How to do this? Elementary. We add to the strategy classes by one function responsible for our operation. It was necessary to write functions in large letters, because the compiler swears at the "and ()" function.

class QstAndOperatorStrategy
{
public :

QstAndOperatorStrategy AND ( const QstCondition & condition )
{
// Code
return * this ;
} ;

QString operatorName ( ) const { return "AND" ; }
} ;

class QstOrOperatorStrategy
{
public :

QstOrOperatorStrategy OR ( const QstCondition & condition )
{
// Code
return * this ;
} ;

QString

Source: https://habr.com/ru/post/93931/


All Articles