Ruby allows you to open up any class definition and plug in your methods into the guts of the existing class. John Carter
opens up
Integer
and adds
factorial
..
class Integer
def factorial
return 1 if self <= 1
self * (self-1).factorial
end
end
This allows him to write
puts 10.factorial
While this can lead to designing nice looking expressions of "pleasing symmetry", that read consistently from
left-to-right (5.succ.factorial.succ.odd), it looks scary to me. Not the
openness of the class, but the
global context in which it is open.
Matz, replying to a
thread, on the difference between ruby and python, mentions ..
"open class" is so strong (often too strong), we can break things easily. In other word, Ruby trust you to give you sharp knives, where Python don't. From the Python point of view, it's wrong, I guess.
In a way, I think the implementation of
globally open class in Ruby brings up similar problems as the obliviousness property of aspects. You really never know what has been added to a class and whether the new implementation that you are plugging in, breaks apart any of the methods added by another developer. And it is only because all extensions in Ruby classes take place on the global namespace without any context whatsoever. Check out the discussion between Reginald Braithwaite and Avi Bryant in the comments section of the above Raganwald post.
Scalability problems in large projects ?
Scala has put in better thoughts in designing lexically scoped open classes. They call it "implicits" and allow extension of existing classes through a lexically scoped implicit conversion. Taking the example from Martin Odersky's
blog, in order to add the
append()
method to Scala's built-in
Array
, define a class as ..
class RichArray[T](value: Array[T]) {
def append(other: Array[T]): Array[T] = {
val result = new Array[T](value.length + other.length)
Array.copy(value, 0, result, 0, value.length)
Array.copy(other, 0, result, value.length, other.length)
result
}
}
and add an implicit conversion from plain arrays to rich arrays:
implicit def enrichArray[T](xs: Array[T]) = new RichArray[T]
Now we can apply the
append()
method to plain arrays as well. It's not as organic as Ruby's open classes, but gives you a much better control towards evolution of APIs as your application codebase scales up. Let's see how ..
I love to program in languages that offer extensibility of abstractions - no, not the inheritance way .. public inheritance is possibly the second most tightest coupling between abstractions (guess what the first one is .. correct! .. the friend class in C++ ..). Scala offers extensibility even to
classes written in Java, and with a rich repertoire of features. It almost
obviates the necessity of plugging in your favorite Dependency Injection framework. And implicits offer features to add extensions to existing class structures.
Multidimensionally Open Classes ..The core APIs that any class exposes abstract its prime responsibility, which are invariant across the entire lifecycle of the domain. This is the minimalist view of designing a class (when in doubt, leave it out !). Whether the large Array class of Ruby is the ideal approach of designing an abstraction, has been beaten to death. But definitely it is the result of a mindset that offers open classes - if
you do not put
assoc
in
Array
,
someone else will.
When we consider extensibility of abstractions, typically we want to extend an abstraction in multiple dimensions. A core domain object often needs to be extended with behaviors that help developers design smart APIs. But, more often than not, these behaviors make sense in a specific context of the application and may seem irrelevant or redundant in other contexts. In John Carter's earlier example, extending
Integer
with
factorial
may make sense when you are designing smart APIs for mathematical calculations. But why should an
Integer
class generally be bothered about computing it's own factorial ? This way, adding all possible math calculations in the global class object, can only lead to a big big bloat violating all good principles of OO design. The extension has to be in a specific context of the class and should be invisible to any other context, for which it seems like a noise.
Have a look at how Scala handles this wreck effect through a simple example ..
I have a class
Person
, which is a domain object in the core package ..
package org.dg.biz.core;
class Person(val lastName: String,val firstName: String,val age: Int) {
override def toString(): String = {
lastName + " " + firstName + " " + age
}
}
While working with my UI classes, I would like to have an API
person.toLabel()
which will display a
JLabel
Swing component for rendering a person's details on a frame. And while working on the messaging part of the application, I would like to have a
person.toXML()
, which will help me generate an XML message out of the
Person
object. But obviously I would not like to have any
javax.swing
imports in my messaging component.
I would like to extend the same core abstraction (
Person
), but along two mutually orthogonal dimensions in a share nothing mode. The messaging component should not be able to invoke
person.toLabel()
, even though it is the same core abstraction that it is extending. The extensions have to honor the context in which they are being used. Then only can we ensure proper separation of concerns and the right modularity in designing application component boundaries.
In Ruby, class objects are global variables, and indiscriminate extension methods plugged into existing classes can lead to reduced maintenability and long term reliability of class structures. Not with Scala *implicits* though ..
Within the ui package, I define an enhancer class
RichPerson
, which provides all extensions to my
Person
class *only* for the UI context ..
package org.dg.biz.ui;
import org.dg.biz.core._
import javax.swing._
class RichPerson(person: Person) {
def toLabel(): JLabel = {
new JLabel(person.toString)
}
}
and define a mixin that provides me the implicit conversion ..
package org.dg.biz.ui;
import org.dg.biz.core._
trait UIFramework {
implicit def enrichPerson(person: Person): RichPerson = {
new RichPerson(person)
}
}
When I write my UI components, the extension designed for the UI context kicks in and serves me the UI-only view of the extended core abstraction. I can use the nice
person.xxx()
syntax on the core abstraction itself, the compiler does the magic underneath through invocation of the conversion function ..
package org.dg.biz.ui;
import org.dg.biz.core._
import java.awt.event.WindowAdapter
import javax.swing._
object Main extends UIFramework {
def main(args: Array[String]) = {
val p = new Person("ghosh", "debasish", 35)
val frame = new JFrame()
frame.addWindowListener(new WindowAdapter(){})
frame.getContentPane().add(p.toLabel)
frame.pack()
frame.setVisible(true)
}
}
Trying to use
person.toXML()
will result in a syntax error ! In fact the IDE can only display the available extensions as part of auto-completion features.
Similarly for the messaging component, I define the following extensions in the messaging package ..
package org.dg.biz.msg;
import org.dg.biz.core._
class RichPerson(person: Person) {
def toXML(): scala.xml.Elem = {
<person>
<name>
<lastname>{person.lastName}</lastname>
<firstname>{person.firstName}</firstname>
</name>
<age>{person.age}</age>
</person>
}
}
and the mixin ..
package org.dg.biz.msg;
import org.dg.biz.core._;
trait MessageFramework {
implicit def enrichPerson(person: Person): RichPerson = {
new RichPerson(person)
}
}
and my messaging application code ..
package org.dg.biz.msg;
import org.dg.biz.core._
object Main extends MessageFramework {
def save(person: scala.xml.Elem) = {
scala.xml.XML.saveFull("person.xml", person, "UTF8",
true, null)
}
def main(args: Array[String]) = {
val p = new Person("ghosh", "debasish", 41)
save(p.toXML)
}
}
and with Controlled Visibility ..We extend our
Person
class in different dimensions for two orthogonal concerns and both of the extensions are mutually exclusive of each other. We cannot access the UI extension from within the messaging part of the application and vice versa. The visibility of the *implicit* conversion functions, along with Scala mixin techniques, ensure that the UI component *only* gets access to the
RichPerson
class meant for UI extension. Hence the global namespace does not get polluted and the class structure does not get bloated. Yet we have the power of open classes.