//****************************************************************************//
//************ SOLID Principles (cont.) - November 8th, 2017 ****************//
//**************************************************************************//

- So, we just started looking at the SOLID principles, and finished going over the "S"; let's finish this up

    - OPEN-CLOSED PRINCIPLE
        - Basically, we say that objects "are open for extension but closed for modification"
        - e.g. Here's a method that does NOT follow this principle:

            public double computePay(int employeeType) {
                switch (employeeType) {
                    case SALARIED: this.computeS(); break;
                    case HOURLY: this.computeH(); break
                    case BONUS: this.computeB(); break;
                }
            }

            - Now, EVERY TIME we have to create a new employee type, we have to modify this class and add a new method for calculating that employee's salary
                - "Switch statements are often - although NOT always - a sign that you can be designing something better"
                - In C, since C doesn't have inheritance, alot of C programmers do something like this
        - To do this in a way that follows the open-closed principle, let's make this method work with a generic "Employee" class, and then just have each of the employee types inherit from "Employee"

            computePay(Employee emp) {
                e.calcPay();
            }

        - Similarly, in Robert Martin's book, let's say we have a method called drawShape:

            drawShape(int shapetype) {
                switch() {
                    case CIRCLE: drawCirc();
                    case SQUARE: drawSquare();
                    (...)
                }
            }

            - Again, this means that every time we add a new shape, we ALSO have to modify this class
            - ...and again, we can redesign this by having a "Shape" class all these shapes inherit from, with a "draw()" method on it

    - LISKOV-SUBSTITUTION PRINCIPLE
        - Created by Barbara Liskov, a programming language researcher and Turing-award winner
            - "She didn't win it; she EARNED that award"
        - Canonically, this means that "Subclasses should be substitutable for their base classes"
            - The most common way to tell we're violating this (at least in Java) is if we have to use the instanceof class. Let's say we have a Rectangle class, and we're testing how it calculates area:

                validate(Rect r) {
                    r.setWidth(5);
                    r.setHeight(6);
                    assert(r.area() == 30);
                }

            - This works great! BUT, we want to add a square, so we have it inherit from the Rect class and to make sure it stays a square, we change the getter to:

                setWidth(int w) {
                    width = w;
                    height = w;
                 }

            - NOW, if we put the square through the validate() method, this assert will FAIL, since a square can't have a width of 5 and a height of 6
            - So, to fix our test, we just say "well, let's check to make sure it's a square, then do something different":

                if (r instanceof Square) {
                    assert (r.area() == 30);
                } else {
                    assert(r.area() == 36);
                }

                - "Now, instanceof isn't always bad, but oftentimes it's a sign that we can better design something"
                - In this case, doing this for just 2 different types isn't too bad...but what if we need to add test conditions for trapezoids, parallelograms, rhombuses, etc.? This method will start getting messy - and fragile - FAST!

            - To fix this, we should design our classes so that on INHERITING classes, pre-conditions cannot be strengthened (must be able to take same input as the base class' methods) and the postconditions cannot be weakened (must be able to take on all the values that the base class can)

    - DEPENDENCY INVERSION PRINCIPLE
        - "Depend on abstractions, not concretions"
        - Basically, assume the least/most general requirements you can and use the MOST ABSTRACT class possible; use "List" instead of "ArrayList" if possible to make your methods less fragile
            - "If we decide a month later to use a LinkedList instead of an ArrayList, then if we're following this principle, we only have to change it where we create the list instead of EVERY place where we used the ArrayList"
        - Use Interfaces instead of direct implementations if you can; that way, if you have to change the implementation, it doesn't wreck everything; it's easy to "swap out" the interface implementation

        - An example:
            - "Hey Bob, the people up in accounting want a program that'll take a file from console and print it"
                - Well, using our design techniques we've learned, we figure out we need a Console class, a FileParser, a Copy, and a Printer class
                - So, we write the "Copier" method like:

                    public void copy() {
                        Console console = new console();
                        Printer p = new Printer();
                        while (!c.endOfFile()) {
                            c.write()
                        }
                    }

            - A week later: "Hey Bob, that copy program was great! Could you make one that prints stuff that we have stored on a disk?"
                - So, we need to keep our current method working, so we just create a new class "Disk" and copy-paste a new method:

                    public void copy2() {
                        Disk disk = new disk();
                        (...all copy paste...)
                    }
            - Then a week later, we get a request to print from word documents...then excel spreadsheets...then...
                - "Soon enough, we've got like 6 methods that're bascially copy-pastes of each other! If anything changes in how we want to read in our data, we're gonna have to edit ALL of them! So, instead of doing that, let's just create an interface like this:"

                    IReader() {
                        public void readNext();
                    }

                - "...now, have all of our 'Disk', 'Console', etc. classes implement this interface, and have our copy method just use the 'IReader' interface instead...and then BOOM, we only have to use one method! And adding new things to read from is SUPER easy!"
            - Another thing to look into for this principle: DEPENDENCY INJECTION!
                - We won't get into DI too much in this class, but it's used very frequently in the real world, so it wouldn't hurt to read up on the basics of it

    - INTERFACE SEGREGATION PRINCIPLE (Yes, we cover the 'I' last)
        - Basically, don't make large, multi-purpose interfaces - instead, use several small, focused-purpose ones
        - Don't make clients depend on interface they don't use
        - Classes should depend on each other through the smallest possible interface
            - One example from a student a few years ago: they were making an online "Go" board game, with the classes "ChatClient", "Player", and "Commentators" all depending on the "GoServer" class; instead, we should write 3 interfaces "IChat", "IGame" and "IComment" that the Server implements instead of 1 massive interface (wait, so what depends on what?)
                - ...we'll finish going over this on Friday