Object Oriented Programming/Classes, Types, and Classic OOP

From Wikibooks, open books for an open world
Jump to navigation Jump to search

Classes, Types, and Classic OOP[edit | edit source]

There are some books on OOP that will tell you that the cornerstones of object oriented programming are encapsulation, inheritance, and polymorphism, or something along those lines. This is fairly typical of the classic OOP view, and there's nothing wrong with it per-se, it's just that we've generalized some ideas and decoupled things here and there since then.

Simple Objects vs Behavioral Entities[edit | edit source]

There are two fundamentally different types of objects, created and designed with different purposes in mind, using different techniques. We label these two types "Simple Objects" and "Behavioral Entities," although these labels are our own. Simple objects tend to represent values, like colors, coordinates, vectors, strings, etc, while behavioral entities tend to represent system components like services, message queues, engines, application logic, etc. ​ Simple objects, if they own anything, are just wrapping what they own (like a system level handle). Usually, however, simple objects are untied to anything else, have no complicated startup or shutdown, and have little overhead. Simple objects tend to demand high performance, guaranteed correctness, discourage incorrect usage, are put in collections, searched, passed around, and generally manipulated by the rest of the program. Writing good simple objects, combined with good collection libraries provided by your language of choice, can easily reduce the complexity of basic operations by as much as 80%. Simple objects tend to focus on the encapsulation side of OOP rather than fancy message passing, as it's usually not a helpful abstraction to think of simple objects sending or receiving messages. ​ Behavioral entities tend to maintain their state in various collections of simple objects, like a server keeping a list of logged in users. Behavioral entities tend to focus on behavior (to state the obvious), and configuring or tailoring that behavior in various circumstances. Message passing mechanisms, including polymorphism, tend to be central to the construction of behavioral entities. As a result, you tend to get whole families of similar entities, for example, an FTP server, an ssh server, an HTTP server, etc. that all have common functionality (like accepting connections). ​ One of the primary forces behind OOP is separating out all of the common elements from these families and writing it once. This has obvious advantages in debugging and maintenance: you only have to fix bugs in common code once. Obviously, this is just abstraction at work but OOP gives you a well understood set of tools to do it with. Some languages features are tailored for one or the other. For example, operator overloading is truly important for simple objects, but mostly irrelevant for behavioral entities.​

Encapsulation[edit | edit source]

Encapsulation is about risk management, reducing your maintenance burden, and limiting your exposure to vulnerabilities —especially those caused by bypassed/forgotten sanity checks or initialization procedures, or various issues that may arise due to the simple fact of the code changing in different ways over time. Technically, encapsulation is hiding internal details behind an opaque barrier so as to force external entities to interact through publicly available access points. ​ Think about it in the context of an OS kernel, like the Linux kernel. In general, you don't want a common user level application modifying any internal kernel data structures directly —you want applications to work through the API (Application Programming Interface). Hence encapsulation is the general term we use for giving varied levels of separation between any core system elements and any common application elements. Otherwise, "unencapsulated code" would be bad for a number of obvious reasons: ​ Applications could easily set invalid or nonsensical values, causing the whole system to crash. Forcing the application to use the API ensures that sanity checks get run on all parameters and all data structures maintain a consistent state. Internal data structures could be updated and change (even drastically so) between seemingly minor kernel updates. Sticking to the API insulates application developers from having to rewrite their code all the time. Applications could be used to "snoop" on each other, elevate their privileges, hog system resources, and violate any number of security protocols if they could directly manipulate kernel data structures. Application developers need little or no understanding of the Linux kernel to access kernel data. The random number engine illustrates this well. It can be accessed through /dev/random instead of wading through kernel structures, and you still get the same data. There are more, surely you can think of some. You may be thinking, "This is supposed to be about object oriented programming. I don't want to be a kernel developer!" Yes, we know, but the software kernel is the perfect example for discussing encapsulation, even though the kernel possesses no object oriented code itself. Encapsulation does not strictly apply to OOP, but it is used most within it. All the kernel's internal data —its message queues, its process lists, its filehandles, etc. —are all encapsulated inside the kernel, and cannot be seen outside the kernel. To work with the kernel, you must use its public interface, the API. For all the same reasons, writing your code in this way pertains the same benefits, even for writing a small app like a text editor or a mail client. ​ Since the entire purpose of encapsulation is to hide details and restrict access, it makes us consider exactly how access is restricted. It also makes us consider what exactly is a detail that needs to be protected, and what exactly should be exposed to the outside world. Figuring out what to expose, and especially at what granularity, is something of an art and we'll talk about it more in the section on interfaces. We'll move on to access restrictions, which come in two basic kinds: compile time and run time. ​ Encapsulation is nothing but a combining of code and data as a single unit. It is achieved using the class concept, e.g.:

 class addition
 {
   int a,b;
   public:
   void read()
   {
     cout<<"\n Enter two numbers";
     cin>>a>>b;
   }
   void print()
   {
     cout<<"\n first number:"<<a;
     cout<<"\n Second number:"<<b;
   }
   int cal()
   {
     return a+b;
   }
 }

In the above example we are combining data (a and b) and the methods (read(), print(), cal()) in to a single unit called by addition. Now we can access those methods by using objects. ​ Compile Time Enforced Access Restrictions Many OOP languages have keywords along the lines of "public", "private" and "protected". These three were used by C++ in particular and have been oft copied since. They are used to group class members into three different levels of access: ​ Public: Part of the public interface, anybody can access this member. Private: An internal detail, only members of this class may access this member. Protected: An internal detail also available to descendants of this class. As you can see, this is not the most flexible framework for controlling access. C++ also has a "friend" keyword to allow certain exceptions to access restrictions, but other problems persist. One common problem results from single classes that grow very large: sometimes you want access restricted to a subset of a classes functions. Alternatively, many generic frameworks like serialization or persistence need access to "private" data members. Unfortunately, there is no way to differentiate between "implementation detail" and "secret information". ​ These kinds of language-level keywords only serve to prevent programs that violate these restrictions from compiling (and most scripting languages are compiled to some intermediate form these days). If somebody used a modified compiler that ignored these access restrictions (or in some languages just used some casting tricks), there is generally nothing to prevent a determined programmer from getting around these barriers. ​ Runtime Enforced Access Restrictions Depending on the chip architecture in use, or possibly on the virtual machine running the byte code, it is sometimes possible to put "protected" information into areas of memory that will generate exceptions if read from, written to, or both. This allows for a program to abort or take other action if encapsulation is violated. ​ Accessors and Virtual (or Calculated) Properties Accessors are just functions that set or return properties of an object. All too often, you'll see this kind of code in Java:

class Dummy {
  private int val;
 
  public int getVal() { return val; }
  public void setVal(int val) { this.val = val; }
}

In this case, getVal and setVal are accessors, or accessor functions. Java promotes lots of gratuitous accessors, and it often looks like unnecessary bloat. Sometimes it is. But often, people miss the point of encapsulation. If Dummy is well thought out, then Val is conceptually a property of Dummy. The fact that in this case that property is kept as a couple of bytes in memory is besides the point for anybody using a "Dummy". Even more importantly, users of Dummies are protected from any changes to how Val is calculated. All too often, the writer of a Dummy class is one himself, and Val has no logical or conceptual significance, and the accessors just get in the way. ​