This is the Decorator pattern, a structural pattern.
Packages is a decorator object: it implements CableTV while holding another CableTV, to which its default queries delegate. Movies and HDChannels extend the price and description of the wrapped account. Because a wrapper itself satisfies CableTV, another wrapper can surround it; features combine without declaring a new software class for each possible package combination.The Decorator pattern participants are:
CableTV: the component object interface, declaring the price and package description visible to clients.BasicPlan: the concrete component, supplying the initial £30 account and its description.Packages: the abstract decorator object, storing one component and forwarding its queries.MoviesandHDChannels, and the extensionSports: concrete decorator objects, adding respectively £10, £7, and £5 and appending their labels.
The checking application is the client that chooses and nests these participants. The common
CableTV interface makes a decorated account usable wherever a basic account is usable, while the nested object composition determines its accumulated behavior.Use a customer object with no selected platforms as the base component, and wrap it once for each selected channel. Each decorator object has the same interface, delegates existing work, adds one platform send, and adds that platform's per-message charge. The code below uses the
Customer and Comms interfaces from the question; every decorator receives the communication implementation explicitly.final class NoChannels(val name: String) extends Customer {
def spread(msg: String): Unit = ()
def cost: Double = 0.0
}
abstract class CustomerDecorator(protected val inner: Customer)
extends Customer {
final val name: String = inner.name
def spread(msg: String): Unit = inner.spread(msg)
def cost: Double = inner.cost
}
final class OnDiscord(c: Customer, out: Comms)
extends CustomerDecorator(c) {
override def spread(msg: String): Unit = {
super.spread(msg)
out.sendD(name, msg)
}
override def cost: Double = super.cost + 0.01
}
final class OnFacebook(c: Customer, out: Comms)
extends CustomerDecorator(c) {
override def spread(msg: String): Unit = {
super.spread(msg)
out.sendF(name, msg)
}
override def cost: Double = super.cost + 0.05
}
final class OnWhatsApp(c: Customer, out: Comms)
extends CustomerDecorator(c) {
override def spread(msg: String): Unit = {
super.spread(msg)
out.sendW(name, msg)
}
override def cost: Double = super.cost + 0.03
}
def customer(name: String, discord: Boolean, facebook: Boolean,
whatsapp: Boolean, out: Comms): Customer = {
var c: Customer = new NoChannels(name)
if (discord) c = new OnDiscord(c, out)
if (facebook) c = new OnFacebook(c, out)
if (whatsapp) c = new OnWhatsApp(c, out)
c
}The factory admits each platform at most once, matching a subset of preferences. For example,
customer("reader", true, false, true, out) sends once to Discord and once to WhatsApp and has cost per message, using the stipulated charges. The innermost wrapper executes first, giving deterministic Discord–Facebook–WhatsApp order for selected channels. The zero-platform case sends nothing and costs zero. Repeated messages reuse the same immutable choice of wrappers.By structural induction on the wrapper chain, the base sends to the empty subset with zero cost; one additional wrapper preserves all prior sends and adds exactly its channel and charge. Thus the sends and charge are composed from the same selected components. There are channel-specific classes instead of one class per subset, and a new channel needs one new wrapper. A direct manual construction containing the same wrapper twice would intentionally send and charge twice, so the factory prevents that configuration.
cost computes a per-message amount rather than charging by itself; a later payment mechanism can use it. The interface uses Double, so numerical tests should allow ordinary floating-point rounding.