Fixing What's Wrong with OOP - Newable/Injectable

137 views
Skip to first unread message

John A. De Goes

unread,
Oct 31, 2009, 12:49:50 PM10/31/09
to Noop

Congratulations on your attempt to fix what's wrong with OOP as we've
come to know it.

Your newable versus injectable wiki page made me want to post my own
thoughts on the issue.

Many, many issues in OOP are caused by mixing of two distinct types of
entities: data entities, whose purpose is to model data; and service
entities, whose purpose is to provide data-oriented services
(typically data sources, data sinks, or data processors, which are
both sources and sinks).

Classic OOP attempts to model both data and services using the "class"
mechanism, which results in entities that acquire the properties of
both data and services; and since the same entity is used, there are
no restrictions regarding how data and services may be used together.

The simplest example of how this leads to problems is java.io.File,
which is a data object representing a node in a file system, which has
acquired properties of a "file system" service. As a result of this
merging of distinct entity types, java.io.File cannot easily be
tested, and the abstraction cannot be easily reused for other file
system types (which is why we have Apache virtual file system and
other projects).

Another example is constructors and methods that accept "service"
classes. This leads to the very dependency & coupling hell that
dependency injection was invented to solve.

Quite simply, OOP got it wrong. A single entity such as "class" should
not be used to model both data and services, and the language should
not allow free form use of data and services.

What's the solution? I think you're onto something with newable versus
injectable, but these are bad names that describe implementation
details, and the current proposal doesn't provide enough control. I'd
suggest that the true concept behind "newable" is data, and the
concept behind "injectable" is service.

Consequently, I think "data" and "service" should be first class
entities in the language, along with "implementation": "data"
represents a data object, "service" represents a service description
(analogous to an interface, but allowing expression of some methods in
terms of other methods, like traits), and "implementation" represents
an implementation of one or more services, which quite possibly
requires other services.

Data Restrictions:

* Data entities may not require any services; they may not hold onto
any references to services.
* Data entities may inherit from other entities. Inheritance for data
objects is the safest kind of inheritance possible. Square really can
inherit from Rectangle without ill effect.
* Data entities may only hold onto references of other data entities.
* Data entities may be created directly with the "new" operator.

data Rectangle {
Int x, y, width, height;
}

Service Restrictions:

* Service implementations must explicitly state the services they
require; they may not hold onto any references to services.
* Service implementations may not inherit from other services.
However, they can be composed from other services through delegation.
* Service implementations may only hold onto references of other data
entities.
* Services implementations may not create other services, they may
only require them.
* Services may return other services that depend on them (which boils
down to: service implementations may return other service
implementations).
* Services may be defined to require data.

service Socket(String server, Int port) {
Byte readByte();

...
}

service Database(String dbname, String username, String password) {
SqlResults query(String query);
...
}

implementation MySqlDatabase provides Database(dbname, username,
password), requires Socket, requires DatabaseConfig {
Socket s = Socket(DatabaseConfig.databaseServer,
DatabaseConfig.databasePort);

public new() {
...
}

SqlResults query(String query) {
...
}
}

These restrictions ensure service dependencies are either explicit, or
injected, both of which permit modularity and testability.

The above syntax is surely flawed and there's a lot more to consider
(for example, the wiring of services to implementations,
implementations providing other implementations, etc), but I think the
idea of separating "class" into different entities with different
purposes and restrictions is a good one, worth pursuing further.

Structural typing & typedefs would enable an interface-like construct
to be defined over data, but which much more flexibility. Aside from
enums, which if they existed would be a kind of data object (see haXe
for a well done implementation of enums), this reduces the number of
entities to five: data, services, implementations, typedefs, and
structural types.

Alex Eagle

unread,
Nov 12, 2009, 11:53:30 AM11/12/09
to Noop
Thanks, John, that's a useful insight. We've been struggling with
giving some real semantic meaning to the newable/injectable thing, and
worried about corner cases where the 'rules' wouldn't be appropriate.

I think the set of constraints you propose are a great start. A couple
refinements:
- Data entities may only be composed of other data entities, so we
have to bootstrap from somewhere. Built-in basic types and collections
would be known data entities. Does that cover all cases, or do we need
to let a user mark their own class as a data entity, regardless of
whether we think it shouldn't be?
- A data entity can still have a method with high cyclomatic
complexity (an EmailMessage with a encodeBase64 method, lets say). A
test might still want to replace it with a MockEmailMessage. If we
allow directly new'ing a data object, there would be no injection
point, and you couldn't mock in a test. Maybe the syntax is the same
as for requesting injection ( EmailMessage m = EmailMessage(); ) but
the compiler can optimize it to a plain new operation.
- Data entities vs services opens the door for things like
serialization and equals/hashCode to be done nicely. I'd like to
serialize data entities straight to Protocol Buffers, and maybe we can
have equals and hashCode implemented correctly based on the properties
of the class (with a way to exclude some)
- Services can take Data Entities as constructor parameters, so we
will need a way to request Assisted Injection (the Guice term for an
element which has some injection points that must be supplied at
creation time, ie. a factory)

