Thread overview
Alternatives to OOP in D
6 days ago
Brother Bill
6 days ago
Serg Gini
6 days ago
Kapendev
6 days ago
monkyyy
6 days ago
H. S. Teoh
6 days ago
Andy Valencia
6 days ago
H. S. Teoh
6 days ago

I have heard that there are better or at least alternative ways to have encapsulation, polymorphism and inheritance outside of OOP.

With OOP in D, we have full support for Single Inheritance, including for Design by Contract (excluding 'old'). D also supports multiple Interfaces.

What would be the alternatives and why would they be better?
I assume the alternatives would have

  1. Better performance
  2. Simpler syntax
  3. Easier to read, write and maintain

If possible, please provide links to documentation or examples.

6 days ago

On Monday, 1 September 2025 at 13:58:23 UTC, Brother Bill wrote:

>

I have heard that there are better or at least alternative ways to have encapsulation, polymorphism and inheritance outside of OOP.

With OOP in D, we have full support for Single Inheritance, including for Design by Contract (excluding 'old'). D also supports multiple Interfaces.

What would be the alternatives and why would they be better?
I assume the alternatives would have

  1. Better performance
  2. Simpler syntax
  3. Easier to read, write and maintain

If possible, please provide links to documentation or examples.

Usually alternative is related to composition.

https://en.wikipedia.org/wiki/Object_composition
https://en.wikipedia.org/wiki/Composition_over_inheritance

6 days ago

On Monday, 1 September 2025 at 13:58:23 UTC, Brother Bill wrote:

>

I have heard that there are better or at least alternative ways to have encapsulation, polymorphism and inheritance outside of OOP.

With OOP in D, we have full support for Single Inheritance, including for Design by Contract (excluding 'old'). D also supports multiple Interfaces.

What would be the alternatives and why would they be better?
I assume the alternatives would have

  1. Better performance
  2. Simpler syntax
  3. Easier to read, write and maintain

If possible, please provide links to documentation or examples.

I sometimes use single-level inheritance with structs when making games:

import std.stdio;

struct Base {
    int hp; // Everyone will have this.

    void draw() { writeln("Debug stuff."); }
}

// Will use `Base.draw` by default.
struct Door {
    Base base;
    alias base this;
}

// Will use the custom draw function.
struct Player {
    Base base;
    alias base this;
    int[] items;

    void draw() { writeln("Player stuff."); }
}

void foo(Base* base) => base.draw();

void main() {
    Door door;
    Player player;;

    player.draw();
    foo(cast(Base*) &player);
    door.draw();
    foo(cast(Base*) &door);
}

It's nice because you can have one (sometimes static) array of game entities. Needs the first member to be Base of course. Alias This

6 days ago

On Monday, 1 September 2025 at 13:58:23 UTC, Brother Bill wrote:

>

I have heard that there are better or at least alternative ways to have encapsulation, polymorphism and inheritance outside of OOP.

With OOP in D, we have full support for Single Inheritance, including for Design by Contract (excluding 'old'). D also supports multiple Interfaces.

What would be the alternatives and why would they be better?
I assume the alternatives would have

  1. Better performance
  2. Simpler syntax
  3. Easier to read, write and maintain

If possible, please provide links to documentation or examples.

>

claims to not start flame wars

Its the wrong question you should answer 3 questions:

  1. where is the data
  2. how does data mutate
  3. how do I control flow

oo is bad because its answers all three questions as a noun; "Im just going to vistor pattern my gamestate manager with my mailboxes"

to start with Id suggest 2 different styles:

  1. going full ranges(standard)

This is a mostly functional style, theres nothing enforcing you to be pure, but first class functions is just how .map works. "Ranges are views of data", so the data stays where it started, if you start with a File(...).byLineCopy, the data is in the file, same with a json parser that provides a range interface. You mutate data with maps and reduces, and you manage control flow with take, drop and sort

  1. plain old c

You have global scope, malloc and goto. You can at any time make all your data in global scope write a giant function; if you must allocate, call malloc, you just go edit data directly and you can do any control flow with goto as its the only thing hardward can do.

The answers the the 3 questions are mix and matchable.


Polymorphoism isn't remotely owned by oo, templates are more polymorphic, usually "upgrades" to templates like "generics" are about preventing their full polymorphic; contracts, generics, all this talk about function attributes, all of it is trading off the flexibility of templates to chase some safety.

