良好的对象结构,可进行选择性组合,而不会发生类爆炸

发布于 2024-12-19 14:06:49 字数 853 浏览 0 评论 0原文

我的真实场景有点难以解释,因此我将其映射到一个更容易识别的领域,例如家庭娱乐设备:

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

松下 XYZ 可以播放 DVD 和 CD。
索尼 ABC 只能播放 CD。
Hitachi PQR 可以播放 DVD 和接收电视。
...
...

每个服务(DVD、CD、TV...)都有大多数模型使用的默认实现,但某些模型具有特定服务的自定义版本。


接口

选择实现 DVD 可化、CD 可化、TV 可化……合同的模型将 导致模型之间存在大量代码重复。


单一继承

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


多重继承

多重继承能够选择性地合并所需的服务并提供默认实现,从表面上看似乎很理想。我更看重将所有 PanasonicXYZ 自定义功能放在一个类中的内聚性,而不是继承引入的耦合。

但我没有使用 C++(而是 PHP),而且我觉得无论如何都有更好的方法。我也不想使用专有扩展,如 mixin 或 5.4 的特征。


组合

我看到类爆炸式增长,特定模型的自定义功能分散在多个类中 - 例如,我需要 PanasonicXYZ_CD 类和 PanasonicXYZ_DVD 类,并且它们只会被松下XYZ 对象。


有更好的结构吗?

编辑:我会仔细考虑一些评论和答案,而不是过早发表评论。

My real scenario is a bit hard to explain so I'll map it to a more recognizable domain, say home entertainment equipment:

A particular piece of equipment can offer different services:

A Panasonic XYZ can play DVDs and CDs.
A Sony ABC can only play CDs.
A Hitachi PQR can play DVDs and receive TV.
...
...

Each service (DVD, CD, TV,...) has a default implementation which most models use, but some models have customized versions of particular services.

Interfaces

Models choosing to implement DVD'izable, CD'izable, TV'izable,... contracts would
result in a lot of code duplication between models.

Single Inheritance

A single superclass implementing the default services would allow me to have a single subclass for each model containing all of its custom behaviour. My superclass would however be quite unwieldy and heavier than it need be for models that don't offer all types of services.

Multiple Inheritance

Multiple inheritance with its ability to selectively incorporate the services required and provide default implementations, on the surface seems ideal. I value the cohesiveness of having all PanasonicXYZ's custom functionality in a single class more than the coupling introduced by the inheritance.

But I'm not using C++ (rather PHP) and I sort of feel there's a better way anyway. Nor do I want to use proprietary extensions like mixins or 5.4's traits.

Composition

I see a class explosion with my custom functionality for a particular model scattered over multiple classes--I'd need a PanasonicXYZ_CD class and PanasonicXYZ_DVD class for example, and they'd only ever be used by the PanasonicXYZ object.

Is there a preferable structure?

Edit: I'll have a good think about some of the comments and answers made instead of prematurely commenting.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

瞎闹 2024-12-26 14:06:49

使用组合 Luke

对象类代表系统中的角色。即使考虑“设备”是很自然的事情,出于 OOP 的目的,最好考虑设备的角色:DVD 播放器、电视接收器、CD 播放器等。

单个设备完成所有操作并不重要其中,思考对象将具有的角色将帮助您结束单一责任对象:

class SonyTvWithDVDPlayer {
     DVDPlayer asDVDPlayer();
     Tv asTv();
}

通过这种方式很容易重构常见功能,您可以拥有一个GenericDVDPlayer< /code> 并将其返回asDVDPlayer 方法。

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

interface  MultifunctionDevice {
      <T> T as(Class<T> functionalitySpec) throws UnsupportedFunctionalityException
}

在代码中您可以执行以下操作:

device.as(DVDPlayer.class).play(dvd);

请注意,在这种情况下,“多功能设备”的行为就像 Product Trader 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);
      }
 }

这与构建器和流利的 API 相结合使得很容易定义不同的设备而不发生类爆炸:

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

Use composition Luke:

Object classes represents a role in your system. Even when is natural to think about a "Device", for OOP purposes is better to think about what are the roles of your device: DVD Player, TV Receiver, CD Player, etc.