Are you confident that all classes can naturally be expressed in one
of the 5 entities you listed? I like the idea a lot, so long as we
don't paint into a corner by over-simplifying the real world.

-Alex

John A. De Goes

unread,
Nov 27, 2009, 1:16:23 PM11/27/09
to Noop
On Nov 12, 9:53 am, Alex Eagle <aeagle22...@gmail.com> wrote:
> - Data entities may only be composed of other data entities, so we
> have to bootstrap from somewhere. Built-in basic types and collections
> would be known data entities. Does that cover all cases, or do we need
> to let a user mark their own class as a data entity, regardless of
> whether we think it shouldn't be?

In the end, everything reduces to primitives.

> - A data entity can still have a method with high cyclomatic
> complexity (an EmailMessage with a encodeBase64 method, lets say). A
> test might still want to replace it with a MockEmailMessage.

The nice thing about a data object is that it's a deterministic
function of the data it contains and the parameters passed to it. In
my experience, this means mocks would be much less important. The only
way to make mocks unnecessary for data objects is to make data objects
mere containers for data, which is in fact what functional languages
do, but which won't be acceptable to OOP developers.

So the question is, how devastating would it be to allow a user to
directly create a data object? In my opinion, not very: since the data
object cannot interact with any services, all of the methods it
provides are going to be data processing-oriented -- algorithms that
are quite testable and stable, which don't have to deal with complex
service interaction semantics (which is where many errors arise in
large-scale systems).

> If we
> allow directly new'ing a data object, there would be no injection
> point, and you couldn't mock in a test. Maybe the syntax is the same
> as for requesting injection ( EmailMessage m = EmailMessage(); ) but
> the compiler can optimize it to a plain new operation.

That would work.

> - Data entities vs services opens the door for things like
> serialization and equals/hashCode to be done nicely.

Indeed, any data object can have an excellent implementation of
equals, hash code, and serialization. All three are a mess right now
in OOP precisely because classes become data/service hybrids.

> I'd like to serialize data entities straight to Protocol Buffers,

I really like that idea. Language-level support for protocol buffers
would be a real treat.

> - Services can take Data Entities as constructor parameters, so we
> will need a way to request Assisted Injection (the Guice term for an
> element which has some injection points that must be supplied at
> creation time, ie. a factory)

It's also possible to deny services the ability to require data
entities as constructor parameters. This just means data will be
pushed into a data service. Both I think have the same effect in the
end, one just achieves greater unity at the expense of forcing
extraction of data dependencies into services.

> Are you confident that all classes can naturally be expressed in one
> of the 5 entities you listed? I like the idea a lot, so long as we
> don't paint into a corner by over-simplifying the real world.

Functional languages have already demonstrated the ability of a data/
service dichotomy to model anything. In Haskell, for example, the
"services" are type classes, the "implementations" are type class
instances, and the "data" are data declarations (or equivalent).
Bringing these concepts into an object-oriented programming language
would be novel, of course, but as long as the concepts are transferred
to equivalently powerful concepts (or close), then you still have
strong assurance for the language's ability to model anything.

Services as described above are injectable, composable, even
dynamically, can possess a default implementation of some methods
(defined in terms of other methods), and could even support generics.
With these benefits, you're achieving something very close to a hybrid
of ML-style modules and Haskell-style type classes.

Still lots of details to work out, of course, but I feel addressing
the flaws with OOP will in fact bring OOP closer to functional
languages, while still retaining benefits of OOP.

Regards,

John

t0rx

unread,
Nov 30, 2009, 3:59:05 PM11/30/09
to Noop

> > I'd like to serialize data entities straight to Protocol Buffers,
>
> I really like that idea. Language-level support for protocol buffers
> would be a real treat.

It feels a bit wrong to make a particular serialisation encoding part
of the language, but then I guess that's no different from native Java
serialisation. How would this relate to the .proto files - just
assume that the indices are in the order of fields so you don't need
one?

Alex Eagle

unread,
Dec 1, 2009, 1:45:51 PM12/1/09
to no...@googlegroups.com
Yeah I'm not sure that the protobuffer support needs to be baked in, or should be an optional library. If we do have some built-in serialization, it may as well be to protos.
I hadn't thought through it much yet. As you suggest, it's important to do the right thing with indices, if we want to maintain the forward/backward compatibility semantics of protobuffers. Maybe someone has a proposal for how this would work?
-Alex
Reply all
Reply to author
Forward
0 new messages