There's famous Design Patterns for OOP book. Wonder if there's Design Patterns for Nim?
A collection of patterns and solutions for common architectural problems.
Nim is mix of functional/OOP and so if you already know any functional / OOP language - you will be able to use Nim right away. Yet, Nim also has some unique features, like methods etc. and it's usage is different and not immediately obvious.
For example - say you need to provide slightly different behaviour for set of similar objects - animals, dog say "woff" and cat say "meow", used as animal.hi().
Should you use object hierarchy with method hi(dog: Dog) to allow custom implementation? Or should you use a single parameterised Animal object Animal(hi: dog_hi)? And so on.
Reading Design Patterns helps to expand the understanding of the language and know better what solution are available and which one would be a better fit for your situation.
I'm a firm believer that the design patterns as popularized by the Java/C++/C# folks reveal flaws in the language design and not how to address architecture problems.
Flaws like:
There are very few that I genuinely consider actually addressing architecture instead of language flaws:
I'm a firm believer that the design patterns as popularized by the Java/C++/C# folks reveal flaws in the language design and not how to address architecture problems.
Agreed. Many OOP design patterns in Java are the boilerplate code needed to compensate for lack of language features. Also (especially in Java) after you read Design Patterns, it's important to know when not to use it, and avoid writing code like factory.mediator.adapter.facade.builder.build().doSomething() :).
Yet, I feel it's very useful to read Design Patterns to improve your understanding of language capabilities. Maybe you should call it not Patterns but like "Solutions of Common Problems".
It's kinda like a short and effective way of reviewing tens of real world programs. But instead of spending lots of time reading actual code of actual programs, you look at its simplified skeleton version in short example in form of Pattern/Solution.
Here are some interesting skeletons
Thanks, good reading for weekends :)
I think a common name for a concept, one that's descriptive (NOT prescriptive), is very useful - for example, "hash function" or "hash table" or "lexicographical stable sort" or "gossip protocol" are extremely useful for human communication and documentation. They cannot be implemented as macros/templates etc, because they are too general (and even Nim's "concept" which comes closer isn't enough). Concrete, specific versions may be implemented, of course, but that's just a type/class/generic; no need for a new "pattern" distinction which adds nothing.
This is, I think, the equivalent to what Alexander's original "Patterns" referred to (w.r.t city architecture), and what the software patterns e.g. as advocated by the GoF claim to do - but as @mratsim says, just show how limited C++/C#/Java/Friends are.
for example, "hash function" or "hash table" or "lexicographical stable sort" or "gossip protocol"
Those are not design patterns, just… programming terms.
Concrete, specific versions may be implemented,
That's implied by the word “implement”. Of course you can't implement something abstract. But when someone says “implement a hash table”, it means a concrete version of a hash table.
We write AI for StarCraft. Each Player has lots of different objects, like Marine unit and Building.
Inheritance is not a good option here, because we going to have lots of procedures working with specific types, and don't need extra complexities like method.
Yet, those objects still share some common things, and sometimes we need to apply the same operation to all of them.
Nim can't store different objects in same collection, but instead the macros could be used:
import sugar, sequtils
type
Marine = object
id: string
Building = object
id: string
Player = object
id: string
marines: seq[Marine]
buildings: seq[Building]
template map*(player: Player, operation: untyped): untyped =
player.marines.map(operation) & player.buildings.map(operation)
let alex = Player(
id: "Alex",
marines: @[Marine(id: "Jim")],
buildings: @[Building(id: "Barrack")]
)
echo alex.map((o) => o.id) Here's a powerful design pattern you can use in Nim: Just write imperative code unless there's a real advantage to using more indirection and slower methods. In the above snippet, the Waiter type represents nothing. The constructPizza proc does nothing. Leave them out, and just call the appropriate proc to create the correct Pizza object directly.
100% - most of the old school Java OO patterns were mostly just work arounds for Java's limitations as a language lacking Closures and case/adt types. I don't think the builder stuff really separates object construction from data representation.
Sometimes it's handy to use something like a delegator pattern though but even then there's often less need for such patterns in Nim. Instead you just pass a callback like constructPizza but why do that until you need to since you don't need to subclass anything to do it? Instead just create the object directly and use enums and a case type if needed for pizza.
Thank you for taking the time to read my post and making a criticism. I provide too few explanations of what I understood from the OOP pattern.
The OOP patterns were not designed to reveal flaws in the languages (not what you told but I want to be precise here) but to provide separation of concerns and recipes to detect and fix (de)coupling.
My example is scholar and limited. It only shows the construction of a Pizza object and nothing more. It doesn't deal with taking orders, shipping pizzas to a client. The bigger a program gets, the more sense could be attributed to encapsulation. Maybe the Waiter would need to query a database to know which ingredients are available, and might only create the pizza if there are enough ingredients. There, I can find a sense to create a Waiter.
The example might also illustrates that in Java, procedures have to be grouped into a class. With more complex industrial applications, I am not sure if naming a class similar to the Waiter would always be possible.
When I said that the second Nim translation does not separate the object construction from the data representation of the object, I meant that changing a Pizza from e.g. an object to an array, a dict or a map was requiring changes in the algorithm of object construction (constructPizza and not the concrete procedures themselves) only in the second example. The correct decoupling to not change the concrete procedures would be to create field mutators.
Example:
type
Ingredients = enum
Dough, sauce, topping
Pizza = array[Ingredients, string]
var spicyPizza: Pizza = ["flaky", "spicy", "pepperoni"]
echo spicyPizza[sauce]
Here, we want to change the methods to:
method buildDough*(self: SpicyPizzaBuilder) =
self.pizza[dough] = "flaky"
method buildSauce*(self: SpicyPizzaBuilder) =
self.pizza[sauce] = "spicy"
method buildTopping*(self: SpicyPizzaBuilder) =
self.pizza[topping] = "pepperoni+salami" We don't need to change the proc constructPizza method though. The procedure doesn't care about the existence of fields neither. If one pizza doesn't have a topping, it suffices to do nothing in the concrete procedure. Conversely, if one needs to add pepper to pizzas or to change the order of instructions like sauce or topping, or if one pizza doesn't have toppings, changing the algorithm doesn't require the pizza to suddenly become a HashMap. Two developers may work separately on each part, one on the abstract and concrete procedures, the other in the building recipe. If accessors are added, a third one may focus on implementing the concrete procedures.
Now, how would you get rid of such coupling with a procedural approach?
proc spicyPizza*(): Pizza =
Pizza(dough: "flaky",
sauce: "spicy",
topping: "pepperoni+salami") This function forces Pizza to be an object with three fields: dough, sauce and topping. Each one of them are Strings. I guess one would need to create three mutators functions setDough, setSauce and setTopping.
Now creating coupling is also interesting in certain situations. The criticism I have with the snippet containing two calls directly to the procedures queenPizza and spicyPizza is that we are not sure whether they are Pizzas anymore. They might not follow the specification given by a common build recipe. The direct Java translation ensures that the sauce is not put before the dough is made. I guess this would still be possible in a procedural world but it is hard to write. I am unsure how to enforce that the body of a procedure follows a strict instruction order. The only solution would be to have concrete functions returning an array of dough, sauce, toppings and another function to build the pizza from the array.
With these explanations in mind, do you think I am still lost in design patterns madness? :)
I am interested with any of your long-term developer experience. I am teaching (practice sessions, not lectures) programming in Java at some Uni. I would be happy to co-write a blog post illustrating better patterns in Nim.
I don't know much about design patterns, so I won't discuss it here. The code below is a translation of the original Java code using ref objects and closures to simulate interfaces.
type
Pizza = object
dough: string
sauce: string
topping: string
IPizzaBuilder = ref object
get_pizza: proc(): Pizza
create_new_pizza: proc()
build_dough: proc()
build_sauce: proc()
build_topping: proc()
QueePizzaBuilder = ref object
pizza: Pizza
SpicyPizzaBuilder = ref object
pizza: Pizza
Waiter = object
builder: IPizzaBuilder
proc create_new_pizza(self: QueePizzaBuilder) =
self.pizza = Pizza()
proc build_dough(self: QueePizzaBuilder) =
self.pizza.dough = "thick crust"
proc build_sauce(self: QueePizzaBuilder) =
self.pizza.sauce = "mild"
proc build_topping(self: QueePizzaBuilder) =
self.pizza.topping = "ham + mushroom"
proc iface(self: QueePizzaBuilder): IPizzaBuilder =
proc get_pizza(): Pizza =
self.pizza
proc create_new_pizza() =
self.create_new_pizza()
proc build_dough() =
self.build_dough()
proc build_sauce() =
self.build_sauce()
proc build_topping() =
self.build_topping()
result = IPizzaBuilder(
get_pizza: get_pizza,
create_new_pizza: create_new_pizza,
build_dough: build_dough,
build_sauce: build_sauce,
build_topping: build_topping,
)
proc create_new_pizza(self: SpicyPizzaBuilder) =
self.pizza = Pizza()
proc build_dough(self: SpicyPizzaBuilder) =
self.pizza.dough = "flacky"
proc build_sauce(self: SpicyPizzaBuilder) =
self.pizza.sauce = "spicy"
proc build_topping(self: SpicyPizzaBuilder) =
self.pizza.topping = "pepperoni + salami"
proc iface(self: SpicyPizzaBuilder): IPizzaBuilder =
proc get_pizza(): Pizza =
self.pizza
proc create_new_pizza() =
self.create_new_pizza()
proc build_dough() =
self.build_dough()
proc build_sauce() =
self.build_sauce()
proc build_topping() =
self.build_topping()
result = IPizzaBuilder(
get_pizza: get_pizza,
create_new_pizza: create_new_pizza,
build_dough: build_dough,
build_sauce: build_sauce,
build_topping: build_topping,
)
proc init(_: type Waiter): Waiter =
Waiter(builder: nil)
proc `pizza_builder=`(self: var Waiter, builder: IPizzaBuilder) =
self.builder = builder
proc construct_pizza(self: Waiter) =
self.builder.create_new_pizza()
self.builder.build_dough()
self.builder.build_sauce()
self.builder.build_topping()
proc get_pizza(self: Waiter): Pizza =
self.builder.get_pizza()
when isMainModule:
var waiter = Waiter.init()
var queen_pizza_builder = QueePizzaBuilder()
var spicy_pizza_builder = SpicyPizzaBuilder()
waiter.pizza_builder = queen_pizza_builder.iface()
waiter.construct_pizza()
let pizza = waiter.get_pizza()
echo pizza.dough
echo pizza.sauce
echo pizza.topping
With these explanations in mind, do you think I am still lost in design patterns madness? :)
I am interested with any of your long-term developer experience. I am teaching (practice sessions, not lectures) programming in Java at some Uni. I would be happy to co-write a blog post illustrating better patterns in Nim.
Buy my book, read the chapters about macros. Teach them these. It's not OOP but then why do your students need OOP when they can learn meta-programming instead.
I am interested with any of your long-term developer experience.
My experience is the less time developers spend thinking about patterns, and the more they are thinking about the actual problem they are solving, the better.
Its funny that the slowest, most brittle, and hardest to comprehend code I've seen was produced by software architects and academics...
So the Builder pattern looks difficult to implement without methods.
Well, yes, because it's inherent to OOPS programming.
The Builder pattern is used in Rust all the time, and Rust is (mostly) not OO. To be clear, by OO I mean, "has some kind of runtime type dispatch on subtypes"; Rust use of the Builder does not involve "dyn Trait". Nim easily supports this non-OO use of Builder via UFCS. Nim also needs it far less because Nim has named and default arguments, which Rust lacks, but if you want to write Builder in Nim as in Rust it's easy: create a FooBuilder for your object type Foo which has the same fields as Foo (or optionals) and a build method you call at the end that returns the Foo.
In my experience with Java, I don’t remember the builder pattern requiring a strict order of setter calls. You can swap setDough and setSauce freely before calling build().
In Nim, you don’t need OOP to implement a builder pattern:
If you’re just setting fields directly, you can use an object constructor:
Pizza(dough: "flat", sauce: "tomato", toppings: @["pineapple", "chicken"])
I have a real use case where this pattern makes sense. I was writing bindings for io_uring, and all interactions with the kernel happen through passing large structs that represent different operations depending on which fields are set. In this context, the builder pattern works perfectly for hiding low-level details — the user of the library doesn’t need to know which fields to set or how to align the data correctly. At the same time, a single helper like makeWriteCmd() isn’t enough, because you might also need to set various flags — for example, whether the command should block until completion or allow the next commands to start immediately.