Core Philosophical Differences

OOP Versus Relational: Beyond the Hardware

1/4/2005


This is the third article in a series of articles where I explore root philosophical differences between relational and OOP from different angles.

When comparing OO and relational philosophies with colleagues, I notice that many of the issues raised relate to hardware and specific implementations. For example, they may say that relational databases are too slow to replace RAM-based structures. Although I tend to disagree with the principle of such assessment, for relational does not define implementation and most RDBMS readily cache in RAM, the current base of tools on the market are often not what I would consider the ideal form of table oriented programming. (Note that something does not necessarily have to be perfect to be useful or an improvement.)

For the sake of argument, let's back off from implementation issues. Let's approach this from a higher- level philosophical viewpoint. Let's not worry about what is faster. Let's also not worry about the distinction between files, disk, RAM, etc. Let's assume that one can use RAM, fast bubble-memory, and/or a database or whatever they need to construct the ideal representation of their favorite paradigm. Let's also not get too caught up in static typing versus dynamic typing, whether a DBA must be contacted to add new table columns, whether or not we can use dynamic tables, etc. In the future these may all merge or be a non-factor. Let's just worry about logical structures instead of physical structures, current vendor product limitations, or bad industry habits.

Perhaps hardware issues really do make the difference in the end. Maybe there are fundamental limits or barriers to given philosophies that limit them from a hardware or implementation perspective that can never be worked around with new algorithms or techniques. But, so far I don't see where this must be the case. If in-born limits are later found, I will address them when they come up. Until then, let's journey to a land removed from the shackles of mortal implementation. We will explore two perspectives here: behavior-based interfaces versus declarative interfaces, and relationship structures.

Behavior Versus Declarations

I have concluded that many forms of OO philosophy are "behaviorist" in tilt. (Not every OO proponent will agree with this assessment, but this is just the result of the non-consensus state of OO these days that we must currently live with.) OO wants to put a behavioral wrapper around everything. A "purist" form of OO does not want direct attributes, but rather "accessors" in the form of "setX" and "getX" methods, for example.

From a behaviorist viewpoint, an object or class can be viewed as a spherical capsule where one has to talk to the interfaces on the surface in order to act on or change the core (state). Imagine a globe where each country is a method. In order to see or change anything in the Earth's core, one must talk to the appropriate country (method) to get permission. Which country you ask depends on what you want from the core.

The problem with this analogy is that there is perhaps nothing very interesting in the Earth's core that we know of, other than melted rock and minerals; but assume that aliens found something interesting or important to them down there and we could give them access to it, such as the vast amounts of iron and nickel metals that are thought to be down there.

We would have methods such as extractIron(amount), extractNickel(amount), inquirePriceOfIron(denomination), inquireTotalAvailableIron(), depositeReplacmentFiller(filler), etc. The aliens can't go and just take the metals, they must ask the appropriate method countries.

Relational, on the other hand, tends to encourage "declarative" designs. Note that relational is not the only declarative approach, but the primary one we are considering here. But declarative approaches do share many things in common. Unlike OO, declarative tends to separate facts about something from actions on that something. Declaring facts and performing actions on facts tend to be different mental departments. Further, activity on facts is governed by standard relational rules (at least partially standardized) under relational declarative models. Even though an entire action may not involve only relational operators, at least the first level of access is.

This introduces a consistency to relational that I find lacking in OO. Using our planet analogy, there is no general OO principle governing similar kinds of common actions. One planet may have a method called "extractIron", yet another might call the same action "getIron", and yet another use extractMineral(mineralType=iron). OO has failed to factor similar kinds of state/data access operations into its system as part of a standard. Each entity's operations are or can easily be reinvented from scratch with no commonality between entities being shared or factored to a higher-order concept. Whether or not relational operators are the ideal operations, they are at least far more consistent between entities than OO is. Any commonality that occurs in OO designs is by loose convention only.

This is especially true when it comes to the relationships between objects/entities/things. However, before we get deeper into relationship issues, let's first introduce another example that is more down-to-Earth. Suppose we were defining a pull-down options list of locations for a user interface.

    Location: (example only)