6 days ago
On Mon, Sep 01, 2025 at 01:58:23PM +0000, Brother Bill via Digitalmars-d-learn wrote:
> I have heard that there are better or at least alternative ways to have encapsulation, polymorphism and inheritance outside of OOP.
> 
> With OOP in D, we have full support for Single Inheritance, including for Design by Contract (excluding 'old').  D also supports multiple Interfaces.
> 
> What would be the alternatives and why would they be better?
> I assume the alternatives would have
> 1. Better performance
> 2. Simpler syntax
> 3. Easier to read, write and maintain
> 
> If possible, please provide links to documentation or examples.

OOP is useful for certain classes (ha) of problems, but not all, as the OOP proponents would like you to believe.  Some classes of problems are better solved with other means.

OOP is useful when you need runtime polymorphism, as in, when the concrete type of your objects cannot be determined until runtime. This flexibility comes with a cost, however.  It adds an extra layer of indirection to your data, which means data access involves an extra pointer access, which in performance-critical loops may cause cache misses and performance degradation.  Also, class objects in D are by-reference types; in some situations this is not ideal.  If you have a large number of small class objects, they can add a significant amount of GC pressure, also not ideal if you're dealing with lots of them inside a tight loop.

My preferred alternative is to use structs and DbI (design by introspection).  Rather than deal with monolithic class hierarchies (that often fail to capture all the true complexities of polymorphic data anyway -- real data is rarely easily decomposed into neat textbook class hierarchies), I have a bunch of data-bearing structs with diverse attributes.  The functions that operate on them are template functions that use compile-time introspection to determine exactly what the concrete type is capable of, and select the most appropriate implementation based on that.  This way, rather than trying to shoehorn the data into a predetermined class hierarchy, the code instead adapts itself to whatever form the data has that it receives.  This allows great malleability in fast prototyping and extending existing code to deal with new forms of data, with the advantage that (almost) all concrete types are resolved at compile-time rather than runtime, so the resulting code is fully optimized to work on the specific forms of data actually encountered, rather than pay the overhead tax on potential forms of data that it does not know until runtime.

In OOP, data and code are tightly coupled, which works well when you only ever need to perform a fixed set of operations on your data. But things get messy once you have diverse operations, or an open set of operations, that you need to perform on your data.  You start running counter to the grain of OOP access restrictions, and end up wasting more time fighting with OOP than OOP helping you to solve your programming problem.  DbI lets you separate data from the algorithms that operate on them, so that your data types can focus on the most effective data storage scheme, while your operations use compile-time introspection to discover what form your data, adapting itself accordingly to work on the data in the most effective way.

