Laravel中的核心概念

导言

Laravel是一款先进的现代化框架,里面有一些概念非常重要。在上手Laravel之前,我认为先弄懂这些概念是很有必要的。你甚至需要重温下PHP OOP知识。我相信很多人对比如getter setter以及invoke、call、_callStatic这些魔术方法甚至this、
self、static这些关键字作用都还是很模糊的(我上一个老大喜欢问这种基础问题,然后答不上来-
-‘)。

DI & IoC

首先名词解释,DI全称是Dependency injection,依赖注入的意思。而IoC是Inversion of control 控制反转。

要了解依赖注入和控制反转,首先我们不得不提到面向对象设计中的五大设计原则:S.O.L.I.D。

S.O.L.I.D - 面向对象五大设计原则

SRP The Single Responsibility Principle 单一责任原则
OCP The Open Closed Principle 开放封闭原则
LSP The Liskov Substitution Principle 里氏替换原则
ISP The Interface Segregation Principle 接口分离原则
DIP The Dependency Inversion Principle 依赖倒置原则

这五种思想原则对我们平常的软件开发设计非常重要,大家可以具体去了解下。

依赖倒置原则

这里我们重点讲下依赖倒置原则:实体必须依靠抽象而不是具体实现。它表示高层次的模块不应该依赖于低层次的模块,它们都应该依赖于抽象。

在传统软件设计中,我们一般都是上层代码依赖下层代码,当下层代码变动时,我们上层代码要跟着变动,维护成本比较高。这时我们可以上层定义接口,下层来实现这个接口,从而使得下层依赖于上层,降低耦合度。(PC主板和鼠标键盘接口就是一个很好的例子,各数据厂商根据主板上的接口来生产自己的鼠标键盘产品,这样鼠标坏了后我们可以随便换个符合接口要求的鼠标,而不用修改主板上的什么东西)

控制反转

上面讲的依赖倒置是一种原则,而控制反转就是实现依赖倒置的一种具体方法。控制反转核心是把上层(类)所依赖单元的实例化过程交由第三方实现,而类中不允许存在对所依赖单元的实例化语句。举个例子:

1
2
3
4
5
6
7
8
9
10
11

class Comment
{
...

public function afterInsert()
{
$notification = new EmailNotification(...);
$notification->send(...);
}
}

如上,假如我们在用户提交评论后通知被评论者,这里通知方式是邮件,而且是直接在类中实例化邮件通知类,这样代码耦合度高,如果换个短信通知方式就不得不改这里面代码,具体好的实现我们下面会讲到。

依赖注入

依赖注入是一种设计模式,是一种IoC的具体实现,实现了IoC自然就符合依赖倒置原则。依赖注入的核心思想是把类中所依赖单元的实例化过程放到类外面中去实现,然后把依赖注入进来。常用的依赖注入方式有属性注入和构造函数注入。比如用构造函数注入解耦上面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

// 通知接口
interface Notifaction
{
public function send(...);
}


// 短信通知实现通知接口
class SmsNotification implements Notification
{
public function send(...)
{
...
}
}

// 评论类
class Comment
{
...

protected $notification;

public function __construct(Notification $smsNotification)
{
$this->notification = $smsNotification;
}

public function afterInsert()
{
$this->notification->send(...);
}
}

// 实例化短信通知类
$smsNotification = new SmsNotification(...);

// 通过构造函数方法注入
$comment = new Comment($smsNotification);

...

$comment->save();

这样,我们先定义Notification接口,里面有个send方法,让后面的通知者不管是邮件类还是短信类都实现这个接口,然后在外面通过构造函数方式注入进来,这样就解决了Comment类对具体通知方法的依赖,只要是实现了Notification接口的,都可以通过构造函数传进来,Comment类完全不用做任何修改。这样无论对于代码维护还是单元测试(可以模拟实现一个Notification类),都非常方便。

依赖注入是IoC的一种具体实现,是一种解耦手段。当然IoC不止这一种实现,比如Yii中的Service Locator(服务定位器)

IoC container/DI container

当项目比较大时,就会有许多类似上面评论类和通知类这种依赖关系,整个项目会非常复杂。这时候就需要一个集中的地方来管理这些依赖,我们把它叫IoC container 控制反转容器,它提供了动态地创建、注入依赖单元、映射依赖关系等功能。这样可以集中管理依赖关系,也减少很多代码量。

