3

Let's assume in our application we want to model cars. We also want to model a car repository where we store some registered cars. How should that be modeled in scala?

Here comes my approach: First, I create a case class PlainCar. This is just a car how it exists in real world, with nothing special in it. Next I create a CarRepository. I also create a RegisteredCar. The CarRepository can now store PlainCars and return them as RegisteredCars. Both PlainCar and RegisteredCar extend the trait Car which provides all the comon methods like drive. RegisteredCar however is special - it owns an instance of PlainCar but also adds a method registrationNumber which returns the registration number; all other methods are just delegated to the PlainCar instance.

I feel however, that there are some flaws of this design. One is that I have to change RegisteredCar when I add a method/property to PlainCar which I think should not have to be done.

My question is therefore, can this be modeled better and if so, how? Are there other drawbacks that I am missing?

Some advice on naming conventions would also be appreciated because PlainCar sounds quite awkward to me.

0

2 Answers 2

3

It sounds as if registration is a state which only has meaning in the context of a repository. You have described two states, (registered with the repository, not registered with the repository). It sounds to me as if what you need is a car container (wrapper if you like). Think of it as a collection which can hold one car, in the same way that Option[Car] or Either[Car, Motorbike] can only hold one object.

So create a RepositoryItem sealed base trait (or sealed abstract class) and create case class instances (which extend RepositoryItem) for each state. The Registered variant would have a registration number. Make your Repository a container of RepositoryItems. It should not be part of the same set of case classes.

Advantages of this approach include

  • You can make it generic (RepositoryItem[A] or RepositoryItem[+A] rather than RepositoryItem[Car]). This means it could become a repository of other things (or could easily expand to hold motorbikes as well as cars).
  • Complete separation of concerns between repository implementation and car implementation. One can change (or add new features) without affecting the other.
  • You can create new subclasses of Car as often as you like. Even if you create Repository[Car] from the start, rather than making it generic, it will be able to hold Car subclasses with no modification.

Have a look at the way Option and Either are designed. You might want to implement at least map, so that you can manipulate a car (or replace it with a bike) without having to take it out of the repository slot.

EDIT:

To explain a point which may have created confusion, judging by your comment...

The Option and Either types have a map function (as does List and any other monad). The map function allows you to pass in a function which can manipulate the object contained. You get back a transformed object still wrapped in the Option.

scala> val o = Some(3)
o: Some[Int] = Some(3)

scala> o map (_ * 5)
res2: Option[Int] = Some(15)

No dependencies between Option and Int but because Option implements map you can manipulate the Int without removing it from Option wrapping.

6
  • Doesn't that mean, Car is coupled with RepositoryItem? So if I remove the whole repository-thing, I have to change Car because it cannot extend RepositoryItem anymore. I actually feel more comfortable when I am able to ship my Car class even to another project without having to touch it in any way and without having to deliver dependencies like RepositoryItem. But maybe I got you wrong, I am not sure. I get the advantage of having a generic repository though.
    – valenterry
    Commented Jan 14, 2015 at 23:45
  • No. Is Option coupled with int if you create an Option[Int]? Is List coupled with String if you create a list of strings? No coupling at all. You seem to have completely misunderstood my point. I said container, the same way an Option or a List is a container. Cars can be added to those, and removed, without any coupling or redesign. Some(Car) will return an Option[Car] with neither Option nor Car knowing anything about each other.
    – itsbruce
    Commented Jan 14, 2015 at 23:48
  • I missunderstood you here. But I still don't get why I should ever create the NotRegistered(Car) thing in my code. I always now, if I handle registered cars or if I handle with just plain ones. Different to Option, where I often/always not know if it is a Some or None. Also, where would the common car methods be implemented (with method body I mean) - in the sealed trait itself?
    – valenterry
    Commented Jan 15, 2015 at 8:30
  • The common Car methods would go in the Car trait or class. Whatever you call your principal car class, your car classes should not contain Repository details. Whether a car is stored in a repository is not an intrinsic property of the car - it is a property of the repository. Why should the parts of your code which know nothing about repositories have to be aware of repository registration numbers or the different repository-related states? You say you are worried about coupling, but you seem determined to complicate the car class itself.
    – itsbruce
    Commented Jan 15, 2015 at 8:44
  • When I have a Car trait, the rest of my code will just work with Car, so it can work with all kind of cars as long as they are cars. But the code that needs to work with the registration will use RegisteredCar as it has to do checks like carA.regNr == carB.regNr. That means only the code that uses the registrationNumber will know about that RegistratedCar class. But from your answer, I see that that RegisteredCar class would not own a PrincipalCar and delegate to it, but the implementation of the methods would rather be in the common Car trait.
    – valenterry
    Commented Jan 15, 2015 at 9:39
2

I feel like I'm missing something, but I would just make your CarRepository a Map[RegistrationNumber, PlainCar] or something similar. Usually with immutable objects, it's much easier to create this kind of association outside the original object.

My second choice would be RegisteredCar extends PlainCar, but I think this creates unnecessary coupling and copies. Both of those options would allow you to just rename PlainCar to Car.

4
  • Hi, the problem with the Map-approach is, that I want to give a registered car to a function and it should be aware, that this car is registered. I could also go for someFunction(plainCar, registerNumber) but naturally it feels better to group both these arguments into one object, because they belong together - and I called this object RegisteredCar. Extending PlainCar also came to my mind, but I have read in different places that extending a case class is not recommended.
    – valenterry
    Commented Jan 15, 2015 at 7:54
  • @valenterry But how much of your code cares about (or should know about) repositories? What happens if you have to create some other context, in a different part of the code, which contains extra information or state about cars? Will you add new case classes? You couldn't, because those states would be orthogonal to repository states.
    – itsbruce
    Commented Jan 15, 2015 at 8:57
  • Well, most code will just work with the plain Car, but some code knows about that a car can be registrated and has to work with their registration number, so it will work with RegisteredCar. What do you mean by extra state? Like a second repository where cars can be registrated in?
    – valenterry
    Commented Jan 15, 2015 at 9:41
  • Multiple repositories would be another reason for considering containers, but I was thinking of other domains with other concerns. Imagine you want to track the insurance status and tax registration of cars; these are things which change (new regulations, changes n cost). I don't think those states should be held in the car class.
    – itsbruce
    Commented Jan 15, 2015 at 12:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.