2

I have the following problem (in Scala)...

I want to read wavefront files (.obj) and transform them to something I can work with later. The wavefront files I want to support are files with the following definitions for:

  • TypeA: vertices and faces
  • TypeB: vertices, texture and faces
  • TypeC: vertices, normals and faces
  • TypeD: vertices, textures, normals and faces

I will read them and create a Mesh (a model class for later use) of it with the following fields:

  • TypeA: Array[Float], Array[Int]
  • TypeB: Array[Float], Array[Float], Array[Int]
  • TypeC: Array[Float], Array[Float], Array[Int]
  • TypeD: Array[Float], Array[Float], Array[Float], Array[Int]

I discovered two approaches:

1. Approach:

each type gets it's own model class

  • TypeA: case class TypeA(vertices: Array[Float], index: Array[Float])
  • TypeB: case class TypeB(vertices: Array[Float], textures: Array[Float], index: Array[Float])
  • TypeC: case class TypeC(vertices: Array[Float], normals: Array[Float], index: Array[Float])
  • TypeD: case class TypeD(vertices: Array[Float], textures: Array[Float], normals: Array[Float], index: Array[Float])

With this approach I don't have to check if all fields are present. I can use them out of the box. The disadvantage is: I need to create a "build"-method for each type (something like: createTypeAFromFile(filename: String))

2. Approach I create something like a uber-model:

case class Mesh(vertices: Array[Float], textures: Option[Array[Float]], normals: Option[Array[Float]], index: Array[Float])

With this approach I only need one "build"-method, but the problem here, later I have to check if the fields I want to use are really present (for normals and textures)

Question:

Does anyone knows a better approach/design, for this kind of problem?

Krzysztof Atłasik
  • 21,985
  • 6
  • 54
  • 76
SleepyX667
  • 670
  • 9
  • 21

2 Answers2

2

It normally helps if you know the operations you will perform on the types, a good blog post is: https://www.sandimetz.com/blog/2016/1/20/the-wrong-abstraction where the author argues that wrong abstraction is way worse than having some duplication.

Go with approach 1, maybe create a trait for all of them, and you will see if you can group them into types and if it is worth it.

2

[Updates thanks to @simpadjo ]

Approach 3 is to create a trait hierarchy to represent the different options:

trait Faces {
  def vertices: Array[Float]
  def faces: Array[Float]
}
trait Textures extends Faces {
  def textures: Array[Float]
}
trait Normals extends Faces {
  def normals: Array[Float]
}
trait Obj3D extends Textures with Normals 

class A(
  val vertices: Array[Float],
  val faces: Array[Float]
) extends Faces

class B(
  val vertices: Array[Float],
  val textures: Array[Float],
  val faces: Array[Float]
) extends Textures

class C(
  val vertices: Array[Float],
  val normals: Array[Float],
  val faces: Array[Float]
) extends Normals

class D(
  val vertices: Array[Float],
  val textures: Array[Float],
  val normals: Array[Float],
  val faces: Array[Float]
) extends Obj3D

The parser can return Faces but it creates the appropriate subclass if the additional fields are available. You can then create a Mesh of Faces but use match to detect the case where additional information is available.

Tim
  • 26,753
  • 2
  • 16
  • 29
  • 1
    Like it. One comment: since there is no implementation inheritance (which is good) `trait Textures extends Faces` looks less arcane for me than `trait Textures { Faces =>` . Also declarations of concrete classes become shorter: `class D extends Normals` – simpadjo Nov 25 '19 at 11:14
  • I don't get it completely... why do I need a `Obj3D` trait? Would it be sufficient to have `classD(val vertices ...) extends Faces with Texture with Normals`? (and why needs `Textures` and `Normals` extends `Faces` --> example `class B(val vertices) extends Faces with with Textures`) – SleepyX667 Nov 26 '19 at 15:41
  • The reason for the different traits is so that you can use them later to say type which type you need. e.g `def reverseNormals(norms: Normals)` would only work on `C` or `D`. Likewise you can `match` on the specific type you need. – Tim Nov 26 '19 at 16:15