Service container 服务容器

Laravel官方文档这样定义服务容器:Laravel服务容器是用于管理类的依赖和执行依赖注入的工具。

首先,服务容器通过DI依赖注入方式实现了IoC,然后它还支持另一种实现:绑定与解析。

绑定

几乎所有服务容器绑定操作都是Service provider(服务提供器)中注册绑定的,服务提供器中可以通过$this->app方式获取服务容器,然后通过服务容器提供的方法比如$this->app->bind(…)等进行具体服务绑定。类似支持的绑定方式还有:

  • 简单绑定
  • 绑定单例
  • 绑定实例
  • 绑定初始数据
  • 绑定接口到实现
  • 上下文绑定
  • 标记
  • 扩展绑定

具体可以查看官方文档:https://laravel.com/docs/5.6/container

解析

绑定后可以从服务容器中解析出对象才能够使用。解析方法包括:

  • 通过 make 方法,接收一个你想要解析的类或者接口
  • 通过数组方式从容器中解析对象
  • 自动注入
示例

我们先定义一个自己的类

1
2
3
4
5
6
7
8

class Foo
{
public function bar()
{
...
}
}

我们把Foo类简单绑定到服务容器:

1
2
3
4

App::bind("foo", function($app){
return new Foo();
})

平时在上下文获取这个实例:

1
2

$foo = App::make("foo"); // $foo就是Foo类的实例

当然,这种绑定和解析平时我们在代码中随便可以写到哪里,但是多了的话就乱起来了。所以我开头说几乎所有这种依赖服务绑定操作都是在Service provider中进行的。

下面就给大家介绍Service provider。

Service provider 服务提供器

为了让依赖注入的代码不至于混乱,Laravel提供了一个服务提供器(Service Provider),它将这些依赖聚集在了一块,统一申明和管理,让依赖变得更加容易维护。

下面都是一些抄来的官话、套话(-_-‘),大家可以直接跳到代码示例,后续再查看官方文档加深理解。

所有服务提供者都需要继承Illuminate\Support\ServiceProvider类。大多数服务提供者都包含 register 和 boot 方法。register方法中,只能将事务绑定到服务容器。不应该在register方法中尝试注册任何事件监听器,路由或者任何其他功能。可以为服务提供者的boot方法设置类型提示。服务容器会自动注入需要的任何依赖。boot方法将在所有其他服务提供者均已注册之后调用。

所有服务提供者都在 config/app.php 配置文件中注册。可以选择推迟服务提供者的注册,直到真正需要注册绑定时,这样可以提供应用程序的性能。

示例

上一个示例我们是自己在上下文中随意定义、获取。下面我们以服务提供者的方式进行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

use Illuminate\Support\ServiceProvider;

class FooServiceProvider extends ServiceProvider {

public function register()
{
$this->app->bind('foo', function()
{
return new Foo();
});
}

}

上面实现了一个Foo的服务提供,我们可以手动注入到上下文中:

1
2

App::register('FooServiceProvider');

当然我们更多的是通过配置文件来完成的,在app/config/app.php中的providers数组里面增加一行:

1
2
3
4
5

'providers' => [

‘FooServiceProvider’,
],

这样我们可以在上下文中直接获取实例:

1
2

App::make(‘foo’)

当然,我们还可以通过门面方式,更方便的操作Foo类。

Facades 门面

门面实际上是应用了设计模式中的外观模式:

外观模式(Facade),他隐藏了系统的复杂性,并向客户端提供了一个可以访问系统的接口。这种类型的设计模式属于结构性模式。为子系统中的一组接口提供了一个统一的访问接口,这个接口使得子系统更容易被访问或者使用。

Laravel中随处可见这些静态方法的调用:

1
2

$value = Cache::get('key');

这些静态调用实际上调用的并不是静态方法,而是通过PHP的魔术方法 __callStatic() 将请求转到了相应的方法上。

比如如果我们看一下 Illuminate\Support\Facades\Cache 这个类,你会发现类中根本没有 get 这个静态方法:

1
2
3
4
5
6
7
8
9
10

class Cache extends Facade
{
/**
* 获取组件的注册名称。
*
* @return string
*/
protected static function getFacadeAccessor() { return 'cache'; }
}

这其中的奥秘在基类Facade中:

1
2
3
4
5
6
7
8
9
10
11
12
13

public static function __callStatic($method, $args)
{
// 获取实例
$instance = static::getFacadeRoot();

if (!$instance) {
throw new RuntimeException('A facade root has not been set.');
}

// 真正调取对应的方法
return $instance->$method(...$args);
}

这里面有一个获取实例的过程,然后去调用具体方法。

示例

接上一个示例,我们平常是通过App::make(‘foo’)来获取实例,然后再调用具体方法。现在我们通过门面的方式简化这个流程:

先定义一个门面:

1
2
3
4
5
6
7
8

use Illuminate\Support\Facades\Facade;

class Foo extends Facade {

protected static function getFacadeAccessor() { return ‘foo’; }

}

然后我们可以很方便的使用Foo类某个方法:

1
2

Foo::bar();

Contracts 契约

Laravel的契约是一组定义框架提供的核心服务的接口。后续针对这个接口可以有多种实现,解耦了具体实现的依赖,在不改变代码逻辑的情况下获得更加多态的结果。

比如你只需在配置文件中指明你需要的缓存驱动(redis,memcached,file等),Laravel会自动帮你切换到这种驱动,而不需要你针对某种驱动更改逻辑和代码。

总结

这些都是些基础的抽象概念,但是是非常重要的,Laravel中随处可见这些思想,是一切实现的基石。

学习的过程中基础是非常重要的,知其然必知其所以然。就像道与术,道是在术之前的,老子说过:”有道无术,术尚可求也,有术无道,止于术“。不过实际中应该是相辅相成的关系,“以道统术,以术得道”。

引用

https://laravel.com/docs/5.6/container
https://laravel-china.org/docs/laravel/5.6
http://www.digpage.com/di.html
http://yansu.org/2014/12/06/ioc-and-facade-in-laravel.html

Share Comments

用Golang撸了个小工具

缘起

我们公司开发环境很特殊,一台本地服务器,然后分配多个ssh账户给开发者。平时上传代码只能ftp/sftp连接上传(以前用过samba共享,但被关了。。)。所以我们平时是在Phpstorm上用sftp远程打开服务器上的项目,然后设置自动上传。这样一般工作没问题。但是有个坑:Phpstorm无法捕获类似git checkout这些更改文件的变化。。所以也就无法让本地代码跟服务器保持一致了。所以也就诞生了想写个这个同步机制的念头,这种场景Golang很适合。于是就开始撸起来。。

项目地址:https://github.com/rootrl/Mancy

实现

大致思路是监测一个文件夹的变化(本地代码库),如果有变化就通过sftp上传到服务器上。

监测文件变化用的是golang的fsnotify package,它提供的监测变化类型如下:

1
2
3
4
5
6
7
8
const (
FSN_CREATE = 1
FSN_MODIFY = 2
FSN_DELETE = 4
FSN_RENAME = 8

FSN_ALL = FSN_MODIFY | FSN_DELETE | FSN_RENAME | FSN_CREATE
)

但是fsnotify有个坑就是只能监测一层文件夹的变化,多层文件夹需要自己遍历挂载事件。后续新建文件夹,重命名这种也要手动加事件。

其中每个事件都对应一个处理通道,我的想法是让文件处理者和事件解耦,因为后面不一定是sftp来处理上传,也可能是rsync,也可能其他处理方式。所以后续处理者只用监听对应事件通道,处理者这里我用了golang的select实现了一个超时机制,有事件就处理,无事件有个几秒的等待时间。

fsnotify这块代码见: https://github.com/rootrl/Mancy/blob/master/file_watcher.go
sftp hanlder见: https://github.com/rootrl/Mancy/blob/master/file_sftp_handler.go

sftp用的是github.com/pkg/sftp这个库,用起来还是挺顺手,但都是写底层的api,所以我单独封装了个sftp_util: https://github.com/rootrl/Mancy/blob/master/sftp_util.go 有一些常见的上传文件/文件夹,删除文件/文件夹等操作

以上基本能实现主要功能了,然后我还定义了个配置文件结构,通过对应Json字符可以把字段自动映射到这个结构上,供后续使用。这也是golang json包的方便之处。

总结

写这个项目主要是用来练手golang的,刚开始阶段,代码可能写得有点垃圾。。比如sftpClent客户端这些目前是用全局变量实现的,能用,但是不够优雅。。后续慢慢改进。。(可能永远不会。。)

