Skip to content

Commit 3ed047d

Browse files
authored
Merge pull request mouredev#4720 from luishendrix92/main
#26, #27 - OCaml
2 parents 1bcb6a5 + 7c08b68 commit 3ed047d

File tree

2 files changed

+479
-0
lines changed

2 files changed

+479
-0
lines changed
Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
open Printf
2+
3+
let ( let* ) = Result.bind
4+
5+
(******************************************************************************)
6+
(* *)
7+
(* Single Responsibility Principle *)
8+
(* *)
9+
(* The 'S' in {b SOLID}, says that classes and functions should only do one *)
10+
(* thing and they should do it very well; meaning, have a single responsi- *)
11+
(* bility. It discourages mixing concerns in a class or function and *)
12+
(* encourages the developer to separate these concerns into classes that *)
13+
(* only {e have one reason to change}, as Bob Martin said in his book, *)
14+
(* Clean Code, which is the originator of the SOLID principles. *)
15+
(* *)
16+
(******************************************************************************)
17+
18+
module ViolatesSRP = struct
19+
let responsibility_1 () = print_endline "I do one thing"
20+
let responsibility_2 () = print_endline "I do another thing"
21+
let responsibility_3 () = print_endline "And yet another thing..."
22+
23+
let use_all_three () =
24+
responsibility_1 ();
25+
responsibility_2 ();
26+
responsibility_3 ()
27+
;;
28+
end
29+
30+
(* The module above is in violation of the SRP, it defines three different
31+
functions that should exist on their own, otherwise whenever we wanted
32+
to change any of them, we'd have to modify the same module, potentially
33+
introducing bugs and making them exclusive to the host module.
34+
35+
The solution is to create a module for each responsibility so that if
36+
such code has to change, I know exactly where to go. *)
37+
38+
module ResponsibilityOne = struct
39+
let one_thing () = print_endline "I do one thing"
40+
end
41+
42+
module ResponsibilityTwo = struct
43+
let another_thing () = print_endline "I do another thing"
44+
end
45+
46+
module ResponsibilityThree = struct
47+
let yet_another () = print_endline "And yet another thing..."
48+
end
49+
50+
module SRPCompliant = struct
51+
let use_all_three () =
52+
ResponsibilityOne.one_thing ();
53+
ResponsibilityTwo.another_thing ();
54+
ResponsibilityThree.yet_another ()
55+
;;
56+
end
57+
58+
(*****************************************************************************)
59+
(* *)
60+
(* DIFICULTAD EXTRA (opcional): *)
61+
(* *)
62+
(* Desarrolla un sistema de gestión para una biblioteca. El sistema necesita *)
63+
(* manejar diferentes aspectos como el registro de libros, la gestión de *)
64+
(* usuarios y el procesamiento de préstamos de libros. *)
65+
(* *)
66+
(* Requisitos: *)
67+
(* 1. Registrar libros: El sistema debe permitir agregar nuevos libros con *)
68+
(* información básica como título, autor y número de copias disponibles. *)
69+
(* 2. Registrar usuarios: Debe permitir agregar nuevos usuarios con info *)
70+
(* básica como nombre, número de identificación y correo electrónico. *)
71+
(* 3. Procesar préstamos de libros: El sistema debe permitir a los usuarios *)
72+
(* tomar prestados y devolver libros. *)
73+
(* *)
74+
(* Instrucciones: *)
75+
(* 1. Diseña una clase que no cumple: Crea una clase Library que maneje *)
76+
(* los tres aspectos mencionados anteriormente (registro de libros, *)
77+
(* usuarios y procesamiento de préstamos). *)
78+
(* 2. Refactoriza el código: Separa las responsabilidades en diferentes *)
79+
(* clases siguiendo el Principio de Responsabilidad Única. *)
80+
(* *)
81+
(*****************************************************************************)
82+
83+
module Library = struct
84+
type book =
85+
{ title : string
86+
; author : string
87+
; mutable stock : int
88+
}
89+
90+
type user =
91+
{ id : int
92+
; name : string
93+
; email : string
94+
}
95+
96+
module LendingSet = Set.Make (struct
97+
type t = int * string
98+
99+
let compare = Stdlib.compare
100+
end)
101+
102+
let books : book list ref =
103+
ref
104+
[ { title = "Dune"; author = "Frank Herbert"; stock = 5 }
105+
; { title = "Lord of the Rings"; author = "J.R.R Tolkien"; stock = 2 }
106+
; { title = "Eye of the world"; author = "Robert Jordan"; stock = 3 }
107+
; { title = "It"; author = "Stephen King"; stock = 8 }
108+
; { title = "Develop a second brain"; author = "Thiago Forte"; stock = 6 }
109+
]
110+
;;
111+
112+
let users : user list ref =
113+
ref [ { id = 1; name = "Luis Lopez"; email = "[email protected]" } ]
114+
;;
115+
116+
let lendings : LendingSet.t ref = ref LendingSet.empty
117+
let register_user user = users := user :: !users
118+
let add_book book = books := book :: !books
119+
120+
let lend_book user_id book_title =
121+
let book =
122+
match List.find_opt (fun book -> book.title = book_title) !books with
123+
| None -> failwith (sprintf "Book [%s] not found" book_title)
124+
| Some book -> book
125+
in
126+
if book.stock > 0
127+
then begin
128+
lendings := LendingSet.add (user_id, book_title) !lendings;
129+
book.stock <- book.stock - 1
130+
end
131+
else failwith (sprintf "Not enough stock for [%s]" book_title)
132+
;;
133+
134+
let return_book user_id book_title =
135+
let should_return = LendingSet.mem (user_id, book_title) !lendings in
136+
if should_return
137+
then begin
138+
let book =
139+
match List.find_opt (fun book -> book.title = book_title) !books with
140+
| None -> failwith (sprintf "Book [%s] not found" book_title)
141+
| Some book -> book
142+
in
143+
lendings := LendingSet.remove (user_id, book_title) !lendings;
144+
book.stock <- book.stock + 1
145+
end
146+
else
147+
failwith
148+
(sprintf "User #%d doesn't need to return [%s]" user_id book_title)
149+
;;
150+
end
151+
152+
(* Refactoring Opportunity
153+
-----------------------
154+
I can apply SRP to separate the monolith of code into modules that do one
155+
thing only, and very well. For this particular case I can implement an
156+
entity data module for books, lendings, and users; then create a repository
157+
functor to have a static storage (Hashtbl) interfaced through convenient
158+
functions for retrieving, deleting, and adding these entities. *)
159+
160+
module type Entity = sig
161+
type id
162+
type t
163+
164+
val get_id : t -> id
165+
end
166+
167+
module Repository (E : Entity) = struct
168+
let data : (E.id, E.t) Hashtbl.t = Hashtbl.create 100
169+
let save elt = Hashtbl.replace data (E.get_id elt) elt
170+
171+
let add elt =
172+
if Hashtbl.mem data (E.get_id elt)
173+
then Error "Unable to add entity, already exists."
174+
else (
175+
save elt;
176+
Ok ())
177+
;;
178+
179+
let delete_by_id elt_id =
180+
if Hashtbl.mem data elt_id
181+
then Ok (Hashtbl.remove data elt_id)
182+
else Error "Can't delete, id doesn't exit."
183+
;;
184+
185+
let get_by_id elt_id =
186+
match Hashtbl.find_opt data elt_id with
187+
| Some elt -> Ok elt
188+
| None -> Error "Entity not found with the provided id."
189+
;;
190+
end
191+
192+
module User = struct
193+
type id = int
194+
195+
type t =
196+
{ id : int
197+
; name : string
198+
; email : string
199+
}
200+
201+
let get_id user = user.id
202+
end
203+
204+
module Book = struct
205+
type id = string
206+
207+
type t =
208+
{ title : string
209+
; author : string
210+
; mutable stock : int
211+
}
212+
213+
let get_id book = book.title
214+
end
215+
216+
module Lending = struct
217+
type id = User.id * Book.id
218+
219+
type t =
220+
{ user_id : User.id
221+
; book_id : Book.id
222+
; return_date : string
223+
}
224+
225+
let get_id lending = lending.user_id, lending.book_id
226+
end
227+
228+
module SRPCompliantLibrary = struct
229+
(* Ideally, we should be able to use dependency injection here and while
230+
technically we can by using functors, the syntax isn't very intuitive
231+
and extension-friendly so I'll stick with this code for now. *)
232+
module Users = Repository (User)
233+
module Books = Repository (Book)
234+
module Lendings = Repository (Lending)
235+
236+
let borrow user_id book_id =
237+
(* Given I'm not using a proper ORM or writing relationship-aware database
238+
code, I need to make sure both entities (user and book) exist before
239+
adding the lending entity, otherwise I'd be violating what in the DB
240+
world is called a "foreign key" constraint. *)
241+
let* _user = Users.get_by_id user_id in
242+
let* book = Books.get_by_id book_id in
243+
let return_date =
244+
Core.Date.(
245+
add_days (today ~zone:Core.Time_float.Zone.utc) 7 |> to_string_american)
246+
in
247+
if book.stock > 0
248+
then begin
249+
book.stock <- book.stock - 1;
250+
Lendings.add { user_id; book_id; return_date }
251+
end
252+
else Error "Can't lend book, not enough stock"
253+
;;
254+
255+
let return user_id book_id =
256+
let* _user = Users.get_by_id user_id in
257+
let* book = Books.get_by_id book_id in
258+
let* _ = Lendings.delete_by_id (user_id, book_id) in
259+
Ok (book.stock <- book.stock + 1)
260+
;;
261+
end
262+
263+
let _ =
264+
let open SRPCompliantLibrary in
265+
let inventory : Book.t list =
266+
[ { title = "Blood Meridian"; author = "John McCarthy"; stock = 5 }
267+
; { title = "The Outsider"; author = "Stephen King"; stock = 2 }
268+
; { title = "The Philosopher's Stone"; author = "J.K Rowling"; stock = 0 }
269+
]
270+
in
271+
List.iter (fun book -> Books.add book |> Result.get_ok) inventory;
272+
Users.add { id = 1; name = "Luis Lopez"; email = "[email protected]" }
273+
|> Result.get_ok;
274+
let res =
275+
let user_id = 1 in
276+
let book_title = "The Outsider" in
277+
let* _ = borrow user_id book_title in
278+
printf "User #%d successfully borrowed '%s'\n" user_id book_title;
279+
let* _ = return user_id book_title in
280+
printf "User #%d successfully returned '%s'\n" user_id book_title;
281+
Ok ()
282+
in
283+
match res with
284+
| Ok _ ->
285+
print_endline "Let's try borrowing a book with no stock!";
286+
borrow 1 "The Philosopher's Stone" |> Result.get_error |> print_endline
287+
| Error err -> print_endline err
288+
;;

0 commit comments

Comments
 (0)