ID:1810596
 
Welcome once again to Snippet Sundays. Today is a special kind of Sunday where my brain isn't fogged by too much work and my time consumed by too much responsibility, so I figured it'd be nice to treat you guys to a bonus installment.

In this installment, we're going to talk about interfaces. No, not the pretty windows-default GDI buttons and gorgeous statpanels we like to wow our playerbase with. This is a programming concept, and it's something that DM technically doesn't do. However, DM is a highly flexible language, which allows us to use fake interfaces to keep our code clean and intelligible.

What are interfaces?

Interfaces are an abstraction technique that allows you to specify that behavior is generally shared between similar, but not identical object structures.

In other programming languages, such as Java, Interfaces are a lot like classes, or DM's types. Interfaces are sort of a package of behavior that you can include multiple of into a class(or type) definition that will modify the final behavior of the defined class and its instances.

This is something similar to how it would look:

class SomeClass extends BaseClass implements SomeInterface, SomeOtherInterface {
//structure goes here.
}


In this example, SomeClass would get all of the behavior of BaseClass, as well as all of the behavior of SomeInterface and SomeOtherInterface. This allows you to have an object be treated like several different types of object at the same time, and inherit multiple kinds of behavior from multiple sources.

This is useful because it allows you to treat entirely different objects similarly, and to access properties and behavior without complex typecasting.

Want to know a secret? DM can do this too... Sort of.


Let's look at an example:

Let's say you want to make some objects in the world able to hold items. Some of these objects could be mobs, some of these objects could be other items, and some of these objects could be instances of /obj sitting on the map. Normally, you would do one of two things:

1) Embed the container behavior at the lowest level, in /atom/movable. This is inelegant and ugly, because not every object in the world needs to have this behavior.

2) Re-implement the container behavior in each object that can act as an item container. This is a better solution, but it's problematic because now it makes our code for working with these container objects really ugly, as we will have to do all kinds of istype checks to work with these objects, or use the call() and ":" look-up operator to work with the members of these types. This can lead to ugly run-time errors and very confusing code.

There's a third way, but first, I want to show you the first two ways.

Method #1:

atom
movable
var
slots = 8 //the number of items that this container can hold
items = 0
list/item_contents //a list for storing the items contained in this container
proc
AddItem(obj/item/item)
var/slot
if(!item_contents)
var/l[slots]
item_contents = l
l[1] = item
++items
return 1
else
slot = item_contents.Find(null)
if(slot)
item_contents[slot] = item
++items
return slot
return 0

RemoveItem(obj/item/item)
if(item_contents)
var/slot = item_contents.Find(item)
if(slot)
item_contents[slot] = null
--items
if(!items)
item_contents = null
return slot
return 0

SwitchItem(obj/item/item1,obj/item/item2)
if(item_contents)
var/slot1 = item_contents.Find(item1)
var/slot2 = item_contents.Find(item2)
if(slot1 && slot2)
item_contents[slot1] = item2
item_contents[slot2] = item1
return 1
return 0

SwitchSlot(slot1,slot2)
if(item_contents)
var/obj/item/item1 = item_contents[slot1]
var/obj/item/item2 = item_contents[slot2]
item_contents[slot1] = item2
item_contents[slot2] = item1
return 1
return 0

GetItems()
if(item_contents && items)
var/list/l = item_contents.Copy(1,0)
while(l.Remove(null))
return null


This example is bad because not everything needs to be a container. Having the behavior embedded at too low of a level is really problematic, because it gives things that have no business being an item container extra variables and functions that could mess your world up if called.

Example #2:

obj
item_container
var
slots = 8 //the number of items that this container can hold
items = 0
list/item_contents //a list for storing the items contained in this container
proc
AddItem(obj/item/item)
var/slot
if(!item_contents)
var/l[slots]
item_contents = l
l[1] = item
++items
return 1
else
slot = item_contents.Find(null)
if(slot)
item_contents[slot] = item
++items
return slot
return 0

RemoveItem(obj/item/item)
if(item_contents)
var/slot = item_contents.Find(item)
if(slot)
item_contents[slot] = null
--items
if(!items)
item_contents = null
return slot
return 0

SwitchItem(obj/item/item1,obj/item/item2)
if(item_contents)
var/slot1 = item_contents.Find(item1)
var/slot2 = item_contents.Find(item2)
if(slot1 && slot2)
item_contents[slot1] = item2
item_contents[slot2] = item1
return 1
return 0

SwitchSlot(slot1,slot2)
if(item_contents)
var/obj/item/item1 = item_contents[slot1]
var/obj/item/item2 = item_contents[slot2]
item_contents[slot1] = item2
item_contents[slot2] = item1
return 1
return 0

GetItems()
if(item_contents && items)
var/list/l = item_contents.Copy(1,0)
while(l.Remove(null));
return null
mob
being
var
slots = 8 //the number of items that this container can hold
items = 0
list/item_contents //a list for storing the items contained in this container
proc
AddItem(obj/item/item)
var/slot
if(!item_contents)
var/l[slots]
item_contents = l
l[1] = item
++items
return 1
else
slot = item_contents.Find(null)
if(slot)
item_contents[slot] = item
++items
return slot
return 0

RemoveItem(obj/item/item)
if(item_contents)
var/slot = item_contents.Find(item)
if(slot)
item_contents[slot] = null
--items
if(!items)
item_contents = null
return slot
return 0

