# Extra Stuff Here are extra things I want to discuss that do not really fall into specific lectures. ## Naming Methods Elegantly One of the important things to consider about coding in OOP (and in general) is properly naming identifiers, functions, and classes. I'm sure this was talked about in your early programming languages. Creating names is a programming skill in and of itself. Creating concise (unlike Java's verbose naming schemes), descriptive names help in the maintainability of the code you're writing. When the name itself tells you what an identifier is for and when the name of the modifier tells you what it does, then you don't need to explain it in documentation or comments. Since you're probably experts in naming identifiers from you C coding experience, lets focus on naming methods. > **Disclaimer**: These naming conventions are according to MY standards of code elegance. I'm not saying that this particular way of naming is the correct way, to be honest these standards are more subjective than objective. While your in my course I hope you'll at least try incorporate these conventions in your code. I don't care about how you write names in other course, but in my course, these are the names that I consider to be *beautiful*. > > Sometimes, even I forget to adhere to my own conventions, especially when I'm writing code outside OOP. You'll probably notice some contradictions to these conventions somewhere in this course. Just consider these contradictions as proof that the instructor that made these resources is indeed a human being. Let me offer you a preemptive *whoops* for all of those contradictions. Methods (and functions in general) can be divided into two types, methods that return something, known as **builders**, and methods that do not return something, known as **manipulators**. ### Builders An inelegant method name: ```python def add(addend1:int, addend2:int) -> int: return addend1 + addend2 ``` Now this seems like a descriptive enough name that describes what the method does. What is wrong with it? The issues start to surface once you invoke the function: ```python solution = 5 - add(3,2) ``` When you see this line of code, your brain reads this as: "solution becomes five minus add three and two" Which can still be understood since we're used to what the method `add()` usually means. But consider this alternative way of naming this function: ```python def sum(addend1:int, addend2:int) -> int: return addend1 + addend2 ``` When called: ```python solution = 5 - sum(3,2) ``` Your brain reads this as: "solution becomes 5 minus sum of 3 and 2" Which makes way more sense. One of the most egregious violations to this convention is the naming of **getter**. Like the following ```python #inside some class def getName(self): return self.__name ``` ```python print("Hello " + p.getName()) ``` Getter functions are usually created to reveal the values of private attributes. First of all getters, are a bit evil and must be avoided as much as possible since they violate the privacy of private attributes. It's supposed to be a private attribute, other classes are not supposed to access. But sometimes exposing these attributes are needed. Sometimes, you don't need to be able to change the value of the private attribute, you just need to know its value. The best way to name a getter is the following. ```python #inside some class def name(self): return self.__name ``` ```python print("Hello " + p.name()) ``` People might complain, that `name()` and `__name` are ambiguous. But for me this ambiguity is fine since they end up meaning the same thing. Plus, they will never be used interchangeably (maybe in functional paradigm they may) because one is a method and the other is an attribute. So the rule of the thumb that you need to follow when you are naming builders is the following, **A method that returns something (a builder), should be named after a noun that describe what it is returning.** #### Some more builders named elegantly This is a factory method, which is responsible for building new instances of a class called person. Therefore this method is called `newPerson()` ```python #inside some class def newPerson(self) -> Person: return Person() ``` This method returns the element of a given list at position $k$ ```python def kthElement(list:[float],k:int) -> float: return list[k] ``` This is a method that returns the concatenation of two strings ```python def concatenation(u:str, v: str) -> str: return u + v ``` ### Manipulators This is an example of an inelegantly named manipulator: ```python #inside some class def nameString(self): print(self.__name) ``` When your brain reads an invocation of this method it appears out of place: ```python if (value > threshold): nameString() else: print("value is invalid") ``` A better name for this function is the following: ```python #inside some class def printName(self): print(self.__name) ``` ```python if (value > threshold): p.printName() else: print("value is invalid") ``` The original name `nameString()` is inelegant because it is named like a builder, when in fact it doesn't build anything. It returns nothing. This method an example of a **manipulator**. It should be named like a manipulator. This particular method manipulates the output stream of wherever you are printing. **Setter** methods are actually named correctly, setter methods are methods that set the value of a specific private attribute: ```python #inside some class def setName(self, newValue:str): self.__name = newValue ``` Setters are also quite evil in the same way getters are evil. These methods change the values of private methods, violating their privacy. Although the usage of setters is unavoidable in some design patterns, avoid them as much as you can. But at least its name follows this convention for naming manipulators. The rule of thumb for naming manipulators is the following. **A method that doesn't return anything should be a manipulator and must be named from the verb that describes the its manipulation**. #### Some more manipulators named elegantly A method that advances time by 5 seconds: ```python #inside some class def skipForward(self): self.__time = self.__time + 5 ``` A method that removes the first element in the list ```python #inside some class def decapitate(self): self.__list = self.__list[1::] ``` > It's called decapitate because it removes the head of he list. Sometimes, creative and descriptive method names like this help you remember what they do, just because they are accurately named but still memorable ### Predicates ```python def isPositive(n:int) -> bool: return n > 0 ``` Predicates are methods that return either true or false. Although these methods are technically builders, they follow a different naming convention. These methods are named like predicates in a sentence. Example: Checks if a word is capitalized ```python def isCapitalized(word:str) -> bool: return word[0].isUpper() ``` Checks is a list is empty ```python def isEmpty(list:[int]) -> bool: return len(list) == 0 ``` The reason why predicates are named like this is because of how they are used inside if statements. The following is way more readable: ```python if (isPositive(n)): return n else: return -n ``` "If n is positive then ..." Compared to this ```python if(positivity(n)): return n else: return -n ``` "If positivity of n then ..." ### Single Responsibility Principle and correct naming The correct naming of methods helps with one of OOP's design principle, SRP. The name of the function describes the one thing it does. For example the method `sum()` does exactly one thing, it builds the sum of two numbers. The method `decapitate()` does exactly one thing, it removes the head of the list. Whenever you encounter a function that gives you trouble in deciding a name because it is both a manipulator and a builder then it means that the method has more than one responsibility. This means that you should break down this method into smaller methods that does exactly one thing. The following method should be broken down ```python def decapitateAndHead(self): head = self.__list[0] self.__list = self.__list[1::] return head ``` Into these two methods: ```python def decapitate(self): self.__list = self.__list[1::] ``` ```python def head(self) -> int: return self.__list[0] ``` ## The special function `__str__()` You will sometimes encounter the following function in the lab exercises. When you add this function inside your class, it makes the instances of that class *stringable* and *printable*. This means that the class now has a string representation, which means that instances of the class can be easily converted to string and can be printed directly using `print()`. For example, given a person class: ```python class Person: def __init__(self,name:str,age:int): self.name = name self.age = age def __str__(self) -> str: return self.name + " " + str(self.age) ``` By implementing the `__str__()` method your class instances can now be treated like a stringable or printable instances: >`__str__()` should always accept nothing (but the reference to self) as a parameter and return a string. **Printing the instance itself** When a printable type is placed inside the print function and nothing else, it automatically converts it as a string and prints it ```python print(Person("Cheems",29)) ``` ``` Cheems 29 ``` The string above is printed since this is the value that `Persons`'s' `__str__()` method returns. **Conversion to string** A stringable instance can be converted to a string using the builtin `str()` function: ```python print("Hi I'm " + str(Person("Cheems",29)) + " years old") ``` ``` Hi I'm Cheems 29 years old ``` This will also work for formatted strings if you use the sigil `%s`: ``` print("Hi I'm %s years old" % Person("Cheems",29)) ``` ``` Hi I'm Cheems 29 years old ```