总之,Golang还是挺不错的!

Share Comments

Javascript原型链、作用域、闭包学习笔记

数据类型

  • 值类型 boolean undefined string number
  • 引用类型 Object null function array

类型判断

值类型 typeof
引用类型 instanceof

1
2
3
4
5
6
7
8
9
10
11
12
var fn = function(){}
console.log(fn instanceof Object) // function

var foo = "hello"
console.log(typeof foo) // string

console.log(typeof null) // object

但是:
null instanceof Object === false

// null 为object是javascript遗留的一个bug

对象

Tip:一切引用类型都是对象,对象是属性的集合

对象是通过函数创建的:

1
var object = {a: 1, b:1} // 语法糖

真实情况:

1
2
3
4
5
var object = new Object();
object.a = 1;
object.b = 1;

console.log(typof Object) // function

原型 prototype

函数是一种对象,每个函数都有一个默认的属性:prototype
这个prototype的值是一个对象(属性的集合), 默认只有一个constructor的属性,指向这个函数本身

1
2
3
4
var Fn = function(){}
Fn.prototype.name = "hello"
Fn.prototype.foo = function(){}
var fn = new Fn()

Fn是一个函数,fn是通过Fn new出来的,这样fn对象就可以访问Fn.prototype中的属性
(问:不new就访问不了?是,但是为什么要new?是不是相当于类和类的实例)

__proto__是隐式属性,这个属性引用了创建这个对象的函数的prototype, 即:

1
fn.__proto__ === Fn.prototype

每个函数都有一个prototype原型,每个对象都有一个__proto__隐式原型
(javascript 不希望开发者用到这个proto)

每个对象都有一个__proto__属性,指向创建该对象的函数的prototype

Object.prototype 的__proto__指向null

1
var o = new Foo();

JavaScript 实际上执行的是:

1
2
3
4
5
6
7
var o = new Object();
o.__proto__ = Foo.prototype;
Foo.call(o);

function Foo(){}
var fn = new Foo
console.log(fn instanceof Foo) // true

instanceof

Instanceof 查找规则
Instanceof运算符的第一个变量是一个对象,暂时称为A;第二个变量一般是一个函数,暂时称为B。

Instanceof的判断规则是:沿着A的__proto__这条线来找,同时沿着B的prototype这条线来找,如果两条线能找到同一个引用,即同一个对象,那么就返回true。如果找到终点还未重合,则返回false

Instanceof 表达的是一种继承关系,或者原型链的结构

1
2
3
4
5
6
7
8
9
10
11
12

function Fn(){}

var fn = new Fn()

fn.a = 10

Fn.prototype.a = 100
Fn.prototype.b = 200

console.log(fn.a)
console.log(fn.b)

fn是通过Fn new过来的,a是fn的基本属性,b从Fn.prototype得来,因为fn.__proto__指向是Foo.prototype

原型链

访问一个对象属性时,先从基本属性上查找,如果没有再沿着__proto__这条链往上找,这就是原型链

判断一个属性是基本属性还是从原型链上查找的,用hasOwnProperty

对象的方法都可以通过prototype扩展

上下文环境

Javascript准备工作:

变量、函数表达式 => 变量的声明,默认赋值为undefined (后期在执行过程中赋值)

this => 赋值 this无论哪个情况都有值,至于值是什么,跟上下文环境有关

函数声明 => 赋值

函数体的执行上下文需要附加
参数 =》 赋值
arguments => 赋值
自由变量的取值作用域 => 赋值

Javascript 在执行一段代码前都会进行一些准备工作来生成执行上下文,这个代码段分三种情况:全局环境,函数体,eval

代码段就是一段文本形式的代码

1
2
3
4
5
6
7
8
9
10
// 全局:
<script>
alert('123')
</script>

// eval:
eval("alert('123')")

// function :
var fn =new Function("x", "console.log(x)")

函数每被调用一次,都会产生一个新的执行上下文环境
函数在定义的时候(不是调用的时候),就已经确定了函数内部自由变量的作用域

自由变量:
在A作用域中使用的变量x,却没有在A作用域中声明(其他作用域中声明的),对于A作用域来说,x就是一个自由变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
var a = 10