Let's look at the setup of such a thing from an OOP perspective, XML perspective, and a relational perspective.

  OO perspective:

    PDlist = new PullDownList()
    PDlist.multiOptions = False  // user must pick just one
    PDlist.setTitle("Location")
    PDlist.addItem("CA", "California")
    PDlist.addItem("HK", "HongKong")
    PDlist.addItem("NY", "New York")
    PDlist.addItem("TX", "Texas")
    PDlist.show()

  XML Perspective:

    <select name="mylist" multiOptions="no"
         title="Location">
      <option value="CA">California</option>
      <option value="HK">Hong Kong</option>
      <option value="NY">New York</option>
      <option value="TX">Texas</option>
    </select>

  Relational Perspective:

    Table: PullDownLists

      ListID  Title     MultiOptions
      ------  -------   ------------
      locats  Location  False
      ...     ...

    Table: ListItems

      ListRef Value Descript
      ------- ----- --------
      locats  CA    California
      locats  HK    Hong Kong
      locats  NY    New York
      locats  TX    Texas

In the XML version, a declarative approach, the process/tool that renders or "executes" the XML from the file or text is not concerned with how the information got into the file/text. Putting information into XML format is not the browser's or screen painter's concern, as long as it follows the proper format.

Similarly, in the relational version, the process/tool that renders the pull-down list does not have to concern itself with how the data got into the tables.

However, in the OOP version, methods to add, change, find, or delete option items are part of the class/object's interface. Thus, OOP interfaces tend to be larger than declarative interfaces because pure OO encapsulation dictates that an interface has to deal with typical "collection-oriented verbs" such as Add, Change, Delete, Sort, Find, Link, etc. Declarative interfaces typically don't have to worry about those because they either "inherit" them automatically from the database, a collection-oriented sub-system, or are left to other means such as a text editor for the XML example. An HTML or XML browser does not care about how, what, where, or who created the HTML/XML, as long as it fits the specifications needed by the browser. This is called "separation of concerns". It allows different tools or actors to specialize in what they do best.

Thus, declarative interfaces tend to be "cleaner". The process of supplying attributes and relationship information is moved to a different process or concern, leaving interfaces that are more specific to the task or issue at hand. OO philosophers seem so concerned about factoring out common implementation that they forgot about factoring interface similarities also, namely common collection-oriented idioms that are in just about every piece of software regardless of domain (industry). They have not applied their own mantra higher up on the "abstraction chain".

It is still up in the air whether it is practical to simply inherit the "collection-oriented verbs" from a shared collection class. I have yet to see a practical implementation. Further, implementation and interface convenience may greatly depend on the OOP language being used. For example, if a language does not support multiple inheritance, then "implementing" collection oriented methods by inheritance may preclude inheriting other more domain-specific implementations. The use of multiple inheritance is a controversial topic I would note.

Relationships

Managing relationships is one of the toughest tasks in software design. It should be given the highest of priorities. One cannot escape relationships; they are a part of life as a software developer. It is sometimes claimed that OO encapsulation can "wrap" or "hide" relationship information; but as long as there are two or more classes that must relate to one another, one has to deal with the relationship.

Take the example of a business invoice. Each invoice contains potentially multiple "line items". Line items are basically specific lines of a printed invoice that describe the products, services, taxes, discounts, fees, etc. for the given invoice. OO proponents will sometimes suggest that these line items can be "encapsulated" inside the invoice object. However, the line items are probably defined in a class of their own, separate from the Invoice object/class. The developer must connect and/or manage the link between the Invoice object and the Line-Item object. This link is not "encapsulated" from the original developer's perspective.

There might be line item interface methods in an Invoice class, such as "addLineItem", but these are simply extras. The developer still has to deal with two objects.

Further, the line items probably somehow reference product objects/classes, since different invoices may reference the same product (catalog entry). Thus, we have at least 3 kinds of objects involved in an invoice: An Invoice object, a Line-Item object, and a Product object.

