良好的对象结构,可选择性合成,不会造成类爆炸


Good object structure for selective composition without class explosion

我的实际场景有点难以解释,所以我将把它映射到一个更容易识别的领域,比如家庭娱乐设备:

一种特定的设备可以提供不同的服务:

松下XYZ可以播放dvd和cd。
索尼ABC只能播放cd。
日立PQR可以播放dvd和接收电视。


每个服务(DVD, CD, TV,…)都有一个大多数模型使用的默认实现,但是一些模型有特定服务的定制版本。//


模型选择实现DVD'izable, CD'izable, TV'izable,…合同将导致模型之间的大量代码重复。//


单继承

实现默认服务的单个超类将允许我为每个包含其所有自定义行为的模型拥有单个子类。但是,对于不提供所有类型服务的模型来说,我的超类会非常笨拙和沉重。//


多重继承

多重继承具有选择性地合并所需服务和提供默认实现的能力,表面上看起来很理想。比起继承引入的耦合,我更看重在单个类中拥有所有松下xyz的自定义功能的内聚性。

但是我没有使用c++(而是PHP),而且我觉得有更好的方法。我也不想使用像mixins或5.4的特性这样的专有扩展。//


作文

我看到了一个类爆炸,我为一个特定模型的自定义功能分散在多个类上——例如,我需要一个PanasonicXYZ_CD类和PanasonicXYZ_DVD类,它们只会被PanasonicXYZ对象使用。//


有没有更好的结构?

编辑:我会好好考虑一些评论和回答,而不是过早地评论。

使用成分卢克:

对象类表示系统中的角色。即使考虑"设备"是很自然的,为了面向对象的目的,最好考虑一下你的设备的角色:DVD播放器,电视接收器,CD播放器,等等。

单个设备完成所有这些并不重要,考虑对象将具有的角色将帮助您以单一职责对象:

结束。
class SonyTvWithDVDPlayer {
     DVDPlayer asDVDPlayer();
     Tv asTv();
}

这样很容易重构公共功能,你可以有一个GenericDVDPlayer,并在asDVDPlayer方法中返回它。

如果你想允许更动态的使用,比如询问Device支持哪些功能,你可以使用一种Product Trader,例如:

interface  MultifunctionDevice {
      <T> T as(Class<T> functionalitySpec) throws UnsupportedFunctionalityException
}
在代码中你可以这样做:
device.as(DVDPlayer.class).play(dvd);

看到,在这种情况下,"多功能设备"的行为就像一个产品贸易商DVDPlayer.class产品规格

有很多不同的方法来实现交易者和规范,一种是使用访问者模式。但是我发现,在很多情况下(当你想要动态配置你的"多功能设备"时),你可以这样做:
class MultifunctionDevice {
      Iterable<Device> devices;
      <T extends Device> T as(Class<T> spec) {
          for (Device dev : devices) {
              if (dev.provides(spec)) return dev;
          }
          throw new UnsupportedFunctionalityException(spec);
      }
 }

与Builder和流畅的API相结合,可以轻松定义不同的设备,而无需类爆炸:

   dvdAndTv = new DeviceBuilder("Sony All in one TV and DVD")
        .addPart(new SonyDvdPlayer())
        .addPart(new SonyTv())
        .build();

我会使用依赖注入做这样的事情:

abstract class Device {
    private function addFunction(Function $function)
    {
        // store it
    }
    public function callFunction($function, $method)
    {
        // check whether local method exists (e.g. $function$method() or dvdPlay())
        // if not: call $function->$method
    }
}

则所有设备扩展Devices,所有功能扩展Function。要更改默认的Function实现,您的设备类(例如SonyAbc)可以提供自己的方法,例如dvdPlay()

我倾向于Composition

你甚至需要每个设备的实际类吗?或者,提供一个特定的电视作为一个带有某些功能的设备的适当连接实例就足够了吗?

samsunSxYZ() {
   Device device = new Device()
   device.addFunction(new SamsungDvdStandard())
   device.addFunction(new SamsungCdStandard())
   return device;
}
samsunSabc() {
   Device device = new Device()
   device.addFunction(new SamsungDvdStandard())
   device.addFunction(new SamsungCdSpecialAbc())
   return device;
}

特性可以通过调用

来检索
DvdFeature dvdFeature = device.getFunction(DvdFeature.class)

服务是否已知且数量有限?如果是这样,我会这样做:

  • 为每一种服务(如DvdPlayer, TvReceiver, CdPlayer等)定义一个接口

  • 给每个服务接口一个默认实现(许多设备将共享的通用实现),并编写您还需要的特定于模型的实现。

  • 定义第二组接口来表示特定设备具有提供服务的能力(例如DvdPlayerCapable),每个接口为服务接口声明一个(唯一命名)getter,例如:

    public DvdPlayer getDvdPlayer();
    
  • 现在您的设备可以组合多个功能接口,并使用该服务的默认实现或特定型号的实现…

如果你想在Sony123Device上进入dvd第十五章,也就是DvdPlayerCapable你可以调用:

mySonyDevice.getDvdPlayer().goToChapter(15);

看来你有两个问题。第一个问题是如何在服务之间传递公共功能,这可以通过继承来解决。如下所示:

public abstract class BaseDevice  
{  
  //common methods with implementations  
}  
public class SonyPlayer extends BaseDevice {...}  
public class SamsungPlayer extends BaseDevice{...}

这个问题的有趣之处在于我们如何进入默认值的定制版本。在这种情况下,我们不想使用继承,因为我们知道它直接违反了LSP。因此,我们可以通过组合来处理它,这将导致类似于以下内容:

public class CustomSonyPlayer  
{  
   SonyPlayer sonyPlayer;  
   //Adds a digital signature after using the SonyPlayer#ripMP3
   public void ripMP3Special(CustomSonyPlayer customSonyPlayer)  
   {  
          MP3 mp3 = sonyPlayer.ripMP3(new MP3("Life goes on"));  
          customSonyPlayer.addSignature(mp3);
   }  
}

或者您可以在实用程序类内部公开公共特殊功能。

过去,我在根抽象类上提供了一个通用的custom_functions。它被序列化存储

然后调用

foreach($device->Custom_Functions() as $name=>$function){
    if(is_int($name))
        echo '<li>',$function, '</li>';
    else
        echo '<li>',$name, ': ', $function, '</li>';
}

添加海关通过方法:

function Add_Custom($name, $function) {
    if(empty($name))
        $this->customizes[] = $function;
    else
        $this->customize[$name] = $function;
}