It doesn't matter that a single device does all of them, thinking in the roles that the object is going to have will help you to end with single responsibility objects:

class SonyTvWithDVDPlayer {
     DVDPlayer asDVDPlayer();
     Tv asTv();
}

In that way is easy to refactor common functionality, you can have a GenericDVDPlayer and return that in the asDVDPlayer method.

If you want to allow a more dynamic usage, like asking to a Device which functionality it supports, you can use a kind of Product Trader, for example:

interface  MultifunctionDevice {
      <T> T as(Class<T> functionalitySpec) throws UnsupportedFunctionalityException
}

and them in the code you could do something like this:

device.as(DVDPlayer.class).play(dvd);

See that in this case the "multifunction device" acts like a Product Trader and the DVDPlayer.class is the spec of the product.

There are a lot of different ways to implement the trader and the spec, one is to use the Visitor pattern. But I found that in a lot of cases (when you want to be able to configure your "multifunction devices" dynamically) you can do something like this:

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);
      }
 }

This combined with a Builder and a fluent API makes easy to define different devices without class explosion:

   dvdAndTv = new DeviceBuilder("Sony All in one TV and DVD")
        .addPart(new SonyDvdPlayer())
        .addPart(new SonyTv())
        .build();
撩动你心 2024-12-26 14:06:49

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

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()

I would use dependency injection to do something like this:

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
    }
}

Then all devices extend Devices, all functions extend Function. To change a default Function implementation, your device class (e.g. SonyAbc) could provide its own methods for e.g. dvdPlay().

她比我温柔 2024-12-26 14:06:49

我赞成《构图》。

您甚至需要每个设备的实际类吗?或者提供特定的电视作为具有某些功能的设备的正确接线实例就足够了吗?

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)

I would favour Composition.

Do you even need actual Classes for each Device? Or would it be sufficient to provide a specific TV as a properly wired instance of a Device with some Features?

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;
}

Features could be retrieved by invoking

DvdFeature dvdFeature = device.getFunction(DvdFeature.class)
东风软 2024-12-26 14:06:49

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

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

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

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

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

因此,如果您想在您的 Sony123Device(DvdPlayerCapable)上观看 DVD 第 15 章,您可以调用:

mySonyDevice.getDvdPlayer().goToChapter(15);

Are the services already known and limited in number?
If so I would do something like this :

  • Define an interface for each kind of service (e.g. DvdPlayer, TvReceiver, CdPlayer, etc...)

  • Give each service interface a default implementation (the common one that many devices will share) and code whichever model-specific implementations you also need.

  • Define a second set of interfaces to denote that a particular device has the capability to offer a service (e.g. DvdPlayerCapable), each interface declaring one (uniquely-named) getter for the service interface, for example:

    public DvdPlayer getDvdPlayer();
    
  • Now your device can combine multiple capability interfaces, and use either the default implementation of the service or a model-specific one...

And so if for instance you want to go to dvd chapter 15 on your Sony123Device, which is a DvdPlayerCapable, you can call:

mySonyDevice.getDvdPlayer().goToChapter(15);
请止步禁区 2024-12-26 14:06:49

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

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);
   }  
}

或者您可以在实用程序类中公开常见特殊功能。

It seems you have two problems. The first problem is how do you pass around common functionality between services, that can be solved via inheritance. Something like the following:

public abstract class BaseDevice  
{  
  //common methods with implementations  
}  

public class SonyPlayer extends BaseDevice {...}  

public class SamsungPlayer extends BaseDevice{...}

The interesting piece of the problem is where we get into customized versions of the defaults. In this instance we would not want to use inheritance because we know it directly violates LSP. So we could approach it via composition which would result in something similar to the following:

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);
   }  
}

Or you could expose the common special functionality inside of utility class.

兔小萌 2024-12-26 14:06:49

过去,我在根抽象类上提供了通用的 Custom_Fuctions。它被序列化以进行存储,

然后我

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;
}

In the past I have provided a generic Custom_Fuctions on my root abstract class. It is serialized for storage

I then call

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

add customs via method:

function Add_Custom($name, $function) {
    if(empty($name))
        $this->customizes[] = $function;
    else
        $this->customize[$name] = $function;
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文