Fork me on GitHub
#beginners
<
2023-01-14
>
Serafeim Papastefanos14:01:42

Hey friends! Is there a sample open source web app using clojure both in back and in front end that follows best practices and can be used as a starting point for our own projects?

practicalli-johnny16:01:20

Generate a project from a template using https://kit-clj.github.io/ and try some of the different options to see how different libraries combine. Note: there are no 'best' practices, only relevant practices...

Rupert (All Street)19:01:07

I agree with the statement that no one repo covers "best practice", since the Clojure ecosystem is much more fragmented than that (in a good way!). On the server side you will likely use libraries for: • Dependency injection framework (e.g. integrant) • server (e.g. jetty) • Ring middleware (e.g. compression, content types) • JSON serialisation (e.g. jsonista/) • HTML generation (e.g. hiccup) • Authentication library • Server routing (e.g. compojure) • Database (e.g. datomic) If you are doing client side rendering, you may also need: • HTML rendering (e.g. uix, reframe) • HTTP Client (e.g. http • Dendendency injection (e.g. integrant) • UI Widget library (e.g. antd) • Client side routing • Serialisation Overall, there are many good options and it depends on your usecase and even subjective preferences.

Serafeim Papastefanos10:01:30

Thank you very much for the tips Ill check the out !

👍 2
peterh20:01:51

You might get some inspiration from the actively maintained and quite successful knowledge management / notebook app logseq, which has an open source repo: https://github.com/logseq/logseq In the same space, there is also Athens, which was heavily inspired by Roam Research (another successful, though not open source, knowledge management app written in Clojure), but is no longer actively maintained (although still quite recent): https://github.com/athensresearch/athens Not sure if that helps in any way and if they are using “best practices” but I always come back to them as real world examples of Clojure(script) apps.

Serafeim Papastefanos15:01:15

Thank you these are really useful!

Hans Lux20:01:08

How to enforce business rules on defrecord? A beginners question. Say I have a todo app. A Todo entity has a topic and a description. The Todo entity is a defrecord. (It probably doesn't need to be since it has no functions yet and does not need polymorphism) Also say there is a business rule that says that descriptions may only start with the letter "a". I could implement this with a function in the namespace todo. change-description [todo], which ensures compliance with the business rule. But how can I prevent a fellow developer erroneously using assoc directly on the defrecord to change the description, for example in a use case doing (assoc todo :description "newDesc") ?

hiredman20:01:47

In general, don't use defrecord. Use regular maps, you can use spec to validate that some set of predicates are true for a given map

hiredman20:01:38

"business objects" should pretty much always be regular maps

Hans Lux20:01:04

thank you for your answer. The validation with specs wouldn't happen in the given case where assoc is used directly on the Todo or map would it? That validation could only happen "after the fact" somewhen couldn't it? I wonder what the best practice is to ensure business rules, like the one given in the example.

hiredman20:01:34

The best practices are if you care about something, then verify that it is true

hiredman21:01:45

The most common cases for verification of that kind of thing are at system boundaries (coming in over the wire, going out to disk, etc)

hiredman21:01:24

You can verify those constraints internally, but that is less common

moe21:01:06

You would have to do something insane (implement some low-level protocols) to actually enforce the constraints at the point they values are assoc'ed onto the map/record, or to prevent that from happening

moe21:01:16

@U0NCTKEV8's advice about checking this stuff at boundaries makes sense. it probably doesn't matter if there is a todo with a description which doesn't start with an "e" for a while, until you want to do something with it

Hans Lux21:01:53

That, for example, would be the function that saves a todo to the database. In my opinion, that function is not the right place to know about the business rules of the application or domain. Storing a todo could happen asynchronously, but feedback to the user might need to be immediately.

moe21:01:07

why not enforce it at the point where you're accepting the user input?

moe21:01:21

(I would put it in both places)

Hans Lux21:01:04

user input could come from very different places. I had to repeat enforcement of that rule in each and keep it in sync. That enforcement may also be in placces I don't trust, like a users browser.

moe21:01:01

I mean where you're accepting user input in your backend. If you're accepting it in multiple places, just use the same spec to ensure it conforms to expectations.

Hans Lux21:01:08

I think that would be a good place for business rules of the application layer. I was looking for a solution that does that for the domain layer. In OO I could design a Todo class, that has a changeDescription() method, which ensures the constaints and there is no other way to accidently bypass these constraints. I was wondering whether there is a typical pattern or best practice in clojure or functional programming in general, that achieves the same.

skylize21:01:29

The Clojure language does not generally offer much in the way of such guard rails, nor assistance with creating them. "We are all adults here." Create and offer tools for transforming your data in the desired way. Trust yourself to use those tools appropriately. Later, when you discover an unplanned use case, and adding a new interface to your data does not involve tons of refactoring to uncage it, remember to be grateful the language made it difficult to box yourself into a corner.

Hans Lux22:01:00

thank you for your answer. So the habit/practice is, that if i want to do anything with a todo (or any other entity) I inspect the corresponding namespace first to see what it has to offer?

skylize23:01:31

Yes. Assuming you don't have excellent docs 😉, reading through a required namespace is common.

skylize23:01:43

Slight tangent: You will often see defn- used to define "private" functions. This increases friction for external use, but by no means prevents it. The popular reason for using defn- is not about causing that friction, but rather as a self-documenting signal to a reader (person) they can mostly ignore this function while working out the external API.

skylize00:01:39

A couple more things to keep in mind about your original question: • Because of immutable data structures, there is less inherent need for this particular type of encapsulation in Clojure than other languages. If someone "alters" the data in an "undesirable" way, that is their own problem. It will not break your code, that accesses the original source, like it might with a mutable object. • If you assoc a Record, you still get a Record back. So if someone falls back on using more core features, that doesn't necessarily mean they lose out on your choice to use a Record instead of a Map. (Same is not true if they dissoc one of the Record fields.)

👍 2
emccue01:01:24

@U04HGPMQJJG I wouldn't >prevent< so much as >mechanically discourage<

emccue01:01:57

just use namespaced keys

emccue02:01:39

(ns domain.todo)

(defn create
  [{:keys [title description]}]
  {::title title
   ::description description})

emccue02:01:49

this creates enough friction to discourage messing with the data you don't know about the constraints of

emccue02:01:11

so your update method that maintains some invariant

(ns domain.todo)

(defn create
  [{:keys [title description]}]
  {::title title
   ::description description})

(defn update-description
  [todo args]
  (update todo ::description (f todo args)))

emccue02:01:21

or expose a schema which encode the invariants. makes them public, but knowledge of what they are exactly is passed around "nominally"

didibus04:01:40

You need to change your thinking a bit. The record is immutable, it cannot be corrupted after creation. You cannot change the description on it. If you could, you would use deftype, and that supports encapsulated fields similar to say Java. So what you're trying to protect against is probably more like making sure that what you insert in the DB or return to the user is a valid todo. You would just have a spec for example in that case, and before inserting in the DB or returning to the user, you would say: is what I'm about to insert in the DB or return to the user a valid todo?

didibus04:01:37

A fellow developer would be allowed to create another record out of yours, one which is not a valid todo, but they would not be allowed to insert that in the DB or return it to the user.

didibus04:01:16

> In OO I could design a Todo class, that has a changeDescription() method, which ensures the constaints and there is no other way to accidently bypass these constraints. There are ways: 1. you can extend the class and override the changeDescription. 2. you can add another method to the class that changes the description field in an invalid way. 3. you could use reflection to change the field. 4. you could be using a language where private fields are just a convention and people can change the fields directly. I've seen 1 and 2 many times introduce bugs in code bases because people did that, especially 2.

didibus05:01:17

But also, don't overthink it too much. Like others said:

(ns app.model.todo
  (:require [clojure.spec.alpha :as s]))

(s/def :todo/description
  (s/and string? #(not (= "a" (first %)))))

(s/def :todo/todo
  (s/keys :req-un [:todo/description]))

(defn make-todo
  [description]
  (s/assert :todo/todo
            {:description description}))

(defn change-description
  [todo description]
  (s/assert :todo/todo
            (assoc todo :description description)))
Other developers are not stupid, it's pretty obvious that functions to manipulate todo are inside the todo namespace.

didibus05:01:13

You can look at: https://github.com/didibus/clj-ddd-example for a more complete example of DDD in Clojure

Hans Lux08:01:57

Thank you very much for your input. I'm well aware of the immutability of records and maps. I'm just used to the idea, that it is "best" to not let invalid entities come into existence in the first place. The intention to guide fellow developers and don't prevent so much appeals to me. I was just wondering how that is best achieved if all developers are new to clojure or less experienced. I am convinced that a combination of a well structured API, documentation and namespaced keys make sure, provide sufficient guidance for users of an API. Tests are presumably a further factor to complete that guidance. You Input and examples are very motivating and helpful. @U0K064KQV thanks for pointing out your ddd example. I've come to like DDD very much and would love to implement my projects with DDD in clojure.

moe08:01:27

with records (I know people told you not to use records, but I'm a huge fan of records — maybe not for todos, but for polymorphism), you have the ->RecordTypeName generated function (which you're free to re-define), and it's extremely bad form to break the record abstraction outside of the record's namespace

moe09:01:26

i.e. by associng. you're not prevented from doing it, it' just something which should not ever be done

phill11:01:52

Alan Perlis "It is better to have 100 functions operate on one data structure than 10 functions on 10 data structures" is a pithy explanation. Information, kept in maps, is adjusted not only with assoc, but also dissoc, select-keys, merge, conj, into, filter, update-keys, etc. Where to put the guards? Not in any of them, because... A huge benefit of immutable structures is that business rules matter only at boundaries. It's OK to make a two-step adjustment of your business object, causing a rule to be dishonored for 1 millisecond, because nowhere in the program is there a pointer whereby the mutated object could inadvertently be used. Let's also remember that business rules are made to be broken. Businesses rely on it. YOU mustn't make a todo that begins with "x", but you may call a Supervisor and THEY can somehow do it. Enforcement is the program's duty, but it need not, ought not be very tightly associated with the business object. Perhaps the "a" rule applies only to todo's transmitted by EBCDIC, which most are but a few are not?

👍 2
didibus19:01:12

In DDD, even in OOP, there are business rules that can't be enforced on the entity itself, the ones that are across entities. For those, you have a domain service that takes two or more entities and make sure to change both of them in a transactionally consistent manner. If you look at my repo example, that's what is happening with transfer-money. Now if you were in OOP, an unknowing developer might get two accounts and call credit and debit on each, not realizing the transaction should be done with the domain service transfer-money functions in order to apply the valid business rules around transferring of money. My point is, like others have said, you can't really protect yourself against a developer making a code change that doesn't know what they are doing and how to make the change correctly. What you would do instead, is create a bounded context, make your account management its own micro-service, have developers work on that code base that are familiar with the domain of that bounded context and the business rules, and expose APIs to other contexts where the developers are not as familiar with the domain of account money management. What you can do though, is have a clean code base, so it is easy to ramp up a new developer to understand how to make code changes correctly and where the business rules are defined, and what to be careful about.

didibus19:01:07

There are also a set of non-functional rules that need to be taken care of correctly, like making sure your DB updates are done atomically, or that you don't break the DB model, etc. You simply will not be able to guard against all those kind of mistakes. Good onboarding processes, mentors, code review processes, test coverage, and wikis/documentation will be your best bet here. Along with a clean code base that is easy to figure out where things are done and how it is all organized.

didibus19:01:56

Also, if you look at my example, I take it a step further, and the model doesn't actually change the state, the domain model and services are not supposed to represent the state of your app, the state is stored elsewhere, like in a database, but they are supposed to tell you if a particular state change is allowed and how to go about making that change. That's why the functions actually don't return you a modified map, but they return you a domain event, that captures valid state changes that you can then go and apply to the application real state.

didibus19:01:30

The way to think about it is: 1. Get the app state 2. Ask the domain model and services: Can I modify the app state in that way? 3. Get an answer back from the model/services as either NO you can't. Or YES you can, here's the valid change that results from this action. 4. Apply the valid change to the app state Where the app state is meant to be part of the Infrastructure layer, so inside the repository, or some other stateful component. That way your domain model and services are stateless and pure. That also means you should not be worried that someone changes the "state" of the domain model incorrectly, because there is not state to be changed there. What you should be worried is that someone changes the app state (where it actually lives, like the DB), without having first checked with the domain service/model if it was allowed.

Hans Lux22:01:05

I agree that OOP does not prevent developers from making mistakes. There are certain practices and techniques as well as language specifics that support developers in doing the right thing. But as you saif, that heavily depends on how clean and well structured the code base is. What I am here for is to learn what practices and techniques there are in clojure and functional programming in general to achieve this. Well desingned namespaces with documented function and specs are two compelling arguments. Unit tests would be another in my opinion, but that does not seem to be as much of a factor in languages that provide "repl driven" development. Another (beginners) question that came up while studying @U0K064KQV ddd example was why you use the threading operator to access a map by key like (-> account :number) instead of (account :number) or even (:number account)? Is that a matter of taste, a certain style or a hint to the compiler ? I find that ddd example of yours really informative and easy to follow. Issuing events to create, update and track application state is another topic of its own, but clojure's rich set of functions to work with sequences seems to lend itself particularly well suited for that task.

didibus00:01:07

> Is that a matter of taste, a certain style or a hint fro the compiler ? Just a matter of taste. If you needed to get something nested it extends to (-> account :number :value) by just adding more keys. And it works equally for Java interop: (-> javaObject .getSomething .getSomethingElse) (-> account :number) is the same as (:number account) , the former expands into the latter, and they'll behave the same. That said, (account :number) is different, in that it will throw in the case account is nil. The others will return nil instead.

skylize00:01:13

Of course if you have the symbol number instead of the keyword :number, and there is any possibility that it might be an index instead of key, then it must be (account number) ( or I guess (-> number account) ? ). :man-shrugging:

Hans Lux15:01:39

I'm fiddling around with specs and am quite happy with the results. It took me a while to figure out that you have to enable "assertion checking". @U0K064KQV Maybe it worth a quick note in the otherwise detailed documentation in your ddd example. (at least for beginners... I suppose)