function fn() {
console.log(a) // 是自由变量 函数创建时就确定了a要取值的作用域
// 下级作用域可以获取上级作用域的变量,反之不行
}

function bar(f) {
var a = 20
f() // 10 而不是20
}

bar(fn)

执行上下文环境通俗定义:在执行代码之前,把将要用到的所有的变量都事先拿出来,有的直接赋值,有的先用undefined占个空

js在代码执行过程中,会有数不清的函数调用,每个函数调用会产生多个上下文环境,这么多的上下文环境需要管理,销毁而释放内存(上下文环境执行栈)

this

this在函数中到底取何值,是在函数真正被调用的时候确定的,函数定义的时候确定不了,因为this的取值是执行上下文环境的一部分,每次调用函数,都会产生一个新的执行上下文环境。

this取值一共分四种情况:

构造函数
构造函数是用来new对象的函数,第一个字母大写

1
2
3
4
5
6
7
function Foo() {
this.name = "test"
console.log(this)
}

var fn = new Foo()
fn.name // test

以上this为即将new出来的对象的

但是如果直接把Foo作为函数调用,this就是window

函数作为对象的一个属性
如果函数作为对象的一个属性时,并且作为对象的一个属性被调用时,函数的this指向该对象。

1
2
3
4
5
6
7
8
var obj = {
x: 10,
fn: function() {
console.log(this, this.x)
}
}

ojb.fn() // this指向obj对象

如果不是作为obj被调用

1
2
3
var fn1 = obj.fn

fn1()

那么this就是指向window,this.x为undefined

函数使用call apply调用

当用call apply调用时,this指向被传入的对象

1
2
3
4
5
6
7
8
9
10
var obj = {
x: 10
}

var fn = function() {
console.log(this)
console.log(this.x)
}

fn.call(obj)

全局&调用普通函数
console.log(this === window) true

普通函数在调用时,其中的this也都是window

执行上下文栈

1
2
3
4
执行上下文栈
压栈 出栈(销毁)
全局上下文环境 =》 函数上下文环境 =》 全局上下文环境
全局上下文环境

代码段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var a = 10,
fn,
bar = function(x) {
var b = 5
fn(x + b) // 进入fn全局上下文环境
}


fn = function(y) {
var c = 5
console.log(y+c)
}

bar(10)// 进入bar上下文环境

在执行代码之前将创建全局上下文环境

作用域

Js没有块级作用域,就是{}中间的语句 例如if for这些
所以要一开始声明变量 避免歧义

JS除了全局作用域外,只有函数可以创建的作用域

作用域在函数定义的时候就已经确定,而不是调用的时候。(同一个作用域有不同的上下文执行环境)(this取值是函数调用的时候确定的)

如果要查找一个作用域下某个变量的值,就需要找到这个作用域下对应的执行上下文环境,再在其中寻找变量的值

自由变量的取值要到创建这个变量的环境去取,而不是调用(静态作用域)。

如果在全局没找到,则未定义。如果是函数作用域,则去创建该函数的作用域去找

闭包

两种应用情况:

1,函数作为返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function fn(){
// 这里当执行完fn的时候,按道理讲应该销毁fn上下文执行环境,但是fn的返回值是一个函数(函数可以创建一个独立的作用域),而这个函数中也引用了max变量,所以max不能被销毁,销毁之后bar函数就找不到max的值了
var max = 10

return function(x) {
if (x > max) {
console.log(x)
}
}
}

var f1 = fn(),
max = 100

f1(15) // 15 这里取值15证明fn的上下文环境没有被销毁,否则按照作用域链来说,max可以取到全局作用域中的值100

2, 函数作为参数

1
2
3
4
5
6
7
8
9
10
11
12
13
var max = 10;

var fn = function (x){
if (x > max) {
console.log(x)
}
}

(function(f){
var max = 100
f(15)
})(fn)
// max取值为10 (去创建这个函数的作用域取值)

普通函数别调用完之后,其执行上下文环境将被销毁,其中的变量也会被同时销毁。但是在闭包中,其上下文执行环境不会被销毁

作用域和上下文执行环境:
作用域在函数定义的时候就已经确定,一个函数只有一个作用域,但是随着被调用的不同,会有多种上下文环境

参考学习

学习自:http://www.cnblogs.com/wangfupeng1988/p/3977924.html

注:这个非常不错,墙裂推荐学习

Share Comments