In addition, product information is likely used by many other objects. Sales and inventory sub-systems likely also use product information, for example. OOP encapsulation cannot "hide away" all these relationships. At most, the invoice line items can be contained "inside" the Invoice object to some extent, but not the Product object because it must be shared with other parts of the system (assuming we don't duplicate information, which is generally frowned upon). The developer is pretty much forced to deal with multiple objects and their relationships. Encapsulation cannot hide every relationship behind a simple interface. It is a fact of life.

Even line items may not be used by just the Invoice object. Inventory management and marketing may also want information about products sold. They may not always care about which invoice a line item was with and don't want to deal with them on an invoice level. See also the containment note.
Although there are many approaches to managing relationships, there tends to be a pattern to how relationships are typically managed in most designs and frameworks. A common theme is the "record". It may also go by the name "dictionary", "associative array", "hash array", "node", and "map". I will use the term "map" here. It is basically a list of attributes (keys) and their values. As described in a prior article, objects can also be thought of or modeled as maps (called a "record" in that article).

Even hierarchical structures can be modeled with maps. For example, in file systems the list of files in a "folder" or "directory" is a map where the key is the file name and the value is the content (bytes) of the files. The reference to the parent folder can be thought of as a special key/value pair within the map. Whether that reference is a disk sector address, RAM address, or a unique folder ID number is not relevant here in our implementation-neutral viewpoint.

Similarly, tables can be built out of maps. In a simplified form, one map holds the primary key to each row in its "key" portion, and its "value" portion holds the pointers to the row maps. Each row is a map in itself.

Thus, the vast majority of structures in our software and computers use or can be modeled (emulated) using maps. Even the hardware can be modeled using maps. For example, RAM can be thought of as a map in which the memory address is the key and the byte (or segment) at that spot is the value. Maps make a pretty good general-purpose structure. Or, at least a multi-purpose structure. I have yet to encounter a structuring technique that cannot be emulated with maps. It may take a lot of linking between maps in some cases, but it can usually be done with enough maps and enough links.

As flexible as they are, in my observation maps are not powerful enough to manage higher levels of complexity, at least not by themselves. They are fine for small or simple stuff, but as the relationships between things grow more complex and involved, they are an insufficient tool from a human perspective. They can technically do the job with enough maps with references/pointers to other maps, but it is hard to grok (mentally manage) such complex bowls of map pasta. The map network grows beyond the ability of most mortals to comprehend, navigate, and manage. We need a yet higher-level or larger-sized structure beyond just a map.

Let's look at some attempts that have been taken to provide a higher-level structure or structures. One of the earlier attempts involved trees (hierarchies). IBM's IMS database system created in the mid 1960's is one such early, and relatively successful attempt at using hierarchical relationships. File systems are probably the most common tree systems in use today. OOP inheritance is also tree- based for the most part.

However, trees have some serious drawbacks that can drag a system down more and more as the complexity grows. Their biggest problem is handling multiple orthogonal traits/factors/dimensions. The first reaction was often to add a new level (tree depth) for each new factor. However, this approach gets unpleasant beyond 3 or so factors. Most large-scale tree systems end up putting cross-links across branches or between multiple trees in order to help mitigate some the limitations of trees. For example, Unix file systems usually have a "link" feature in which a cross-link acts more or less like a virtual file or folder. Even IMS eventually added cross- linking ability, and is thus no longer "purely" hierarchical. Most large-scale tree-based systems eventually end up adding cross-links of some kind. Pure trees just don't cut it.

Dedicated structures are another approach. Dedicated structures include stacks, queues, linked lists, ordered lists, etc. Trees and maps are sometimes included in these, by the way. However, dedicated structures lack "chameleon" ability. They do what they are meant to do well, but cannot easily bend to handle unanticipated uses or viewpoints. One has to either keep converting from one dedicated structure to another, or copy the same information around over and over. Copying is risky because one has to track or remember where to propagate any updates that are made to information.

Webs (networks) of maps have generally proven more flexible at a larger scale than dedicated structures. Maps make it easier to point to (reference) shared information so that copying is not needed as often. If two different sections of code or two different objects need to reference the same map, they simply both point to the now-shared node. If you want a link from A to B, you simply create one. But dedicated structures usually have rules that forbid or limit just any link from being added. Generally, something no longer fits a given dedicated structure definition if such links are inserted. A stack which lets you see into the middle without popping all those above the target item is no longer a "pure" stack. For small, temporary, local uses, dedicated structures might be satisfactory, but they don't scale in complexity as well as the already-mentioned alternatives.

Another larger-scale-structure movement is "design patterns", which is a movement given wind by the famous "Gang of Four" OOP design book. Design patterns can be characterized pretty much as an attempt to catalog map web arrangements. (Each map is usually a class or object.) Some will say that they are more than that because behavior is involved, not just data; but dispatching behavior from a map is mostly just an implementation detail. Whether a map "cell" contains data, a method, a function, a pointer (to another map), or a code snippet of some kind is just specifics of implementation. Just add some syntax to more easily execute the cell content instead of treat it as raw data, and you have the equivalent of OO methods. (Note that OO did not invent this technique. Lambda calculus and Lisp came earlier.)

Design patterns are a good try, but they suffer some of the same problems that dedicated structures (above) do. They don't morph or bend very well to other uses or viewpoints, at least not without creating fragile dependencies between multiple patterns. A given map may have to participate in multiple patterns. One ends up "throwing pointers" at the problem of handling new requirements such that it becomes hard to tell where one pattern starts and the other ends.

Part of the reasons for their fragility is that they are usually hard-wired into programming code. If they were treated more like data structures, then we could query such structures to study them and provide alternative viewpoints for particular needs. Again though, this is merely an implementation detail, which we are trying to ignore in this article. Because they are usually found in code instead of data structures is not a reason to complain about them here. So, let's assume that we can view, see, and manage them as code or as data structures as we wish. Maps are maps, whether represented as code or as data structures. Some of you like them as code, some like them as data structures, others as Tinker Toys reconstituted on the living room floor. The logical structure is still the same.

So, if we ignore specific representations that bother us, then what is wrong with them? Well, I still find them hard to manage, especially when all the interrelationships and complexities of the real world are tacked on. As was stated above, a given map may participate in multiple patterns. Identifying a pattern could be like finding a needle inside overlapping haystacks. It kind of reminds me of those optical illusions where you can see different animals depending on where your eye chooses to focus. A horses' rump may also be part of a crab's claw. A pattern is not really a concrete thing, but rather a given viewpoint.

This is where my relational bigotry comes in. Relational philosophy provides discipline and consistency lacking in the Sea of Maps approach. I often compare navigational structures (map webs) to shanty towns. If you need a new reference to something, you simply tack on yet another pointer. If an object needs to reference another object, it simply references it.

In a shanty town if you want to add electricity to your shack, you simply make friends with your neighbor and splice into his/her main wire for the cost of a chicken or two per month. Most of the time it will work. However, if everybody does this, it soon creates brown-outs, short circuits, fires, etc. This is sometimes known as the "tragedy of the commons". There should to be rules and regulations for how and when new connections are added, maintained, shared, and monitored.

Relational introduces at least three things that provide structural discipline. First is manipulation of information only via "relational algebra", which is basically a set of operations that act using tables (real or virtual) and expressions that follow certain rules. (Note that the current crop of RDBMS and relational languages don't necessarily follow all of the rules.)

The second structural discipline feature is the concept of "entity". Relational records (which are also maps) must belong to an entity. OO may have concepts like inheritance which in a rough way can provide a similar "father structure" for maps, but it is merely an optional tool rather than central and mandatory for the framework of the paradigm. Plus, concepts such as multiple inheritance can further complicate the picture.

The third important relational rule is unique identity. Each row in a table must have a way to uniquely identify it. And, this "key" generally must have an externally- representable form. In other words, it has a textual representation that humans can read.

Databases can also provide a lot of functionality out-of- the-box, including multi-user contention (transaction) management, query ability, referential integrity, persistence, concurrent backups, etc. By following the rules, you either automatically get these features, or can easily add them by upgrading to a bigger RDBMS with little or no code rework.

Not only does it introduce consistency into software design, but following these rules allows the mind to study patterns and formulate ideas about attributes and relationships that might not otherwise be apparent. Relational allows one to do abstract "math" of sorts on information. For example, it can help the machine find the best physical query path or strategy without programmer intervention. It helps separate "what" from "how". It is hard to do math on things without rules.

In addition to shanty towns, I like to compare navigational structures (webs of maps) to the "go to" of pre-structured programming. Goto's are the behavioral equivalent of data pointers in many ways. Structural programming (blocks) introduced discipline and consistency to program flow. Two different developers working independently on the same problem are more likely to come up with a similar flow solution using blocks than using goto's. There are thus less surprises when working with someone else's code. Another benefit of blocks is that indentation provides visual cues to program flow. I think relational provides similar benefits in that it is easier to query information in order to provide helpful perspectives on information for changing needs.

Early in my career I was required to deal extensively with goto's because upgrading the programming language in my shop would have required other expenditures that the owners were not willing to make. To keep my sanity I started forming an informal goto "pattern" catalog in my head. In many ways this practice resembled the works of the GOF OOP pattern movement. But, just like the goto patterns, it is an attempt to document the mess rather than rise above the mess.

One can perhaps argue that relational technique as it stands at the present time is not fully up to the job. Perhaps DBA's have too much control over the process, current RDBMS are too bulky or static, SQL itself is a sloppy language, and so forth. I agree that there are flaws with many existing relational products and industry habits, but that in a good many cases their net strength still outweighs these implementation flaws.

However, getting away from implementation, industry habits, and hardware; from a logical point of view, relational appears to be the better paradigm. At the very least it introduces discipline and consistency to attribute and link management that OO still lacks. I invite you to evaluate it from a purely logical perspective, ignoring the limitations of your current equipment, software, operating system, file system, etc.

If we can all agree on the theory, then maybe there will be more effort behind fixing and improving the implementations.


© Copyright 2005 by Findy Services and B. Jacobs