Caveat: I am not a graduate of a 4-year computer course, so this post is just a summary of my understanding of OOP in general and not a definitive guide or an expert opinion.
Object-oriented programming paradigm is essentially the division of a system into modular objects (run-time entities) responsible for their own data and behaviour, with such objects being a combination of structured programming and abstract data types. It encourages hiding of data which is only accessible through methods bundled with it. OOP languages are said to have the following minimum fundamental feature set:
- Encapsulation, the bundling together of data structure and method collection that operate on these data structure into a single component called an object (similar to abstract data type or module but with inheritable and polymorphic properties and support dynamic dispatch) possessing state (data) and behaviour (methods) and used to abstract real world entities.
- Inheritance, basing of an object/class from another object/class (a) to re-use the same implementation as that of the base class or prototype object, or (b) to specify that the same methods and behaviours is maintained in the derived class.
- Polymorphism, the provision of a single interface to entities of different types.
- Open recursion, the invocation of a method inside another method of the same object using a special keyword-like variable (this, self or Me), The invoking method may be defined in the superclass and the invoked method in the subclass thru overriding.
This is an example of a combined abstraction of data and code. Some classes though are just wrappers of methods, that’s why there are rants about this in here or here. There are two ways an object can be created:
- Class-based, where an object is created out of a class which serves as its template or blueprint. This includes Simula67, Smalltalk, Eiffel, C++, Java, Objective-C, C# and PHP. Click here for a list of some class-based languages.
Information hiding is a related but independent notion to encapsulation; that of restricting access to some of the object components, such that only thru an interface are object data or methods exposed to its clients. Information hiding is not a part of OOP features. The purposes of information hiding in objects are:
- To act as a barrier to prevent unauthorized direct inspection or manipulation of the object’s internal state that could result in them becoming invalid, inconsistent or corrupted; and vice versa, to prevent internal change affecting or propagating to the other parts of the program outside of the object, like collision of identically named variables.
- To limit inter-dependencies between software components and limit extensive modification if there’s a design change in the code, and group together related components for easier understanding and maintainability. Changes are local rather than global.
- To reduce development risk by deciding which components will be stable (the interface) and which parts are allowed to change and improve (the implementation). By having a stable interface, the clients can rely on the interface that it will not change much like a contract.
With regards to information hiding in C#, although properties look like member fields by not having “( )”, they are in fact methods, access methods to be precise, which may get or set hidden internal data fields: “a property is a pair of getter and setter access methods that provide access to a field or calculate the needed values.”. Not all internal fields have to have a corresponding getter/setter access method.
Composition or the combination of simpler component objects without independent existence into more complex composite objects resulting in a “has-a relation, is also one manifestation of encapsulation. Here, the composite object owns the component, being responsible for their creation and destruction. The component object gets destroyed together with its composite object and can never be transferred to another composite object, it may only be part of one composite. Multiple has-a relations combine to form a possessive hierarchy. One way of implementing this in C# is thru interfaces. Advantages of composition over inheritance include: (a) more stable business domain objects and (b) higher design flexibility since it can accommodate future requirement changes that would otherwise require complete restructuring of the class hierarchy or inheritance model. One disadvantage is that all of the methods being provided by the composed class must be implemented in the composite class, unlike inheritance where only classes with different behaviour from base class need to override the base methods, but this is avoided if the language supports mixins or traits.
- Interface Inheritance, or specifying that the names of the methods and attributes in the base interface is found in the derived interface.
- Subtyping, or specification inheritance, where all the methods (implementations) and attributes of a base class are found in the derived class resulting from is-a relationship. This is found only in statically-typed class-based OO languages, such as Java, C#, C++ and Scala.
- Implementation Inheritance establishes syntactic relationship and not necessarily semantic relationship, especially those that allow mutable objects.
Types of inheritance by the number of base classes or objects its inheriting from:
- Single inheritance, where a class/object may inherit only from a single other class/object but protocols in Objective-C and Swift, interfaces in Java and C#, traits in Scala, modules in Ruby) provide a functionality to have multiple inheritance. Languages that use this and how they resolve diamond problems are: C# , Objective-C, Smalltalk, and Swift. (none, since base implementations are either overridden or hidden), Java (in Java 8 must re-implement the method or result in compile error), Ruby (method redefinition obscures any previously existing definition at the time of execution) and Scala (right-first depth-first search of extended ‘traits’, then retain only the last occurrence of each module in the resulting list).
- Multiple Inheritance, where a class/object may inherit only from multiple classes/objects. Additionally, Eiffel has repeated inheritance from one superclass apart from multiple inheritance. Languages that uses this and how they resolve ambiguity like the diamond problems are C++ (virtual inheritance or explicitly by properly qualifying from which it is inheriting), Common Lisp Object System (method that matched the most specific argument, order in the declaration of containing class, overridden thru method combination or reflection thru metaobject protocol), Curl (secondary constructors on shared classes), Dylan (MRO/C3 linearization), Eiffel (automatic joining if same name and implementation; explicitly thru select and rename directives), Logtalk (masked out by default, renaming thru method alias), OCaml (precedence by the last in the class inheritance list, override by qualifying with class definition), Perl (left-first depth first searching on an inheritance ordered list, or overridden with MRO/C3 linearization or other algorithms), Perl 6 (as as Perl), Python (MRO/C3 linearization on an inheritance ordered list), and Tcl (MRO/C3 linearization).
- Is there a language where
- the default way to handle the diamond problem has this precedence: (1) automatic joining if same name and implementation (Eiffel), (2) method that matched the most specific argument (CLOS) and (3) right-first depth-first search of methods, then retain only the last occurrence of each module in the resulting list (Scala), then
- can be overridden by any of these: (1) explicit qualification of ancestor class (Eiffel), (2) reflection thru metaobject protocol (CLOS), (3) method combination (CLOS), with
- the ability to rename methods thru alias (Logtalk)?
Types of inheritance according to manner of inheritance:
- Classical Inheritance or class inheritance, where attributes/variables and implementation/methods (and contracts in Eiffel) are automatically inherited from one or more base classes, parent classes or superclasses and retained by derived classes, child classes or subclasses creating a class hierarchy. Used mainly by class-based languages but can be simulated by some prototype-based OO languages. Derived classes may modify or supplement the inherited functionality thru overriding (marked as virtual, abstract or override in the base and override in derived C#) or hiding (marked in C# as new in the derived member) but must have the same method signature and visibility. Some classes or methods may be marked as uninheritable thru class modifiers (final in Java, sealed in C#, frozen in Eiffel). Private methods can’t be overridden due to its inaccessibility or invisibility outside of the class it is a member method. Static methods can be overridden as well.
Common constraints in inheritance using classes are:
- Fixed inheritance hierarchy and data type at instantiation and cannot change in time, locking the developer within their original designs. (Can be partially solved with the decorator pattern.) “Role based design should be used when it is conceivable that the same object participates in different roles at different times, and inheritance based design should be used when the common aspects of multiple classes are factored as superclasses, and do not change with time.”
- Client code have access to the base class data, either because the base class is public or by casting. This can be mitigated by making the base class members protected in most of these languages (C++, Java, C#, etc.)
- The yoyo problem, where the programmer has to keep flipping between many different class definitions to follow the control flow of the program with a long and complicated inheritance graph. To avoid this problem, keep the inheritance as shallow as possible.
- Ad hoc polymorphism, where several functions with an identical function name accept a number of parameters of different but limited types in distinct combinations. This allows the functions to provide potentially completely heterogeneous implementations depending on distinctness of names, order and types of parameters supplied and may involve type promotion, or type coercion. Advantages are (a) less verbose function names and reduced code length, (b) uniform interface or notation across different objects, (c) specialization of each function to only 1 set of distinct parameter combination, (d) increased program efficiency, and (e) enforce mandatory data members if constructors. Two examples:
- function overloading, creating several functions that happen to have the same function name differing by method signature: ( (a) return type, and the input parameter’s (b) arity, (c) data types and (d) order), and is resolved at compile-time using “best match technique” in statically-typed OO languages. Class constructors may also be overloaded. Coder comprehension might be affected if parameter types involve type coercion or matches the base “object”. In C++, there is no overloading across scope, such that a function in outer scope must be imported with the “using” keyword. This is different from subtype polymorphism.
- operator overloading, creating different implementations of operators depending on their arguments so as to resemble some notation in the domain the programmer is working on.
- Parametric polymorphism or generics, where a functions (or data type) don’t depend on the type of the parameters it accepts (the code does not mention any specific type), and so is implemented generically to handle the identical data type. ML first introduced this in 1975 and has other forms of parametric polymorphism.
- Subtype polymorphism or subsumption polymorphism or inclusion polymorphism or subtyping, where a method written to operate on elements of one type can work on elements of another type thru the notion of substitutability (either because (a) one is derived from the other thru inheritance from supertype to subtype [nominal subtyping] or (b) both types define all the same methods thru duck typing [structural subtyping]) with the correct method determined at runtime thru virtual functions. For this reason, this may also be called interface inheritance. Read more about covariance and contravariance as this relates to subtyping.
- Bounded polymorphism, or the interaction between parametric polymorphism and subtype polymorphism, or parametric polymorphism within the bounds of a range of subtypes of a particular type.
Only contents of comments showing that what I’ve written is incorrect will be retained. All other comments will be deleted or edited.