How to avoid repeating code between back-, middle-, and front-ends?

When building a full-stack app, I find it frustrating to define basically 3 nearly identical types:

  1. What I consider the main struct: defining the logic of the type disregarding any GUI/DB needs. This is the struct you’d define normally. This struct would contain only the most basic fields needed to define it “in real life” (e.g. a User will have name, email, and password). Methods associated with it would only deal with actions that have to do with what it actually is.
  2. The back-end struct: same as in #1 but for the database, with the obligatory id and maybe public id for associations with other entities. Methods with this one would be CRUD.
  3. The front-end struct: Again, same as #1 but for the GUI. User-facing with some additional checks and validations.

How do you cope with this duplicity (triplicity)? It isn’t the end of the world, but I keep thinking that if and when I’ll want to change some property of one of the types (probably #1) I’ll need to manually propagate (read replicate) the change to all 3 (nearly identical) implementations.

The specific tools I’m using right now are Genie, Stipple, SQLite, SearchLightMySQL (and later postgres).

Thanks for any insight!

Share the business object in 1) with the other two tiers. The other tiers can wrap it as a member of a struct that adds tier-dependent information.

3 Likes

I do not use Genie, but in ZulipReminderBot.jl I’ve used the following approach:

  1. Define business logic in usual structure
  2. All table related information (like table name, primary key, autoincrement property and so on) define as functions over struct type. The main idea is that all information of this kind is the same for all instances of this type so should be defined only once. Later in code I just use these functions at places where it is necessary.

You probably can simulate the same behaviour with getproperty method if Genie.jl allows to use arbitrary structures.

2 Likes

Thank you for your input!
So, I had a similar idea, but for id this is problematic/impossible:
Say I do this:

abstract type HasID end

struct User <: HasID
  name::String
  email::String
end

Base.getproperty(x::HasID, field::Symbol) = field === :id ? hash(x) : getfield(x, field)

and then I have this for free:

julia> a = User("a", "b")
User("a", "b")

julia> a.name
"a"

julia> a.email
"b"

julia> a.id
0xc537b8713bce82db

the id is then no longer immutable (if a user changes their name/email), and I can’t assign an ID to a user, only read it. So the DB can no longer assign IDs.

How did you solve that, or did I misunderstand you?

Ah, you are right of course. I was thinking about table properties, but surely there are per-object differences in db and business data representation.

In my case I just added id to business struct. I guess in more general case one should do something like

struct User
  name::String
  email::String
end

struct DBRow{T}
  id::Int
  val::T
end

and have smart enough ORM/Database which can represent flat table structure in this form, so upon select you get object of the type DBRow{User} and you can always extract User values with x.val.

This problem looks suspiciously similar to inheritance problem in Julia, so it probably have similar ways of solving.

1 Like

I’m trying to implement @essenciary’s solution suggested in this issue for my own needs now, but it would be great to see some MWE in practice: how exactly we connect the DB (and GUI) to the business data representation.

2 Likes