Object-oriented Paradigm Question
Asked Answered
A

10

8

Even though I've been programming for quite a while now, when it comes to coupling objects I always seem to bang my head against the wall so I'm wondering if anyone has any resources or golden rules I can follow.

Let me give a small example, in no particular language...

class Person {
    private int personnel_id
    private String first_name;
    private String last_name;
    private int personnel_level;
    //Lab labs[4]; <- Lab(s) the Person works in
}

class Lab {
    private int lab_id;
    private String lab_name;
    //Person[99] personnel; <- Person(s) working in the Lab
}

Lets ignore ctors/setters/getters/dtors for now and just instantiate some stuff...

Person people = new Person[1500];
Lab labs = new Lab[10];

My question is.. what's the best practice here...

people["Gordon Freeman"].blewUp((Lab)"Black Mesa");
-> returns T/F

or...

labs["BlackMesa"].blownUpBy((Person)"Gordon Freeman");
-> returns T/F

or maybe it doesn't even matter :S

The real-life example I'm working on is a ton more complex. Whenever the Person does something, everyone in the Lab needs to be notified, etc, and I'm just trying to figure out if there are any principles I can apply here.

Almondeyed answered 15/7, 2010 at 3:32 Comment(4)
What is returns T/F ?Sapir
I suppose it is returns True or False?Handedness
Yes, sorry T/F = returns true or false.Almondeyed
+1 for Gordon Freeman and Black Mesa exampleHandoff
L
3

My answer is a combination of several existing answers.

The essential problem here is that there is a hidden concept here. The method isn't really talking about lab object or the person object, but about the relationship between them. (As suggested by @dacris and @vs.)

One way to deal with such situations is to use a language with double-dispatch (Thank you, @Ken.)

Another way, is to have auto-generated code (Thank you @vs.) in which case there would be methods available in either direction.

But often those solutions aren't practical - changing entire languages over this seems overkill.

The auto-generated solution gives us an insight though. Both techniques should be legal. So you could implement both techniques manually.

However, if you don't want to repeat yourself, this approach makes it clear that EITHER direction is legal. So don't sweat it, too much.

If you are coding a system where the Person object has other uses apart from exploding things, it would be better for the coupling to go from Lab to Person (i.e. put the methods on the Lab object) so the Person object can be used elsewhere without having to deal with changes to the Lab object or the explosion-related methods.

... and vice-versa. If all a person does is explode things, then the logic should be there to keep the lab clean and pristine (which is important for labs!)

Listed answered 15/7, 2010 at 10:20 Comment(0)
B
2

You might want to read a bit about the Observer and Publish/Subscribe patterns. What you're describing is pretty much the classic application for the Observer pattern. The pub/sub pattern is basically the same idea abstracted a bit more to help scaling.

In any case, given how well known this pattern already is, you might as well follow its convention unless you encounter a situation where you're really sure you benefit from doing otherwise.

Bentwood answered 15/7, 2010 at 4:6 Comment(2)
Hmmm... So you are suggesting that Person observers Lab, so they can be told when they are blownup, but that still leaves the question of how the explosion is triggered (so to speak)- e.g. lab1.exploded(culprit) or culprit.explode(lab1).Listed
I really like pubsub, but like @Listed said, the coupling "issue" is still there. Maybe it's just how my brain works. In the end, you pretty much get the same result, but for example... when I leave a room in real life. I just.. leave the room. The room doesn't have to "lose me"... nor does the room have to tell everyone else in the room that "David left". Maybe I'm just being OCD about it for no good reason, I've written this type of code millions of times before >_<Almondeyed
I
1

Think like you're speaking English. The general rule is, verbs (and methods) should have "active voice" as much as possible -- that is, an object should do something, rather than have something done to it.

If it's an event, passive voice makes a little more sense -- the Lab should know what Persons are in it, but some random Person (even one working in the same Lab) probably shouldn't, so a notification that the Lab blew up would be best coming from the Lab itself. But really, it's about personal (or team) preference in that case.

Indicatory answered 15/7, 2010 at 3:36 Comment(3)
I disagree with the first part. The rules of English can be used to come up with good method names, one the responsibilities are determined, but they shouldn't dictate object responsibilities themselves.Listed
Dictate, no -- but they do tend to go hand-in-hand. An object should act, rather than be acted on, when possible. That's part of the thought behind OOP.Indicatory
If you say lab.explode(culprit) the lab object is acting, rather than being acted on. If you say culprit.explode(lab) the person object is acting, rather than being acted on. Doesn't help distinguish in this case.Listed
C
1

I am not entirely sure what your example means, but an

An excellent book that has what you want in it is Applying UML and Patterns by Craig Larman.

The book talks extensively about assigning responsibilities. For example, you might use the Information Expert pattern, in which case, the object that has the most knowledge of the variables involved will be the one that is given the responsibility of having the method.

Compute answered 15/7, 2010 at 4:39 Comment(0)
F
1

You're right. I think this is one of the major problems of most of today's object-oriented systems: often, methods seem to naturally "belong to" an object, but often they don't.

Systems with multiple dispatch neatly avoid this problem. For example, in Dylan, you might say something like:

define method blows-up(p :: <person>, l :: <lab>) => explodes :: <boolean>;
  // ...returns #f or #t...