(If you're interested in more details of DbI, search online for "design by introspection" and you should see the relevant material.)

Here's an example of the contrast between OOP and DbI, from one of my own projects.  The basic problem is that I have a bunch of data, residing in various objects of diverse types, representing program state, that I want to serialize and save to disk, to be restored at a later time.

In the traditional OOP approach, you'd create an interface, say Serializable, that might look something like this:

	interface Serializable {
		void serialize(Storage store);
	}

Then you'd add this interface to every class you want to serialize, and implement the .serialize method to write the class data to the Storage. If you have n classes, this means writing n different functions.

In my actual project, however, I chose to use DbI instead.  In DbI, you *don't* impose any additional structure on the incoming data.  In contrast to the OOP philosophy of data-hiding, DbI is usually most effective when your types have public access data.  Instead of writing n different functions for n different types (and I have a *lot* of different types -- one for every kind of data in the program that I want to serialize), I just have *one* template function that handles it all.

Here's what it looks like:

	void serialize(T)(Storage store, T data) {
		static if (is(T == int)) {
			store.write(data.to!string);
		} else if (is(T == string)) {
			store.write(data);
		} else if (...) {
			...	// handle other basic data types here
		} else if (is(T == S[], S)) {
			// we have an array, loop over the elements and
			// serialize each of them
			foreach (e; data) {
				// N.B.: recursive call, to a
				// *different* instantiation of
				// serialize() adapted to the specific
				// type of the array element
				serialize(store, e);
			}
		} else if (is(T == struct)) {
			// This is a struct; loop over its fields and
			// invoke the appropriate overload of .serialize
			// to handle each specific field type
			foreach (field; FieldNameTuple!T) {
				serialize(store, __traits(getMember, data, field));
			}
		} else {
			static assert(0, "Unsupported type: " ~ T.stringof);
		}
	}

Notice that .serialize does NOT have any code that deals with specific user-defined types.  Rather, it detects built-in types and aggregates, and uses compile-time introspection to adapt itself to each type of data it encounters, leveraging the combinatorial nature of these basic type building blocks to handle anything that the caller might throw at it. Instead of writing n different .serialize functions, one for each user-defined type, like you'd do in OOP, here you have a *single* function that handles everything.  There are only as many static if cases as there are basic types you'd like to handle (and you don't even need to include all of them -- only those you actually use). With just a small, bounded set of static if blocks, you can handle *any* number of types.  And you can throw new user-defined data types at it and have it Just Work(tm) without adding a single new line of code to .serialize(!).

You can also incrementally build up your .serialize function: that last static assert is there deliberately so that if you ever hand it something it doesn't know what to do with, it will forcefully stop compilation loudly and tell you exactly what's the missing type that it hasn't learned to handle yet.  Then you just add another appropriate static if block to the function, and it will now learn to handle that new type *everywhere it might occur*, including deep inside some nested aggregate type that it has never seen before.

IOW, once your .serialize function is able to handle all the basic types you might have in your user-defined types, it will be able to handle any kind of new data type.  All without needing to add a single new line of code.  Compare this with the OOP case where every new class you add will require to inherit from the Serialiable interface, and then you'd have to implement the .serialize function.  And hope that you didn't make a mistake and leave out an important data field.  Whereas our DbI .serialize function automatically discovers all your data fields and serializes them using the appropriate overloads -- without human effort, and therefore without room for human error.

//

Now, you might ask, what if I want to serialize OOP objects?  Since the whole point of OOP is that you *don't* know the concrete type of your data until runtime, how can .serialize know what the concrete data fields of the object are, in order to serialize them?  Since they are not known at compile-time, does that mean our class objects are left out in the cold while the structs enjoy the power of our DbI .serialize function?

Nope!  Here's how, with a little scaffolding, we can teach our clever DbI .serialize function to dance with OOP class objects too:

First, we create a CRTP template class that will serve as our DbI analog of class interfaces:

	class Serializable(Derived, Base = Object) : Base {
		static if (!is(Base : Serializable!(Base, C), C)) {
			// This is the top of our class hierarchy,
			// declare the .serialize method.
			void serialize(Storage store) {
				serializeImpl(store);
			}
		} else {
			// This is a derived class in our hierarchy;
			// override the base class method
			override void serialize(Storage store) {
				serializeImpl(store);
			}
		}

		private void serialize(Storage store) {
			// Record concrete type, since it's not
			// predictable at compile-time
			store.saveClassName(Derived.stringof);

			// Downcast to concrete subclass and save it
			serializeClassFields(store, cast(Derived) this);
		}
	}

The saveClassFields function is similar to our original DbI .serialize function, except that it takes care to iterate over base class members as well, so that when serializing a derived class we don't miss base class members that will be required to deserialize the object later. Other than this handling, it just forwards the bulk of the work to the DbI .serialize function that uses compile-time introspection to discover and serialize these members.

All that remains, then, is to add this static if block to our DbI .serialize function:

	void serialize(T)(Storage store, T data) {
		...
		else static if (is(T == class)) {
			serializeClassFields(store, data);
		}
	}

and, for every class that we want to serialize, declare them this way:

	class MyClass : Serializable!(MyBaseClass, MyClass) {
		...
	}

This slightly unusual way of declaring a base class is to allow the Serializable template class to inject the necessary .serialize() methods into the class objects so that you don't have to write class-specific serialize methods by hand.

And with this, our clever little DbI .serialize function now knows how to serialize classes, too, and to do so automatically and with almost no human intervention (other than declaring the class in the above way). Now, you can even use OOP with DbI and enjoy its benefits!  (Well OK, to a certain extent.  The above assumes that your classes are data-storage classes with public members that can be mechanically serialized. If this isn't the case, you'll have to do more work.  Which is why I prefer not to do that.  Data-centric types work better.)

//

Now, the above is really only half of the story.  The other half is deserialization, which follows the same principle: the corresponding DbI .deserialize function does pretty much the same thing: use compile-time introspection to discover the incoming type's data fields, read the serialized data from disk, and use std.conv.to to convert it to the correct type (in spite of the naysayers, I think std.conv.to is one of the best things that's ever happened to D -- my .deserialize function literally consists of calls to `data.field = serializedString.to!T` -- and it all Just Works(tm)).  All fully automated and with no human intervention, which means that once you've fully debugged .serialize and .deserialize, you don't ever have to worry about serialization again; all new data types you add will automatically be serializable / deserializable with no further effort (or bugs).

Handling classes in deserialization is a bit tricky, but nothing too hard by combining template classes with static ctors.  I won't get into the details here, but if you're curious, just ask and I'd be more than happy to spill all the gory details.  Basically, we (again) use compile-time introspection to discover base classes, instantiate a factory function for recreating the class, then use a static ctor to inject this function into a global registry of factories that the deserialization code can use to deserialize the original class.

And of course, as you should expect by now, this is fully automated thanks to templates and DbI; the programmer does not need to write a single line of boilerplate to make it all work.  Just declare the class using the CRTP serializable injector above, and the templates take care of the rest of the boilerplate for you.  No need for human intervention, and therefore no room for human error.  It all Just Works(tm).

//

Metaprogramming is D's greatest strength, and templates are one of the keystones.  Rather than shy away from templates, we should rather embrace them and exploit them in ways other languages can only dream of.


T

-- 
Real Programmers use "cat > a.out".
6 days ago
On Monday, 1 September 2025 at 16:26:15 UTC, H. S. Teoh wrote:

> OOP is useful for certain classes (ha) of problems, but not all, as the OOP proponents would like you to believe.  Some classes of problems are better solved with other means.

Do note that I had to bite the bullet and write an OO File class, just so I could write emulated (but compilation/type compatible) subclasses of File.  String- and ubyte[]-based reader/writer instances, specifically.  Think Python's StringIO.

It sure made a number of things easy once I could use polymorphism wrt File.  I'm very glad that D has classic OO semantics available.

Andy

6 days ago
On Mon, Sep 01, 2025 at 09:28:29PM +0000, Andy Valencia via Digitalmars-d-learn wrote:
> On Monday, 1 September 2025 at 16:26:15 UTC, H. S. Teoh wrote:
> 
> > OOP is useful for certain classes (ha) of problems, but not all, as the OOP proponents would like you to believe.  Some classes of problems are better solved with other means.
> 
> Do note that I had to bite the bullet and write an OO File class, just so I could write emulated (but compilation/type compatible) subclasses of File.  String- and ubyte[]-based reader/writer instances, specifically.  Think Python's StringIO.

Yeah, OO is useful for that.

OTOH I've also tended to use the following pattern for testing functions that do I/O:

	auto processFile(File = std.stdio.File)(File input) {
		...
		foreach (line; input.byLine) { ... }
		...
	}

	unittest {
		struct MockFile {
			auto byLine() {
				return ... /* mock implementation of .byLine here */
			}
		}
		auto output = processFile(MockFile());
		assert(output == expectedOutput);
	}

So normally, `File` will bind to std.stdio.File, but the unittest can override this to be a mock file type where I can use simple code to inject input strings to test the implementation of processFile. The nice thing about this is that MockFile doesn't have to implement most of std.stdio.File's API; only those actually used by processFile(). Whereas if you used a subclass you may have to write more code just to do lip service to the base class API so that it will compile, even if some of this code may actually never get used.

Also, since this is a template, when not compiling with -unittest the MockFile instantiation of processFile never happens, so the release build binary does not contain unittest template bloat, and processFile binds directly to std.stdio.File without any intermediate indirections.


> It sure made a number of things easy once I could use polymorphism wrt File.  I'm very glad that D has classic OO semantics available.
[...]

As I said, OO has its place, and should be used when it's appropriate. Just don't shoehorn every programming problem and its neighbour's NP-complete dog into an OO paradigm when it doesn't even fit OO's domain.


T

-- 
All men are mortal. Socrates is mortal. Therefore all men are Socrates.
5 days ago

On Monday, 1 September 2025 at 13:58:23 UTC, Brother Bill wrote:

>

I have heard that there are better or at least alternative ways to have encapsulation, polymorphism and inheritance outside of OOP.

With OOP in D, we have full support for Single Inheritance, including for Design by Contract (excluding 'old'). D also supports multiple Interfaces.

What would be the alternatives and why would they be better?
I assume the alternatives would have

  1. Better performance
  2. Simpler syntax
  3. Easier to read, write and maintain

If possible, please provide links to documentation or examples.

I have heard that people use structs and do these things themselves. I know of one library that implements this: https://code.dlang.org/packages/tardy

IMO, the largest problem with classes for people is that it's always a reference type, and generally heap (gc) allocated. So performance and expressiveness. IMO, reference type is the correct default for polymorphism.

And also note that structs already support encapsulation. They just don't do inheritance/polymorphism.

I believe last year's dconf Walter hinted that he would be open to adding struct inheritance/interfaces.

I think roll-your-own polymorphism is always going to suck compared to language-supplied.

-Steve