views:

101

answers:

4

Our software represents each device as a concrete class. So let's say I have a class named Cpu-Version-1, which is derived from a abstract baseclass called Device. Now, the CPU vendor wants to release the economic version of this CPU ( Cpu-Version-2 ), which is a feature reduced version of Cpu-Version-1 (obviously for a lower price ). 90% code of Cpu-Version-2 is same as of Cpu-Version-1, but the remaining 10% code is no longer same. i.e. these are the features that have been removed in Cpu-Version-2 and so I should no longer represent it.

Now what would be the best way to design this class? I could inherit Cpu-Version-2 from Cpu-Version-1 (even though the inheritance is imperfect, since these two devices are peers). This would force me to override a lot of methods of cpu-version-1 to do nothing ( which looks ugly and somehow doesn't feel right). Also I don't think I could change the baseclass (Cpu-Version-1 or it's base) since the code is already in production and requires lot of approvals and justification. How would you make this design decision if you were me?

How do we make Cpu-Version-2 as much reusable and maintainable? Is there any OOP principles that you would follow? Or is it better tradeoff to take the easy wayout and override all the non-applicable methods of Cpu-Version-1 to do nothing? I code in object-oriented Perl.

+4  A: 

I think one of the major stumbling blocks that impedes the progress of object-oriented programming projects is the common misconception that inheritance is the only way to compose new classes from multiple sources. But inheritance in general is actually a pretty sucky model, and only really works for things that can be neatly taxonomized in a perfect hierarchy. As you have discovered, the real world often doesn't work that way.

It sounds like inheritance is not a good model for this problem. Fortunately, there are other models available for constructing classes in Perl.

With Moose, you can use roles for composing classes with a combination of things they "do" without having to create a complicated (and in this case, ill-suited) inheritance hierarchy.

If you don't want something as heavy-duty as Moose, there are also simpler options like mixin.

Another option that may interest you is prototype-based composition, as with Class::Prototyped. This is still hierarchy-based, but gives you a lot more flexibility by allowing you to mess with individual instances and then use those as the prototype for new instances. This is how OO is done in languages like Javascript and Self.

friedo
Thanks Friedo. I need to readup on Moose and Mixin, to see how it fits to my scenario.
rajachan
+2  A: 

You have several options. I don't think having CPU-Version-2 derive from CPU-Version-1 is the answer, though.

If your code is "refactorable" (i.e. you have plenty of unit tests which would allow you to refactor with confidence), then you might consider creating a CPU class which inherits directly from Device. All the common methods could be moved up to CPU, and then you could derive CPU-Version-X from there.

Better yet, however, might be to use composition. Keep an instance of CPU-Version-1 in your CPU-Version-2 object. For any common methods, your CPU-Version-2 object can simply delegate to the equivalent method in CPU-Version-1. You can augment this behavior with CPU-Version-2's own methods to satisfy the requirements.

For instance (pseudocode):

class CPU-Version-2:
   CPU-Version-1 cpu;

   int commonMethod1():
      return cpu.method1();

   void commonMethod2():
      cpu.doSomething();

   int newMethod():
      x = cpu.getSomeValue();
      y = x + 42;
      return y;

Does this help?

Matt Caldwell
Thanks, that was really helpful. I don't think I could refactor "cpu-version-1" code , since the code is already in production. Second option looks good, but do you think violating hierarchy is a strong enough reason to reject inheritance in this case?
rajachan
Also each of these classes have 1000 of methods..Do you think it's a good idea to go with composition and implements common function for each of these methods? Also , if you need to add any common methods , you need to update both "CPU-Version-1/2" . Do we need to accept this tradeoff to maintain code-Integrity ?
rajachan
Any subclass should have an "is a" relationship with its parent class. If you have CPU-Version-2 deriving from CPU-Version-1, then you need to evaluate the truth of the following statement: "CPU-Version-2 is a CPU-Version-1". In this case, the statement is false. In fact, the converse is true; "CPU-Version-1 is a CPU-Version-2", since CPU-Version-2 is the simplest of these objects. This is reason enough to reject inheritance, unless you "flip" the relationship as I suggested in the first option above. Since you cannot refactor in this case, composition is an excellent choice.
Matt Caldwell
The fact that there are so many methods might pose a problem. Any design decision like this will involve trade-offs. There is no right or wrong answer. In the end, you have to provide software that works. :) What I would do is refactor, if possible, so that you have a common CPU base class from which you can derive CPU-Version-X. I wouldn't try this without a solid unit test suite, though. If you can't refactor, then either of the other options will work. Neither is really the "right" solution, but probably acceptable given the limitations, and they each have their own pros and cons.
Matt Caldwell
+3  A: 

If you can't go all the way to using Moose and roles, you can refactor the CPU classes to use composition and delegation.

CPU1 is a CPU
CPU1 has a Frobnicator
CPU1 has a Thingummy
CPU1 has a Poddlewhick
CPU1 has a Fridgemagnet

Where Frobnicator, Thingummy and Poddlewhick are all classes that operate on a CPU object.

CPU2 is a CPU
CPU2 has a Frobnicator
CPU2 has a Thingummy
CPU2 has a Poddlewhick

CPU2 is just like CPU1 but has no Fridgemagnet.

Methods to composed classes would need to get a copy of the CPU object passed to them so that they can request info from the delegator.

package CPU;

sub delegate {
    my $self     = shift;
    my $accessor = shift;
    my $method   = shift;

    return $self->$accessor->$method( $self, @_ );
}

package CPU1;

use Frobnicator;
use Thingummy;
use Poddlewhick;
use Fridgemagnet;


package CPU2;

use Frobnicator;    
use Thingummy;
use Poddlewhick;


package Frobnicator;

use Exporter;
our @ISA = /Exporter/;

our @EXPORT_OK = qw( frobnicate );
our @EXPORT = @EXPORT_OK;

sub frobnicate {
    my $delegator = shift;

    return $delegator->delegate( frobnicator => frob => @_ );
}

This approach leads to a lot of repeated code. You could probably work up some pseudo-role type code injection by importing functions into your classes with a use statement. But if you need to go that far, just use Moose rather than reimplementing it.

Do this ONLY if you can't just use Moose with roles. They work better, with less hassle and boilerplate.

daotoad
+1  A: 

It sounds like you need to move more stuff into your abstract class so you can make concrete classes with exactly what they need, and each inherits from the abstract class.

Alternatively, since you say you cannot change the base class (so, going the not-as-reuseable and not-as-maintainable route), you can just nerf stuff in Cpu-Version-2. If you are inheriting from Cpu-Version-1 but don't want one of it's methods, override it to do nothing:

 package CPU::V2;
 use parent qw( CPU::V1 );

 sub dont_need_this_feature { return }

Without seeing the actual classes and what else you've done, that's as good a guess as I can give you.

brian d foy