end method;

(I linked to the c2.com MultiMethods page because I think it does the least-bad job of describing this. Wikipedia has a page for Multiple_Dispatch, but its example is pretty awful.)

Fractionize answered 15/7, 2010 at 5:14 Comment(0)
S
1

oO give you a different perspective on this: actually you're not interested in either Persons or Labs, but in a relation between them. If you look at it from a UML- or database perspective, you would see that this relation is very much a new concept in your (mental) model. See @dacris comment above as well, where he introduces a new class.

If you would use ORM (Object-Relational Mapping), like when you would do when engineering with UML models, those two methods blowsUp() and blownUpBy() would be automatically code-generated, with their respective runtime checks to ensure their consistency.

Larman's book should indeed contain something about this topic for you.

Simply answered 15/7, 2010 at 6:1 Comment(0)
G
0

I think that it is related to real world and your coding convention rather than general good practices. For your English, I still prefer call people.notify(lab). However, if you want your lab to have some data about who call it, which person, you can do lab.isNotifiedBy(people).

The good practice here is that it makes sense for you and your colleague when looking at the code, they understand what it does, if they want to find a method, they know where they should start rather than just keep searching or asking you

Gnat answered 15/7, 2010 at 3:39 Comment(0)
E
0

I like designing things like this:

let blackMesa = labs["BlackMesa"]
if (blackMesa.isDestroyed) 
{
    let destroyer = blackMesa.destroyer
}
Electoral answered 15/7, 2010 at 3:40 Comment(0)
M
0

In this case I'd like to introduce a new object - LabExplosion

class LabExplosion
{
    private Person personResponsible;
    private Lab labAffected;
}

Then, keep a repository of LabExplosions somewhere, and do something like:

// To find out if Gordon Freeman ever blew up Black Mesa
explosions.find("Gordon Freeman", "Black Mesa").length > 0;
// returns T/F
Makeyevka answered 15/7, 2010 at 4:27 Comment(1)
and I guess you instantiate a new LabExplosion whenever someone blows up a lab?Almondeyed
S
0

what's the best practice here...

It depends on your use case, how is the user going to use the system?. Would it be a Lab being "blowed" by a Person? or the use case of the system is to have some Person blow up Labs?

or maybe it doesn't even matter :S

At the end the result is the same, but the important thing here is the semantic of the code. If sounds silly to have Labs being blowed by people, then don't do it.

So the golden rule, as BobTurbo mention, is to find out the "information expert" ( see: GRASP ) in the system and give the control to that object.

You usually define a user history or narrative on how the system would be used, if, for instance, the narrative is:

When a person does something everyone in the lab has to be notified.

Then, to me it means that a Person works in a Lab, when it is created, that person may receive the lab he works on, and register himself to be notified of what happens in that perticula lab.

Since the lab has the list of the persons to notify, it makes sense to be the lab who performs the notification ( Person gives control to Lab in this case )

Then probably the Person could be defined as:

labs.Person {
     - name: String
     - lab : Lab 

     + Person( withLab: Lab , andName: String ) {
           self.lab = withLab
           self.name = andName
           self.lab.subscribe( self ) // want to know what happens
      }


     + blowUpLab() {
           lab.boom!(blownBy:self)
       }
       // receive a lab even notification 
       // performed by "someone" 
     + labEvent( lab:Lab, by: Person  ) {
          // if is my lab and it wasn't me?
          if( self.labg .== lab .&& self .!= by ) {
             // ok it was someone else.... 
          }
       }
  }

So, the person does something in the lab, in this case the public method blowUpLab which just blows up the person's lab by invoking the Lab's boom! method.

In turn the Lab perform the method actions and notify all its subscribers at the end:

labs.Lab {
    - labName:String
    - subscribers: Person[0..*]

    + subscribe( to: Person ) {
          subscribers.add( to ) 
      }

    + boom!( blowedBy: Person ) {
         // do blow up the lab 
         .... 
         // and then notify:
        subscriber.forEach( person: Person ) {
             person.labEvent( self, blowedBy )
         }
     }
 }

This is the observer pattern.

Finally your main app will create persons and labs and execute the use case:

 labs.MainApp {
     _ main() {
          blackMesaLab = Lab("BlackMesa")
          gordon = Person( withLab: blackMesaLab, andName: "Gordon Freeman")
          scott  = Person( withLab: blackMesaLab, andName: "Scott Tiger")
          james  = Person( withLab: Lab("SO Labs"), andName:" James Hetfield");

          persons = Array( gordon, scott, james )

          .... 

         while( true ) {
              // every now and then, have someone blowing up it's lab 
              if ( randomNumber() .== 42 ) {
                  person.at( randomPosition ).blowUpLab()
             } 
         }
       }
   } 

This main app, will create three person, with some lab, only two of them are related ( scott and gordon )

Randomly one of them will receive the blowUpLab message and will perform the method. The lab in turn will notify all the subscribers of that lab.

So, when James Hetfield, blow its lab, no one will be notified :)

The point is Do describe your use case, and identify the information expert there; give the control to that object, and let that object rely the control to other object, but only according to your use case

I hope it makes sense.

Sapir answered 15/7, 2010 at 5:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.