Organizing Classes

Not for 'how-to' coding questions but PHP theory instead, this forum is here for those of us who wish to learn about design aspects of programming with PHP.

Moderator: General Moderators

User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Re: Organizing Classes

Post by Christopher »

astions wrote:It seems that the infix parser would need to be a distinct class that uses the number and calculator class. Is that right?
Yeah, any sort of equation parsers would wrap the calculator class you implemented. Depending on whether it was a stack or recursive design would determine how it called your class. Perhaps you could implement an op($op, $value) method in your class that is called like $calc->op('/', 2) or $calc->op('+', 10). It would call the other methods. That would provide support for symbol based callers.
(#10850)
User avatar
Benjamin
Site Administrator
Posts: 6935
Joined: Sun May 19, 2002 10:24 pm

Re: Organizing Classes

Post by Benjamin »

PCSpectra wrote:Try and implement one that supports infix and you'll see what they mean. :)
Thank you PCSpectra. Implementing that feature caused me to refactor the number class.

I have changed the following:
  • The number class now has only 1 get and set method. The base is specified as an optional second parameter.
  • I removed the base_convert() function because it doesn't support floats. I replaced it with other base conversion functions, but they don't seem to be reliable either :(
arborint wrote:Perhaps you could implement an op($op, $value) method in your class that is called like $calc->op('/', 2) or $calc->op('+', 10).
I think that's a good idea, but when I tried to do it, it seemed that it would be a bit clunky. I'll look into it more.

Here are the revised classes:

Code: Select all

 
class calculator {
    private $_n;
 
    public function __construct(number $n) {
        $this->_n = $n;
    }
 
    public function add(number $n) {
        $this->_n->set($this->_n->get() + $n->get());
    }
 
    public function subtract(number $n) {
        $this->_n->set($this->_n->get() - $n->get());
    }
 
    public function divide(number $n) {
        $this->_n->set($this->_n->get() / $n->get());
    }
 
    public function multiply(number $n) {
        $this->_n->set($this->_n->get() * $n->get());
    }
 
    public function equals() {
        return $this->_n;
    }
}
 
class number {
    private $_n;
 
    public function __construct($n = 0, $base = 10) {
        $this->_store($n, $base);
    }
 
    public function set($n, $base = 10) {
        $this->_store($n, $base);
    }
 
    public function get($base = 10) {
        return $this->_retrieve($base);
    }
 
    private function _store($n, $base) {
        switch ($base) {
            case 2:
                $this->_n = bindec($n);
                break;
            case 8:
                $this->_n = octdec($n);
                break;
            case 10:
                $this->_n = $n;
                break;
            case 16:
                $this->_n = dexhec($n);
                break;
            default:
                $this->_n = $n;
                trigger_error("" . get_class($this) . "() Second parameter must be one of 2, 8, 10, 16");
                break;
        }
    }
 
    private function _retrieve($base) {
        switch ($base) {
            case 2:
                 return decbin($this->_n);
                break;
            case 8:
                return decoct($this->_n);
                break;
            case 10:
                return $this->_n;
                break;
            case 16:
                return hexdec($this->_n);
                break;
            default:
                trigger_error("" . get_class($this) . "() Second parameter must be one of 2, 8, 10, 16");
                return $this->_n;
                break;
        }
    }
}
 
class numberSet {
    private $_numbers = array();
 
    public function addNumber(number $number) {
        $this->_numbers[] = $number;
    }
 
    public function getAverage() {
        $calculator = new calculator(new number());
 
        foreach ($this->_numbers as $number) {
            $calculator->add($number);
        }
 
        $calculator->divide(new number(count($this->_numbers)));
 
        return $calculator->equals();
    }
}
 
Here is infix notation parser:

Code: Select all

 
/*
 * NOTE: does not support exponents or roots
 */
 
class infixNotation {
    /*
     * stores the original equation
     */
    private $_equation;
 
    /*
     * stores the equation as it is calculated by the parsing engine
     */
    private $_equation_buffer;
 
    /*
     * stores the number base of the equation
     */
    private $_base;
 
    /*
     * regex for finding the inner most expression inside ()
     */
    const REGEX_NESTED_EXPRESSION = '#\(([^()]{1,})\)#';
 
    /*
     * regex for finding the first multiplication or division set in an expression
     */
    const REGEX_FIRST_MUL_OR_DIV = '#([\da-f\.]+)\s*([/\*])\s*([\da-f\.]+)#';
 
    /*
     * regex for finding the first subtraction or addition set in an expression
     */
    const REGEX_FIRST_SUB_OR_ADD = '#([\da-f\.]+)\s*([-\+])\s*([\da-f\.]+)#';
 
    public function __construct($e = '', $base = 10) {
        $this->setEquation($e);
        $this->_base = $base;
    }
 
    public function setEquation($e, $base = 10) {
        $this->_equation = $e;
        $this->_base = $base;
    }
 
    public function getResult() {
        $this->_equation_buffer = $this->_equation;
 
        while ($expression = $this->_parseEquation()) {
            $this->_equation_buffer = preg_replace(self::REGEX_NESTED_EXPRESSION, $this->_parseExpression($expression), $this->_equation_buffer, 1);
        }
 
        return $this->_equation_buffer = $this->_parseExpression($this->_equation_buffer);
    }
 
    private function _parseEquation() {
        if (preg_match(self::REGEX_NESTED_EXPRESSION, $this->_equation_buffer, $expression)) {
            return $expression[1];
        } else {
            return false;
        }
    }
 
    private function _parseExpression($expression) {
        while (preg_match(self::REGEX_FIRST_MUL_OR_DIV, $expression, $x)) {
            $calculate = new calculator(new number($x[1], $this->_base));
 
            if ($x[2] == '/') {
                $calculate->divide(new number($x[3], $this->_base));
            } else {
                $calculate->multiply(new number($x[3], $this->_base));
            }
 
            $result = $calculate->equals();
 
            $expression = preg_replace(self::REGEX_FIRST_MUL_OR_DIV, $result->get($this->_base), $expression, 1);
        }
 
        while (preg_match(self::REGEX_FIRST_SUB_OR_ADD, $expression, $x)) {
            $calculate = new calculator(new number($x[1], $this->_base));
 
            if ($x[2] == '-') {
                $calculate->subtract(new number($x[3], $this->_base));
            } else {
                $calculate->add(new number($x[3], $this->_base));
            }
 
            $result = $calculate->equals();
 
            $expression = preg_replace(self::REGEX_FIRST_SUB_OR_ADD, $result->get($this->_base), $expression, 1);
        }
 
        return trim($expression);
    }
}
 
Here is a base 10 use case for the notation parser:

Code: Select all

 
$inf = new infixNotation('2+2');
echo $inf->getResult() . "<br>\n";
 
$inf = new infixNotation('2 + 2');
echo $inf->getResult() . "<br>\n";
 
$inf = new infixNotation('2 + 2 * 10');
echo $inf->getResult() . "<br>\n";
 
$inf = new infixNotation('(2 + 2) * 10');
echo $inf->getResult() . "<br>\n";
 
$inf = new infixNotation('(2 + 2) * 10 / 5');
echo $inf->getResult() . "<br>\n";
 
$inf = new infixNotation('(2 + 2) * 10 / 5 + 50');
echo $inf->getResult() . "<br>\n";
 
$inf = new infixNotation('(2 + 2) * 10 / 5 + 50 * 500');
echo $inf->getResult() . "<br>\n";
 
$inf = new infixNotation('(2 + 2) * 10 / (5 + 50) * 500');
echo $inf->getResult() . "<br>\n";
 
$inf = new infixNotation('(2 + 2) + (10 / 5) + 50 * 500 - 100');
echo $inf->getResult() . "<br>\n";
 
$inf = new infixNotation('(2 * 2 + 3 * 10) + (10 / 5) + 50 * 500 - 100');
echo $inf->getResult() . "<br>\n";
 
$inf = new infixNotation('(2 * (2 + 3) * 10) + (10 / 5) + 50 * 500 - 100');
echo $inf->getResult() . "<br>\n";
 
$inf = new infixNotation('(2 * (2 * 5 + 3) * 10) + (10 / 5) + 50 * 500 - 100');
echo $inf->getResult() . "<br>\n";
 
$inf = new infixNotation('(2 * (2 * (5 + 3)) * 10) + (10 / 5) + 50 * 500 - 100');
echo $inf->getResult() . "<br>\n";
 
And the output:
4
4
22
40
8
58
25008
363.636363636
24906
24936
25002
25162
25222
Here is a base 2 (binary) use case for the notation parser:

Code: Select all

 
$inf = new infixNotation('10+10', 2);
$x = new number($inf->getResult(), 2);
echo $x->get() . "\n<br />";
 
$inf = new infixNotation('10 + 10', 2);
$x = new number($inf->getResult(), 2);
echo $x->get() . "\n<br />";
 
$inf = new infixNotation('10 + 10 * 1010', 2);
$x = new number($inf->getResult(), 2);
echo $x->get() . "\n<br />";
 
$inf = new infixNotation('(10 + 10) * 1010', 2);
$x = new number($inf->getResult(), 2);
echo $x->get() . "\n<br />";
 
$inf = new infixNotation('(10 + 10) * 1010 / 101', 2);
$x = new number($inf->getResult(), 2);
echo $x->get() . "\n<br />";
 
$inf = new infixNotation('(10 + 10) * 1010 / 101 + 110010', 2);
$x = new number($inf->getResult(), 2);
echo $x->get() . "\n<br />";
 
$inf = new infixNotation('(10 + 10) * 1010 / 101 + 110010 * 111110100', 2);
$x = new number($inf->getResult(), 2);
echo $x->get() . "\n<br />";
 
$inf = new infixNotation('(10 + 10) * 1010 / (101 + 110010) * 111110100', 2);
$x = new number($inf->getResult(), 2);
echo $x->get() . "\n<br />";
 
$inf = new infixNotation('(10 + 10) + (1010 / 101) + 110010 * 111110100 - 1100100', 2);
$x = new number($inf->getResult(), 2);
echo $x->get() . "\n<br />";
 
$inf = new infixNotation('(10 * 10 + 11 * 1010) + (1010 / 101) + 110010 * 111110100 - 1100100', 2);
$x = new number($inf->getResult(), 2);
echo $x->get() . "\n<br />";
 
$inf = new infixNotation('(10 * (10 + 11) * 1010) + (1010 / 101) + 110010 * 111110100 - 1100100', 2);
$x = new number($inf->getResult(), 2);
echo $x->get() . "\n<br />";
 
$inf = new infixNotation('(10 * (10 * 101 + 11) * 1010) + (1010 / 101) + 110010 * 111110100 - 1100100', 2);
$x = new number($inf->getResult(), 2);
echo $x->get() . "\n<br />";
 
$inf = new infixNotation('(10 * (10 * (101 + 11)) * 1010) + (1010 / 101) + 110010 * 111110100 - 1100100', 2);
$x = new number($inf->getResult(), 2);
echo $x->get() . "\n<br />";
 
And the output:
4
4
22
40
8
58
25008
0
24906
24936
25002
25162
25222
As you can see, the numbers class is having issues base converting floats :(

Other than that I'm pretty happy with how this has progressed. What's next?
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Re: Organizing Classes

Post by Christopher »

Nice...
(#10850)
josh
DevNet Master
Posts: 4872
Joined: Wed Feb 11, 2004 3:23 pm
Location: Palm beach, Florida

Re: Organizing Classes

Post by josh »

astions wrote: What's next?
TDD
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Re: Organizing Classes

Post by Christopher »

Or more realistically some test coverage for these classes to see how tests fit with OO. You might want to start looking into patterns, which are regularly occurring problems/solutions -- sort of one level more abstract conceptually than recipe/cookbook solutions.
(#10850)
User avatar
Benjamin
Site Administrator
Posts: 6935
Joined: Sun May 19, 2002 10:24 pm

Re: Organizing Classes

Post by Benjamin »

I've been thinking about how this has evolved and here's what I think.

I started with a class that wasn't built correctly from the start. With good input from the posts I received I was able to refactor it into something significantly better. After arborint stated that classes should be something, rather than an action I followed his recommendation and renamed the average class to numberSet. At this point if I was a teacher, I would have given my code a grade of B or C.

Then PCSpectra comes along and says hey, try writing an infix notation parser, effectively throwing a wrench at me. So I had to do some refactoring in order to ensure that all these classes worked together cohesively. I'm very glad that happened because I want to ensure that I write classes correctly from the start. I don't want to write a library full of classes and have to refactor a bunch of them to add a feature or something.

This is by no means production quality code in my mind. There are issues I see in it that need to be fixed, but it has been a good exercise.

Josh, you are recommending TDD, arborint, you are saying that I should write test cases for the current classes. After attempting to debug this, I can definitely see why OO programmers are all for unit tests and the like. Debugging OO is a b****.

My existing coding style, a mix of objects and procedural doesn't require that as errors are pretty much spotted instantly and the source doesn't have to be tracked down.

I will continue to research this and learn more about it. From what I see so far there are definitely benefits to using pure OO programming, but there are also drawbacks as well. These drawbacks may include increased development time, increased difficulty in finding bugs, increased performance overhead and a much smaller pool of programmers qualified to work on the codebase. On the other hand, procedural, when not written by an experienced programmer can exhibit the same problems.

I think the net gain of OO is a long term investment. In the end it becomes more cost effective to maintain and modify.

At this point I want to make sure I have a solid understanding of Models. I've got a feeling that there should be something between the model and the view. Anyone want to tell me the name of that, if there is one?

My goal is to merge my existing style with OO. I'm not sure what will come out but I intend to mix the best of both worlds if possible. I'm not saying that I want to mix procedural with OO, I'm referring to my style of programming, which is a balance of efficiency and accelerated development.
Last edited by Benjamin on Thu Feb 19, 2009 3:36 am, edited 1 time in total.
josh
DevNet Master
Posts: 4872
Joined: Wed Feb 11, 2004 3:23 pm
Location: Palm beach, Florida

Re: Organizing Classes

Post by josh »

Controllers can create models and assign them to the view, or a view can invoke a helper which pulls models from behind the scenes into the view
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Re: Organizing Classes

Post by Christopher »

astions wrote:Josh, you are recommending TDD, arborint, you are saying that I should write test cases for the current classes. After attempting to debug this, I can definitely see why OO programmers are all for unit tests and the like. Debugging OO is a b****.
I recommended writing unit tests because you need to know how to write unit tests before you can do TDD. Write a few tests and see the tests reveal a few problems, and you will understand the wonder of testing.
astions wrote:At this point I want to make sure I have a solid understanding of Models. I've got a feeling that there should be something between the model and the view. Anyone want to tell me the name of that, if there is one?

My goal is to merge my existing style with OO. I'm not sure what will come out but I intend to mix the best of both worlds if possible. I'm not saying that I want to mix procedural with OO, I'm referring to my style of programming, which is a balance of efficiency and accelerated development.
I think maybe a new thread is in order. One about MVC. (ugh! ;))
(#10850)
User avatar
Benjamin
Site Administrator
Posts: 6935
Joined: Sun May 19, 2002 10:24 pm

Re: Organizing Classes

Post by Benjamin »

I know your right. I'll open a new thread in a bit.
alex.barylski
DevNet Evangelist
Posts: 6267
Joined: Tue Dec 21, 2004 5:00 pm
Location: Winnipeg

Re: Organizing Classes

Post by alex.barylski »

One about MVC. (ugh! )
I think 'ugh' in this context was intended to disuade you from starting yet another MVC topic. While I agree it's been beaten to death and will always be subject to personal and professional opinion, it's one of my fav' topics and I love reading and hearing about how others see the triad of components play a role in software architecture.

Start a new topic! :drunk:

The model is the model. The view is the view and the controller is the controller...it doesn't get simpler than that... :P
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Re: Organizing Classes

Post by Christopher »

PCSpectra wrote:I think 'ugh' in this context was intended to disuade you from starting yet another MVC topic.
No intention to dissuade. Just ruing the ...
(#10850)
alex.barylski
DevNet Evangelist
Posts: 6267
Joined: Tue Dec 21, 2004 5:00 pm
Location: Winnipeg

Re: Organizing Classes

Post by alex.barylski »

No intention to dissuade. Just ruing the ...
I see... :lol:
Post Reply