SwitchItem(obj/item/item1,obj/item/item2)
if(item_contents)
var/slot1 = item_contents.Find(item1)
var/slot2 = item_contents.Find(item2)
if(slot1 && slot2)
item_contents[slot1] = item2
item_contents[slot2] = item1
return 1
return 0

SwitchSlot(slot1,slot2)
if(item_contents)
var/obj/item/item1 = item_contents[slot1]
var/obj/item/item2 = item_contents[slot2]
item_contents[slot1] = item2
item_contents[slot2] = item1
return 1
return 0

GetItems()
if(item_contents && items)
var/list/l = item_contents.Copy(1,0)
while(l.Remove(null));
return null


As you can see, this example's just verbose and it's annoying to work with because every time you update the code for one type of container, you need to sync those updates to every other object that could be a container.


Now that we've seen the two bad examples, let's take a look at something really cool you can do instead:

#define ITEM_CONTAINER var/slots=8;var/items=0;proc/AddItem(obj/item/item){var/slot;if(!item_contents){var/l[slots];item_contents = l;l[1] = item;++items;return 1;}else{slot = item_contents.Find(null);if(slot){item_contents[slot] = item;++items;return slot;}}return 0;};var/list/item_contents;proc/RemoveItem(obj/item/item){if(item_contents){var/slot = item_contents.Find(item);if(slot){item_contents[slot] = null;--items;if(!items){item_contents = null;}}return slot;}return 0;};proc/SwitchItem(obj/item/item1,obj/item/item2){if(item_contents){var/slot1 = item_contents.Find(item1);var/slot2 = item_contents.Find(item2);if(slot1 && slot2){item_contents[slot1] = item2;item_contents[slot2] = item1;return 1;}}return 0;};proc/SwitchSlot(slot1,slot2){if(item_contents){var/obj/item/item1 = item_contents[slot1];var/obj/item/item2 = item_contents[slot2];item_contents[slot1] = item2;item_contents[slot2] = item1;return 1;}return 0;};proc/GetItems(){if(item_contents && items){var/list/l = item_contents.Copy(1,0);while(l.Remove(null));}return null;};

proc
isContainer(datum/d)
if(istype(d,/datum) && ("item_contents" in d.vars))
return 1
return 0

item_container
var
slots = 8 //the number of items that this container can hold
items = 0
list/item_contents //a list for storing the items contained in this container
proc
AddItem(obj/item/item)
var/slot
if(!item_contents)
var/l[slots]
item_contents = l
l[1] = item
++items
return 1
else
slot = item_contents.Find(null)
if(slot)
item_contents[slot] = item
++items
return slot
return 0

RemoveItem(obj/item/item)
if(item_contents)
var/slot = item_contents.Find(item)
if(slot)
item_contents[slot] = null
--items
if(!items)
item_contents = null
return slot
return 0

SwitchItem(obj/item/item1,obj/item/item2)
if(item_contents)
var/slot1 = item_contents.Find(item1)
var/slot2 = item_contents.Find(item2)
if(slot1 && slot2)
item_contents[slot1] = item2
item_contents[slot2] = item1
return 1
return 0
SwitchSlot(slot1,slot2)
if(item_contents)
var/obj/item/item1 = item_contents[slot1]
var/obj/item/item2 = item_contents[slot2]
item_contents[slot1] = item2
item_contents[slot2] = item1
return 1
return 0

GetItems()
if(item_contents && items)
var/list/l = item_contents.Copy(1,0)
while(l.Remove(null));
return null


What this snippet does, is take and put all the item_container generic code behind a single line of code into a preprocessor macro. It's really ugly, I know, and a bit annoying to work with, but assuming you don't have to change it later, it actually allows you to do some really convenient things. For starters, check this out:

mob
being
ITEM_CONTAINER

obj
item_container
ITEM_CONTAINER

item
container
ITEM_CONTAINER

equipment
container
ITEM_CONTAINER


I just set up container behavior under four distinct type paths in less than 20 lines of code.

I really don't recommend you do this often for everything. It's best to only do this when you need to treat a lot of different types in a similar way, and don't want to deal with the headaches caused by refactoring and type-checking.

Now, you might be wondering about why I also wrote all that code out into a datum as well. Actually, that datum isn't meant to be created at any time in your project. Don't create them. The reason I left the code intact inside of that datum was to give a clean copy of the code we condensed down to one line, so you can easily read what those functions do.

Now, it might seem strange to you to define a datum that you aren't going to initialize, but there's a reason for this. We can now treat that datum's type as an interface just like in programming languages like Java.

var/item_container/container
for(var/atom/o in containers)
container = o
world << container.items
container.GetItems()


Notice how we're casting the atom into a variable that doesn't match its type? DM allows this. It also allows us to perform explicit member references using the "." operator.

Normally, you see people doing stuff like this in DM:

for(var/atom/o in containers)
world << o:items
call(o,"GetItems")()


Sure, the code is smaller, but the example up top actually runs marginally faster than the code below. It's because call() is slightly slower than an explicit invocation.

Again, this approach isn't something I'd recommend using ALL the time, but if you find yourself seriously needing multiple-inheritance-like structures in DM, this is one way to go about it. There are definite cons to this approach compared to the other two approaches I showed, though. This approach isn't "the best", it's just one option. I think it's a good option in specific cases, but not the be-all-end-all for how to approach multiple-inheritance-like logic.