Làm thế nào để mô hình hóa ví dụ này
Làm thế nào điều này có thể được mô hình hóa với đơn nguyên Reader?
Tôi không chắc liệu điều này có nên được lập mô hình với Reader hay không, nhưng nó có thể được thực hiện bằng cách:
- mã hóa các lớp dưới dạng các hàm làm cho mã chơi đẹp hơn với Reader
- soạn các chức năng với Reader để hiểu và sử dụng nó
Ngay trước khi bắt đầu, tôi cần cho bạn biết về những điều chỉnh mã mẫu nhỏ mà tôi cảm thấy có lợi cho câu trả lời này. Thay đổi đầu tiên là về FindUsers.inactive
phương pháp. Tôi để nó trả về List[String]
để danh sách các địa chỉ có thể được sử dụng trong UserReminder.emailInactive
phương thức. Tôi cũng đã thêm các triển khai đơn giản vào các phương thức. Cuối cùng, mẫu sẽ sử dụng phiên bản Reader monad được cuộn thủ công sau:
case class Reader[Conf, T](read: Conf => T) { self =>
def map[U](convert: T => U): Reader[Conf, U] =
Reader(self.read andThen convert)
def flatMap[V](toReader: T => Reader[Conf, V]): Reader[Conf, V] =
Reader[Conf, V](conf => toReader(self.read(conf)).read(conf))
def local[BiggerConf](extractFrom: BiggerConf => Conf): Reader[BiggerConf, T] =
Reader[BiggerConf, T](extractFrom andThen self.read)
}
object Reader {
def pure[C, A](a: A): Reader[C, A] =
Reader(_ => a)
implicit def funToReader[Conf, A](read: Conf => A): Reader[Conf, A] =
Reader(read)
}
Bước mô hình hóa 1. Mã hóa các lớp dưới dạng hàm
Có lẽ đó là tùy chọn, tôi không chắc, nhưng sau này nó làm cho việc hiểu rõ hơn. Lưu ý, chức năng kết quả đó bị hạn chế. Nó cũng lấy (các) đối số của hàm tạo trước đây làm tham số đầu tiên của chúng (danh sách tham số). Đường đó
class Foo(dep: Dep) {
def bar(arg: Arg): Res = ???
}
trở thành
object Foo {
def bar: Dep => Arg => Res = ???
}
Hãy ghi nhớ rằng mỗi người Dep
, Arg
, Res
loại hoàn toàn có thể tùy ý: một tuple, một chức năng hoặc một loại đơn giản.
Đây là mã mẫu sau khi điều chỉnh ban đầu, được chuyển đổi thành các hàm:
trait Datastore { def runQuery(query: String): List[String] }
trait EmailServer { def sendEmail(to: String, content: String): Unit }
object FindUsers {
def inactive: Datastore => () => List[String] =
dataStore => () => dataStore.runQuery("select inactive")
}
object UserReminder {
def emailInactive(inactive: () => List[String]): EmailServer => () => Unit =
emailServer => () => inactive().foreach(emailServer.sendEmail(_, "We miss you"))
}
object CustomerRelations {
def retainUsers(emailInactive: () => Unit): () => Unit =
() => {
println("emailing inactive users")
emailInactive()
}
}
Một điều cần lưu ý ở đây là các chức năng cụ thể không phụ thuộc vào toàn bộ đối tượng, mà chỉ phụ thuộc vào các bộ phận được sử dụng trực tiếp. Trường hợp trong phiên bản OOP UserReminder.emailInactive()
sẽ gọi userFinder.inactive()
ở đây nó chỉ gọi inactive()
- một hàm được truyền cho nó trong tham số đầu tiên.
Xin lưu ý rằng mã hiển thị ba thuộc tính mong muốn từ câu hỏi:
- rõ ràng mỗi chức năng cần loại phụ thuộc nào
- ẩn sự phụ thuộc của một chức năng này với một chức năng khác
retainUsers
phương thức không cần biết về sự phụ thuộc vào Kho dữ liệu
Bước lập mô hình 2. Sử dụng Trình đọc để soạn các hàm và chạy chúng
Đơn nguyên trình đọc cho phép bạn chỉ soạn các hàm phụ thuộc vào cùng một loại. Đây thường không phải là một trường hợp. Trong ví dụ của chúng tôi
FindUsers.inactive
phụ thuộc vào Datastore
và UserReminder.emailInactive
vào EmailServer
. Để giải quyết vấn đề đó, người ta có thể giới thiệu một kiểu mới (thường được gọi là Cấu hình) chứa tất cả các phụ thuộc, sau đó thay đổi các chức năng để tất cả chúng phụ thuộc vào nó và chỉ lấy từ nó dữ liệu có liên quan. Điều đó rõ ràng là sai từ quan điểm quản lý phụ thuộc bởi vì theo cách đó bạn làm cho các chức năng này cũng phụ thuộc vào các loại mà ngay từ đầu chúng không nên biết.
May mắn thay, hóa ra có một cách để làm cho hàm hoạt động Config
ngay cả khi nó chỉ chấp nhận một số phần của nó làm tham số. Đó là một phương thức được gọi local
, được định nghĩa trong Reader. Nó cần được cung cấp một cách để trích xuất phần có liên quan từ Config
.
Kiến thức này được áp dụng cho ví dụ dưới đây sẽ trông giống như sau:
object Main extends App {
case class Config(dataStore: Datastore, emailServer: EmailServer)
val config = Config(
new Datastore { def runQuery(query: String) = List("john.doe@fizzbuzz.com") },
new EmailServer { def sendEmail(to: String, content: String) = println(s"sending [$content] to $to") }
)
import Reader._
val reader = for {
getAddresses <- FindUsers.inactive.local[Config](_.dataStore)
emailInactive <- UserReminder.emailInactive(getAddresses).local[Config](_.emailServer)
retainUsers <- pure(CustomerRelations.retainUsers(emailInactive))
} yield retainUsers
reader.read(config)()
}
Ưu điểm khi sử dụng các tham số của hàm tạo
Ở những khía cạnh nào việc sử dụng Reader Monad cho một "ứng dụng kinh doanh" như vậy sẽ tốt hơn so với việc chỉ sử dụng các tham số của hàm tạo?
Tôi hy vọng rằng bằng cách chuẩn bị câu trả lời này, tôi sẽ dễ dàng tự đánh giá hơn về những khía cạnh nào mà nó sẽ đánh bại các nhà xây dựng đơn giản. Tuy nhiên, nếu tôi phải liệt kê những thứ này, đây là danh sách của tôi. Tuyên bố từ chối trách nhiệm: Tôi có nền tảng OOP và tôi có thể không đánh giá cao Reader và Kleisli hoàn toàn vì tôi không sử dụng chúng.
- Tính đồng nhất - không có vấn đề gì về độ ngắn / dài để hiểu, nó chỉ là một Trình đọc và bạn có thể dễ dàng soạn nó với một phiên bản khác, có lẽ chỉ giới thiệu thêm một loại Cấu hình và rắc một số
local
lệnh lên trên nó. Điểm này là IMO đúng hơn là một vấn đề của sở thích, bởi vì khi bạn sử dụng các hàm tạo, không ai ngăn cản bạn soạn bất cứ thứ gì bạn thích, trừ khi ai đó làm điều gì đó ngu ngốc, như làm công việc trong hàm tạo được coi là một hành vi xấu trong OOP.
- Reader là một đơn nguyên, vì vậy nó nhận được tất cả các lợi ích liên quan đến điều đó -
sequence
, traverse
các phương pháp được triển khai miễn phí.
- Trong một số trường hợp, bạn có thể thấy chỉ nên xây dựng Trình đọc một lần và sử dụng nó cho nhiều loại Cấu hình. Với các hàm tạo không ai ngăn cản bạn làm điều đó, bạn chỉ cần xây dựng lại toàn bộ đồ thị đối tượng cho mỗi Cấu hình đến. Mặc dù tôi không gặp vấn đề gì với điều đó (tôi thậm chí thích làm điều đó với mọi yêu cầu nộp đơn), nhưng đó không phải là một ý tưởng rõ ràng đối với nhiều người vì những lý do tôi chỉ có thể suy đoán.
- Reader thúc đẩy bạn sử dụng các chức năng nhiều hơn, điều này sẽ chơi tốt hơn với ứng dụng được viết theo phong cách FP chủ yếu.
- Người đọc phân tách mối quan tâm; bạn có thể tạo, tương tác với mọi thứ, xác định logic mà không cần cung cấp phụ thuộc. Thực tế cung cấp sau này, riêng biệt. (Cảm ơn Ken Scrambler về điểm này). Đây là ưu điểm thường thấy của Reader, nhưng điều đó cũng có thể xảy ra với các hàm tạo đơn giản.
Tôi cũng muốn nói những điều tôi không thích trong Reader.
- Tiếp thị. Đôi khi tôi nhận được ấn tượng rằng Reader được tiếp thị cho tất cả các loại phụ thuộc, không phân biệt đó là cookie phiên hay cơ sở dữ liệu. Đối với tôi, có rất ít ý nghĩa khi sử dụng Reader cho các đối tượng thực tế không đổi, như máy chủ email hoặc kho lưu trữ từ ví dụ này. Đối với những phụ thuộc như vậy, tôi thấy các hàm tạo đơn giản và / hoặc các hàm được áp dụng một phần tốt hơn. Về cơ bản, Reader cung cấp cho bạn sự linh hoạt để bạn có thể chỉ định phụ thuộc của mình trong mỗi cuộc gọi, nhưng nếu bạn không thực sự cần điều đó, bạn chỉ phải trả thuế của nó.
- Độ nặng tiềm ẩn - sử dụng Reader mà không có ẩn ý sẽ khiến ví dụ khó đọc. Mặt khác, khi bạn ẩn các phần ồn ào bằng cách sử dụng hàm ý và mắc một số lỗi, trình biên dịch đôi khi sẽ cung cấp cho bạn các thông báo khó giải mã.
- Lễ với
pure
, local
và tạo ra các lớp học Config riêng / sử dụng các bộ cho điều đó. Reader buộc bạn phải thêm một số mã không liên quan đến miền có vấn đề, do đó sẽ tạo ra một số nhiễu trong mã. Mặt khác, một ứng dụng sử dụng hàm tạo thường sử dụng mô hình nhà máy, cũng là từ bên ngoài miền vấn đề, vì vậy điểm yếu này không nghiêm trọng lắm.
Điều gì sẽ xảy ra nếu tôi không muốn chuyển đổi các lớp của mình thành các đối tượng có hàm?
Bạn muốn. Về mặt kỹ thuật, bạn có thể tránh điều đó, nhưng hãy nhìn xem điều gì sẽ xảy ra nếu tôi không chuyển đổi FindUsers
lớp thành đối tượng. Dòng tương ứng để hiểu sẽ giống như sau:
getAddresses <- ((ds: Datastore) => new FindUsers(ds).inactive _).local[Config](_.dataStore)
cái nào không đọc được phải không? Vấn đề là Reader hoạt động trên các chức năng, vì vậy nếu bạn chưa có chúng, bạn cần phải xây dựng chúng nội tuyến, điều này thường không đẹp cho lắm.